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];