diff --git a/plugin/contents/config/main.xml b/plugin/contents/config/main.xml
index 9c9c205..bc52416 100644
--- a/plugin/contents/config/main.xml
+++ b/plugin/contents/config/main.xml
@@ -8,5 +8,11 @@
true
+
+ false
+
+
+ false
+
diff --git a/plugin/contents/ui/config.qml b/plugin/contents/ui/config.qml
index 371848d..54e83e0 100644
--- a/plugin/contents/ui/config.qml
+++ b/plugin/contents/ui/config.qml
@@ -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
+ }
}
}
diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html
index 7c1918b..e040b2d 100644
--- a/plugin/contents/ui/dancer.html
+++ b/plugin/contents/ui/dancer.html
@@ -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; }
+
@@ -1021,6 +1129,7 @@ body.balada .spotlight-wrap { opacity: 1; }
@@ -1078,6 +1187,9 @@ body.balada .spotlight-wrap { opacity: 1; }
+
+
+
@@ -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.
diff --git a/plugin/contents/ui/main.qml b/plugin/contents/ui/main.qml
index aae1e42..4b3c525 100644
--- a/plugin/contents/ui/main.qml
+++ b/plugin/contents/ui/main.qml
@@ -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 {
diff --git a/toggle-balada.sh b/toggle-balada.sh
index ced31b0..ee0c63e 100755
--- a/toggle-balada.sh
+++ b/toggle-balada.sh
@@ -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
diff --git a/toggle-chuva.sh b/toggle-chuva.sh
new file mode 100755
index 0000000..84559b0
--- /dev/null
+++ b/toggle-chuva.sh
@@ -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