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