feat: chuva mode — moon, rain, lightning; clean transitions with balada
Rain mode with canvas rain, stormy sky, sun-to-moon transition and lightning strobe. Mode switching fixed so only one of moon/globe shows: file pollers are edge-triggered (stale /tmp files no longer fight), toggle scripts keep the files mutually exclusive, exits fade fast while entrances stay slow, drawGlobe solely owns globe opacity, and leaving chuva restores the album-color theme on fog/horizon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
This commit is contained in:
parent
36220dc34f
commit
39bc5d0794
6 changed files with 320 additions and 23 deletions
|
|
@ -8,5 +8,11 @@
|
|||
<entry name="ShowSkeleton" type="Bool">
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="BaladaMode" type="Bool">
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ChuvaMode" type="Bool">
|
||||
<default>false</default>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ ColumnLayout {
|
|||
id: root
|
||||
|
||||
property alias cfg_ShowSkeleton: skeletonCheck.checked
|
||||
property alias cfg_BaladaMode: baladaCheck.checked
|
||||
property alias cfg_ChuvaMode: chuvaCheck.checked
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
|
|
@ -18,5 +20,19 @@ ColumnLayout {
|
|||
Kirigami.FormData.label: i18n("Esqueleto:")
|
||||
text: i18n("Mostrar o robô dançando")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: baladaCheck
|
||||
Kirigami.FormData.label: i18n("Modo balada:")
|
||||
text: i18n("Mirror ball + disco floor + refletores")
|
||||
onCheckedChanged: if (checked) chuvaCheck.checked = false
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: chuvaCheck
|
||||
Kirigami.FormData.label: i18n("Modo chuva:")
|
||||
text: i18n("Lua + chuva + relâmpagos")
|
||||
onCheckedChanged: if (checked) baladaCheck.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,24 +1510,28 @@ 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)`);
|
||||
horizonEl.style.background = `linear-gradient(to right,
|
||||
transparent 0%,
|
||||
rgba(${r},${g},${b},0.7) 20%,
|
||||
rgba(${r},${g},${b},1) 50%,
|
||||
rgba(${r},${g},${b},0.7) 80%,
|
||||
transparent 100%)`;
|
||||
horizonEl.style.boxShadow =
|
||||
`0 0 30px rgba(${r},${g},${b},0.9), 0 0 90px rgba(${r},${g},${b},0.3)`;
|
||||
if (!chuva) {
|
||||
horizonEl.style.background = `linear-gradient(to right,
|
||||
transparent 0%,
|
||||
rgba(${r},${g},${b},0.7) 20%,
|
||||
rgba(${r},${g},${b},1) 50%,
|
||||
rgba(${r},${g},${b},0.7) 80%,
|
||||
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');
|
||||
|
|
@ -1704,7 +1820,7 @@ const MIRROR_LIGHTS = [
|
|||
function drawGlobe(ts) {
|
||||
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);
|
||||
|
|
@ -1873,21 +1989,24 @@ setInterval(() => { if (balada) globeBeat(); }, BEAT_MS);
|
|||
function setBaladaMode(on) {
|
||||
if (on === balada) return;
|
||||
balada = on;
|
||||
if (on) setChuvaMode(false);
|
||||
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 */
|
||||
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;
|
||||
baladaFileVal = v;
|
||||
setBaladaMode(v === '1');
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setTimeout(pollBaladaFile, 800));
|
||||
})();
|
||||
|
|
@ -1896,10 +2015,150 @@ 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;
|
||||
|
||||
function drawRain(ts) {
|
||||
requestAnimationFrame(drawRain);
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
(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);
|
||||
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;
|
||||
chuvaFileVal = v;
|
||||
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();
|
||||
requestAnimationFrame(drawRain);
|
||||
} 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.
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@ WallpaperItem {
|
|||
|
||||
function applyConfig() {
|
||||
webView.runJavaScript("setShowSkeleton(" + Plasmoid.configuration.ShowSkeleton + ")")
|
||||
webView.runJavaScript("setBaladaMode(" + Plasmoid.configuration.BaladaMode + ")")
|
||||
webView.runJavaScript("setChuvaMode(" + Plasmoid.configuration.ChuvaMode + ")")
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Plasmoid.configuration
|
||||
function onShowSkeletonChanged() { applyConfig() }
|
||||
function onBaladaModeChanged() { applyConfig() }
|
||||
function onChuvaModeChanged() { applyConfig() }
|
||||
}
|
||||
|
||||
WebEngineView {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
[ "$(cat /tmp/skeledance-balada 2>/dev/null)" = "1" ] && echo 0 > /tmp/skeledance-balada || echo 1 > /tmp/skeledance-balada
|
||||
if [ "$(cat /tmp/skeledance-balada 2>/dev/null)" = "1" ]; then
|
||||
echo 0 > /tmp/skeledance-balada
|
||||
else
|
||||
echo 1 > /tmp/skeledance-balada
|
||||
echo 0 > /tmp/skeledance-chuva # modos são mutuamente exclusivos
|
||||
fi
|
||||
|
|
|
|||
7
toggle-chuva.sh
Executable file
7
toggle-chuva.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
if [ "$(cat /tmp/skeledance-chuva 2>/dev/null)" = "1" ]; then
|
||||
echo 0 > /tmp/skeledance-chuva
|
||||
else
|
||||
echo 1 > /tmp/skeledance-chuva
|
||||
echo 0 > /tmp/skeledance-balada # modos são mutuamente exclusivos
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue