mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
feat(backgrounds): add fireflies background
This commit is contained in:
122
src/components/ui/backgrounds/fireflies.tsx
Normal file
122
src/components/ui/backgrounds/fireflies.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { sanitizeColor, clampNumber } from './types';
|
||||||
|
import { useAnimationLoop, getMobileDpr } from '@/hooks/useAnimationLoop';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Firefly {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
radius: number;
|
||||||
|
phase: number;
|
||||||
|
pulseSpeed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FirefliesState {
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
fireflies: Firefly[];
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
dpr: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FirefliesBackground({ settings }: Props) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const stateRef = useRef<FirefliesState | null>(null);
|
||||||
|
|
||||||
|
const color = sanitizeColor(settings.color, '#ffd166');
|
||||||
|
const count = clampNumber(settings.count, 5, 200, 40);
|
||||||
|
const speed = clampNumber(settings.speed, 0.1, 3, 1);
|
||||||
|
const size = clampNumber(settings.size, 0.5, 6, 2);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const dpr = getMobileDpr();
|
||||||
|
const parent = canvas.parentElement;
|
||||||
|
const w = parent?.offsetWidth ?? window.innerWidth;
|
||||||
|
const h = parent?.offsetHeight ?? window.innerHeight;
|
||||||
|
canvas.width = w * dpr;
|
||||||
|
canvas.height = h * dpr;
|
||||||
|
canvas.style.width = `${w}px`;
|
||||||
|
canvas.style.height = `${h}px`;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
|
const fireflies: Firefly[] = Array.from({ length: Math.floor(count) }, () => ({
|
||||||
|
x: Math.random() * w,
|
||||||
|
y: Math.random() * h,
|
||||||
|
vx: (Math.random() - 0.5) * speed * 0.6,
|
||||||
|
vy: (Math.random() - 0.5) * speed * 0.6,
|
||||||
|
radius: size * (0.5 + Math.random() * 0.5),
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
pulseSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
}));
|
||||||
|
|
||||||
|
stateRef.current = { ctx, fireflies, w, h, dpr };
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
const nw = parent?.offsetWidth ?? window.innerWidth;
|
||||||
|
const nh = parent?.offsetHeight ?? window.innerHeight;
|
||||||
|
canvas.width = nw * dpr;
|
||||||
|
canvas.height = nh * dpr;
|
||||||
|
canvas.style.width = `${nw}px`;
|
||||||
|
canvas.style.height = `${nh}px`;
|
||||||
|
if (stateRef.current) {
|
||||||
|
stateRef.current.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
stateRef.current.w = nw;
|
||||||
|
stateRef.current.h = nh;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => window.removeEventListener('resize', onResize);
|
||||||
|
}, [count, speed, size]);
|
||||||
|
|
||||||
|
useAnimationLoop(
|
||||||
|
(time) => {
|
||||||
|
const state = stateRef.current;
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
const { ctx, fireflies, w, h } = state;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
for (const f of fireflies) {
|
||||||
|
f.x += f.vx + Math.sin(time / 2000 + f.phase) * 0.3 * speed;
|
||||||
|
f.y += f.vy + Math.cos(time / 2400 + f.phase) * 0.2 * speed;
|
||||||
|
|
||||||
|
if (f.x < -10) f.x = w + 10;
|
||||||
|
if (f.x > w + 10) f.x = -10;
|
||||||
|
if (f.y < -10) f.y = h + 10;
|
||||||
|
if (f.y > h + 10) f.y = -10;
|
||||||
|
|
||||||
|
const pulse = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin((time / 1000) * f.pulseSpeed + f.phase));
|
||||||
|
|
||||||
|
ctx.globalAlpha = pulse * 0.25;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(f.x, f.y, f.radius * 3, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.globalAlpha = pulse;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(f.x, f.y, f.radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
},
|
||||||
|
[color, count, speed, size],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ const backgroundImports: Record<Exclude<BackgroundType, 'none'>, () => Promise<u
|
|||||||
dots: () => import('./grid-background'),
|
dots: () => import('./grid-background'),
|
||||||
spotlight: () => import('./spotlight-bg'),
|
spotlight: () => import('./spotlight-bg'),
|
||||||
ripple: () => import('./background-ripple'),
|
ripple: () => import('./background-ripple'),
|
||||||
|
fireflies: () => import('./fireflies'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Prefetch the JS chunk for a background type (call early to avoid lazy-load delay) */
|
/** Prefetch the JS chunk for a background type (call early to avoid lazy-load delay) */
|
||||||
@@ -47,6 +48,7 @@ export const backgroundComponents: Record<
|
|||||||
dots: lazy(() => import('./grid-background')),
|
dots: lazy(() => import('./grid-background')),
|
||||||
spotlight: lazy(() => import('./spotlight-bg')),
|
spotlight: lazy(() => import('./spotlight-bg')),
|
||||||
ripple: lazy(() => import('./background-ripple')),
|
ripple: lazy(() => import('./background-ripple')),
|
||||||
|
fireflies: lazy(() => import('./fireflies')),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registry of all background definitions with settings for the editor
|
// Registry of all background definitions with settings for the editor
|
||||||
@@ -578,4 +580,40 @@ export const backgroundRegistry: BackgroundDefinition[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'fireflies',
|
||||||
|
labelKey: 'admin.backgrounds.fireflies',
|
||||||
|
descriptionKey: 'admin.backgrounds.firefliesDesc',
|
||||||
|
category: 'canvas',
|
||||||
|
settings: [
|
||||||
|
{ key: 'color', label: 'admin.backgrounds.particleColor', type: 'color', default: '#ffd166' },
|
||||||
|
{
|
||||||
|
key: 'count',
|
||||||
|
label: 'admin.backgrounds.count',
|
||||||
|
type: 'number',
|
||||||
|
min: 5,
|
||||||
|
max: 200,
|
||||||
|
step: 5,
|
||||||
|
default: 40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'speed',
|
||||||
|
label: 'admin.backgrounds.speed',
|
||||||
|
type: 'number',
|
||||||
|
min: 0.1,
|
||||||
|
max: 3,
|
||||||
|
step: 0.1,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'size',
|
||||||
|
label: 'admin.backgrounds.size',
|
||||||
|
type: 'number',
|
||||||
|
min: 0.5,
|
||||||
|
max: 6,
|
||||||
|
step: 0.5,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type BackgroundType =
|
|||||||
| 'dots'
|
| 'dots'
|
||||||
| 'spotlight'
|
| 'spotlight'
|
||||||
| 'ripple'
|
| 'ripple'
|
||||||
|
| 'fireflies'
|
||||||
| 'none';
|
| 'none';
|
||||||
|
|
||||||
export interface AnimationConfig {
|
export interface AnimationConfig {
|
||||||
|
|||||||
@@ -1153,6 +1153,8 @@
|
|||||||
"noiseDesc": "Noise gradients with motion",
|
"noiseDesc": "Noise gradients with motion",
|
||||||
"ripple": "Ripple",
|
"ripple": "Ripple",
|
||||||
"rippleDesc": "Ripple wave effect",
|
"rippleDesc": "Ripple wave effect",
|
||||||
|
"fireflies": "Fireflies",
|
||||||
|
"firefliesDesc": "Drifting glowing dots with pulsation",
|
||||||
"geminiEffect": "Gemini Effect",
|
"geminiEffect": "Gemini Effect",
|
||||||
"geminiEffectDesc": "SVG effect like Google Gemini",
|
"geminiEffectDesc": "SVG effect like Google Gemini",
|
||||||
"speed": "Speed",
|
"speed": "Speed",
|
||||||
|
|||||||
@@ -1035,6 +1035,8 @@
|
|||||||
"noiseDesc": "گرادیانهای نویزی با حرکت",
|
"noiseDesc": "گرادیانهای نویزی با حرکت",
|
||||||
"ripple": "موج",
|
"ripple": "موج",
|
||||||
"rippleDesc": "اثر موج دایرهای",
|
"rippleDesc": "اثر موج دایرهای",
|
||||||
|
"fireflies": "کرمهای شبتاب",
|
||||||
|
"firefliesDesc": "نقاط درخشان شناور با تپش",
|
||||||
"geminiEffect": "افکت Gemini",
|
"geminiEffect": "افکت Gemini",
|
||||||
"geminiEffectDesc": "افکت SVG مشابه Google Gemini",
|
"geminiEffectDesc": "افکت SVG مشابه Google Gemini",
|
||||||
"speed": "سرعت",
|
"speed": "سرعت",
|
||||||
|
|||||||
@@ -1183,6 +1183,8 @@
|
|||||||
"noiseDesc": "Шумовые градиенты с движением",
|
"noiseDesc": "Шумовые градиенты с движением",
|
||||||
"ripple": "Ripple",
|
"ripple": "Ripple",
|
||||||
"rippleDesc": "Волновой эффект при клике",
|
"rippleDesc": "Волновой эффект при клике",
|
||||||
|
"fireflies": "Fireflies",
|
||||||
|
"firefliesDesc": "Дрейфующие светящиеся точки с пульсацией",
|
||||||
"geminiEffect": "Gemini Effect",
|
"geminiEffect": "Gemini Effect",
|
||||||
"geminiEffectDesc": "SVG-эффект как на Google Gemini",
|
"geminiEffectDesc": "SVG-эффект как на Google Gemini",
|
||||||
"speed": "Скорость",
|
"speed": "Скорость",
|
||||||
|
|||||||
@@ -1035,6 +1035,8 @@
|
|||||||
"noiseDesc": "带运动的噪声渐变",
|
"noiseDesc": "带运动的噪声渐变",
|
||||||
"ripple": "涟漪",
|
"ripple": "涟漪",
|
||||||
"rippleDesc": "涟漪波纹效果",
|
"rippleDesc": "涟漪波纹效果",
|
||||||
|
"fireflies": "萤火虫",
|
||||||
|
"firefliesDesc": "漂浮闪烁的发光点",
|
||||||
"geminiEffect": "Gemini效果",
|
"geminiEffect": "Gemini效果",
|
||||||
"geminiEffectDesc": "类似Google Gemini的SVG效果",
|
"geminiEffectDesc": "类似Google Gemini的SVG效果",
|
||||||
"speed": "速度",
|
"speed": "速度",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const VALID_TYPES: ReadonlySet<string> = new Set<BackgroundType>([
|
|||||||
'dots',
|
'dots',
|
||||||
'spotlight',
|
'spotlight',
|
||||||
'ripple',
|
'ripple',
|
||||||
|
'fireflies',
|
||||||
'none',
|
'none',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user