Compare commits

...

4 commits

Author SHA1 Message Date
vini
0caf1fbbf8 test: script para ciclo manual de modos com reload do wallpaper
Recarrega o plugin via D-Bus (troca para org.kde.image e volta) e
alterna chuva -> balada -> chuva -> retrowave com pausas configuráveis
para inspecionar as transições.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:27:04 -03:00
vini
de994bced9 refactor: idle-friendly render loops, drop dead strobe code
Rain and globe rAF loops now start on demand and stop when their
effect fully fades, instead of ticking every frame forever. Lightning
clears the bluish strobe tint after each burst. Removed triggerStrobe,
which was disabled and unreferenced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:23:16 -03:00
vini
f20d8adf4f fix: first /tmp file poll is baseline only, no longer overrides config
On page load the first poll counted as a change and applied the file
value, so a stale /tmp file could flip the mode set by the Plasma
config ~800ms after startup. Record the baseline silently; only real
changes toggle modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:18:53 -03:00
vini
39bc5d0794 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
2026-07-19 10:18:34 -03:00
7 changed files with 383 additions and 41 deletions

View file

@ -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>

View file

@ -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
}
}
}

View file

@ -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');
@ -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.

View file

@ -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 {

38
test-modes.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Testa o ciclo de modos do wallpaper: recarrega o plugin e alterna
# retrowave -> chuva -> balada -> chuva -> retrowave com pausas para observar.
set -e
PLUGIN_ID="com.vini.dancingcharacter"
PAUSE="${1:-5}" # segundos entre cada transição (padrão 5)
plasma_script() {
local js="for (let i of desktops()) { i.wallpaperPlugin = \"$1\"; }"
qdbus6 org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "$js" 2>/dev/null \
|| qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "$js"
}
set_modes() { # set_modes <chuva> <balada>
echo "$1" > /tmp/skeledance-chuva
echo "$2" > /tmp/skeledance-balada
}
echo "==> Recarregando wallpaper (troca para org.kde.image e volta)..."
plasma_script "org.kde.image"
sleep 1
plasma_script "$PLUGIN_ID"
sleep 2
echo "==> chuva ON"
set_modes 1 0; sleep "$PAUSE"
echo "==> balada ON (transição chuva -> balada)"
set_modes 0 1; sleep "$PAUSE"
echo "==> chuva ON (transição balada -> chuva)"
set_modes 1 0; sleep "$PAUSE"
echo "==> ambos OFF (volta ao retrowave)"
set_modes 0 0
echo "==> Ciclo completo. Confira: só um de lua/globo visível por vez."

View file

@ -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
View 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