mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
import React, { useEffect, useRef, useMemo } from 'react';
|
|
import { sanitizeColor, clampNumber, safeBoolean } from './types';
|
|
import { useAnimationLoop, getMobileDpr } from '@/hooks/useAnimationLoop';
|
|
|
|
interface Props {
|
|
settings: Record<string, unknown>;
|
|
}
|
|
|
|
const COLORS = [
|
|
'#93c5fd',
|
|
'#f9a8d4',
|
|
'#86efac',
|
|
'#fde047',
|
|
'#fca5a5',
|
|
'#d8b4fe',
|
|
'#a5b4fc',
|
|
'#c4b5fd',
|
|
];
|
|
|
|
function hexToRgb(hex: string): [number, number, number] {
|
|
let value = hex.replace('#', '');
|
|
if (value.length === 3)
|
|
value = value
|
|
.split('')
|
|
.map((c) => c + c)
|
|
.join('');
|
|
if (!/^[0-9a-fA-F]{6}$/.test(value)) return [129, 140, 248];
|
|
const v = parseInt(value, 16);
|
|
return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
|
|
}
|
|
|
|
interface CellData {
|
|
rgb: [number, number, number];
|
|
phase: number;
|
|
period: number;
|
|
}
|
|
|
|
const SKEW_X_TAN = Math.tan((-48 * Math.PI) / 180);
|
|
const SKEW_Y_TAN = Math.tan((14 * Math.PI) / 180);
|
|
const GRID_SCALE = 0.675;
|
|
|
|
interface BoxesState {
|
|
ctx: CanvasRenderingContext2D;
|
|
w: number;
|
|
h: number;
|
|
}
|
|
|
|
export default React.memo(function BackgroundBoxes({ settings }: Props) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const stateRef = useRef<BoxesState | null>(null);
|
|
const rows = clampNumber(settings.rows, 4, 30, 15);
|
|
const cols = clampNumber(settings.cols, 4, 30, 15);
|
|
const boxColor = sanitizeColor(settings.boxColor, '#818cf8');
|
|
const lineColor = sanitizeColor(settings.lineColor, '#334155');
|
|
const multicolor =
|
|
settings.multicolor === undefined
|
|
? boxColor === '#818cf8'
|
|
: safeBoolean(settings.multicolor, true);
|
|
|
|
const cells = useMemo((): CellData[] => {
|
|
return Array.from({ length: rows * cols }, () => ({
|
|
rgb: hexToRgb(multicolor ? COLORS[Math.floor(Math.random() * COLORS.length)] : boxColor),
|
|
phase: Math.random() * 8,
|
|
period: 3 + Math.random() * 4,
|
|
}));
|
|
}, [rows, cols, boxColor, multicolor]);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const dpr = getMobileDpr();
|
|
const parent = canvas.parentElement;
|
|
if (!parent) return;
|
|
const rect = parent.getBoundingClientRect();
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = rect.height * dpr;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
stateRef.current = { ctx, w: canvas.width, h: canvas.height };
|
|
|
|
const onResize = () => {
|
|
const r = parent.getBoundingClientRect();
|
|
canvas.width = r.width * dpr;
|
|
canvas.height = r.height * dpr;
|
|
if (stateRef.current) {
|
|
stateRef.current.w = canvas.width;
|
|
stateRef.current.h = canvas.height;
|
|
}
|
|
};
|
|
|
|
window.addEventListener('resize', onResize);
|
|
return () => window.removeEventListener('resize', onResize);
|
|
}, []);
|
|
|
|
useAnimationLoop(
|
|
(now) => {
|
|
const state = stateRef.current;
|
|
if (!state) return;
|
|
|
|
const { ctx, w, h } = state;
|
|
const t = now * 0.001;
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
ctx.save();
|
|
|
|
const cx = w / 2;
|
|
const cy = h / 2;
|
|
ctx.translate(cx, cy);
|
|
ctx.transform(1, 0, SKEW_X_TAN, 1, 0, 0);
|
|
ctx.transform(1, SKEW_Y_TAN, 0, 1, 0, 0);
|
|
ctx.scale(GRID_SCALE, GRID_SCALE);
|
|
ctx.translate(-cx, -cy);
|
|
|
|
const gw = w * 3;
|
|
const gh = h * 3;
|
|
const ox = -w;
|
|
const oy = -h;
|
|
const cellW = gw / cols;
|
|
const cellH = gh / rows;
|
|
|
|
for (let i = 0; i < cells.length; i++) {
|
|
const cell = cells[i];
|
|
const cycleT = ((t + cell.phase) % cell.period) / cell.period;
|
|
const alpha = 0.15 * Math.sin(cycleT * Math.PI);
|
|
if (alpha < 0.005) continue;
|
|
|
|
const col = i % cols;
|
|
const row = (i - col) / cols;
|
|
const [r, g, b] = cell.rgb;
|
|
|
|
ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
|
|
ctx.fillRect(ox + col * cellW, oy + row * cellH, cellW, cellH);
|
|
}
|
|
|
|
ctx.globalAlpha = 0.5;
|
|
ctx.strokeStyle = lineColor;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
|
|
for (let r = 0; r <= rows; r++) {
|
|
const y = oy + r * cellH;
|
|
ctx.moveTo(ox, y);
|
|
ctx.lineTo(ox + gw, y);
|
|
}
|
|
for (let c = 0; c <= cols; c++) {
|
|
const x = ox + c * cellW;
|
|
ctx.moveTo(x, oy);
|
|
ctx.lineTo(x, oy + gh);
|
|
}
|
|
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
},
|
|
[cells, rows, cols, lineColor],
|
|
);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
}}
|
|
/>
|
|
);
|
|
});
|