feat: Via Láctea surrealista — fita suave e poucas nuvens grandes
Troca o visual de simulação (milhares de partículas + grão) por pintura estilizada: fita contínua de sprites grandes sobrepostos ao longo do equador galáctico, 8 nuvens grandes em regiões reais (bojo, Scutum, Carina, Cygnus) e Grande Fenda + Saco de Carvão recortados com destination-out — bordas macias, zero ruído de pontos. A âncora astronômica continua: tudo gira com o céu real. Sprites escalam com a altura da tela (nítido em qualquer resolução). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
This commit is contained in:
parent
fd7f131b26
commit
96486b447c
1 changed files with 67 additions and 109 deletions
|
|
@ -1282,8 +1282,9 @@ const G2E = [
|
||||||
[-0.4838350155, 0.7469822445, 0.4559837762],
|
[-0.4838350155, 0.7469822445, 0.4559837762],
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Visual de astrofoto de longa exposição: grão estelar fino + brilho difuso
|
/* ── Via Láctea estilizada: fita suave + poucas nuvens grandes ──
|
||||||
com gradiente térmico (núcleo âmbar -> anticentro azul) + faixas de poeira */
|
Â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) {
|
function galToEq(l, b) {
|
||||||
const cb = Math.cos(b * DEG);
|
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 gx = cb * Math.cos(l * DEG), gy = cb * Math.sin(l * DEG), gz = Math.sin(b * DEG);
|
||||||
|
|
@ -1302,107 +1303,53 @@ function mwColor(dl) {
|
||||||
return [mix(255, 236, 186), mix(226, 224, 205), mix(188, 214, 242)];
|
return [mix(255, 236, 186), mix(226, 224, 205), mix(188, 214, 242)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nuvens escuras discretas em posições reais aproximadas
|
const mwBand = []; // fita + nuvens (aditivo) {ra, dec, size, alpha, ci}
|
||||||
(Saco de Carvão junto ao Cruzeiro, região do Cachimbo em Ofiúco...) */
|
const mwShadow = []; // Grande Fenda (recorte) {ra, dec, size, alpha}
|
||||||
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() {
|
(function buildMilkyWay() {
|
||||||
const rand = mulberry32(0x5EEDED);
|
/* fita contínua: sprites grandes bem sobrepostos ao longo de b~0 */
|
||||||
const gauss = () => {
|
for (let l = 0; l < 360; l += 2.5) {
|
||||||
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 dl = Math.min(l, 360 - l);
|
||||||
const central = Math.exp(-((dl / 80) ** 2));
|
const central = Math.exp(-((dl / 80) ** 2));
|
||||||
if (rand() > 0.22 + 0.78 * central) continue;
|
const wobble = 1.2 * Math.sin(l * DEG * 3.1) + 0.8 * Math.sin(l * DEG * 1.3 + 2);
|
||||||
const b = gauss() * (4.5 + 2.5 * central + 3.5 * Math.exp(-((dl / 22) ** 2)));
|
const eq = galToEq(l, wobble * 0.6);
|
||||||
const dust = mwDust(l, b, dl);
|
mwBand.push({
|
||||||
if (rand() > dust + 0.06) continue; // poeira esconde o grão
|
ra: eq.ra, dec: eq.dec,
|
||||||
const boost = cloudBoost(l, b);
|
size: 170 + 170 * central,
|
||||||
if (boost < 0.3 && rand() < 0.25 * central) continue;
|
alpha: 0.05 + 0.13 * central,
|
||||||
const eq = galToEq(l, b);
|
ci: mwColor(dl),
|
||||||
mwGrain.push({ ra: eq.ra, dec: eq.dec,
|
});
|
||||||
alpha: 0.10 + rand() * 0.30 + 0.2 * boost,
|
|
||||||
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 do brilho difuso: quente / neutro / frio (escolhido por cor) */
|
/* Sprites macios (quente / neutro / frio) + sprite de sombra p/ recorte */
|
||||||
function makeMwSprite(r, g, b) {
|
function makeMwSprite(r, g, b) {
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
c.width = c.height = 64;
|
c.width = c.height = 64;
|
||||||
|
|
@ -1418,6 +1365,10 @@ function makeMwSprite(r, g, b) {
|
||||||
const mwSprites = [makeMwSprite(255, 226, 188), // quente (núcleo)
|
const mwSprites = [makeMwSprite(255, 226, 188), // quente (núcleo)
|
||||||
makeMwSprite(236, 224, 214), // neutro
|
makeMwSprite(236, 224, 214), // neutro
|
||||||
makeMwSprite(188, 214, 242)]; // frio (anticentro)
|
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 */
|
||||||
|
const mwLayer = document.createElement('canvas');
|
||||||
function spriteFor(ci) { return mwSprites[ci[2] > ci[0] ? 2 : (ci[0] > 240 ? 0 : 1)]; }
|
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
|
/* ── Passo de projeção: recalcula posições de tela (a cada 30s o céu gira
|
||||||
|
|
@ -1439,27 +1390,34 @@ function projectSky() {
|
||||||
!document.body.classList.contains('chuva');
|
!document.body.classList.contains('chuva');
|
||||||
twinkList = [];
|
twinkList = [];
|
||||||
|
|
||||||
bctx.globalCompositeOperation = 'lighter';
|
/* fita da Via Láctea na camada própria, fenda recortada, depois composta */
|
||||||
for (const p of mwGlow) {
|
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 aa = altAz(p.ra, p.dec, lst);
|
||||||
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
|
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
|
||||||
if (!sc) continue;
|
if (!sc) continue;
|
||||||
const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
|
const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
|
||||||
if (a < 0.002) continue;
|
if (a < 0.004) continue;
|
||||||
bctx.globalAlpha = a;
|
const s = p.size * sScale;
|
||||||
bctx.drawImage(spriteFor(p.ci), sc.x - p.size / 2, sc.y - p.size / 2, p.size, p.size);
|
mctx.globalAlpha = a;
|
||||||
|
mctx.drawImage(spriteFor(p.ci), sc.x - s / 2, sc.y - s / 2, s, s);
|
||||||
}
|
}
|
||||||
bctx.globalAlpha = 1;
|
mctx.globalCompositeOperation = 'destination-out';
|
||||||
for (const p of mwGrain) {
|
for (const p of mwShadow) {
|
||||||
const aa = altAz(p.ra, p.dec, lst);
|
const aa = altAz(p.ra, p.dec, lst);
|
||||||
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
|
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
|
||||||
if (!sc) continue;
|
if (!sc) continue;
|
||||||
const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
|
const s = p.size * sScale;
|
||||||
if (a < 0.01) continue;
|
mctx.globalAlpha = p.alpha;
|
||||||
bctx.fillStyle = `rgba(${p.ci[0]},${p.ci[1]},${p.ci[2]},${a.toFixed(3)})`;
|
mctx.drawImage(mwShadowSprite, sc.x - s / 2, sc.y - s / 2, s, s);
|
||||||
bctx.fillRect(sc.x - 0.5, sc.y - 0.5, 1, 1);
|
|
||||||
}
|
}
|
||||||
bctx.globalCompositeOperation = 'source-over';
|
mctx.globalAlpha = 1;
|
||||||
|
mctx.globalCompositeOperation = 'source-over';
|
||||||
|
bctx.drawImage(mwLayer, 0, 0);
|
||||||
|
|
||||||
for (let i = 0; i < STAR_DATA.length; i++) {
|
for (let i = 0; i < STAR_DATA.length; i++) {
|
||||||
const [ra, dec, mag, ci] = STAR_DATA[i];
|
const [ra, dec, mag, ci] = STAR_DATA[i];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue