diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html index cd2ef1e..f738d49 100644 --- a/plugin/contents/ui/dancer.html +++ b/plugin/contents/ui/dancer.html @@ -794,16 +794,19 @@ const ctx = canvas.getContext('2d'); const STAR_COUNT = 220; const stars = []; +const STAR_COLORS = [[255,221,170],[204,216,255],[238,238,255]]; + function initStars() { stars.length = 0; for (let i = 0; i < STAR_COUNT; i++) { + const c = STAR_COLORS[Math.random() < 0.15 ? 0 : Math.random() < 0.5 ? 1 : 2]; stars.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height * 0.62, r: Math.random() * 1.6 + 0.2, phase: Math.random() * Math.PI * 2, freq: 0.4 + Math.random() * 1.2, - color: Math.random() < 0.15 ? '#ffddaa' : (Math.random() < 0.5 ? '#ccd8ff' : '#eeeeff'), + cr: c[0], cg: c[1], cb: c[2], }); } } @@ -818,13 +821,10 @@ function drawStars(ts) { ctx.clearRect(0, 0, canvas.width, canvas.height); const t = ts * 0.001; stars.forEach(s => { - const alpha = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase)); + const alpha = (0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase))).toFixed(2); ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); - ctx.fillStyle = s.color.replace(')', `, ${alpha.toFixed(2)})`) - .replace('rgb', 'rgba').replace('#ccd8ff', `rgba(204,216,255,${alpha.toFixed(2)})`) - .replace('#eeeeff', `rgba(238,238,255,${alpha.toFixed(2)})`) - .replace('#ffddaa', `rgba(255,221,170,${alpha.toFixed(2)})`); + ctx.fillStyle = `rgba(${s.cr},${s.cg},${s.cb},${alpha})`; ctx.fill(); }); requestAnimationFrame(drawStars); @@ -845,6 +845,8 @@ const V_LINES = 24; const H_LINES = 24; let floorScroll = 0; let lastFloorTs = 0; +let floorFadeGrad = null; +let floorFadeGradH = 0; function resizeFloor() { floorCanvas.width = window.innerWidth; @@ -891,11 +893,14 @@ function drawFloor(ts) { floorCtx.stroke(); } - // Fade near horizon to hide sub-pixel lines - const fade = floorCtx.createLinearGradient(0, 0, 0, H * 0.3); - fade.addColorStop(0, '#000814'); - fade.addColorStop(1, 'rgba(0,8,20,0)'); - floorCtx.fillStyle = fade; + // Fade near horizon to hide sub-pixel lines (gradient cached, only rebuilt on resize) + if (!floorFadeGrad || floorFadeGradH !== H) { + floorFadeGradH = H; + floorFadeGrad = floorCtx.createLinearGradient(0, 0, 0, H * 0.3); + floorFadeGrad.addColorStop(0, '#000814'); + floorFadeGrad.addColorStop(1, 'rgba(0,8,20,0)'); + } + floorCtx.fillStyle = floorFadeGrad; floorCtx.fillRect(0, 0, W, H * 0.3); requestAnimationFrame(drawFloor); @@ -983,22 +988,29 @@ function detectBeat(vals) { bassEma = bassEma * 0.96 + bass * 0.04; } -// Called with semicolon-separated CAVA values (0–100 per bar) +// Called with semicolon-separated CAVA values (0–100 per bar) — SSE may fire >60fps, +// so we buffer and flush once per animation frame to avoid mid-frame DOM thrashing. +let pendingCava = null; + function updateBars(raw) { - if (!isPlaying) return; - const vals = raw.split(';').map(v => Math.min(100, parseInt(v, 10) || 0)); - detectBeat(vals); - - // VU meter (low bars) - vuBars.forEach((bar, i) => { - const v = vals[i] || 0; - bar.style.height = Math.max(3, (v / 100) * MAX_BAR_H) + 'px'; - bar.style.opacity = 0.3 + 0.7 * (v / 100); - }); - - + pendingCava = raw; } +function flushBars() { + if (pendingCava && isPlaying) { + const vals = pendingCava.split(';').map(v => Math.min(100, parseInt(v, 10) || 0)); + detectBeat(vals); + vuBars.forEach((bar, i) => { + const v = vals[i] || 0; + bar.style.height = Math.max(3, (v / 100) * MAX_BAR_H) + 'px'; + bar.style.opacity = 0.3 + 0.7 * (v / 100); + }); + pendingCava = null; + } + requestAnimationFrame(flushBars); +} +requestAnimationFrame(flushBars); + /* ============================================================ MUSIC-LIGHT THEME (album color) ============================================================ */