diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html
index c4382ac..8fe137a 100644
--- a/plugin/contents/ui/dancer.html
+++ b/plugin/contents/ui/dancer.html
@@ -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));