diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html
index e51a2b9..f5790ea 100644
--- a/plugin/contents/ui/dancer.html
+++ b/plugin/contents/ui/dancer.html
@@ -1264,17 +1264,6 @@ function glareFactor(x, y, W, H, on) {
return Math.max(0, Math.min(1, (d - 150) / 240));
}
-/* ── Via Láctea: partículas geradas em coords galácticas (RNG com semente,
- estável entre reloads) e convertidas p/ equatorial J2000 uma única vez ── */
-function mulberry32(seed) {
- return function () {
- seed |= 0; seed = seed + 0x6D2B79F5 | 0;
- let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
- };
-}
-
/* Transposta da matriz equatorial->galáctica (Hipparcos, J2000) */
const G2E = [
[-0.0548755604, 0.4941094279, -0.8676661490],
@@ -1282,94 +1271,151 @@ const G2E = [
[-0.4838350155, 0.7469822445, 0.4559837762],
];
-/* ── Via Láctea estilizada: fita suave + poucas nuvens grandes ──
- Âncora astronômica real (equador galáctico -> gira com o céu de verdade),
- mas pintura surrealista: sprites grandes e macios, sem ruído de pontos */
-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)];
-}
-
-const mwBand = []; // fita + nuvens (aditivo) {ra, dec, size, alpha, ci}
-const mwShadow = []; // Grande Fenda (recorte) {ra, dec, size, alpha}
-(function buildMilkyWay() {
- /* fita contínua: sprites grandes bem sobrepostos ao longo de b~0 */
- for (let l = 0; l < 360; l += 2.5) {
- const dl = Math.min(l, 360 - l);
- const central = Math.exp(-((dl / 80) ** 2));
- const wobble = 1.2 * Math.sin(l * DEG * 3.1) + 0.8 * Math.sin(l * DEG * 1.3 + 2);
- const eq = galToEq(l, wobble * 0.6);
- mwBand.push({
- ra: eq.ra, dec: eq.dec,
- size: 170 + 170 * central,
- alpha: 0.05 + 0.13 * central,
- ci: mwColor(dl),
- });
- }
- /* bojo + poucas nuvens grandes em regiões reais (Carina, Cygnus...) */
- const clouds = [
- { l: 0, b: 0, s: 420, a: 0.20 }, // bojo central (Sagitário)
- { l: 8, b: -1.5, s: 300, a: 0.15 },
- { l: 352, b: 1.5, s: 300, a: 0.15 },
- { l: 25, b: 1, s: 240, a: 0.11 }, // Scutum
- { l: 330, b: -1, s: 260, a: 0.12 },
- { l: 55, b: 0.5, s: 210, a: 0.09 },
- { l: 285, b: -0.5, s: 230, a: 0.10 }, // Carina
- { l: 80, b: 1, s: 190, a: 0.08 }, // Cygnus
- ];
- for (const c of clouds) {
- const eq = galToEq(c.l, c.b);
- mwBand.push({ ra: eq.ra, dec: eq.dec, size: c.s, alpha: c.a,
- ci: mwColor(Math.min(c.l, 360 - c.l)) });
- }
- /* Grande Fenda: sombras grandes e macias serpenteando sobre a fita */
- for (let l = -58; l <= 42; l += 6) {
- const eq = galToEq((l + 360) % 360, -0.6 + 1.6 * Math.sin(l * 0.05 + 0.8));
- mwShadow.push({
- ra: eq.ra, dec: eq.dec,
- size: 150 + 90 * Math.abs(Math.sin(l * 0.11 + 2)),
- alpha: 0.45 + 0.30 * Math.abs(Math.sin(l * 0.07)),
- });
- }
- /* Saco de Carvão — mancha escura junto ao Cruzeiro do Sul */
- const cs = galToEq(303, -1.5);
- mwShadow.push({ ra: cs.ra, dec: cs.dec, size: 170, alpha: 0.8 });
-})();
-
-/* Sprites macios (quente / neutro / frio) + sprite de sombra p/ recorte */
-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)
-const mwShadowSprite = makeMwSprite(255, 255, 255); // recorte via destination-out
-
-/* camada própria da fita — a fenda é recortada antes de compor no céu */
+/* ── Via Láctea procedural contínua ──
+ A textura é calculada no espaço galáctico em uma camada de baixa resolução.
+ Assim a faixa, o bojo e a poeira acompanham a projeção real sem denunciar
+ sprites circulares ou custar um passe em resolução cheia. */
+const MW_RENDER_SCALE = 0.25;
const mwLayer = document.createElement('canvas');
-function spriteFor(ci) { return mwSprites[ci[2] > ci[0] ? 2 : (ci[0] > 240 ? 0 : 1)]; }
+
+function mwHash(ix, iy) {
+ let h = Math.imul(ix, 374761393) + Math.imul(iy, 668265263);
+ h = Math.imul(h ^ (h >>> 13), 1274126177);
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967295;
+}
+
+function mwValueNoise(x, y, periodX) {
+ const fx = Math.floor(x), fy = Math.floor(y);
+ const tx0 = x - fx, ty0 = y - fy;
+ const tx = tx0 * tx0 * (3 - 2 * tx0);
+ const ty = ty0 * ty0 * (3 - 2 * ty0);
+ const wrap = n => ((n % periodX) + periodX) % periodX;
+ const a = mwHash(wrap(fx), fy);
+ const b = mwHash(wrap(fx + 1), fy);
+ const c = mwHash(wrap(fx), fy + 1);
+ const d = mwHash(wrap(fx + 1), fy + 1);
+ const top = a + (b - a) * tx;
+ const bot = c + (d - c) * tx;
+ return top + (bot - top) * ty;
+}
+
+/* Ruído periódico em longitude: não deixa emenda em l=0°/360°. */
+function mwFbm(l, b) {
+ let value = 0, norm = 0, amp = 1;
+ for (const period of [4, 8, 16, 32]) {
+ value += amp * mwValueNoise(l / 360 * period, (b + 90) / 360 * period, period);
+ norm += amp;
+ amp *= 0.52;
+ }
+ return value / norm;
+}
+
+function mwAngularDistance(l, target) {
+ const d = Math.abs(l - target);
+ return Math.min(d, 360 - d);
+}
+
+/* Densidade luminosa em coordenadas galácticas. O detalhe vem do ruído,
+ enquanto as formas grandes continuam astronomicamente reconhecíveis. */
+function mwDensity(l, b) {
+ const dl = Math.min(l, 360 - l);
+ const core = Math.exp(-((dl / 72) ** 2));
+ const width = 4.2 + 5.2 * core;
+ const cloud = mwFbm(l, b);
+ const fine = mwFbm((l * 2) % 360, b * 2.2);
+
+ let density = Math.exp(-0.5 * (b / width) ** 2) *
+ (0.34 + 1.05 * cloud ** 1.7);
+ density += 0.16 * Math.exp(-0.5 * (b / (width * 2.6)) ** 2) *
+ (0.45 + 0.55 * cloud);
+ density += 1.05 * Math.exp(-0.5 * ((dl / 19) ** 2 + (b / 11) ** 2)) *
+ (0.65 + 0.45 * cloud);
+
+ /* Grande Fenda: duas trilhas estreitas, deformadas e com força irregular. */
+ const riftReach = 1 - Math.max(0, Math.min(1, (dl - 70) / 55));
+ const lr = l * DEG;
+ const riftA = 0.7 + 1.45 * Math.sin(lr * 2.1 + 0.5) + 0.45 * Math.sin(lr * 7.0);
+ const riftB = -2.3 + 0.75 * Math.sin(lr * 1.4 + 2.2);
+ const darkA = Math.exp(-0.5 * ((b - riftA) / (1.0 + 0.7 * fine)) ** 2) *
+ (0.58 + 0.28 * fine);
+ const darkB = Math.exp(-0.5 * ((b - riftB) / 1.15) ** 2) * 0.48;
+ density *= 1 - Math.min(0.90, (darkA + darkB) * riftReach);
+
+ /* Saco de Carvão junto ao Cruzeiro do Sul. */
+ const coal = Math.exp(-0.5 * ((mwAngularDistance(l, 303) / 3.8) ** 2 +
+ ((b + 1.5) / 2.8) ** 2));
+ density *= 1 - 0.84 * coal;
+
+ return Math.max(0, density * (0.24 + 0.76 * Math.exp(-((dl / 105) ** 2))));
+}
+
+function mwRgb(dl) {
+ const t = Math.min(1, dl / 145);
+ const warm = [255, 220, 181], neutral = [222, 226, 235], cool = [177, 207, 244];
+ const a = t < 0.5 ? warm : neutral;
+ const z = t < 0.5 ? neutral : cool;
+ const u = t < 0.5 ? t * 2 : (t - 0.5) * 2;
+ return [a[0] + (z[0] - a[0]) * u,
+ a[1] + (z[1] - a[1]) * u,
+ a[2] + (z[2] - a[2]) * u];
+}
+
+function renderMilkyWay(W, H, horizonPx, lst, glareOn) {
+ const rw = Math.max(1, Math.ceil(W * MW_RENDER_SCALE));
+ const rh = Math.max(1, Math.ceil(horizonPx * MW_RENDER_SCALE));
+ if (mwLayer.width !== rw || mwLayer.height !== rh) {
+ mwLayer.width = rw;
+ mwLayer.height = rh;
+ }
+ const mctx = mwLayer.getContext('2d');
+ const image = mctx.createImageData(rw, rh);
+ const pixels = image.data;
+ const sl = Math.sin(obsLat * DEG), cl = Math.cos(obsLat * DEG);
+ const lstRad = lst * DEG, cosLst = Math.cos(lstRad), sinLst = Math.sin(lstRad);
+
+ const azSin = new Float32Array(rw), azCos = new Float32Array(rw);
+ for (let x = 0; x < rw; x++) {
+ const az = (VIEW_AZ + ((x + 0.5) / rw - 0.5) * FOV_AZ) * DEG;
+ azSin[x] = Math.sin(az);
+ azCos[x] = Math.cos(az);
+ }
+
+ let offset = 0;
+ for (let y = 0; y < rh; y++) {
+ const screenY = (y + 0.5) / rh * horizonPx;
+ const alt = ALT_TOP * (1 - screenY / horizonPx);
+ const sa = Math.sin(alt * DEG), ca = Math.cos(alt * DEG);
+ const ext = extinction(alt);
+ for (let x = 0; x < rw; x++, offset += 4) {
+ const north = ca * azCos[x], east = ca * azSin[x];
+ const hourX = -north * sl + sa * cl;
+ const ez = north * cl + sa * sl;
+ const ex = cosLst * hourX - sinLst * east;
+ const ey = sinLst * hourX + cosLst * east;
+
+ /* E2G = transposta de G2E. */
+ const gx = G2E[0][0] * ex + G2E[1][0] * ey + G2E[2][0] * ez;
+ const gy = G2E[0][1] * ex + G2E[1][1] * ey + G2E[2][1] * ez;
+ const gz = G2E[0][2] * ex + G2E[1][2] * ey + G2E[2][2] * ez;
+ const l = (Math.atan2(gy, gx) / DEG + 360) % 360;
+ const b = Math.asin(Math.max(-1, Math.min(1, gz))) / DEG;
+ if (Math.abs(b) > 35) continue;
+
+ const density = mwDensity(l, b);
+ if (density < 0.012) continue;
+ const screenX = (x + 0.5) / rw * W;
+ const glare = glareFactor(screenX, screenY, W, H, glareOn);
+ const alpha = Math.min(0.26, density * 0.145) * ext * glare;
+ if (alpha < 0.002) continue;
+ const rgb = mwRgb(Math.min(l, 360 - l));
+ pixels[offset] = rgb[0];
+ pixels[offset + 1] = rgb[1];
+ pixels[offset + 2] = rgb[2];
+ pixels[offset + 3] = alpha * 255;
+ }
+ }
+ mctx.putImageData(image, 0, 0);
+}
/* ── 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)
@@ -1390,34 +1436,10 @@ function projectSky() {
!document.body.classList.contains('chuva');
twinkList = [];
- /* fita da Via Láctea na camada própria, fenda recortada, depois composta */
- const sScale = H / 1080;
- mwLayer.width = W;
- mwLayer.height = H;
- const mctx = mwLayer.getContext('2d');
- mctx.globalCompositeOperation = 'lighter';
- for (const p of mwBand) {
- 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.004) continue;
- const s = p.size * sScale;
- mctx.globalAlpha = a;
- mctx.drawImage(spriteFor(p.ci), sc.x - s / 2, sc.y - s / 2, s, s);
- }
- mctx.globalCompositeOperation = 'destination-out';
- for (const p of mwShadow) {
- const aa = altAz(p.ra, p.dec, lst);
- const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
- if (!sc) continue;
- const s = p.size * sScale;
- mctx.globalAlpha = p.alpha;
- mctx.drawImage(mwShadowSprite, sc.x - s / 2, sc.y - s / 2, s, s);
- }
- mctx.globalAlpha = 1;
- mctx.globalCompositeOperation = 'source-over';
- bctx.drawImage(mwLayer, 0, 0);
+ renderMilkyWay(W, H, horizonPx, lst, glareOn);
+ bctx.imageSmoothingEnabled = true;
+ bctx.imageSmoothingQuality = 'high';
+ bctx.drawImage(mwLayer, 0, 0, W, horizonPx);
for (let i = 0; i < STAR_DATA.length; i++) {
const [ra, dec, mag, ci] = STAR_DATA[i];