/** * Preloaded Graphics Demos * * Each demo exports: * - name: Display name * - description: Short description * - setup(app, state): Called once to create objects * - update(app, state, objects): Called every frame * - cleanup(app, objects): Called when switching demos */ // ============================================================================ // DEMO 1: BOUNCING PARTICLES // ============================================================================ export const bouncingParticles = { name: "Bouncing Particles", description: "Colorful particles bouncing around the screen", setup(app, state) { const particles = []; const colors = [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0x6c5ce7]; for (let i = 0; i < 50; i++) { const particle = new PIXI.Graphics(); const size = 5 + Math.random() * 10; particle.circle(0, 0, size); particle.fill(colors[Math.floor(Math.random() * colors.length)]); particle.x = Math.random() * app.screen.width; particle.y = Math.random() * app.screen.height; particle.vx = (Math.random() - 0.5) * 8; particle.vy = (Math.random() - 0.5) * 8; particle.size = size; app.stage.addChild(particle); particles.push(particle); } return { particles }; }, update(app, state, objects) { objects.particles.forEach(p => { p.x += p.vx; p.y += p.vy; // Bounce off edges if (p.x < p.size || p.x > app.screen.width - p.size) p.vx *= -1; if (p.y < p.size || p.y > app.screen.height - p.size) p.vy *= -1; // Clamp to screen p.x = Math.max(p.size, Math.min(app.screen.width - p.size, p.x)); p.y = Math.max(p.size, Math.min(app.screen.height - p.size, p.y)); }); }, cleanup(app, objects) { objects.particles.forEach(p => p.destroy()); } }; // ============================================================================ // DEMO 2: SPIROGRAPH // ============================================================================ export const spirograph = { name: "Spirograph", description: "Mesmerizing geometric spiral patterns", setup(app, state) { const graphics = new PIXI.Graphics(); app.stage.addChild(graphics); return { graphics, angle: 0, points: [] }; }, update(app, state, objects) { const cx = app.screen.width / 2; const cy = app.screen.height / 2; const t = state.state.time.current; // Generate new point const r1 = 150; const r2 = 50; const r3 = 30; const x = cx + Math.cos(t * 0.5) * r1 + Math.cos(t * 2) * r2 + Math.cos(t * 5) * r3; const y = cy + Math.sin(t * 0.5) * r1 + Math.sin(t * 2) * r2 + Math.sin(t * 5) * r3; objects.points.push({ x, y }); // Keep only last 500 points if (objects.points.length > 500) { objects.points.shift(); } // Draw trail objects.graphics.clear(); if (objects.points.length > 1) { for (let i = 1; i < objects.points.length; i++) { const alpha = i / objects.points.length; const hue = (i / objects.points.length) * 360; objects.graphics.moveTo(objects.points[i-1].x, objects.points[i-1].y); objects.graphics.lineTo(objects.points[i].x, objects.points[i].y); objects.graphics.stroke({ width: 2, color: hslToHex(hue, 100, 60), alpha }); } } }, cleanup(app, objects) { objects.graphics.destroy(); } }; // ============================================================================ // DEMO 3: STARFIELD // ============================================================================ export const starfield = { name: "Starfield", description: "Flying through space at warp speed", setup(app, state) { const stars = []; for (let i = 0; i < 200; i++) { const star = new PIXI.Graphics(); star.circle(0, 0, 2); star.fill(0xffffff); star.x = (Math.random() - 0.5) * app.screen.width * 2; star.y = (Math.random() - 0.5) * app.screen.height * 2; star.z = Math.random() * 1000; app.stage.addChild(star); stars.push(star); } return { stars }; }, update(app, state, objects) { const cx = app.screen.width / 2; const cy = app.screen.height / 2; const speed = 5; objects.stars.forEach(star => { star.z -= speed; if (star.z <= 0) { star.z = 1000; star.x = (Math.random() - 0.5) * app.screen.width * 2; star.y = (Math.random() - 0.5) * app.screen.height * 2; } const screenX = cx + (star.x / star.z) * 200; const screenY = cy + (star.y / star.z) * 200; const size = (1 - star.z / 1000) * 4 + 1; star.x = star.x; star.y = star.y; star.position.set(screenX, screenY); star.scale.set(size); star.alpha = 1 - star.z / 1000; }); }, cleanup(app, objects) { objects.stars.forEach(s => s.destroy()); } }; // ============================================================================ // DEMO 4: WAVE INTERFERENCE // ============================================================================ export const waveInterference = { name: "Wave Interference", description: "Rippling wave patterns", setup(app, state) { const gridSize = 20; const cols = Math.floor(app.screen.width / gridSize); const rows = Math.floor(app.screen.height / gridSize); const circles = []; for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const circle = new PIXI.Graphics(); circle.circle(0, 0, 4); circle.fill(0x4ecdc4); circle.x = i * gridSize + gridSize / 2; circle.y = j * gridSize + gridSize / 2; circle.baseX = circle.x; circle.baseY = circle.y; app.stage.addChild(circle); circles.push(circle); } } return { circles, sources: [ { x: app.screen.width * 0.3, y: app.screen.height * 0.5 }, { x: app.screen.width * 0.7, y: app.screen.height * 0.5 } ]}; }, update(app, state, objects) { const t = state.state.time.current; objects.circles.forEach(c => { let totalOffset = 0; objects.sources.forEach(source => { const dx = c.baseX - source.x; const dy = c.baseY - source.y; const dist = Math.sqrt(dx * dx + dy * dy); totalOffset += Math.sin(dist * 0.05 - t * 3) * 10; }); c.y = c.baseY + totalOffset; c.alpha = 0.3 + (Math.sin(totalOffset * 0.1) + 1) * 0.35; }); }, cleanup(app, objects) { objects.circles.forEach(c => c.destroy()); } }; // ============================================================================ // DEMO 5: CIRCLE PACKING // ============================================================================ export const circlePacking = { name: "Circle Packing", description: "Organic growth simulation", setup(app, state) { const circles = []; return { circles, attempts: 0 }; }, update(app, state, objects) { // Try to add a new circle each frame const maxAttempts = 100; const maxCircles = 150; if (objects.circles.length >= maxCircles) return; for (let i = 0; i < 10; i++) { const x = Math.random() * app.screen.width; const y = Math.random() * app.screen.height; const minRadius = 5; const maxRadius = 60; let valid = true; let radius = minRadius; // Find largest radius that doesn't overlap for (let r = minRadius; r < maxRadius; r++) { let overlaps = false; for (const other of objects.circles) { const dx = x - other.x; const dy = y - other.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < r + other.radius + 2) { overlaps = true; break; } } if (overlaps) { break; } radius = r; } if (radius > minRadius) { const circle = new PIXI.Graphics(); circle.circle(0, 0, radius); const hue = (objects.circles.length * 137.5) % 360; circle.fill(hslToHex(hue, 70, 60)); circle.x = x; circle.y = y; circle.radius = radius; app.stage.addChild(circle); objects.circles.push(circle); break; } } }, cleanup(app, objects) { objects.circles.forEach(c => c.destroy()); } }; // ============================================================================ // DEMO 6: PERLIN FLOW FIELD // ============================================================================ export const flowField = { name: "Flow Field", description: "Particles following a noise field", setup(app, state) { const particles = []; const colors = [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0x6c5ce7, 0xfeca57]; for (let i = 0; i < 300; i++) { const particle = new PIXI.Graphics(); particle.circle(0, 0, 2); particle.fill(colors[Math.floor(Math.random() * colors.length)]); particle.alpha = 0.6; particle.x = Math.random() * app.screen.width; particle.y = Math.random() * app.screen.height; particle.vx = 0; particle.vy = 0; particle.color = colors[Math.floor(Math.random() * colors.length)]; app.stage.addChild(particle); particles.push(particle); } return { particles }; }, update(app, state, objects) { const t = state.state.time.current; objects.particles.forEach(p => { // Simple noise-like function using sin/cos const angle = noise(p.x * 0.005, p.y * 0.005, t * 0.3) * Math.PI * 2; p.vx += Math.cos(angle) * 0.3; p.vy += Math.sin(angle) * 0.3; // Damping p.vx *= 0.95; p.vy *= 0.95; p.x += p.vx; p.y += p.vy; // Wrap around screen if (p.x < 0) p.x = app.screen.width; if (p.x > app.screen.width) p.x = 0; if (p.y < 0) p.y = app.screen.height; if (p.y > app.screen.height) p.y = 0; }); }, cleanup(app, objects) { objects.particles.forEach(p => p.destroy()); } }; // ============================================================================ // DEMO 7: DNA HELIX // ============================================================================ export const dnaHelix = { name: "DNA Helix", description: "Rotating double helix structure", setup(app, state) { const helix1 = []; const helix2 = []; const connectors = []; const segments = 40; for (let i = 0; i < segments; i++) { const sphere1 = new PIXI.Graphics(); sphere1.circle(0, 0, 8); sphere1.fill(0x4ecdc4); app.stage.addChild(sphere1); helix1.push(sphere1); const sphere2 = new PIXI.Graphics(); sphere2.circle(0, 0, 8); sphere2.fill(0xff6b6b); app.stage.addChild(sphere2); helix2.push(sphere2); const connector = new PIXI.Graphics(); app.stage.addChild(connector); connectors.push(connector); } return { helix1, helix2, connectors }; }, update(app, state, objects) { const t = state.state.time.current; const cx = app.screen.width / 2; const cy = app.screen.height / 2; const radius = 100; const height = app.screen.height * 0.8; const spacing = height / objects.helix1.length; objects.helix1.forEach((sphere, i) => { const y = i * spacing - height / 2 + cy; const angle = t + i * 0.3; const x = cx + Math.cos(angle) * radius; const z = Math.sin(angle) * radius; sphere.x = x; sphere.y = y; sphere.scale.set(1 + z / 200); sphere.alpha = 0.5 + z / 400; }); objects.helix2.forEach((sphere, i) => { const y = i * spacing - height / 2 + cy; const angle = t + i * 0.3 + Math.PI; const x = cx + Math.cos(angle) * radius; const z = Math.sin(angle) * radius; sphere.x = x; sphere.y = y; sphere.scale.set(1 + z / 200); sphere.alpha = 0.5 + z / 400; }); // Draw connectors objects.connectors.forEach((connector, i) => { connector.clear(); connector.moveTo(objects.helix1[i].x, objects.helix1[i].y); connector.lineTo(objects.helix2[i].x, objects.helix2[i].y); connector.stroke({ width: 2, color: 0x666666, alpha: 0.3 }); }); }, cleanup(app, objects) { objects.helix1.forEach(s => s.destroy()); objects.helix2.forEach(s => s.destroy()); objects.connectors.forEach(c => c.destroy()); } }; // ============================================================================ // DEMO 8: FIREWORKS // ============================================================================ export const fireworks = { name: "Fireworks", description: "Explosive particle celebration", setup(app, state) { return { explosions: [], nextExplosion: 0 }; }, update(app, state, objects) { const t = state.state.time.current; // Create new explosion every second if (t > objects.nextExplosion) { objects.nextExplosion = t + 0.5 + Math.random(); const explosion = { x: Math.random() * app.screen.width, y: Math.random() * app.screen.height * 0.7, particles: [], color: Math.random() * 0xffffff, born: t }; // Create particles for (let i = 0; i < 50; i++) { const angle = (i / 50) * Math.PI * 2; const speed = 2 + Math.random() * 4; const particle = new PIXI.Graphics(); particle.circle(0, 0, 3); particle.fill(explosion.color); particle.x = explosion.x; particle.y = explosion.y; particle.vx = Math.cos(angle) * speed; particle.vy = Math.sin(angle) * speed; app.stage.addChild(particle); explosion.particles.push(particle); } objects.explosions.push(explosion); } // Update explosions objects.explosions = objects.explosions.filter(explosion => { const age = t - explosion.born; if (age > 3) { explosion.particles.forEach(p => p.destroy()); return false; } explosion.particles.forEach(p => { p.vx *= 0.98; p.vy += 0.1; // Gravity p.x += p.vx; p.y += p.vy; p.alpha = 1 - age / 3; }); return true; }); }, cleanup(app, objects) { objects.explosions.forEach(explosion => { explosion.particles.forEach(p => p.destroy()); }); } }; // ============================================================================ // DEMO 9: MATRIX RAIN // ============================================================================ export const matrixRain = { name: "Matrix Rain", description: "Falling digital rain effect", setup(app, state) { const fontSize = 16; const columns = Math.floor(app.screen.width / fontSize); const drops = []; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*"; for (let i = 0; i < columns; i++) { const text = new PIXI.Text('', { fontFamily: 'monospace', fontSize: fontSize, fill: 0x00ff00 }); text.x = i * fontSize; text.y = -Math.random() * app.screen.height; app.stage.addChild(text); drops.push({ text, speed: 1 + Math.random() * 3, chars: chars }); } return { drops }; }, update(app, state, objects) { objects.drops.forEach(drop => { drop.y = (drop.y || drop.text.y) + drop.speed; drop.text.y = drop.y; // Random character if (Math.random() > 0.95) { drop.text.text = drop.chars[Math.floor(Math.random() * drop.chars.length)]; } // Reset to top if (drop.y > app.screen.height) { drop.y = -20; drop.text.alpha = 1; } // Fade trail drop.text.alpha = Math.max(0.1, drop.text.alpha - 0.01); }); }, cleanup(app, objects) { objects.drops.forEach(d => d.text.destroy()); } }; // ============================================================================ // DEMO 10: SOLAR SYSTEM // ============================================================================ export const solarSystem = { name: "Solar System", description: "Orbiting planets around a star", setup(app, state) { const cx = app.screen.width / 2; const cy = app.screen.height / 2; // Sun const sun = new PIXI.Graphics(); sun.circle(0, 0, 30); sun.fill(0xffd700); sun.x = cx; sun.y = cy; app.stage.addChild(sun); // Planets const planets = [ { radius: 60, size: 6, speed: 2.0, color: 0x8b7355 }, { radius: 100, size: 10, speed: 1.5, color: 0xff6347 }, { radius: 150, size: 12, speed: 1.0, color: 0x4169e1 }, { radius: 200, size: 8, speed: 0.7, color: 0xff4500 }, { radius: 260, size: 18, speed: 0.4, color: 0xdaa520 }, ]; const planetObjects = planets.map(config => { const planet = new PIXI.Graphics(); planet.circle(0, 0, config.size); planet.fill(config.color); planet.config = config; app.stage.addChild(planet); return planet; }); return { sun, planets: planetObjects, cx, cy }; }, update(app, state, objects) { const t = state.state.time.current; objects.planets.forEach((planet, i) => { const angle = t * planet.config.speed; planet.x = objects.cx + Math.cos(angle) * planet.config.radius; planet.y = objects.cy + Math.sin(angle) * planet.config.radius; }); }, cleanup(app, objects) { objects.sun.destroy(); objects.planets.forEach(p => p.destroy()); } }; // ============================================================================ // UTILITIES // ============================================================================ function hslToHex(h, s, l) { s /= 100; l /= 100; const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs((h / 60) % 2 - 1)); const m = l - c/2; let r = 0, g = 0, b = 0; if (0 <= h && h < 60) { r = c; g = x; b = 0; } else if (60 <= h && h < 120) { r = x; g = c; b = 0; } else if (120 <= h && h < 180) { r = 0; g = c; b = x; } else if (180 <= h && h < 240) { r = 0; g = x; b = c; } else if (240 <= h && h < 300) { r = x; g = 0; b = c; } else if (300 <= h && h < 360) { r = c; g = 0; b = x; } r = Math.round((r + m) * 255); g = Math.round((g + m) * 255); b = Math.round((b + m) * 255); return (r << 16) | (g << 8) | b; } function noise(x, y, z) { return Math.sin(x + Math.cos(y)) * Math.cos(y + Math.sin(z)) * Math.sin(z + Math.cos(x)); } // ============================================================================ // EXPORT ALL DEMOS // ============================================================================ export const allDemos = [ bouncingParticles, spirograph, starfield, waveInterference, circlePacking, flowField, dnaHelix, fireworks, matrixRain, solarSystem ];