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="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">
|
||||
@ -14,13 +14,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<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"
|
||||
role="main"
|
||||
aria-label="Login form">
|
||||
<div class="shine" aria-hidden="true"></div>
|
||||
<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">
|
||||
<div class="shine"></div>
|
||||
<span>
|
||||
<div class="logo-container">
|
||||
<div class="card-title text-center">
|
||||
@ -33,5 +31,7 @@
|
||||
<ng-content select="[body]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@ -2,7 +2,6 @@
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
// Background and overlay layers
|
||||
.gradient-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -10,10 +9,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
|
||||
// Performance optimizations
|
||||
will-change: auto; // Let browser decide
|
||||
contain: layout style paint; // CSS containment for better performance
|
||||
}
|
||||
|
||||
.gradient-overlay {
|
||||
@ -22,13 +17,11 @@
|
||||
left: 0;
|
||||
width: 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;
|
||||
pointer-events: none;
|
||||
contain: layout style; // Lighter containment for overlay
|
||||
}
|
||||
|
||||
// Main login container
|
||||
.login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -40,7 +33,6 @@
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
|
||||
// Typography
|
||||
h2 {
|
||||
font-family: "Spartan", sans-serif;
|
||||
font-size: 1.5rem;
|
||||
@ -51,140 +43,65 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Logo container
|
||||
.logo-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.logo {
|
||||
width: var(--login-logo-width, 55px);
|
||||
height: var(--login-logo-height, 55px);
|
||||
background-image: var(--login-logo-image, url("/assets/images/logo.png"));
|
||||
background-size: var(--login-logo-bg-size, contain);
|
||||
background-repeat: var(--login-logo-bg-repeat, no-repeat);
|
||||
width: var(--login-logo-width);
|
||||
height: var(--login-logo-height);
|
||||
background-image: var(--login-logo-image);
|
||||
background-size: var(--login-logo-bg-size);
|
||||
background-repeat: var(--login-logo-bg-repeat);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
// Login container
|
||||
.login-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Enhanced card with performance optimizations
|
||||
.card {
|
||||
color: #fff;
|
||||
max-width: 300px;
|
||||
border-width: var(--login-card-border-width, 1px);
|
||||
border-style: var(--login-card-border-style, solid);
|
||||
border-color: var(--login-card-border-color, rgba(255, 255, 255, 0.03));
|
||||
background: var(--login-card-bg, rgba(255, 255, 255, 0.1));
|
||||
backdrop-filter: var(--login-card-backdrop-filter, blur(6.7px));
|
||||
-webkit-backdrop-filter: var(--login-card-backdrop-filter, blur(6.7px));
|
||||
|
||||
// Enhanced shadows with dynamic system
|
||||
box-shadow:
|
||||
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;
|
||||
border-width: var(--login-card-border-width);
|
||||
border-style: var(--login-card-border-style);
|
||||
border-color: var(--login-card-border-color);
|
||||
background: var(--login-card-bg);
|
||||
box-shadow: var(--login-card-box-shadow);
|
||||
backdrop-filter: var(--login-card-backdrop-filter);
|
||||
-webkit-backdrop-filter: var(--login-card-backdrop-filter);
|
||||
transform-style: preserve-3d; /* Ensures child elements also participate in 3D */
|
||||
transition: transform 0.1s ease-out;
|
||||
transform-origin: center center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Optimized transitions
|
||||
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
|
||||
// Input fields with dynamic shadows
|
||||
::ng-deep input,
|
||||
::ng-deep .form-control {
|
||||
box-shadow:
|
||||
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);
|
||||
|
||||
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;
|
||||
transition: box-shadow 0.1s ease-out, border-color 0.15s ease-in-out;
|
||||
|
||||
&:focus {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
box-shadow:
|
||||
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);
|
||||
|
||||
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;
|
||||
transition: transform 0.15s ease, box-shadow 0.1s ease-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
@ -199,12 +116,6 @@
|
||||
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);
|
||||
}
|
||||
|
||||
// Focus states for accessibility
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--primary-color, #4ac694);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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,
|
||||
::ng-deep .form-control,
|
||||
::ng-deep .btn {
|
||||
transition: none;
|
||||
&:focus {
|
||||
border: 2px solid white;
|
||||
|
||||
}
|
||||
|
||||
|
||||
::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 { AsyncPipe, NgStyle } from "@angular/common";
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
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;
|
||||
}
|
||||
import {ChangeDetectionStrategy, Component, HostListener, inject, OnInit, OnDestroy} from '@angular/core';
|
||||
import {AsyncPipe, NgStyle} from "@angular/common";
|
||||
import {NavService} from "../../../_services/nav.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-splash-container',
|
||||
templateUrl: './splash-container.component.html',
|
||||
styleUrls: ['./splash-container.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgStyle, AsyncPipe],
|
||||
standalone: true
|
||||
imports: [
|
||||
NgStyle,
|
||||
AsyncPipe
|
||||
]
|
||||
})
|
||||
export class SplashContainerComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
// Dependency injection
|
||||
export class SplashContainerComponent implements OnInit, OnDestroy {
|
||||
protected readonly navService = inject(NavService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||
private maxTilt = 5; // Maximum tilt angle in degrees
|
||||
private animationId?: number;
|
||||
private resizeHandler?: () => void;
|
||||
private tiltElement?: HTMLElement;
|
||||
private mouseMoveThrottleId?: number;
|
||||
private lastMouseEvent?: MouseEvent;
|
||||
|
||||
// ViewChild for safer DOM access
|
||||
@ViewChild('gradientCanvas', { static: true })
|
||||
private canvasRef?: ElementRef<HTMLCanvasElement>;
|
||||
ngOnInit() {
|
||||
this.initGradientAnimationWithCssVars();
|
||||
this.cacheTiltElement();
|
||||
}
|
||||
|
||||
@ViewChild('tiltCard', { static: true })
|
||||
private tiltCardRef?: ElementRef<HTMLElement>;
|
||||
|
||||
// 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 };
|
||||
ngOnDestroy() {
|
||||
if (this.animationId !== undefined) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
}
|
||||
|
||||
return this.calculateTiltEffect(mouse);
|
||||
});
|
||||
|
||||
// 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) {
|
||||
if (this.mouseMoveThrottleId !== undefined) {
|
||||
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.updateMousePosition(event);
|
||||
this.updateTiltEffects();
|
||||
if (this.lastMouseEvent) {
|
||||
this.handleMouseMove(this.lastMouseEvent);
|
||||
}
|
||||
this.mouseMoveThrottleId = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize gradient colors from CSS variables with proper fallbacks
|
||||
*/
|
||||
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
|
||||
];
|
||||
private handleMouseMove(event: MouseEvent) {
|
||||
if (!this.tiltElement) return;
|
||||
|
||||
this.gradientPoints = [
|
||||
{ x: 0.2, y: 0.2, vx: 0.001, vy: 0.0015, color: this.getCssColorOrDefault('--gradient-color-1', defaultColors[0]) },
|
||||
{ x: 0.8, y: 0.3, vx: -0.0015, vy: 0.001, color: this.getCssColorOrDefault('--gradient-color-2', defaultColors[1]) },
|
||||
{ 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]) }
|
||||
];
|
||||
const elementBounds = this.tiltElement.getBoundingClientRect();
|
||||
const mouseX = event.clientX;
|
||||
const mouseY = event.clientY;
|
||||
|
||||
// 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)`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely initialize canvas with proper error handling
|
||||
*/
|
||||
private initializeCanvas(): void {
|
||||
const canvas = this.canvasRef?.nativeElement;
|
||||
private getCssVariable(variableName: string, element?: HTMLElement): string | null {
|
||||
const targetElement = element || document.documentElement;
|
||||
const value = getComputedStyle(targetElement).getPropertyValue(variableName).trim();
|
||||
|
||||
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) {
|
||||
console.warn('Canvas element not found, gradient animation disabled');
|
||||
return;
|
||||
return; // Exit gracefully if canvas element doesn't exist
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d', {
|
||||
alpha: false,
|
||||
desynchronized: true // Better performance for animations
|
||||
});
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
console.warn('Unable to get 2D context, gradient animation disabled');
|
||||
return;
|
||||
if (!ctx) {
|
||||
return; // Exit gracefully if context cannot be obtained
|
||||
}
|
||||
|
||||
this.canvasContext = context;
|
||||
this.setupCanvasOptimizations();
|
||||
this.resizeCanvas();
|
||||
}
|
||||
// Firefox-specific optimizations for smoother gradients
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
|
||||
/**
|
||||
* Apply performance optimizations to canvas context
|
||||
*/
|
||||
private setupCanvasOptimizations(): void {
|
||||
if (!this.canvasContext) return;
|
||||
// Set canvas size
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
// Firefox-specific optimizations
|
||||
this.canvasContext.imageSmoothingEnabled = true;
|
||||
this.canvasContext.imageSmoothingQuality = 'high';
|
||||
this.resizeHandler = resizeCanvas;
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', this.resizeHandler);
|
||||
|
||||
const canvas = this.canvasRef!.nativeElement;
|
||||
// Apply Firefox-specific CSS smoothing
|
||||
canvas.style.imageRendering = 'auto';
|
||||
canvas.style.backfaceVisibility = 'hidden';
|
||||
canvas.style.perspective = '1000px';
|
||||
canvas.style.transform = 'translateZ(0)';
|
||||
canvas.style.willChange = 'transform';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize ResizeObserver for better performance than window resize events
|
||||
*/
|
||||
private initializeResizeObserver(): void {
|
||||
if (!('ResizeObserver' in window) || !this.canvasRef?.nativeElement) {
|
||||
// Fallback to window resize for older browsers
|
||||
this.initializeFallbackResize();
|
||||
return;
|
||||
}
|
||||
// Get colors from CSS variables
|
||||
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 };
|
||||
const gradientColor3 = this.getCssVariableAsRgb('--gradient-color-3') || { r: 255, g: 215, b: 0 };
|
||||
const gradientColor4 = this.getCssVariableAsRgb('--gradient-color-4') || { r: 255, g: 20, b: 147 };
|
||||
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === this.canvasRef!.nativeElement) {
|
||||
this.resizeCanvas();
|
||||
break;
|
||||
}
|
||||
// Gradient points configuration with CSS variable colors
|
||||
const gradientPoints = [
|
||||
{
|
||||
x: 0.2,
|
||||
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);
|
||||
|
||||
/**
|
||||
* Fallback resize handling for older browsers
|
||||
*/
|
||||
private initializeFallbackResize(): void {
|
||||
const resizeHandler = () => this.resizeCanvas();
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
// Update point positions
|
||||
gradientPoints.forEach(point => {
|
||||
point.x += point.vx;
|
||||
point.y += point.vy;
|
||||
|
||||
// Cleanup using takeUntilDestroyed
|
||||
this.destroyRef.onDestroy(() => {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
});
|
||||
}
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Resize canvas with device pixel ratio consideration
|
||||
*/
|
||||
private resizeCanvas(): void {
|
||||
const canvas = this.canvasRef?.nativeElement;
|
||||
if (!canvas || !this.canvasContext) return;
|
||||
// Keep within 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));
|
||||
});
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
// Create gradients for each point with more color stops for smoother transitions
|
||||
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)
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
gradient.addColorStop(0, `rgba(${point.color.r}, ${point.color.g}, ${point.color.b}, 0.15)`);
|
||||
gradient.addColorStop(0.2, `rgba(${point.color.r}, ${point.color.g}, ${point.color.b}, 0.12)`);
|
||||
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
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
|
||||
// Scale the drawing context so everything draws at the correct size
|
||||
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);
|
||||
this.animationId = requestAnimationFrame(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 {
|
||||
// Remove hash and whitespace
|
||||
hex = hex.replace(/^#/, '').trim();
|
||||
// Remove the hash if present
|
||||
hex = hex.replace('#', '');
|
||||
|
||||
// Handle 3-digit hex
|
||||
// Handle 3-digit hex codes (e.g., #fff -> #ffffff)
|
||||
if (hex.length === 3) {
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to RGB
|
||||
return {
|
||||
r: parseInt(hex.substring(0, 2), 16),
|
||||
g: parseInt(hex.substring(2, 4), 16),
|
||||
b: parseInt(hex.substring(4, 6), 16)
|
||||
};
|
||||
}
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
return { r, g, b };
|
||||
}
|
||||
}
|
@ -9,18 +9,12 @@
|
||||
--gradient-color-4: #ff1493; /* Fourth gradient orb (left-center) */
|
||||
--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-y: 0px;
|
||||
--dynamic-shadow-blur: 8px;
|
||||
--dynamic-shadow-spread: 0px;
|
||||
--shadow-intensity: 0.5;
|
||||
|
||||
// Shine effect positioning
|
||||
--shine-pos-x: 50%;
|
||||
--shine-pos-y: 50%;
|
||||
|
||||
// Calculated shadow colors with intensity
|
||||
--shadow-intensity: 0.5; // Controls shadow opacity based on distance from light
|
||||
--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-button: rgba(0, 0, 0, calc(0.15 * var(--shadow-intensity)));
|
||||
|
Loading…
x
Reference in New Issue
Block a user