From 2f4994918d4344f8eb229f036b57053d9f0a2f47 Mon Sep 17 00:00:00 2001 From: vini Date: Sun, 19 Jul 2026 11:07:54 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Via=20L=C3=A1ctea=20estilo=20astrofoto?= =?UTF-8?q?=20+=20capa/letra=20trocadas=20de=20lado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Via Láctea em três camadas ancoradas em coordenadas galácticas reais: véu difuso (3600 blobs) com gradiente térmico — creme-âmbar no núcleo, azul no anticentro —, grão estelar fino (9500 pontos sub-pixel) e estrutura: bojo central, Grande Fenda + faixa de poeira secundária, 14 nuvens estelares (Scutum/Sagitário/Carina). Layout: capa do álbum vai para a esquerda, letra para a direita (alinhada à direita). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj --- plugin/contents/ui/dancer.html | 145 +++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 36 deletions(-) diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html index 85ab375..ed8919e 100644 --- a/plugin/contents/ui/dancer.html +++ b/plugin/contents/ui/dancer.html @@ -164,7 +164,7 @@ body.balada .nebula::after { opacity: 1; } .album-art { position: absolute; top: 4%; - right: 2%; + left: 2%; width: 330px; height: 330px; border-radius: 6px; @@ -254,7 +254,7 @@ body.balada .music-ticker { ============================================================ */ .lyric-display { position: absolute; - left: 2%; + right: 2%; top: 4%; width: 38%; height: 150px; @@ -273,6 +273,7 @@ body.balada .music-ticker { .lyric-ctx { display: block; width: 100%; + text-align: right; /* letra ancorada à direita da tela */ font-family: 'Courier New', monospace; letter-spacing: 0.02em; white-space: normal; @@ -1281,51 +1282,114 @@ const G2E = [ [-0.4838350155, 0.7469822445, 0.4559837762], ]; -const MW_COUNT = 2600; -const mwPoints = []; +/* Visual de astrofoto de longa exposição: grão estelar fino + brilho difuso + com gradiente térmico (núcleo âmbar -> anticentro azul) + faixas de poeira */ +function galToEq(l, b) { + const cb = Math.cos(b * DEG); + const gx = cb * Math.cos(l * DEG), gy = cb * Math.sin(l * DEG), gz = Math.sin(b * DEG); + return { + ra: (Math.atan2( + G2E[1][0] * gx + G2E[1][1] * gy + G2E[1][2] * gz, + G2E[0][0] * gx + G2E[0][1] * gy + G2E[0][2] * gz) / DEG + 360) % 360, + dec: Math.asin(G2E[2][0] * gx + G2E[2][1] * gy + G2E[2][2] * gz) / DEG, + }; +} + +/* Cor por distância angular ao centro galáctico: creme-âmbar -> neutro -> azul */ +function mwColor(dl) { + const t = Math.min(1, dl / 140); + const mix = (a, b2, c) => Math.round(t < 0.5 ? a + (b2 - a) * t * 2 : b2 + (c - b2) * (t - 0.5) * 2); + return [mix(255, 236, 186), mix(226, 224, 205), mix(188, 214, 242)]; +} + +/* Faixas de poeira (Grande Fenda + secundária): 1 = livre, ~0.1 = dentro da poeira */ +function mwDust(l, b, dl) { + if (dl > 100) return 1; + const fade = dl > 70 ? (100 - dl) / 30 : 1; // fenda some longe do centro + const lane1 = Math.abs(b - 1.6 * Math.sin(l * DEG * 2.2)) < + (2.2 + 1.2 * Math.sin(l * 0.7)) * fade; + const lane2 = Math.abs(b + 2.6 - 1.2 * Math.sin(l * DEG * 1.4 + 1.7)) < + (1.3 + 0.8 * Math.sin(l * 1.3 + 0.5)) * fade; + return (lane1 || lane2) ? 0.1 : 1; +} + +/* Nuvens estelares (Scutum, Sagitário, Carina...) — adensamentos fixos na banda */ +const MW_CLOUDS = []; +const mwGlow = []; // blobs difusos {ra,dec,size,alpha,ci} +const mwGrain = []; // grão estelar fino {ra,dec,alpha,rgb} (function buildMilkyWay() { const rand = mulberry32(0x5EEDED); const gauss = () => { const u = Math.max(rand(), 1e-9), v = rand(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); }; + for (let i = 0; i < 14; i++) { + const l = (rand() * 160 - 80 + 360) % 360; // nuvens no lado do centro + MW_CLOUDS.push({ l, b: gauss() * 2.5, r: 2 + rand() * 4 }); + } + const cloudBoost = (l, b) => { + let boost = 0; + for (const c of MW_CLOUDS) { + const dlc = Math.min(Math.abs(l - c.l), 360 - Math.abs(l - c.l)); + const d2 = (dlc / c.r) ** 2 + ((b - c.b) / c.r) ** 2; + if (d2 < 1) boost = Math.max(boost, 1 - d2); + } + return boost; + }; + let guard = 0; - while (mwPoints.length < MW_COUNT && guard++ < MW_COUNT * 50) { + while (mwGlow.length < 3600 && guard++ < 300000) { const l = rand() * 360; - const dl = Math.min(l, 360 - l); // distância angular do centro galáctico + const dl = Math.min(l, 360 - l); const central = Math.exp(-((dl / 75) ** 2)); - if (rand() > 0.30 + 0.70 * central) continue; // banda mais densa perto do centro - const b = gauss() * (5.5 + 3.5 * central); - /* Grande Fenda: faixa escura serpenteante perto de b=0 no lado do centro */ - const rift = dl < 85 && - Math.abs(b - 1.8 * Math.sin(l * DEG * 2.2)) < 2.6 + 1.4 * Math.sin(l * 0.9); - const bright = (0.35 + 0.65 * central) * (rift ? 0.25 : 1); - const cb = Math.cos(b * DEG); - const gx = cb * Math.cos(l * DEG), gy = cb * Math.sin(l * DEG), gz = Math.sin(b * DEG); - const ex = G2E[0][0] * gx + G2E[0][1] * gy + G2E[0][2] * gz; - const ey = G2E[1][0] * gx + G2E[1][1] * gy + G2E[1][2] * gz; - const ez = G2E[2][0] * gx + G2E[2][1] * gy + G2E[2][2] * gz; - mwPoints.push({ - ra: (Math.atan2(ey, ex) / DEG + 360) % 360, - dec: Math.asin(ez) / DEG, - size: 26 + rand() * 55, - alpha: bright * (0.04 + rand() * 0.075), - }); + if (rand() > 0.28 + 0.72 * central) continue; + /* bojo central: banda mais gorda perto do núcleo */ + const b = gauss() * (5 + 3 * central + 4 * Math.exp(-((dl / 22) ** 2))); + const dust = mwDust(l, b, dl); + const boost = cloudBoost(l, b); + const alpha = (0.35 + 0.65 * central) * dust * (1 + 1.2 * boost) * + (0.028 + rand() * 0.05); + if (alpha < 0.005) continue; + const eq = galToEq(l, b); + mwGlow.push({ ra: eq.ra, dec: eq.dec, size: 14 + rand() * 30, + alpha, ci: mwColor(dl) }); + } + + guard = 0; + while (mwGrain.length < 9500 && guard++ < 500000) { + const l = rand() * 360; + const dl = Math.min(l, 360 - l); + const central = Math.exp(-((dl / 80) ** 2)); + if (rand() > 0.22 + 0.78 * central) continue; + const b = gauss() * (4.5 + 2.5 * central + 3.5 * Math.exp(-((dl / 22) ** 2))); + const dust = mwDust(l, b, dl); + if (rand() > dust + 0.06) continue; // poeira esconde o grão + const boost = cloudBoost(l, b); + if (boost < 0.3 && rand() < 0.25 * central) continue; + const eq = galToEq(l, b); + mwGrain.push({ ra: eq.ra, dec: eq.dec, + alpha: 0.10 + rand() * 0.30 + 0.2 * boost, + ci: mwColor(dl) }); } })(); -/* Sprite suave reutilizado por todas as partículas da Via Láctea */ -const mwSprite = document.createElement('canvas'); -mwSprite.width = mwSprite.height = 64; -(function () { - const s = mwSprite.getContext('2d'); - const g = s.createRadialGradient(32, 32, 0, 32, 32, 32); - g.addColorStop(0, 'rgba(210,220,255,1)'); - g.addColorStop(0.5, 'rgba(190,205,250,0.4)'); - g.addColorStop(1, 'rgba(180,200,250,0)'); - s.fillStyle = g; +/* Sprites do brilho difuso: quente / neutro / frio (escolhido por cor) */ +function makeMwSprite(r, g, b) { + const c = document.createElement('canvas'); + c.width = c.height = 64; + const s = c.getContext('2d'); + const grad = s.createRadialGradient(32, 32, 0, 32, 32, 32); + grad.addColorStop(0, `rgba(${r},${g},${b},1)`); + grad.addColorStop(0.5, `rgba(${r},${g},${b},0.4)`); + grad.addColorStop(1, `rgba(${r},${g},${b},0)`); + s.fillStyle = grad; s.fillRect(0, 0, 64, 64); -})(); + return c; +} +const mwSprites = [makeMwSprite(255, 226, 188), // quente (núcleo) + makeMwSprite(236, 224, 214), // neutro + makeMwSprite(188, 214, 242)]; // frio (anticentro) +function spriteFor(ci) { return mwSprites[ci[2] > ci[0] ? 2 : (ci[0] > 240 ? 0 : 1)]; } /* ── Passo de projeção: recalcula posições de tela (a cada 30s o céu gira ~0.125°, imperceptível). Camada estática (Via Láctea + estrelas fracas) @@ -1347,16 +1411,25 @@ function projectSky() { twinkList = []; bctx.globalCompositeOperation = 'lighter'; - for (const p of mwPoints) { + for (const p of mwGlow) { const aa = altAz(p.ra, p.dec, lst); const sc = skyToScreen(aa.alt, aa.az, W, horizonPx); if (!sc) continue; const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn); if (a < 0.002) continue; bctx.globalAlpha = a; - bctx.drawImage(mwSprite, sc.x - p.size / 2, sc.y - p.size / 2, p.size, p.size); + bctx.drawImage(spriteFor(p.ci), sc.x - p.size / 2, sc.y - p.size / 2, p.size, p.size); } bctx.globalAlpha = 1; + for (const p of mwGrain) { + const aa = altAz(p.ra, p.dec, lst); + const sc = skyToScreen(aa.alt, aa.az, W, horizonPx); + if (!sc) continue; + const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn); + if (a < 0.01) continue; + bctx.fillStyle = `rgba(${p.ci[0]},${p.ci[1]},${p.ci[2]},${a.toFixed(3)})`; + bctx.fillRect(sc.x - 0.5, sc.y - 0.5, 1, 1); + } bctx.globalCompositeOperation = 'source-over'; for (let i = 0; i < STAR_DATA.length; i++) {