Revert "feat: Via Láctea real — mapa all-sky do Gaia amostrado por pixel"

This reverts commit c4c9574946.
This commit is contained in:
vini 2026-07-19 11:26:52 -03:00
parent c4c9574946
commit fd7f131b26
4 changed files with 168 additions and 165 deletions

View file

@ -34,10 +34,8 @@ plugin/
main.qml # Plasma WallpaperItem — hosts everything
dancer.html # Self-contained animation (HTML + CSS + JS)
starcatalog.js # Generated star data (Yale BSC) — see tools/build_catalog.py
milkyway.jpg # Gaia all-sky map (ESA/Gaia/DPAC, CC BY-SA 3.0 IGO) — see tools/build_skymap.py
tools/
build_catalog.py # Regenerates starcatalog.js from the Yale Bright Star Catalog
build_skymap.py # Regenerates milkyway.jpg (Hammer -> plate carrée) from ESA's Gaia map
```
### main.qml — the Plasma host
@ -52,7 +50,7 @@ Pure HTML/CSS/JS, no external dependencies. All visuals are self-contained:
| Layer | Technique |
|---|---|
| Realistic sky (stars + Milky Way) | `<canvas>`; real-time alt-az projection of `starcatalog.js` (2,887 Yale BSC stars) + per-pixel sampling of the real Gaia all-sky map (`milkyway.jpg`) in galactic coords, for the configured lat/long (default Toledo-PR), view facing east. Static layer pre-rendered offscreen, bright stars twinkle per frame; reprojects every 30 s. Debug: `window.__skyTimeOffset` (ms) fast-forwards the sky |
| Realistic sky (stars + Milky Way) | `<canvas>`; real-time alt-az projection of `starcatalog.js` (2,887 Yale BSC stars) + seeded procedural Milky Way for the configured lat/long (default Toledo-PR). Static layer pre-rendered offscreen, bright stars twinkle per frame; reprojects every 30 s. Debug: `window.__skyTimeOffset` (ms) fast-forwards the sky |
| Nebula blobs, grid floor, horizon | CSS animations |
| DANCEBOT-9000 robot | Pure CSS (divs + `@keyframes`) |
| Chest EQ bars & VU meter | JS-driven `requestAnimationFrame` |

View file

@ -1264,99 +1264,161 @@ function glareFactor(x, y, W, H, on) {
return Math.max(0, Math.min(1, (d - 150) / 240));
}
/* ── Via Láctea REAL: mapa all-sky do Gaia ("Gaia's sky in colour",
ESA/Gaia/DPAC, CC BY-SA 3.0 IGO — reprojetado por tools/build_skymap.py).
Cada pixel do céu é amostrado em (l,b) galácticas reais via tempo sideral:
é a banda fotografada, nascendo/girando/se pondo com o céu de verdade ── */
const MW_INTENSITY = 0.62; // ganho global da banda
const MW_BLACK = 52; // nível de preto (remove o véu cinza do mapa)
const MW_SCALE = 4; // amostra a 1/4 da resolução (difuso -> upscale ok)
/* ── 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;
};
}
/* Matriz equatorial -> galáctica (Hipparcos, J2000) */
const E2G = [
[-0.0548755604, -0.8734370902, -0.4838350155],
[ 0.4941094279, -0.4448296300, 0.7469822445],
[-0.8676661490, -0.1980763734, 0.4559837762],
/* Transposta da matriz equatorial->galáctica (Hipparcos, J2000) */
const G2E = [
[-0.0548755604, 0.4941094279, -0.8676661490],
[-0.8734370902, -0.4448296300, -0.1980763734],
[-0.4838350155, 0.7469822445, 0.4559837762],
];
const mwTex = { data: null, w: 0, h: 0 };
const mwImg = new Image();
mwImg.onload = () => {
/* 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)];
}
/* Nuvens escuras discretas em posições reais aproximadas
(Saco de Carvão junto ao Cruzeiro, região do Cachimbo em Ofiúco...) */
const MW_DARKS = [
{ l: 303, b: -1.5, rl: 3.5, rb: 3.0, s: 0.85 }, // Saco de Carvão
{ l: 357, b: 4.5, rl: 5.0, rb: 2.2, s: 0.80 }, // Cachimbo (Ofiúco)
{ l: 31, b: -1.0, rl: 4.0, rb: 1.8, s: 0.75 },
{ l: 16, b: 2.5, rl: 3.0, rb: 1.6, s: 0.70 },
{ l: 344, b: -2.5, rl: 3.2, rb: 1.8, s: 0.70 },
];
/* Contribuição de uma faixa de poeira com borda suave (0 = fora, s = centro) */
function laneDark(b, center, halfw, strength) {
const d = Math.abs(b - center) / Math.max(halfw, 0.3);
return d >= 1 ? 0 : strength * (1 - d * d) ** 2;
}
/* Poeira: 1 = céu livre, ->0.08 = poeira densa. Caminhos e larguras
irregulares (senos incomensuráveis) + escuridão em manchas, não uniforme */
function mwDust(l, b, dl) {
if (dl > 105) return 1;
const fade = Math.min(1, (105 - dl) / 35); // fenda some longe do centro
const x = l * DEG;
const wob1 = Math.sin(x * 2.2) + 0.6 * Math.sin(x * 4.9 + 1.7) +
0.35 * Math.sin(x * 9.3 + 0.6);
const wob2 = Math.sin(x * 1.4 + 1.7) + 0.6 * Math.sin(x * 3.7 + 4.2);
const patch1 = 0.62 + 0.38 * Math.sin(x * 1.1 + 2.9) * Math.sin(x * 2.6 + 0.4);
const patch2 = 0.55 + 0.45 * Math.sin(x * 0.9 + 5.1);
let dark = laneDark(b, 0.9 * wob1, 2.1 + 0.9 * Math.sin(x * 3.3 + 2.2),
0.9 * patch1);
dark = Math.max(dark, laneDark(b, -2.4 + 0.7 * wob2,
1.4 + 0.7 * Math.sin(x * 2.8 + 1.1),
0.8 * patch2));
for (const d of MW_DARKS) {
const dlc = Math.min(Math.abs(l - d.l), 360 - Math.abs(l - d.l));
const dd = (dlc / d.rl) ** 2 + ((b - d.b) / d.rb) ** 2;
if (dd < 1) dark = Math.max(dark, d.s * (1 - dd));
}
return 1 - Math.min(0.92, dark * fade);
}
/* 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 (mwGlow.length < 3600 && guard++ < 300000) {
const l = rand() * 360;
const dl = Math.min(l, 360 - l);
const central = Math.exp(-((dl / 75) ** 2));
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) });
}
})();
/* Sprites do brilho difuso: quente / neutro / frio (escolhido por cor) */
function makeMwSprite(r, g, b) {
const c = document.createElement('canvas');
c.width = mwImg.width; c.height = mwImg.height;
const s = c.getContext('2d', { willReadFrequently: true });
s.drawImage(mwImg, 0, 0);
mwTex.data = s.getImageData(0, 0, c.width, c.height).data;
mwTex.w = c.width; mwTex.h = c.height;
projectSky(); // primeira renderização já com a textura
};
mwImg.src = 'milkyway.jpg';
/* Grade pré-computada por pixel (a 1/MW_SCALE): vetor equatorial no frame
LST=0 + extinção + clarão. Por passo só muda a rotação sideral (Rz) */
const mwCanvas = document.createElement('canvas');
let mwGrid = null;
function buildMwGrid() {
const W = canvas.width, H = canvas.height;
if (!W || !H) { mwGrid = null; return; }
const horizonPx = H * HORIZON_F;
const gw = Math.ceil(W / MW_SCALE), gh = Math.ceil(horizonPx / MW_SCALE);
const n = gw * gh;
const wx = new Float32Array(n), wy = new Float32Array(n), wz = new Float32Array(n);
const ext = new Float32Array(n), glr = new Float32Array(n);
const cl = Math.cos(obsLat * DEG), sl = Math.sin(obsLat * DEG);
let i = 0;
for (let gy = 0; gy < gh; gy++) {
const sy = (gy + 0.5) * MW_SCALE;
const alt = (1 - sy / horizonPx) * ALT_TOP;
const ca = Math.cos(alt * DEG), sa = Math.sin(alt * DEG);
for (let gx = 0; gx < gw; gx++, i++) {
const sx = (gx + 0.5) * MW_SCALE;
const az = (VIEW_AZ + (sx / W - 0.5) * FOV_AZ) * DEG;
const xh = ca * Math.cos(az), yh = ca * Math.sin(az);
wx[i] = -sl * xh + cl * sa; // cosδ·cosH
wy[i] = -yh; // cosδ·sinH
wz[i] = cl * xh + sl * sa; // sinδ
ext[i] = extinction(alt);
glr[i] = glareFactor(sx, sy, W, H, true);
}
}
mwCanvas.width = gw; mwCanvas.height = gh;
mwGrid = { w: gw, h: gh, wx, wy, wz, ext, glare: glr,
img: mwCanvas.getContext('2d').createImageData(gw, gh) };
}
/* Amostra o mapa galáctico para o LST dado e pinta o mwCanvas */
function renderMilkyWay(lstDeg, glareOn) {
if (!mwGrid || !mwTex.data) return false;
const { wx, wy, wz, ext, glare, img } = mwGrid;
const cL = Math.cos(lstDeg * DEG), sL = Math.sin(lstDeg * DEG);
/* galáctico = E2G · Rz\'(LST), com RA = LST H */
const m00 = E2G[0][0] * cL + E2G[0][1] * sL, m01 = E2G[0][0] * sL - E2G[0][1] * cL, m02 = E2G[0][2];
const m10 = E2G[1][0] * cL + E2G[1][1] * sL, m11 = E2G[1][0] * sL - E2G[1][1] * cL, m12 = E2G[1][2];
const m20 = E2G[2][0] * cL + E2G[2][1] * sL, m21 = E2G[2][0] * sL - E2G[2][1] * cL, m22 = E2G[2][2];
const tw = mwTex.w, thh = mwTex.h, tex = mwTex.data, px = img.data;
const INV2PI = 1 / (2 * Math.PI), INVPI = 1 / Math.PI, HPI = Math.PI / 2;
for (let i = 0, j = 0; i < wx.length; i++, j += 4) {
const gx2 = m00 * wx[i] + m01 * wy[i] + m02 * wz[i];
const gy2 = m10 * wx[i] + m11 * wy[i] + m12 * wz[i];
const gz2 = m20 * wx[i] + m21 * wy[i] + m22 * wz[i];
const l = Math.atan2(gy2, gx2); // rad, + p/ esquerda no mapa
const b = Math.asin(Math.max(-1, Math.min(1, gz2)));
let tx = ((Math.PI - l) * INV2PI * tw) | 0;
let ty = ((HPI - b) * INVPI * thh) | 0;
if (tx >= tw) tx = tw - 1;
if (ty >= thh) ty = thh - 1;
const k = (ty * tw + tx) * 4;
const f = ext[i] * (glareOn ? glare[i] : 1) * MW_INTENSITY;
px[j] = Math.max(0, tex[k] - MW_BLACK) * f;
px[j + 1] = Math.max(0, tex[k + 1] - MW_BLACK) * f;
px[j + 2] = Math.max(0, tex[k + 2] - MW_BLACK) * f;
px[j + 3] = 255;
}
mwCanvas.getContext('2d').putImageData(img, 0, 0);
return true;
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)
@ -1377,12 +1439,27 @@ function projectSky() {
!document.body.classList.contains('chuva');
twinkList = [];
if (renderMilkyWay(lst, glareOn)) {
bctx.globalCompositeOperation = 'lighter';
bctx.imageSmoothingEnabled = true;
bctx.drawImage(mwCanvas, 0, 0, W, horizonPx);
bctx.globalCompositeOperation = 'source-over';
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(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++) {
const [ra, dec, mag, ci] = STAR_DATA[i];
@ -1421,14 +1498,12 @@ function setObserverLocation(lat, lon) {
if (lat === obsLat && lon === obsLon) return;
obsLat = lat;
obsLon = lon;
buildMwGrid(); // grade depende da latitude
projectSky();
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
buildMwGrid();
projectSky();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""Gera plugin/contents/ui/milkyway.jpg a partir do mapa all-sky do Gaia.
Fonte: "Gaia's sky in colour" (ESA/Gaia/DPAC, CC BY-SA 3.0 IGO), 8000x4000,
projeção HAMMER em coordenadas galácticas (elipse, centro l=0/b=0, l crescendo
para a esquerda, b=+90 no topo). Este script reprojeta para plate carrée
(mesma convenção de l/b) e reduz para textura leve.
O dancer.html amostra a textura por pixel via (l, b) reais a banda é a
Via Láctea fotografada de verdade, projetada em tempo real.
Requer Pillow e numpy. Uso: python3 tools/build_skymap.py
"""
import io
import math
import urllib.request
from pathlib import Path
import numpy as np
from PIL import Image
URL = ("https://upload.wikimedia.org/wikipedia/commons/e/ea/"
"Gaia%E2%80%99s_sky_in_colour_ESA393127.png")
CACHE = Path.home() / ".cache/skeledance/gaia_allsky.png"
OUT = Path(__file__).resolve().parent.parent / "plugin/contents/ui/milkyway.jpg"
SIZE = (3072, 1536) # textura final (plate carrée 2:1)
OVERSAMPLE = 2 # reprojeta a 2x e reduz com LANCZOS (anti-aliasing)
QUALITY = 85
def fetch() -> bytes:
if CACHE.exists():
print(f"Usando cache {CACHE}")
return CACHE.read_bytes()
print(f"Baixando {URL} ...")
req = urllib.request.Request(URL, headers={"User-Agent": "skeledance-build/1.0"})
raw = urllib.request.urlopen(req, timeout=180).read()
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_bytes(raw)
print(f" {len(raw) / 1e6:.1f} MB (cacheado)")
return raw
def main():
Image.MAX_IMAGE_PIXELS = None
src = np.asarray(Image.open(io.BytesIO(fetch())).convert("RGB"))
sh, sw = src.shape[:2]
tw, th = SIZE[0] * OVERSAMPLE, SIZE[1] * OVERSAMPLE
# Grade de saída plate carrée: l = 180 -> -180 (esq -> dir), b = 90 -> -90
l = np.deg2rad(180 - (np.arange(tw) + 0.5) * 360 / tw)
b = np.deg2rad(90 - (np.arange(th) + 0.5) * 180 / th)
L, B = np.meshgrid(l, b)
# Projeção Hammer direta -> posição na imagem-fonte
denom = np.sqrt(1 + np.cos(B) * np.cos(L / 2))
x = 2 * math.sqrt(2) * np.cos(B) * np.sin(L / 2) / denom # + = esquerda
y = math.sqrt(2) * np.sin(B) / denom # + = cima
sx = np.clip(((1 - x / (2 * math.sqrt(2))) * sw / 2).astype(int), 0, sw - 1)
sy = np.clip(((1 - y / math.sqrt(2)) * sh / 2).astype(int), 0, sh - 1)
out = Image.fromarray(src[sy, sx])
out = out.resize(SIZE, Image.LANCZOS)
out.save(OUT, "JPEG", quality=QUALITY, optimize=True)
print(f"{OUT} ({OUT.stat().st_size / 1024:.0f} KB, {SIZE[0]}x{SIZE[1]})")
if __name__ == "__main__":
main()