|
|
|
|
@ -789,7 +789,9 @@ body.balada .music-ticker {
|
|
|
|
|
bottom 2.8s cubic-bezier(0.16, 1, 0.3, 1),
|
|
|
|
|
transform 2.8s cubic-bezier(0.16, 1, 0.3, 1),
|
|
|
|
|
background 7s ease,
|
|
|
|
|
box-shadow 7s ease;
|
|
|
|
|
box-shadow 7s ease,
|
|
|
|
|
width 3s ease,
|
|
|
|
|
height 3s ease;
|
|
|
|
|
/* listras geradas por JS — ver buildSunStripes() */
|
|
|
|
|
background:
|
|
|
|
|
linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.28)),
|
|
|
|
|
@ -1003,6 +1005,112 @@ body.balada .spotlight-wrap { opacity: 1; }
|
|
|
|
|
.nebula-b::after { background: radial-gradient(ellipse, rgba(180,0,255,0.18), transparent 70%); }
|
|
|
|
|
.nebula-c::after { background: radial-gradient(ellipse, rgba(255,140,0,0.14), transparent 70%); }
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
CHUVA — CANVAS DE CHUVA
|
|
|
|
|
============================================================ */
|
|
|
|
|
#rainCanvas {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
z-index: 45;
|
|
|
|
|
mix-blend-mode: screen;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
CHUVA — MODO CHUVA
|
|
|
|
|
============================================================ */
|
|
|
|
|
|
|
|
|
|
/* Fundo: céu tempestuoso escuro */
|
|
|
|
|
body.chuva { background: #04080f; }
|
|
|
|
|
|
|
|
|
|
/* Estrelas somem atrás das nuvens */
|
|
|
|
|
#starfield { transition: opacity 3s ease; }
|
|
|
|
|
body.chuva #starfield { opacity: 0.1; }
|
|
|
|
|
|
|
|
|
|
/* Meteoros somem — a chuva substitui */
|
|
|
|
|
#meteorLayer { transition: opacity 2s ease; }
|
|
|
|
|
body.chuva #meteorLayer { opacity: 0; }
|
|
|
|
|
|
|
|
|
|
/* Sol vira lua: sobe, encolhe, fica prateado */
|
|
|
|
|
body.chuva .synthwave-sun {
|
|
|
|
|
bottom: 70%;
|
|
|
|
|
transform: translateX(-50%) translateY(0);
|
|
|
|
|
width: 160px;
|
|
|
|
|
height: 160px;
|
|
|
|
|
box-shadow:
|
|
|
|
|
0 0 22px rgba(180,205,255,0.35),
|
|
|
|
|
0 0 70px rgba(150,180,255,0.12),
|
|
|
|
|
0 0 160px rgba(120,155,255,0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Halo lunar — suave e frio */
|
|
|
|
|
body.chuva .synthwave-sun::after {
|
|
|
|
|
inset: -45%;
|
|
|
|
|
background: radial-gradient(ellipse at 50% 50%,
|
|
|
|
|
rgba(190,210,255,0.12) 0%,
|
|
|
|
|
rgba(160,185,255,0.07) 40%,
|
|
|
|
|
transparent 65%
|
|
|
|
|
);
|
|
|
|
|
animation: none;
|
|
|
|
|
filter: blur(18px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Listras do sol somem, elementos de balada somem */
|
|
|
|
|
body.chuva #sunStripes { opacity: 0; }
|
|
|
|
|
body.chuva #sunBaladaBg { opacity: 0; transition: opacity 1.5s ease; }
|
|
|
|
|
body.chuva .sun-rays-wrap { opacity: 0; }
|
|
|
|
|
body.chuva .spotlight-wrap { opacity: 0; }
|
|
|
|
|
body.chuva #discoDots { opacity: 0; }
|
|
|
|
|
|
|
|
|
|
/* Nuvens de tempestade via nebulae */
|
|
|
|
|
body.chuva .nebula::after { opacity: 1; }
|
|
|
|
|
body.chuva .nebula-a::after { background: radial-gradient(ellipse, rgba(35,45,75,0.55), transparent 70%); }
|
|
|
|
|
body.chuva .nebula-b::after { background: radial-gradient(ellipse, rgba(25,35,65,0.48), transparent 70%); }
|
|
|
|
|
body.chuva .nebula-c::after { background: radial-gradient(ellipse, rgba(45,55,85,0.42), transparent 70%); }
|
|
|
|
|
|
|
|
|
|
/* Horizonte frio e tempestuoso */
|
|
|
|
|
body.chuva .horizon {
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
to right,
|
|
|
|
|
transparent 0%,
|
|
|
|
|
rgba(70,120,200,0.5) 20%,
|
|
|
|
|
rgba(120,165,245,0.75) 50%,
|
|
|
|
|
rgba(70,120,200,0.5) 80%,
|
|
|
|
|
transparent 100%
|
|
|
|
|
) !important;
|
|
|
|
|
box-shadow:
|
|
|
|
|
0 0 18px rgba(100,150,235,0.55),
|
|
|
|
|
0 0 65px rgba(80,125,210,0.18) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Face lunar */
|
|
|
|
|
#sunChuvaFace {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 1.2s ease; /* saída rápida — entrada lenta definida em body.chuva */
|
|
|
|
|
background: radial-gradient(circle at 38% 32%,
|
|
|
|
|
#eef2fa 0%,
|
|
|
|
|
#c0cad8 42%,
|
|
|
|
|
#7888a8 78%,
|
|
|
|
|
#506080 100%
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
#sunChuvaFace::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: radial-gradient(circle at 66% 27%,
|
|
|
|
|
rgba(18,28,58,0.72) 22%,
|
|
|
|
|
rgba(22,38,72,0.38) 52%,
|
|
|
|
|
transparent 70%
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
body.chuva #sunChuvaFace { opacity: 1; transition: opacity 5s ease; }
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
@ -1021,6 +1129,7 @@ body.balada .spotlight-wrap { opacity: 1; }
|
|
|
|
|
<div class="synthwave-sun">
|
|
|
|
|
<div id="sunBaladaBg"></div>
|
|
|
|
|
<div id="sunStripes"></div>
|
|
|
|
|
<div id="sunChuvaFace"></div>
|
|
|
|
|
<div class="sun-rays-wrap" id="sunRaysWrap"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@ -1078,6 +1187,9 @@ body.balada .spotlight-wrap { opacity: 1; }
|
|
|
|
|
<!-- BALADA: partículas de luz -->
|
|
|
|
|
<div id="discoDots" style="position:absolute;inset:0;pointer-events:none;z-index:48;"></div>
|
|
|
|
|
|
|
|
|
|
<!-- CHUVA: canvas de chuva -->
|
|
|
|
|
<canvas id="rainCanvas"></canvas>
|
|
|
|
|
|
|
|
|
|
<!-- Scanlines overlay -->
|
|
|
|
|
<div class="scanlines"></div>
|
|
|
|
|
|
|
|
|
|
@ -1398,9 +1510,12 @@ requestAnimationFrame(flushBars);
|
|
|
|
|
/* ============================================================
|
|
|
|
|
MUSIC-LIGHT THEME (album color)
|
|
|
|
|
============================================================ */
|
|
|
|
|
let lastThemeRgb = null;
|
|
|
|
|
function applyTheme(rgb) {
|
|
|
|
|
lastThemeRgb = rgb;
|
|
|
|
|
const [r, g, b] = rgb;
|
|
|
|
|
document.documentElement.style.setProperty('--theme-color', `rgba(${r},${g},${b},1)`);
|
|
|
|
|
if (!chuva) {
|
|
|
|
|
horizonEl.style.background = `linear-gradient(to right,
|
|
|
|
|
transparent 0%,
|
|
|
|
|
rgba(${r},${g},${b},0.7) 20%,
|
|
|
|
|
@ -1409,13 +1524,14 @@ function applyTheme(rgb) {
|
|
|
|
|
transparent 100%)`;
|
|
|
|
|
horizonEl.style.boxShadow =
|
|
|
|
|
`0 0 30px rgba(${r},${g},${b},0.9), 0 0 90px rgba(${r},${g},${b},0.3)`;
|
|
|
|
|
fogWisps.forEach(w => {
|
|
|
|
|
w.style.background = `radial-gradient(ellipse, rgba(${r},${g},${b},0.75) 0%, transparent 70%)`;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
vuBars.forEach(bar => {
|
|
|
|
|
bar.style.background = `rgb(${r},${g},${b})`;
|
|
|
|
|
bar.style.boxShadow = `0 0 4px rgba(${r},${g},${b},0.8)`;
|
|
|
|
|
});
|
|
|
|
|
fogWisps.forEach(w => {
|
|
|
|
|
w.style.background = `radial-gradient(ellipse, rgba(${r},${g},${b},0.75) 0%, transparent 70%)`;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const albumArtEl = document.getElementById('albumArt');
|
|
|
|
|
@ -1686,7 +1802,15 @@ globeCanvas.style.cssText =
|
|
|
|
|
'opacity:0;transition:opacity 1.5s ease;';
|
|
|
|
|
sunEl.appendChild(globeCanvas);
|
|
|
|
|
const gctx = globeCanvas.getContext('2d');
|
|
|
|
|
let globeRot = 0, lastGlobeTs = 0;
|
|
|
|
|
let globeRot = 0, lastGlobeTs = 0, globeLoopOn = false;
|
|
|
|
|
|
|
|
|
|
/* Loop roda só enquanto o globo é visível — dorme quando baladaAmt zera */
|
|
|
|
|
function startGlobeLoop() {
|
|
|
|
|
if (globeLoopOn) return;
|
|
|
|
|
globeLoopOn = true;
|
|
|
|
|
lastGlobeTs = 0;
|
|
|
|
|
requestAnimationFrame(drawGlobe);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 8 luzes nos eixos ortogonais — cobertura 360° garantida.
|
|
|
|
|
±Y não são afetados pelo lrot, ±X e ±Z orbitam entre si. */
|
|
|
|
|
@ -1702,9 +1826,10 @@ const MIRROR_LIGHTS = [
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function drawGlobe(ts) {
|
|
|
|
|
if (!balada && baladaAmt < 0.01) { globeLoopOn = false; lastGlobeTs = 0; return; }
|
|
|
|
|
requestAnimationFrame(drawGlobe);
|
|
|
|
|
if (baladaAmt < 0.01) { lastGlobeTs = 0; return; }
|
|
|
|
|
globeCanvas.style.opacity = baladaAmt;
|
|
|
|
|
globeCanvas.style.opacity = chuva ? 0 : baladaAmt;
|
|
|
|
|
const dt = lastGlobeTs ? Math.min((ts - lastGlobeTs) / 1000, 0.05) : 0;
|
|
|
|
|
lastGlobeTs = ts;
|
|
|
|
|
globeRot = (globeRot + dt * 0.5) % (Math.PI * 2);
|
|
|
|
|
@ -1784,7 +1909,6 @@ function drawGlobe(ts) {
|
|
|
|
|
|
|
|
|
|
gctx.restore();
|
|
|
|
|
}
|
|
|
|
|
requestAnimationFrame(drawGlobe);
|
|
|
|
|
const discoDots = document.getElementById('discoDots');
|
|
|
|
|
const strobeEl = document.getElementById('strobe');
|
|
|
|
|
|
|
|
|
|
@ -1843,22 +1967,6 @@ setInterval(() => {
|
|
|
|
|
if (baladaAmt > 0.3 && discoDots.childElementCount < 28) spawnDiscoDot();
|
|
|
|
|
}, 280);
|
|
|
|
|
|
|
|
|
|
/* ── Strobe no beat ── */
|
|
|
|
|
let strobeCooldown = false;
|
|
|
|
|
function triggerStrobe() {
|
|
|
|
|
if (!balada || strobeCooldown) return;
|
|
|
|
|
strobeCooldown = true;
|
|
|
|
|
strobeEl.style.transition = 'none';
|
|
|
|
|
strobeEl.style.opacity = '0.2';
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
strobeEl.style.transition = 'opacity 0.3s ease';
|
|
|
|
|
strobeEl.style.opacity = '0';
|
|
|
|
|
setTimeout(() => { strobeCooldown = false; }, 600);
|
|
|
|
|
}, 50);
|
|
|
|
|
}
|
|
|
|
|
/* strobe desativado — muito agressivo visualmente */
|
|
|
|
|
/* setInterval(() => { if (balada && Math.random() < 0.35) triggerStrobe(); }, BEAT_MS * 2); */
|
|
|
|
|
|
|
|
|
|
/* ── Pulso do sol/disco no beat ── */
|
|
|
|
|
function globeBeat() {
|
|
|
|
|
if (!balada || !sunEl) return;
|
|
|
|
|
@ -1873,21 +1981,26 @@ setInterval(() => { if (balada) globeBeat(); }, BEAT_MS);
|
|
|
|
|
function setBaladaMode(on) {
|
|
|
|
|
if (on === balada) return;
|
|
|
|
|
balada = on;
|
|
|
|
|
if (on) { setChuvaMode(false); startGlobeLoop(); }
|
|
|
|
|
document.body.classList.toggle('balada', balada);
|
|
|
|
|
globeCanvas.style.opacity = on ? '1' : '0';
|
|
|
|
|
/* opacidade do globo é controlada por drawGlobe via baladaAmt */
|
|
|
|
|
}
|
|
|
|
|
function toggleBalada() { setBaladaMode(!balada); }
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', e => {
|
|
|
|
|
if ((e.key === 'b' || e.key === 'B') && !e.target.matches('input,textarea'))
|
|
|
|
|
toggleBalada();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/* Polling de arquivos — toggle externo */
|
|
|
|
|
/* Polling de arquivos — só reage a MUDANÇAS no arquivo; um arquivo parado
|
|
|
|
|
em "1" não fica religando o modo e brigando com o outro toggle.
|
|
|
|
|
A primeira leitura é só baseline: arquivo velho não sobrescreve a config. */
|
|
|
|
|
let baladaFileVal = null;
|
|
|
|
|
(function pollBaladaFile() {
|
|
|
|
|
fetch('file:///tmp/skeledance-balada?t=' + Date.now())
|
|
|
|
|
.then(r => r.text())
|
|
|
|
|
.then(t => setBaladaMode(t.trim() === '1'))
|
|
|
|
|
.then(t => {
|
|
|
|
|
const v = t.trim();
|
|
|
|
|
if (v === baladaFileVal) return;
|
|
|
|
|
const first = baladaFileVal === null;
|
|
|
|
|
baladaFileVal = v;
|
|
|
|
|
if (!first) setBaladaMode(v === '1');
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
.finally(() => setTimeout(pollBaladaFile, 800));
|
|
|
|
|
})();
|
|
|
|
|
@ -1896,10 +2009,163 @@ document.addEventListener('keydown', e => {
|
|
|
|
|
fetch('file:///tmp/skeledance-skeleton?t=' + Date.now())
|
|
|
|
|
.then(r => r.text())
|
|
|
|
|
.then(t => setShowSkeleton(t.trim() !== '0'))
|
|
|
|
|
.catch(() => {}) /* arquivo ausente = skeleton visível */
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
.finally(() => setTimeout(pollSkeletonFile, 800));
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', e => {
|
|
|
|
|
if (!e.target.matches('input,textarea')) {
|
|
|
|
|
if (e.key === 'b' || e.key === 'B') toggleBalada();
|
|
|
|
|
if (e.key === 'r' || e.key === 'R') toggleChuva();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
CHUVA MODE
|
|
|
|
|
============================================================ */
|
|
|
|
|
let chuva = false;
|
|
|
|
|
let chuvaAmt = 0;
|
|
|
|
|
|
|
|
|
|
const rainCanvas = document.getElementById('rainCanvas');
|
|
|
|
|
const rctx = rainCanvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
const RAIN_COUNT = 280;
|
|
|
|
|
const rainDrops = [];
|
|
|
|
|
const RAIN_ANGLE = Math.PI / 13; // ~14° da vertical
|
|
|
|
|
const RAIN_DX = Math.sin(RAIN_ANGLE);
|
|
|
|
|
const RAIN_DY = Math.cos(RAIN_ANGLE);
|
|
|
|
|
|
|
|
|
|
function resizeRain() {
|
|
|
|
|
rainCanvas.width = window.innerWidth;
|
|
|
|
|
rainCanvas.height = window.innerHeight;
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener('resize', resizeRain);
|
|
|
|
|
|
|
|
|
|
function initRain() {
|
|
|
|
|
const W = rainCanvas.width || window.innerWidth;
|
|
|
|
|
const H = rainCanvas.height || window.innerHeight;
|
|
|
|
|
rainDrops.length = 0;
|
|
|
|
|
for (let i = 0; i < RAIN_COUNT; i++) {
|
|
|
|
|
rainDrops.push({
|
|
|
|
|
x: -60 + Math.random() * (W + 120),
|
|
|
|
|
y: Math.random() * H,
|
|
|
|
|
len: 11 + Math.random() * 18,
|
|
|
|
|
speed: 440 + Math.random() * 360,
|
|
|
|
|
alpha: 0.3 + Math.random() * 0.45,
|
|
|
|
|
w: 0.8 + Math.random() * 0.8,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let lastRainTs = 0;
|
|
|
|
|
let rainLoopOn = false;
|
|
|
|
|
|
|
|
|
|
/* Loop roda só enquanto há chuva visível — dorme quando chuvaAmt zera */
|
|
|
|
|
function startRainLoop() {
|
|
|
|
|
if (rainLoopOn) return;
|
|
|
|
|
rainLoopOn = true;
|
|
|
|
|
lastRainTs = 0;
|
|
|
|
|
requestAnimationFrame(drawRain);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawRain(ts) {
|
|
|
|
|
const dt = lastRainTs ? Math.min((ts - lastRainTs) / 1000, 0.05) : 0;
|
|
|
|
|
lastRainTs = ts;
|
|
|
|
|
chuvaAmt += ((chuva ? 1 : 0) - chuvaAmt) * Math.min(1, dt * 0.5);
|
|
|
|
|
|
|
|
|
|
const W = rainCanvas.width, H = rainCanvas.height;
|
|
|
|
|
rctx.clearRect(0, 0, W, H);
|
|
|
|
|
if (!chuva && chuvaAmt < 0.005) { rainLoopOn = false; return; }
|
|
|
|
|
requestAnimationFrame(drawRain);
|
|
|
|
|
if (chuvaAmt < 0.005) return;
|
|
|
|
|
|
|
|
|
|
rainDrops.forEach(d => {
|
|
|
|
|
d.x += d.speed * dt * RAIN_DX;
|
|
|
|
|
d.y += d.speed * dt * RAIN_DY;
|
|
|
|
|
if (d.y > H + d.len || d.x > W + d.len) {
|
|
|
|
|
d.y = -d.len - Math.random() * 80;
|
|
|
|
|
d.x = -60 + Math.random() * (W + 120);
|
|
|
|
|
}
|
|
|
|
|
rctx.beginPath();
|
|
|
|
|
rctx.moveTo(d.x, d.y);
|
|
|
|
|
rctx.lineTo(d.x + d.len * RAIN_DX, d.y + d.len * RAIN_DY);
|
|
|
|
|
rctx.strokeStyle = `rgba(160,195,235,${(d.alpha * chuvaAmt).toFixed(3)})`;
|
|
|
|
|
rctx.lineWidth = d.w;
|
|
|
|
|
rctx.stroke();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Relâmpago ── */
|
|
|
|
|
function triggerLightning() {
|
|
|
|
|
if (!chuva) return;
|
|
|
|
|
const flashes = 1 + Math.floor(Math.random() * 3);
|
|
|
|
|
let delay = 0;
|
|
|
|
|
for (let i = 0; i < flashes; i++) {
|
|
|
|
|
const intensity = (0.5 + Math.random() * 0.32).toFixed(2);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
strobeEl.style.transition = 'none';
|
|
|
|
|
strobeEl.style.background = 'rgba(210,228,255,1)';
|
|
|
|
|
strobeEl.style.opacity = intensity;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
strobeEl.style.transition = 'opacity 0.2s ease';
|
|
|
|
|
strobeEl.style.opacity = '0';
|
|
|
|
|
}, 50 + Math.random() * 30 | 0);
|
|
|
|
|
}, delay);
|
|
|
|
|
delay += 100 + (Math.random() * 110 | 0);
|
|
|
|
|
}
|
|
|
|
|
/* limpa o tom azulado do strobe depois da rajada */
|
|
|
|
|
setTimeout(() => { strobeEl.style.background = ''; }, delay + 300);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(function scheduleLightning() {
|
|
|
|
|
if (chuva) triggerLightning();
|
|
|
|
|
setTimeout(scheduleLightning, 10000 + Math.random() * 22000 | 0);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/* ── Toggle ── */
|
|
|
|
|
function setChuvaMode(on) {
|
|
|
|
|
if (on === chuva) return;
|
|
|
|
|
chuva = on;
|
|
|
|
|
if (on) { setBaladaMode(false); startRainLoop(); }
|
|
|
|
|
document.body.classList.toggle('chuva', chuva);
|
|
|
|
|
if (on) {
|
|
|
|
|
fogWisps.forEach(w => {
|
|
|
|
|
w.style.background =
|
|
|
|
|
'radial-gradient(ellipse, rgba(70,100,160,0.5) 0%, transparent 70%)';
|
|
|
|
|
});
|
|
|
|
|
} else if (lastThemeRgb) {
|
|
|
|
|
applyTheme(lastThemeRgb); // devolve a cor do álbum ao fog/horizonte
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function toggleChuva() { setChuvaMode(!chuva); }
|
|
|
|
|
|
|
|
|
|
let chuvaFileVal = null;
|
|
|
|
|
(function pollChuvaFile() {
|
|
|
|
|
fetch('file:///tmp/skeledance-chuva?t=' + Date.now())
|
|
|
|
|
.then(r => r.text())
|
|
|
|
|
.then(t => {
|
|
|
|
|
const v = t.trim();
|
|
|
|
|
if (v === chuvaFileVal) return;
|
|
|
|
|
const first = chuvaFileVal === null;
|
|
|
|
|
chuvaFileVal = v;
|
|
|
|
|
if (!first) setChuvaMode(v === '1');
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
.finally(() => setTimeout(pollChuvaFile, 800));
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/* ── Inicializa rain canvas (separado de initCanvases pois roda após as declarações) ── */
|
|
|
|
|
(function initRainOnLoad() {
|
|
|
|
|
if (window.innerWidth > 0 && window.innerHeight > 0) {
|
|
|
|
|
resizeRain();
|
|
|
|
|
initRain();
|
|
|
|
|
if (chuva) startRainLoop();
|
|
|
|
|
} else {
|
|
|
|
|
setTimeout(initRainOnLoad, 80);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
DEV SHIM — only active when served via HTTP (not from QML)
|
|
|
|
|
Simulates playerctl updates so the music ticker works in-browser.
|
|
|
|
|
|