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>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user