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-3 {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inset: -20%;
|
||||
pointer-events: none;
|
||||
will-change: background;
|
||||
}
|
||||
|
||||
.fl-ambient-light__layer-1 {
|
||||
opacity: 0.5;
|
||||
filter: blur(60px);
|
||||
}
|
||||
|
||||
.fl-ambient-light__layer-2 {
|
||||
opacity: 0.4;
|
||||
filter: blur(80px);
|
||||
}
|
||||
|
||||
.fl-ambient-light__layer-3 {
|
||||
opacity: 0.3;
|
||||
filter: blur(100px);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class FlAmbientLightComponent {
|
||||
}
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -82,7 +82,7 @@ export class FlAuroraComponent {
|
||||
this.renderStaticState();
|
||||
} else {
|
||||
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();
|
||||
|
||||
@@ -59,14 +59,14 @@ export class FlConfettiComponent {
|
||||
this.resizeCanvas();
|
||||
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
});
|
||||
|
||||
// Watch for active changes
|
||||
effect(() => {
|
||||
const isActive = this.active();
|
||||
if (isActive && !this.isAnimating) {
|
||||
this.trigger();
|
||||
}
|
||||
// Watch for active changes (must be after canvas is ready)
|
||||
effect(() => {
|
||||
const isActive = this.active();
|
||||
if (isActive && !this.isAnimating) {
|
||||
this.trigger();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Component, ChangeDetectionStrategy, input, inject,
|
||||
afterNextRender, DestroyRef, signal, effect,
|
||||
ElementRef, afterNextRender, DestroyRef, signal, effect,
|
||||
} from '@angular/core';
|
||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
host: { 'class': 'fl-counter-rollup' },
|
||||
})
|
||||
export class FlCounterRollupComponent {
|
||||
private el = inject(ElementRef);
|
||||
private reducedMotion = inject(ReducedMotionService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
@@ -32,24 +33,48 @@ export class FlCounterRollupComponent {
|
||||
private targetValue = 0;
|
||||
private startTime = 0;
|
||||
private animationFrame: number | null = null;
|
||||
private observer: IntersectionObserver | null = null;
|
||||
private hasAnimated = false;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.startAnimation();
|
||||
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();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
// React to value changes
|
||||
// React to value changes after initial animation
|
||||
const newValue = this.value();
|
||||
this.startValue = this.currentValue;
|
||||
this.targetValue = newValue;
|
||||
this.startAnimation();
|
||||
if (this.hasAnimated) {
|
||||
this.startAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
if (this.animationFrame !== null) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
this.observer?.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -66,14 +66,14 @@ export class FlFireworksComponent {
|
||||
this.resizeCanvas();
|
||||
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
});
|
||||
|
||||
// Watch for active changes
|
||||
effect(() => {
|
||||
const isActive = this.active();
|
||||
if (isActive && !this.isAnimating) {
|
||||
this.trigger();
|
||||
}
|
||||
// Watch for active changes (must be after canvas is ready)
|
||||
effect(() => {
|
||||
const isActive = this.active();
|
||||
if (isActive && !this.isAnimating) {
|
||||
this.trigger();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class FlFloatingLayersComponent {
|
||||
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.loopCleanup = this.animationLoop.registerWithElement(id, this.el.nativeElement, () => this.tick());
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
|
||||
@@ -91,7 +91,7 @@ export class FlFlowFieldComponent {
|
||||
this.ctx.fillRect(0, 0, this.width, this.height);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export class FlFractalLandscapeComponent {
|
||||
this.renderFrame(0);
|
||||
} else {
|
||||
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();
|
||||
|
||||
@@ -62,7 +62,7 @@ export class FlGradientMeshComponent {
|
||||
}
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -81,7 +81,7 @@ export class FlMatrixRainComponent {
|
||||
this.initColumns();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export class FlParticleFieldComponent {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -192,21 +192,68 @@ export class FlParticleFieldComponent {
|
||||
if (p.y > h + p.size) p.y = -p.size;
|
||||
}
|
||||
|
||||
// Draw connections
|
||||
// Draw connections using spatial grid
|
||||
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 j = i + 1; j < this.particles.length; j++) {
|
||||
const a = this.particles[i];
|
||||
const b = this.particles[j];
|
||||
const d = distance2D(a.x, a.y, b.x, b.y);
|
||||
if (d < connDist) {
|
||||
const alpha = (1 - d / connDist) * connOpacity;
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
ctx.stroke();
|
||||
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];
|
||||
|
||||
for (const j of neighbor) {
|
||||
if (j <= i) continue; // avoid duplicates
|
||||
if (connCounts[j] >= maxConnsPerParticle) continue;
|
||||
|
||||
const b = this.particles[j];
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
const d = Math.sqrt(dx * dx + dy * dy);
|
||||
if (d < connDist) {
|
||||
const alpha = (1 - d / connDist) * connOpacity;
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
ctx.stroke();
|
||||
connCounts[i]++;
|
||||
connCounts[j]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Component, ChangeDetectionStrategy, input, inject,
|
||||
afterNextRender, DestroyRef, signal,
|
||||
ElementRef, afterNextRender, DestroyRef, signal,
|
||||
} from '@angular/core';
|
||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
host: { 'class': 'fl-scramble-reveal' },
|
||||
})
|
||||
export class FlScrambleRevealComponent {
|
||||
private el = inject(ElementRef);
|
||||
private reducedMotion = inject(ReducedMotionService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
@@ -28,6 +29,7 @@ export class FlScrambleRevealComponent {
|
||||
private scrambleCount = 0;
|
||||
private maxScrambles = 3;
|
||||
private interval: any = null;
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
@@ -36,13 +38,24 @@ export class FlScrambleRevealComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.startScramble();
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
this.observer?.disconnect();
|
||||
this.observer = null;
|
||||
this.startScramble();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.observer?.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Component, ChangeDetectionStrategy, input, inject,
|
||||
afterNextRender, DestroyRef, signal,
|
||||
ElementRef, afterNextRender, DestroyRef, signal,
|
||||
} from '@angular/core';
|
||||
import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ReducedMotionService } from '../../services/reduced-motion.service';
|
||||
host: { 'class': 'fl-typewriter' },
|
||||
})
|
||||
export class FlTypewriterComponent {
|
||||
private el = inject(ElementRef);
|
||||
private reducedMotion = inject(ReducedMotionService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
@@ -37,6 +38,7 @@ export class FlTypewriterComponent {
|
||||
private typingInterval: any = null;
|
||||
private cursorInterval: any = null;
|
||||
private isTyping = false;
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
@@ -45,12 +47,23 @@ export class FlTypewriterComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => this.startTyping(), this.delay());
|
||||
this.startCursorBlink();
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
this.observer?.disconnect();
|
||||
this.observer = null;
|
||||
setTimeout(() => this.startTyping(), this.delay());
|
||||
this.startCursorBlink();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
});
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.cleanup();
|
||||
this.observer?.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ export class FlVoronoiMeshComponent {
|
||||
private pixelRatio = 1;
|
||||
private loopCleanup: (() => void) | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private cellCache: Uint8Array | null = null;
|
||||
private lastSeedPositions: { x: number; y: number }[] = [];
|
||||
private cacheStep = 4;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
@@ -87,7 +90,7 @@ export class FlVoronoiMeshComponent {
|
||||
this.createSeeds();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -106,6 +109,7 @@ export class FlVoronoiMeshComponent {
|
||||
}
|
||||
|
||||
private createSeeds(): void {
|
||||
this.cellCache = null;
|
||||
const count = this.seedCount();
|
||||
const cols = this.colors();
|
||||
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 {
|
||||
if (!this.ctx) return;
|
||||
|
||||
@@ -145,55 +160,65 @@ export class FlVoronoiMeshComponent {
|
||||
const ctx = this.ctx;
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
|
||||
// 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));
|
||||
const step = Math.max(4, Math.floor(6 / this.pixelRatio));
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
for (let x = 0; x < w; x += step) {
|
||||
for (let y = 0; y < h; y += step) {
|
||||
let minDist = Infinity;
|
||||
let nearestColor = this.seeds[0]?.color ?? '#000';
|
||||
const cols = Math.ceil(w / step);
|
||||
const rows = Math.ceil(h / step);
|
||||
const totalCells = cols * rows;
|
||||
|
||||
for (const s of this.seeds) {
|
||||
const d = (x - s.x) * (x - s.x) + (y - s.y) * (y - s.y);
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
nearestColor = s.color;
|
||||
// Only recalculate cell assignments if seeds moved significantly
|
||||
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) {
|
||||
minDist = d;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw edges using second-nearest distance comparison
|
||||
// Draw edges only when showEdges is true
|
||||
if (this.showEdges()) {
|
||||
ctx.strokeStyle = this.edgeColor();
|
||||
ctx.lineWidth = this.edgeWidth();
|
||||
|
||||
for (let x = 0; x < w; x += step) {
|
||||
for (let y = 0; y < h; y += step) {
|
||||
let min1 = Infinity;
|
||||
let min2 = Infinity;
|
||||
|
||||
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);
|
||||
const edgeColor = this.edgeColor();
|
||||
for (let col = 1; col < cols; col++) {
|
||||
for (let row = 1; row < rows; row++) {
|
||||
const idx = cache[col * rows + row];
|
||||
// Check if neighboring cells have different seed assignments
|
||||
if (idx !== cache[(col - 1) * rows + row] || idx !== cache[col * rows + (row - 1)]) {
|
||||
ctx.fillStyle = edgeColor;
|
||||
ctx.fillRect(col * step, row * step, step, step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ export class AnimationLoopService {
|
||||
private startTime = 0;
|
||||
private running = false;
|
||||
|
||||
/** Track which registered IDs are currently visible */
|
||||
private visibilityMap = new Map<string, boolean>();
|
||||
private observers = new Map<string, IntersectionObserver>();
|
||||
|
||||
constructor() {
|
||||
this.destroyRef.onDestroy(() => {
|
||||
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 {
|
||||
if (this.running) return;
|
||||
if (this.reducedMotion.reduced()) return;
|
||||
|
||||
@@ -114,5 +114,5 @@
|
||||
|
||||
@keyframes fl-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-#{$distance}); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user