Swaps DANCEBOT-9000 CSS robot for a pool of APNG characters that cycle randomly every 8s and on track change: 4 skeleton GIFs, a dancing baby (YouTube clip), and Pepe. Background removal done with ffmpeg colorkey. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1039 lines
29 KiB
HTML
1039 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
|
|
/* ============================================================
|
|
RESET + BASE
|
|
============================================================ */
|
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
html, body {
|
|
width: 100%; height: 100%;
|
|
overflow: hidden;
|
|
background: #000814;
|
|
user-select: none;
|
|
}
|
|
|
|
.scene {
|
|
position: relative;
|
|
width: 100%; height: 100%;
|
|
}
|
|
|
|
/* ============================================================
|
|
BACKGROUND: NEBULA
|
|
============================================================ */
|
|
.nebula {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
filter: blur(80px);
|
|
pointer-events: none;
|
|
animation: nebulaFloat ease-in-out infinite;
|
|
}
|
|
.nebula-a {
|
|
width: 500px; height: 350px;
|
|
background: radial-gradient(ellipse, rgba(120,0,220,0.18), transparent 70%);
|
|
top: 5%; left: 5%;
|
|
animation-duration: 9s;
|
|
}
|
|
.nebula-b {
|
|
width: 420px; height: 300px;
|
|
background: radial-gradient(ellipse, rgba(0,80,200,0.15), transparent 70%);
|
|
top: 15%; right: 8%;
|
|
animation-duration: 12s;
|
|
animation-direction: reverse;
|
|
}
|
|
.nebula-c {
|
|
width: 300px; height: 200px;
|
|
background: radial-gradient(ellipse, rgba(200,0,100,0.1), transparent 70%);
|
|
bottom: 40%; left: 20%;
|
|
animation-duration: 7s;
|
|
animation-delay: -3s;
|
|
}
|
|
@keyframes nebulaFloat {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
50% { transform: translate(25px, -20px) scale(1.05); }
|
|
}
|
|
|
|
/* ============================================================
|
|
BACKGROUND: STARFIELD CANVAS
|
|
============================================================ */
|
|
#starfield {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ============================================================
|
|
RETROWAVE GRID FLOOR
|
|
============================================================ */
|
|
#floorCanvas {
|
|
position: absolute;
|
|
bottom: 0; left: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Horizon glow line */
|
|
.horizon {
|
|
position: absolute;
|
|
bottom: 41.5%;
|
|
left: 0; right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(
|
|
to right,
|
|
transparent 0%,
|
|
rgba(255, 0, 150, 0.9) 20%,
|
|
rgba(0, 220, 255, 1) 50%,
|
|
rgba(255, 0, 150, 0.9) 80%,
|
|
transparent 100%
|
|
);
|
|
box-shadow: 0 0 25px rgba(0,220,255,0.8), 0 0 80px rgba(0,200,255,0.25);
|
|
animation: horizonPulse 1s ease-in-out infinite;
|
|
}
|
|
@keyframes horizonPulse {
|
|
0%, 100% { opacity: 0.75; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
/* ============================================================
|
|
VU METER (moved up)
|
|
============================================================ */
|
|
.vu-meter {
|
|
position: absolute;
|
|
bottom: 10%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 4px;
|
|
pointer-events: none;
|
|
}
|
|
.vu-bar {
|
|
width: 9px;
|
|
min-height: 3px;
|
|
border-radius: 3px 3px 0 0;
|
|
}
|
|
|
|
/* ============================================================
|
|
MUSIC TICKER
|
|
============================================================ */
|
|
.music-ticker {
|
|
position: absolute;
|
|
bottom: 4%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
max-width: 80vw;
|
|
overflow: hidden;
|
|
}
|
|
.music-icon {
|
|
color: #ff44aa;
|
|
font-size: 18px;
|
|
text-shadow: 0 0 10px #ff44aa;
|
|
animation: iconPulse 0.5s ease-in-out infinite alternate;
|
|
flex-shrink: 0;
|
|
}
|
|
@keyframes iconPulse {
|
|
from { opacity: 0.5; transform: scale(0.9); }
|
|
to { opacity: 1; transform: scale(1.1); }
|
|
}
|
|
.music-label {
|
|
color: rgba(0, 200, 255, 0.6);
|
|
font-family: 'Courier New', monospace;
|
|
font-size: clamp(9px, 1vw, 13px);
|
|
letter-spacing: 3px;
|
|
flex-shrink: 0;
|
|
}
|
|
.music-scroll-wrap {
|
|
overflow: hidden;
|
|
max-width: 60vw;
|
|
}
|
|
.music-text {
|
|
display: inline-block;
|
|
color: #ffffff;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: clamp(11px, 1.2vw, 17px);
|
|
text-shadow: 0 0 8px rgba(0,200,255,0.7);
|
|
white-space: nowrap;
|
|
animation: musicScroll 18s linear infinite;
|
|
}
|
|
.music-text.short { animation: none; }
|
|
|
|
@keyframes musicScroll {
|
|
0% { transform: translateX(100%); }
|
|
100% { transform: translateX(-100%); }
|
|
}
|
|
|
|
/* ============================================================
|
|
MUSIC NOTES (JS-spawned)
|
|
============================================================ */
|
|
.music-note {
|
|
position: absolute;
|
|
pointer-events: none;
|
|
animation: noteFly linear forwards;
|
|
opacity: 0;
|
|
}
|
|
@keyframes noteFly {
|
|
0% { transform: translateY(0) rotate(-10deg) scale(0.6); opacity: 0; }
|
|
12% { opacity: 1; }
|
|
88% { opacity: 0.9; }
|
|
100% { transform: translateY(-110px) rotate(18deg) scale(1.3); opacity: 0; }
|
|
}
|
|
|
|
/* ============================================================
|
|
SCANLINES OVERLAY
|
|
============================================================ */
|
|
.scanlines {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
transparent 0px, transparent 3px,
|
|
rgba(0,0,0,0.07) 3px, rgba(0,0,0,0.07) 4px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* ============================================================
|
|
ROBOT WRAPPER
|
|
120 BPM = 0.5s per beat. All animations key off this tempo.
|
|
============================================================ */
|
|
.robot-wrap {
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: 32%;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.skeleton-gif {
|
|
display: block;
|
|
height: 40vh;
|
|
width: auto;
|
|
filter: drop-shadow(0 0 20px rgba(0,200,255,0.4));
|
|
}
|
|
|
|
/* Entire robot bounces on the beat (0.5s = 1 beat at 120 BPM) */
|
|
.robot {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
animation: robotBounce 0.5s ease-in-out infinite alternate;
|
|
}
|
|
@keyframes robotBounce {
|
|
from { transform: translateY(0) scaleY(1) scaleX(1); }
|
|
to { transform: translateY(-16px) scaleY(1.02) scaleX(0.99); }
|
|
}
|
|
|
|
/* ---- ANTENNA ---- */
|
|
.antenna-wrap {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-end;
|
|
height: 44px;
|
|
}
|
|
.antenna-stick {
|
|
width: 5px; height: 36px;
|
|
background: linear-gradient(to top, #6678a0, #aabbd0);
|
|
border-radius: 3px;
|
|
position: relative;
|
|
transform-origin: bottom center;
|
|
animation: antennaSway 0.5s ease-in-out infinite alternate;
|
|
}
|
|
/* The sway is opposite to the bounce so it looks reactive */
|
|
@keyframes antennaSway {
|
|
from { transform: rotate(-14deg); }
|
|
to { transform: rotate(14deg); }
|
|
}
|
|
.antenna-ball {
|
|
position: absolute;
|
|
top: -11px; left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 16px; height: 16px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 35% 25%, #ff99cc, #ff0077);
|
|
box-shadow: 0 0 12px #ff0077, 0 0 30px rgba(255,0,100,0.5);
|
|
animation: ballBlink 0.25s ease-in-out infinite alternate;
|
|
}
|
|
@keyframes ballBlink {
|
|
from { opacity: 0.45; box-shadow: 0 0 5px #ff0077; }
|
|
to { opacity: 1; box-shadow: 0 0 18px #ff0077, 0 0 40px rgba(255,0,100,0.6); }
|
|
}
|
|
|
|
/* ---- HEAD ---- */
|
|
.head {
|
|
width: 94px; height: 78px;
|
|
background: linear-gradient(145deg, #c8d4e8 0%, #8898b8 55%, #6070a0 100%);
|
|
border-radius: 22px;
|
|
border: 2px solid #99aacc;
|
|
box-shadow:
|
|
inset 0 3px 7px rgba(255,255,255,0.38),
|
|
inset 0 -2px 5px rgba(0,0,60,0.3),
|
|
0 6px 18px rgba(0,0,100,0.5);
|
|
position: relative;
|
|
animation: headTilt 1s ease-in-out infinite;
|
|
margin-top: 3px;
|
|
}
|
|
@keyframes headTilt {
|
|
0% { transform: rotate(-4deg) translateY(0); }
|
|
25% { transform: rotate(0deg) translateY(-3px); }
|
|
50% { transform: rotate(4deg) translateY(0); }
|
|
75% { transform: rotate(0deg) translateY(-3px); }
|
|
100% { transform: rotate(-4deg) translateY(0); }
|
|
}
|
|
|
|
/* EYES */
|
|
.eyes {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
padding: 20px 14px 0;
|
|
}
|
|
.eye {
|
|
width: 23px; height: 23px;
|
|
background: radial-gradient(circle at 38% 30%, #aaffdd, #00cc66);
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 10px #00ff88, 0 0 22px rgba(0,255,136,0.45);
|
|
animation: eyeBlink 3.7s ease-in-out infinite;
|
|
position: relative;
|
|
}
|
|
/* Specular dot */
|
|
.eye::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 5px; left: 5px;
|
|
width: 7px; height: 7px;
|
|
background: rgba(255,255,255,0.75);
|
|
border-radius: 50%;
|
|
}
|
|
/* Second eye blinks slightly offset */
|
|
.eye:last-child {
|
|
animation-delay: 0.08s;
|
|
}
|
|
@keyframes eyeBlink {
|
|
0%, 87%, 100% { transform: scaleY(1); }
|
|
93% { transform: scaleY(0.07); }
|
|
}
|
|
|
|
/* LED MOUTH */
|
|
.mouth-panel {
|
|
margin: 7px auto 0;
|
|
width: 54px; height: 11px;
|
|
background: #000c20;
|
|
border-radius: 4px;
|
|
border: 1px solid rgba(0,200,255,0.45);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-around;
|
|
padding: 0 5px;
|
|
overflow: hidden;
|
|
}
|
|
.mouth-seg {
|
|
width: 6px; height: 7px;
|
|
border-radius: 2px;
|
|
animation: mouthSegAnim 0.5s ease-in-out infinite alternate;
|
|
}
|
|
.mouth-seg:nth-child(1) { background:#00ddff; animation-delay:0.00s; }
|
|
.mouth-seg:nth-child(2) { background:#00ccee; animation-delay:0.10s; }
|
|
.mouth-seg:nth-child(3) { background:#00bbff; animation-delay:0.05s; }
|
|
.mouth-seg:nth-child(4) { background:#00ccee; animation-delay:0.10s; }
|
|
.mouth-seg:nth-child(5) { background:#00ddff; animation-delay:0.00s; }
|
|
@keyframes mouthSegAnim {
|
|
from { opacity:1; box-shadow: 0 0 4px currentColor; }
|
|
to { opacity:0.25; box-shadow: none; }
|
|
}
|
|
|
|
/* ---- NECK ---- */
|
|
.neck {
|
|
width: 26px; height: 13px;
|
|
background: linear-gradient(to bottom, #7080a8, #50607a);
|
|
border-radius: 0 0 5px 5px;
|
|
box-shadow: inset 0 -2px 4px rgba(0,0,60,0.4);
|
|
}
|
|
|
|
/* ---- MIDDLE: left arm + torso + right arm ---- */
|
|
.middle {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
/* SHOULDER BALL */
|
|
.shoulder {
|
|
width: 18px; height: 18px;
|
|
margin-top: 10px;
|
|
background: radial-gradient(circle at 32% 28%, #aabbd0, #5a6890);
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 8px rgba(0,100,255,0.25);
|
|
z-index: 1;
|
|
}
|
|
|
|
/* UPPER ARM */
|
|
.arm {
|
|
width: 21px; height: 66px;
|
|
background: linear-gradient(to bottom, #8090b8, #5868a0);
|
|
border-radius: 11px;
|
|
position: relative;
|
|
transform-origin: top center;
|
|
box-shadow:
|
|
inset 0 2px 4px rgba(255,255,255,0.2),
|
|
0 3px 10px rgba(0,0,80,0.4);
|
|
margin-top: 4px;
|
|
}
|
|
.arm.left { animation: armSwingL 1s ease-in-out infinite; }
|
|
.arm.right { animation: armSwingR 1s ease-in-out infinite; }
|
|
|
|
@keyframes armSwingL {
|
|
0%, 100% { transform: rotate(-52deg); }
|
|
50% { transform: rotate(42deg); }
|
|
}
|
|
@keyframes armSwingR {
|
|
0%, 100% { transform: rotate(52deg); }
|
|
50% { transform: rotate(-42deg); }
|
|
}
|
|
|
|
/* HAND */
|
|
.hand {
|
|
position: absolute;
|
|
bottom: -13px; left: -7px;
|
|
width: 35px; height: 35px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 35% 30%, #aabbd0, #5868a0);
|
|
box-shadow: 0 0 10px rgba(0,100,255,0.22);
|
|
}
|
|
|
|
/* ---- TORSO ---- */
|
|
.torso {
|
|
width: 104px; height: 95px;
|
|
background: linear-gradient(145deg, #9aaac0 0%, #6070a0 55%, #485888 100%);
|
|
border-radius: 14px;
|
|
border: 2px solid #7888b8;
|
|
box-shadow:
|
|
inset 0 3px 7px rgba(255,255,255,0.2),
|
|
inset 0 -3px 7px rgba(0,0,60,0.3),
|
|
0 6px 22px rgba(0,0,100,0.6);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
|
|
/* CHEST SCREEN */
|
|
.chest-screen {
|
|
width: 72px; height: 60px;
|
|
background: #000c20;
|
|
border-radius: 9px;
|
|
border: 1px solid rgba(0,200,255,0.65);
|
|
box-shadow: 0 0 14px rgba(0,200,255,0.5), inset 0 0 14px rgba(0,80,200,0.3);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: space-evenly;
|
|
padding: 6px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
/* Glass sheen */
|
|
.chest-screen::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; left: 0; right: 50%; height: 50%;
|
|
background: linear-gradient(135deg, rgba(255,255,255,0.06), transparent);
|
|
border-radius: 9px 0 0 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.eq-row {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 3px;
|
|
width: 100%; height: 13px;
|
|
}
|
|
.eq-col {
|
|
flex: 1;
|
|
border-radius: 2px 2px 0 0;
|
|
/* height and animation driven by JS */
|
|
}
|
|
|
|
.chest-logo {
|
|
color: #00ccff;
|
|
font-size: 11px;
|
|
font-family: monospace;
|
|
text-shadow: 0 0 6px #00ccff;
|
|
letter-spacing: 2px;
|
|
animation: logoFlicker 1.5s steps(2) infinite;
|
|
}
|
|
@keyframes logoFlicker {
|
|
0%, 66% { opacity: 1; }
|
|
33%, 100% { opacity: 0.55; }
|
|
}
|
|
|
|
/* ---- HIP ---- */
|
|
.hip {
|
|
width: 92px; height: 24px;
|
|
background: linear-gradient(to bottom, #5868a0, #404a78);
|
|
border-radius: 0 0 10px 10px;
|
|
border: 1px solid rgba(80,100,180,0.6);
|
|
box-shadow: 0 5px 12px rgba(0,0,100,0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.rivet {
|
|
width: 9px; height: 9px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 32% 28%, #aabbd0, #4a5880);
|
|
box-shadow: 0 1px 3px rgba(0,0,80,0.5);
|
|
}
|
|
|
|
/* ---- LEGS ---- */
|
|
.legs {
|
|
display: flex;
|
|
gap: 14px;
|
|
margin-top: 2px;
|
|
}
|
|
.leg-grp {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
/* UPPER LEG */
|
|
.upper-leg {
|
|
width: 28px; height: 68px;
|
|
background: linear-gradient(to bottom, #6070a0, #485878);
|
|
border-radius: 14px;
|
|
transform-origin: top center;
|
|
box-shadow:
|
|
inset 0 2px 5px rgba(255,255,255,0.15),
|
|
0 3px 10px rgba(0,0,80,0.5);
|
|
}
|
|
.upper-leg.left { animation: upperLegL 1s ease-in-out infinite; }
|
|
.upper-leg.right { animation: upperLegR 1s ease-in-out infinite; }
|
|
|
|
@keyframes upperLegL {
|
|
0% { transform: rotate(-18deg); }
|
|
25% { transform: rotate(4deg) translateY(-4px); }
|
|
50% { transform: rotate(18deg); }
|
|
75% { transform: rotate(4deg) translateY(-4px); }
|
|
100% { transform: rotate(-18deg); }
|
|
}
|
|
@keyframes upperLegR {
|
|
0% { transform: rotate(18deg); }
|
|
25% { transform: rotate(4deg) translateY(-4px); }
|
|
50% { transform: rotate(-18deg); }
|
|
75% { transform: rotate(4deg) translateY(-4px); }
|
|
100% { transform: rotate(18deg); }
|
|
}
|
|
|
|
/* KNEE JOINT */
|
|
.knee {
|
|
width: 22px; height: 22px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 35% 28%, #8090b8, #384868);
|
|
box-shadow: 0 2px 7px rgba(0,0,100,0.5);
|
|
margin: -3px 0;
|
|
}
|
|
|
|
/* LOWER LEG */
|
|
.lower-leg {
|
|
width: 23px; height: 52px;
|
|
background: linear-gradient(to bottom, #505878, #303858);
|
|
border-radius: 12px;
|
|
position: relative;
|
|
transform-origin: top center;
|
|
box-shadow:
|
|
inset 0 2px 4px rgba(255,255,255,0.12),
|
|
0 3px 10px rgba(0,0,80,0.5);
|
|
}
|
|
.lower-leg.left { animation: lowerLegL 1s ease-in-out infinite; }
|
|
.lower-leg.right { animation: lowerLegR 1s ease-in-out infinite; }
|
|
|
|
/* Lower legs bend opposite to upper, giving a knee-flex effect */
|
|
@keyframes lowerLegL {
|
|
0%, 100% { transform: rotate(12deg); }
|
|
50% { transform: rotate(-18deg); }
|
|
}
|
|
@keyframes lowerLegR {
|
|
0%, 100% { transform: rotate(-12deg); }
|
|
50% { transform: rotate(18deg); }
|
|
}
|
|
|
|
/* FOOT */
|
|
.foot {
|
|
position: absolute;
|
|
bottom: -15px; left: -9px;
|
|
width: 41px; height: 21px;
|
|
background: linear-gradient(135deg, #6878a8, #384068);
|
|
border-radius: 8px 14px 16px 6px;
|
|
box-shadow: 0 7px 10px rgba(0,0,100,0.6), 0 0 12px rgba(0,80,255,0.12);
|
|
}
|
|
/* Toe ridge */
|
|
.foot::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 3px; right: 5px;
|
|
width: 55%; height: 4px;
|
|
background: rgba(100,130,210,0.35);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* ============================================================
|
|
DANCE STYLE VARIANTS (JS switches body data-dance attr)
|
|
============================================================ */
|
|
|
|
/* SPIN mode — whole robot rotates */
|
|
[data-dance="spin"] .robot {
|
|
animation: robotSpin 2s linear infinite !important;
|
|
}
|
|
@keyframes robotSpin {
|
|
from { transform: rotateY(0deg) translateY(-8px); }
|
|
to { transform: rotateY(360deg) translateY(-8px); }
|
|
}
|
|
|
|
/* WAVE mode — arms raise high repeatedly */
|
|
[data-dance="wave"] .arm.left {
|
|
animation: armWaveL 0.5s ease-in-out infinite !important;
|
|
}
|
|
[data-dance="wave"] .arm.right {
|
|
animation: armWaveR 0.5s ease-in-out infinite !important;
|
|
}
|
|
@keyframes armWaveL {
|
|
0%, 100% { transform: rotate(-80deg); }
|
|
50% { transform: rotate(-10deg); }
|
|
}
|
|
@keyframes armWaveR {
|
|
0%, 100% { transform: rotate(80deg); }
|
|
50% { transform: rotate(10deg); }
|
|
}
|
|
[data-dance="wave"] .robot {
|
|
animation: robotBounce 0.25s ease-in-out infinite alternate !important;
|
|
}
|
|
|
|
/* SHUFFLE mode — fast leg steps */
|
|
[data-dance="shuffle"] .upper-leg.left {
|
|
animation: upperLegL 0.25s ease-in-out infinite !important;
|
|
}
|
|
[data-dance="shuffle"] .upper-leg.right {
|
|
animation: upperLegR 0.25s ease-in-out infinite !important;
|
|
}
|
|
[data-dance="shuffle"] .lower-leg.left {
|
|
animation: lowerLegL 0.25s ease-in-out infinite !important;
|
|
}
|
|
[data-dance="shuffle"] .lower-leg.right {
|
|
animation: lowerLegR 0.25s ease-in-out infinite !important;
|
|
}
|
|
[data-dance="shuffle"] .arm.left {
|
|
animation: armSwingL 0.5s ease-in-out infinite !important;
|
|
}
|
|
[data-dance="shuffle"] .arm.right {
|
|
animation: armSwingR 0.5s ease-in-out infinite !important;
|
|
}
|
|
|
|
/* MOONWALK mode — body slides and leans */
|
|
[data-dance="moonwalk"] .robot {
|
|
animation: moonwalkLean 1s ease-in-out infinite alternate !important;
|
|
}
|
|
@keyframes moonwalkLean {
|
|
from { transform: translateX(-20px) rotate(-5deg); }
|
|
to { transform: translateX(20px) rotate(5deg); }
|
|
}
|
|
[data-dance="moonwalk"] .upper-leg.left {
|
|
animation: moonLegL 1s ease-in-out infinite !important;
|
|
}
|
|
[data-dance="moonwalk"] .upper-leg.right {
|
|
animation: moonLegR 1s ease-in-out infinite !important;
|
|
}
|
|
@keyframes moonLegL {
|
|
0%, 100% { transform: rotate(25deg); }
|
|
50% { transform: rotate(-5deg); }
|
|
}
|
|
@keyframes moonLegR {
|
|
0%, 100% { transform: rotate(-25deg); }
|
|
50% { transform: rotate(5deg); }
|
|
}
|
|
|
|
/* ROBOT SHADOW (ellipse on the floor) */
|
|
.robot-shadow {
|
|
position: absolute;
|
|
bottom: -28px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 130px; height: 22px;
|
|
background: radial-gradient(ellipse, rgba(0,100,255,0.38) 0%, transparent 68%);
|
|
border-radius: 50%;
|
|
animation: shadowAnim 0.5s ease-in-out infinite alternate;
|
|
pointer-events: none;
|
|
}
|
|
@keyframes shadowAnim {
|
|
from { transform: translateX(-50%) scaleX(0.75) scaleY(0.6); opacity: 0.35; }
|
|
to { transform: translateX(-50%) scaleX(1.3) scaleY(1.6); opacity: 0.85; }
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="scene">
|
|
|
|
<!-- Nebula blobs -->
|
|
<div class="nebula nebula-a"></div>
|
|
<div class="nebula nebula-b"></div>
|
|
<div class="nebula nebula-c"></div>
|
|
|
|
<!-- Twinkling starfield -->
|
|
<canvas id="starfield"></canvas>
|
|
|
|
<!-- Retrowave grid floor -->
|
|
<canvas id="floorCanvas"></canvas>
|
|
|
|
<!-- Horizon glow -->
|
|
<div class="horizon"></div>
|
|
|
|
<!-- ===================== ROBOT ===================== -->
|
|
<div class="robot-wrap">
|
|
<img src="skeleton6.apng" class="skeleton-gif" alt="skeleton">
|
|
<div class="robot-shadow"></div>
|
|
</div><!-- .robot-wrap -->
|
|
|
|
<!-- VU Meter -->
|
|
<div class="vu-meter" id="vuMeter"></div>
|
|
|
|
<!-- Music ticker -->
|
|
<div class="music-ticker">
|
|
<span class="music-icon">♫</span>
|
|
<span class="music-label">NOW PLAYING</span>
|
|
<div class="music-scroll-wrap">
|
|
<span class="music-text short" id="musicText">— —</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scanlines overlay -->
|
|
<div class="scanlines"></div>
|
|
|
|
</div><!-- .scene -->
|
|
|
|
<script>
|
|
/* ============================================================
|
|
CONSTANTS
|
|
120 BPM → beat = 500ms. All timings derived from this.
|
|
============================================================ */
|
|
const BPM = 120;
|
|
const BEAT_MS = 60000 / BPM; // 500ms
|
|
|
|
/* ============================================================
|
|
STARFIELD
|
|
============================================================ */
|
|
const canvas = document.getElementById('starfield');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
const STAR_COUNT = 220;
|
|
const stars = [];
|
|
|
|
function initStars() {
|
|
stars.length = 0;
|
|
for (let i = 0; i < STAR_COUNT; i++) {
|
|
stars.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height * 0.62,
|
|
r: Math.random() * 1.6 + 0.2,
|
|
phase: Math.random() * Math.PI * 2,
|
|
freq: 0.4 + Math.random() * 1.2,
|
|
color: Math.random() < 0.15 ? '#ffddaa' : (Math.random() < 0.5 ? '#ccd8ff' : '#eeeeff'),
|
|
});
|
|
}
|
|
}
|
|
|
|
function resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
initStars();
|
|
}
|
|
|
|
function drawStars(ts) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
const t = ts * 0.001;
|
|
stars.forEach(s => {
|
|
const alpha = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase));
|
|
ctx.beginPath();
|
|
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
|
ctx.fillStyle = s.color.replace(')', `, ${alpha.toFixed(2)})`)
|
|
.replace('rgb', 'rgba').replace('#ccd8ff', `rgba(204,216,255,${alpha.toFixed(2)})`)
|
|
.replace('#eeeeff', `rgba(238,238,255,${alpha.toFixed(2)})`)
|
|
.replace('#ffddaa', `rgba(255,221,170,${alpha.toFixed(2)})`);
|
|
ctx.fill();
|
|
});
|
|
requestAnimationFrame(drawStars);
|
|
}
|
|
|
|
window.addEventListener('resize', resizeCanvas);
|
|
resizeCanvas();
|
|
requestAnimationFrame(drawStars);
|
|
|
|
/* ============================================================
|
|
FLOOR CANVAS — perspective grid with true vanishing point
|
|
============================================================ */
|
|
const floorCanvas = document.getElementById('floorCanvas');
|
|
const floorCtx = floorCanvas.getContext('2d');
|
|
|
|
const FLOOR_RATIO = 0.42;
|
|
const V_LINES = 24;
|
|
const H_LINES = 24;
|
|
let floorScroll = 0;
|
|
let lastFloorTs = 0;
|
|
|
|
function resizeFloor() {
|
|
floorCanvas.width = window.innerWidth;
|
|
floorCanvas.height = Math.round(window.innerHeight * FLOOR_RATIO);
|
|
}
|
|
|
|
function drawFloor(ts) {
|
|
const dt = lastFloorTs ? Math.min((ts - lastFloorTs) / 1000, 0.1) : 0;
|
|
lastFloorTs = ts;
|
|
floorScroll = (floorScroll + 0.06 * dt) % 1;
|
|
|
|
const W = floorCanvas.width;
|
|
const H = floorCanvas.height;
|
|
const vx = W / 2;
|
|
|
|
floorCtx.clearRect(0, 0, W, H);
|
|
|
|
// Vertical lines radiating from vanishing point (center top)
|
|
// Bottom spread exceeds screen width so laterals are filled
|
|
const spread = W * 4;
|
|
const spreadStart = vx - spread / 2;
|
|
floorCtx.strokeStyle = 'rgba(0, 200, 255, 0.32)';
|
|
floorCtx.lineWidth = 2;
|
|
for (let i = 0; i <= V_LINES; i++) {
|
|
const bx = spreadStart + (i / V_LINES) * spread;
|
|
floorCtx.beginPath();
|
|
floorCtx.moveTo(vx, 0);
|
|
floorCtx.lineTo(bx, H);
|
|
floorCtx.stroke();
|
|
}
|
|
|
|
// Horizontal lines — perspective-correct spacing, animated
|
|
for (let i = 0; i < H_LINES; i++) {
|
|
const t = ((i / H_LINES) + floorScroll) % 1;
|
|
const depth = Math.pow(t, 2.2); // power curve: compressed near horizon
|
|
const y = depth * H;
|
|
const x1 = vx - (spread / 2) * depth;
|
|
const x2 = vx + (spread / 2) * depth;
|
|
floorCtx.strokeStyle = `rgba(0, 200, 255, ${(0.12 + depth * 0.25).toFixed(2)})`;
|
|
floorCtx.lineWidth = Math.max(1, depth * 11);
|
|
floorCtx.beginPath();
|
|
floorCtx.moveTo(x1, y);
|
|
floorCtx.lineTo(x2, y);
|
|
floorCtx.stroke();
|
|
}
|
|
|
|
// Fade near horizon to hide sub-pixel lines
|
|
const fade = floorCtx.createLinearGradient(0, 0, 0, H * 0.3);
|
|
fade.addColorStop(0, '#000814');
|
|
fade.addColorStop(1, 'rgba(0,8,20,0)');
|
|
floorCtx.fillStyle = fade;
|
|
floorCtx.fillRect(0, 0, W, H * 0.3);
|
|
|
|
requestAnimationFrame(drawFloor);
|
|
}
|
|
|
|
window.addEventListener('resize', resizeFloor);
|
|
resizeFloor();
|
|
requestAnimationFrame(drawFloor);
|
|
|
|
|
|
/* ============================================================
|
|
CHEST EQ BARS (JS-animated for smooth randomness)
|
|
============================================================ */
|
|
const EQ_COLS = 7;
|
|
const EQ_COLORS_T = ['#00ffcc','#00eebb','#00ddff','#0099ff','#0055ff','#00ddff','#00ffcc'];
|
|
const EQ_COLORS_B = ['#ff44aa','#ff55bb','#ff66cc','#ff44aa','#ff66cc','#ff55bb','#ff44aa'];
|
|
|
|
function buildEqRow(container, colors) {
|
|
const cols = [];
|
|
for (let i = 0; i < EQ_COLS; i++) {
|
|
const d = document.createElement('div');
|
|
d.className = 'eq-col';
|
|
d.style.background = colors[i];
|
|
d.style.height = '50%';
|
|
d._phase = Math.random() * Math.PI * 2;
|
|
d._speed = 3 + Math.random() * 5;
|
|
container.appendChild(d);
|
|
cols.push(d);
|
|
}
|
|
return cols;
|
|
}
|
|
|
|
const eqTopCols = buildEqRow(document.getElementById('eqTop'), EQ_COLORS_T);
|
|
const eqBotCols = buildEqRow(document.getElementById('eqBot'), EQ_COLORS_B);
|
|
|
|
function animateEQ(ts) {
|
|
const t = ts * 0.001;
|
|
[...eqTopCols, ...eqBotCols].forEach(col => {
|
|
const pct = 15 + 85 * (0.5 + 0.5 * Math.sin(t * col._speed + col._phase));
|
|
col.style.height = pct + '%';
|
|
col.style.opacity = 0.5 + 0.5 * (pct / 100);
|
|
});
|
|
requestAnimationFrame(animateEQ);
|
|
}
|
|
requestAnimationFrame(animateEQ);
|
|
|
|
/* ============================================================
|
|
VU METER (bottom of screen)
|
|
============================================================ */
|
|
const VU_COUNT = 38;
|
|
const vuMeter = document.getElementById('vuMeter');
|
|
const vuBars = [];
|
|
|
|
const VU_PALETTE = [
|
|
'#00ffcc','#00eebb','#00ddaa','#00cc99','#00bb88',
|
|
'#00aaff','#0099ee','#0088dd','#0077cc','#0066bb',
|
|
'#0099ff','#0088ee','#0077dd',
|
|
'#ff44aa','#ff55bb','#ff66cc','#ff77dd',
|
|
'#ff66cc','#ff55bb','#ff44aa',
|
|
'#0077dd','#0088ee','#0099ff',
|
|
'#0066bb','#0077cc','#0088dd','#0099ee','#00aaff',
|
|
'#00bb88','#00cc99','#00ddaa','#00eebb','#00ffcc',
|
|
'#00eebb','#00ddaa','#00cc99','#00bb88',
|
|
];
|
|
|
|
for (let i = 0; i < VU_COUNT; i++) {
|
|
const bar = document.createElement('div');
|
|
bar.className = 'vu-bar';
|
|
bar.style.background = VU_PALETTE[i % VU_PALETTE.length];
|
|
bar.style.boxShadow = `0 0 4px ${VU_PALETTE[i % VU_PALETTE.length]}`;
|
|
bar._maxH = 15 + Math.random() * 55;
|
|
bar._phase = Math.random() * Math.PI * 2;
|
|
bar._speed = 1.5 + Math.random() * 4;
|
|
vuMeter.appendChild(bar);
|
|
vuBars.push(bar);
|
|
}
|
|
|
|
function animateVU(ts) {
|
|
const t = ts * 0.001;
|
|
vuBars.forEach(bar => {
|
|
const h = bar._maxH * Math.abs(Math.sin(t * bar._speed + bar._phase));
|
|
bar.style.height = Math.max(3, h) + 'px';
|
|
bar.style.opacity = 0.55 + 0.45 * (h / bar._maxH);
|
|
});
|
|
requestAnimationFrame(animateVU);
|
|
}
|
|
requestAnimationFrame(animateVU);
|
|
|
|
/* ============================================================
|
|
FLOATING MUSIC NOTES
|
|
============================================================ */
|
|
const NOTES = ['♪','♫','♩','♬','♭','♮'];
|
|
const N_CLR = ['#ff44aa','#ff88cc','#cc44ff','#ff66bb','#aa44ff','#ff55dd'];
|
|
const scene = document.querySelector('.scene');
|
|
|
|
function spawnNote() {
|
|
const el = document.createElement('div');
|
|
el.className = 'music-note';
|
|
el.textContent = NOTES[Math.floor(Math.random() * NOTES.length)];
|
|
const clr = N_CLR[Math.floor(Math.random() * N_CLR.length)];
|
|
el.style.color = clr;
|
|
el.style.textShadow = `0 0 12px ${clr}, 0 0 24px ${clr}`;
|
|
el.style.fontSize = (18 + Math.random() * 18) + 'px';
|
|
/* Scatter notes around the robot */
|
|
el.style.left = (42 + Math.random() * 16) + '%';
|
|
el.style.bottom = (42 + Math.random() * 6) + '%';
|
|
const dur = 1.6 + Math.random() * 1.2;
|
|
el.style.animationDuration = dur + 's';
|
|
scene.appendChild(el);
|
|
setTimeout(() => el.remove(), dur * 1000 + 100);
|
|
}
|
|
|
|
/* Spawn on every beat (500ms) with a 50 % chance of two notes */
|
|
setInterval(() => {
|
|
spawnNote();
|
|
if (Math.random() < 0.5) spawnNote();
|
|
}, BEAT_MS);
|
|
|
|
/* First note immediately */
|
|
spawnNote();
|
|
|
|
/* ============================================================
|
|
MUSIC INFO (called by QML via runJavaScript)
|
|
============================================================ */
|
|
function updateMusic(artist, title) {
|
|
const el = document.getElementById('musicText');
|
|
const text = (artist && title) ? `${artist} — ${title}`
|
|
: (title || artist || '— —');
|
|
if (el.textContent === text) return;
|
|
el.textContent = text;
|
|
pickSkeleton();
|
|
// scroll only if text is long
|
|
const wrap = el.parentElement;
|
|
el.classList.toggle('short', el.scrollWidth <= wrap.clientWidth + 10);
|
|
}
|
|
|
|
/* ============================================================
|
|
DANCE STYLE MACHINE
|
|
Cycles: bounce → wave → shuffle → moonwalk → spin → repeat
|
|
Each style lasts 8s (= 16 beats at 120 BPM)
|
|
============================================================ */
|
|
const DANCE_STYLES = ['bounce', 'wave', 'shuffle', 'moonwalk', 'spin'];
|
|
const STYLE_LABELS = ['BOUNCE', 'WAVE', 'SHUFFLE', 'MOONWALK', 'SPIN'];
|
|
let danceIdx = 0;
|
|
|
|
function nextDanceStyle() {
|
|
danceIdx = (danceIdx + 1) % DANCE_STYLES.length;
|
|
document.body.setAttribute('data-dance', DANCE_STYLES[danceIdx]);
|
|
}
|
|
|
|
// Start on default bounce
|
|
document.body.setAttribute('data-dance', 'bounce');
|
|
setInterval(nextDanceStyle, 8000);
|
|
|
|
/* ============================================================
|
|
SKELETON SWITCHER
|
|
Add new files to SKELETONS to include them in the rotation.
|
|
Switches on music change and randomly every 16 beats (8s).
|
|
============================================================ */
|
|
const SKELETONS = ['skeleton.apng', 'skeleton2.apng', 'skeleton3.apng', 'skeleton4.apng', 'skeleton5.apng', 'skeleton6.apng'];
|
|
const skeletonImg = document.querySelector('.skeleton-gif');
|
|
let currentSkel = 5;
|
|
|
|
function pickSkeleton() {
|
|
if (SKELETONS.length < 2) return;
|
|
let next;
|
|
do { next = Math.floor(Math.random() * SKELETONS.length); }
|
|
while (next === currentSkel);
|
|
currentSkel = next;
|
|
skeletonImg.src = SKELETONS[currentSkel];
|
|
}
|
|
|
|
setInterval(pickSkeleton, 8000);
|
|
|
|
/* ============================================================
|
|
DEV SHIM — only active when served via HTTP (not from QML)
|
|
Simulates playerctl updates so the music ticker works in-browser.
|
|
============================================================ */
|
|
if (window.location.protocol !== 'file:' && !window.qt) {
|
|
const devTracks = [
|
|
['Daft Punk', 'Get Lucky'],
|
|
['Kavinsky', 'Nightcall'],
|
|
['Justice', 'D.A.N.C.E.'],
|
|
['M83', 'Midnight City'],
|
|
['Chromatics', 'Shadow'],
|
|
];
|
|
let devIdx = 0;
|
|
function devTick() {
|
|
const [artist, title] = devTracks[devIdx % devTracks.length];
|
|
updateMusic(artist, title);
|
|
devIdx++;
|
|
}
|
|
devTick();
|
|
setInterval(devTick, 5000);
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|