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++) {