diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 56c0ba9..0db8444 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation' import { useEffect } from 'react' import { useAuthStore } from '@/store/authStore' import { useThemeStore, ACCENT_PRESETS } from '@/store/themeStore' -import { useBgStore, BG_PRESETS, type BgMode } from '@/store/bgStore' +import { useBgStore, BG_PRESETS, DEFAULT_RAYS, type BgMode } from '@/store/bgStore' import { getActiveAccent } from '@/store/themeStore' import Header from '@/components/Header' import ColorWheel from '@/components/ColorWheel' @@ -175,7 +175,7 @@ const BG_PREVIEWS: Record = { export default function SettingsPage() { const { user } = useAuthStore() const { accentIdx, customHex, setAccent, setCustom } = useThemeStore() - const { bgMode, setBg } = useBgStore() + const { bgMode, setBg, raysConfig, setRaysConfig } = useBgStore() const router = useRouter() const activeAccent = getActiveAccent(accentIdx, customHex) @@ -269,6 +269,48 @@ export default function SettingsPage() { ) })} + + {/* Rays config — shown only when rays mode is active */} + {bgMode === 'rays' && ( +
+
+

+ Настройки лучей +

+ +
+
+ {([ + { key: 'count', label: 'Количество', min: 4, max: 16, step: 1, fmt: (v: number) => String(Math.round(v)) }, + { key: 'speed', label: 'Скорость', min: 0.2, max: 3.0, step: 0.1, fmt: (v: number) => v.toFixed(1) + 'x' }, + { key: 'brightness', label: 'Яркость', min: 0.3, max: 2.5, step: 0.1, fmt: (v: number) => v.toFixed(1) + 'x' }, + { key: 'spread', label: 'Ширина', min: 0.3, max: 2.0, step: 0.1, fmt: (v: number) => v.toFixed(1) + 'x' }, + ] as const).map(({ key, label, min, max, step, fmt }) => ( +
+
+ {label} + + {fmt(raysConfig[key])} + +
+ setRaysConfig({ [key]: Number(e.target.value) })} + className="w-full h-1.5 rounded-full cursor-pointer appearance-none bg-white/[0.08]" + style={{ accentColor: 'var(--accent)' }} + /> +
+ ))} +
+
+ )} diff --git a/apps/web/src/components/AudioBackground.tsx b/apps/web/src/components/AudioBackground.tsx index cd1392e..e9d4768 100644 --- a/apps/web/src/components/AudioBackground.tsx +++ b/apps/web/src/components/AudioBackground.tsx @@ -10,8 +10,12 @@ type Star = { x: number; y: number; r: number; ba: number; ph: number; sp: numbe type Drop = { x: number; y: number; speed: number; len: number; alpha: number } export default function AudioBackground() { - const canvasRef = useRef(null) - const { bgMode } = useBgStore() + const canvasRef = useRef(null) + const { bgMode, raysConfig } = useBgStore() + const raysRef = useRef(raysConfig) + + // Keep rays config in sync without restarting the animation loop + useEffect(() => { raysRef.current = raysConfig }, [raysConfig]) useEffect(() => { if (bgMode === 'none') return @@ -24,6 +28,7 @@ export default function AudioBackground() { let rafId: number let smoothBass = 0 let smoothMid = 0 + let fastBass = 0 // fast tracker for onset/beat detection let dataBuf: Uint8Array | null = null const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight } @@ -38,61 +43,53 @@ export default function AudioBackground() { audioState.analyser.getByteFrequencyData(dataBuf) const bEnd = Math.max(1, Math.ceil(n * 0.1)) for (let i = 0; i < bEnd; i++) rB = Math.max(rB, dataBuf[i] / 255) - const mEnd = Math.ceil(n * 0.4) - let mSum = 0 + const mEnd = Math.ceil(n * 0.4); let mSum = 0 for (let i = bEnd; i < mEnd; i++) mSum += dataBuf[i] / 255 rM = mSum / Math.max(1, mEnd - bEnd) } + fastBass += (rB - fastBass) * 0.45 smoothBass += (rB - smoothBass) * 0.1 smoothMid += (rM - smoothMid) * 0.07 } - const ar = () => document.documentElement.style.getPropertyValue('--accent-rgb') || '200,255,0' + const ac = () => document.documentElement.style.getPropertyValue('--accent-rgb') || '200,255,0' - // ── ORBS (vivid) ────────────────────────────────────────────────────────── + // ── ORBS — ambient, subtle ──────────────────────────────────────────────── const drawOrbs = () => { - const W = canvas.width, H = canvas.height, a = ar() - const t = Date.now() / 4500 - const br = Math.sin(t) * 0.05 + Math.cos(t * 0.7) * 0.025 - const diag = Math.hypot(W, H), base = diag * 0.68 + const W = canvas.width, H = canvas.height, a = ac() + const t = Date.now() / 5000 + const br = Math.sin(t) * 0.04 + Math.cos(t * 0.7) * 0.02 + const diag = Math.hypot(W, H), base = diag * 0.62 - 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) - // 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)`) + // accent top-left + const r1 = base * (0.78 + smoothBass * 0.45 + br) + const a1 = 0.07 + smoothBass * 0.08 + const g1 = ctx.createRadialGradient(W * 0.12, H * 0.06, 0, W * 0.12, H * 0.06, r1) + g1.addColorStop(0, `rgba(${a},${a1})`); g1.addColorStop(0.42, `rgba(${a},${a1 * 0.25})`); g1.addColorStop(1, `rgba(${a},0)`) ctx.fillStyle = g1; ctx.fillRect(0, 0, W, H) - // 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)') + // pink bottom-right + const r2 = base * (0.70 + smoothMid * 0.40 - br * 0.5) + const a2 = 0.06 + smoothMid * 0.07 + const g2 = ctx.createRadialGradient(W * 0.88, H * 0.94, 0, W * 0.88, H * 0.94, r2) + g2.addColorStop(0, `rgba(255,60,172,${a2})`); g2.addColorStop(0.42, `rgba(255,60,172,${a2 * 0.25})`); 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 - 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) + // purple center — only shows with audio + const c = smoothBass * 0.5 + smoothMid * 0.5 + if (c > 0.008) { + const r3 = base * (0.40 + c * 0.35) + 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,${0.035 + c * 0.06})`); g3.addColorStop(1, 'rgba(140,100,255,0)') + ctx.fillStyle = g3; ctx.fillRect(0, 0, W, H) + } } // ── WAVES ───────────────────────────────────────────────────────────────── const drawWaves = () => { - const W = canvas.width, H = canvas.height, a = ar() + const W = canvas.width, H = canvas.height, a = ac() const t = Date.now() / 1000 ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, W, H) const layers = [ @@ -120,9 +117,8 @@ export default function AudioBackground() { 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, a = ar() + const W = canvas.width, H = canvas.height, a = ac() ctx.fillStyle = 'rgba(10,10,15,0.2)'; ctx.fillRect(0, 0, W, H) const spd = 1 + smoothBass * 3 for (const p of PTS) { @@ -137,8 +133,7 @@ export default function AudioBackground() { 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 + const maxD = Math.min(W, H) * 0.12; ctx.lineWidth = 0.5 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, dy = (PTS[i].y - PTS[j].y) * H @@ -150,161 +145,176 @@ export default function AudioBackground() { } } - // ── AURORA (vivid) ──────────────────────────────────────────────────────── + // ── AURORA — subtle shimmer, not a flood ────────────────────────────────── const drawAurora = () => { - const W = canvas.width, H = canvas.height, a = ar() - const t = Date.now() / 3000 + const W = canvas.width, H = canvas.height, a = ac() + const t = Date.now() / 3500 ctx.clearRect(0, 0, W, H); ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, W, H) const bands = [ - { 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' }, + { cx: 0.10, cy: 0.16, w: 0.72, h: 0.22, ph: t * 0.7, al: 0.10 + smoothBass * 0.08, c: a }, + { cx: 0.58, cy: 0.09, w: 0.88, h: 0.18, ph: -t * 0.5, al: 0.08 + smoothMid * 0.07, c: '140,100,255' }, + { cx: 0.85, cy: 0.30, w: 0.62, h: 0.20, ph: t * 0.6, al: 0.09 + smoothBass * 0.07, c: '255,60,172' }, + { cx: 0.30, cy: 0.50, w: 0.78, h: 0.16, ph: -t * 0.8, al: 0.07 + smoothMid * 0.06, c: a }, + { cx: 0.70, cy: 0.64, w: 0.58, h: 0.18, ph: t * 0.5, al: 0.06 + smoothBass * 0.05, c: '140,100,255' }, + { cx: 0.18, cy: 0.78, w: 0.68, h: 0.16, ph: -t * 0.65,al: 0.05 + smoothMid * 0.05, c: '255,60,172' }, ] - for (const band of bands) { - const cx = band.cx * W - const cy = (band.cy + Math.sin(band.ph) * 0.10) * H - const rw = band.w * W - const rh = band.h * H * (1 + smoothBass * 0.45) + const cx = band.cx * W, cy = (band.cy + Math.sin(band.ph) * 0.07) * H + const rw = band.w * W, rh = band.h * H * (1 + smoothBass * 0.3) 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.4, `rgba(${band.c},${band.al * 0.5})`); g.addColorStop(1, `rgba(${band.c},0)`) + g.addColorStop(0, `rgba(${band.c},${band.al})`); g.addColorStop(0.45, `rgba(${band.c},${band.al * 0.4})`); 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() } } - // ── PULSE ───────────────────────────────────────────────────────────────── + // ── PULSE — beat-driven expanding rings ─────────────────────────────────── const RINGS: Ring[] = [] - let prevBass = 0 + let prevFast = 0 const drawPulse = () => { - const W = canvas.width, H = canvas.height, a = ar() + const W = canvas.width, H = canvas.height, a = ac() 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) + ctx.fillStyle = 'rgba(10,10,15,0.18)'; 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 + // Beat onset: fastBass rises sharply above smoothBass + const onset = fastBass > smoothBass + 0.10 && fastBass > 0.22 && fastBass > prevFast + 0.04 + if (onset) RINGS.push({ r: 10, alpha: 0.7 + fastBass * 0.3, speed: 3 + fastBass * 9 }) + prevFast = fastBass - if (Math.random() < 0.014) - RINGS.push({ r: 5, alpha: 0.16, speed: 1.4 }) + // Slow ambient rings so mode looks alive without audio + if (Math.random() < 0.008 && RINGS.length < 3) RINGS.push({ r: 5, alpha: 0.22, speed: 1.5 }) for (let i = RINGS.length - 1; i >= 0; i--) { const ring = RINGS[i] - ring.r += ring.speed; ring.alpha -= 0.006 + ring.r += ring.speed + ring.alpha -= 0.008 if (ring.alpha <= 0 || ring.r > maxR) { RINGS.splice(i, 1); continue } + + // Main ring 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 = 1.5 + ring.alpha * 3.5; ctx.stroke() + + // Inner echo + if (ring.r > 40 && ring.alpha > 0.15) { + ctx.beginPath(); ctx.arc(cx, cy, ring.r * 0.80, 0, Math.PI * 2) + ctx.strokeStyle = `rgba(255,60,172,${ring.alpha * 0.4})` 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) + // Persistent center glow + const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 80 + smoothBass * 120) + cg.addColorStop(0, `rgba(${a},${0.08 + smoothBass * 0.14})`); cg.addColorStop(1, `rgba(${a},0)`) + ctx.fillStyle = cg; 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, + 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 W = canvas.width, H = canvas.height, a = ac() 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)`) + 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.04+smoothMid*0.04})`); 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 alpha = s.ba * (0.35 + tw * 0.65) * (1 + smoothMid * 0.4) 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() + 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 }, () => ({ + // ── RAIN — sparse, soft streaks ─────────────────────────────────────────── + const DROPS: Drop[] = Array.from({ length: 30 }, () => ({ 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, + speed: Math.random() * 0.0015 + 0.0008, + len: Math.random() * 0.055 + 0.03, + alpha: Math.random() * 0.35 + 0.12, })) - 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 + const W = canvas.width, H = canvas.height, a = ac() + ctx.fillStyle = 'rgba(10,10,15,0.12)'; ctx.fillRect(0, 0, W, H) + const spd = 1 + smoothBass * 2.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) + if (d.y > 1.08) { d.y = -d.len - 0.02; d.x = Math.random() } + const x = d.x * W, y = d.y * H, len = d.len * H * (1 + smoothBass * 0.4) 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() + g.addColorStop(0, `rgba(${a},0)`); g.addColorStop(0.5, `rgba(${a},${d.alpha * 0.3})`); g.addColorStop(1, `rgba(${a},${d.alpha})`) + ctx.fillStyle = g; ctx.fillRect(x - 0.7, y - len, 1.4, len) + ctx.beginPath(); ctx.arc(x, y, 1.4, 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) - + // ── RAYS — tapered beams from center ────────────────────────────────────── 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) + const W = canvas.width, H = canvas.height, a = ac() + const cfg = raysRef.current + const t = (Date.now() / 1000) * cfg.speed * 0.08 + const cx = W * 0.5, cy = H * 0.55 + const maxR = Math.hypot(W, H) * 1.1 + const br = cfg.brightness + const sp = cfg.spread 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() + const count = cfg.count + + // Primary rays — rotate forward + for (let i = 0; i < count; i++) { + const angle = (i / count) * Math.PI * 2 + t + const isMain = i % 2 === 0 + const hw = Math.tan((0.055 + smoothBass * 0.035) * sp) * maxR + const al = ((isMain ? 0.12 : 0.07) + smoothBass * 0.10 + smoothMid * 0.03) * br + + const ex = cx + Math.cos(angle) * maxR + const ey = cy + Math.sin(angle) * maxR + const px = -Math.sin(angle) * hw + const py = Math.cos(angle) * hw + + ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(ex + px, ey + py); ctx.lineTo(ex - px, ey - py); ctx.closePath() + + const grad = ctx.createLinearGradient(cx, cy, ex, ey) + grad.addColorStop(0, `rgba(${a},${al * 2.5})`) + grad.addColorStop(0.12, `rgba(${a},${al})`) + grad.addColorStop(0.55, `rgba(${a},${al * 0.35})`) + grad.addColorStop(1, `rgba(${a},0)`) + ctx.fillStyle = grad; 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() + + // Secondary rays — counter-rotate, pink tint + const cnt2 = Math.max(2, Math.floor(count / 2)) + for (let i = 0; i < cnt2; i++) { + const angle = (i / cnt2) * Math.PI * 2 - t * 0.65 + Math.PI / count + const hw = Math.tan(0.035 * sp) * maxR + const al = (0.05 + smoothMid * 0.06) * br + + const ex = cx + Math.cos(angle) * maxR + const ey = cy + Math.sin(angle) * maxR + const px = -Math.sin(angle) * hw + const py = Math.cos(angle) * hw + + ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(ex + px, ey + py); ctx.lineTo(ex - px, ey - py); ctx.closePath() + const grad = ctx.createLinearGradient(cx, cy, ex, ey) + grad.addColorStop(0, `rgba(255,60,172,${al * 2.5})`) + grad.addColorStop(0.15, `rgba(255,60,172,${al})`) + grad.addColorStop(1, 'rgba(255,60,172,0)') + ctx.fillStyle = grad; 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)`) + + // Center glow + const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 100 + smoothBass * 140) + cg.addColorStop(0, `rgba(${a},${(0.14 + smoothBass * 0.18) * br})`); cg.addColorStop(1, `rgba(${a},0)`) ctx.fillStyle = cg; ctx.fillRect(0, 0, W, H) } @@ -316,7 +326,6 @@ export default function AudioBackground() { } as Record void>)[bgMode] ?? drawOrbs ctx.clearRect(0, 0, canvas.width, canvas.height) - const loop = () => { tick(); drawFn(); rafId = requestAnimationFrame(loop) } loop() diff --git a/apps/web/src/store/bgStore.ts b/apps/web/src/store/bgStore.ts index f304fa7..802d222 100644 --- a/apps/web/src/store/bgStore.ts +++ b/apps/web/src/store/bgStore.ts @@ -4,11 +4,7 @@ import { create } from 'zustand' export type BgMode = 'orbs' | 'waves' | 'particles' | 'aurora' | 'pulse' | 'stars' | 'rain' | 'rays' | 'none' -export interface BgPreset { - id: BgMode - name: string - desc: string -} +export interface BgPreset { id: BgMode; name: string; desc: string } export const BG_PRESETS: BgPreset[] = [ { id: 'orbs', name: 'Орбы', desc: 'Цветовые пятна' }, @@ -22,21 +18,43 @@ export const BG_PRESETS: BgPreset[] = [ { id: 'none', name: 'Нет', desc: 'Чистый фон' }, ] -const KEY = 'pm_bg' +export interface RaysConfig { + count: number // 4–16 + speed: number // 0.2–3.0 (rotation speed multiplier) + brightness: number // 0.3–2.5 + spread: number // 0.3–2.0 (ray width multiplier) +} + +export const DEFAULT_RAYS: RaysConfig = { count: 9, speed: 1, brightness: 1, spread: 1 } + +const KEY_BG = 'pm_bg' +const KEY_RAYS = 'pm_rays' interface BgStore { bgMode: BgMode + raysConfig: RaysConfig setBg: (mode: BgMode) => void + setRaysConfig: (cfg: Partial) => void } -export const useBgStore = create((set) => ({ +export const useBgStore = create((set, get) => ({ bgMode: (() => { if (typeof window === 'undefined') return 'orbs' - const saved = localStorage.getItem(KEY) as BgMode | null + const saved = localStorage.getItem(KEY_BG) as BgMode | null return saved && BG_PRESETS.some(p => p.id === saved) ? saved : 'orbs' })(), + raysConfig: (() => { + if (typeof window === 'undefined') return DEFAULT_RAYS + try { return { ...DEFAULT_RAYS, ...JSON.parse(localStorage.getItem(KEY_RAYS) || '{}') } } + catch { return DEFAULT_RAYS } + })(), setBg: (mode) => { - if (typeof window !== 'undefined') localStorage.setItem(KEY, mode) + if (typeof window !== 'undefined') localStorage.setItem(KEY_BG, mode) set({ bgMode: mode }) }, + setRaysConfig: (cfg) => { + const next = { ...get().raysConfig, ...cfg } + if (typeof window !== 'undefined') localStorage.setItem(KEY_RAYS, JSON.stringify(next)) + set({ raysConfig: next }) + }, }))