feat: balada mode — mirror ball, disco floor, spotlights, sun transition
- Mirror ball globe: canvas-based tile rendering with winner-takes-all specular lighting + Lambert diffuse edges; 8 orthogonal lights orbit slowly (~4 min/rev) ensuring no dark periods - Disco floor tiles: per-frame alpha/color lerp (dt-based) decoupled from the 400ms randomization gate — eliminates choppy low-FPS appearance; smooth ~1.2s fade-in, ~2.4s color blending per tile - Sun: organic JS-generated stripes (sqrt-spaced, perspective-correct), opacity crossfade overlay for retrowave↔balada gradient transition, slow position animation (rises to sky in balada mode) - Spotlights, disco particles, ray rotation, beat pulse - toggle-balada.sh / toggle-skeleton.sh: external IPC scripts via /tmp files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
71845ff2a7
commit
d422cea905
3 changed files with 522 additions and 15 deletions
|
|
@ -31,6 +31,7 @@ html, body {
|
||||||
filter: blur(80px);
|
filter: blur(80px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: nebulaFloat ease-in-out infinite;
|
animation: nebulaFloat ease-in-out infinite;
|
||||||
|
transition: background 3.5s ease;
|
||||||
}
|
}
|
||||||
.nebula-a {
|
.nebula-a {
|
||||||
width: 500px; height: 350px;
|
width: 500px; height: 350px;
|
||||||
|
|
@ -184,7 +185,11 @@ html, body {
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.8s;
|
transition: opacity 0.8s, top 0.8s ease;
|
||||||
|
}
|
||||||
|
body.balada .music-ticker {
|
||||||
|
/* acima do VU meter: bottom 5% + 70px barras + 10px gap */
|
||||||
|
top: calc(100% - 5% - 70px - 10px - 28px);
|
||||||
}
|
}
|
||||||
.music-icon {
|
.music-icon {
|
||||||
color: #ff44aa;
|
color: #ff44aa;
|
||||||
|
|
@ -772,14 +777,13 @@ html, body {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 41.5%;
|
bottom: 41.5%;
|
||||||
transform: translateX(-50%) translateY(50%);
|
transform: translateX(-50%) translateY(50%);
|
||||||
|
transition:
|
||||||
|
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;
|
||||||
|
/* listras geradas por JS — ver buildSunStripes() */
|
||||||
background:
|
background:
|
||||||
repeating-linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent 0px,
|
|
||||||
transparent 13px,
|
|
||||||
rgba(20, 0, 50, 0.72) 13px,
|
|
||||||
rgba(20, 0, 50, 0.72) 17px
|
|
||||||
),
|
|
||||||
linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.28)),
|
linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.28)),
|
||||||
linear-gradient(to bottom,
|
linear-gradient(to bottom,
|
||||||
#fff9cc 0%,
|
#fff9cc 0%,
|
||||||
|
|
@ -842,6 +846,155 @@ html, body {
|
||||||
to { transform: translateY(-38px) scaleX(0.88); opacity: 0.45; }
|
to { transform: translateY(-38px) scaleX(0.88); opacity: 0.45; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
BALADA — SOL VIRA DISCO (raios giram ao redor do centro)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* Sol sobe para o céu */
|
||||||
|
body.balada .synthwave-sun {
|
||||||
|
bottom: 70%;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
/* background fica no #sunBaladaBg para crossfade suave via opacidade */
|
||||||
|
box-shadow:
|
||||||
|
0 0 10px 2px rgba(255,113,206,0.18),
|
||||||
|
0 0 28px 6px rgba(185,103,255,0.10),
|
||||||
|
0 0 60px 12px rgba(1,205,254,0.05),
|
||||||
|
0 0 120px 25px rgba(185,103,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Overlay de balada — fade in/out suave sobre o fundo retrowave */
|
||||||
|
#sunBaladaBg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 9s ease;
|
||||||
|
background: radial-gradient(circle at 40% 35%, #2a0050 0%, #08000f 100%);
|
||||||
|
}
|
||||||
|
body.balada #sunBaladaBg { opacity: 1; }
|
||||||
|
|
||||||
|
/* Listras orgânicas do sol retrowave — geradas por buildSunStripes() */
|
||||||
|
#sunStripes {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 9s ease;
|
||||||
|
}
|
||||||
|
body.balada #sunStripes { opacity: 0; }
|
||||||
|
|
||||||
|
/* Linhas do globo desenhadas via canvas JS — ver drawGlobe() */
|
||||||
|
body.balada .synthwave-sun::before { display: none; }
|
||||||
|
|
||||||
|
/* Halo vaporwave — mais discreto */
|
||||||
|
body.balada .synthwave-sun::after {
|
||||||
|
inset: -50%;
|
||||||
|
background: radial-gradient(ellipse at 50% 50%,
|
||||||
|
rgba(255,113,206,0.12) 0%,
|
||||||
|
rgba(185,103,255,0.08) 38%,
|
||||||
|
rgba(1,205,254,0.04) 60%,
|
||||||
|
transparent 74%
|
||||||
|
);
|
||||||
|
animation: haloBreath 4s ease-in-out infinite alternate;
|
||||||
|
filter: blur(22px);
|
||||||
|
}
|
||||||
|
@keyframes haloBreath {
|
||||||
|
from { transform: scale(0.97); opacity: 0.5; }
|
||||||
|
to { transform: scale(1.06); opacity: 0.85; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raios: container centrado no sol (140px = raio de 280px) */
|
||||||
|
.sun-rays-wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 140px; left: 140px;
|
||||||
|
width: 0; height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 1.8s ease 1s;
|
||||||
|
}
|
||||||
|
body.balada .sun-rays-wrap { opacity: 1; }
|
||||||
|
|
||||||
|
.sun-ray {
|
||||||
|
position: absolute;
|
||||||
|
top: -1.5px; left: 0;
|
||||||
|
height: 3px;
|
||||||
|
transform-origin: left center;
|
||||||
|
border-radius: 0 3px 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
BALADA — REFLETORES (SPOTLIGHTS)
|
||||||
|
============================================================ */
|
||||||
|
.spotlight-wrap {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 41;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 2.2s ease 0.6s;
|
||||||
|
}
|
||||||
|
body.balada .spotlight-wrap { opacity: 1; }
|
||||||
|
|
||||||
|
.spl {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.spl::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -60px; /* começa acima da tela — esconde o corte da geometria */
|
||||||
|
left: -35px;
|
||||||
|
width: 70px; height: calc(68vh + 60px);
|
||||||
|
clip-path: polygon(22% 0%, 78% 0%, 100% 100%, 0% 100%);
|
||||||
|
transform-origin: 50% 60px; /* pivô na borda superior da tela */
|
||||||
|
opacity: 0.12;
|
||||||
|
}
|
||||||
|
.spl-1 { left: 15%; }
|
||||||
|
.spl-1::after { background: linear-gradient(rgba(255,113,206,0.9),transparent); animation: sws1 9s ease-in-out infinite; }
|
||||||
|
.spl-2 { left: 50%; }
|
||||||
|
.spl-2::after { background: linear-gradient(rgba(1,205,254,0.9),transparent); animation: sws2 11s ease-in-out infinite; animation-delay:-3s; }
|
||||||
|
.spl-3 { left: 85%; }
|
||||||
|
.spl-3::after { background: linear-gradient(rgba(185,103,255,0.9),transparent); animation: sws1 8s ease-in-out infinite; animation-delay:-2s; }
|
||||||
|
.spl-4 { left: 30%; }
|
||||||
|
.spl-4::after { background: linear-gradient(rgba(5,255,161,0.9),transparent); animation: sws3 13s ease-in-out infinite; animation-delay:-5s; }
|
||||||
|
.spl-5 { left: 70%; }
|
||||||
|
.spl-5::after { background: linear-gradient(rgba(255,251,150,0.9),transparent); animation: sws2 10s ease-in-out infinite; animation-delay:-7s; }
|
||||||
|
|
||||||
|
@keyframes sws1 { 0%,100%{ transform:rotate(-20deg); } 50%{ transform:rotate(20deg); } }
|
||||||
|
@keyframes sws2 { 0%,100%{ transform:rotate(24deg); } 50%{ transform:rotate(-24deg); } }
|
||||||
|
@keyframes sws3 { 0%,100%{ transform:rotate(-14deg); } 50%{ transform:rotate(30deg); } }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
BALADA — PARTÍCULAS DE LUZ + STROBE
|
||||||
|
============================================================ */
|
||||||
|
.disco-dot {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: discoDotFly linear forwards;
|
||||||
|
}
|
||||||
|
@keyframes discoDotFly {
|
||||||
|
0% { opacity:0; transform:translate(0,0) scale(0); }
|
||||||
|
8% { opacity:1; transform:translate(var(--tx1),var(--ty1)) scale(1); }
|
||||||
|
90% { opacity:0.85; transform:translate(var(--tx2),var(--ty2)) scale(0.6); }
|
||||||
|
100%{ opacity:0; transform:translate(var(--tx3),var(--ty3)) scale(0); }
|
||||||
|
}
|
||||||
|
.strobe {
|
||||||
|
position: fixed; inset: 0;
|
||||||
|
background: white; opacity: 0;
|
||||||
|
pointer-events: none; z-index: 9990;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
BALADA — SOBRESCRITAS DO AMBIENTE
|
||||||
|
============================================================ */
|
||||||
|
body.balada .nebula-a { background: radial-gradient(ellipse, rgba(255,0,100,0.22), transparent 70%); }
|
||||||
|
body.balada .nebula-b { background: radial-gradient(ellipse, rgba(180,0,255,0.18), transparent 70%); }
|
||||||
|
body.balada .nebula-c { background: radial-gradient(ellipse, rgba(255,140,0,0.14), transparent 70%); }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -856,8 +1009,12 @@ html, body {
|
||||||
<!-- Twinkling starfield -->
|
<!-- Twinkling starfield -->
|
||||||
<canvas id="starfield"></canvas>
|
<canvas id="starfield"></canvas>
|
||||||
|
|
||||||
<!-- Synthwave sun — sits behind floor grid and horizon -->
|
<!-- Synthwave sun — sits behind floor grid and horizon; becomes disco ball in balada mode -->
|
||||||
<div class="synthwave-sun"></div>
|
<div class="synthwave-sun">
|
||||||
|
<div id="sunBaladaBg"></div>
|
||||||
|
<div id="sunStripes"></div>
|
||||||
|
<div class="sun-rays-wrap" id="sunRaysWrap"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Retrowave grid floor -->
|
<!-- Retrowave grid floor -->
|
||||||
<canvas id="floorCanvas"></canvas>
|
<canvas id="floorCanvas"></canvas>
|
||||||
|
|
@ -901,9 +1058,24 @@ html, body {
|
||||||
<!-- Lyrics display — verse karaoke -->
|
<!-- Lyrics display — verse karaoke -->
|
||||||
<div class="lyric-display" id="lyricDisplay"><div class="lyric-inner" id="lyricInner"></div></div>
|
<div class="lyric-display" id="lyricDisplay"><div class="lyric-inner" id="lyricInner"></div></div>
|
||||||
|
|
||||||
|
<!-- BALADA: refletores do teto -->
|
||||||
|
<div class="spotlight-wrap">
|
||||||
|
<div class="spl spl-1"></div>
|
||||||
|
<div class="spl spl-2"></div>
|
||||||
|
<div class="spl spl-3"></div>
|
||||||
|
<div class="spl spl-4"></div>
|
||||||
|
<div class="spl spl-5"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BALADA: partículas de luz -->
|
||||||
|
<div id="discoDots" style="position:absolute;inset:0;pointer-events:none;z-index:48;"></div>
|
||||||
|
|
||||||
<!-- Scanlines overlay -->
|
<!-- Scanlines overlay -->
|
||||||
<div class="scanlines"></div>
|
<div class="scanlines"></div>
|
||||||
|
|
||||||
|
<!-- Strobe flash -->
|
||||||
|
<div class="strobe" id="strobe"></div>
|
||||||
|
|
||||||
</div><!-- .scene -->
|
</div><!-- .scene -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -968,10 +1140,36 @@ const floorCanvas = document.getElementById('floorCanvas');
|
||||||
const floorCtx = floorCanvas.getContext('2d');
|
const floorCtx = floorCanvas.getContext('2d');
|
||||||
|
|
||||||
const FLOOR_RATIO = 0.42;
|
const FLOOR_RATIO = 0.42;
|
||||||
|
|
||||||
|
/* Disco floor tiles */
|
||||||
|
const DISCO_COLORS = [
|
||||||
|
[255,113,206],[185,103,255],[1,205,254],
|
||||||
|
[5,255,161],[255,251,150],[255,68,170],
|
||||||
|
[200,0,255],[0,180,255]
|
||||||
|
];
|
||||||
|
const TCOLS = 24, TROWS = 24; // match V_LINES=24, H_LINES=24 for 1:1 tile alignment
|
||||||
|
const discoTileGrid = Array.from({length: TROWS * TCOLS}, () => {
|
||||||
|
const c = DISCO_COLORS[Math.floor(Math.random() * DISCO_COLORS.length)];
|
||||||
|
return { r: c[0], g: c[1], b: c[2], tr: c[0], tg: c[1], tb: c[2], alpha: 0, target: 0,
|
||||||
|
rgba(a) { return `rgba(${this.r|0},${this.g|0},${this.b|0},${a.toFixed(2)})`; } };
|
||||||
|
});
|
||||||
|
let lastTileTs = 0;
|
||||||
|
function tickDiscoTiles(ts) {
|
||||||
|
if (ts - lastTileTs < 400) return;
|
||||||
|
lastTileTs = ts;
|
||||||
|
discoTileGrid.forEach(t => {
|
||||||
|
if (Math.random() < 0.04) {
|
||||||
|
const c = DISCO_COLORS[Math.floor(Math.random() * DISCO_COLORS.length)];
|
||||||
|
t.tr = c[0]; t.tg = c[1]; t.tb = c[2];
|
||||||
|
t.target = Math.random() < 0.4 ? 0 : 0.12 + Math.random() * 0.28;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
const V_LINES = 24;
|
const V_LINES = 24;
|
||||||
const H_LINES = 24;
|
const H_LINES = 24;
|
||||||
let floorScroll = 0;
|
let floorScroll = 0;
|
||||||
let lastFloorTs = 0;
|
let lastFloorTs = 0;
|
||||||
|
let baladaAmt = 0; // 0 = retrowave, 1 = balada — transição suave
|
||||||
let floorFadeGrad = null;
|
let floorFadeGrad = null;
|
||||||
let floorFadeGradH = 0;
|
let floorFadeGradH = 0;
|
||||||
|
|
||||||
|
|
@ -984,6 +1182,7 @@ function drawFloor(ts) {
|
||||||
const dt = lastFloorTs ? Math.min((ts - lastFloorTs) / 1000, 0.1) : 0;
|
const dt = lastFloorTs ? Math.min((ts - lastFloorTs) / 1000, 0.1) : 0;
|
||||||
lastFloorTs = ts;
|
lastFloorTs = ts;
|
||||||
floorScroll = (floorScroll + 0.06 * dt) % 1;
|
floorScroll = (floorScroll + 0.06 * dt) % 1;
|
||||||
|
baladaAmt += ((balada ? 1 : 0) - baladaAmt) * Math.min(1, dt * 0.6); // ~1.7s de transição
|
||||||
|
|
||||||
const W = floorCanvas.width;
|
const W = floorCanvas.width;
|
||||||
const H = floorCanvas.height;
|
const H = floorCanvas.height;
|
||||||
|
|
@ -992,10 +1191,47 @@ function drawFloor(ts) {
|
||||||
floorCtx.clearRect(0, 0, W, H);
|
floorCtx.clearRect(0, 0, W, H);
|
||||||
|
|
||||||
// Vertical lines radiating from vanishing point (center top)
|
// Vertical lines radiating from vanishing point (center top)
|
||||||
// Bottom spread exceeds screen width so laterals are filled
|
|
||||||
const spread = W * 4;
|
const spread = W * 4;
|
||||||
const spreadStart = vx - spread / 2;
|
const spreadStart = vx - spread / 2;
|
||||||
floorCtx.strokeStyle = 'rgba(0, 200, 255, 0.32)';
|
const floorHue = (ts * 0.04) % 360;
|
||||||
|
|
||||||
|
/* ── Tiles de balada anos 80 — fade controlado por baladaAmt ── */
|
||||||
|
if (baladaAmt > 0.01) {
|
||||||
|
tickDiscoTiles(ts);
|
||||||
|
const lerpA = Math.min(1, dt * 1.8);
|
||||||
|
const lerpC = Math.min(1, dt * 0.9);
|
||||||
|
discoTileGrid.forEach(t => {
|
||||||
|
t.alpha += (t.target - t.alpha) * lerpA;
|
||||||
|
t.r += (t.tr - t.r) * lerpC;
|
||||||
|
t.g += (t.tg - t.g) * lerpC;
|
||||||
|
t.b += (t.tb - t.b) * lerpC;
|
||||||
|
});
|
||||||
|
const colStep = spread / TCOLS;
|
||||||
|
for (let r = 0; r < TROWS; r++) {
|
||||||
|
const t1 = ((r / TROWS) + floorScroll) % 1;
|
||||||
|
const t2 = (((r + 1) / TROWS) + floorScroll) % 1;
|
||||||
|
const d1 = Math.pow(t1, 2.2), d2 = Math.pow(t2, 2.2);
|
||||||
|
const y1 = d1 * H, y2 = d2 * H;
|
||||||
|
if (y2 <= y1 || y2 - y1 < 1) continue;
|
||||||
|
for (let c = 0; c < TCOLS; c++) {
|
||||||
|
const tile = discoTileGrid[r * TCOLS + c];
|
||||||
|
if (tile.alpha < 0.01) continue;
|
||||||
|
const bx1 = spreadStart + c * colStep;
|
||||||
|
const bx2 = bx1 + colStep;
|
||||||
|
floorCtx.fillStyle = tile.rgba(tile.alpha * baladaAmt);
|
||||||
|
floorCtx.beginPath();
|
||||||
|
floorCtx.moveTo(vx + (bx1 - vx) * d1, y1);
|
||||||
|
floorCtx.lineTo(vx + (bx2 - vx) * d1, y1);
|
||||||
|
floorCtx.lineTo(vx + (bx2 - vx) * d2, y2);
|
||||||
|
floorCtx.lineTo(vx + (bx1 - vx) * d2, y2);
|
||||||
|
floorCtx.closePath();
|
||||||
|
floorCtx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vH = 200 + (floorHue - 200) * baladaAmt;
|
||||||
|
floorCtx.strokeStyle = `hsla(${vH}, 100%, 65%, 0.32)`;
|
||||||
floorCtx.lineWidth = 2;
|
floorCtx.lineWidth = 2;
|
||||||
for (let i = 0; i <= V_LINES; i++) {
|
for (let i = 0; i <= V_LINES; i++) {
|
||||||
const bx = spreadStart + (i / V_LINES) * spread;
|
const bx = spreadStart + (i / V_LINES) * spread;
|
||||||
|
|
@ -1008,11 +1244,13 @@ function drawFloor(ts) {
|
||||||
// Horizontal lines — perspective-correct spacing, animated
|
// Horizontal lines — perspective-correct spacing, animated
|
||||||
for (let i = 0; i < H_LINES; i++) {
|
for (let i = 0; i < H_LINES; i++) {
|
||||||
const t = ((i / H_LINES) + floorScroll) % 1;
|
const t = ((i / H_LINES) + floorScroll) % 1;
|
||||||
const depth = Math.pow(t, 2.2); // power curve: compressed near horizon
|
const depth = Math.pow(t, 2.2);
|
||||||
const y = depth * H;
|
const y = depth * H;
|
||||||
const x1 = vx - (spread / 2) * depth;
|
const x1 = vx - (spread / 2) * depth;
|
||||||
const x2 = vx + (spread / 2) * depth;
|
const x2 = vx + (spread / 2) * depth;
|
||||||
floorCtx.strokeStyle = `rgba(0, 200, 255, ${(0.12 + depth * 0.25).toFixed(2)})`;
|
const alpha = (0.12 + depth * 0.25).toFixed(2);
|
||||||
|
const hH = (200 + (floorHue + i * 12 - 200) * baladaAmt + 360) % 360;
|
||||||
|
floorCtx.strokeStyle = `hsla(${hH}, 100%, 65%, ${alpha})`;
|
||||||
floorCtx.lineWidth = Math.max(1, depth * 11);
|
floorCtx.lineWidth = Math.max(1, depth * 11);
|
||||||
floorCtx.beginPath();
|
floorCtx.beginPath();
|
||||||
floorCtx.moveTo(x1, y);
|
floorCtx.moveTo(x1, y);
|
||||||
|
|
@ -1332,7 +1570,7 @@ let isPlaying = false;
|
||||||
|
|
||||||
/* Spawn on every beat (500ms) with a 50 % chance of two notes */
|
/* Spawn on every beat (500ms) with a 50 % chance of two notes */
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!isPlaying) return;
|
if (!isPlaying || !showSkeleton) return;
|
||||||
spawnNote();
|
spawnNote();
|
||||||
if (Math.random() < 0.5) spawnNote();
|
if (Math.random() < 0.5) spawnNote();
|
||||||
}, BEAT_MS);
|
}, BEAT_MS);
|
||||||
|
|
@ -1389,6 +1627,271 @@ setInterval(nextDanceStyle, 8000);
|
||||||
function pickSkeleton() {}
|
function pickSkeleton() {}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
BALADA MODE
|
||||||
|
============================================================ */
|
||||||
|
let balada = false;
|
||||||
|
|
||||||
|
const sunEl = document.querySelector('.synthwave-sun');
|
||||||
|
|
||||||
|
/* Listras orgânicas do sol — perspectiva compressa: mais densas em baixo, esparsas em cima.
|
||||||
|
Distribuição sqrt(i/(N+1)): primeira listra ~22% do topo, última ~95%, gap diminuindo para baixo. */
|
||||||
|
function buildSunStripes() {
|
||||||
|
const el = document.getElementById('sunStripes');
|
||||||
|
const N = 28;
|
||||||
|
const stops = ['transparent 0%'];
|
||||||
|
|
||||||
|
for (let i = 1; i <= N; i++) {
|
||||||
|
// sqrt(i/N): primeira listra em ~19% do topo, última exatamente em 100%
|
||||||
|
const t = Math.sqrt(i / N);
|
||||||
|
const pct = 100 * t;
|
||||||
|
const frac = (i - 1) / (N - 1); // 0→1 linear para rampas
|
||||||
|
|
||||||
|
// Espessura: 0.7px (fino no topo) → 6px (sólido no fundo)
|
||||||
|
const wPx = 0.7 + 5.3 * frac;
|
||||||
|
const wPct = wPx / 280 * 100;
|
||||||
|
|
||||||
|
// Opacidade: 0.14 (suave mas visível no topo) → 0.72 no fundo, raiz para não ser flat
|
||||||
|
const a = (0.14 + 0.58 * Math.sqrt(frac)).toFixed(2);
|
||||||
|
|
||||||
|
const p1 = Math.max(0, pct - wPct / 2).toFixed(3);
|
||||||
|
const p2 = Math.min(100, pct + wPct / 2).toFixed(3);
|
||||||
|
|
||||||
|
stops.push(`transparent ${p1}%`);
|
||||||
|
stops.push(`rgba(14,0,30,${a}) ${p1}%`);
|
||||||
|
stops.push(`rgba(14,0,30,${a}) ${p2}%`);
|
||||||
|
stops.push(`transparent ${p2}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
stops.push('transparent 100%');
|
||||||
|
el.style.background = `linear-gradient(to bottom,${stops.join(',')})`;
|
||||||
|
}
|
||||||
|
buildSunStripes();
|
||||||
|
const sunRaysWrap = document.getElementById('sunRaysWrap');
|
||||||
|
|
||||||
|
/* ── Canvas do globo — latitude/longitude com curvatura real ── */
|
||||||
|
const globeCanvas = document.createElement('canvas');
|
||||||
|
globeCanvas.width = 280;
|
||||||
|
globeCanvas.height = 280;
|
||||||
|
globeCanvas.style.cssText =
|
||||||
|
'position:absolute;inset:0;border-radius:50%;pointer-events:none;' +
|
||||||
|
'opacity:0;transition:opacity 1.5s ease;';
|
||||||
|
sunEl.appendChild(globeCanvas);
|
||||||
|
const gctx = globeCanvas.getContext('2d');
|
||||||
|
let globeRot = 0, lastGlobeTs = 0;
|
||||||
|
|
||||||
|
/* 8 luzes nos eixos ortogonais — cobertura 360° garantida.
|
||||||
|
±Y não são afetados pelo lrot, ±X e ±Z orbitam entre si. */
|
||||||
|
const MIRROR_LIGHTS = [
|
||||||
|
{ x: 1, y: 0, z: 0, r: 1, g: 205, b: 254 }, // cyan +X
|
||||||
|
{ x: -1, y: 0, z: 0, r: 185, g: 103, b: 255 }, // purple -X
|
||||||
|
{ x: 0, y: 1, z: 0, r: 255, g: 113, b: 206 }, // pink +Y (topo — fixo)
|
||||||
|
{ x: 0, y: -1, z: 0, r: 5, g: 255, b: 161 }, // mint -Y (base — fixo)
|
||||||
|
{ x: 0, y: 0, z: 1, r: 255, g: 255, b: 255 }, // branco +Z
|
||||||
|
{ x: 0, y: 0, z: -1, r: 255, g: 251, b: 150 }, // amarelo -Z
|
||||||
|
{ x: 0.71, y: 0.71, z: 0, r: 255, g: 113, b: 206 }, // pink diagonal alta
|
||||||
|
{ x: -0.71, y: -0.71, z: 0, r: 5, g: 255, b: 161 }, // mint diagonal baixa
|
||||||
|
];
|
||||||
|
|
||||||
|
function drawGlobe(ts) {
|
||||||
|
requestAnimationFrame(drawGlobe);
|
||||||
|
if (baladaAmt < 0.01) { lastGlobeTs = 0; return; }
|
||||||
|
globeCanvas.style.opacity = baladaAmt;
|
||||||
|
const dt = lastGlobeTs ? Math.min((ts - lastGlobeTs) / 1000, 0.05) : 0;
|
||||||
|
lastGlobeTs = ts;
|
||||||
|
globeRot = (globeRot + dt * 0.5) % (Math.PI * 2);
|
||||||
|
|
||||||
|
const N_LAT = 14;
|
||||||
|
const BASE_LON = 28;
|
||||||
|
const FILL = 0.78;
|
||||||
|
const SHINE = 8; // expoente baixo → highlights amplos, muitos tiles acesos
|
||||||
|
|
||||||
|
const R = 137, cx = 140, cy = 140;
|
||||||
|
gctx.clearRect(0, 0, 280, 280);
|
||||||
|
gctx.save();
|
||||||
|
gctx.beginPath();
|
||||||
|
gctx.arc(cx, cy, R, 0, Math.PI * 2);
|
||||||
|
gctx.clip();
|
||||||
|
gctx.fillStyle = '#06000c';
|
||||||
|
gctx.fillRect(0, 0, 280, 280);
|
||||||
|
|
||||||
|
for (let lat = 0; lat < N_LAT; lat++) {
|
||||||
|
const phi1 = -Math.PI / 2 + (lat / N_LAT) * Math.PI;
|
||||||
|
const phi2 = -Math.PI / 2 + ((lat + 1) / N_LAT) * Math.PI;
|
||||||
|
const phiC = (phi1 + phi2) / 2;
|
||||||
|
const cosP = Math.cos(phiC), sinP = Math.sin(phiC);
|
||||||
|
const nLon = Math.max(3, Math.round(BASE_LON * cosP));
|
||||||
|
|
||||||
|
for (let lon = 0; lon < nLon; lon++) {
|
||||||
|
const th1 = globeRot + (lon / nLon) * Math.PI * 2;
|
||||||
|
const th2 = globeRot + ((lon + 1) / nLon) * Math.PI * 2;
|
||||||
|
const thC = (th1 + th2) / 2;
|
||||||
|
|
||||||
|
const nx = cosP * Math.sin(thC);
|
||||||
|
const ny = sinP;
|
||||||
|
const nz = cosP * Math.cos(thC);
|
||||||
|
if (nz <= 0.02) continue;
|
||||||
|
|
||||||
|
// Vetor refletido da câmera (0,0,1) sobre a normal do tile
|
||||||
|
const rx = 2 * nz * nx;
|
||||||
|
const ry = 2 * nz * ny;
|
||||||
|
const rz = 2 * nz * nz - 1;
|
||||||
|
|
||||||
|
const lrot = ts * 0.000026;
|
||||||
|
let bestDot = 0, bestR = 0, bestG = 0, bestB = 0;
|
||||||
|
let dr = 0, dg = 0, db = 0;
|
||||||
|
for (const L of MIRROR_LIGHTS) {
|
||||||
|
const lx = L.x * Math.cos(lrot) - L.z * Math.sin(lrot);
|
||||||
|
const lz = L.x * Math.sin(lrot) + L.z * Math.cos(lrot);
|
||||||
|
const spec = rx * lx + ry * L.y + rz * lz;
|
||||||
|
if (spec > bestDot) { bestDot = spec; bestR = L.r; bestG = L.g; bestB = L.b; }
|
||||||
|
const diff = Math.max(0, nx * lx + ny * L.y + nz * lz);
|
||||||
|
dr += diff * L.r * 0.07; dg += diff * L.g * 0.07; db += diff * L.b * 0.07;
|
||||||
|
}
|
||||||
|
const s = bestDot > 0.2 ? Math.pow(bestDot, SHINE) : 0;
|
||||||
|
const amb = 0.18 + 0.08 * nz;
|
||||||
|
let cr = amb * 100 + dr + s * bestR;
|
||||||
|
let cg = amb * 65 + dg + s * bestG;
|
||||||
|
let cb = amb * 170 + db + s * bestB;
|
||||||
|
|
||||||
|
const dP = (phi2 - phi1) * FILL, oP = (phi2 - phi1) * (1 - FILL) / 2;
|
||||||
|
const dTh = (th2 - th1) * FILL, oTh = (th2 - th1) * (1 - FILL) / 2;
|
||||||
|
const pts = [];
|
||||||
|
for (let ci = 0; ci < 4; ci++) {
|
||||||
|
const p = phi1 + oP + (ci >> 1) * dP;
|
||||||
|
const th = th1 + oTh + (ci & 1) * dTh;
|
||||||
|
pts.push([cx + R * Math.cos(p) * Math.sin(th), cy - R * Math.sin(p)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
gctx.fillStyle = `rgb(${Math.min(255,cr)|0},${Math.min(255,cg)|0},${Math.min(255,cb)|0})`;
|
||||||
|
gctx.beginPath();
|
||||||
|
gctx.moveTo(pts[0][0], pts[0][1]);
|
||||||
|
gctx.lineTo(pts[1][0], pts[1][1]);
|
||||||
|
gctx.lineTo(pts[3][0], pts[3][1]);
|
||||||
|
gctx.lineTo(pts[2][0], pts[2][1]);
|
||||||
|
gctx.closePath();
|
||||||
|
gctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gctx.restore();
|
||||||
|
}
|
||||||
|
requestAnimationFrame(drawGlobe);
|
||||||
|
const discoDots = document.getElementById('discoDots');
|
||||||
|
const strobeEl = document.getElementById('strobe');
|
||||||
|
|
||||||
|
/* ── Raios do sol/disco — paleta vaporwave ── */
|
||||||
|
const RAY_COLORS = [
|
||||||
|
'#ff71ce','#b967ff','#01cdfe',
|
||||||
|
'#05ffa1','#fffb96','#b967ff',
|
||||||
|
'#ff71ce','#01cdfe','#b967ff','#ff71ce'
|
||||||
|
];
|
||||||
|
(function buildRays() {
|
||||||
|
for (let i = 0; i < RAY_COLORS.length; i++) {
|
||||||
|
const r = document.createElement('div');
|
||||||
|
r.className = 'sun-ray';
|
||||||
|
const col = RAY_COLORS[i];
|
||||||
|
r.style.width = '95vmax';
|
||||||
|
r.style.height = '18px';
|
||||||
|
r.style.top = '-9px';
|
||||||
|
r.style.transform = `rotate(${(i / RAY_COLORS.length) * 360}deg)`;
|
||||||
|
/* gradiente suave — sem borda dura */
|
||||||
|
r.style.background =
|
||||||
|
`linear-gradient(to right, ${col}28 0%, ${col}12 45%, transparent 80%)`;
|
||||||
|
r.style.filter = 'blur(3px)';
|
||||||
|
sunRaysWrap.appendChild(r);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
/* Rotação contínua dos raios */
|
||||||
|
(function animateRays(ts) {
|
||||||
|
if (balada) sunRaysWrap.style.transform = `rotate(${(ts * 0.025) % 360}deg)`;
|
||||||
|
requestAnimationFrame(animateRays);
|
||||||
|
})(0);
|
||||||
|
|
||||||
|
/* ── Partículas de luz disparadas do sol ── */
|
||||||
|
function spawnDiscoDot() {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.className = 'disco-dot';
|
||||||
|
const col = RAY_COLORS[Math.floor(Math.random() * RAY_COLORS.length)];
|
||||||
|
const sz = 4 + Math.random() * 7;
|
||||||
|
const dur = 1.4 + Math.random() * 2;
|
||||||
|
const ox = 48 + Math.random() * 4;
|
||||||
|
const oy = 22 + Math.random() * 4;
|
||||||
|
const s = () => (Math.random() - 0.5) * 2;
|
||||||
|
d.style.cssText =
|
||||||
|
`width:${sz}px;height:${sz}px;` +
|
||||||
|
`background:${col};box-shadow:0 0 ${sz * 1.5}px ${col};` +
|
||||||
|
`left:${ox}vw;top:${oy}vh;` +
|
||||||
|
`--tx1:${s()*12}vw;--ty1:${s()*8}vh;` +
|
||||||
|
`--tx2:${s()*35}vw;--ty2:${5 + Math.random() * 30}vh;` +
|
||||||
|
`--tx3:${s()*65}vw;--ty3:${15 + Math.random() * 55}vh;` +
|
||||||
|
`animation-duration:${dur}s;`;
|
||||||
|
discoDots.appendChild(d);
|
||||||
|
setTimeout(() => d.remove(), dur * 1000 + 100);
|
||||||
|
}
|
||||||
|
setInterval(() => {
|
||||||
|
if (!balada) return;
|
||||||
|
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;
|
||||||
|
sunEl.style.boxShadow =
|
||||||
|
'0 0 14px 3px rgba(255,113,206,0.08),' +
|
||||||
|
'0 0 30px 6px rgba(185,103,255,0.05)';
|
||||||
|
setTimeout(() => { sunEl.style.boxShadow = ''; }, 1200);
|
||||||
|
}
|
||||||
|
setInterval(() => { if (balada) globeBeat(); }, BEAT_MS);
|
||||||
|
|
||||||
|
/* ── Toggle ── */
|
||||||
|
function setBaladaMode(on) {
|
||||||
|
if (on === balada) return;
|
||||||
|
balada = on;
|
||||||
|
document.body.classList.toggle('balada', balada);
|
||||||
|
globeCanvas.style.opacity = on ? '1' : '0';
|
||||||
|
}
|
||||||
|
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 */
|
||||||
|
(function pollBaladaFile() {
|
||||||
|
fetch('file:///tmp/skeledance-balada?t=' + Date.now())
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => setBaladaMode(t.trim() === '1'))
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setTimeout(pollBaladaFile, 800));
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function pollSkeletonFile() {
|
||||||
|
fetch('file:///tmp/skeledance-skeleton?t=' + Date.now())
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => setShowSkeleton(t.trim() !== '0'))
|
||||||
|
.catch(() => {}) /* arquivo ausente = skeleton visível */
|
||||||
|
.finally(() => setTimeout(pollSkeletonFile, 800));
|
||||||
|
})();
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
DEV SHIM — only active when served via HTTP (not from QML)
|
DEV SHIM — only active when served via HTTP (not from QML)
|
||||||
Simulates playerctl updates so the music ticker works in-browser.
|
Simulates playerctl updates so the music ticker works in-browser.
|
||||||
|
|
|
||||||
2
toggle-balada.sh
Executable file
2
toggle-balada.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
[ "$(cat /tmp/skeledance-balada 2>/dev/null)" = "1" ] && echo 0 > /tmp/skeledance-balada || echo 1 > /tmp/skeledance-balada
|
||||||
2
toggle-skeleton.sh
Executable file
2
toggle-skeleton.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
[ "$(cat /tmp/skeledance-skeleton 2>/dev/null)" = "0" ] && echo 1 > /tmp/skeledance-skeleton || echo 0 > /tmp/skeledance-skeleton
|
||||||
Loading…
Add table
Add a link
Reference in a new issue