This repository has been archived on 2026-06-18. You can view files and clone it, but cannot push or open issues or pull requests.
Files
flair-elements-ui/src/components/fl-floating-layers/fl-floating-layers.component.ts
Giuliano Silvestro daf6182e94 feat: add @sda/flair-elements-ui library
Angular 19+ standalone library with 61 components, 15 directives,
and 8 services for decorative visual effects, animations, and
micro-interactions.

Components include particle systems, flow fields, aurora effects,
tilt/holographic/spotlight cards, text effects, dividers, scroll
animations, generative art, celebrations, image treatments,
morphing shapes, terminal output, and pattern textures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:24:52 +10:00

80 lines
2.7 KiB
TypeScript

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: `<ng-content />`,
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)`;
}
}
}