Smooth periodic sky reprojection

This commit is contained in:
vini 2026-07-19 14:17:54 -03:00
parent 3619d9ca0b
commit 8c1389557b

View file

@ -1576,22 +1576,31 @@ let constellationsFileVal = null;
/* ── 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');
é pré-renderizada fora da tela; só as brilhantes cintilam por frame.
Três buffers impedem que o compositor veja a textura sendo substituída. ── */
let baseCanvas = document.createElement('canvas');
let previousBaseCanvas = document.createElement('canvas');
let renderCanvas = document.createElement('canvas');
let hasSkyFrame = false;
let skyTransitionStart = -Infinity;
const SKY_TRANSITION_MS = 1400;
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');
if (renderCanvas.width !== W || renderCanvas.height !== H) {
renderCanvas.width = W;
renderCanvas.height = H;
}
const bctx = renderCanvas.getContext('2d');
bctx.clearRect(0, 0, W, H);
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 = [];
const nextTwinkList = [];
if (renderMilkyWay(lst, glareOn)) {
bctx.imageSmoothingEnabled = true;
@ -1621,7 +1630,7 @@ function projectSky() {
const c = STAR_PALETTE[ci];
if (mag <= TWINKLE_MAG) {
/* fase/freq determinísticas por índice — cintilação estável */
twinkList.push({
nextTwinkList.push({
x: sc.x, y: sc.y, r, a,
cr: c[0], cg: c[1], cb: c[2],
phase: i * 2.3999,
@ -1638,6 +1647,22 @@ function projectSky() {
bctx.fillRect(sc.x - r, sc.y - r, r * 2, r * 2);
}
}
/* O quadro só se torna visível depois de estar completo. O antigo fica
intacto para um crossfade que disfarça o pequeno avanço sideral. */
if (hasSkyFrame) {
const recycled = previousBaseCanvas;
previousBaseCanvas = baseCanvas;
baseCanvas = renderCanvas;
renderCanvas = recycled;
skyTransitionStart = performance.now();
} else {
const recycled = baseCanvas;
baseCanvas = renderCanvas;
renderCanvas = recycled;
hasSkyFrame = true;
}
twinkList = nextTwinkList;
}
function setObserverLocation(lat, lon) {
@ -1660,7 +1685,21 @@ function resizeCanvas() {
function drawStars(ts) {
if (ts - lastProjMs > 30000) { lastProjMs = ts; projectSky(); }
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(baseCanvas, 0, 0);
const transition = Math.max(0, Math.min(1,
(ts - skyTransitionStart) / SKY_TRANSITION_MS));
if (transition < 1 && previousBaseCanvas.width === canvas.width &&
previousBaseCanvas.height === canvas.height) {
const blend = transition * transition * (3 - 2 * transition);
ctx.globalAlpha = 1 - blend;
ctx.drawImage(previousBaseCanvas, 0, 0);
ctx.globalCompositeOperation = 'lighter';
ctx.globalAlpha = blend;
ctx.drawImage(baseCanvas, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
} else {
ctx.drawImage(baseCanvas, 0, 0);
}
const t = ts * 0.001;
for (const s of twinkList) {
const tw = 1 - s.amp * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase));