mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-08-30 23:00:06 -04:00
Revert "More performance enhancements, polishing"
This reverts commit 6ca8c1fd8e165f40780e7e2eb11e49d0343c644e.
This commit is contained in:
parent
6ca8c1fd8e
commit
334e636cf2
@ -1,392 +0,0 @@
|
|||||||
import { Injectable, inject, DestroyRef } from '@angular/core';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { BehaviorSubject, fromEvent, throttleTime, map } from 'rxjs';
|
|
||||||
|
|
||||||
// Types for better type safety
|
|
||||||
interface GradientPoint {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
vx: number;
|
|
||||||
vy: number;
|
|
||||||
color: { r: number; g: number; b: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AnimationConfig {
|
|
||||||
gradientRadius: number;
|
|
||||||
boundsMargin: number;
|
|
||||||
velocityRange: { min: number; max: number };
|
|
||||||
colors: { r: number; g: number; b: number }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class GradientAnimationService {
|
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
|
||||||
|
|
||||||
// Animation state
|
|
||||||
private animationId?: number;
|
|
||||||
private gradientPoints: GradientPoint[] = [];
|
|
||||||
private canvas?: HTMLCanvasElement;
|
|
||||||
private context?: CanvasRenderingContext2D;
|
|
||||||
private isRunning = false;
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
private readonly defaultConfig: AnimationConfig = {
|
|
||||||
gradientRadius: 0.5,
|
|
||||||
boundsMargin: 0.1,
|
|
||||||
velocityRange: { min: 0.001, max: 0.0018 },
|
|
||||||
colors: [
|
|
||||||
{ r: 73, g: 197, b: 147 }, // Default gradient color 1
|
|
||||||
{ r: 138, g: 43, b: 226 }, // Default gradient color 2
|
|
||||||
{ r: 255, g: 215, b: 0 }, // Default gradient color 3
|
|
||||||
{ r: 255, g: 20, b: 147 } // Default gradient color 4
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Observables for reactive updates
|
|
||||||
private readonly isAnimating$ = new BehaviorSubject<boolean>(false);
|
|
||||||
private readonly frameRate$ = new BehaviorSubject<number>(0);
|
|
||||||
|
|
||||||
// Public API
|
|
||||||
public readonly animationState$ = this.isAnimating$.asObservable();
|
|
||||||
public readonly currentFrameRate$ = this.frameRate$.asObservable();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the gradient animation system
|
|
||||||
*/
|
|
||||||
public initialize(canvas: HTMLCanvasElement, config?: Partial<AnimationConfig>): boolean {
|
|
||||||
try {
|
|
||||||
this.canvas = canvas;
|
|
||||||
this.context = this.setupCanvasContext(canvas);
|
|
||||||
|
|
||||||
if (!this.context) {
|
|
||||||
console.warn('Failed to get canvas context for gradient animation');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalConfig = { ...this.defaultConfig, ...config };
|
|
||||||
this.initializeGradientPoints(finalConfig);
|
|
||||||
this.setupResizeHandling();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize gradient animation:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the animation loop
|
|
||||||
*/
|
|
||||||
public start(): void {
|
|
||||||
if (this.isRunning || !this.context || !this.canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isRunning = true;
|
|
||||||
this.isAnimating$.next(true);
|
|
||||||
this.startAnimationLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the animation loop
|
|
||||||
*/
|
|
||||||
public stop(): void {
|
|
||||||
if (!this.isRunning) return;
|
|
||||||
|
|
||||||
this.isRunning = false;
|
|
||||||
this.isAnimating$.next(false);
|
|
||||||
|
|
||||||
if (this.animationId !== undefined) {
|
|
||||||
cancelAnimationFrame(this.animationId);
|
|
||||||
this.animationId = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update gradient colors from CSS variables
|
|
||||||
*/
|
|
||||||
public updateColorsFromCSS(): void {
|
|
||||||
const colorVariables = [
|
|
||||||
'--gradient-color-1',
|
|
||||||
'--gradient-color-2',
|
|
||||||
'--gradient-color-3',
|
|
||||||
'--gradient-color-4'
|
|
||||||
];
|
|
||||||
|
|
||||||
colorVariables.forEach((variable, index) => {
|
|
||||||
if (this.gradientPoints[index]) {
|
|
||||||
const cssColor = this.getCssColorValue(variable);
|
|
||||||
if (cssColor) {
|
|
||||||
this.gradientPoints[index].color = cssColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose of all resources
|
|
||||||
*/
|
|
||||||
public dispose(): void {
|
|
||||||
this.stop();
|
|
||||||
this.canvas = undefined;
|
|
||||||
this.context = undefined;
|
|
||||||
this.gradientPoints = [];
|
|
||||||
this.isAnimating$.complete();
|
|
||||||
this.frameRate$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup canvas context with optimizations
|
|
||||||
*/
|
|
||||||
private setupCanvasContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D | null {
|
|
||||||
const context = canvas.getContext('2d', {
|
|
||||||
alpha: false,
|
|
||||||
desynchronized: true,
|
|
||||||
colorSpace: 'srgb'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!context) return null;
|
|
||||||
|
|
||||||
// Apply performance optimizations
|
|
||||||
context.imageSmoothingEnabled = true;
|
|
||||||
context.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
// Canvas CSS optimizations
|
|
||||||
canvas.style.imageRendering = 'auto';
|
|
||||||
canvas.style.backfaceVisibility = 'hidden';
|
|
||||||
canvas.style.transform = 'translateZ(0)';
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize gradient points with configuration
|
|
||||||
*/
|
|
||||||
private initializeGradientPoints(config: AnimationConfig): void {
|
|
||||||
const positions = [
|
|
||||||
{ x: 0.2, y: 0.2 },
|
|
||||||
{ x: 0.8, y: 0.3 },
|
|
||||||
{ x: 0.5, y: 0.8 },
|
|
||||||
{ x: 0.3, y: 0.6 }
|
|
||||||
];
|
|
||||||
|
|
||||||
this.gradientPoints = positions.map((pos, index) => ({
|
|
||||||
x: pos.x,
|
|
||||||
y: pos.y,
|
|
||||||
vx: this.randomVelocity(config.velocityRange),
|
|
||||||
vy: this.randomVelocity(config.velocityRange),
|
|
||||||
color: config.colors[index] || config.colors[0]
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate random velocity within range
|
|
||||||
*/
|
|
||||||
private randomVelocity(range: { min: number; max: number }): number {
|
|
||||||
const velocity = range.min + Math.random() * (range.max - range.min);
|
|
||||||
return Math.random() > 0.5 ? velocity : -velocity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup responsive canvas resizing
|
|
||||||
*/
|
|
||||||
private setupResizeHandling(): void {
|
|
||||||
if (!this.canvas) return;
|
|
||||||
|
|
||||||
// Use ResizeObserver for better performance
|
|
||||||
if ('ResizeObserver' in window) {
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.resizeCanvas();
|
|
||||||
});
|
|
||||||
|
|
||||||
resizeObserver.observe(this.canvas);
|
|
||||||
|
|
||||||
// Cleanup observer
|
|
||||||
this.destroyRef.onDestroy(() => {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback to window resize with throttling
|
|
||||||
fromEvent(window, 'resize')
|
|
||||||
.pipe(
|
|
||||||
throttleTime(100),
|
|
||||||
takeUntilDestroyed(this.destroyRef)
|
|
||||||
)
|
|
||||||
.subscribe(() => this.resizeCanvas());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial resize
|
|
||||||
this.resizeCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize canvas with device pixel ratio support
|
|
||||||
*/
|
|
||||||
private resizeCanvas(): void {
|
|
||||||
if (!this.canvas || !this.context) return;
|
|
||||||
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Set actual size in memory
|
|
||||||
this.canvas.width = rect.width * dpr;
|
|
||||||
this.canvas.height = rect.height * dpr;
|
|
||||||
|
|
||||||
// Scale canvas back down using CSS
|
|
||||||
this.canvas.style.width = `${rect.width}px`;
|
|
||||||
this.canvas.style.height = `${rect.height}px`;
|
|
||||||
|
|
||||||
// Scale the drawing context
|
|
||||||
this.context.scale(dpr, dpr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main animation loop with performance monitoring
|
|
||||||
*/
|
|
||||||
private startAnimationLoop(): void {
|
|
||||||
let lastTime = performance.now();
|
|
||||||
let frameCount = 0;
|
|
||||||
let lastFpsUpdate = lastTime;
|
|
||||||
|
|
||||||
const animate = (currentTime: number): void => {
|
|
||||||
if (!this.isRunning || !this.context || !this.canvas) return;
|
|
||||||
|
|
||||||
// Calculate frame rate
|
|
||||||
frameCount++;
|
|
||||||
if (currentTime - lastFpsUpdate >= 1000) {
|
|
||||||
this.frameRate$.next(frameCount);
|
|
||||||
frameCount = 0;
|
|
||||||
lastFpsUpdate = currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render frame
|
|
||||||
this.renderFrame();
|
|
||||||
|
|
||||||
// Schedule next frame
|
|
||||||
this.animationId = requestAnimationFrame(animate);
|
|
||||||
lastTime = currentTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.animationId = requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a single animation frame
|
|
||||||
*/
|
|
||||||
private renderFrame(): void {
|
|
||||||
if (!this.context || !this.canvas) return;
|
|
||||||
|
|
||||||
// Clear canvas with theme background
|
|
||||||
const bgColor = this.getCssVariable('--elevation-layer2-dark-solid') || '#212121';
|
|
||||||
this.context.fillStyle = bgColor;
|
|
||||||
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
|
|
||||||
// Update and render gradient points
|
|
||||||
this.updateGradientPoints();
|
|
||||||
this.renderGradientPoints();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update gradient point positions with boundary collision
|
|
||||||
*/
|
|
||||||
private updateGradientPoints(): void {
|
|
||||||
const margin = this.defaultConfig.boundsMargin;
|
|
||||||
|
|
||||||
for (const point of this.gradientPoints) {
|
|
||||||
// Update position
|
|
||||||
point.x += point.vx;
|
|
||||||
point.y += point.vy;
|
|
||||||
|
|
||||||
// Boundary collision with margin
|
|
||||||
if (point.x <= margin || point.x >= 1 - margin) {
|
|
||||||
point.vx *= -1;
|
|
||||||
}
|
|
||||||
if (point.y <= margin || point.y >= 1 - margin) {
|
|
||||||
point.vy *= -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp to bounds
|
|
||||||
point.x = Math.max(margin, Math.min(1 - margin, point.x));
|
|
||||||
point.y = Math.max(margin, Math.min(1 - margin, point.y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render gradient points with optimized radial gradients
|
|
||||||
*/
|
|
||||||
private renderGradientPoints(): void {
|
|
||||||
if (!this.context || !this.canvas) return;
|
|
||||||
|
|
||||||
const width = this.canvas.width;
|
|
||||||
const height = this.canvas.height;
|
|
||||||
|
|
||||||
for (const point of this.gradientPoints) {
|
|
||||||
const centerX = point.x * width;
|
|
||||||
const centerY = point.y * height;
|
|
||||||
const radius = Math.max(width, height) * this.defaultConfig.gradientRadius;
|
|
||||||
|
|
||||||
const gradient = this.context.createRadialGradient(
|
|
||||||
centerX, centerY, 0,
|
|
||||||
centerX, centerY, radius
|
|
||||||
);
|
|
||||||
|
|
||||||
// Optimized gradient stops
|
|
||||||
const { r, g, b } = point.color;
|
|
||||||
gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.15)`);
|
|
||||||
gradient.addColorStop(0.2, `rgba(${r}, ${g}, ${b}, 0.12)`);
|
|
||||||
gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.08)`);
|
|
||||||
gradient.addColorStop(0.7, `rgba(${r}, ${g}, ${b}, 0.03)`);
|
|
||||||
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
||||||
|
|
||||||
this.context.fillStyle = gradient;
|
|
||||||
this.context.fillRect(0, 0, width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CSS variable value safely
|
|
||||||
*/
|
|
||||||
private getCssVariable(variableName: string): string | null {
|
|
||||||
try {
|
|
||||||
const value = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue(variableName)
|
|
||||||
.trim();
|
|
||||||
return value || null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CSS color as RGB object
|
|
||||||
*/
|
|
||||||
private getCssColorValue(variableName: string): { r: number; g: number; b: number } | null {
|
|
||||||
const cssValue = this.getCssVariable(variableName);
|
|
||||||
if (!cssValue) return null;
|
|
||||||
|
|
||||||
return this.hexToRgb(cssValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert hex to RGB
|
|
||||||
*/
|
|
||||||
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
||||||
hex = hex.replace(/^#/, '').trim();
|
|
||||||
|
|
||||||
if (hex.length === 3) {
|
|
||||||
hex = hex.split('').map(char => char + char).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hex.length !== 6 || !/^[0-9A-Fa-f]{6}$/.test(hex)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
r: parseInt(hex.substring(0, 2), 16),
|
|
||||||
g: parseInt(hex.substring(2, 4), 16),
|
|
||||||
b: parseInt(hex.substring(4, 6), 16)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
<div class="mx-auto container login text-center"
|
|
||||||
[ngStyle]="{'height': (navService.navbarVisible$ | async) ? 'calc(var(--vh, 1vh) * 100 - var(--nav-offset))' : 'calc(var(--vh, 1vh) * 100)'}">
|
|
||||||
<canvas #gradientCanvas id="gradient-canvas" class="gradient-background" aria-hidden="true"></canvas>
|
|
||||||
<div class="gradient-overlay" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
|
<div class="mx-auto container login text-center"
|
||||||
|
[ngStyle]="{'height': (navService.navbarVisible$ | async) ? 'calc(var(--vh, 1vh) * 100 - var(--nav-offset))' : 'calc(var(--vh, 1vh) * 100)'}">
|
||||||
|
<canvas id="gradient-canvas" class="gradient-background"></canvas>
|
||||||
|
<div class="gradient-overlay"></div>
|
||||||
<div class="row align-items-center row-cols-1 logo-container mb-3 justify-content-center">
|
<div class="row align-items-center row-cols-1 logo-container mb-3 justify-content-center">
|
||||||
<div class="col col-md-4 col-sm-12 col-xs-12 align-self-center p-0">
|
<div class="col col-md-4 col-sm-12 col-xs-12 align-self-center p-0">
|
||||||
<div class="row align-items-center row-cols-1 justify-content-center">
|
<div class="row align-items-center row-cols-1 justify-content-center">
|
||||||
@ -14,13 +14,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="row align-items-center row-cols-1 login-container justify-content-center">
|
<div class="row align-items-center row-cols-1 login-container justify-content-center">
|
||||||
<div #tiltCard class="col col-xl-2 col-lg-3 col-md-3 col-sm-10 col-xs-10 align-self-center card tilt p-3"
|
<div class="col col-xl-2 col-lg-3 col-md-3 col-sm-10 col-xs-10 align-self-center card tilt p-3">
|
||||||
role="main"
|
<div class="shine"></div>
|
||||||
aria-label="Login form">
|
|
||||||
<div class="shine" aria-hidden="true"></div>
|
|
||||||
<span>
|
<span>
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<div class="card-title text-center">
|
<div class="card-title text-center">
|
||||||
@ -33,5 +31,7 @@
|
|||||||
<ng-content select="[body]"></ng-content>
|
<ng-content select="[body]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
@ -2,7 +2,6 @@
|
|||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background and overlay layers
|
|
||||||
.gradient-background {
|
.gradient-background {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -10,10 +9,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
// Performance optimizations
|
|
||||||
will-change: auto; // Let browser decide
|
|
||||||
contain: layout style paint; // CSS containment for better performance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient-overlay {
|
.gradient-overlay {
|
||||||
@ -22,13 +17,11 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, var(--gradient-darkness, 0));
|
background-color: rgba(0, 0, 0, var(--gradient-darkness));
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
contain: layout style; // Lighter containment for overlay
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main login container
|
|
||||||
.login {
|
.login {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -40,7 +33,6 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
|
|
||||||
// Typography
|
|
||||||
h2 {
|
h2 {
|
||||||
font-family: "Spartan", sans-serif;
|
font-family: "Spartan", sans-serif;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@ -51,140 +43,65 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo container
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: var(--login-logo-width, 55px);
|
width: var(--login-logo-width);
|
||||||
height: var(--login-logo-height, 55px);
|
height: var(--login-logo-height);
|
||||||
background-image: var(--login-logo-image, url("/assets/images/logo.png"));
|
background-image: var(--login-logo-image);
|
||||||
background-size: var(--login-logo-bg-size, contain);
|
background-size: var(--login-logo-bg-size);
|
||||||
background-repeat: var(--login-logo-bg-repeat, no-repeat);
|
background-repeat: var(--login-logo-bg-repeat);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login container
|
|
||||||
.login-container {
|
.login-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced card with performance optimizations
|
|
||||||
.card {
|
.card {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
border-width: var(--login-card-border-width, 1px);
|
border-width: var(--login-card-border-width);
|
||||||
border-style: var(--login-card-border-style, solid);
|
border-style: var(--login-card-border-style);
|
||||||
border-color: var(--login-card-border-color, rgba(255, 255, 255, 0.03));
|
border-color: var(--login-card-border-color);
|
||||||
background: var(--login-card-bg, rgba(255, 255, 255, 0.1));
|
background: var(--login-card-bg);
|
||||||
backdrop-filter: var(--login-card-backdrop-filter, blur(6.7px));
|
box-shadow: var(--login-card-box-shadow);
|
||||||
-webkit-backdrop-filter: var(--login-card-backdrop-filter, blur(6.7px));
|
backdrop-filter: var(--login-card-backdrop-filter);
|
||||||
|
-webkit-backdrop-filter: var(--login-card-backdrop-filter);
|
||||||
// Enhanced shadows with dynamic system
|
transform-style: preserve-3d; /* Ensures child elements also participate in 3D */
|
||||||
box-shadow:
|
transition: transform 0.1s ease-out;
|
||||||
var(--login-card-box-shadow, 0 4px 30px rgba(0, 0, 0, 0.1)),
|
|
||||||
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color-intense);
|
|
||||||
|
|
||||||
// 3D transform setup
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// Optimized transitions
|
// Input fields with dynamic shadows
|
||||||
transition:
|
|
||||||
transform 0.1s cubic-bezier(0.4, 0, 0.2, 1),
|
|
||||||
box-shadow 0.1s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
// Performance hints
|
|
||||||
will-change: transform, box-shadow;
|
|
||||||
contain: layout style;
|
|
||||||
|
|
||||||
// Enhanced shine effect
|
|
||||||
.shine {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at var(--shine-pos-x) var(--shine-pos-y),
|
|
||||||
rgba(255, 255, 255, 0.1) 0%,
|
|
||||||
rgba(255, 255, 255, 0.08) 20%,
|
|
||||||
rgba(255, 255, 255, 0.03) 40%,
|
|
||||||
rgba(255, 255, 255, 0) 60%
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: background 0.1s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
contain: layout style paint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus states
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--primary-color, #4ac694);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card title styling
|
|
||||||
::ng-deep .card-title {
|
|
||||||
h2 {
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card text styling
|
|
||||||
.card-text {
|
|
||||||
font-family: "EBGaramond", "Helvetica Neue", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced input styling with dynamic shadows
|
|
||||||
::ng-deep input,
|
::ng-deep input,
|
||||||
::ng-deep .form-control {
|
::ng-deep .form-control {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 4px rgba(0, 0, 0, 0.1),
|
0 2px 4px rgba(0, 0, 0, 0.1),
|
||||||
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
||||||
|
transition: box-shadow 0.1s ease-out, border-color 0.15s ease-in-out;
|
||||||
transition:
|
|
||||||
box-shadow 0.1s cubic-bezier(0.4, 0, 0.2, 1),
|
|
||||||
border-color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
contain: layout style;
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
var(--login-input-box-shadow-focus, 0 0 0 1px rgba(74, 198, 148, 0.8)),
|
var(--login-input-box-shadow-focus),
|
||||||
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accessibility improvements
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--primary-color, #4ac694);
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced button styling with micro-interactions
|
// Button styles with dynamic shadows
|
||||||
::ng-deep .btn {
|
::ng-deep .btn {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 4px var(--dynamic-shadow-color-button),
|
0 2px 4px var(--dynamic-shadow-color-button),
|
||||||
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.1s ease-out;
|
||||||
transition:
|
|
||||||
transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
|
|
||||||
box-shadow 0.1s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
contain: layout style;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@ -199,12 +116,6 @@
|
|||||||
0 2px 4px var(--dynamic-shadow-color-button),
|
0 2px 4px var(--dynamic-shadow-color-button),
|
||||||
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
var(--dynamic-shadow-x) var(--dynamic-shadow-y) var(--dynamic-shadow-blur) var(--dynamic-shadow-spread) var(--dynamic-shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus states for accessibility
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--primary-color, #4ac694);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary button enhanced styling
|
// Primary button enhanced styling
|
||||||
@ -229,70 +140,43 @@
|
|||||||
var(--dynamic-shadow-x) var(--dynamic-shadow-y) calc(var(--dynamic-shadow-blur) + 2px) var(--dynamic-shadow-spread) var(--dynamic-shadow-color-intense);
|
var(--dynamic-shadow-x) var(--dynamic-shadow-y) calc(var(--dynamic-shadow-blur) + 2px) var(--dynamic-shadow-spread) var(--dynamic-shadow-color-intense);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Responsive design improvements
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.login {
|
|
||||||
.card {
|
|
||||||
max-width: 90%;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
// Reduce effects intensity on mobile for better performance
|
|
||||||
::ng-deep .btn {
|
|
||||||
&:hover {
|
|
||||||
transform: none; // Disable hover lift on touch devices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduced motion preferences
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.login .card {
|
|
||||||
transition: none;
|
|
||||||
will-change: auto;
|
|
||||||
|
|
||||||
.shine {
|
.shine {
|
||||||
transition: none;
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at var(--shine-pos-x, 50%) var(--shine-pos-y, 50%),
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
rgba(255, 255, 255, 0.08) 20%,
|
||||||
|
rgba(255, 255, 255, 0.03) 40%,
|
||||||
|
rgba(255, 255, 255, 0) 60%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: background 0.1s ease-out;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep input,
|
&:focus {
|
||||||
::ng-deep .form-control,
|
border: 2px solid white;
|
||||||
::ng-deep .btn {
|
|
||||||
transition: none;
|
}
|
||||||
|
|
||||||
|
|
||||||
|
::ng-deep.card-title {
|
||||||
|
h2 {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
font-family: "EBGaramond", "Helvetica Neue", sans-serif;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient-background {
|
|
||||||
will-change: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// High contrast mode support
|
|
||||||
@media (prefers-contrast: high) {
|
|
||||||
.login .card {
|
|
||||||
border: 2px solid var(--primary-color, #4ac694);
|
|
||||||
background: var(--bs-body-bg, #1f2020);
|
|
||||||
backdrop-filter: none;
|
|
||||||
-webkit-backdrop-filter: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print styles
|
|
||||||
@media print {
|
|
||||||
.gradient-background,
|
|
||||||
.gradient-overlay,
|
|
||||||
.shine {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .card {
|
|
||||||
box-shadow: none;
|
|
||||||
border: 1px solid #000;
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,472 +1,291 @@
|
|||||||
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, inject, OnDestroy, OnInit, ViewChild, DestroyRef, signal, computed, AfterViewInit } from '@angular/core';
|
import {ChangeDetectionStrategy, Component, HostListener, inject, OnInit, OnDestroy} from '@angular/core';
|
||||||
import { AsyncPipe, NgStyle } from "@angular/common";
|
import {AsyncPipe, NgStyle} from "@angular/common";
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import {NavService} from "../../../_services/nav.service";
|
||||||
import { NavService } from "../../../_services/nav.service";
|
|
||||||
|
|
||||||
// Types for better type safety
|
|
||||||
interface GradientPoint {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
vx: number;
|
|
||||||
vy: number;
|
|
||||||
color: { r: number; g: number; b: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MousePosition {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TiltEffect {
|
|
||||||
rotateX: number;
|
|
||||||
rotateY: number;
|
|
||||||
shadowX: number;
|
|
||||||
shadowY: number;
|
|
||||||
intensity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-splash-container',
|
selector: 'app-splash-container',
|
||||||
templateUrl: './splash-container.component.html',
|
templateUrl: './splash-container.component.html',
|
||||||
styleUrls: ['./splash-container.component.scss'],
|
styleUrls: ['./splash-container.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [NgStyle, AsyncPipe],
|
imports: [
|
||||||
standalone: true
|
NgStyle,
|
||||||
|
AsyncPipe
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class SplashContainerComponent implements OnInit, OnDestroy, AfterViewInit {
|
export class SplashContainerComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
// Dependency injection
|
|
||||||
protected readonly navService = inject(NavService);
|
protected readonly navService = inject(NavService);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private maxTilt = 5; // Maximum tilt angle in degrees
|
||||||
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
private animationId?: number;
|
||||||
|
private resizeHandler?: () => void;
|
||||||
|
private tiltElement?: HTMLElement;
|
||||||
|
private mouseMoveThrottleId?: number;
|
||||||
|
private lastMouseEvent?: MouseEvent;
|
||||||
|
|
||||||
// ViewChild for safer DOM access
|
ngOnInit() {
|
||||||
@ViewChild('gradientCanvas', { static: true })
|
this.initGradientAnimationWithCssVars();
|
||||||
private canvasRef?: ElementRef<HTMLCanvasElement>;
|
this.cacheTiltElement();
|
||||||
|
}
|
||||||
|
|
||||||
@ViewChild('tiltCard', { static: true })
|
ngOnDestroy() {
|
||||||
private tiltCardRef?: ElementRef<HTMLElement>;
|
if (this.animationId !== undefined) {
|
||||||
|
cancelAnimationFrame(this.animationId);
|
||||||
// Configuration constants
|
|
||||||
private readonly MAX_TILT = 5;
|
|
||||||
private readonly SHADOW_MULTIPLIER = 2;
|
|
||||||
private readonly MIN_INTENSITY = 0.1;
|
|
||||||
private readonly GRADIENT_RADIUS = 0.5;
|
|
||||||
|
|
||||||
// State management with signals (Angular 16+)
|
|
||||||
private readonly mousePosition = signal<MousePosition>({ x: 0, y: 0 });
|
|
||||||
private readonly isMouseOverCard = signal(false);
|
|
||||||
|
|
||||||
// Computed values for reactive updates
|
|
||||||
private readonly tiltEffect = computed<TiltEffect>(() => {
|
|
||||||
const mouse = this.mousePosition();
|
|
||||||
const isOver = this.isMouseOverCard();
|
|
||||||
|
|
||||||
if (isOver || !this.tiltCardRef?.nativeElement) {
|
|
||||||
return { rotateX: 0, rotateY: 0, shadowX: 0, shadowY: 0, intensity: 0.5 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.calculateTiltEffect(mouse);
|
if (this.mouseMoveThrottleId !== undefined) {
|
||||||
});
|
|
||||||
|
|
||||||
// Resource tracking
|
|
||||||
private gradientAnimationId?: number;
|
|
||||||
private mouseMoveThrottleId?: number;
|
|
||||||
private resizeObserver?: ResizeObserver;
|
|
||||||
private gradientPoints: GradientPoint[] = [];
|
|
||||||
private canvasContext?: CanvasRenderingContext2D;
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.initializeGradientColors();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
// Safer initialization after view is ready
|
|
||||||
this.initializeCanvas();
|
|
||||||
this.initializeResizeObserver();
|
|
||||||
this.startGradientAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('document:mousemove', ['$event'])
|
|
||||||
onMouseMove(event: MouseEvent): void {
|
|
||||||
// Cancel previous throttled update
|
|
||||||
if (this.mouseMoveThrottleId) {
|
|
||||||
cancelAnimationFrame(this.mouseMoveThrottleId);
|
cancelAnimationFrame(this.mouseMoveThrottleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throttle updates to animation frame rate
|
if (this.resizeHandler) {
|
||||||
|
window.removeEventListener('resize', this.resizeHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cacheTiltElement() {
|
||||||
|
// Cache the tilt element reference to avoid repeated DOM queries
|
||||||
|
this.tiltElement = document.querySelector('.tilt') as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:mousemove', ['$event'])
|
||||||
|
onMouseMove(event: MouseEvent) {
|
||||||
|
// Store the latest mouse event
|
||||||
|
this.lastMouseEvent = event;
|
||||||
|
|
||||||
|
// Cancel any pending throttled update
|
||||||
|
if (this.mouseMoveThrottleId !== undefined) {
|
||||||
|
return; // Already scheduled, skip this event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule the actual update for the next animation frame
|
||||||
this.mouseMoveThrottleId = requestAnimationFrame(() => {
|
this.mouseMoveThrottleId = requestAnimationFrame(() => {
|
||||||
this.updateMousePosition(event);
|
if (this.lastMouseEvent) {
|
||||||
this.updateTiltEffects();
|
this.handleMouseMove(this.lastMouseEvent);
|
||||||
|
}
|
||||||
this.mouseMoveThrottleId = undefined;
|
this.mouseMoveThrottleId = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private handleMouseMove(event: MouseEvent) {
|
||||||
* Initialize gradient colors from CSS variables with proper fallbacks
|
if (!this.tiltElement) return;
|
||||||
*/
|
|
||||||
private initializeGradientColors(): void {
|
|
||||||
const defaultColors = [
|
|
||||||
{ r: 73, g: 197, b: 147 }, // --gradient-color-1
|
|
||||||
{ r: 138, g: 43, b: 226 }, // --gradient-color-2
|
|
||||||
{ r: 255, g: 215, b: 0 }, // --gradient-color-3
|
|
||||||
{ r: 255, g: 20, b: 147 } // --gradient-color-4
|
|
||||||
];
|
|
||||||
|
|
||||||
this.gradientPoints = [
|
const elementBounds = this.tiltElement.getBoundingClientRect();
|
||||||
{ x: 0.2, y: 0.2, vx: 0.001, vy: 0.0015, color: this.getCssColorOrDefault('--gradient-color-1', defaultColors[0]) },
|
const mouseX = event.clientX;
|
||||||
{ x: 0.8, y: 0.3, vx: -0.0015, vy: 0.001, color: this.getCssColorOrDefault('--gradient-color-2', defaultColors[1]) },
|
const mouseY = event.clientY;
|
||||||
{ x: 0.5, y: 0.8, vx: 0.0012, vy: -0.0018, color: this.getCssColorOrDefault('--gradient-color-3', defaultColors[2]) },
|
|
||||||
{ x: 0.3, y: 0.6, vx: -0.0018, vy: -0.0012, color: this.getCssColorOrDefault('--gradient-color-4', defaultColors[3]) }
|
// Check if mouse is over the element
|
||||||
];
|
const isMouseOverElement =
|
||||||
|
mouseX >= elementBounds.left &&
|
||||||
|
mouseX <= elementBounds.right &&
|
||||||
|
mouseY >= elementBounds.top &&
|
||||||
|
mouseY <= elementBounds.bottom;
|
||||||
|
|
||||||
|
// Always calculate shine position relative to the element
|
||||||
|
const relativeX = mouseX - elementBounds.left;
|
||||||
|
const relativeY = mouseY - elementBounds.top;
|
||||||
|
|
||||||
|
// Convert to percentage and clamp to keep shine within element bounds (0-100%)
|
||||||
|
const shineX = Math.max(0, Math.min(100, (relativeX / elementBounds.width) * 100));
|
||||||
|
const shineY = Math.max(0, Math.min(100, (relativeY / elementBounds.height) * 100));
|
||||||
|
|
||||||
|
// Apply clamped shine position to keep it within element
|
||||||
|
document.documentElement.style.setProperty('--shine-pos-x', `${Math.round(shineX)}%`);
|
||||||
|
document.documentElement.style.setProperty('--shine-pos-y', `${Math.round(shineY)}%`);
|
||||||
|
|
||||||
|
// Calculate tilt values for shadow effects
|
||||||
|
const centerX = elementBounds.left + elementBounds.width / 2;
|
||||||
|
const centerY = elementBounds.top + elementBounds.height / 2;
|
||||||
|
const tiltX = ((mouseY - centerY) / window.innerHeight) * this.maxTilt;
|
||||||
|
const tiltY = ((mouseX - centerX) / window.innerWidth) * this.maxTilt;
|
||||||
|
|
||||||
|
// Calculate shadow offset based on tilt (same direction as mouse for closer-darker effect)
|
||||||
|
const shadowOffsetX = tiltY * 2; // Same direction as tilt for light-source effect
|
||||||
|
const shadowOffsetY = tiltX * 2;
|
||||||
|
|
||||||
|
// Calculate distance from mouse to element center for shadow intensity
|
||||||
|
const distanceFromMouse = Math.sqrt(
|
||||||
|
Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2)
|
||||||
|
);
|
||||||
|
const maxDistance = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
|
||||||
|
const normalizedDistance = Math.min(distanceFromMouse / maxDistance, 1);
|
||||||
|
|
||||||
|
// Calculate shadow intensity (stronger when closer, weaker when farther)
|
||||||
|
const shadowIntensity = Math.max(0.1, 1 - normalizedDistance); // Min 10%, Max 100%
|
||||||
|
|
||||||
|
// Set CSS variables for dynamic shadows with intensity
|
||||||
|
document.documentElement.style.setProperty('--dynamic-shadow-x', `${shadowOffsetX}px`);
|
||||||
|
document.documentElement.style.setProperty('--dynamic-shadow-y', `${shadowOffsetY}px`);
|
||||||
|
document.documentElement.style.setProperty('--shadow-intensity', shadowIntensity.toString());
|
||||||
|
|
||||||
|
// Apply tilt effect based on hover state
|
||||||
|
if (isMouseOverElement) {
|
||||||
|
// Reset tilt to zero when hovering over element
|
||||||
|
this.tiltElement.style.transform = 'perspective(500px) rotateX(0deg) rotateY(0deg)';
|
||||||
|
// Reset shadows when hovering
|
||||||
|
document.documentElement.style.setProperty('--dynamic-shadow-x', '0px');
|
||||||
|
document.documentElement.style.setProperty('--dynamic-shadow-y', '0px');
|
||||||
|
document.documentElement.style.setProperty('--shadow-intensity', '0.5');
|
||||||
|
} else {
|
||||||
|
// Apply tilt effect based on distance from element center when not hovering
|
||||||
|
this.tiltElement.style.transform = `perspective(500px) rotateX(${tiltX}deg) rotateY(${tiltY}deg)`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private getCssVariable(variableName: string, element?: HTMLElement): string | null {
|
||||||
* Safely initialize canvas with proper error handling
|
const targetElement = element || document.documentElement;
|
||||||
*/
|
const value = getComputedStyle(targetElement).getPropertyValue(variableName).trim();
|
||||||
private initializeCanvas(): void {
|
|
||||||
const canvas = this.canvasRef?.nativeElement;
|
return value || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCssVariableAsRgb(variableName: string, element?: HTMLElement): { r: number; g: number; b: number } | null {
|
||||||
|
const hexValue = this.getCssVariable(variableName, element);
|
||||||
|
|
||||||
|
if (!hexValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hexToRgb(hexValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated gradient initialization using CSS variables
|
||||||
|
private initGradientAnimationWithCssVars() {
|
||||||
|
const canvas = document.getElementById('gradient-canvas') as HTMLCanvasElement;
|
||||||
|
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
console.warn('Canvas element not found, gradient animation disabled');
|
return; // Exit gracefully if canvas element doesn't exist
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = canvas.getContext('2d', {
|
const ctx = canvas.getContext('2d');
|
||||||
alpha: false,
|
|
||||||
desynchronized: true // Better performance for animations
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!context) {
|
if (!ctx) {
|
||||||
console.warn('Unable to get 2D context, gradient animation disabled');
|
return; // Exit gracefully if context cannot be obtained
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canvasContext = context;
|
// Firefox-specific optimizations for smoother gradients
|
||||||
this.setupCanvasOptimizations();
|
ctx.imageSmoothingEnabled = true;
|
||||||
this.resizeCanvas();
|
ctx.imageSmoothingQuality = 'high';
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Set canvas size
|
||||||
* Apply performance optimizations to canvas context
|
const resizeCanvas = () => {
|
||||||
*/
|
canvas.width = window.innerWidth;
|
||||||
private setupCanvasOptimizations(): void {
|
canvas.height = window.innerHeight;
|
||||||
if (!this.canvasContext) return;
|
};
|
||||||
|
|
||||||
// Firefox-specific optimizations
|
this.resizeHandler = resizeCanvas;
|
||||||
this.canvasContext.imageSmoothingEnabled = true;
|
resizeCanvas();
|
||||||
this.canvasContext.imageSmoothingQuality = 'high';
|
window.addEventListener('resize', this.resizeHandler);
|
||||||
|
|
||||||
const canvas = this.canvasRef!.nativeElement;
|
// Apply Firefox-specific CSS smoothing
|
||||||
canvas.style.imageRendering = 'auto';
|
canvas.style.imageRendering = 'auto';
|
||||||
canvas.style.backfaceVisibility = 'hidden';
|
canvas.style.backfaceVisibility = 'hidden';
|
||||||
|
canvas.style.perspective = '1000px';
|
||||||
canvas.style.transform = 'translateZ(0)';
|
canvas.style.transform = 'translateZ(0)';
|
||||||
canvas.style.willChange = 'transform';
|
canvas.style.willChange = 'transform';
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Get colors from CSS variables
|
||||||
* Initialize ResizeObserver for better performance than window resize events
|
const gradientColor1 = this.getCssVariableAsRgb('--gradient-color-1') || { r: 73, g: 197, b: 147 };
|
||||||
*/
|
const gradientColor2 = this.getCssVariableAsRgb('--gradient-color-2') || { r: 138, g: 43, b: 226 };
|
||||||
private initializeResizeObserver(): void {
|
const gradientColor3 = this.getCssVariableAsRgb('--gradient-color-3') || { r: 255, g: 215, b: 0 };
|
||||||
if (!('ResizeObserver' in window) || !this.canvasRef?.nativeElement) {
|
const gradientColor4 = this.getCssVariableAsRgb('--gradient-color-4') || { r: 255, g: 20, b: 147 };
|
||||||
// Fallback to window resize for older browsers
|
|
||||||
this.initializeFallbackResize();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver((entries) => {
|
// Gradient points configuration with CSS variable colors
|
||||||
for (const entry of entries) {
|
const gradientPoints = [
|
||||||
if (entry.target === this.canvasRef!.nativeElement) {
|
{
|
||||||
this.resizeCanvas();
|
x: 0.2,
|
||||||
break;
|
y: 0.2,
|
||||||
}
|
vx: 0.001,
|
||||||
|
vy: 0.0015,
|
||||||
|
color: gradientColor1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0.8,
|
||||||
|
y: 0.3,
|
||||||
|
vx: -0.0015,
|
||||||
|
vy: 0.001,
|
||||||
|
color: gradientColor2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0.5,
|
||||||
|
y: 0.8,
|
||||||
|
vx: 0.0012,
|
||||||
|
vy: -0.0018,
|
||||||
|
color: gradientColor3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0.3,
|
||||||
|
y: 0.6,
|
||||||
|
vx: -0.0018,
|
||||||
|
vy: -0.0012,
|
||||||
|
color: gradientColor4
|
||||||
}
|
}
|
||||||
});
|
];
|
||||||
|
|
||||||
this.resizeObserver.observe(this.canvasRef.nativeElement);
|
const animate = () => {
|
||||||
}
|
// Clear canvas with background color from CSS variable
|
||||||
|
const canvasBackgroundColor = this.getCssVariable('--elevation-layer2-dark-solid') || '#1f2020';
|
||||||
|
ctx.fillStyle = canvasBackgroundColor;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
/**
|
// Update point positions
|
||||||
* Fallback resize handling for older browsers
|
gradientPoints.forEach(point => {
|
||||||
*/
|
point.x += point.vx;
|
||||||
private initializeFallbackResize(): void {
|
point.y += point.vy;
|
||||||
const resizeHandler = () => this.resizeCanvas();
|
|
||||||
window.addEventListener('resize', resizeHandler);
|
|
||||||
|
|
||||||
// Cleanup using takeUntilDestroyed
|
// Bounce off edges
|
||||||
this.destroyRef.onDestroy(() => {
|
if (point.x <= 0.1 || point.x >= 0.9) point.vx *= -1;
|
||||||
window.removeEventListener('resize', resizeHandler);
|
if (point.y <= 0.1 || point.y >= 0.9) point.vy *= -1;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Keep within bounds
|
||||||
* Resize canvas with device pixel ratio consideration
|
point.x = Math.max(0.1, Math.min(0.9, point.x));
|
||||||
*/
|
point.y = Math.max(0.1, Math.min(0.9, point.y));
|
||||||
private resizeCanvas(): void {
|
});
|
||||||
const canvas = this.canvasRef?.nativeElement;
|
|
||||||
if (!canvas || !this.canvasContext) return;
|
|
||||||
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
// Create gradients for each point with more color stops for smoother transitions
|
||||||
const rect = canvas.getBoundingClientRect();
|
gradientPoints.forEach((point, index) => {
|
||||||
|
const gradient = ctx.createRadialGradient(
|
||||||
|
point.x * canvas.width,
|
||||||
|
point.y * canvas.height,
|
||||||
|
0,
|
||||||
|
point.x * canvas.width,
|
||||||
|
point.y * canvas.height,
|
||||||
|
canvas.width * 0.5
|
||||||
|
);
|
||||||
|
|
||||||
// Set actual size in memory (scaled for device pixel ratio)
|
gradient.addColorStop(0, `rgba(${point.color.r}, ${point.color.g}, ${point.color.b}, 0.15)`);
|
||||||
canvas.width = rect.width * dpr;
|
gradient.addColorStop(0.2, `rgba(${point.color.r}, ${point.color.g}, ${point.color.b}, 0.12)`);
|
||||||
canvas.height = rect.height * dpr;
|
gradient.addColorStop(0.4, `rgba(${point.color.r}, ${point.color.g}, ${point.color.b}, 0.08)`);
|
||||||
|
gradient.addColorStop(0.7, `rgba(${point.color.r}, ${point.color.g}, ${point.color.b}, 0.03)`);
|
||||||
|
gradient.addColorStop(1, `rgba(${point.color.r}, ${point.color.g}, ${point.color.b}, 0)`);
|
||||||
|
|
||||||
// Scale the canvas back down using CSS
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
canvas.style.width = `${rect.width}px`;
|
ctx.fillStyle = gradient;
|
||||||
canvas.style.height = `${rect.height}px`;
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
});
|
||||||
|
|
||||||
// Scale the drawing context so everything draws at the correct size
|
this.animationId = requestAnimationFrame(animate);
|
||||||
this.canvasContext.scale(dpr, dpr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the gradient animation loop
|
|
||||||
*/
|
|
||||||
private startGradientAnimation(): void {
|
|
||||||
if (!this.canvasContext || this.gradientAnimationId) return;
|
|
||||||
|
|
||||||
const animate = (): void => {
|
|
||||||
this.renderGradientFrame();
|
|
||||||
this.gradientAnimationId = requestAnimationFrame(animate);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
animate();
|
animate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a single gradient animation frame
|
|
||||||
*/
|
|
||||||
private renderGradientFrame(): void {
|
|
||||||
if (!this.canvasContext || !this.canvasRef?.nativeElement) return;
|
|
||||||
|
|
||||||
const canvas = this.canvasRef.nativeElement;
|
|
||||||
const ctx = this.canvasContext;
|
|
||||||
|
|
||||||
// Clear with theme background
|
|
||||||
const bgColor = this.getCssVariable('--elevation-layer2-dark-solid') || '#212121';
|
|
||||||
ctx.fillStyle = bgColor;
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
// Update and render gradient points
|
|
||||||
this.updateGradientPoints();
|
|
||||||
this.renderGradientPoints(ctx, canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update gradient point positions with boundary checking
|
|
||||||
*/
|
|
||||||
private updateGradientPoints(): void {
|
|
||||||
for (const point of this.gradientPoints) {
|
|
||||||
point.x += point.vx;
|
|
||||||
point.y += point.vy;
|
|
||||||
|
|
||||||
// Bounce off edges
|
|
||||||
if (point.x <= 0.1 || point.x >= 0.9) point.vx *= -1;
|
|
||||||
if (point.y <= 0.1 || point.y >= 0.9) point.vy *= -1;
|
|
||||||
|
|
||||||
// Clamp to bounds
|
|
||||||
point.x = Math.max(0.1, Math.min(0.9, point.x));
|
|
||||||
point.y = Math.max(0.1, Math.min(0.9, point.y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render gradient points with optimized radial gradients
|
|
||||||
*/
|
|
||||||
private renderGradientPoints(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement): void {
|
|
||||||
for (const point of this.gradientPoints) {
|
|
||||||
const gradient = ctx.createRadialGradient(
|
|
||||||
point.x * canvas.width,
|
|
||||||
point.y * canvas.height,
|
|
||||||
0,
|
|
||||||
point.x * canvas.width,
|
|
||||||
point.y * canvas.height,
|
|
||||||
canvas.width * this.GRADIENT_RADIUS
|
|
||||||
);
|
|
||||||
|
|
||||||
// Optimized gradient stops
|
|
||||||
const { r, g, b } = point.color;
|
|
||||||
gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.15)`);
|
|
||||||
gradient.addColorStop(0.2, `rgba(${r}, ${g}, ${b}, 0.12)`);
|
|
||||||
gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.08)`);
|
|
||||||
gradient.addColorStop(0.7, `rgba(${r}, ${g}, ${b}, 0.03)`);
|
|
||||||
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
||||||
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update mouse position with boundary checks
|
|
||||||
*/
|
|
||||||
private updateMousePosition(event: MouseEvent): void {
|
|
||||||
const cardElement = this.tiltCardRef?.nativeElement;
|
|
||||||
if (!cardElement) return;
|
|
||||||
|
|
||||||
const rect = cardElement.getBoundingClientRect();
|
|
||||||
const isOver = event.clientX >= rect.left &&
|
|
||||||
event.clientX <= rect.right &&
|
|
||||||
event.clientY >= rect.top &&
|
|
||||||
event.clientY <= rect.bottom;
|
|
||||||
|
|
||||||
this.mousePosition.set({ x: event.clientX, y: event.clientY });
|
|
||||||
this.isMouseOverCard.set(isOver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate tilt effect based on mouse position
|
|
||||||
*/
|
|
||||||
private calculateTiltEffect(mouse: MousePosition): TiltEffect {
|
|
||||||
const cardElement = this.tiltCardRef?.nativeElement;
|
|
||||||
if (!cardElement) {
|
|
||||||
return { rotateX: 0, rotateY: 0, shadowX: 0, shadowY: 0, intensity: 0.5 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = cardElement.getBoundingClientRect();
|
|
||||||
const centerX = rect.left + rect.width / 2;
|
|
||||||
const centerY = rect.top + rect.height / 2;
|
|
||||||
|
|
||||||
const tiltX = ((mouse.y - centerY) / window.innerHeight) * this.MAX_TILT;
|
|
||||||
const tiltY = ((mouse.x - centerX) / window.innerWidth) * this.MAX_TILT;
|
|
||||||
|
|
||||||
const shadowX = tiltY * this.SHADOW_MULTIPLIER;
|
|
||||||
const shadowY = tiltX * this.SHADOW_MULTIPLIER;
|
|
||||||
|
|
||||||
// Calculate distance-based intensity
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
Math.pow(mouse.x - centerX, 2) + Math.pow(mouse.y - centerY, 2)
|
|
||||||
);
|
|
||||||
const maxDistance = Math.sqrt(
|
|
||||||
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2)
|
|
||||||
);
|
|
||||||
const intensity = Math.max(this.MIN_INTENSITY, 1 - (distance / maxDistance));
|
|
||||||
|
|
||||||
return {
|
|
||||||
rotateX: tiltX,
|
|
||||||
rotateY: tiltY,
|
|
||||||
shadowX,
|
|
||||||
shadowY,
|
|
||||||
intensity
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply tilt effects to DOM using CSS custom properties
|
|
||||||
*/
|
|
||||||
private updateTiltEffects(): void {
|
|
||||||
const effect = this.tiltEffect();
|
|
||||||
const cardElement = this.tiltCardRef?.nativeElement;
|
|
||||||
|
|
||||||
if (!cardElement) return;
|
|
||||||
|
|
||||||
// Apply transform
|
|
||||||
const transform = `perspective(500px) rotateX(${effect.rotateX}deg) rotateY(${effect.rotateY}deg)`;
|
|
||||||
cardElement.style.transform = transform;
|
|
||||||
|
|
||||||
// Update CSS custom properties for shadows
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.style.setProperty('--dynamic-shadow-x', `${effect.shadowX}px`);
|
|
||||||
root.style.setProperty('--dynamic-shadow-y', `${effect.shadowY}px`);
|
|
||||||
root.style.setProperty('--shadow-intensity', effect.intensity.toString());
|
|
||||||
|
|
||||||
// Update shine position
|
|
||||||
if (!this.isMouseOverCard()) {
|
|
||||||
const mouse = this.mousePosition();
|
|
||||||
const rect = cardElement.getBoundingClientRect();
|
|
||||||
const relativeX = Math.max(0, Math.min(100, ((mouse.x - rect.left) / rect.width) * 100));
|
|
||||||
const relativeY = Math.max(0, Math.min(100, ((mouse.y - rect.top) / rect.height) * 100));
|
|
||||||
|
|
||||||
root.style.setProperty('--shine-pos-x', `${Math.round(relativeX)}%`);
|
|
||||||
root.style.setProperty('--shine-pos-y', `${Math.round(relativeY)}%`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CSS variable with proper error handling
|
|
||||||
*/
|
|
||||||
private getCssVariable(variableName: string, element: HTMLElement = document.documentElement): string | null {
|
|
||||||
try {
|
|
||||||
const value = getComputedStyle(element).getPropertyValue(variableName).trim();
|
|
||||||
return value || null;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to get CSS variable ${variableName}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CSS color as RGB object with fallback
|
|
||||||
*/
|
|
||||||
private getCssColorOrDefault(variableName: string, defaultColor: { r: number; g: number; b: number }): { r: number; g: number; b: number } {
|
|
||||||
const cssValue = this.getCssVariable(variableName);
|
|
||||||
if (!cssValue) return defaultColor;
|
|
||||||
|
|
||||||
const rgbColor = this.hexToRgb(cssValue);
|
|
||||||
return rgbColor || defaultColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert hex color to RGB with improved validation
|
|
||||||
*/
|
|
||||||
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||||
// Remove hash and whitespace
|
// Remove the hash if present
|
||||||
hex = hex.replace(/^#/, '').trim();
|
hex = hex.replace('#', '');
|
||||||
|
|
||||||
// Handle 3-digit hex
|
// Handle 3-digit hex codes (e.g., #fff -> #ffffff)
|
||||||
if (hex.length === 3) {
|
if (hex.length === 3) {
|
||||||
hex = hex.split('').map(char => char + char).join('');
|
hex = hex.split('').map(char => char + char).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate format
|
// Validate hex format - return null for invalid formats
|
||||||
if (hex.length !== 6 || !/^[0-9A-Fa-f]{6}$/.test(hex)) {
|
if (hex.length !== 6 || !/^[0-9A-Fa-f]{6}$/.test(hex)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to RGB
|
// Convert to RGB
|
||||||
return {
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
r: parseInt(hex.substring(0, 2), 16),
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
g: parseInt(hex.substring(2, 4), 16),
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
b: parseInt(hex.substring(4, 6), 16)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return { r, g, b };
|
||||||
* Comprehensive cleanup of all resources
|
|
||||||
*/
|
|
||||||
private cleanup(): void {
|
|
||||||
// Cancel animation frames
|
|
||||||
if (this.gradientAnimationId !== undefined) {
|
|
||||||
cancelAnimationFrame(this.gradientAnimationId);
|
|
||||||
this.gradientAnimationId = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mouseMoveThrottleId !== undefined) {
|
|
||||||
cancelAnimationFrame(this.mouseMoveThrottleId);
|
|
||||||
this.mouseMoveThrottleId = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup ResizeObserver
|
|
||||||
if (this.resizeObserver) {
|
|
||||||
this.resizeObserver.disconnect();
|
|
||||||
this.resizeObserver = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset CSS custom properties
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.style.removeProperty('--dynamic-shadow-x');
|
|
||||||
root.style.removeProperty('--dynamic-shadow-y');
|
|
||||||
root.style.removeProperty('--shadow-intensity');
|
|
||||||
root.style.removeProperty('--shine-pos-x');
|
|
||||||
root.style.removeProperty('--shine-pos-y');
|
|
||||||
|
|
||||||
// Clear references
|
|
||||||
this.canvasContext = undefined;
|
|
||||||
this.gradientPoints = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,18 +9,12 @@
|
|||||||
--gradient-color-4: #ff1493; /* Fourth gradient orb (left-center) */
|
--gradient-color-4: #ff1493; /* Fourth gradient orb (left-center) */
|
||||||
--gradient-darkness: 0; /* Adjust from 0 (no overlay) to 1 (completely dark) */
|
--gradient-darkness: 0; /* Adjust from 0 (no overlay) to 1 (completely dark) */
|
||||||
|
|
||||||
// Dynamic shadow system
|
// Dynamic shadow variables for tilt effects
|
||||||
--dynamic-shadow-x: 0px;
|
--dynamic-shadow-x: 0px;
|
||||||
--dynamic-shadow-y: 0px;
|
--dynamic-shadow-y: 0px;
|
||||||
--dynamic-shadow-blur: 8px;
|
--dynamic-shadow-blur: 8px;
|
||||||
--dynamic-shadow-spread: 0px;
|
--dynamic-shadow-spread: 0px;
|
||||||
--shadow-intensity: 0.5;
|
--shadow-intensity: 0.5; // Controls shadow opacity based on distance from light
|
||||||
|
|
||||||
// Shine effect positioning
|
|
||||||
--shine-pos-x: 50%;
|
|
||||||
--shine-pos-y: 50%;
|
|
||||||
|
|
||||||
// Calculated shadow colors with intensity
|
|
||||||
--dynamic-shadow-color: rgba(0, 0, 0, calc(0.3 * var(--shadow-intensity)));
|
--dynamic-shadow-color: rgba(0, 0, 0, calc(0.3 * var(--shadow-intensity)));
|
||||||
--dynamic-shadow-color-intense: rgba(0, 0, 0, calc(0.5 * var(--shadow-intensity)));
|
--dynamic-shadow-color-intense: rgba(0, 0, 0, calc(0.5 * var(--shadow-intensity)));
|
||||||
--dynamic-shadow-color-button: rgba(0, 0, 0, calc(0.15 * var(--shadow-intensity)));
|
--dynamic-shadow-color-button: rgba(0, 0, 0, calc(0.15 * var(--shadow-intensity)));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user