mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(backgrounds): add constellation background
This commit is contained in:
127
src/components/ui/backgrounds/constellation.tsx
Normal file
127
src/components/ui/backgrounds/constellation.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { sanitizeColor, clampNumber } from './types';
|
||||||
|
import { useAnimationLoop, getMobileDpr } from '@/hooks/useAnimationLoop';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConstellationState {
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
particles: Particle[];
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
dpr: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConstellationBackground({ settings }: Props) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const stateRef = useRef<ConstellationState | null>(null);
|
||||||
|
|
||||||
|
const particleColor = sanitizeColor(settings.particleColor, '#818cf8');
|
||||||
|
const lineColor = sanitizeColor(settings.lineColor, '#818cf8');
|
||||||
|
const count = clampNumber(settings.count, 10, 200, 60);
|
||||||
|
const linkDistance = clampNumber(settings.linkDistance, 40, 300, 120);
|
||||||
|
|
||||||
|
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 particles: Particle[] = Array.from({ length: Math.floor(count) }, () => ({
|
||||||
|
x: Math.random() * w,
|
||||||
|
y: Math.random() * h,
|
||||||
|
vx: (Math.random() - 0.5) * 0.5,
|
||||||
|
vy: (Math.random() - 0.5) * 0.5,
|
||||||
|
}));
|
||||||
|
|
||||||
|
stateRef.current = { ctx, particles, 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]);
|
||||||
|
|
||||||
|
useAnimationLoop(() => {
|
||||||
|
const state = stateRef.current;
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
const { ctx, particles, w, h } = state;
|
||||||
|
const linkDistanceSq = linkDistance * linkDistance;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
for (const p of particles) {
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
|
||||||
|
if (p.x < 0 || p.x > w) p.vx = -p.vx;
|
||||||
|
if (p.y < 0 || p.y > h) p.vy = -p.vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = lineColor;
|
||||||
|
ctx.lineWidth = 0.8;
|
||||||
|
|
||||||
|
for (let i = 0; i < particles.length; i++) {
|
||||||
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
|
const dx = particles[i].x - particles[j].x;
|
||||||
|
const dy = particles[i].y - particles[j].y;
|
||||||
|
const distSq = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
if (distSq < linkDistanceSq) {
|
||||||
|
const dist = Math.sqrt(distSq);
|
||||||
|
ctx.globalAlpha = (1 - dist / linkDistance) * 0.35;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(particles[i].x, particles[i].y);
|
||||||
|
ctx.lineTo(particles[j].x, particles[j].y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 0.8;
|
||||||
|
ctx.fillStyle = particleColor;
|
||||||
|
|
||||||
|
for (const p of particles) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, 1.8, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}, [particleColor, lineColor, count, linkDistance]);
|
||||||
|
|
||||||
|
return <canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />;
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ const backgroundImports: Record<Exclude<BackgroundType, 'none'>, () => Promise<u
|
|||||||
starfield: () => import('./starfield'),
|
starfield: () => import('./starfield'),
|
||||||
'matrix-rain': () => import('./matrix-rain'),
|
'matrix-rain': () => import('./matrix-rain'),
|
||||||
'liquid-gradient': () => import('./liquid-gradient'),
|
'liquid-gradient': () => import('./liquid-gradient'),
|
||||||
|
constellation: () => import('./constellation'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 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) */
|
||||||
@@ -57,6 +58,7 @@ export const backgroundComponents: Record<
|
|||||||
starfield: lazy(() => import('./starfield')),
|
starfield: lazy(() => import('./starfield')),
|
||||||
'matrix-rain': lazy(() => import('./matrix-rain')),
|
'matrix-rain': lazy(() => import('./matrix-rain')),
|
||||||
'liquid-gradient': lazy(() => import('./liquid-gradient')),
|
'liquid-gradient': lazy(() => import('./liquid-gradient')),
|
||||||
|
constellation: lazy(() => import('./constellation')),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registry of all background definitions with settings for the editor
|
// Registry of all background definitions with settings for the editor
|
||||||
@@ -757,4 +759,37 @@ export const backgroundRegistry: BackgroundDefinition[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'constellation',
|
||||||
|
labelKey: 'admin.backgrounds.constellation',
|
||||||
|
descriptionKey: 'admin.backgrounds.constellationDesc',
|
||||||
|
category: 'canvas',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: 'particleColor',
|
||||||
|
label: 'admin.backgrounds.particleColor',
|
||||||
|
type: 'color',
|
||||||
|
default: '#818cf8',
|
||||||
|
},
|
||||||
|
{ key: 'lineColor', label: 'admin.backgrounds.lineColor', type: 'color', default: '#818cf8' },
|
||||||
|
{
|
||||||
|
key: 'count',
|
||||||
|
label: 'admin.backgrounds.count',
|
||||||
|
type: 'number',
|
||||||
|
min: 10,
|
||||||
|
max: 200,
|
||||||
|
step: 5,
|
||||||
|
default: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'linkDistance',
|
||||||
|
label: 'admin.backgrounds.linkDistance',
|
||||||
|
type: 'number',
|
||||||
|
min: 40,
|
||||||
|
max: 300,
|
||||||
|
step: 10,
|
||||||
|
default: 120,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type BackgroundType =
|
|||||||
| 'starfield'
|
| 'starfield'
|
||||||
| 'matrix-rain'
|
| 'matrix-rain'
|
||||||
| 'liquid-gradient'
|
| 'liquid-gradient'
|
||||||
|
| 'constellation'
|
||||||
| 'none';
|
| 'none';
|
||||||
|
|
||||||
export interface AnimationConfig {
|
export interface AnimationConfig {
|
||||||
|
|||||||
@@ -1163,6 +1163,8 @@
|
|||||||
"matrixRainDesc": "Falling columns of glyphs",
|
"matrixRainDesc": "Falling columns of glyphs",
|
||||||
"liquidGradient": "Liquid Gradient",
|
"liquidGradient": "Liquid Gradient",
|
||||||
"liquidGradientDesc": "Blurred mesh gradient blobs",
|
"liquidGradientDesc": "Blurred mesh gradient blobs",
|
||||||
|
"constellation": "Constellation",
|
||||||
|
"constellationDesc": "Particles linked by lines",
|
||||||
"geminiEffect": "Gemini Effect",
|
"geminiEffect": "Gemini Effect",
|
||||||
"geminiEffectDesc": "SVG effect like Google Gemini",
|
"geminiEffectDesc": "SVG effect like Google Gemini",
|
||||||
"speed": "Speed",
|
"speed": "Speed",
|
||||||
@@ -1186,6 +1188,7 @@
|
|||||||
"lineColor": "Line Color",
|
"lineColor": "Line Color",
|
||||||
"wind": "Wind",
|
"wind": "Wind",
|
||||||
"charset": "Charset",
|
"charset": "Charset",
|
||||||
|
"linkDistance": "Link Distance",
|
||||||
"bgColor": "Background Color",
|
"bgColor": "Background Color",
|
||||||
"fillColor": "Fill Color",
|
"fillColor": "Fill Color",
|
||||||
"color1": "Color 1",
|
"color1": "Color 1",
|
||||||
|
|||||||
@@ -1045,6 +1045,8 @@
|
|||||||
"matrixRainDesc": "ستونهای نمادهای در حال سقوط",
|
"matrixRainDesc": "ستونهای نمادهای در حال سقوط",
|
||||||
"liquidGradient": "گرادیان مایع",
|
"liquidGradient": "گرادیان مایع",
|
||||||
"liquidGradientDesc": "حبابهای گرادیان مات و روان",
|
"liquidGradientDesc": "حبابهای گرادیان مات و روان",
|
||||||
|
"constellation": "صورت فلکی",
|
||||||
|
"constellationDesc": "ذرات متصل با خطوط",
|
||||||
"geminiEffect": "افکت Gemini",
|
"geminiEffect": "افکت Gemini",
|
||||||
"geminiEffectDesc": "افکت SVG مشابه Google Gemini",
|
"geminiEffectDesc": "افکت SVG مشابه Google Gemini",
|
||||||
"speed": "سرعت",
|
"speed": "سرعت",
|
||||||
@@ -1068,6 +1070,7 @@
|
|||||||
"lineColor": "رنگ خطوط",
|
"lineColor": "رنگ خطوط",
|
||||||
"wind": "باد",
|
"wind": "باد",
|
||||||
"charset": "مجموعه نویسهها",
|
"charset": "مجموعه نویسهها",
|
||||||
|
"linkDistance": "فاصله اتصال",
|
||||||
"bgColor": "رنگ پسزمینه",
|
"bgColor": "رنگ پسزمینه",
|
||||||
"fillColor": "رنگ پر کردن",
|
"fillColor": "رنگ پر کردن",
|
||||||
"color1": "رنگ ۱",
|
"color1": "رنگ ۱",
|
||||||
|
|||||||
@@ -1193,6 +1193,8 @@
|
|||||||
"matrixRainDesc": "Падающие колонки символов",
|
"matrixRainDesc": "Падающие колонки символов",
|
||||||
"liquidGradient": "Liquid Gradient",
|
"liquidGradient": "Liquid Gradient",
|
||||||
"liquidGradientDesc": "Жидкий mesh-градиент из размытых блобов",
|
"liquidGradientDesc": "Жидкий mesh-градиент из размытых блобов",
|
||||||
|
"constellation": "Constellation",
|
||||||
|
"constellationDesc": "Частицы, связанные линиями",
|
||||||
"geminiEffect": "Gemini Effect",
|
"geminiEffect": "Gemini Effect",
|
||||||
"geminiEffectDesc": "SVG-эффект как на Google Gemini",
|
"geminiEffectDesc": "SVG-эффект как на Google Gemini",
|
||||||
"speed": "Скорость",
|
"speed": "Скорость",
|
||||||
@@ -1216,6 +1218,7 @@
|
|||||||
"lineColor": "Цвет линий",
|
"lineColor": "Цвет линий",
|
||||||
"wind": "Ветер",
|
"wind": "Ветер",
|
||||||
"charset": "Набор символов",
|
"charset": "Набор символов",
|
||||||
|
"linkDistance": "Дистанция связи",
|
||||||
"bgColor": "Цвет фона",
|
"bgColor": "Цвет фона",
|
||||||
"fillColor": "Цвет заливки",
|
"fillColor": "Цвет заливки",
|
||||||
"color1": "Цвет 1",
|
"color1": "Цвет 1",
|
||||||
|
|||||||
@@ -1045,6 +1045,8 @@
|
|||||||
"matrixRainDesc": "下落的字符列",
|
"matrixRainDesc": "下落的字符列",
|
||||||
"liquidGradient": "流体渐变",
|
"liquidGradient": "流体渐变",
|
||||||
"liquidGradientDesc": "模糊的网格渐变色块",
|
"liquidGradientDesc": "模糊的网格渐变色块",
|
||||||
|
"constellation": "星座",
|
||||||
|
"constellationDesc": "由线条连接的粒子",
|
||||||
"geminiEffect": "Gemini效果",
|
"geminiEffect": "Gemini效果",
|
||||||
"geminiEffectDesc": "类似Google Gemini的SVG效果",
|
"geminiEffectDesc": "类似Google Gemini的SVG效果",
|
||||||
"speed": "速度",
|
"speed": "速度",
|
||||||
@@ -1068,6 +1070,7 @@
|
|||||||
"lineColor": "线条颜色",
|
"lineColor": "线条颜色",
|
||||||
"wind": "风力",
|
"wind": "风力",
|
||||||
"charset": "字符集",
|
"charset": "字符集",
|
||||||
|
"linkDistance": "连接距离",
|
||||||
"bgColor": "背景颜色",
|
"bgColor": "背景颜色",
|
||||||
"fillColor": "填充颜色",
|
"fillColor": "填充颜色",
|
||||||
"color1": "颜色1",
|
"color1": "颜色1",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const VALID_TYPES: ReadonlySet<string> = new Set<BackgroundType>([
|
|||||||
'starfield',
|
'starfield',
|
||||||
'matrix-rain',
|
'matrix-rain',
|
||||||
'liquid-gradient',
|
'liquid-gradient',
|
||||||
|
'constellation',
|
||||||
'none',
|
'none',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user