fix: tune orbs/aurora/pulse/rain, redesign rays with settings

Orbs: opacity halved — ambient, not a flood of color.
Aurora: opacity halved, 6 bands, slower movement.
Pulse: fast bass tracker (0.45 factor) for onset detection; ambient
  rings so mode stays alive without audio playing.
Rain: 30 drops (was 65), 1.4px wide, soft gradient, bass mult 2.5x.
Rays: complete redesign — tapered triangle beams from center using
  linear gradients; secondary counter-rotating pink layer; center glow.
  Reads raysConfig (count/speed/brightness/spread) via ref — no restart.
Settings: rays config panel (4 sliders + reset) appears when rays active.
bgStore: RaysConfig interface, setRaysConfig, localStorage persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 21:12:34 +03:00
parent 7f2f6f7e44
commit 87ba7a0ecf
3 changed files with 218 additions and 149 deletions

View File

@@ -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<BgMode, React.ReactNode> = {
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() {
)
})}
</div>
{/* Rays config — shown only when rays mode is active */}
{bgMode === 'rays' && (
<div className="mt-4 p-4 bg-surface2 rounded-[10px] border border-white/[0.07]">
<div className="flex items-center justify-between mb-3">
<p className="text-[11px] font-display font-semibold tracking-[1.2px] uppercase text-muted">
Настройки лучей
</p>
<button
onClick={() => setRaysConfig(DEFAULT_RAYS)}
className="text-[10px] font-display font-semibold text-muted hover:text-app-text transition-colors cursor-pointer"
>
Сбросить
</button>
</div>
<div className="flex flex-col gap-4">
{([
{ 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 }) => (
<div key={key}>
<div className="flex items-center justify-between mb-1.5">
<span className="text-[12px] text-app-text/70">{label}</span>
<span className="text-[12px] font-mono font-medium" style={{ color: 'var(--accent)' }}>
{fmt(raysConfig[key])}
</span>
</div>
<input
type="range"
min={min} max={max} step={step}
value={raysConfig[key]}
onChange={(e) => 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)' }}
/>
</div>
))}
</div>
</div>
)}
</section>
</div>
</main>

View File

@@ -11,7 +11,11 @@ type Drop = { x: number; y: number; speed: number; len: number; alpha: number }
export default function AudioBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const { bgMode } = useBgStore()
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<ArrayBuffer> | 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
// 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,${a3})`); g3.addColorStop(1, 'rgba(140,100,255,0)')
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)
// 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, 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)`)
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<string, () => void>)[bgMode] ?? drawOrbs
ctx.clearRect(0, 0, canvas.width, canvas.height)
const loop = () => { tick(); drawFn(); rafId = requestAnimationFrame(loop) }
loop()

View File

@@ -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 // 416
speed: number // 0.23.0 (rotation speed multiplier)
brightness: number // 0.32.5
spread: number // 0.32.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<RaysConfig>) => void
}
export const useBgStore = create<BgStore>((set) => ({
export const useBgStore = create<BgStore>((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 })
},
}))