feat: bg presets in settings, shuffle playlists, remove theme from ExtraTab
- Add 4 live background modes: Орбы, Волны, Частицы, Аврора + Нет - Settings page shows visual preview cards for each bg mode (persisted) - Add shuffle button (⇄) to PlaylistCard and FavoritesCard - Remove accent color picker from ExtraTab (settings page only) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
'use client'
|
||||
'use client'
|
||||
|
||||
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 }
|
||||
|
||||
export default function AudioBackground() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const { bgMode } = useBgStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (bgMode === 'none') return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
@@ -24,81 +30,204 @@ export default function AudioBackground() {
|
||||
resize()
|
||||
window.addEventListener('resize', resize)
|
||||
|
||||
const draw = () => {
|
||||
rafId = requestAnimationFrame(draw)
|
||||
const W = canvas.width
|
||||
const H = canvas.height
|
||||
|
||||
let rawBass = 0
|
||||
let rawMid = 0
|
||||
|
||||
const tick = () => {
|
||||
let rB = 0, rM = 0
|
||||
if (audioState.analyser && audioState.isPlaying) {
|
||||
const binCount = audioState.analyser.frequencyBinCount
|
||||
if (!dataBuf || dataBuf.length !== binCount) dataBuf = new Uint8Array(new ArrayBuffer(binCount))
|
||||
const n = audioState.analyser.frequencyBinCount
|
||||
if (!dataBuf || dataBuf.length !== n) dataBuf = new Uint8Array(new ArrayBuffer(n))
|
||||
audioState.analyser.getByteFrequencyData(dataBuf)
|
||||
const bassEnd = Math.max(1, Math.ceil(binCount * 0.1))
|
||||
for (let i = 0; i < bassEnd; i++) rawBass = Math.max(rawBass, dataBuf[i] / 255)
|
||||
const midStart = bassEnd
|
||||
const midEnd = Math.ceil(binCount * 0.4)
|
||||
let midSum = 0
|
||||
for (let i = midStart; i < midEnd; i++) midSum += dataBuf[i] / 255
|
||||
rawMid = midSum / Math.max(1, midEnd - midStart)
|
||||
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
|
||||
for (let i = bEnd; i < mEnd; i++) mSum += dataBuf[i] / 255
|
||||
rM = mSum / Math.max(1, mEnd - bEnd)
|
||||
}
|
||||
smoothBass += (rB - smoothBass) * 0.1
|
||||
smoothMid += (rM - smoothMid) * 0.07
|
||||
}
|
||||
|
||||
smoothBass += (rawBass - smoothBass) * 0.1
|
||||
smoothMid += (rawMid - smoothMid) * 0.07
|
||||
const acRgb = () =>
|
||||
document.documentElement.style.getPropertyValue('--accent-rgb') || '200,255,0'
|
||||
|
||||
// ── ORBS ─────────────────────────────────────────────────────────────────
|
||||
const drawOrbs = () => {
|
||||
const W = canvas.width, H = canvas.height
|
||||
const ar = acRgb()
|
||||
const t = Date.now() / 4500
|
||||
const breathe = Math.sin(t) * 0.03 + Math.cos(t * 0.7) * 0.015
|
||||
const b = Math.sin(t) * 0.03 + Math.cos(t * 0.7) * 0.015
|
||||
const diag = Math.hypot(W, H), base = diag * 0.55
|
||||
|
||||
ctx.clearRect(0, 0, W, H)
|
||||
ctx.fillStyle = '#0a0a0f'
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
const accentRgb = document.documentElement.style.getPropertyValue('--accent-rgb') || '200,255,0'
|
||||
const diag = Math.hypot(W, H)
|
||||
const base = diag * 0.55
|
||||
|
||||
// Accent orb — bass driven, top-left
|
||||
const r1 = base * (0.75 + smoothBass * 0.55 + breathe)
|
||||
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(${accentRgb},${a1})`)
|
||||
g1.addColorStop(0.45, `rgba(${accentRgb},${a1 * 0.28})`)
|
||||
g1.addColorStop(1, `rgba(${accentRgb},0)`)
|
||||
ctx.fillStyle = g1
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
g1.addColorStop(0, `rgba(${ar},${a1})`); g1.addColorStop(0.45, `rgba(${ar},${a1 * 0.28})`); g1.addColorStop(1, `rgba(${ar},0)`)
|
||||
ctx.fillStyle = g1; ctx.fillRect(0, 0, W, H)
|
||||
|
||||
// Pink orb — mid driven, bottom-right
|
||||
const r2 = base * (0.65 + smoothMid * 0.45 - breathe * 0.6)
|
||||
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)')
|
||||
ctx.fillStyle = g2
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
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)')
|
||||
ctx.fillStyle = g2; ctx.fillRect(0, 0, W, H)
|
||||
|
||||
// Purple orb — combined, center, fades in when playing
|
||||
const combined = smoothBass * 0.55 + smoothMid * 0.45
|
||||
if (combined > 0.005) {
|
||||
const r3 = base * (0.38 + combined * 0.32)
|
||||
const a3 = combined * 0.035
|
||||
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,${a3})`)
|
||||
g3.addColorStop(1, 'rgba(140,100,255,0)')
|
||||
ctx.fillStyle = g3
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
draw()
|
||||
// ── WAVES ─────────────────────────────────────────────────────────────────
|
||||
const drawWaves = () => {
|
||||
const W = canvas.width, H = canvas.height
|
||||
const ar = acRgb()
|
||||
const t = Date.now() / 1000
|
||||
|
||||
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.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.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},` },
|
||||
]
|
||||
|
||||
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.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)`)
|
||||
ctx.fillStyle = g; ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
}))
|
||||
|
||||
const drawParticles = () => {
|
||||
const W = canvas.width, H = canvas.height
|
||||
const ar = acRgb()
|
||||
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
|
||||
const mv = 0.0012
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
const 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── AURORA ────────────────────────────────────────────────────────────────
|
||||
const drawAurora = () => {
|
||||
const W = canvas.width, H = canvas.height
|
||||
const ar = acRgb()
|
||||
const t = Date.now() / 3000
|
||||
|
||||
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 },
|
||||
]
|
||||
|
||||
for (const band of bands) {
|
||||
const cx = band.cx * W
|
||||
const cy = (band.cy + Math.sin(band.phase) * 0.06) * 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 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()
|
||||
}
|
||||
}
|
||||
|
||||
// ── LOOP ──────────────────────────────────────────────────────────────────
|
||||
const drawFn = { orbs: drawOrbs, waves: drawWaves, particles: drawParticles, aurora: drawAurora }[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)
|
||||
}
|
||||
loop()
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
window.removeEventListener('resize', resize)
|
||||
}
|
||||
}, [])
|
||||
}, [bgMode])
|
||||
|
||||
if (bgMode === 'none') return null
|
||||
|
||||
return (
|
||||
<canvas
|
||||
|
||||
Reference in New Issue
Block a user