Sam5920's picture
a dancing cartoon
56504cf verified
// GrooveToon Interactive - Main Script
// Global State
const state = {
isPlaying: false,
currentDance: 'bounce',
tempo: 120,
accentColor: 'primary',
secondaryColor: 'secondary',
audioContext: null,
beatInterval: null,
particles: [],
characterMood: 'happy'
};
// Color Themes
const themes = {
primary: {
500: '#f43f5e',
600: '#e11d48',
400: '#fb7185'
},
secondary: {
500: '#8b5cf6',
600: '#7c3aed',
400: '#a78bfa'
}
};
// Dance Moves Library
const danceMoves = {
bounce: {
name: 'Bounce',
css: {
body: 'animation: dance-bounce 0.5s ease-in-out infinite',
arms: 'animation: dance-wave 1s ease-in-out infinite',
head: 'animation: dance-head-bop 0.5s ease-in-out infinite'
}
},
sway: {
name: 'Sway',
css: {
body: 'animation: dance-sway 2s ease-in-out infinite',
arms: 'animation: dance-wave 2s ease-in-out infinite reverse',
head: 'animation: dance-sway 2s ease-in-out infinite 0.5s'
}
},
spin: {
name: 'Spin',
css: {
body: 'animation: dance-spin 2s ease-in-out infinite',
arms: 'animation: dance-sway 1s ease-in-out infinite',
head: 'animation: none'
}
},
wave: {
name: 'Wave',
css: {
body: 'animation: float-gentle 3s ease-in-out infinite',
arms: 'animation: dance-wave 0.5s ease-in-out infinite',
head: 'animation: dance-head-bop 0.25s ease-in-out infinite'
}
},
idle: {
name: 'Idle',
css: {
body: 'animation: float-gentle 4s ease-in-out infinite',
arms: 'animation: none',
head: 'animation: none'
}
}
};
// Audio System
class AudioEngine {
constructor() {
this.ctx = null;
this.masterGain = null;
this.isInitialized = false;
}
init() {
if (this.isInitialized) return;
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
this.isInitialized = true;
}
playKick(time) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.frequency.setValueAtTime(150, time);
osc.frequency.exponentialRampToValueAtTime(0.01, time + 0.5);
gain.gain.setValueAtTime(1, time);
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.5);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(time);
osc.stop(time + 0.5);
}
playHiHat(time) {
const bufferSize = this.ctx.sampleRate * 0.1;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.value = 5000;
const gain = this.ctx.createGain();
gain.gain.value = 0.3;
noise.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
noise.start(time);
}
playSnare(time) {
const bufferSize = this.ctx.sampleRate * 0.2;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.05));
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.value = 1000;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.5, time);
gain.gain.exponentialRampToValueAtTime(0.01, time + 0.2);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
noise.start(time);
}
setTempo(bpm) {
state.tempo = bpm;
if (state.isPlaying) {
this.stopBeat();
this.startBeat();
}
}
startBeat() {
this.init();
const beatDuration = 60 / state.tempo;
let beatCount = 0;
const scheduleBeat = () => {
const now = this.ctx.currentTime;
const nextBeat = Math.ceil(now / beatDuration) * beatDuration;
this.playKick(nextBeat);
if (beatCount % 2 === 1) this.playSnare(nextBeat);
this.playHiHat(nextBeat + beatDuration / 2);
beatCount++;
const delay = (nextBeat - now) * 1000;
this.beatTimeout = setTimeout(scheduleBeat, delay);
};
scheduleBeat();
// Visual beat indicator
state.beatInterval = setInterval(() => {
document.dispatchEvent(new CustomEvent('beat'));
}, beatDuration * 1000);
}
stopBeat() {
clearTimeout(this.beatTimeout);
clearInterval(state.beatInterval);
}
}
// Visual Effects Engine
class VisualEffects {
constructor() {
this.canvas = document.getElementById('bgCanvas');
this.ctx = this.canvas.getContext('2d');
this.particles = [];
this.resize();
window.addEventListener('resize', () => this.resize());
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
createParticle(x, y, type = 'sparkle') {
const particle = {
x, y,
vx: (Math.random() - 0.5) * 4,
vy: -Math.random() * 3 - 1,
life: 1,
decay: 0.02,
size: Math.random() * 4 + 2,
hue: Math.random() > 0.5 ? 348 : 262, // primary or secondary hue
type
};
this.particles.push(particle);
}
createBurst(x, y) {
for (let i = 0; i < 20; i++) {
this.createParticle(x, y, 'burst');
}
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 10, 0.1)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw connecting lines between nearby particles
for (let i = 0; i < this.particles.length; i++) {
for (let j = i + 1; j < this.particles.length; j++) {
const dx = this.particles[i].x - this.particles[j].x;
const dy = this.particles[i].y - this.particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100) {
this.ctx.beginPath();
this.ctx.strokeStyle = `hsla(${this.particles[i].hue}, 70%, 60%, ${0.1 * (1 - dist/100)})`;
this.ctx.lineWidth = 1;
this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
this.ctx.stroke();
}
}
}
// Update and draw particles
this.particles = this.particles.filter(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.05; // gravity
p.life -= p.decay;
if (p.life > 0) {
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
this.ctx.fillStyle = `hsla(${p.hue}, 80%, 60%, ${p.life})`;
this.ctx.fill();
return true;
}
return false;
});
// Draw subtle grid
this.drawGrid();
requestAnimationFrame(() => this.animate());
}
drawGrid() {
const time = Date.now() * 0.001;
const gridSize = 50;
this.ctx.strokeStyle = 'rgba(244, 63, 94, 0.03)';
this.ctx.lineWidth = 1;
for (let x = 0; x < this.canvas.width; x += gridSize) {
const wave = Math.sin(x * 0.01 + time) * 10;
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x + wave, this.canvas.height);
this.ctx.stroke();
}
for (let y = 0; y < this.canvas.height; y += gridSize) {
const wave = Math.cos(y * 0.01 + time) * 10;
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(this.canvas.width, y + wave);
this.ctx.stroke();
}
}
}
// Character Controller
class CharacterController {
constructor() {
this.element = document.getElementById('dancer');
this.currentMove = 'idle';
}
setDance(moveName) {
this.currentMove = moveName;
const move = danceMoves[moveName] || danceMoves.idle;
// Apply CSS animations to character parts
const characterParts = this.element.shadowRoot;
if (!characterParts) return;
const body = characterParts.getElementById('char-body');
const leftArm = characterParts.getElementById('char-arm-left');
const rightArm = characterParts.getElementById('char-arm-right');
const head = characterParts.getElementById('char-head');
if (body) body.style.cssText = move.css.body;
if (leftArm) leftArm.style.cssText = move.css.arms;
if (rightArm) rightArm.style.cssText = move.css.arms + ';animation-direction: reverse;';
if (head) head.style.cssText = move.css.head;
// Dispatch event for UI update
document.dispatchEvent(new CustomEvent('dance-change', { detail: moveName }));
}
setMood(mood) {
state.characterMood = mood;
const characterParts = this.element.shadowRoot;
if (!characterParts) return;
const face = characterParts.getElementById('char-face');
if (face) {
face.setAttribute('data-mood', mood);
}
}
pulse() {
const body = this.element.shadowRoot?.getElementById('char-body');
if (body) {
body.style.filter = 'brightness(1.3)';
setTimeout(() => {
body.style.filter = 'brightness(1)';
}, 100);
}
}
}
// Main App Controller
class GrooveApp {
constructor() {
this.audio = new AudioEngine();
this.visuals = new VisualEffects();
this.character = new CharacterController();
this.initListeners();
this.character.setDance('idle');
}
initListeners() {
// Play/Pause
document.addEventListener('toggle-play', (e) => {
state.isPlaying = e.detail.playing;
if (state.isPlaying) {
this.audio.startBeat();
this.character.setDance(state.currentDance);
} else {
this.audio.stopBeat();
this.character.setDance('idle');
}
});
// Dance change
document.addEventListener('dance-select', (e) => {
state.currentDance = e.detail.dance;
if (state.isPlaying) {
this.character.setDance(state.currentDance);
}
});
// Tempo change
document.addEventListener('tempo-change', (e) => {
this.audio.setTempo(e.detail.tempo);
});
// Beat event
document.addEventListener('beat', () => {
if (state.isPlaying) {
this.character.pulse();
// Random particles
const x = Math.random() * window.innerWidth;
const y = window.innerHeight - 200;
this.visuals.createParticle(x, y);
}
});
// Color change
document.addEventListener('color-change', (e) => {
const { primary, secondary } = e.detail;
document.documentElement.style.setProperty('--primary-hue', this.hexToHue(primary));
document.documentElement.style.setProperty('--secondary-hue', this.hexToHue(secondary));
});
// Click effects
document.addEventListener('click', (e) => {
if (e.target.closest('button') || e.target.closest('.control-btn')) {
this.visuals.createBurst(e.clientX, e.clientY);
}
});
}
hexToHue(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
if (max !== min) {
const d = max - min;
h = max === r ? (g - b) / d + (g < b ? 6 : 0) :
max === g ? (b - r) / d + 2 :
(r - g) / d + 4;
h *= 60;
}
return h;
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.grooveApp = new GrooveApp();
});