feat: céu realista em tempo real — estrelas do BSC + Via Láctea

Substitui os 220 pontos aleatórios por projeção alt-az real: tempo
sideral local (GMST validado contra Meeus 12b), estrelas do catálogo
com cor/brilho reais e Via Láctea procedural gerada em coordenadas
galácticas (matriz Hipparcos J2000) — a banda gira rígida com o céu,
com Grande Fenda e núcleo em Sagitário. Vista para o sul (Cruzeiro,
α Cen e Carina quase sempre em cena). Camada estática pré-renderizada
offscreen; só estrelas de mag<=4 cintilam por frame; reprojeção a cada
30s. Extinção atmosférica no horizonte e clarão ao redor do sol
cenográfico. Debug: window.__skyTimeOffset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
This commit is contained in:
vini 2026-07-19 10:52:05 -03:00
parent 998de4a945
commit 05e465e33f

View file

@ -1198,6 +1198,7 @@ body.chuva #sunChuvaFace { opacity: 1; transition: opacity 5s ease; }
</div><!-- .scene --> </div><!-- .scene -->
<script src="starcatalog.js"></script>
<script> <script>
/* ============================================================ /* ============================================================
CONSTANTS CONSTANTS
@ -1207,47 +1208,215 @@ const BPM = 120;
const BEAT_MS = 60000 / BPM; // 500ms const BEAT_MS = 60000 / BPM; // 500ms
/* ============================================================ /* ============================================================
STARFIELD CÉU REALISTA — Yale BSC (starcatalog.js) + Via Láctea
Posições reais para o observador via tempo sideral local.
O sol vaporwave é cenográfico — só a camada de estrelas é real.
============================================================ */ ============================================================ */
const canvas = document.getElementById('starfield'); const canvas = document.getElementById('starfield');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const STAR_COUNT = 220; let obsLat = -24.72, obsLon = -53.74; // Toledo-PR; sobrescrito pela config
const stars = []; const VIEW_AZ = 180; // centro da tela olha para o SUL: Cruzeiro, α Cen e
// Carina ficam quase sempre em cena, e o céu gira
// visivelmente ao redor do polo sul celeste
const FOV_AZ = 140; // largura da tela em graus de azimute
const ALT_TOP = 75; // altitude no topo da tela
const HORIZON_F = 0.585; // fração da altura da tela até o horizonte
const TWINKLE_MAG = 4.0; // até esta magnitude a estrela cintila; acima é estática
const DEG = Math.PI / 180;
window.__skyTimeOffset = 0; // debug: ms somados ao relógio p/ testar a rotação
const STAR_COLORS = [[255,221,170],[204,216,255],[238,238,255]]; /* Tempo sideral local em graus (GMST + longitude leste) */
function localSiderealDeg(ms) {
const d = ms / 86400000 - 10957.5; // dias desde J2000.0
return ((280.46061837 + 360.98564736629 * d + obsLon) % 360 + 360) % 360;
}
function initStars() { /* RA/Dec (graus) -> altitude/azimute (graus, az a partir do norte p/ leste) */
stars.length = 0; function altAz(raDeg, decDeg, lstDeg) {
for (let i = 0; i < STAR_COUNT; i++) { const H = (lstDeg - raDeg) * DEG;
const c = STAR_COLORS[Math.random() < 0.15 ? 0 : Math.random() < 0.5 ? 1 : 2]; const cd = Math.cos(decDeg * DEG), sd = Math.sin(decDeg * DEG);
stars.push({ const cl = Math.cos(obsLat * DEG), sl = Math.sin(obsLat * DEG);
x: Math.random() * canvas.width, const xh = -cd * Math.cos(H) * sl + sd * cl; // norte
y: Math.random() * canvas.height * 0.62, const yh = -cd * Math.sin(H); // leste
r: Math.random() * 1.6 + 0.2, const zh = cd * Math.cos(H) * cl + sd * sl; // zênite
phase: Math.random() * Math.PI * 2, return { alt: Math.asin(zh) / DEG, az: Math.atan2(yh, xh) / DEG };
freq: 0.4 + Math.random() * 1.2, }
cr: c[0], cg: c[1], cb: c[2],
/* Projeção cilíndrica: azimute -> x, altitude -> y. null = fora da tela */
function skyToScreen(alt, az, W, horizonPx) {
const daz = ((az - VIEW_AZ) % 360 + 540) % 360 - 180;
if (Math.abs(daz) > FOV_AZ / 2 || alt < -3 || alt > ALT_TOP) return null;
return { x: (0.5 + daz / FOV_AZ) * W, y: horizonPx * (1 - alt / ALT_TOP) };
}
/* Extinção atmosférica: estrelas somem suavemente perto do horizonte */
function extinction(alt) {
return Math.max(0, Math.min(1, (alt + 1) / 14));
}
/* Clarão ao redor do sol retrowave (só no modo padrão — nos outros o sol
sobe/encolhe e o próprio elemento cobre o que está atrás) */
function glareFactor(x, y, W, H, on) {
if (!on) return 1;
const d = Math.hypot(x - W / 2, y - H * HORIZON_F);
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],
[-0.8734370902, -0.4448296300, -0.1980763734],
[-0.4838350155, 0.7469822445, 0.4559837762],
];
const MW_COUNT = 2600;
const mwPoints = [];
(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);
};
let guard = 0;
while (mwPoints.length < MW_COUNT && guard++ < MW_COUNT * 50) {
const l = rand() * 360;
const dl = Math.min(l, 360 - l); // distância angular do centro galáctico
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.012 + rand() * 0.03),
}); });
} }
})();
/* 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;
s.fillRect(0, 0, 64, 64);
})();
/* ── 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)
é pré-renderizada em baseCanvas; só as brilhantes cintilam por frame ── */
const baseCanvas = document.createElement('canvas');
let twinkList = [];
let lastProjMs = -Infinity;
function projectSky() {
const W = canvas.width, H = canvas.height;
if (!W || !H) return;
baseCanvas.width = W;
baseCanvas.height = H;
const bctx = baseCanvas.getContext('2d');
const horizonPx = H * HORIZON_F;
const lst = localSiderealDeg(Date.now() + window.__skyTimeOffset);
const glareOn = !document.body.classList.contains('balada') &&
!document.body.classList.contains('chuva');
twinkList = [];
bctx.globalCompositeOperation = 'lighter';
for (const p of mwPoints) {
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.globalAlpha = 1;
bctx.globalCompositeOperation = 'source-over';
for (let i = 0; i < STAR_DATA.length; i++) {
const [ra, dec, mag, ci] = STAR_DATA[i];
const aa = altAz(ra, dec, lst);
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
if (!sc) continue;
const a = Math.max(0.25, Math.min(1, 1.15 - 0.15 * mag)) *
extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
if (a < 0.01) continue;
const r = Math.max(0.35, 2.7 - 0.42 * mag);
const c = STAR_PALETTE[ci];
if (mag <= TWINKLE_MAG) {
/* fase/freq determinísticas por índice — cintilação estável */
twinkList.push({
x: sc.x, y: sc.y, r, a,
cr: c[0], cg: c[1], cb: c[2],
phase: i * 2.3999,
freq: 0.35 + ((i * 0.61803) % 1) * 1.1,
amp: mag < 1.5 ? 0.18 : 0.45,
});
} else if (r > 0.8) {
bctx.beginPath();
bctx.arc(sc.x, sc.y, r, 0, Math.PI * 2);
bctx.fillStyle = `rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(3)})`;
bctx.fill();
} else {
bctx.fillStyle = `rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(3)})`;
bctx.fillRect(sc.x - r, sc.y - r, r * 2, r * 2);
}
}
}
function setObserverLocation(lat, lon) {
lat = parseFloat(lat); lon = parseFloat(lon);
if (!isFinite(lat) || !isFinite(lon)) return;
if (lat === obsLat && lon === obsLon) return;
obsLat = lat;
obsLon = lon;
projectSky();
} }
function resizeCanvas() { function resizeCanvas() {
canvas.width = window.innerWidth; canvas.width = window.innerWidth;
canvas.height = window.innerHeight; canvas.height = window.innerHeight;
initStars(); projectSky();
} }
function drawStars(ts) { function drawStars(ts) {
if (ts - lastProjMs > 30000) { lastProjMs = ts; projectSky(); }
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(baseCanvas, 0, 0);
const t = ts * 0.001; const t = ts * 0.001;
stars.forEach(s => { for (const s of twinkList) {
const alpha = (0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase))).toFixed(2); const tw = 1 - s.amp * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase));
ctx.beginPath(); ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${s.cr},${s.cg},${s.cb},${alpha})`; ctx.fillStyle = `rgba(${s.cr},${s.cg},${s.cb},${(s.a * tw).toFixed(3)})`;
ctx.fill(); ctx.fill();
}); }
requestAnimationFrame(drawStars); requestAnimationFrame(drawStars);
} }
@ -1984,6 +2153,7 @@ function setBaladaMode(on) {
if (on) { setChuvaMode(false); startGlobeLoop(); } if (on) { setChuvaMode(false); startGlobeLoop(); }
document.body.classList.toggle('balada', balada); document.body.classList.toggle('balada', balada);
/* opacidade do globo é controlada por drawGlobe via baladaAmt */ /* opacidade do globo é controlada por drawGlobe via baladaAmt */
projectSky(); // rebake do clarão solar no céu
} }
function toggleBalada() { setBaladaMode(!balada); } function toggleBalada() { setBaladaMode(!balada); }
@ -2137,6 +2307,7 @@ function setChuvaMode(on) {
} else if (lastThemeRgb) { } else if (lastThemeRgb) {
applyTheme(lastThemeRgb); // devolve a cor do álbum ao fog/horizonte applyTheme(lastThemeRgb); // devolve a cor do álbum ao fog/horizonte
} }
projectSky(); // rebake do clarão solar no céu
} }
function toggleChuva() { setChuvaMode(!chuva); } function toggleChuva() { setChuvaMode(!chuva); }