Spaces:
Running
Running
a dancing cartoon
Browse files- README.md +8 -5
- components/groove-character.js +290 -0
- components/groove-controls.js +404 -0
- components/groove-footer.js +162 -0
- components/groove-header.js +140 -0
- index.html +97 -19
- script.js +443 -0
- style.css +182 -19
README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: GrooveToon Interactive 🎭
|
| 3 |
+
colorFrom: gray
|
| 4 |
+
colorTo: yellow
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
components/groove-character.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GrooveToon Character Component - Animated SVG Character
|
| 2 |
+
class GrooveCharacter extends HTMLElement {
|
| 3 |
+
constructor() {
|
| 4 |
+
super();
|
| 5 |
+
this.mood = 'happy';
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
connectedCallback() {
|
| 9 |
+
this.attachShadow({ mode: 'open' });
|
| 10 |
+
this.render();
|
| 11 |
+
this.setupInteractions();
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
render() {
|
| 15 |
+
this.shadowRoot.innerHTML = `
|
| 16 |
+
<style>
|
| 17 |
+
:host {
|
| 18 |
+
display: block;
|
| 19 |
+
width: 320px;
|
| 20 |
+
height: 420px;
|
| 21 |
+
position: relative;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.character-container {
|
| 25 |
+
width: 100%;
|
| 26 |
+
height: 100%;
|
| 27 |
+
position: relative;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* Body Parts with Animation Support */
|
| 31 |
+
.char-svg {
|
| 32 |
+
width: 100%;
|
| 33 |
+
height: 100%;
|
| 34 |
+
filter: drop-shadow(0 20px 40px rgba(244, 63, 94, 0.3));
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#char-body {
|
| 38 |
+
transform-origin: center bottom;
|
| 39 |
+
transition: filter 0.1s ease;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
#char-head {
|
| 43 |
+
transform-origin: center bottom;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
#char-arm-left, #char-arm-right {
|
| 47 |
+
transform-origin: top center;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
#char-leg-left, #char-leg-right {
|
| 51 |
+
transform-origin: top center;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* Mood-Based Face Styles */
|
| 55 |
+
.face-happy .eye { ry: 8; }
|
| 56 |
+
.face-happy .mouth {
|
| 57 |
+
d: path("M 95 130 Q 120 150 145 130");
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.face-excited .eye {
|
| 61 |
+
ry: 12;
|
| 62 |
+
rx: 10;
|
| 63 |
+
}
|
| 64 |
+
.face-excited .mouth {
|
| 65 |
+
d: path("M 100 125 Q 120 160 140 125 Z");
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.face-chill .eye { ry: 4; }
|
| 69 |
+
.face-chill .mouth {
|
| 70 |
+
d: path("M 105 135 Q 120 140 135 135");
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.face-hype .eye {
|
| 74 |
+
ry: 6;
|
| 75 |
+
rx: 12;
|
| 76 |
+
}
|
| 77 |
+
.face-hype .mouth {
|
| 78 |
+
d: path("M 90 120 Q 120 170 150 120");
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Gradient Definitions */
|
| 82 |
+
.grad-primary { fill: url(#gradPrimary); }
|
| 83 |
+
.grad-secondary { fill: url(#gradSecondary); }
|
| 84 |
+
.grad-accent { fill: url(#gradAccent); }
|
| 85 |
+
|
| 86 |
+
/* Interactive Hover States */
|
| 87 |
+
.character-container:hover #char-body {
|
| 88 |
+
filter: brightness(1.1);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/* Glow Effects */
|
| 92 |
+
.glow-layer {
|
| 93 |
+
position: absolute;
|
| 94 |
+
inset: -20px;
|
| 95 |
+
background: radial-gradient(ellipse at center, rgba(244, 63, 94, 0.2) 0%, transparent 70%);
|
| 96 |
+
pointer-events: none;
|
| 97 |
+
opacity: 0.5;
|
| 98 |
+
animation: glow-pulse 2s ease-in-out infinite;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
@keyframes glow-pulse {
|
| 102 |
+
0%, 100% { opacity: 0.3; transform: scale(1); }
|
| 103 |
+
50% { opacity: 0.6; transform: scale(1.02); }
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* Floating particles around character */
|
| 107 |
+
.orbit-particle {
|
| 108 |
+
position: absolute;
|
| 109 |
+
width: 12px;
|
| 110 |
+
height: 12px;
|
| 111 |
+
border-radius: 50%;
|
| 112 |
+
background: linear-gradient(135deg, #f43f5e, #8b5cf6);
|
| 113 |
+
animation: orbit 4s linear infinite;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.orbit-particle:nth-child(2) {
|
| 117 |
+
animation-delay: -1.5s;
|
| 118 |
+
width: 8px;
|
| 119 |
+
height: 8px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.orbit-particle:nth-child(3) {
|
| 123 |
+
animation-delay: -3s;
|
| 124 |
+
width: 6px;
|
| 125 |
+
height: 6px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
@keyframes orbit {
|
| 129 |
+
0% { transform: rotate(0deg) translateX(180px) rotate(0deg); }
|
| 130 |
+
100% { transform: rotate(360deg) translateX(180px) rotate(-360deg); }
|
| 131 |
+
}
|
| 132 |
+
</style>
|
| 133 |
+
|
| 134 |
+
<div class="character-container">
|
| 135 |
+
<!-- Orbiting Particles -->
|
| 136 |
+
<div class="orbit-particle" style="left: 50%; top: 50%;"></div>
|
| 137 |
+
<div class="orbit-particle" style="left: 50%; top: 50%;"></div>
|
| 138 |
+
<div class="orbit-particle" style="left: 50%; top: 50%;"></div>
|
| 139 |
+
|
| 140 |
+
<!-- Glow Background -->
|
| 141 |
+
<div class="glow-layer"></div>
|
| 142 |
+
|
| 143 |
+
<!-- Main Character SVG -->
|
| 144 |
+
<svg class="char-svg" viewBox="0 0 240 320" xmlns="http://www.w3.org/2000/svg">
|
| 145 |
+
<defs>
|
| 146 |
+
<!-- Dynamic Gradients -->
|
| 147 |
+
<linearGradient id="gradPrimary" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 148 |
+
<stop offset="0%" stop-color="#f43f5e" />
|
| 149 |
+
<stop offset="50%" stop-color="#e11d48" />
|
| 150 |
+
<stop offset="100%" stop-color="#be123c" />
|
| 151 |
+
</linearGradient>
|
| 152 |
+
<linearGradient id="gradSecondary" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 153 |
+
<stop offset="0%" stop-color="#8b5cf6" />
|
| 154 |
+
<stop offset="50%" stop-color="#7c3aed" />
|
| 155 |
+
<stop offset="100%" stop-color="#6d28d9" />
|
| 156 |
+
</linearGradient>
|
| 157 |
+
<radialGradient id="gradAccent" cx="50%" cy="50%" r="50%">
|
| 158 |
+
<stop offset="0%" stop-color="#fb7185" />
|
| 159 |
+
<stop offset="100%" stop-color="#f43f5e" />
|
| 160 |
+
</radialGradient>
|
| 161 |
+
<filter id="glow"><feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
| 162 |
+
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
| 163 |
+
</filter>
|
| 164 |
+
</defs>
|
| 165 |
+
|
| 166 |
+
<!-- Shadow -->
|
| 167 |
+
<ellipse cx="120" cy="305" rx="60" ry="15" fill="rgba(0,0,0,0.3)" filter="url(#glow)"/>
|
| 168 |
+
|
| 169 |
+
<!-- Left Arm (Behind Body) -->
|
| 170 |
+
<g id="char-arm-left">
|
| 171 |
+
<path d="M 75 140 Q 45 180 40 230" stroke="url(#gradSecondary)" stroke-width="20" fill="none" stroke-linecap="round"/>
|
| 172 |
+
<circle cx="40" cy="235" r="15" fill="url(#gradAccent)"/>
|
| 173 |
+
</g>
|
| 174 |
+
|
| 175 |
+
<!-- Right Arm (Behind Body) -->
|
| 176 |
+
<g id="char-arm-right">
|
| 177 |
+
<path d="M 165 140 Q 195 180 200 230" stroke="url(#gradSecondary)" stroke-width="20" fill="none" stroke-linecap="round"/>
|
| 178 |
+
<circle cx="200" cy="235" r="15" fill="url(#gradAccent)"/>
|
| 179 |
+
</g>
|
| 180 |
+
|
| 181 |
+
<!-- Left Leg -->
|
| 182 |
+
<g id="char-leg-left">
|
| 183 |
+
<path d="M 95 220 L 90 280" stroke="url(#gradSecondary)" stroke-width="18" fill="none" stroke-linecap="round"/>
|
| 184 |
+
<ellipse cx="90" cy="285" rx="14" ry="10" fill="#fb7185"/>
|
| 185 |
+
</g>
|
| 186 |
+
|
| 187 |
+
<!-- Right Leg -->
|
| 188 |
+
<g id="char-leg-right">
|
| 189 |
+
<path d="M 145 220 L 150 280" stroke="url(#gradSecondary)" stroke-width="18" fill="none" stroke-linecap="round"/>
|
| 190 |
+
<ellipse cx="150" cy="285" rx="14" ry="10" fill="#fb7185"/>
|
| 191 |
+
</g>
|
| 192 |
+
|
| 193 |
+
<!-- Body -->
|
| 194 |
+
<g id="char-body">
|
| 195 |
+
<!-- Main Body Shape -->
|
| 196 |
+
<ellipse cx="120" cy="190" rx="55" ry="65" fill="url(#gradPrimary)"/>
|
| 197 |
+
|
| 198 |
+
<!-- Body Details -->
|
| 199 |
+
<circle cx="120" cy="170" r="25" fill="rgba(255,255,255,0.15)"/>
|
| 200 |
+
<ellipse cx="120" cy="210" rx="30" ry="25" fill="rgba(255,255,255,0.1)"/>
|
| 201 |
+
|
| 202 |
+
<!-- Music Note Decoration -->
|
| 203 |
+
<g transform="translate(140, 160) rotate(15)">
|
| 204 |
+
<text x="0" y="0" fill="rgba(255,255,255,0.4)" font-size="30" font-family="Fredoka One">♪</text>
|
| 205 |
+
</g>
|
| 206 |
+
</g>
|
| 207 |
+
|
| 208 |
+
<!-- Head -->
|
| 209 |
+
<g id="char-head">
|
| 210 |
+
<!-- Head Base -->
|
| 211 |
+
<circle cx="120" cy="85" r="60" fill="url(#gradPrimary)"/>
|
| 212 |
+
|
| 213 |
+
<!-- Headphones -->
|
| 214 |
+
<g id="headphones">
|
| 215 |
+
<path d="M 55 85 Q 55 20 120 20 Q 185 20 185 85" stroke="url(#gradSecondary)" stroke-width="12" fill="none"/>
|
| 216 |
+
<rect x="45" y="65" width="20" height="40" rx="10" fill="url(#gradSecondary)"/>
|
| 217 |
+
<rect x="175" y="65" width="20" height="40" rx="10" fill="url(#gradSecondary)"/>
|
| 218 |
+
<rect x="50" y="70" width="10" height="30" rx="5" fill="#a78bfa"/>
|
| 219 |
+
<rect x="180" y="70" width="10" height="30" rx="5" fill="#a78bfa"/>
|
| 220 |
+
</g>
|
| 221 |
+
|
| 222 |
+
<!-- Face Group -->
|
| 223 |
+
<g id="char-face" class="face-happy" data-mood="happy">
|
| 224 |
+
<!-- Eyes -->
|
| 225 |
+
<ellipse class="eye left" cx="95" cy="90" rx="10" ry="8" fill="white"/>
|
| 226 |
+
<circle cx="95" cy="90" r="5" fill="#1a1a2e"/>
|
| 227 |
+
<ellipse class="eye right" cx="145" cy="90" rx="10" ry="8" fill="white"/>
|
| 228 |
+
<circle cx="145" cy="90" r="5" fill="#1a1a2e"/>
|
| 229 |
+
|
| 230 |
+
<!-- Cheeks -->
|
| 231 |
+
<ellipse cx="85" cy="105" rx="8" ry="5" fill="rgba(251,113,133,0.5)"/>
|
| 232 |
+
<ellipse cx="155" cy="105" rx="8" ry="5" fill="rgba(251,113,133,0.5)"/>
|
| 233 |
+
|
| 234 |
+
<!-- Mouth (dynamic path) -->
|
| 235 |
+
<path class="mouth" d="M 95 110 Q 120 135 145 110" stroke="#1a1a2e" stroke-width="4" fill="none" stroke-linecap="round"/>
|
| 236 |
+
</g>
|
| 237 |
+
|
| 238 |
+
<!-- Antenna -->
|
| 239 |
+
<line x1="120" y1="25" x2="120" y2="5" stroke="url(#gradSecondary)" stroke-width="4"/>
|
| 240 |
+
<circle cx="120" cy="5" r="8" fill="url(#gradAccent)">
|
| 241 |
+
<animate attributeName="opacity" values="1;0.5;1" dur="0.5s" repeatCount="indefinite"/>
|
| 242 |
+
</circle>
|
| 243 |
+
</g>
|
| 244 |
+
</svg>
|
| 245 |
+
</div>
|
| 246 |
+
`;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
setupInteractions() {
|
| 250 |
+
// Click to change mood
|
| 251 |
+
this.shadowRoot.querySelector('.character-container').addEventListener('click', () => {
|
| 252 |
+
const moods = ['happy', 'excited', 'chill', 'hype'];
|
| 253 |
+
const currentIndex = moods.indexOf(this.mood);
|
| 254 |
+
this.mood = moods[(currentIndex + 1) % moods.length];
|
| 255 |
+
|
| 256 |
+
const face = this.shadowRoot.getElementById('char-face');
|
| 257 |
+
face.setAttribute('class', `face-${this.mood}`);
|
| 258 |
+
face.setAttribute('data-mood', this.mood);
|
| 259 |
+
|
| 260 |
+
// Create burst effect
|
| 261 |
+
const rect = this.getBoundingClientRect();
|
| 262 |
+
document.dispatchEvent(new CustomEvent('character-click', {
|
| 263 |
+
detail: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
| 264 |
+
}));
|
| 265 |
+
});
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
// Public methods for external control
|
| 269 |
+
setMood(mood) {
|
| 270 |
+
this.mood = mood;
|
| 271 |
+
const face = this.shadowRoot?.getElementById('char-face');
|
| 272 |
+
if (face) {
|
| 273 |
+
face.setAttribute('class', `face-${mood}`);
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
pulse() {
|
| 278 |
+
const body = this.shadowRoot?.getElementById('char-body');
|
| 279 |
+
if (body) {
|
| 280 |
+
body.style.filter = 'brightness(1.4) drop-shadow(0 0 20px #f43f5e)';
|
| 281 |
+
requestAnimationFrame(() => {
|
| 282 |
+
setTimeout(() => {
|
| 283 |
+
body.style.filter = '';
|
| 284 |
+
}, 100);
|
| 285 |
+
});
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
customElements.define('groove-character', GrooveCharacter);
|
components/groove-controls.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GrooveToon Controls Component
|
| 2 |
+
class GrooveControls extends HTMLElement {
|
| 3 |
+
constructor() {
|
| 4 |
+
super();
|
| 5 |
+
this.isPlaying = false;
|
| 6 |
+
this.currentDance = 'bounce';
|
| 7 |
+
this.tempo = 120;
|
| 8 |
+
this.primaryColor = '#f43f5e';
|
| 9 |
+
this.secondaryColor = '#8b5cf6';
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
connectedCallback() {
|
| 13 |
+
this.attachShadow({ mode: 'open' });
|
| 14 |
+
this.render();
|
| 15 |
+
this.setupEventListeners();
|
| 16 |
+
this.setupAudioVisualization();
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
render() {
|
| 20 |
+
this.shadowRoot.innerHTML = `
|
| 21 |
+
<style>
|
| 22 |
+
:host {
|
| 23 |
+
display: block;
|
| 24 |
+
width: 100%;
|
| 25 |
+
max-width: 800px;
|
| 26 |
+
margin: 0 auto;
|
| 27 |
+
padding: 1.5rem;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.controls-panel {
|
| 31 |
+
background: rgba(0, 0, 0, 0.4);
|
| 32 |
+
backdrop-filter: blur(20px);
|
| 33 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 34 |
+
border-radius: 24px;
|
| 35 |
+
padding: 1.5rem;
|
| 36 |
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.main-controls {
|
| 40 |
+
display: flex;
|
| 41 |
+
gap: 1rem;
|
| 42 |
+
align-items: center;
|
| 43 |
+
justify-content: center;
|
| 44 |
+
margin-bottom: 1.5rem;
|
| 45 |
+
flex-wrap: wrap;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.play-btn {
|
| 49 |
+
width: 72px;
|
| 50 |
+
height: 72px;
|
| 51 |
+
border-radius: 50%;
|
| 52 |
+
border: none;
|
| 53 |
+
background: linear-gradient(135deg, #f43f5e, #e11d48);
|
| 54 |
+
cursor: pointer;
|
| 55 |
+
display: flex;
|
| 56 |
+
align-items: center;
|
| 57 |
+
justify-content: center;
|
| 58 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 59 |
+
box-shadow: 0 10px 40px rgba(244, 63, 94, 0.4);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.play-btn:hover {
|
| 63 |
+
transform: scale(1.05);
|
| 64 |
+
box-shadow: 0 15px 50px rgba(244, 63, 94, 0.5);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.play-btn.playing {
|
| 68 |
+
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
| 69 |
+
box-shadow: 0 10px 40px rgba(139, 92, 246, 0.4);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.play-btn svg {
|
| 73 |
+
width: 32px;
|
| 74 |
+
height: 32px;
|
| 75 |
+
color: white;
|
| 76 |
+
margin-left: 4px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.play-btn.playing svg {
|
| 80 |
+
margin-left: 0;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.dance-grid {
|
| 84 |
+
display: grid;
|
| 85 |
+
grid-template-columns: repeat(4, 1fr);
|
| 86 |
+
gap: 0.75rem;
|
| 87 |
+
margin-bottom: 1.5rem;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.dance-btn {
|
| 91 |
+
padding: 0.875rem 1rem;
|
| 92 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 93 |
+
border-radius: 12px;
|
| 94 |
+
background: rgba(255, 255, 255, 0.05);
|
| 95 |
+
color: rgba(255, 255, 255, 0.8);
|
| 96 |
+
font-weight: 600;
|
| 97 |
+
font-size: 0.875rem;
|
| 98 |
+
cursor: pointer;
|
| 99 |
+
transition: all 0.2s ease;
|
| 100 |
+
display: flex;
|
| 101 |
+
flex-direction: column;
|
| 102 |
+
align-items: center;
|
| 103 |
+
gap: 0.25rem;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.dance-btn:hover {
|
| 107 |
+
background: rgba(255, 255, 255, 0.1);
|
| 108 |
+
transform: translateY(-2px);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.dance-btn.active {
|
| 112 |
+
background: linear-gradient(135deg, rgba(244, 63, 94, 0.2), rgba(139, 92, 246, 0.2));
|
| 113 |
+
border-color: #f43f5e;
|
| 114 |
+
color: white;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.dance-btn svg {
|
| 118 |
+
width: 20px;
|
| 119 |
+
height: 20px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.sliders-section {
|
| 123 |
+
display: grid;
|
| 124 |
+
grid-template-columns: 1fr 1fr;
|
| 125 |
+
gap: 1.5rem;
|
| 126 |
+
margin-bottom: 1.5rem;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@media (max-width: 640px) {
|
| 130 |
+
.sliders-section {
|
| 131 |
+
grid-template-columns: 1fr;
|
| 132 |
+
}
|
| 133 |
+
.dance-grid {
|
| 134 |
+
grid-template-columns: repeat(2, 1fr);
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.slider-group {
|
| 139 |
+
display: flex;
|
| 140 |
+
flex-direction: column;
|
| 141 |
+
gap: 0.5rem;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.slider-label {
|
| 145 |
+
display: flex;
|
| 146 |
+
justify-content: space-between;
|
| 147 |
+
align-items: center;
|
| 148 |
+
color: rgba(255, 255, 255, 0.7);
|
| 149 |
+
font-size: 0.875rem;
|
| 150 |
+
font-weight: 600;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.slider-value {
|
| 154 |
+
color: #f43f5e;
|
| 155 |
+
font-weight: 700;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
input[type="range"] {
|
| 159 |
+
-webkit-appearance: none;
|
| 160 |
+
width: 100%;
|
| 161 |
+
height: 6px;
|
| 162 |
+
border-radius: 3px;
|
| 163 |
+
background: rgba(255, 255, 255, 0.1);
|
| 164 |
+
outline: none;
|
| 165 |
+
cursor: pointer;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 169 |
+
-webkit-appearance: none;
|
| 170 |
+
width: 20px;
|
| 171 |
+
height: 20px;
|
| 172 |
+
border-radius: 50%;
|
| 173 |
+
background: linear-gradient(135deg, #f43f5e, #8b5cf6);
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
box-shadow: 0 0 10px rgba(244, 63, 94, 0.5);
|
| 176 |
+
transition: transform 0.2s;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
input[type="range"]::-webkit-slider-thumb:hover {
|
| 180 |
+
transform: scale(1.1);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.visualizer {
|
| 184 |
+
height: 60px;
|
| 185 |
+
display: flex;
|
| 186 |
+
align-items: flex-end;
|
| 187 |
+
justify-content: center;
|
| 188 |
+
gap: 4px;
|
| 189 |
+
padding: 1rem;
|
| 190 |
+
background: rgba(0, 0, 0, 0.3);
|
| 191 |
+
border-radius: 12px;
|
| 192 |
+
margin-bottom: 1.5rem;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.viz-bar {
|
| 196 |
+
width: 8px;
|
| 197 |
+
background: linear-gradient(to top, #f43f5e, #8b5cf6);
|
| 198 |
+
border-radius: 4px;
|
| 199 |
+
transition: height 0.1s ease;
|
| 200 |
+
min-height: 4px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.color-controls {
|
| 204 |
+
display: flex;
|
| 205 |
+
gap: 1rem;
|
| 206 |
+
justify-content: center;
|
| 207 |
+
flex-wrap: wrap;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.color-group {
|
| 211 |
+
display: flex;
|
| 212 |
+
align-items: center;
|
| 213 |
+
gap: 0.75rem;
|
| 214 |
+
padding: 0.75rem 1rem;
|
| 215 |
+
background: rgba(255, 255, 255, 0.05);
|
| 216 |
+
border-radius: 12px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.color-label {
|
| 220 |
+
font-size: 0.875rem;
|
| 221 |
+
color: rgba(255, 255, 255, 0.7);
|
| 222 |
+
font-weight: 600;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
input[type="color"] {
|
| 226 |
+
width: 48px;
|
| 227 |
+
height: 48px;
|
| 228 |
+
border: none;
|
| 229 |
+
border-radius: 12px;
|
| 230 |
+
cursor: pointer;
|
| 231 |
+
background: transparent;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
input[type="color"]::-webkit-color-swatch-wrapper {
|
| 235 |
+
padding: 0;
|
| 236 |
+
border-radius: 12px;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
input[type="color"]::-webkit-color-swatch {
|
| 240 |
+
border: 2px solid rgba(255, 255, 255, 0.2);
|
| 241 |
+
border-radius: 12px;
|
| 242 |
+
}
|
| 243 |
+
</style>
|
| 244 |
+
|
| 245 |
+
<div class="controls-panel">
|
| 246 |
+
<!-- Audio Visualizer -->
|
| 247 |
+
<div class="visualizer" id="visualizer">
|
| 248 |
+
${Array(16).fill(0).map((_, i) => `<div class="viz-bar" style="height: ${4 + Math.random() * 20}px"></div>`).join('')}
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<!-- Main Play Control -->
|
| 252 |
+
<div class="main-controls">
|
| 253 |
+
<button class="play-btn ${this.isPlaying ? 'playing' : ''}" id="playBtn">
|
| 254 |
+
${this.isPlaying ?
|
| 255 |
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>' :
|
| 256 |
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>'
|
| 257 |
+
}
|
| 258 |
+
</button>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<!-- Dance Style Selector -->
|
| 262 |
+
<div class="dance-grid">
|
| 263 |
+
<button class="dance-btn ${this.currentDance === 'bounce' ? 'active' : ''}" data-dance="bounce">
|
| 264 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
| 265 |
+
Bounce
|
| 266 |
+
</button>
|
| 267 |
+
<button class="dance-btn ${this.currentDance === 'sway' ? 'active' : ''}" data-dance="sway">
|
| 268 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M4 18h16M4 6h16"/></svg>
|
| 269 |
+
Sway
|
| 270 |
+
</button>
|
| 271 |
+
<button class="dance-btn ${this.currentDance === 'spin' ? 'active' : ''}" data-dance="spin">
|
| 272 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
| 273 |
+
Spin
|
| 274 |
+
</button>
|
| 275 |
+
<button class="dance-btn ${this.currentDance === 'wave' ? 'active' : ''}" data-dance="wave">
|
| 276 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s4-4 6-4 4 4 6 4 4-4 6-4"/></svg>
|
| 277 |
+
Wave
|
| 278 |
+
</button>
|
| 279 |
+
</div>
|
| 280 |
+
|
| 281 |
+
<!-- Sliders -->
|
| 282 |
+
<div class="sliders-section">
|
| 283 |
+
<div class="slider-group">
|
| 284 |
+
<div class="slider-label">
|
| 285 |
+
<span>Tempo (BPM)</span>
|
| 286 |
+
<span class="slider-value" id="bpmValue">${this.tempo}</span>
|
| 287 |
+
</div>
|
| 288 |
+
<input type="range" id="tempoSlider" min="60" max="200" value="${this.tempo}">
|
| 289 |
+
</div>
|
| 290 |
+
<div class="slider-group">
|
| 291 |
+
<div class="slider-label">
|
| 292 |
+
<span>Groove Level</span>
|
| 293 |
+
<span class="slider-value" id="grooveValue">⚡ Max</span>
|
| 294 |
+
</div>
|
| 295 |
+
<input type="range" id="grooveSlider" min="1" max="10" value="10">
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
|
| 299 |
+
<!-- Color Picker -->
|
| 300 |
+
<div class="color-controls">
|
| 301 |
+
<div class="color-group">
|
| 302 |
+
<span class="color-label">Primary</span>
|
| 303 |
+
<input type="color" id="primaryColor" value="${this.primaryColor}">
|
| 304 |
+
</div>
|
| 305 |
+
<div class="color-group">
|
| 306 |
+
<span class="color-label">Accent</span>
|
| 307 |
+
<input type="color" id="secondaryColor" value="${this.secondaryColor}">
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
`;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
setupEventListeners() {
|
| 315 |
+
// Play button
|
| 316 |
+
const playBtn = this.shadowRoot.getElementById('playBtn');
|
| 317 |
+
playBtn.addEventListener('click', () => {
|
| 318 |
+
this.isPlaying = !this.isPlaying;
|
| 319 |
+
playBtn.classList.toggle('playing', this.isPlaying);
|
| 320 |
+
playBtn.innerHTML = this.isPlaying ?
|
| 321 |
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>' :
|
| 322 |
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
|
| 323 |
+
|
| 324 |
+
document.dispatchEvent(new CustomEvent('toggle-play', {
|
| 325 |
+
detail: { playing: this.isPlaying }
|
| 326 |
+
}));
|
| 327 |
+
});
|
| 328 |
+
|
| 329 |
+
// Dance buttons
|
| 330 |
+
this.shadowRoot.querySelectorAll('.dance-btn').forEach(btn => {
|
| 331 |
+
btn.addEventListener('click', () => {
|
| 332 |
+
this.shadowRoot.querySelectorAll('.dance-btn').forEach(b => b.classList.remove('active'));
|
| 333 |
+
btn.classList.add('active');
|
| 334 |
+
this.currentDance = btn.dataset.dance;
|
| 335 |
+
document.dispatchEvent(new CustomEvent('dance-select', {
|
| 336 |
+
detail: { dance: this.currentDance }
|
| 337 |
+
}));
|
| 338 |
+
});
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
// Tempo slider
|
| 342 |
+
const tempoSlider = this.shadowRoot.getElementById('tempoSlider');
|
| 343 |
+
const bpmValue = this.shadowRoot.getElementById('bpmValue');
|
| 344 |
+
tempoSlider.addEventListener('input', (e) => {
|
| 345 |
+
this.tempo = parseInt(e.target.value);
|
| 346 |
+
bpmValue.textContent = this.tempo;
|
| 347 |
+
document.dispatchEvent(new CustomEvent('tempo-change', {
|
| 348 |
+
detail: { tempo: this.tempo }
|
| 349 |
+
}));
|
| 350 |
+
});
|
| 351 |
+
|
| 352 |
+
// Color pickers
|
| 353 |
+
const primaryPicker = this.shadowRoot.getElementById('primaryColor');
|
| 354 |
+
const secondaryPicker = this.shadowRoot.getElementById('secondaryColor');
|
| 355 |
+
|
| 356 |
+
const updateColors = () => {
|
| 357 |
+
this.primaryColor = primaryPicker.value;
|
| 358 |
+
this.secondaryColor = secondaryPicker.value;
|
| 359 |
+
|
| 360 |
+
// Update CSS custom properties
|
| 361 |
+
const style = this.shadowRoot.style;
|
| 362 |
+
style.setProperty('--primary', this.primaryColor);
|
| 363 |
+
style.setProperty('--secondary', this.secondaryColor);
|
| 364 |
+
|
| 365 |
+
document.dispatchEvent(new CustomEvent('color-change', {
|
| 366 |
+
detail: { primary: this.primaryColor, secondary: this.secondaryColor }
|
| 367 |
+
}));
|
| 368 |
+
};
|
| 369 |
+
|
| 370 |
+
primaryPicker.addEventListener('input', updateColors);
|
| 371 |
+
secondaryPicker.addEventListener('input', updateColors);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
setupAudioVisualization() {
|
| 375 |
+
const visualizer = this.shadowRoot.getElementById('visualizer');
|
| 376 |
+
const bars = visualizer.querySelectorAll('.viz-bar');
|
| 377 |
+
|
| 378 |
+
// Beat animation
|
| 379 |
+
document.addEventListener('beat', () => {
|
| 380 |
+
if (!this.isPlaying) return;
|
| 381 |
+
|
| 382 |
+
bars.forEach((bar, i) => {
|
| 383 |
+
const height = 4 + Math.random() * 50 + (Math.sin(Date.now() / 200 + i) * 20);
|
| 384 |
+
bar.style.height = `${Math.min(height, 56)}px`;
|
| 385 |
+
bar.style.opacity = 0.5 + Math.random() * 0.5;
|
| 386 |
+
});
|
| 387 |
+
});
|
| 388 |
+
|
| 389 |
+
// Idle animation
|
| 390 |
+
const animate = () => {
|
| 391 |
+
if (!this.isPlaying) {
|
| 392 |
+
bars.forEach((bar, i) => {
|
| 393 |
+
const height = 4 + Math.sin(Date.now() / 500 + i * 0.5) * 10 + 5;
|
| 394 |
+
bar.style.height = `${height}px`;
|
| 395 |
+
bar.style.opacity = 0.3;
|
| 396 |
+
});
|
| 397 |
+
}
|
| 398 |
+
requestAnimationFrame(animate);
|
| 399 |
+
};
|
| 400 |
+
animate();
|
| 401 |
+
}
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
customElements.define('groove-controls', GrooveControls);
|
components/groove-footer.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GrooveToon Footer Component
|
| 2 |
+
class GrooveFooter extends HTMLElement {
|
| 3 |
+
constructor() {
|
| 4 |
+
super();
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
connectedCallback() {
|
| 8 |
+
this.attachShadow({ mode: 'open' });
|
| 9 |
+
this.render();
|
| 10 |
+
this.setupShortcuts();
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
render() {
|
| 14 |
+
this.shadowRoot.innerHTML = `
|
| 15 |
+
<style>
|
| 16 |
+
:host {
|
| 17 |
+
display: block;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.footer {
|
| 21 |
+
padding: 1.5rem 2rem;
|
| 22 |
+
display: flex;
|
| 23 |
+
justify-content: space-between;
|
| 24 |
+
align-items: center;
|
| 25 |
+
flex-wrap: wrap;
|
| 26 |
+
gap: 1rem;
|
| 27 |
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.shortcuts {
|
| 31 |
+
display: flex;
|
| 32 |
+
align-items: center;
|
| 33 |
+
gap: 1rem;
|
| 34 |
+
font-size: 0.75rem;
|
| 35 |
+
color: rgba(255, 255, 255, 0.5);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.shortcut {
|
| 39 |
+
display: flex;
|
| 40 |
+
align-items: center;
|
| 41 |
+
gap: 0.375rem;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.key {
|
| 45 |
+
padding: 0.25rem 0.5rem;
|
| 46 |
+
background: rgba(255, 255, 255, 0.1);
|
| 47 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 48 |
+
border-radius: 6px;
|
| 49 |
+
font-weight: 600;
|
| 50 |
+
color: rgba(255, 255, 255, 0.8);
|
| 51 |
+
font-size: 0.625rem;
|
| 52 |
+
text-transform: uppercase;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.stats {
|
| 56 |
+
display: flex;
|
| 57 |
+
align-items: center;
|
| 58 |
+
gap: 1.5rem;
|
| 59 |
+
font-size: 0.875rem;
|
| 60 |
+
color: rgba(255, 255, 255, 0.6);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.stat {
|
| 64 |
+
display: flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 0.5rem;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.stat-value {
|
| 70 |
+
color: #f43f5e;
|
| 71 |
+
font-weight: 700;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.social-links {
|
| 75 |
+
display: flex;
|
| 76 |
+
gap: 0.75rem;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.social-btn {
|
| 80 |
+
width: 40px;
|
| 81 |
+
height: 40px;
|
| 82 |
+
border-radius: 10px;
|
| 83 |
+
background: rgba(255, 255, 255, 0.05);
|
| 84 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 85 |
+
display: flex;
|
| 86 |
+
align-items: center;
|
| 87 |
+
justify-content: center;
|
| 88 |
+
color: rgba(255, 255, 255, 0.7);
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
transition: all 0.2s ease;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.social-btn:hover {
|
| 94 |
+
background: rgba(244, 63, 94, 0.2);
|
| 95 |
+
border-color: rgba(244, 63, 94, 0.3);
|
| 96 |
+
color: #f43f5e;
|
| 97 |
+
transform: translateY(-2px);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.social-btn svg {
|
| 101 |
+
width: 20px;
|
| 102 |
+
height: 20px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
@media (max-width: 768px) {
|
| 106 |
+
.footer {
|
| 107 |
+
flex-direction: column;
|
| 108 |
+
text-align: center;
|
| 109 |
+
padding: 1rem;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.shortcuts {
|
| 113 |
+
display: none;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
</style>
|
| 117 |
+
|
| 118 |
+
<footer class="footer">
|
| 119 |
+
<div class="shortcuts">
|
| 120 |
+
<span class="shortcut"><span class="key">Space</span> Play/Pause</span>
|
| 121 |
+
<span class="shortcut"><span class="key">1-4</span> Dance</span>
|
| 122 |
+
<span class="shortcut"><span class="key">↑↓</span> Tempo</span>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div class="stats">
|
| 126 |
+
<div class="stat">
|
| 127 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
| 128 |
+
<span>Dance: <span class="stat-value" id="currentDose">Bounce</span></span>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="stat">
|
| 131 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 10v3"/><path d="M6 6v11"/><path d="M10 3v18"/><path d="M14 8v7"/><path d="M18 5v13"/><path d="M22 10v4"/></svg>
|
| 132 |
+
<span>BPM: <span class="stat-value" id="currentBpm">120</span></span>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div class="social-links">
|
| 137 |
+
<button class="social-btn" title="Share" onclick="alert('Share feature coming soon!')">
|
| 138 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
|
| 139 |
+
</button>
|
| 140 |
+
<button class="social-btn" title="Fullscreen" onclick="document.documentElement.requestFullscreen()">
|
| 141 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
|
| 142 |
+
</button>
|
| 143 |
+
</div>
|
| 144 |
+
</footer>
|
| 145 |
+
`;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
setupShortcuts() {
|
| 149 |
+
// Update stats display
|
| 150 |
+
document.addEventListener('dance-change', (e) => {
|
| 151 |
+
const doseEl = this.shadowRoot.getElementById('currentDose');
|
| 152 |
+
if (doseEl) doseEl.textContent = e.detail.charAt(0).toUpperCase() + e.detail.slice(1);
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
document.addEventListener('tempo-change', (e) => {
|
| 156 |
+
const bpmEl = this.shadowRoot.getElementById('currentBpm');
|
| 157 |
+
if (bpmEl) bpmEl.textContent = e.detail.tempo;
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
customElements.define('groove-footer', GrooveFooter);
|
components/groove-header.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GrooveToon Header Component
|
| 2 |
+
class GrooveHeader extends HTMLElement {
|
| 3 |
+
constructor() {
|
| 4 |
+
super();
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
connectedCallback() {
|
| 8 |
+
this.attachShadow({ mode: 'open' });
|
| 9 |
+
this.shadowRoot.innerHTML = `
|
| 10 |
+
<style>
|
| 11 |
+
:host {
|
| 12 |
+
display: block;
|
| 13 |
+
}
|
| 14 |
+
.header {
|
| 15 |
+
padding: 1.5rem 2rem;
|
| 16 |
+
display: flex;
|
| 17 |
+
justify-content: space-between;
|
| 18 |
+
align-items: center;
|
| 19 |
+
}
|
| 20 |
+
.logo {
|
| 21 |
+
display: flex;
|
| 22 |
+
align-items: center;
|
| 23 |
+
gap: 0.75rem;
|
| 24 |
+
font-family: 'Fredoka One', cursive;
|
| 25 |
+
font-size: 1.75rem;
|
| 26 |
+
color: white;
|
| 27 |
+
text-decoration: none;
|
| 28 |
+
cursor: pointer;
|
| 29 |
+
}
|
| 30 |
+
.logo-icon {
|
| 31 |
+
width: 48px;
|
| 32 |
+
height: 48px;
|
| 33 |
+
background: linear-gradient(135deg, #f43f5e, #8b5cf6);
|
| 34 |
+
border-radius: 16px;
|
| 35 |
+
display: flex;
|
| 36 |
+
align-items: center;
|
| 37 |
+
justify-content: center;
|
| 38 |
+
animation: logo-bounce 2s ease-in-out infinite;
|
| 39 |
+
box-shadow: 0 0 20px rgba(244, 63, 94, 0.4);
|
| 40 |
+
}
|
| 41 |
+
@keyframes logo-bounce {
|
| 42 |
+
0%, 100% { transform: scale(1) rotate(-5deg); }
|
| 43 |
+
50% { transform: scale(1.05) rotate(5deg); }
|
| 44 |
+
}
|
| 45 |
+
.logo-text {
|
| 46 |
+
background: linear-gradient(90deg, #f43f5e, #8b5cf6);
|
| 47 |
+
-webkit-background-clip: text;
|
| 48 |
+
-webkit-text-fill-color: transparent;
|
| 49 |
+
background-clip: text;
|
| 50 |
+
}
|
| 51 |
+
.nav-links {
|
| 52 |
+
display: flex;
|
| 53 |
+
gap: 2rem;
|
| 54 |
+
align-items: center;
|
| 55 |
+
}
|
| 56 |
+
.nav-link {
|
| 57 |
+
color: rgba(255, 255, 255, 0.7);
|
| 58 |
+
text-decoration: none;
|
| 59 |
+
font-weight: 600;
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
gap: 0.5rem;
|
| 63 |
+
padding: 0.5rem 1rem;
|
| 64 |
+
border-radius: 9999px;
|
| 65 |
+
transition: all 0.3s ease;
|
| 66 |
+
}
|
| 67 |
+
.nav-link:hover {
|
| 68 |
+
color: white;
|
| 69 |
+
background: rgba(255, 255, 255, 0.1);
|
| 70 |
+
}
|
| 71 |
+
.status {
|
| 72 |
+
display: flex;
|
| 73 |
+
align-items: center;
|
| 74 |
+
gap: 0.5rem;
|
| 75 |
+
padding: 0.5rem 1rem;
|
| 76 |
+
background: rgba(139, 92, 246, 0.2);
|
| 77 |
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
| 78 |
+
border-radius: 9999px;
|
| 79 |
+
font-size: 0.875rem;
|
| 80 |
+
font-weight: 600;
|
| 81 |
+
}
|
| 82 |
+
.status-dot {
|
| 83 |
+
width: 8px;
|
| 84 |
+
height: 8px;
|
| 85 |
+
background: #8b5cf6;
|
| 86 |
+
border-radius: 50%;
|
| 87 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 88 |
+
}
|
| 89 |
+
.status.playing .status-dot {
|
| 90 |
+
background: #f43f5e;
|
| 91 |
+
animation: pulse 0.5s ease-in-out infinite;
|
| 92 |
+
}
|
| 93 |
+
@media (max-width: 768px) {
|
| 94 |
+
.nav-links {
|
| 95 |
+
display: none;
|
| 96 |
+
}
|
| 97 |
+
.header {
|
| 98 |
+
padding: 1rem;
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
</style>
|
| 102 |
+
<header class="header">
|
| 103 |
+
<a href="index.html" class="logo">
|
| 104 |
+
<div class="logo-icon">
|
| 105 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
| 106 |
+
<path d="M9 18V5l12-2v13"></path>
|
| 107 |
+
<circle cx="6" cy="18" r="3"></circle>
|
| 108 |
+
<circle cx="18" cy="16" r="3"></circle>
|
| 109 |
+
</svg>
|
| 110 |
+
</div>
|
| 111 |
+
<span class="logo-text">GrooveToon</span>
|
| 112 |
+
</a>
|
| 113 |
+
<nav class="nav-links">
|
| 114 |
+
<a href="index.html" class="nav-link">
|
| 115 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
| 116 |
+
Stage
|
| 117 |
+
</a>
|
| 118 |
+
<a href="#" class="nav-link" onclick="event.preventDefault(); alert('Studio coming soon!')">
|
| 119 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v14a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path><path d="M19 10v4a7 7 0 0 1-14 0v-4"></path><line x1="12" y1="19" x2="12" y2="22"></line></svg>
|
| 120 |
+
Studio
|
| 121 |
+
</a>
|
| 122 |
+
</nav>
|
| 123 |
+
<div class="status" id="status">
|
| 124 |
+
<span class="status-dot"></span>
|
| 125 |
+
<span id="status-text">Ready to Groove</span>
|
| 126 |
+
</div>
|
| 127 |
+
</header>
|
| 128 |
+
`;
|
| 129 |
+
|
| 130 |
+
// Listen for play state changes
|
| 131 |
+
document.addEventListener('toggle-play', (e) => {
|
| 132 |
+
const status = this.shadowRoot.getElementById('status');
|
| 133 |
+
const statusText = this.shadowRoot.getElementById('status-text');
|
| 134 |
+
status.classList.toggle('playing', e.detail.playing);
|
| 135 |
+
statusText.textContent = e.detail.playing ? 'Dancing!' : 'Ready to Groove';
|
| 136 |
+
});
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
customElements.define('groove-header', GrooveHeader);
|
index.html
CHANGED
|
@@ -1,19 +1,97 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>GrooveToon - Interactive Dancing Character</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 11 |
+
<script>
|
| 12 |
+
tailwind.config = {
|
| 13 |
+
theme: {
|
| 14 |
+
extend: {
|
| 15 |
+
colors: {
|
| 16 |
+
primary: {
|
| 17 |
+
50: '#fff1f2',
|
| 18 |
+
100: '#ffe4e6',
|
| 19 |
+
200: '#fecdd3',
|
| 20 |
+
300: '#fda4af',
|
| 21 |
+
400: '#fb7185',
|
| 22 |
+
500: '#f43f5e',
|
| 23 |
+
600: '#e11d48',
|
| 24 |
+
700: '#be123c',
|
| 25 |
+
800: '#9f1239',
|
| 26 |
+
900: '#881337',
|
| 27 |
+
},
|
| 28 |
+
secondary: {
|
| 29 |
+
50: '#f5f3ff',
|
| 30 |
+
100: '#ede9fe',
|
| 31 |
+
200: '#ddd6fe',
|
| 32 |
+
300: '#c4b5fd',
|
| 33 |
+
400: '#a78bfa',
|
| 34 |
+
500: '#8b5cf6',
|
| 35 |
+
600: '#7c3aed',
|
| 36 |
+
700: '#6d28d9',
|
| 37 |
+
800: '#5b21b6',
|
| 38 |
+
900: '#4c1d95',
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
animation: {
|
| 42 |
+
'bounce-slow': 'bounce 2s infinite',
|
| 43 |
+
'pulse-slow': 'pulse 3s infinite',
|
| 44 |
+
'spin-slow': 'spin 8s linear infinite',
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
</script>
|
| 50 |
+
</head>
|
| 51 |
+
<body class="bg-neutral-950 text-white min-h-screen overflow-x-hidden">
|
| 52 |
+
<!-- WebGL Background Canvas -->
|
| 53 |
+
<canvas id="bgCanvas" class="fixed inset-0 w-full h-full pointer-events-none"></canvas>
|
| 54 |
+
|
| 55 |
+
<!-- Main Content -->
|
| 56 |
+
<div class="relative z-10 min-h-screen flex flex-col">
|
| 57 |
+
<!-- Header -->
|
| 58 |
+
<groove-header></groove-header>
|
| 59 |
+
|
| 60 |
+
<!-- Main Stage -->
|
| 61 |
+
<main class="flex-1 flex flex-col items-center justify-center px-4 py-8">
|
| 62 |
+
<!-- Dance Floor -->
|
| 63 |
+
<div class="relative w-full max-w-4xl">
|
| 64 |
+
<!-- Stage Lights -->
|
| 65 |
+
<div class="absolute -top-20 left-1/4 w-32 h-32 bg-primary-500/20 rounded-full blur-3xl animate-pulse-slow"></div>
|
| 66 |
+
<div class="absolute -top-20 right-1/4 w-32 h-32 bg-secondary-500/20 rounded-full blur-3xl animate-pulse-slow" style="animation-delay: 1s;"></div>
|
| 67 |
+
|
| 68 |
+
<!-- Spotlight -->
|
| 69 |
+
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-96 h-[600px] bg-gradient-to-b from-primary-500/10 to-transparent pointer-events-none"></div>
|
| 70 |
+
|
| 71 |
+
<!-- Character Container -->
|
| 72 |
+
<div class="relative flex justify-center items-end min-h-[500px] pb-8">
|
| 73 |
+
<groove-character id="dancer"></groove-character>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<!-- Floor Reflection -->
|
| 77 |
+
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-[80%] h-24 bg-gradient-to-t from-primary-500/10 to-transparent rounded-full blur-xl"></div>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<!-- Controls -->
|
| 81 |
+
<groove-controls></groove-controls>
|
| 82 |
+
</main>
|
| 83 |
+
|
| 84 |
+
<!-- Footer -->
|
| 85 |
+
<groove-footer></groove-footer>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<!-- Scripts -->
|
| 89 |
+
<script src="components/groove-header.js"></script>
|
| 90 |
+
<script src="components/groove-character.js"></script>
|
| 91 |
+
<script src="components/groove-controls.js"></script>
|
| 92 |
+
<script src="components/groove-footer.js"></script>
|
| 93 |
+
<script src="script.js"></script>
|
| 94 |
+
<script>feather.replace();</script>
|
| 95 |
+
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
|
| 96 |
+
</body>
|
| 97 |
+
</html>
|
script.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GrooveToon Interactive - Main Script
|
| 2 |
+
|
| 3 |
+
// Global State
|
| 4 |
+
const state = {
|
| 5 |
+
isPlaying: false,
|
| 6 |
+
currentDance: 'bounce',
|
| 7 |
+
tempo: 120,
|
| 8 |
+
accentColor: 'primary',
|
| 9 |
+
secondaryColor: 'secondary',
|
| 10 |
+
audioContext: null,
|
| 11 |
+
beatInterval: null,
|
| 12 |
+
particles: [],
|
| 13 |
+
characterMood: 'happy'
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// Color Themes
|
| 17 |
+
const themes = {
|
| 18 |
+
primary: {
|
| 19 |
+
500: '#f43f5e',
|
| 20 |
+
600: '#e11d48',
|
| 21 |
+
400: '#fb7185'
|
| 22 |
+
},
|
| 23 |
+
secondary: {
|
| 24 |
+
500: '#8b5cf6',
|
| 25 |
+
600: '#7c3aed',
|
| 26 |
+
400: '#a78bfa'
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
// Dance Moves Library
|
| 31 |
+
const danceMoves = {
|
| 32 |
+
bounce: {
|
| 33 |
+
name: 'Bounce',
|
| 34 |
+
css: {
|
| 35 |
+
body: 'animation: dance-bounce 0.5s ease-in-out infinite',
|
| 36 |
+
arms: 'animation: dance-wave 1s ease-in-out infinite',
|
| 37 |
+
head: 'animation: dance-head-bop 0.5s ease-in-out infinite'
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
sway: {
|
| 41 |
+
name: 'Sway',
|
| 42 |
+
css: {
|
| 43 |
+
body: 'animation: dance-sway 2s ease-in-out infinite',
|
| 44 |
+
arms: 'animation: dance-wave 2s ease-in-out infinite reverse',
|
| 45 |
+
head: 'animation: dance-sway 2s ease-in-out infinite 0.5s'
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
spin: {
|
| 49 |
+
name: 'Spin',
|
| 50 |
+
css: {
|
| 51 |
+
body: 'animation: dance-spin 2s ease-in-out infinite',
|
| 52 |
+
arms: 'animation: dance-sway 1s ease-in-out infinite',
|
| 53 |
+
head: 'animation: none'
|
| 54 |
+
}
|
| 55 |
+
},
|
| 56 |
+
wave: {
|
| 57 |
+
name: 'Wave',
|
| 58 |
+
css: {
|
| 59 |
+
body: 'animation: float-gentle 3s ease-in-out infinite',
|
| 60 |
+
arms: 'animation: dance-wave 0.5s ease-in-out infinite',
|
| 61 |
+
head: 'animation: dance-head-bop 0.25s ease-in-out infinite'
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
idle: {
|
| 65 |
+
name: 'Idle',
|
| 66 |
+
css: {
|
| 67 |
+
body: 'animation: float-gentle 4s ease-in-out infinite',
|
| 68 |
+
arms: 'animation: none',
|
| 69 |
+
head: 'animation: none'
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
// Audio System
|
| 75 |
+
class AudioEngine {
|
| 76 |
+
constructor() {
|
| 77 |
+
this.ctx = null;
|
| 78 |
+
this.masterGain = null;
|
| 79 |
+
this.isInitialized = false;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
init() {
|
| 83 |
+
if (this.isInitialized) return;
|
| 84 |
+
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
| 85 |
+
this.masterGain = this.ctx.createGain();
|
| 86 |
+
this.masterGain.gain.value = 0.3;
|
| 87 |
+
this.masterGain.connect(this.ctx.destination);
|
| 88 |
+
this.isInitialized = true;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
playKick(time) {
|
| 92 |
+
const osc = this.ctx.createOscillator();
|
| 93 |
+
const gain = this.ctx.createGain();
|
| 94 |
+
|
| 95 |
+
osc.frequency.setValueAtTime(150, time);
|
| 96 |
+
osc.frequency.exponentialRampToValueAtTime(0.01, time + 0.5);
|
| 97 |
+
|
| 98 |
+
gain.gain.setValueAtTime(1, time);
|
| 99 |
+
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.5);
|
| 100 |
+
|
| 101 |
+
osc.connect(gain);
|
| 102 |
+
gain.connect(this.masterGain);
|
| 103 |
+
|
| 104 |
+
osc.start(time);
|
| 105 |
+
osc.stop(time + 0.5);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
playHiHat(time) {
|
| 109 |
+
const bufferSize = this.ctx.sampleRate * 0.1;
|
| 110 |
+
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
| 111 |
+
const data = buffer.getChannelData(0);
|
| 112 |
+
|
| 113 |
+
for (let i = 0; i < bufferSize; i++) {
|
| 114 |
+
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
const noise = this.ctx.createBufferSource();
|
| 118 |
+
noise.buffer = buffer;
|
| 119 |
+
|
| 120 |
+
const filter = this.ctx.createBiquadFilter();
|
| 121 |
+
filter.type = 'highpass';
|
| 122 |
+
filter.frequency.value = 5000;
|
| 123 |
+
|
| 124 |
+
const gain = this.ctx.createGain();
|
| 125 |
+
gain.gain.value = 0.3;
|
| 126 |
+
|
| 127 |
+
noise.connect(filter);
|
| 128 |
+
filter.connect(gain);
|
| 129 |
+
gain.connect(this.masterGain);
|
| 130 |
+
|
| 131 |
+
noise.start(time);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
playSnare(time) {
|
| 135 |
+
const bufferSize = this.ctx.sampleRate * 0.2;
|
| 136 |
+
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
| 137 |
+
const data = buffer.getChannelData(0);
|
| 138 |
+
|
| 139 |
+
for (let i = 0; i < bufferSize; i++) {
|
| 140 |
+
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.05));
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
const noise = this.ctx.createBufferSource();
|
| 144 |
+
noise.buffer = buffer;
|
| 145 |
+
|
| 146 |
+
const filter = this.ctx.createBiquadFilter();
|
| 147 |
+
filter.type = 'highpass';
|
| 148 |
+
filter.frequency.value = 1000;
|
| 149 |
+
|
| 150 |
+
const gain = this.ctx.createGain();
|
| 151 |
+
gain.gain.setValueAtTime(0.5, time);
|
| 152 |
+
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.2);
|
| 153 |
+
|
| 154 |
+
noise.connect(filter);
|
| 155 |
+
filter.connect(gain);
|
| 156 |
+
gain.connect(this.masterGain);
|
| 157 |
+
|
| 158 |
+
noise.start(time);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
setTempo(bpm) {
|
| 162 |
+
state.tempo = bpm;
|
| 163 |
+
if (state.isPlaying) {
|
| 164 |
+
this.stopBeat();
|
| 165 |
+
this.startBeat();
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
startBeat() {
|
| 170 |
+
this.init();
|
| 171 |
+
const beatDuration = 60 / state.tempo;
|
| 172 |
+
let beatCount = 0;
|
| 173 |
+
|
| 174 |
+
const scheduleBeat = () => {
|
| 175 |
+
const now = this.ctx.currentTime;
|
| 176 |
+
const nextBeat = Math.ceil(now / beatDuration) * beatDuration;
|
| 177 |
+
|
| 178 |
+
this.playKick(nextBeat);
|
| 179 |
+
if (beatCount % 2 === 1) this.playSnare(nextBeat);
|
| 180 |
+
this.playHiHat(nextBeat + beatDuration / 2);
|
| 181 |
+
|
| 182 |
+
beatCount++;
|
| 183 |
+
|
| 184 |
+
const delay = (nextBeat - now) * 1000;
|
| 185 |
+
this.beatTimeout = setTimeout(scheduleBeat, delay);
|
| 186 |
+
};
|
| 187 |
+
|
| 188 |
+
scheduleBeat();
|
| 189 |
+
|
| 190 |
+
// Visual beat indicator
|
| 191 |
+
state.beatInterval = setInterval(() => {
|
| 192 |
+
document.dispatchEvent(new CustomEvent('beat'));
|
| 193 |
+
}, beatDuration * 1000);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
stopBeat() {
|
| 197 |
+
clearTimeout(this.beatTimeout);
|
| 198 |
+
clearInterval(state.beatInterval);
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Visual Effects Engine
|
| 203 |
+
class VisualEffects {
|
| 204 |
+
constructor() {
|
| 205 |
+
this.canvas = document.getElementById('bgCanvas');
|
| 206 |
+
this.ctx = this.canvas.getContext('2d');
|
| 207 |
+
this.particles = [];
|
| 208 |
+
this.resize();
|
| 209 |
+
window.addEventListener('resize', () => this.resize());
|
| 210 |
+
this.animate();
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
resize() {
|
| 214 |
+
this.canvas.width = window.innerWidth;
|
| 215 |
+
this.canvas.height = window.innerHeight;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
createParticle(x, y, type = 'sparkle') {
|
| 219 |
+
const particle = {
|
| 220 |
+
x, y,
|
| 221 |
+
vx: (Math.random() - 0.5) * 4,
|
| 222 |
+
vy: -Math.random() * 3 - 1,
|
| 223 |
+
life: 1,
|
| 224 |
+
decay: 0.02,
|
| 225 |
+
size: Math.random() * 4 + 2,
|
| 226 |
+
hue: Math.random() > 0.5 ? 348 : 262, // primary or secondary hue
|
| 227 |
+
type
|
| 228 |
+
};
|
| 229 |
+
this.particles.push(particle);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
createBurst(x, y) {
|
| 233 |
+
for (let i = 0; i < 20; i++) {
|
| 234 |
+
this.createParticle(x, y, 'burst');
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
animate() {
|
| 239 |
+
this.ctx.fillStyle = 'rgba(10, 10, 10, 0.1)';
|
| 240 |
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
| 241 |
+
|
| 242 |
+
// Draw connecting lines between nearby particles
|
| 243 |
+
for (let i = 0; i < this.particles.length; i++) {
|
| 244 |
+
for (let j = i + 1; j < this.particles.length; j++) {
|
| 245 |
+
const dx = this.particles[i].x - this.particles[j].x;
|
| 246 |
+
const dy = this.particles[i].y - this.particles[j].y;
|
| 247 |
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
| 248 |
+
|
| 249 |
+
if (dist < 100) {
|
| 250 |
+
this.ctx.beginPath();
|
| 251 |
+
this.ctx.strokeStyle = `hsla(${this.particles[i].hue}, 70%, 60%, ${0.1 * (1 - dist/100)})`;
|
| 252 |
+
this.ctx.lineWidth = 1;
|
| 253 |
+
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
|
| 254 |
+
this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
|
| 255 |
+
this.ctx.stroke();
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
// Update and draw particles
|
| 261 |
+
this.particles = this.particles.filter(p => {
|
| 262 |
+
p.x += p.vx;
|
| 263 |
+
p.y += p.vy;
|
| 264 |
+
p.vy += 0.05; // gravity
|
| 265 |
+
p.life -= p.decay;
|
| 266 |
+
|
| 267 |
+
if (p.life > 0) {
|
| 268 |
+
this.ctx.beginPath();
|
| 269 |
+
this.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
|
| 270 |
+
this.ctx.fillStyle = `hsla(${p.hue}, 80%, 60%, ${p.life})`;
|
| 271 |
+
this.ctx.fill();
|
| 272 |
+
return true;
|
| 273 |
+
}
|
| 274 |
+
return false;
|
| 275 |
+
});
|
| 276 |
+
|
| 277 |
+
// Draw subtle grid
|
| 278 |
+
this.drawGrid();
|
| 279 |
+
|
| 280 |
+
requestAnimationFrame(() => this.animate());
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
drawGrid() {
|
| 284 |
+
const time = Date.now() * 0.001;
|
| 285 |
+
const gridSize = 50;
|
| 286 |
+
|
| 287 |
+
this.ctx.strokeStyle = 'rgba(244, 63, 94, 0.03)';
|
| 288 |
+
this.ctx.lineWidth = 1;
|
| 289 |
+
|
| 290 |
+
for (let x = 0; x < this.canvas.width; x += gridSize) {
|
| 291 |
+
const wave = Math.sin(x * 0.01 + time) * 10;
|
| 292 |
+
this.ctx.beginPath();
|
| 293 |
+
this.ctx.moveTo(x, 0);
|
| 294 |
+
this.ctx.lineTo(x + wave, this.canvas.height);
|
| 295 |
+
this.ctx.stroke();
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
for (let y = 0; y < this.canvas.height; y += gridSize) {
|
| 299 |
+
const wave = Math.cos(y * 0.01 + time) * 10;
|
| 300 |
+
this.ctx.beginPath();
|
| 301 |
+
this.ctx.moveTo(0, y);
|
| 302 |
+
this.ctx.lineTo(this.canvas.width, y + wave);
|
| 303 |
+
this.ctx.stroke();
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// Character Controller
|
| 309 |
+
class CharacterController {
|
| 310 |
+
constructor() {
|
| 311 |
+
this.element = document.getElementById('dancer');
|
| 312 |
+
this.currentMove = 'idle';
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
setDance(moveName) {
|
| 316 |
+
this.currentMove = moveName;
|
| 317 |
+
const move = danceMoves[moveName] || danceMoves.idle;
|
| 318 |
+
|
| 319 |
+
// Apply CSS animations to character parts
|
| 320 |
+
const characterParts = this.element.shadowRoot;
|
| 321 |
+
if (!characterParts) return;
|
| 322 |
+
|
| 323 |
+
const body = characterParts.getElementById('char-body');
|
| 324 |
+
const leftArm = characterParts.getElementById('char-arm-left');
|
| 325 |
+
const rightArm = characterParts.getElementById('char-arm-right');
|
| 326 |
+
const head = characterParts.getElementById('char-head');
|
| 327 |
+
|
| 328 |
+
if (body) body.style.cssText = move.css.body;
|
| 329 |
+
if (leftArm) leftArm.style.cssText = move.css.arms;
|
| 330 |
+
if (rightArm) rightArm.style.cssText = move.css.arms + ';animation-direction: reverse;';
|
| 331 |
+
if (head) head.style.cssText = move.css.head;
|
| 332 |
+
|
| 333 |
+
// Dispatch event for UI update
|
| 334 |
+
document.dispatchEvent(new CustomEvent('dance-change', { detail: moveName }));
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
setMood(mood) {
|
| 338 |
+
state.characterMood = mood;
|
| 339 |
+
const characterParts = this.element.shadowRoot;
|
| 340 |
+
if (!characterParts) return;
|
| 341 |
+
|
| 342 |
+
const face = characterParts.getElementById('char-face');
|
| 343 |
+
if (face) {
|
| 344 |
+
face.setAttribute('data-mood', mood);
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
pulse() {
|
| 349 |
+
const body = this.element.shadowRoot?.getElementById('char-body');
|
| 350 |
+
if (body) {
|
| 351 |
+
body.style.filter = 'brightness(1.3)';
|
| 352 |
+
setTimeout(() => {
|
| 353 |
+
body.style.filter = 'brightness(1)';
|
| 354 |
+
}, 100);
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// Main App Controller
|
| 360 |
+
class GrooveApp {
|
| 361 |
+
constructor() {
|
| 362 |
+
this.audio = new AudioEngine();
|
| 363 |
+
this.visuals = new VisualEffects();
|
| 364 |
+
this.character = new CharacterController();
|
| 365 |
+
|
| 366 |
+
this.initListeners();
|
| 367 |
+
this.character.setDance('idle');
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
initListeners() {
|
| 371 |
+
// Play/Pause
|
| 372 |
+
document.addEventListener('toggle-play', (e) => {
|
| 373 |
+
state.isPlaying = e.detail.playing;
|
| 374 |
+
if (state.isPlaying) {
|
| 375 |
+
this.audio.startBeat();
|
| 376 |
+
this.character.setDance(state.currentDance);
|
| 377 |
+
} else {
|
| 378 |
+
this.audio.stopBeat();
|
| 379 |
+
this.character.setDance('idle');
|
| 380 |
+
}
|
| 381 |
+
});
|
| 382 |
+
|
| 383 |
+
// Dance change
|
| 384 |
+
document.addEventListener('dance-select', (e) => {
|
| 385 |
+
state.currentDance = e.detail.dance;
|
| 386 |
+
if (state.isPlaying) {
|
| 387 |
+
this.character.setDance(state.currentDance);
|
| 388 |
+
}
|
| 389 |
+
});
|
| 390 |
+
|
| 391 |
+
// Tempo change
|
| 392 |
+
document.addEventListener('tempo-change', (e) => {
|
| 393 |
+
this.audio.setTempo(e.detail.tempo);
|
| 394 |
+
});
|
| 395 |
+
|
| 396 |
+
// Beat event
|
| 397 |
+
document.addEventListener('beat', () => {
|
| 398 |
+
if (state.isPlaying) {
|
| 399 |
+
this.character.pulse();
|
| 400 |
+
// Random particles
|
| 401 |
+
const x = Math.random() * window.innerWidth;
|
| 402 |
+
const y = window.innerHeight - 200;
|
| 403 |
+
this.visuals.createParticle(x, y);
|
| 404 |
+
}
|
| 405 |
+
});
|
| 406 |
+
|
| 407 |
+
// Color change
|
| 408 |
+
document.addEventListener('color-change', (e) => {
|
| 409 |
+
const { primary, secondary } = e.detail;
|
| 410 |
+
document.documentElement.style.setProperty('--primary-hue', this.hexToHue(primary));
|
| 411 |
+
document.documentElement.style.setProperty('--secondary-hue', this.hexToHue(secondary));
|
| 412 |
+
});
|
| 413 |
+
|
| 414 |
+
// Click effects
|
| 415 |
+
document.addEventListener('click', (e) => {
|
| 416 |
+
if (e.target.closest('button') || e.target.closest('.control-btn')) {
|
| 417 |
+
this.visuals.createBurst(e.clientX, e.clientY);
|
| 418 |
+
}
|
| 419 |
+
});
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
hexToHue(hex) {
|
| 423 |
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
| 424 |
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
| 425 |
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
| 426 |
+
const max = Math.max(r, g, b);
|
| 427 |
+
const min = Math.min(r, g, b);
|
| 428 |
+
let h = 0;
|
| 429 |
+
if (max !== min) {
|
| 430 |
+
const d = max - min;
|
| 431 |
+
h = max === r ? (g - b) / d + (g < b ? 6 : 0) :
|
| 432 |
+
max === g ? (b - r) / d + 2 :
|
| 433 |
+
(r - g) / d + 4;
|
| 434 |
+
h *= 60;
|
| 435 |
+
}
|
| 436 |
+
return h;
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// Initialize when DOM is ready
|
| 441 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 442 |
+
window.grooveApp = new GrooveApp();
|
| 443 |
+
});
|
style.css
CHANGED
|
@@ -1,28 +1,191 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
}
|
| 25 |
|
| 26 |
-
.
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Custom CSS for GrooveToon */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@400;600;700;800&display=swap');
|
| 3 |
+
|
| 4 |
+
/* CSS Variables for Theme */
|
| 5 |
+
:root {
|
| 6 |
+
--primary-hue: 348;
|
| 7 |
+
--secondary-hue: 262;
|
| 8 |
+
--bg-luminance: 8%;
|
| 9 |
+
--glow-strength: 1;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
* {
|
| 13 |
+
font-family: 'Nunito', sans-serif;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
h1, h2, h3, .font-display {
|
| 17 |
+
font-family: 'Fredoka One', cursive;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* Smooth Transitions */
|
| 21 |
+
* {
|
| 22 |
+
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
| 23 |
+
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
| 24 |
+
transition-duration: 200ms;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Glass Effect */
|
| 28 |
+
.glass {
|
| 29 |
+
background: rgba(255, 255, 255, 0.05);
|
| 30 |
+
backdrop-filter: blur(10px);
|
| 31 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.glass-dark {
|
| 35 |
+
background: rgba(0, 0, 0, 0.3);
|
| 36 |
+
backdrop-filter: blur(10px);
|
| 37 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Neon Glow Effects */
|
| 41 |
+
.neon-primary {
|
| 42 |
+
box-shadow: 0 0 20px hsla(var(--primary-hue), 90%, 60%, 0.5),
|
| 43 |
+
0 0 40px hsla(var(--primary-hue), 90%, 60%, 0.3),
|
| 44 |
+
0 0 60px hsla(var(--primary-hue), 90%, 60%, 0.1);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.neon-secondary {
|
| 48 |
+
box-shadow: 0 0 20px hsla(var(--secondary-hue), 90%, 60%, 0.5),
|
| 49 |
+
0 0 40px hsla(var(--secondary-hue), 90%, 60%, 0.3),
|
| 50 |
+
0 0 60px hsla(var(--secondary-hue), 90%, 60%, 0.1);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* Character Animations */
|
| 54 |
+
@keyframes dance-bounce {
|
| 55 |
+
0%, 100% { transform: translateY(0) scale(1); }
|
| 56 |
+
50% { transform: translateY(-20px) scale(1.02); }
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
@keyframes dance-sway {
|
| 60 |
+
0%, 100% { transform: rotate(-5deg); }
|
| 61 |
+
50% { transform: rotate(5deg); }
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
@keyframes dance-spin {
|
| 65 |
+
0% { transform: rotate(0deg) scale(1); }
|
| 66 |
+
50% { transform: rotate(180deg) scale(1.1); }
|
| 67 |
+
100% { transform: rotate(360deg) scale(1); }
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
@keyframes dance-wave {
|
| 71 |
+
0%, 100% { transform: rotate(-20deg) translateY(0); }
|
| 72 |
+
25% { transform: rotate(10deg) translateY(-10px); }
|
| 73 |
+
50% { transform: rotate(-10deg) translateY(0); }
|
| 74 |
+
75% { transform: rotate(20deg) translateY(-10px); }
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
@keyframes dance-head-bop {
|
| 78 |
+
0%, 100% { transform: rotate(0deg) translateY(0); }
|
| 79 |
+
25% { transform: rotate(-10deg) translateY(5px); }
|
| 80 |
+
50% { transform: rotate(0deg) translateY(0); }
|
| 81 |
+
75% { transform: rotate(10deg) translateY(5px); }
|
| 82 |
}
|
| 83 |
|
| 84 |
+
@keyframes float-gentle {
|
| 85 |
+
0%, 100% { transform: translateY(0); }
|
| 86 |
+
50% { transform: translateY(-10px); }
|
| 87 |
}
|
| 88 |
|
| 89 |
+
@keyframes glow-pulse {
|
| 90 |
+
0%, 100% { filter: brightness(1) drop-shadow(0 0 5px currentColor); }
|
| 91 |
+
50% { filter: brightness(1.2) drop-shadow(0 0 20px currentColor); }
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
+
/* Particle Effects */
|
| 95 |
+
@keyframes particle-float {
|
| 96 |
+
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
| 97 |
+
10% { opacity: 1; }
|
| 98 |
+
90% { opacity: 1; }
|
| 99 |
+
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
|
| 100 |
}
|
| 101 |
|
| 102 |
+
.particle {
|
| 103 |
+
position: fixed;
|
| 104 |
+
pointer-events: none;
|
| 105 |
+
animation: particle-float linear infinite;
|
| 106 |
}
|
| 107 |
+
|
| 108 |
+
/* Custom Scrollbar */
|
| 109 |
+
::-webkit-scrollbar {
|
| 110 |
+
width: 8px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
::-webkit-scrollbar-track {
|
| 114 |
+
background: #171717;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
::-webkit-scrollbar-thumb {
|
| 118 |
+
background: linear-gradient(180deg, #f43f5e, #8b5cf6);
|
| 119 |
+
border-radius: 4px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
::-webkit-scrollbar-thumb:hover {
|
| 123 |
+
background: linear-gradient(180deg, #fb7185, #a78bfa);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Range Slider Styling */
|
| 127 |
+
input[type="range"] {
|
| 128 |
+
-webkit-appearance: none;
|
| 129 |
+
appearance: none;
|
| 130 |
+
background: transparent;
|
| 131 |
+
cursor: pointer;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
input[type="range"]::-webkit-slider-track {
|
| 135 |
+
background: rgba(255, 255, 255, 0.1);
|
| 136 |
+
height: 6px;
|
| 137 |
+
border-radius: 3px;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 141 |
+
-webkit-appearance: none;
|
| 142 |
+
appearance: none;
|
| 143 |
+
background: linear-gradient(135deg, #f43f5e, #8b5cf6);
|
| 144 |
+
height: 20px;
|
| 145 |
+
width: 20px;
|
| 146 |
+
border-radius: 50%;
|
| 147 |
+
margin-top: -7px;
|
| 148 |
+
box-shadow: 0 0 10px rgba(244, 63, 94, 0.5);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
input[type="range"]::-moz-range-track {
|
| 152 |
+
background: rgba(255, 255, 255, 0.1);
|
| 153 |
+
height: 6px;
|
| 154 |
+
border-radius: 3px;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
input[type="range"]::-moz-range-thumb {
|
| 158 |
+
background: linear-gradient(135deg, #f43f5e, #8b5cf6);
|
| 159 |
+
height: 20px;
|
| 160 |
+
width: 20px;
|
| 161 |
+
border-radius: 50%;
|
| 162 |
+
border: none;
|
| 163 |
+
box-shadow: 0 0 10px rgba(244, 63, 94, 0.5);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* Beat Indicator */
|
| 167 |
+
.beat-bar {
|
| 168 |
+
animation: beat-pulse 0.5s ease-in-out;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
@keyframes beat-pulse {
|
| 172 |
+
0%, 100% { transform: scaleY(1); }
|
| 173 |
+
50% { transform: scaleY(2); }
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/* Responsive Touch Targets */
|
| 177 |
+
@media (hover: none) and (pointer: coarse) {
|
| 178 |
+
button, .control-btn {
|
| 179 |
+
min-height: 44px;
|
| 180 |
+
min-width: 44px;
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/* Reduced Motion */
|
| 185 |
+
@media (prefers-reduced-motion: reduce) {
|
| 186 |
+
*, *::before, *::after {
|
| 187 |
+
animation-duration: 0.01ms !important;
|
| 188 |
+
animation-iteration-count: 1 !important;
|
| 189 |
+
transition-duration: 0.01ms !important;
|
| 190 |
+
}
|
| 191 |
+
}
|