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:
95
src/components/fl-blob/fl-blob.component.ts
Normal file
95
src/components/fl-blob/fl-blob.component.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
Component, ChangeDetectionStrategy, input, inject,
|
||||
ElementRef, afterNextRender, DestroyRef, signal,
|
||||
} from '@angular/core';
|
||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
import { AnimationLoopService } from '../../services/animation-loop.service';
|
||||
import { generateBlobPath } from '../../utils/svg.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'fl-blob',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<svg
|
||||
class="fl-blob__svg"
|
||||
[attr.viewBox]="viewBox()"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="blob-gradient-{{id}}" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
@for (color of colors(); track $index) {
|
||||
<stop
|
||||
[attr.offset]="($index / (colors().length - 1)) * 100 + '%'"
|
||||
[attr.stop-color]="color"
|
||||
/>
|
||||
}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
[attr.d]="blobPath()"
|
||||
[attr.fill]="'url(#blob-gradient-' + id + ')'"
|
||||
/>
|
||||
</svg>
|
||||
`,
|
||||
styleUrl: './fl-blob.component.scss',
|
||||
host: {
|
||||
'class': 'fl-blob',
|
||||
},
|
||||
})
|
||||
export class FlBlobComponent {
|
||||
private el = inject(ElementRef);
|
||||
private reducedMotion = inject(ReducedMotionService);
|
||||
private animationLoop = inject(AnimationLoopService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
// Inputs
|
||||
readonly colors = input<string[]>(['#3b82f6', '#8b5cf6']);
|
||||
readonly speed = input(1);
|
||||
readonly complexity = input(8);
|
||||
readonly size = input(200);
|
||||
|
||||
// Internal
|
||||
private elapsed = 0;
|
||||
private loopCleanup: (() => void) | null = null;
|
||||
protected readonly id = `blob-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
// Signals
|
||||
readonly blobPath = signal('');
|
||||
readonly viewBox = signal('0 0 200 200');
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.viewBox.set(`0 0 ${this.size()} ${this.size()}`);
|
||||
|
||||
if (this.reducedMotion.reduced()) {
|
||||
this.updateBlob(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = `blob-${Math.random().toString(36).slice(2)}`;
|
||||
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.loopCleanup?.();
|
||||
});
|
||||
}
|
||||
|
||||
private tick(delta: number): void {
|
||||
this.elapsed += delta * this.speed();
|
||||
this.updateBlob(this.elapsed);
|
||||
}
|
||||
|
||||
private updateBlob(phase: number): void {
|
||||
const size = this.size();
|
||||
const cx = size / 2;
|
||||
const cy = size / 2;
|
||||
const radius = size * 0.35;
|
||||
const variance = size * 0.1;
|
||||
const points = this.complexity();
|
||||
|
||||
const path = generateBlobPath(cx, cy, radius, points, variance, phase);
|
||||
this.blobPath.set(path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user