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:
parent
998de4a945
commit
05e465e33f
1 changed files with 191 additions and 20 deletions
|
|
@ -1198,6 +1198,7 @@ body.chuva #sunChuvaFace { opacity: 1; transition: opacity 5s ease; }
|
|||
|
||||
</div><!-- .scene -->
|
||||
|
||||
<script src="starcatalog.js"></script>
|
||||
<script>
|
||||
/* ============================================================
|
||||
CONSTANTS
|
||||
|
|
@ -1207,47 +1208,215 @@ const BPM = 120;
|
|||
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 ctx = canvas.getContext('2d');
|
||||
|
||||
const STAR_COUNT = 220;
|
||||
const stars = [];
|
||||
let obsLat = -24.72, obsLon = -53.74; // Toledo-PR; sobrescrito pela config
|
||||
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() {
|
||||
stars.length = 0;
|
||||
for (let i = 0; i < STAR_COUNT; i++) {
|
||||
const c = STAR_COLORS[Math.random() < 0.15 ? 0 : Math.random() < 0.5 ? 1 : 2];
|
||||
stars.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height * 0.62,
|
||||
r: Math.random() * 1.6 + 0.2,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
freq: 0.4 + Math.random() * 1.2,
|
||||
cr: c[0], cg: c[1], cb: c[2],
|
||||
/* RA/Dec (graus) -> altitude/azimute (graus, az a partir do norte p/ leste) */
|
||||
function altAz(raDeg, decDeg, lstDeg) {
|
||||
const H = (lstDeg - raDeg) * DEG;
|
||||
const cd = Math.cos(decDeg * DEG), sd = Math.sin(decDeg * DEG);
|
||||
const cl = Math.cos(obsLat * DEG), sl = Math.sin(obsLat * DEG);
|
||||
const xh = -cd * Math.cos(H) * sl + sd * cl; // norte
|
||||
const yh = -cd * Math.sin(H); // leste
|
||||
const zh = cd * Math.cos(H) * cl + sd * sl; // zênite
|
||||
return { alt: Math.asin(zh) / DEG, az: Math.atan2(yh, xh) / DEG };
|
||||
}
|
||||
|
||||
/* 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() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
initStars();
|
||||
projectSky();
|
||||
}
|
||||
|
||||
function drawStars(ts) {
|
||||
if (ts - lastProjMs > 30000) { lastProjMs = ts; projectSky(); }
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(baseCanvas, 0, 0);
|
||||
const t = ts * 0.001;
|
||||
stars.forEach(s => {
|
||||
const alpha = (0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase))).toFixed(2);
|
||||
for (const s of twinkList) {
|
||||
const tw = 1 - s.amp * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase));
|
||||
ctx.beginPath();
|
||||
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();
|
||||
});
|
||||
}
|
||||
requestAnimationFrame(drawStars);
|
||||
}
|
||||
|
||||
|
|
@ -1984,6 +2153,7 @@ function setBaladaMode(on) {
|
|||
if (on) { setChuvaMode(false); startGlobeLoop(); }
|
||||
document.body.classList.toggle('balada', balada);
|
||||
/* opacidade do globo é controlada por drawGlobe via baladaAmt */
|
||||
projectSky(); // rebake do clarão solar no céu
|
||||
}
|
||||
function toggleBalada() { setBaladaMode(!balada); }
|
||||
|
||||
|
|
@ -2137,6 +2307,7 @@ function setChuvaMode(on) {
|
|||
} else if (lastThemeRgb) {
|
||||
applyTheme(lastThemeRgb); // devolve a cor do álbum ao fog/horizonte
|
||||
}
|
||||
projectSky(); // rebake do clarão solar no céu
|
||||
}
|
||||
function toggleChuva() { setChuvaMode(!chuva); }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue