import { Component, ChangeDetectionStrategy, input, inject, ElementRef, afterNextRender, DestroyRef, contentChildren, } from '@angular/core'; import { ReducedMotionService } from '../../services/reduced-motion.service'; import { CursorTrackerService } from '../../services/cursor-tracker.service'; import { AnimationLoopService } from '../../services/animation-loop.service'; import { lerp, mapRange, clamp } from '../../utils/math.utils'; @Component({ selector: 'fl-floating-layers', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ``, styleUrl: './fl-floating-layers.component.scss', host: { 'class': 'fl-floating-layers' }, }) export class FlFloatingLayersComponent { private el = inject(ElementRef); private reducedMotion = inject(ReducedMotionService); private cursorTracker = inject(CursorTrackerService); private animationLoop = inject(AnimationLoopService); private destroyRef = inject(DestroyRef); // Inputs readonly maxOffset = input(30); readonly ease = input(0.05); readonly inverted = input(false); private currentX = 0; private currentY = 0; private cursorCleanup: (() => void) | null = null; private loopCleanup: (() => void) | null = null; constructor() { afterNextRender(() => { if (this.reducedMotion.reduced()) return; this.cursorCleanup = this.cursorTracker.subscribe(); const id = `floating-layers-${Math.random().toString(36).slice(2)}`; this.loopCleanup = this.animationLoop.register(id, () => this.tick()); }); this.destroyRef.onDestroy(() => { this.cursorCleanup?.(); this.loopCleanup?.(); }); } private tick(): void { const host = this.el.nativeElement as HTMLElement; const rect = host.getBoundingClientRect(); const pos = this.cursorTracker.position(); const normX = mapRange( clamp(pos.x, rect.left, rect.right), rect.left, rect.right, -1, 1, ); const normY = mapRange( clamp(pos.y, rect.top, rect.bottom), rect.top, rect.bottom, -1, 1, ); const easeVal = this.ease(); this.currentX = lerp(this.currentX, normX, easeVal); this.currentY = lerp(this.currentY, normY, easeVal); const children = host.children; const max = this.maxOffset(); const inv = this.inverted() ? -1 : 1; for (let i = 0; i < children.length; i++) { const child = children[i] as HTMLElement; const depth = parseFloat(child.getAttribute('data-depth') ?? `${(i + 1) / children.length}`); const offsetX = this.currentX * max * depth * inv; const offsetY = this.currentY * max * depth * inv; child.style.transform = `translate(${offsetX}px, ${offsetY}px)`; } } }