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,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.fl-circuit-board__svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
157
src/components/fl-circuit-board/fl-circuit-board.component.ts
Normal file
157
src/components/fl-circuit-board/fl-circuit-board.component.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
Component, ChangeDetectionStrategy, input, inject,
|
||||
DestroyRef, afterNextRender, signal,
|
||||
} from '@angular/core';
|
||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
import { AnimationLoopService } from '../../services/animation-loop.service';
|
||||
|
||||
interface CircuitPath {
|
||||
d: string;
|
||||
pulseOffset: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'fl-circuit-board',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<svg
|
||||
class="fl-circuit-board__svg"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient [id]="pulseGradientId">
|
||||
<stop offset="0%" [attr.stop-color]="color()" stop-opacity="0" />
|
||||
<stop offset="50%" [attr.stop-color]="pulseColor()" stop-opacity="1" />
|
||||
<stop offset="100%" [attr.stop-color]="color()" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Circuit traces -->
|
||||
@for (circuit of circuits(); track $index) {
|
||||
<path
|
||||
[attr.d]="circuit.d"
|
||||
fill="none"
|
||||
[attr.stroke]="color()"
|
||||
stroke-width="0.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
}
|
||||
|
||||
<!-- Animated pulses -->
|
||||
@if (!reducedMotion.reduced()) {
|
||||
@for (circuit of circuits(); track $index) {
|
||||
<path
|
||||
[attr.d]="circuit.d"
|
||||
fill="none"
|
||||
[attr.stroke]="pulseGradientFill"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
[style.stroke-dasharray]="10"
|
||||
[style.stroke-dashoffset]="getPulseOffset($index, circuit.pulseOffset)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Connection nodes -->
|
||||
@for (node of nodes(); track $index) {
|
||||
<circle
|
||||
[attr.cx]="node.x"
|
||||
[attr.cy]="node.y"
|
||||
r="0.8"
|
||||
[attr.fill]="pulseColor()"
|
||||
/>
|
||||
}
|
||||
</svg>
|
||||
`,
|
||||
styleUrl: './fl-circuit-board.component.scss',
|
||||
host: { 'class': 'fl-circuit-board' },
|
||||
})
|
||||
export class FlCircuitBoardComponent {
|
||||
protected reducedMotion = inject(ReducedMotionService);
|
||||
private animationLoop = inject(AnimationLoopService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
// Inputs
|
||||
readonly color = input('#00ff88');
|
||||
readonly pulseColor = input('#00ffff');
|
||||
readonly speed = input(1);
|
||||
readonly density = input(0.5);
|
||||
|
||||
protected readonly circuits = signal<CircuitPath[]>([]);
|
||||
protected readonly nodes = signal<{ x: number; y: number }[]>([]);
|
||||
protected readonly pulseGradientId = `circuit-pulse-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
private loopCleanup: (() => void) | null = null;
|
||||
private time = 0;
|
||||
|
||||
protected get pulseGradientFill(): string {
|
||||
return `url(#${this.pulseGradientId})`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.generateCircuits();
|
||||
|
||||
if (!this.reducedMotion.reduced()) {
|
||||
const id = `circuit-board-${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.time += delta * this.speed();
|
||||
}
|
||||
|
||||
protected getPulseOffset(index: number, baseOffset: number): number {
|
||||
return (this.time * 50 + baseOffset) % 100;
|
||||
}
|
||||
|
||||
private generateCircuits(): void {
|
||||
const circuitPaths: CircuitPath[] = [];
|
||||
const nodeList: { x: number; y: number }[] = [];
|
||||
const pathCount = Math.floor(10 * this.density());
|
||||
|
||||
// Generate grid-based circuit paths
|
||||
for (let i = 0; i < pathCount; i++) {
|
||||
const startX = Math.random() * 100;
|
||||
const startY = Math.random() * 100;
|
||||
|
||||
const path: string[] = [`M ${startX} ${startY}`];
|
||||
let currentX = startX;
|
||||
let currentY = startY;
|
||||
|
||||
const segments = 3 + Math.floor(Math.random() * 4);
|
||||
|
||||
for (let j = 0; j < segments; j++) {
|
||||
const horizontal = Math.random() > 0.5;
|
||||
const distance = 10 + Math.random() * 20;
|
||||
|
||||
if (horizontal) {
|
||||
currentX += (Math.random() > 0.5 ? 1 : -1) * distance;
|
||||
currentX = Math.max(0, Math.min(100, currentX));
|
||||
} else {
|
||||
currentY += (Math.random() > 0.5 ? 1 : -1) * distance;
|
||||
currentY = Math.max(0, Math.min(100, currentY));
|
||||
}
|
||||
|
||||
path.push(`L ${currentX} ${currentY}`);
|
||||
nodeList.push({ x: currentX, y: currentY });
|
||||
}
|
||||
|
||||
circuitPaths.push({
|
||||
d: path.join(' '),
|
||||
pulseOffset: Math.random() * 100,
|
||||
});
|
||||
}
|
||||
|
||||
this.circuits.set(circuitPaths);
|
||||
this.nodes.set(nodeList);
|
||||
}
|
||||
}
|
||||
1
src/components/fl-circuit-board/index.ts
Normal file
1
src/components/fl-circuit-board/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './fl-circuit-board.component';
|
||||
Reference in New Issue
Block a user