fix: improve demo performance and fix broken component animations
- Add IntersectionObserver-based visibility pausing to AnimationLoopService (registerWithElement) so canvas components auto-pause when off-screen - Fix voronoi-mesh: increase step size, add cell cache with invalidation - Fix particle-field: replace O(n²) connections with spatial grid partitioning - Fix ambient-light: remove GPU-killing blur filters, use inset -20% instead - Tune demo: reduce particle/seed counts, remove always-on floating-layers - Fix celebration components: move effect() inside afterNextRender so canvas is ready, pass [active]="true", render as fixed overlays instead of inline - Fix one-shot text effects (typewriter, scramble-reveal, counter-rollup): defer animation until element is visible via IntersectionObserver Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
34
demo/angular.json
Normal file
34
demo/angular.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"demo": {
|
||||||
|
"projectType": "application",
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": ["zone.js"],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"styles": ["src/styles.scss"],
|
||||||
|
"scripts": [],
|
||||||
|
"preserveSymlinks": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "demo:build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
demo/package.json
Normal file
25
demo/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "flair-elements-ui-demo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "ng serve --port 4203",
|
||||||
|
"build": "ng build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^19.1.0",
|
||||||
|
"@angular/common": "^19.1.0",
|
||||||
|
"@angular/compiler": "^19.1.0",
|
||||||
|
"@angular/core": "^19.1.0",
|
||||||
|
"@angular/platform-browser": "^19.1.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^19.1.0",
|
||||||
|
"@sda/flair-elements-ui": "file:../dist",
|
||||||
|
"@sda/base-ui": "file:../../base-ui/dist"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^19.1.0",
|
||||||
|
"@angular/cli": "^19.1.0",
|
||||||
|
"@angular/compiler-cli": "^19.1.0",
|
||||||
|
"typescript": "~5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
833
demo/src/app.component.ts
Normal file
833
demo/src/app.component.ts
Normal file
@@ -0,0 +1,833 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
// Scroll & Progress
|
||||||
|
FlScrollProgressComponent,
|
||||||
|
|
||||||
|
// Backgrounds & Fields
|
||||||
|
FlParticleFieldComponent,
|
||||||
|
FlFlowFieldComponent,
|
||||||
|
FlMatrixRainComponent,
|
||||||
|
FlAuroraComponent,
|
||||||
|
FlVoronoiMeshComponent,
|
||||||
|
FlFractalLandscapeComponent,
|
||||||
|
FlAmbientLightComponent,
|
||||||
|
FlFloatingLayersComponent,
|
||||||
|
|
||||||
|
// Text Effects
|
||||||
|
FlGradientTextComponent,
|
||||||
|
FlTypewriterComponent,
|
||||||
|
FlTextShimmerComponent,
|
||||||
|
FlGlitchTextComponent,
|
||||||
|
FlScrambleRevealComponent,
|
||||||
|
FlCounterRollupComponent,
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
FlTiltCardComponent,
|
||||||
|
FlHolographicCardComponent,
|
||||||
|
FlSpotlightCardComponent,
|
||||||
|
|
||||||
|
// Generative Art
|
||||||
|
FlBlobComponent,
|
||||||
|
FlGenerativePatternComponent,
|
||||||
|
FlGradientMeshComponent,
|
||||||
|
|
||||||
|
// Dividers
|
||||||
|
FlWaveDividerComponent,
|
||||||
|
FlGradientDividerComponent,
|
||||||
|
FlGlowDividerComponent,
|
||||||
|
|
||||||
|
// Celebrations
|
||||||
|
FlConfettiComponent,
|
||||||
|
FlFireworksComponent,
|
||||||
|
FlSparkleComponent,
|
||||||
|
FlEmojiRainComponent,
|
||||||
|
FlCheckmarkFlourishComponent,
|
||||||
|
|
||||||
|
// Patterns & Textures
|
||||||
|
FlDotGridComponent,
|
||||||
|
FlBlueprintGridComponent,
|
||||||
|
FlTopographicLinesComponent,
|
||||||
|
FlCircuitBoardComponent,
|
||||||
|
|
||||||
|
// Status Indicators
|
||||||
|
FlBreathingDotComponent,
|
||||||
|
FlLiveIndicatorComponent,
|
||||||
|
FlSignalStrengthComponent,
|
||||||
|
FlHeartbeatLineComponent,
|
||||||
|
|
||||||
|
// Directives
|
||||||
|
FlHoverLiftDirective,
|
||||||
|
FlMagneticDirective,
|
||||||
|
FlRippleDirective,
|
||||||
|
FlNeonGlowDirective,
|
||||||
|
FlCursorGlowDirective,
|
||||||
|
FlRevealOnScrollDirective,
|
||||||
|
FlPressSquishDirective,
|
||||||
|
FlTextGradientDirective,
|
||||||
|
} from '@sda/flair-elements-ui';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
// Scroll & Progress
|
||||||
|
FlScrollProgressComponent,
|
||||||
|
|
||||||
|
// Backgrounds & Fields
|
||||||
|
FlParticleFieldComponent,
|
||||||
|
FlFlowFieldComponent,
|
||||||
|
FlMatrixRainComponent,
|
||||||
|
FlAuroraComponent,
|
||||||
|
FlVoronoiMeshComponent,
|
||||||
|
FlFractalLandscapeComponent,
|
||||||
|
FlAmbientLightComponent,
|
||||||
|
|
||||||
|
// Text Effects
|
||||||
|
FlGradientTextComponent,
|
||||||
|
FlTypewriterComponent,
|
||||||
|
FlTextShimmerComponent,
|
||||||
|
FlGlitchTextComponent,
|
||||||
|
FlScrambleRevealComponent,
|
||||||
|
FlCounterRollupComponent,
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
FlTiltCardComponent,
|
||||||
|
FlHolographicCardComponent,
|
||||||
|
FlSpotlightCardComponent,
|
||||||
|
|
||||||
|
// Generative Art
|
||||||
|
FlBlobComponent,
|
||||||
|
FlGenerativePatternComponent,
|
||||||
|
FlGradientMeshComponent,
|
||||||
|
|
||||||
|
// Dividers
|
||||||
|
FlWaveDividerComponent,
|
||||||
|
FlGradientDividerComponent,
|
||||||
|
FlGlowDividerComponent,
|
||||||
|
|
||||||
|
// Celebrations
|
||||||
|
FlConfettiComponent,
|
||||||
|
FlFireworksComponent,
|
||||||
|
FlSparkleComponent,
|
||||||
|
FlEmojiRainComponent,
|
||||||
|
FlCheckmarkFlourishComponent,
|
||||||
|
|
||||||
|
// Patterns & Textures
|
||||||
|
FlDotGridComponent,
|
||||||
|
FlBlueprintGridComponent,
|
||||||
|
FlTopographicLinesComponent,
|
||||||
|
FlCircuitBoardComponent,
|
||||||
|
|
||||||
|
// Status Indicators
|
||||||
|
FlBreathingDotComponent,
|
||||||
|
FlLiveIndicatorComponent,
|
||||||
|
FlSignalStrengthComponent,
|
||||||
|
FlHeartbeatLineComponent,
|
||||||
|
|
||||||
|
// Directives
|
||||||
|
FlHoverLiftDirective,
|
||||||
|
FlMagneticDirective,
|
||||||
|
FlRippleDirective,
|
||||||
|
FlNeonGlowDirective,
|
||||||
|
FlCursorGlowDirective,
|
||||||
|
FlRevealOnScrollDirective,
|
||||||
|
FlPressSquishDirective,
|
||||||
|
FlTextGradientDirective,
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<div class="showcase">
|
||||||
|
<!-- Ambient Background Effects -->
|
||||||
|
<fl-ambient-light class="ambient-bg" [opacity]="0.15" [blur]="60"></fl-ambient-light>
|
||||||
|
|
||||||
|
<!-- 1. HERO SECTION -->
|
||||||
|
<fl-scroll-progress class="scroll-progress"></fl-scroll-progress>
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<fl-particle-field class="hero-particles" [count]="50"></fl-particle-field>
|
||||||
|
<div class="hero-content">
|
||||||
|
<fl-gradient-text
|
||||||
|
[colors]="['#ff0080', '#7928ca', '#0070f3']"
|
||||||
|
class="hero-title">
|
||||||
|
Flair Elements UI
|
||||||
|
</fl-gradient-text>
|
||||||
|
<fl-typewriter
|
||||||
|
[text]="'Stunning visual effects and animations for modern web applications'"
|
||||||
|
[speed]="50"
|
||||||
|
class="hero-subtitle">
|
||||||
|
</fl-typewriter>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-wave-divider></fl-wave-divider>
|
||||||
|
|
||||||
|
<!-- 2. INTERACTIVE CARDS -->
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Interactive Cards</h2>
|
||||||
|
<div class="cards-grid">
|
||||||
|
<fl-tilt-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>Tilt Card</h3>
|
||||||
|
<p>3D tilt effect that follows your cursor</p>
|
||||||
|
</div>
|
||||||
|
</fl-tilt-card>
|
||||||
|
|
||||||
|
<fl-holographic-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>Holographic Card</h3>
|
||||||
|
<p>Iridescent holographic shimmer effect</p>
|
||||||
|
</div>
|
||||||
|
</fl-holographic-card>
|
||||||
|
|
||||||
|
<fl-spotlight-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>Spotlight Card</h3>
|
||||||
|
<p>Dynamic spotlight that follows your mouse</p>
|
||||||
|
</div>
|
||||||
|
</fl-spotlight-card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-gradient-divider [colors]="['#ff0080', '#7928ca']"></fl-gradient-divider>
|
||||||
|
|
||||||
|
<!-- 3. TEXT EFFECTS -->
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Text Effects</h2>
|
||||||
|
<div class="text-effects">
|
||||||
|
<div class="text-effect-item">
|
||||||
|
<label>Gradient Text:</label>
|
||||||
|
<fl-gradient-text
|
||||||
|
[colors]="['#f093fb', '#f5576c']">
|
||||||
|
Beautiful Gradients
|
||||||
|
</fl-gradient-text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-effect-item">
|
||||||
|
<label>Shimmer:</label>
|
||||||
|
<fl-text-shimmer>Shimmering Effect</fl-text-shimmer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-effect-item">
|
||||||
|
<label>Glitch:</label>
|
||||||
|
<fl-glitch-text [text]="'Glitch Effect'"></fl-glitch-text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-effect-item">
|
||||||
|
<label>Scramble Reveal:</label>
|
||||||
|
<fl-scramble-reveal [text]="'Decoded Message'"></fl-scramble-reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-effect-item">
|
||||||
|
<label>Counter:</label>
|
||||||
|
<fl-counter-rollup
|
||||||
|
[value]="9999"
|
||||||
|
[duration]="2000">
|
||||||
|
</fl-counter-rollup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-glow-divider [color]="'#0070f3'"></fl-glow-divider>
|
||||||
|
|
||||||
|
<!-- 4. CANVAS BACKGROUNDS -->
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Canvas Backgrounds</h2>
|
||||||
|
<div class="canvas-grid">
|
||||||
|
<div class="canvas-box">
|
||||||
|
<fl-flow-field [particleCount]="400"></fl-flow-field>
|
||||||
|
<span class="canvas-label">Flow Field</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas-box">
|
||||||
|
<fl-matrix-rain></fl-matrix-rain>
|
||||||
|
<span class="canvas-label">Matrix Rain</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas-box">
|
||||||
|
<fl-aurora></fl-aurora>
|
||||||
|
<span class="canvas-label">Aurora</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas-box">
|
||||||
|
<fl-voronoi-mesh [seedCount]="15"></fl-voronoi-mesh>
|
||||||
|
<span class="canvas-label">Voronoi Mesh</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas-box">
|
||||||
|
<fl-fractal-landscape></fl-fractal-landscape>
|
||||||
|
<span class="canvas-label">Fractal Landscape</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-wave-divider></fl-wave-divider>
|
||||||
|
|
||||||
|
<!-- 5. GENERATIVE ART -->
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Generative Art</h2>
|
||||||
|
<div class="generative-grid">
|
||||||
|
<div class="generative-box">
|
||||||
|
<fl-blob></fl-blob>
|
||||||
|
<span class="canvas-label">Blob</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="generative-box">
|
||||||
|
<fl-generative-pattern></fl-generative-pattern>
|
||||||
|
<span class="canvas-label">Generative Pattern</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="generative-box">
|
||||||
|
<fl-gradient-mesh [points]="3" [blur]="40"></fl-gradient-mesh>
|
||||||
|
<span class="canvas-label">Gradient Mesh</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-gradient-divider [colors]="['#0070f3', '#00dfd8']"></fl-gradient-divider>
|
||||||
|
|
||||||
|
<!-- 7. CELEBRATIONS -->
|
||||||
|
<section class="section celebrations-section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Celebrations</h2>
|
||||||
|
<div class="celebrations">
|
||||||
|
<button (click)="triggerConfetti = true" flPressSquish class="celebration-btn">
|
||||||
|
Confetti
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button (click)="triggerFireworks = true" flPressSquish class="celebration-btn">
|
||||||
|
Fireworks
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button (click)="triggerSparkle = true" flPressSquish class="celebration-btn">
|
||||||
|
Sparkle
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button (click)="triggerEmoji = true" flPressSquish class="celebration-btn">
|
||||||
|
Emoji Rain
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button (click)="triggerCheckmark = true" flPressSquish class="celebration-btn">
|
||||||
|
Checkmark
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (triggerConfetti) {
|
||||||
|
<fl-confetti class="celebration-overlay" [active]="true"></fl-confetti>
|
||||||
|
}
|
||||||
|
@if (triggerFireworks) {
|
||||||
|
<fl-fireworks class="celebration-overlay" [active]="true"></fl-fireworks>
|
||||||
|
}
|
||||||
|
@if (triggerSparkle) {
|
||||||
|
<fl-sparkle class="celebration-overlay" [active]="true"></fl-sparkle>
|
||||||
|
}
|
||||||
|
@if (triggerEmoji) {
|
||||||
|
<fl-emoji-rain class="celebration-overlay" [active]="true" [emojis]="['🎉', '🎊', '⭐', '💫']"></fl-emoji-rain>
|
||||||
|
}
|
||||||
|
@if (triggerCheckmark) {
|
||||||
|
<fl-checkmark-flourish class="celebration-overlay" [active]="true"></fl-checkmark-flourish>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-glow-divider [color]="'#7928ca'"></fl-glow-divider>
|
||||||
|
|
||||||
|
<!-- 8. DIRECTIVES -->
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Interactive Directives</h2>
|
||||||
|
<div class="directives-grid">
|
||||||
|
<div class="directive-card" flHoverLift>
|
||||||
|
<h4>Hover Lift</h4>
|
||||||
|
<p>Elevates on hover</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directive-card" flMagnetic>
|
||||||
|
<h4>Magnetic</h4>
|
||||||
|
<p>Attracted to cursor</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directive-card" flRipple>
|
||||||
|
<h4>Ripple</h4>
|
||||||
|
<p>Click for ripple effect</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directive-card" flNeonGlow>
|
||||||
|
<h4>Neon Glow</h4>
|
||||||
|
<p>Glowing neon border</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directive-card" flCursorGlow>
|
||||||
|
<h4>Cursor Glow</h4>
|
||||||
|
<p>Glow follows cursor</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directive-card" flRevealOnScroll>
|
||||||
|
<h4>Reveal on Scroll</h4>
|
||||||
|
<p>Animates into view</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directive-card" flPressSquish>
|
||||||
|
<h4>Press Squish</h4>
|
||||||
|
<p>Click to squish</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directive-card">
|
||||||
|
<h4 flTextGradient>Text Gradient</h4>
|
||||||
|
<p>Gradient text directive</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-wave-divider></fl-wave-divider>
|
||||||
|
|
||||||
|
<!-- 9. PATTERNS & TEXTURES -->
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Patterns & Textures</h2>
|
||||||
|
<div class="patterns-grid">
|
||||||
|
<div class="pattern-box">
|
||||||
|
<fl-dot-grid></fl-dot-grid>
|
||||||
|
<span class="canvas-label">Dot Grid</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pattern-box">
|
||||||
|
<fl-blueprint-grid></fl-blueprint-grid>
|
||||||
|
<span class="canvas-label">Blueprint Grid</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pattern-box">
|
||||||
|
<fl-topographic-lines></fl-topographic-lines>
|
||||||
|
<span class="canvas-label">Topographic Lines</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pattern-box">
|
||||||
|
<fl-circuit-board></fl-circuit-board>
|
||||||
|
<span class="canvas-label">Circuit Board</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-gradient-divider [colors]="['#ff0080', '#00dfd8']"></fl-gradient-divider>
|
||||||
|
|
||||||
|
<!-- 10. STATUS INDICATORS -->
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Status Indicators</h2>
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="status-item">
|
||||||
|
<fl-breathing-dot [color]="'#00ff00'"></fl-breathing-dot>
|
||||||
|
<span>Breathing Dot</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<fl-live-indicator></fl-live-indicator>
|
||||||
|
<span>Live Indicator</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<fl-signal-strength [level]="4"></fl-signal-strength>
|
||||||
|
<span>Signal Strength</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<fl-heartbeat-line [color]="'#ff0080'"></fl-heartbeat-line>
|
||||||
|
<span>Heartbeat Line</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fl-glow-divider [color]="'#ff0080'"></fl-glow-divider>
|
||||||
|
|
||||||
|
<!-- 11. AMBIENT -->
|
||||||
|
<section class="section final-section">
|
||||||
|
<h2 class="section-title" flRevealOnScroll>Ambient Effects</h2>
|
||||||
|
<p class="ambient-note">
|
||||||
|
Ambient Light is active throughout this entire showcase,
|
||||||
|
creating subtle atmospheric effects in the background.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<fl-gradient-text
|
||||||
|
[colors]="['#ff0080', '#7928ca', '#0070f3']">
|
||||||
|
Built with Flair Elements UI
|
||||||
|
</fl-gradient-text>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #ffffff;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase {
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient Backgrounds */
|
||||||
|
.ambient-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll Progress */
|
||||||
|
.scroll-progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-particles {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 5rem;
|
||||||
|
font-weight: 900;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Styles */
|
||||||
|
.section {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 6rem 2rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards Grid */
|
||||||
|
.cards-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 1rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p {
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text Effects */
|
||||||
|
.text-effects {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-effect-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-effect-item label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canvas Grid */
|
||||||
|
.canvas-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-box {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 10;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generative Grid */
|
||||||
|
.generative-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generative-box {
|
||||||
|
position: relative;
|
||||||
|
height: 350px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Celebrations */
|
||||||
|
.celebrations-section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.celebrations {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.celebration-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.celebration-btn {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.celebration-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directives Grid */
|
||||||
|
.directives-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directive-card {
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directive-card h4 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directive-card p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directive-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Patterns Grid */
|
||||||
|
.patterns-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-box {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Grid */
|
||||||
|
.status-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 3rem;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item span {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Final Section */
|
||||||
|
.final-section {
|
||||||
|
min-height: 50vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ambient-note {
|
||||||
|
max-width: 700px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
opacity: 0.8;
|
||||||
|
padding: 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-grid,
|
||||||
|
.canvas-grid,
|
||||||
|
.generative-grid,
|
||||||
|
.directives-grid,
|
||||||
|
.patterns-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-effect-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-effect-item label {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
triggerConfetti = false;
|
||||||
|
triggerFireworks = false;
|
||||||
|
triggerSparkle = false;
|
||||||
|
triggerEmoji = false;
|
||||||
|
triggerCheckmark = false;
|
||||||
|
}
|
||||||
13
demo/src/index.html
Normal file
13
demo/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Flair Elements UI Demo</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="data:,">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
demo/src/main.ts
Normal file
6
demo/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, {
|
||||||
|
providers: []
|
||||||
|
}).catch((err) => console.error(err));
|
||||||
32
demo/src/styles.scss
Normal file
32
demo/src/styles.scss
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@use '@sda/flair-elements-ui/src/styles' as flair;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #ffffff;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0ea5e9;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
13
demo/tsconfig.app.json
Normal file
13
demo/tsconfig.app.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
31
demo/tsconfig.json
Normal file
31
demo/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,22 +10,18 @@
|
|||||||
.fl-ambient-light__layer-2,
|
.fl-ambient-light__layer-2,
|
||||||
.fl-ambient-light__layer-3 {
|
.fl-ambient-light__layer-3 {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: -20%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
will-change: background;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fl-ambient-light__layer-1 {
|
.fl-ambient-light__layer-1 {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
filter: blur(60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fl-ambient-light__layer-2 {
|
.fl-ambient-light__layer-2 {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
filter: blur(80px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fl-ambient-light__layer-3 {
|
.fl-ambient-light__layer-3 {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
filter: blur(100px);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class FlAmbientLightComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = `ambient-light-${Math.random().toString(36).slice(2)}`;
|
const id = `ambient-light-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, this.el.nativeElement, (delta) => this.tick(delta));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class FlAuroraComponent {
|
|||||||
this.renderStaticState();
|
this.renderStaticState();
|
||||||
} else {
|
} else {
|
||||||
const id = `aurora-${Math.random().toString(36).slice(2)}`;
|
const id = `aurora-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (_, elapsed) => this.tick(elapsed));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, svg, (_, elapsed) => this.tick(elapsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ready.emit();
|
this.ready.emit();
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ export class FlConfettiComponent {
|
|||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
|
|
||||||
window.addEventListener('resize', () => this.resizeCanvas());
|
window.addEventListener('resize', () => this.resizeCanvas());
|
||||||
});
|
|
||||||
|
|
||||||
// Watch for active changes
|
// Watch for active changes (must be after canvas is ready)
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const isActive = this.active();
|
const isActive = this.active();
|
||||||
if (isActive && !this.isAnimating) {
|
if (isActive && !this.isAnimating) {
|
||||||
this.trigger();
|
this.trigger();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
this.loopCleanup?.();
|
this.loopCleanup?.();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Component, ChangeDetectionStrategy, input, inject,
|
Component, ChangeDetectionStrategy, input, inject,
|
||||||
afterNextRender, DestroyRef, signal, effect,
|
ElementRef, afterNextRender, DestroyRef, signal, effect,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import { ReducedMotionService } from '../../services/reduced-motion.service';
|
|||||||
host: { 'class': 'fl-counter-rollup' },
|
host: { 'class': 'fl-counter-rollup' },
|
||||||
})
|
})
|
||||||
export class FlCounterRollupComponent {
|
export class FlCounterRollupComponent {
|
||||||
|
private el = inject(ElementRef);
|
||||||
private reducedMotion = inject(ReducedMotionService);
|
private reducedMotion = inject(ReducedMotionService);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
@@ -32,24 +33,48 @@ export class FlCounterRollupComponent {
|
|||||||
private targetValue = 0;
|
private targetValue = 0;
|
||||||
private startTime = 0;
|
private startTime = 0;
|
||||||
private animationFrame: number | null = null;
|
private animationFrame: number | null = null;
|
||||||
|
private observer: IntersectionObserver | null = null;
|
||||||
|
private hasAnimated = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
afterNextRender(() => {
|
afterNextRender(() => {
|
||||||
|
if (this.reducedMotion.reduced()) {
|
||||||
|
this.targetValue = this.value();
|
||||||
|
this.currentValue = this.targetValue;
|
||||||
|
this.formattedValue.set(this.formatNumber(this.targetValue));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.targetValue = this.value();
|
||||||
|
this.observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0]?.isIntersecting && !this.hasAnimated) {
|
||||||
|
this.observer?.disconnect();
|
||||||
|
this.observer = null;
|
||||||
|
this.hasAnimated = true;
|
||||||
this.startAnimation();
|
this.startAnimation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
this.observer.observe(this.el.nativeElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
// React to value changes
|
// React to value changes after initial animation
|
||||||
const newValue = this.value();
|
const newValue = this.value();
|
||||||
this.startValue = this.currentValue;
|
this.startValue = this.currentValue;
|
||||||
this.targetValue = newValue;
|
this.targetValue = newValue;
|
||||||
|
if (this.hasAnimated) {
|
||||||
this.startAnimation();
|
this.startAnimation();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
if (this.animationFrame !== null) {
|
if (this.animationFrame !== null) {
|
||||||
cancelAnimationFrame(this.animationFrame);
|
cancelAnimationFrame(this.animationFrame);
|
||||||
}
|
}
|
||||||
|
this.observer?.disconnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,15 +66,15 @@ export class FlFireworksComponent {
|
|||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
|
|
||||||
window.addEventListener('resize', () => this.resizeCanvas());
|
window.addEventListener('resize', () => this.resizeCanvas());
|
||||||
});
|
|
||||||
|
|
||||||
// Watch for active changes
|
// Watch for active changes (must be after canvas is ready)
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const isActive = this.active();
|
const isActive = this.active();
|
||||||
if (isActive && !this.isAnimating) {
|
if (isActive && !this.isAnimating) {
|
||||||
this.trigger();
|
this.trigger();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
this.loopCleanup?.();
|
this.loopCleanup?.();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class FlFloatingLayersComponent {
|
|||||||
if (this.reducedMotion.reduced()) return;
|
if (this.reducedMotion.reduced()) return;
|
||||||
this.cursorCleanup = this.cursorTracker.subscribe();
|
this.cursorCleanup = this.cursorTracker.subscribe();
|
||||||
const id = `floating-layers-${Math.random().toString(36).slice(2)}`;
|
const id = `floating-layers-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, () => this.tick());
|
this.loopCleanup = this.animationLoop.registerWithElement(id, this.el.nativeElement, () => this.tick());
|
||||||
});
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class FlFlowFieldComponent {
|
|||||||
this.ctx.fillRect(0, 0, this.width, this.height);
|
this.ctx.fillRect(0, 0, this.width, this.height);
|
||||||
|
|
||||||
const id = `flow-field-${Math.random().toString(36).slice(2)}`;
|
const id = `flow-field-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, canvas, (delta) => this.tick(delta));
|
||||||
this.ready.emit();
|
this.ready.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class FlFractalLandscapeComponent {
|
|||||||
this.renderFrame(0);
|
this.renderFrame(0);
|
||||||
} else {
|
} else {
|
||||||
const id = `fractal-${Math.random().toString(36).slice(2)}`;
|
const id = `fractal-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (_, elapsed) => this.renderFrame(elapsed));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, canvas, (_, elapsed) => this.renderFrame(elapsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ready.emit();
|
this.ready.emit();
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export class FlGradientMeshComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = `gradient-mesh-${Math.random().toString(36).slice(2)}`;
|
const id = `gradient-mesh-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, this.el.nativeElement, (delta) => this.tick(delta));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export class FlMatrixRainComponent {
|
|||||||
this.initColumns();
|
this.initColumns();
|
||||||
|
|
||||||
const id = `matrix-rain-${Math.random().toString(36).slice(2)}`;
|
const id = `matrix-rain-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, canvas, (delta) => this.tick(delta));
|
||||||
this.ready.emit();
|
this.ready.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export class FlParticleFieldComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = `particle-field-${Math.random().toString(36).slice(2)}`;
|
const id = `particle-field-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, canvas, (delta) => this.tick(delta));
|
||||||
this.ready.emit();
|
this.ready.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,21 +192,68 @@ export class FlParticleFieldComponent {
|
|||||||
if (p.y > h + p.size) p.y = -p.size;
|
if (p.y > h + p.size) p.y = -p.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw connections
|
// Draw connections using spatial grid
|
||||||
if (useConnections) {
|
if (useConnections) {
|
||||||
|
const cellSize = connDist;
|
||||||
|
const gridCols = Math.ceil(w / cellSize) + 1;
|
||||||
|
const gridRows = Math.ceil(h / cellSize) + 1;
|
||||||
|
const grid = new Array(gridCols * gridRows);
|
||||||
|
for (let i = 0; i < grid.length; i++) grid[i] = [];
|
||||||
|
|
||||||
|
// Assign particles to grid cells
|
||||||
for (let i = 0; i < this.particles.length; i++) {
|
for (let i = 0; i < this.particles.length; i++) {
|
||||||
for (let j = i + 1; j < this.particles.length; j++) {
|
const p = this.particles[i];
|
||||||
|
const col = Math.floor(p.x / cellSize);
|
||||||
|
const row = Math.floor(p.y / cellSize);
|
||||||
|
if (col >= 0 && col < gridCols && row >= 0 && row < gridRows) {
|
||||||
|
grid[col * gridRows + row].push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxConnsPerParticle = 5;
|
||||||
|
const connCounts = new Uint8Array(this.particles.length);
|
||||||
|
const drawn = new Set<string>();
|
||||||
|
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
|
||||||
|
for (let col = 0; col < gridCols; col++) {
|
||||||
|
for (let row = 0; row < gridRows; row++) {
|
||||||
|
const cell = grid[col * gridRows + row];
|
||||||
|
if (cell.length === 0) continue;
|
||||||
|
|
||||||
|
// Check this cell and neighboring cells
|
||||||
|
for (let dc = -1; dc <= 1; dc++) {
|
||||||
|
for (let dr = -1; dr <= 1; dr++) {
|
||||||
|
const nc = col + dc;
|
||||||
|
const nr = row + dr;
|
||||||
|
if (nc < 0 || nc >= gridCols || nr < 0 || nr >= gridRows) continue;
|
||||||
|
const neighbor = grid[nc * gridRows + nr];
|
||||||
|
|
||||||
|
for (const i of cell) {
|
||||||
|
if (connCounts[i] >= maxConnsPerParticle) continue;
|
||||||
const a = this.particles[i];
|
const a = this.particles[i];
|
||||||
|
|
||||||
|
for (const j of neighbor) {
|
||||||
|
if (j <= i) continue; // avoid duplicates
|
||||||
|
if (connCounts[j] >= maxConnsPerParticle) continue;
|
||||||
|
|
||||||
const b = this.particles[j];
|
const b = this.particles[j];
|
||||||
const d = distance2D(a.x, a.y, b.x, b.y);
|
const dx = a.x - b.x;
|
||||||
|
const dy = a.y - b.y;
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy);
|
||||||
if (d < connDist) {
|
if (d < connDist) {
|
||||||
const alpha = (1 - d / connDist) * connOpacity;
|
const alpha = (1 - d / connDist) * connOpacity;
|
||||||
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
|
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(a.x, a.y);
|
ctx.moveTo(a.x, a.y);
|
||||||
ctx.lineTo(b.x, b.y);
|
ctx.lineTo(b.x, b.y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
connCounts[i]++;
|
||||||
|
connCounts[j]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Component, ChangeDetectionStrategy, input, inject,
|
Component, ChangeDetectionStrategy, input, inject,
|
||||||
afterNextRender, DestroyRef, signal,
|
ElementRef, afterNextRender, DestroyRef, signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import { ReducedMotionService } from '../../services/reduced-motion.service';
|
|||||||
host: { 'class': 'fl-scramble-reveal' },
|
host: { 'class': 'fl-scramble-reveal' },
|
||||||
})
|
})
|
||||||
export class FlScrambleRevealComponent {
|
export class FlScrambleRevealComponent {
|
||||||
|
private el = inject(ElementRef);
|
||||||
private reducedMotion = inject(ReducedMotionService);
|
private reducedMotion = inject(ReducedMotionService);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export class FlScrambleRevealComponent {
|
|||||||
private scrambleCount = 0;
|
private scrambleCount = 0;
|
||||||
private maxScrambles = 3;
|
private maxScrambles = 3;
|
||||||
private interval: any = null;
|
private interval: any = null;
|
||||||
|
private observer: IntersectionObserver | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
afterNextRender(() => {
|
afterNextRender(() => {
|
||||||
@@ -36,13 +38,24 @@ export class FlScrambleRevealComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0]?.isIntersecting) {
|
||||||
|
this.observer?.disconnect();
|
||||||
|
this.observer = null;
|
||||||
this.startScramble();
|
this.startScramble();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
this.observer.observe(this.el.nativeElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
this.observer?.disconnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Component, ChangeDetectionStrategy, input, inject,
|
Component, ChangeDetectionStrategy, input, inject,
|
||||||
afterNextRender, DestroyRef, signal,
|
ElementRef, afterNextRender, DestroyRef, signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ import { ReducedMotionService } from '../../services/reduced-motion.service';
|
|||||||
host: { 'class': 'fl-typewriter' },
|
host: { 'class': 'fl-typewriter' },
|
||||||
})
|
})
|
||||||
export class FlTypewriterComponent {
|
export class FlTypewriterComponent {
|
||||||
|
private el = inject(ElementRef);
|
||||||
private reducedMotion = inject(ReducedMotionService);
|
private reducedMotion = inject(ReducedMotionService);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export class FlTypewriterComponent {
|
|||||||
private typingInterval: any = null;
|
private typingInterval: any = null;
|
||||||
private cursorInterval: any = null;
|
private cursorInterval: any = null;
|
||||||
private isTyping = false;
|
private isTyping = false;
|
||||||
|
private observer: IntersectionObserver | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
afterNextRender(() => {
|
afterNextRender(() => {
|
||||||
@@ -45,12 +47,23 @@ export class FlTypewriterComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0]?.isIntersecting) {
|
||||||
|
this.observer?.disconnect();
|
||||||
|
this.observer = null;
|
||||||
setTimeout(() => this.startTyping(), this.delay());
|
setTimeout(() => this.startTyping(), this.delay());
|
||||||
this.startCursorBlink();
|
this.startCursorBlink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
this.observer.observe(this.el.nativeElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
this.observer?.disconnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ export class FlVoronoiMeshComponent {
|
|||||||
private pixelRatio = 1;
|
private pixelRatio = 1;
|
||||||
private loopCleanup: (() => void) | null = null;
|
private loopCleanup: (() => void) | null = null;
|
||||||
private resizeObserver: ResizeObserver | null = null;
|
private resizeObserver: ResizeObserver | null = null;
|
||||||
|
private cellCache: Uint8Array | null = null;
|
||||||
|
private lastSeedPositions: { x: number; y: number }[] = [];
|
||||||
|
private cacheStep = 4;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
afterNextRender(() => {
|
afterNextRender(() => {
|
||||||
@@ -87,7 +90,7 @@ export class FlVoronoiMeshComponent {
|
|||||||
this.createSeeds();
|
this.createSeeds();
|
||||||
|
|
||||||
const id = `voronoi-${Math.random().toString(36).slice(2)}`;
|
const id = `voronoi-${Math.random().toString(36).slice(2)}`;
|
||||||
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
|
this.loopCleanup = this.animationLoop.registerWithElement(id, canvas, (delta) => this.tick(delta));
|
||||||
this.ready.emit();
|
this.ready.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +109,7 @@ export class FlVoronoiMeshComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createSeeds(): void {
|
private createSeeds(): void {
|
||||||
|
this.cellCache = null;
|
||||||
const count = this.seedCount();
|
const count = this.seedCount();
|
||||||
const cols = this.colors();
|
const cols = this.colors();
|
||||||
const spd = this.speed();
|
const spd = this.speed();
|
||||||
@@ -121,6 +125,17 @@ export class FlVoronoiMeshComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private needsCacheInvalidation(): boolean {
|
||||||
|
if (!this.cellCache || this.lastSeedPositions.length !== this.seeds.length) return true;
|
||||||
|
const step = Math.max(4, Math.floor(6 / this.pixelRatio));
|
||||||
|
for (let i = 0; i < this.seeds.length; i++) {
|
||||||
|
const dx = this.seeds[i].x - this.lastSeedPositions[i].x;
|
||||||
|
const dy = this.seeds[i].y - this.lastSeedPositions[i].y;
|
||||||
|
if (dx * dx + dy * dy > step * step) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private tick(delta: number): void {
|
private tick(delta: number): void {
|
||||||
if (!this.ctx) return;
|
if (!this.ctx) return;
|
||||||
|
|
||||||
@@ -145,55 +160,65 @@ export class FlVoronoiMeshComponent {
|
|||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const w = this.width;
|
const w = this.width;
|
||||||
const h = this.height;
|
const h = this.height;
|
||||||
|
const step = Math.max(4, Math.floor(6 / this.pixelRatio));
|
||||||
// Use pixel-based approach for Voronoi (brute-force nearest-seed)
|
|
||||||
// For performance, use a step size
|
|
||||||
const step = Math.max(2, Math.floor(4 / this.pixelRatio));
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
for (let x = 0; x < w; x += step) {
|
const cols = Math.ceil(w / step);
|
||||||
for (let y = 0; y < h; y += step) {
|
const rows = Math.ceil(h / step);
|
||||||
let minDist = Infinity;
|
const totalCells = cols * rows;
|
||||||
let nearestColor = this.seeds[0]?.color ?? '#000';
|
|
||||||
|
|
||||||
for (const s of this.seeds) {
|
// Only recalculate cell assignments if seeds moved significantly
|
||||||
const d = (x - s.x) * (x - s.x) + (y - s.y) * (y - s.y);
|
if (this.needsCacheInvalidation()) {
|
||||||
|
this.cellCache = new Uint8Array(totalCells);
|
||||||
|
this.cacheStep = step;
|
||||||
|
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
const x = col * step;
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
const y = row * step;
|
||||||
|
let minDist = Infinity;
|
||||||
|
let nearestIdx = 0;
|
||||||
|
|
||||||
|
for (let s = 0; s < this.seeds.length; s++) {
|
||||||
|
const dx = x - this.seeds[s].x;
|
||||||
|
const dy = y - this.seeds[s].y;
|
||||||
|
const d = dx * dx + dy * dy;
|
||||||
if (d < minDist) {
|
if (d < minDist) {
|
||||||
minDist = d;
|
minDist = d;
|
||||||
nearestColor = s.color;
|
nearestIdx = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = nearestColor;
|
this.cellCache[col * rows + row] = nearestIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastSeedPositions = this.seeds.map(s => ({ x: s.x, y: s.y }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render from cache
|
||||||
|
const cache = this.cellCache!;
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
const x = col * step;
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
const y = row * step;
|
||||||
|
const seedIdx = cache[col * rows + row];
|
||||||
|
ctx.fillStyle = this.seeds[seedIdx]?.color ?? '#000';
|
||||||
ctx.fillRect(x, y, step, step);
|
ctx.fillRect(x, y, step, step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw edges using second-nearest distance comparison
|
// Draw edges only when showEdges is true
|
||||||
if (this.showEdges()) {
|
if (this.showEdges()) {
|
||||||
ctx.strokeStyle = this.edgeColor();
|
const edgeColor = this.edgeColor();
|
||||||
ctx.lineWidth = this.edgeWidth();
|
for (let col = 1; col < cols; col++) {
|
||||||
|
for (let row = 1; row < rows; row++) {
|
||||||
for (let x = 0; x < w; x += step) {
|
const idx = cache[col * rows + row];
|
||||||
for (let y = 0; y < h; y += step) {
|
// Check if neighboring cells have different seed assignments
|
||||||
let min1 = Infinity;
|
if (idx !== cache[(col - 1) * rows + row] || idx !== cache[col * rows + (row - 1)]) {
|
||||||
let min2 = Infinity;
|
ctx.fillStyle = edgeColor;
|
||||||
|
ctx.fillRect(col * step, row * step, step, step);
|
||||||
for (const s of this.seeds) {
|
|
||||||
const d = (x - s.x) * (x - s.x) + (y - s.y) * (y - s.y);
|
|
||||||
if (d < min1) {
|
|
||||||
min2 = min1;
|
|
||||||
min1 = d;
|
|
||||||
} else if (d < min2) {
|
|
||||||
min2 = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ratio = min1 / min2;
|
|
||||||
if (ratio > 0.95) {
|
|
||||||
ctx.fillStyle = this.edgeColor();
|
|
||||||
ctx.fillRect(x, y, step, step);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export class AnimationLoopService {
|
|||||||
private startTime = 0;
|
private startTime = 0;
|
||||||
private running = false;
|
private running = false;
|
||||||
|
|
||||||
|
/** Track which registered IDs are currently visible */
|
||||||
|
private visibilityMap = new Map<string, boolean>();
|
||||||
|
private observers = new Map<string, IntersectionObserver>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
this.stop();
|
this.stop();
|
||||||
@@ -42,6 +46,37 @@ export class AnimationLoopService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Register a callback tied to an element's visibility. Auto-pauses when off-screen. */
|
||||||
|
registerWithElement(id: string, element: Element, callback: (delta: number, elapsed: number) => void, priority = 0): () => void {
|
||||||
|
this.visibilityMap.set(id, false);
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
this.visibilityMap.set(id, entry.isIntersecting);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0 }
|
||||||
|
);
|
||||||
|
observer.observe(element);
|
||||||
|
this.observers.set(id, observer);
|
||||||
|
|
||||||
|
const wrappedCallback = (delta: number, elapsed: number): void => {
|
||||||
|
if (this.visibilityMap.get(id)) {
|
||||||
|
callback(delta, elapsed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseCleanup = this.register(id, wrappedCallback, priority);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
baseCleanup();
|
||||||
|
observer.disconnect();
|
||||||
|
this.observers.delete(id);
|
||||||
|
this.visibilityMap.delete(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private start(): void {
|
private start(): void {
|
||||||
if (this.running) return;
|
if (this.running) return;
|
||||||
if (this.reducedMotion.reduced()) return;
|
if (this.reducedMotion.reduced()) return;
|
||||||
|
|||||||
@@ -114,5 +114,5 @@
|
|||||||
|
|
||||||
@keyframes fl-float {
|
@keyframes fl-float {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0); }
|
||||||
50% { transform: translateY(-#{$distance}); }
|
50% { transform: translateY(-10px); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user