Revert "More performance enhancements, polishing"

This reverts commit 6ca8c1fd8e165f40780e7e2eb11e49d0343c644e.
This commit is contained in:
Robbie Davis 2025-08-01 22:58:59 -04:00
parent 6ca8c1fd8e
commit 334e636cf2
5 changed files with 289 additions and 984 deletions

View File

@ -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)
};
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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 };
}
}

View File

@ -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)));