feat: estiliza Via Láctea como astrofoto de longa exposição
This commit is contained in:
parent
d8d99903d6
commit
e59fe5a27a
1 changed files with 67 additions and 35 deletions
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue