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)
============================================================ */