From 02697dae1f42a10908350ea74111ce6891167c6c Mon Sep 17 00:00:00 2001 From: vini Date: Sun, 19 Jul 2026 14:54:42 -0300 Subject: [PATCH] Add five minute panoramic sky rotation --- plugin/contents/ui/dancer.html | 78 +++++++++++++++++++++++++++++----- toggle-camera-rotation.sh | 11 +++++ 2 files changed, 79 insertions(+), 10 deletions(-) create mode 100755 toggle-camera-rotation.sh diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html index daa52fd..83819e7 100644 --- a/plugin/contents/ui/dancer.html +++ b/plugin/contents/ui/dancer.html @@ -1254,9 +1254,11 @@ if (typeof CONSTELLATIONS !== 'undefined') { constellationArtImages.set(constellation.id, entry); } } -const VIEW_AZ = 90; // centro da tela olha para o LESTE — a tela física do - // usuário está voltada p/ leste, então o wallpaper age - // como janela: estrelas nascem subindo dentro da cena +const CAMERA_HOME_AZ = 90; // posição inicial: leste +const CAMERA_TURN_MS = 5 * 60 * 1000; +let viewAz = CAMERA_HOME_AZ; +let cameraRotating = false; +let cameraRotationStartMs = Date.now(); const FOV_AZ = 140; // largura da tela em graus de azimute const ALT_TOP = 75; // altitude no topo da tela const HORIZON_F = 0.585; // fração da altura da tela até o horizonte @@ -1267,6 +1269,23 @@ let skyTimeScale = 1; let skyClockRealMs = Date.now(); let skyClockSimMs = skyClockRealMs; +function currentCameraAz() { + if (!cameraRotating) return CAMERA_HOME_AZ; + const phase = ((Date.now() - cameraRotationStartMs) % CAMERA_TURN_MS) / + CAMERA_TURN_MS; + return (CAMERA_HOME_AZ + phase * 360) % 360; +} + +function setCameraRotating(on) { + on = !!on; + if (on === cameraRotating) return; + cameraRotating = on; + cameraRotationStartMs = Date.now(); + viewAz = CAMERA_HOME_AZ; + lastProjMs = -Infinity; + projectSky(); +} + function skyNowMs() { const realNow = Date.now(); return skyClockSimMs + (realNow - skyClockRealMs) * skyTimeScale + @@ -1310,7 +1329,7 @@ function altAz(raDeg, decDeg, lstDeg) { /* Projeção cilíndrica: azimute -> x, altitude -> y. null = fora da tela */ function skyToScreen(alt, az, W, horizonPx) { - const daz = ((az - VIEW_AZ) % 360 + 540) % 360 - 180; + const daz = ((az - viewAz) % 360 + 540) % 360 - 180; if (Math.abs(daz) > FOV_AZ / 2 || alt < -3 || alt > ALT_TOP) return null; return { x: (0.5 + daz / FOV_AZ) * W, y: horizonPx * (1 - alt / ALT_TOP) }; } @@ -1380,7 +1399,7 @@ function buildMwGrid() { const ca = Math.cos(alt * DEG), sa = Math.sin(alt * DEG); for (let gx = 0; gx < gw; gx++, i++) { const sx = (gx + 0.5) * MW_SAMPLE_SCALE; - const az = (VIEW_AZ + (sx / W - 0.5) * FOV_AZ) * DEG; + const az = (CAMERA_HOME_AZ + (sx / W - 0.5) * FOV_AZ) * DEG; const north = ca * Math.cos(az), east = ca * Math.sin(az); wx[i] = -sl * north + cl * sa; wy[i] = -east; @@ -1408,13 +1427,36 @@ function renderMilkyWay(lstDeg, glareOn) { const m20 = E2G[2][0] * cL + E2G[2][1] * sL; const m21 = E2G[2][0] * sL - E2G[2][1] * cL; const m22 = E2G[2][2]; + /* Gira o viewport em torno do zênite sem reconstruir a grade de milhões + de pixels. A matriz horizontal é incorporada à equatorial-galáctica. */ + const cameraDelta = (viewAz - CAMERA_HOME_AZ) * DEG; + const cr = Math.cos(cameraDelta), sr = Math.sin(cameraDelta); + const clat = Math.cos(obsLat * DEG), slat = Math.sin(obsLat * DEG); + const r00 = cr * slat * slat + clat * clat; + const r01 = -slat * sr; + const r02 = clat * slat * (1 - cr); + const r10 = slat * sr; + const r11 = cr; + const r12 = -clat * sr; + const r20 = r02; + const r21 = clat * sr; + const r22 = cr * clat * clat + slat * slat; + const n00 = m00 * r00 + m01 * r10 + m02 * r20; + const n01 = m00 * r01 + m01 * r11 + m02 * r21; + const n02 = m00 * r02 + m01 * r12 + m02 * r22; + const n10 = m10 * r00 + m11 * r10 + m12 * r20; + const n11 = m10 * r01 + m11 * r11 + m12 * r21; + const n12 = m10 * r02 + m11 * r12 + m12 * r22; + const n20 = m20 * r00 + m21 * r10 + m22 * r20; + const n21 = m20 * r01 + m21 * r11 + m22 * r21; + const n22 = m20 * r02 + m21 * r12 + m22 * r22; const tw = mwTex.w, th = mwTex.h, tex = mwTex.data, px = img.data; const inv2pi = 1 / (2 * Math.PI), invpi = 1 / Math.PI; for (let i = 0, j = 0; i < wx.length; i++, j += 4) { - const gx = m00 * wx[i] + m01 * wy[i] + m02 * wz[i]; - const gy = m10 * wx[i] + m11 * wy[i] + m12 * wz[i]; - const gz = m20 * wx[i] + m21 * wy[i] + m22 * wz[i]; + const gx = n00 * wx[i] + n01 * wy[i] + n02 * wz[i]; + const gy = n10 * wx[i] + n11 * wy[i] + n12 * wz[i]; + const gz = n20 * wx[i] + n21 * wy[i] + n22 * wz[i]; const l = Math.atan2(gy, gx); const b = Math.asin(Math.max(-1, Math.min(1, gz))); const absB = Math.abs(b / DEG); @@ -1512,7 +1554,7 @@ function drawConstellationArt(c, constellation, lst, W, horizonPx, previewProjec return { x: sc.x, y: sc.y, alt: ALT_TOP / 2 }; } const aa = altAz(anchor.sky[0], anchor.sky[1], lst); - const daz = ((aa.az - VIEW_AZ) % 360 + 540) % 360 - 180; + const daz = ((aa.az - viewAz) % 360 + 540) % 360 - 180; return { x: (0.5 + daz / FOV_AZ) * W, y: horizonPx * (1 - aa.alt / ALT_TOP), @@ -1738,6 +1780,7 @@ function projectSky() { } const bctx = renderCanvas.getContext('2d'); bctx.clearRect(0, 0, W, bufferH); + viewAz = currentCameraAz(); const lst = localSiderealDeg(skyNowMs()); const glareOn = !document.body.classList.contains('balada') && !document.body.classList.contains('chuva'); @@ -1824,7 +1867,7 @@ function resizeCanvas() { } function drawStars(ts) { - const projectionInterval = skyTimeScale > 1 ? 4000 : 30000; + const projectionInterval = (skyTimeScale > 1 || cameraRotating) ? 4000 : 30000; if (ts - lastProjMs > projectionInterval) { lastProjMs = ts; projectSky(); } ctx.clearRect(0, 0, canvas.width, canvas.height); const transition = Math.max(0, Math.min(1, @@ -1871,6 +1914,21 @@ let skyTimeScaleFileVal = null; .finally(() => setTimeout(pollSkyTimeScaleFile, 800)); })(); +/* Giro panorâmico controlado por toggle-camera-rotation.sh. */ +let cameraRotationFileVal = null; +(function pollCameraRotationFile() { + fetch('file:///tmp/skeledance-camera-rotation?t=' + Date.now()) + .then(r => r.text()) + .then(t => { + const v = t.trim(); + if (v === cameraRotationFileVal) return; + cameraRotationFileVal = v; + setCameraRotating(v === '1'); + }) + .catch(() => {}) + .finally(() => setTimeout(pollCameraRotationFile, 800)); +})(); + /* ============================================================ FLOOR CANVAS — perspective grid with true vanishing point ============================================================ */ diff --git a/toggle-camera-rotation.sh b/toggle-camera-rotation.sh new file mode 100755 index 0000000..59bfdec --- /dev/null +++ b/toggle-camera-rotation.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +STATE_FILE=/tmp/skeledance-camera-rotation + +if [ "$(cat "$STATE_FILE" 2>/dev/null)" = "1" ]; then + printf '0\n' > "$STATE_FILE" + printf 'Câmera fixa olhando para leste\n' +else + printf '1\n' > "$STATE_FILE" + printf 'Rotação panorâmica ativa: 360° em 5 minutos\n' +fi