From 292117cf568a133788e5e2e600a0d9d8cc61f6d2 Mon Sep 17 00:00:00 2001 From: Kirko Date: Sat, 25 Apr 2026 20:41:35 +0300 Subject: [PATCH] feat: 4 new bg modes (pulse/stars/rain/rays), vivid orbs & aurora MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orbs: bigger radius, opacity 0.14+, 4th orb added. Aurora: 6 bands with opacity 0.22+, more movement. New: Пульс (expanding rings on beat), Звёзды (twinkling starfield), Дождь (falling streaks), Лучи (rotating sunburst rays). Settings page shows 9 bg options with static SVG previews. Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/settings/page.tsx | 172 +++++++---- apps/web/src/components/AudioBackground.tsx | 318 +++++++++++++------- apps/web/src/store/bgStore.ts | 12 +- 3 files changed, 331 insertions(+), 171 deletions(-) diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 34e3f7e..5f73034 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -7,27 +7,20 @@ import { useThemeStore, ACCENT_PRESETS } from '@/store/themeStore' import { useBgStore, BG_PRESETS, type BgMode } from '@/store/bgStore' import Header from '@/components/Header' +// ── Preview SVGs ───────────────────────────────────────────────────────────── + function OrbsPreview() { return ( - - - - - - - - - - - - + + + - - - + + + ) } @@ -36,31 +29,22 @@ function WavesPreview() { return ( - - - - + + + + ) } function ParticlesPreview() { - const dots: [number, number][] = [[18,14],[52,10],[88,22],[32,38],[72,48],[14,54],[86,56],[50,28],[68,8],[30,20]] - const lines: [number,number][] = [[0,1],[1,2],[0,3],[1,7],[3,5],[2,4],[4,6],[3,7],[7,8],[1,8],[9,0],[9,3]] + const d: [number,number][] = [[18,14],[52,10],[88,22],[32,38],[72,48],[14,54],[86,56],[50,28],[68,8],[30,20]] + const ln: [number,number][] = [[0,1],[1,2],[0,3],[1,7],[3,5],[2,4],[4,6],[3,7],[7,8],[1,8],[9,0],[9,3]] return ( - {lines.map(([a, b], i) => ( - - ))} - {dots.map(([x, y], i) => ( - - ))} + {ln.map(([a,b],i) => )} + {d.map(([x,y],i) => )} ) } @@ -69,24 +53,95 @@ function AuroraPreview() { return ( - - - - - - - - - - - - + + + - - - - + + + + + + + ) +} + +function PulsePreview() { + return ( + + + + + + + + + + + + + ) +} + +function StarsPreview() { + const stars: [number,number,number][] = [ + [12,8,1.4],[34,5,0.9],[58,12,1.1],[80,4,1.5],[102,9,0.8],[18,22,1.0],[45,18,1.3],[72,20,0.9],[96,25,1.2], + [8,38,0.8],[28,42,1.1],[55,35,1.4],[78,40,0.9],[108,36,1.0],[22,56,1.2],[50,58,0.8],[75,54,1.3],[100,60,1.0], + [40,28,0.7],[88,14,1.1],[15,48,0.9],[64,48,0.8] + ] + return ( + + + + + + + {stars.map(([x,y,r],i) => ( + + ))} + + ) +} + +function RainPreview() { + const drops: [number,number,number][] = [ + [10,8,20],[25,0,28],[40,15,22],[55,5,25],[70,10,18],[85,2,30],[100,12,24],[112,6,20], + [18,35,22],[33,28,26],[48,40,19],[63,30,24],[78,38,21],[93,25,28],[108,34,20], + ] + return ( + + + {drops.map(([x,y,len],i) => ( + + + + + ))} + + ) +} + +function RaysPreview() { + const rays = Array.from({length: 9}, (_,i) => (i/9)*360) + return ( + + + + + + {rays.map((deg, i) => { + const rad = (deg * Math.PI) / 180 + const x2 = 60 + Math.cos(rad) * 90 + const y2 = 41 + Math.sin(rad) * 90 + return ( + + ) + })} + + ) } @@ -95,8 +150,8 @@ function NonePreview() { return ( - - + + ) } @@ -106,9 +161,15 @@ const BG_PREVIEWS: Record = { waves: , particles: , aurora: , + pulse: , + stars: , + rain: , + rays: , none: , } +// ── Page ───────────────────────────────────────────────────────────────────── + export default function SettingsPage() { const { user } = useAuthStore() const { accentIdx, setAccent } = useThemeStore() @@ -128,14 +189,14 @@ export default function SettingsPage() {

Настройки

- {/* Appearance */}

Внешний вид

+ {/* Accent color */}

Акцентный цвет

-
+
{ACCENT_PRESETS.map((preset, i) => ( ))}
+ {/* Live background */}

Живой фон

{BG_PRESETS.map((preset) => { diff --git a/apps/web/src/components/AudioBackground.tsx b/apps/web/src/components/AudioBackground.tsx index d3ce089..cd1392e 100644 --- a/apps/web/src/components/AudioBackground.tsx +++ b/apps/web/src/components/AudioBackground.tsx @@ -4,7 +4,10 @@ import { useEffect, useRef } from 'react' import { audioState } from '@/lib/audioState' import { useBgStore } from '@/store/bgStore' -type Pt = { x: number; y: number; vx: number; vy: number; r: number; a: number } +type Pt = { x: number; y: number; vx: number; vy: number; r: number; a: number } +type Ring = { r: number; alpha: number; speed: number } +type Star = { x: number; y: number; r: number; ba: number; ph: number; sp: number } +type Drop = { x: number; y: number; speed: number; len: number; alpha: number } export default function AudioBackground() { const canvasRef = useRef(null) @@ -20,13 +23,10 @@ export default function AudioBackground() { let rafId: number let smoothBass = 0 - let smoothMid = 0 + let smoothMid = 0 let dataBuf: Uint8Array | null = null - const resize = () => { - canvas.width = window.innerWidth - canvas.height = window.innerHeight - } + const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight } resize() window.addEventListener('resize', resize) @@ -47,69 +47,66 @@ export default function AudioBackground() { smoothMid += (rM - smoothMid) * 0.07 } - const acRgb = () => - document.documentElement.style.getPropertyValue('--accent-rgb') || '200,255,0' + const ar = () => document.documentElement.style.getPropertyValue('--accent-rgb') || '200,255,0' - // ── ORBS ───────────────────────────────────────────────────────────────── + // ── ORBS (vivid) ────────────────────────────────────────────────────────── const drawOrbs = () => { - const W = canvas.width, H = canvas.height - const ar = acRgb() + const W = canvas.width, H = canvas.height, a = ar() const t = Date.now() / 4500 - const b = Math.sin(t) * 0.03 + Math.cos(t * 0.7) * 0.015 - const diag = Math.hypot(W, H), base = diag * 0.55 + const br = Math.sin(t) * 0.05 + Math.cos(t * 0.7) * 0.025 + const diag = Math.hypot(W, H), base = diag * 0.68 ctx.clearRect(0, 0, W, H) ctx.fillStyle = '#0a0a0f' ctx.fillRect(0, 0, W, H) - const r1 = base * (0.75 + smoothBass * 0.55 + b) - const a1 = 0.055 + smoothBass * 0.07 - const g1 = ctx.createRadialGradient(W * 0.18, H * 0.08, 0, W * 0.18, H * 0.08, r1) - g1.addColorStop(0, `rgba(${ar},${a1})`); g1.addColorStop(0.45, `rgba(${ar},${a1 * 0.28})`); g1.addColorStop(1, `rgba(${ar},0)`) + // Top-left — accent, bass-driven + const r1 = base * (0.82 + smoothBass * 0.7 + br) + const a1 = 0.14 + smoothBass * 0.18 + const g1 = ctx.createRadialGradient(W * 0.10, H * 0.04, 0, W * 0.10, H * 0.04, r1) + g1.addColorStop(0, `rgba(${a},${a1})`); g1.addColorStop(0.38, `rgba(${a},${a1 * 0.4})`); g1.addColorStop(1, `rgba(${a},0)`) ctx.fillStyle = g1; ctx.fillRect(0, 0, W, H) - const r2 = base * (0.65 + smoothMid * 0.45 - b * 0.6) - const a2 = 0.045 + smoothMid * 0.055 - const g2 = ctx.createRadialGradient(W * 0.82, H * 0.92, 0, W * 0.82, H * 0.92, r2) - g2.addColorStop(0, `rgba(255,60,172,${a2})`); g2.addColorStop(0.45, `rgba(255,60,172,${a2 * 0.28})`); g2.addColorStop(1, 'rgba(255,60,172,0)') + // Bottom-right — pink, mid-driven + const r2 = base * (0.75 + smoothMid * 0.60 - br * 0.6) + const a2 = 0.13 + smoothMid * 0.16 + const g2 = ctx.createRadialGradient(W * 0.90, H * 0.96, 0, W * 0.90, H * 0.96, r2) + g2.addColorStop(0, `rgba(255,60,172,${a2})`); g2.addColorStop(0.38, `rgba(255,60,172,${a2 * 0.4})`); g2.addColorStop(1, 'rgba(255,60,172,0)') ctx.fillStyle = g2; ctx.fillRect(0, 0, W, H) + // Center — purple const c = smoothBass * 0.55 + smoothMid * 0.45 - if (c > 0.005) { - const r3 = base * (0.38 + c * 0.32) - const g3 = ctx.createRadialGradient(W * 0.5, H * 0.52, 0, W * 0.5, H * 0.52, r3) - g3.addColorStop(0, `rgba(140,100,255,${c * 0.035})`); g3.addColorStop(1, 'rgba(140,100,255,0)') - ctx.fillStyle = g3; ctx.fillRect(0, 0, W, H) - } + const r3 = base * (0.48 + c * 0.45) + const a3 = 0.06 + c * 0.10 + const g3 = ctx.createRadialGradient(W * 0.5, H * 0.5, 0, W * 0.5, H * 0.5, r3) + g3.addColorStop(0, `rgba(140,100,255,${a3})`); g3.addColorStop(1, 'rgba(140,100,255,0)') + ctx.fillStyle = g3; ctx.fillRect(0, 0, W, H) + + // Bottom-left — accent, extra depth + const r4 = base * (0.58 + smoothBass * 0.5 + br * 0.5) + const a4 = 0.08 + smoothBass * 0.10 + const g4 = ctx.createRadialGradient(W * 0.04, H * 0.94, 0, W * 0.04, H * 0.94, r4) + g4.addColorStop(0, `rgba(${a},${a4})`); g4.addColorStop(0.5, `rgba(${a},${a4 * 0.2})`); g4.addColorStop(1, `rgba(${a},0)`) + ctx.fillStyle = g4; ctx.fillRect(0, 0, W, H) } // ── WAVES ───────────────────────────────────────────────────────────────── const drawWaves = () => { - const W = canvas.width, H = canvas.height - const ar = acRgb() + const W = canvas.width, H = canvas.height, a = ar() const t = Date.now() / 1000 - - ctx.clearRect(0, 0, W, H) - ctx.fillStyle = '#0a0a0f' - ctx.fillRect(0, 0, W, H) - + ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, W, H) const layers = [ - { y: 0.70, amp: 20 + smoothBass * 60, freq: 0.007, ph: t * 0.35, al: 0.12, c: `rgba(${ar},` }, + { y: 0.70, amp: 20 + smoothBass * 60, freq: 0.007, ph: t * 0.35, al: 0.12, c: `rgba(${a},` }, { y: 0.76, amp: 16 + smoothMid * 45, freq: 0.011, ph: -t * 0.5, al: 0.09, c: 'rgba(255,60,172,' }, - { y: 0.81, amp: 24 + smoothBass * 75, freq: 0.005, ph: t * 0.28, al: 0.14, c: `rgba(${ar},` }, + { y: 0.81, amp: 24 + smoothBass * 75, freq: 0.005, ph: t * 0.28, al: 0.14, c: `rgba(${a},` }, { y: 0.87, amp: 12 + smoothMid * 35, freq: 0.013, ph: -t * 0.62, al: 0.08, c: 'rgba(140,100,255,' }, - { y: 0.93, amp: 30 + smoothBass * 90, freq: 0.004, ph: t * 0.18, al: 0.18, c: `rgba(${ar},` }, + { y: 0.93, amp: 30 + smoothBass * 90, freq: 0.004, ph: t * 0.18, al: 0.18, c: `rgba(${a},` }, ] - for (const l of layers) { const baseY = H * l.y - ctx.beginPath() - ctx.moveTo(-2, baseY) - for (let x = 0; x <= W + 3; x += 3) { - ctx.lineTo(x, baseY - + Math.sin(x * l.freq + l.ph) * l.amp - + Math.sin(x * l.freq * 2.4 + l.ph * 1.6) * l.amp * 0.25) - } + ctx.beginPath(); ctx.moveTo(-2, baseY) + for (let x = 0; x <= W + 3; x += 3) + ctx.lineTo(x, baseY + Math.sin(x * l.freq + l.ph) * l.amp + Math.sin(x * l.freq * 2.4 + l.ph * 1.6) * l.amp * 0.25) ctx.lineTo(W + 2, H + 2); ctx.lineTo(-2, H + 2); ctx.closePath() const g = ctx.createLinearGradient(0, baseY - l.amp * 1.5, 0, H) g.addColorStop(0, `${l.c}${l.al})`); g.addColorStop(1, `${l.c}0)`) @@ -120,111 +117,210 @@ export default function AudioBackground() { // ── PARTICLES ───────────────────────────────────────────────────────────── const PTS: Pt[] = Array.from({ length: 55 }, () => ({ x: Math.random(), y: Math.random(), - vx: (Math.random() - 0.5) * 0.00025, - vy: (Math.random() - 0.5) * 0.00025, - r: Math.random() * 1.5 + 0.6, - a: Math.random() * 0.35 + 0.1, + vx: (Math.random() - 0.5) * 0.00025, vy: (Math.random() - 0.5) * 0.00025, + r: Math.random() * 1.5 + 0.6, a: Math.random() * 0.35 + 0.1, })) const drawParticles = () => { - const W = canvas.width, H = canvas.height - const ar = acRgb() + const W = canvas.width, H = canvas.height, a = ar() + ctx.fillStyle = 'rgba(10,10,15,0.2)'; ctx.fillRect(0, 0, W, H) const spd = 1 + smoothBass * 3 - - ctx.fillStyle = 'rgba(10,10,15,0.2)' - ctx.fillRect(0, 0, W, H) - for (const p of PTS) { p.x += p.vx * spd; p.y += p.vy * spd if (p.x < 0) p.x += 1; if (p.x > 1) p.x -= 1 if (p.y < 0) p.y += 1; if (p.y > 1) p.y -= 1 if (smoothBass > 0.35 && Math.random() < 0.07) { - p.vx += (Math.random() - 0.5) * 0.0006 - p.vy += (Math.random() - 0.5) * 0.0006 + p.vx += (Math.random() - 0.5) * 0.0006; p.vy += (Math.random() - 0.5) * 0.0006 const mv = 0.0012 - p.vx = Math.max(-mv, Math.min(mv, p.vx)) - p.vy = Math.max(-mv, Math.min(mv, p.vy)) + p.vx = Math.max(-mv, Math.min(mv, p.vx)); p.vy = Math.max(-mv, Math.min(mv, p.vy)) } - ctx.beginPath() - ctx.arc(p.x * W, p.y * H, p.r * (1 + smoothBass * 1.2), 0, Math.PI * 2) - ctx.fillStyle = `rgba(${ar},${p.a * (0.7 + smoothMid * 0.3)})` - ctx.fill() + ctx.beginPath(); ctx.arc(p.x * W, p.y * H, p.r * (1 + smoothBass * 1.2), 0, Math.PI * 2) + ctx.fillStyle = `rgba(${a},${p.a * (0.7 + smoothMid * 0.3)})`; ctx.fill() } - const maxD = Math.min(W, H) * 0.12 ctx.lineWidth = 0.5 - for (let i = 0; i < PTS.length; i++) { + for (let i = 0; i < PTS.length; i++) for (let j = i + 1; j < PTS.length; j++) { - const dx = (PTS[i].x - PTS[j].x) * W - const dy = (PTS[i].y - PTS[j].y) * H + const dx = (PTS[i].x - PTS[j].x) * W, dy = (PTS[i].y - PTS[j].y) * H const d = Math.sqrt(dx * dx + dy * dy) if (d < maxD) { - ctx.beginPath() - ctx.moveTo(PTS[i].x * W, PTS[i].y * H) - ctx.lineTo(PTS[j].x * W, PTS[j].y * H) - ctx.strokeStyle = `rgba(${ar},${(1 - d / maxD) * 0.07 * (1 + smoothMid)})` - ctx.stroke() + ctx.beginPath(); ctx.moveTo(PTS[i].x * W, PTS[i].y * H); ctx.lineTo(PTS[j].x * W, PTS[j].y * H) + ctx.strokeStyle = `rgba(${a},${(1 - d / maxD) * 0.07 * (1 + smoothMid)})`; ctx.stroke() } } - } } - // ── AURORA ──────────────────────────────────────────────────────────────── + // ── AURORA (vivid) ──────────────────────────────────────────────────────── const drawAurora = () => { - const W = canvas.width, H = canvas.height - const ar = acRgb() + const W = canvas.width, H = canvas.height, a = ar() const t = Date.now() / 3000 - - ctx.clearRect(0, 0, W, H) - ctx.fillStyle = '#0a0a0f' - ctx.fillRect(0, 0, W, H) + ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, W, H) const bands = [ - { cx: 0.15, cy: 0.25, w: 0.55, h: 0.30, phase: t * 0.6, al: 0.08 + smoothBass * 0.06, c: ar }, - { cx: 0.50, cy: 0.15, w: 0.70, h: 0.22, phase: -t * 0.4, al: 0.06 + smoothMid * 0.05, c: '140,100,255' }, - { cx: 0.80, cy: 0.35, w: 0.50, h: 0.28, phase: t * 0.5, al: 0.07 + smoothBass * 0.04, c: '255,60,172' }, - { cx: 0.35, cy: 0.55, w: 0.60, h: 0.20, phase: -t * 0.7, al: 0.05 + smoothMid * 0.04, c: ar }, + { cx: 0.08, cy: 0.15, w: 0.75, h: 0.30, ph: t * 0.8, al: 0.22 + smoothBass * 0.18, c: a }, + { cx: 0.55, cy: 0.08, w: 0.90, h: 0.22, ph: -t * 0.6, al: 0.18 + smoothMid * 0.15, c: '140,100,255' }, + { cx: 0.88, cy: 0.28, w: 0.65, h: 0.28, ph: t * 0.7, al: 0.20 + smoothBass * 0.14, c: '255,60,172' }, + { cx: 0.28, cy: 0.48, w: 0.80, h: 0.20, ph: -t * 0.9, al: 0.14 + smoothMid * 0.12, c: a }, + { cx: 0.72, cy: 0.62, w: 0.60, h: 0.24, ph: t * 0.55, al: 0.12 + smoothBass * 0.10, c: '140,100,255' }, + { cx: 0.18, cy: 0.78, w: 0.70, h: 0.22, ph: -t * 0.7, al: 0.10 + smoothMid * 0.08, c: '255,60,172' }, ] for (const band of bands) { const cx = band.cx * W - const cy = (band.cy + Math.sin(band.phase) * 0.06) * H + const cy = (band.cy + Math.sin(band.ph) * 0.10) * H const rw = band.w * W - const rh = band.h * H * (1 + smoothBass * 0.3) - - ctx.save() - ctx.translate(cx, cy) - ctx.scale(1, rh / rw) - + const rh = band.h * H * (1 + smoothBass * 0.45) + ctx.save(); ctx.translate(cx, cy); ctx.scale(1, rh / rw) const g = ctx.createRadialGradient(0, 0, 0, 0, 0, rw) - g.addColorStop(0, `rgba(${band.c},${band.al})`) - g.addColorStop(0.35, `rgba(${band.c},${band.al * 0.5})`) - g.addColorStop(1, `rgba(${band.c},0)`) - ctx.fillStyle = g - ctx.beginPath() - ctx.arc(0, 0, rw, 0, Math.PI * 2) - ctx.fill() - ctx.restore() + g.addColorStop(0, `rgba(${band.c},${band.al})`); g.addColorStop(0.4, `rgba(${band.c},${band.al * 0.5})`); g.addColorStop(1, `rgba(${band.c},0)`) + ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0, 0, rw, 0, Math.PI * 2); ctx.fill(); ctx.restore() } } - // ── LOOP ────────────────────────────────────────────────────────────────── - const drawFn = { orbs: drawOrbs, waves: drawWaves, particles: drawParticles, aurora: drawAurora }[bgMode] ?? drawOrbs + // ── PULSE ───────────────────────────────────────────────────────────────── + const RINGS: Ring[] = [] + let prevBass = 0 + + const drawPulse = () => { + const W = canvas.width, H = canvas.height, a = ar() + const cx = W * 0.5, cy = H * 0.5 + const maxR = Math.hypot(W, H) * 0.8 + + ctx.fillStyle = 'rgba(10,10,15,0.14)'; ctx.fillRect(0, 0, W, H) + + if (smoothBass > prevBass + 0.07 && smoothBass > 0.18) + RINGS.push({ r: 12, alpha: 0.6 + smoothBass * 0.35, speed: 3 + smoothBass * 8 }) + prevBass = smoothBass + + if (Math.random() < 0.014) + RINGS.push({ r: 5, alpha: 0.16, speed: 1.4 }) + + for (let i = RINGS.length - 1; i >= 0; i--) { + const ring = RINGS[i] + ring.r += ring.speed; ring.alpha -= 0.006 + if (ring.alpha <= 0 || ring.r > maxR) { RINGS.splice(i, 1); continue } + ctx.beginPath(); ctx.arc(cx, cy, ring.r, 0, Math.PI * 2) + ctx.strokeStyle = `rgba(${a},${ring.alpha})` + ctx.lineWidth = 1.5 + ring.alpha * 3; ctx.stroke() + if (ring.r > 30 && ring.alpha > 0.12) { + ctx.beginPath(); ctx.arc(cx, cy, ring.r * 0.82, 0, Math.PI * 2) + ctx.strokeStyle = `rgba(255,60,172,${ring.alpha * 0.45})` + ctx.lineWidth = 0.8; ctx.stroke() + } + } + + const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, 90 + smoothBass * 130) + g.addColorStop(0, `rgba(${a},${0.06 + smoothBass * 0.12})`); g.addColorStop(1, `rgba(${a},0)`) + ctx.fillStyle = g; ctx.fillRect(0, 0, W, H) + } + + // ── STARS ───────────────────────────────────────────────────────────────── + const STARS: Star[] = Array.from({ length: 180 }, () => ({ + x: Math.random(), y: Math.random(), + r: Math.random() * 1.4 + 0.3, + ba: Math.random() * 0.55 + 0.15, + ph: Math.random() * Math.PI * 2, + sp: Math.random() * 1.5 + 0.4, + })) + + const drawStars = () => { + const W = canvas.width, H = canvas.height, a = ar() + const t = Date.now() / 1000 + ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, W, H) + + const n1 = ctx.createRadialGradient(W * 0.30, H * 0.35, 0, W * 0.30, H * 0.35, W * 0.55) + n1.addColorStop(0, `rgba(${a},${0.05 + smoothMid * 0.05})`); n1.addColorStop(1, `rgba(${a},0)`) + ctx.fillStyle = n1; ctx.fillRect(0, 0, W, H) + const n2 = ctx.createRadialGradient(W * 0.72, H * 0.62, 0, W * 0.72, H * 0.62, W * 0.45) + n2.addColorStop(0, `rgba(140,100,255,${0.04 + smoothBass * 0.05})`); n2.addColorStop(1, 'rgba(140,100,255,0)') + ctx.fillStyle = n2; ctx.fillRect(0, 0, W, H) + + for (const s of STARS) { + const tw = (Math.sin(t * s.sp + s.ph) + 1) * 0.5 + const alpha = s.ba * (0.35 + tw * 0.65) * (1 + smoothMid * 0.45) + const r = s.r * (1 + smoothBass * tw * 1.0) + if (s.r > 1.1) { + ctx.beginPath(); ctx.arc(s.x * W, s.y * H, r * 2.8, 0, Math.PI * 2) + ctx.fillStyle = `rgba(${a},${alpha * 0.18})`; ctx.fill() + } + ctx.beginPath(); ctx.arc(s.x * W, s.y * H, r, 0, Math.PI * 2) + ctx.fillStyle = `rgba(${a},${alpha})`; ctx.fill() + } + } + + // ── RAIN ────────────────────────────────────────────────────────────────── + const DROPS: Drop[] = Array.from({ length: 65 }, () => ({ + x: Math.random(), y: Math.random(), + speed: Math.random() * 0.003 + 0.0015, + len: Math.random() * 0.07 + 0.04, + alpha: Math.random() * 0.5 + 0.2, + })) + + const drawRain = () => { + const W = canvas.width, H = canvas.height, a = ar() + ctx.fillStyle = 'rgba(10,10,15,0.1)'; ctx.fillRect(0, 0, W, H) + const spd = 1 + smoothBass * 4.5 + for (const d of DROPS) { + d.y += d.speed * spd + if (d.y > 1.1) { d.y = -d.len; d.x = Math.random() } + const x = d.x * W, y = d.y * H + const len = d.len * H * (1 + smoothBass * 0.6) + const g = ctx.createLinearGradient(x, y - len, x, y) + g.addColorStop(0, `rgba(${a},0)`); g.addColorStop(0.6, `rgba(${a},${d.alpha * 0.5})`); g.addColorStop(1, `rgba(${a},${d.alpha})`) + ctx.fillStyle = g; ctx.fillRect(x - 1, y - len, 2, len) + ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI * 2) + ctx.fillStyle = `rgba(${a},${d.alpha})`; ctx.fill() + } + } + + // ── RAYS ────────────────────────────────────────────────────────────────── + const NUM_RAYS = 9 + const RAY_OFF = Array.from({ length: NUM_RAYS }, (_, i) => (i / NUM_RAYS) * Math.PI * 2) + + const drawRays = () => { + const W = canvas.width, H = canvas.height, a = ar() + const t = Date.now() / 10000 + const cx = W * 0.5, cy = H * 0.58 + const maxR = Math.hypot(W, H) + + ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, W, H) + + for (let i = 0; i < NUM_RAYS; i++) { + const angle = RAY_OFF[i] + t + const hw = 0.065 + smoothBass * 0.055 + const al = (i % 2 === 0 ? 0.08 : 0.05) + smoothBass * 0.09 + smoothMid * 0.02 + ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, maxR, angle - hw, angle + hw); ctx.closePath() + const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxR * 0.65) + g.addColorStop(0, `rgba(${a},${al * 2.8})`); g.addColorStop(0.25, `rgba(${a},${al})`); g.addColorStop(1, `rgba(${a},0)`) + ctx.fillStyle = g; ctx.fill() + } + for (let i = 0; i < 4; i++) { + const angle = RAY_OFF[i * 2] + t * 1.4 + Math.PI * 0.28 + const hw = 0.04 + smoothMid * 0.035 + const al = 0.04 + smoothMid * 0.05 + ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, maxR, angle - hw, angle + hw); ctx.closePath() + const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxR * 0.5) + g.addColorStop(0, `rgba(255,60,172,${al * 2.5})`); g.addColorStop(1, 'rgba(255,60,172,0)') + ctx.fillStyle = g; ctx.fill() + } + const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 120 + smoothBass * 160) + cg.addColorStop(0, `rgba(${a},${0.12 + smoothBass * 0.18})`); cg.addColorStop(1, `rgba(${a},0)`) + ctx.fillStyle = cg; ctx.fillRect(0, 0, W, H) + } + + // ── LOOP ────────────────────────────────────────────────────────────────── + const drawFn = ({ + orbs: drawOrbs, waves: drawWaves, particles: drawParticles, + aurora: drawAurora, pulse: drawPulse, stars: drawStars, + rain: drawRain, rays: drawRays, + } as Record void>)[bgMode] ?? drawOrbs - // Clear once to avoid bleed from previous mode ctx.clearRect(0, 0, canvas.width, canvas.height) - const loop = () => { - tick() - drawFn() - rafId = requestAnimationFrame(loop) - } + const loop = () => { tick(); drawFn(); rafId = requestAnimationFrame(loop) } loop() - return () => { - cancelAnimationFrame(rafId) - window.removeEventListener('resize', resize) - } + return () => { cancelAnimationFrame(rafId); window.removeEventListener('resize', resize) } }, [bgMode]) if (bgMode === 'none') return null diff --git a/apps/web/src/store/bgStore.ts b/apps/web/src/store/bgStore.ts index 0e03a8e..f304fa7 100644 --- a/apps/web/src/store/bgStore.ts +++ b/apps/web/src/store/bgStore.ts @@ -2,7 +2,7 @@ import { create } from 'zustand' -export type BgMode = 'orbs' | 'waves' | 'particles' | 'aurora' | 'none' +export type BgMode = 'orbs' | 'waves' | 'particles' | 'aurora' | 'pulse' | 'stars' | 'rain' | 'rays' | 'none' export interface BgPreset { id: BgMode @@ -11,10 +11,14 @@ export interface BgPreset { } export const BG_PRESETS: BgPreset[] = [ - { id: 'orbs', name: 'Орбы', desc: 'Мягкие цветовые пятна' }, - { id: 'waves', name: 'Волны', desc: 'Звуковые волны снизу' }, - { id: 'particles', name: 'Частицы', desc: 'Плавающая сеть точек' }, + { id: 'orbs', name: 'Орбы', desc: 'Цветовые пятна' }, + { id: 'waves', name: 'Волны', desc: 'Звуковые волны' }, + { id: 'particles', name: 'Частицы', desc: 'Сеть точек' }, { id: 'aurora', name: 'Аврора', desc: 'Северное сияние' }, + { id: 'pulse', name: 'Пульс', desc: 'Расходящиеся кольца' }, + { id: 'stars', name: 'Звёзды', desc: 'Звёздное небо' }, + { id: 'rain', name: 'Дождь', desc: 'Световые капли' }, + { id: 'rays', name: 'Лучи', desc: 'Лучи из центра' }, { id: 'none', name: 'Нет', desc: 'Чистый фон' }, ]