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,4 @@
|
||||
.fl-counter-trigger {
|
||||
display: inline-block;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
Component, ChangeDetectionStrategy, input, inject,
|
||||
ElementRef, afterNextRender, DestroyRef, signal,
|
||||
} from '@angular/core';
|
||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
import { ScrollObserverService } from '../../services/scroll-observer.service';
|
||||
|
||||
@Component({
|
||||
selector: 'fl-counter-trigger',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `{{ prefix() }}{{ formattedValue() }}{{ suffix() }}`,
|
||||
styleUrl: './fl-counter-trigger.component.scss',
|
||||
host: { 'class': 'fl-counter-trigger' },
|
||||
})
|
||||
export class FlCounterTriggerComponent {
|
||||
private el = inject(ElementRef);
|
||||
private reducedMotion = inject(ReducedMotionService);
|
||||
private scrollObserver = inject(ScrollObserverService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
// Inputs
|
||||
readonly value = input.required<number>();
|
||||
readonly duration = input(2000);
|
||||
readonly prefix = input('');
|
||||
readonly suffix = input('');
|
||||
|
||||
// Internal
|
||||
readonly formattedValue = signal('0');
|
||||
|
||||
private currentValue = 0;
|
||||
private startValue = 0;
|
||||
private targetValue = 0;
|
||||
private startTime = 0;
|
||||
private animationFrame: number | null = null;
|
||||
private hasTriggered = false;
|
||||
private observerCleanup: (() => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
const host = this.el.nativeElement as HTMLElement;
|
||||
|
||||
this.observerCleanup = this.scrollObserver.observe(
|
||||
host,
|
||||
(entry) => {
|
||||
if (entry.isIntersecting && !this.hasTriggered) {
|
||||
this.hasTriggered = true;
|
||||
this.startAnimation();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.observerCleanup?.();
|
||||
if (this.animationFrame !== null) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startAnimation(): void {
|
||||
this.startValue = 0;
|
||||
this.targetValue = this.value();
|
||||
|
||||
if (this.reducedMotion.reduced()) {
|
||||
this.currentValue = this.targetValue;
|
||||
this.formattedValue.set(this.formatNumber(this.targetValue));
|
||||
return;
|
||||
}
|
||||
|
||||
this.startTime = performance.now();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private animate = (): void => {
|
||||
const now = performance.now();
|
||||
const elapsed = now - this.startTime;
|
||||
const duration = this.duration();
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Ease out cubic
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
|
||||
this.currentValue = this.startValue + (this.targetValue - this.startValue) * eased;
|
||||
this.formattedValue.set(this.formatNumber(this.currentValue));
|
||||
|
||||
if (progress < 1) {
|
||||
this.animationFrame = requestAnimationFrame(this.animate);
|
||||
} else {
|
||||
this.currentValue = this.targetValue;
|
||||
this.formattedValue.set(this.formatNumber(this.targetValue));
|
||||
}
|
||||
};
|
||||
|
||||
private formatNumber(value: number): string {
|
||||
const fixed = Math.round(value).toString();
|
||||
return fixed.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
}
|
||||
1
src/components/fl-counter-trigger/index.ts
Normal file
1
src/components/fl-counter-trigger/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './fl-counter-trigger.component';
|
||||
Reference in New Issue
Block a user