feat: estiliza Via Láctea como astrofoto de longa exposição

This commit is contained in:
vini 2026-07-19 13:21:52 -03:00
parent d8d99903d6
commit e59fe5a27a

View file

@ -1275,7 +1275,8 @@ const G2E = [
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 MW_RENDER_SCALE = 0.50;
const MW_NOISE_PERIODS = [5, 10, 20, 40, 80];
const mwLayer = document.createElement('canvas');
function mwHash(ix, iy) {
@ -1289,11 +1290,12 @@ function mwValueNoise(x, y, periodX) {
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 x0 = ((fx % periodX) + periodX) % periodX;
const x1 = (x0 + 1) % periodX;
const a = mwHash(x0, fy);
const b = mwHash(x1, fy);
const c = mwHash(x0, fy + 1);
const d = mwHash(x1, fy + 1);
const top = a + (b - a) * tx;
const bot = c + (d - c) * tx;
return top + (bot - top) * ty;
@ -1302,44 +1304,64 @@ function mwValueNoise(x, y, periodX) {
/* 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]) {
for (const period of MW_NOISE_PERIODS) {
value += amp * mwValueNoise(l / 360 * period, (b + 90) / 360 * period, period);
norm += amp;
amp *= 0.52;
amp *= 0.50;
}
return value / norm;
}
function mwFineNoise(l, b) {
const n1 = mwValueNoise(l / 360 * 160, (b + 90) / 360 * 160, 160);
const n2 = mwValueNoise(l / 360 * 320, (b + 90) / 360 * 320, 320);
return n1 * 0.68 + n2 * 0.32;
}
function mwAngularDistance(l, target) {
const d = Math.abs(l - target);
return Math.min(d, 360 - d);
}
function mwCloud(l, b, cl, cb, wl, wb, strength) {
const x = mwAngularDistance(l, cl) / wl;
const y = (b - cb) / wb;
return strength * Math.exp(-0.5 * (x * x + y * y));
}
/* 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);
const fine = mwFineNoise(l, b);
const warpedB = b - (cloud - 0.5) * (2.2 + 1.8 * core);
const width = 3.7 + 5.8 * core + 1.3 * cloud;
const plane = Math.exp(-0.5 * (warpedB / width) ** 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);
/* Luz integrada: halo largo, nuvens irregulares e grão fino de estrelas. */
let density = plane * (0.20 + 1.45 * cloud ** 2.1);
density += 0.12 * Math.exp(-0.5 * (warpedB / (width * 2.9)) ** 2) *
(0.35 + 0.65 * cloud);
density += plane * Math.max(0, fine - 0.46) ** 2 * 3.8;
/* Grandes nuvens estelares reais, exageradas como numa longa exposição. */
density += mwCloud(l, b, 0, 0.0, 20, 11, 1.55); // bojo de Sagitário
density += mwCloud(l, b, 25, 0.5, 10, 4, 0.62); // Scutum
density += mwCloud(l, b, 330, -0.7, 11, 5, 0.52);
density += mwCloud(l, b, 285, -0.4, 13, 5, 0.55); // Carina
density += mwCloud(l, b, 80, 0.8, 12, 4, 0.38); // Cygnus
/* 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);
const riftA = 0.6 + 1.55 * Math.sin(lr * 2.1 + 0.5) + 0.55 * Math.sin(lr * 7.0);
const riftB = -2.5 + 0.85 * Math.sin(lr * 1.4 + 2.2);
const darkA = Math.exp(-0.5 * ((b - riftA) / (0.72 + 0.72 * fine)) ** 2) *
(0.66 + 0.30 * fine);
const darkB = Math.exp(-0.5 * ((b - riftB) / 0.95) ** 2) * 0.58;
density *= 1 - Math.min(0.94, (darkA + darkB) * riftReach);
/* Saco de Carvão junto ao Cruzeiro do Sul. */
const coal = Math.exp(-0.5 * ((mwAngularDistance(l, 303) / 3.8) ** 2 +
@ -1349,15 +1371,18 @@ function mwDensity(l, b) {
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;
function mwWriteRgb(pixels, offset, dl) {
const t = Math.min(1, dl / 150);
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];
if (t < 0.5) {
pixels[offset] = 255 + (226 - 255) * u;
pixels[offset + 1] = 199 + (218 - 199) * u;
pixels[offset + 2] = 145 + (239 - 145) * u;
} else {
pixels[offset] = 226 + (148 - 226) * u;
pixels[offset + 1] = 218 + (201 - 218) * u;
pixels[offset + 2] = 239 + (255 - 239) * u;
}
}
function renderMilkyWay(W, H, horizonPx, lst, glareOn) {
@ -1405,12 +1430,11 @@ function renderMilkyWay(W, H, horizonPx, lst, glareOn) {
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;
/* Curva de exposição comprime o núcleo sem apagar estruturas tênues. */
const exposure = 1 - Math.exp(-density * 0.72);
const alpha = Math.min(0.46, exposure * 0.42) * 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];
mwWriteRgb(pixels, offset, Math.min(l, 360 - l));
pixels[offset + 3] = alpha * 255;
}
}
@ -1439,7 +1463,15 @@ function projectSky() {
renderMilkyWay(W, H, horizonPx, lst, glareOn);
bctx.imageSmoothingEnabled = true;
bctx.imageSmoothingQuality = 'high';
bctx.save();
bctx.globalCompositeOperation = 'lighter';
bctx.globalAlpha = 0.48;
bctx.filter = `blur(${Math.max(10, H * 0.014)}px) saturate(155%)`;
bctx.drawImage(mwLayer, 0, 0, W, horizonPx);
bctx.globalAlpha = 0.92;
bctx.filter = 'contrast(118%) saturate(128%)';
bctx.drawImage(mwLayer, 0, 0, W, horizonPx);
bctx.restore();
for (let i = 0; i < STAR_DATA.length; i++) {
const [ra, dec, mag, ci] = STAR_DATA[i];