
EchoCanvas
Prompt
Create an interactive web canvas where user mouse movements generate dynamic, evolving visual patterns synchronized with sound. The experience should feel like painting with echoes of sound and light. Here’s the core idea: When the user moves the mouse, abstract shapes (e.g., circles, lines, or splines) are drawn on the canvas, with colors and sizes influenced by the speed and direction of movement. Each shape triggers a unique sound (e.g., a tone, chime, or synth wave) based on its position and properties. Shapes fade out over time, but their "echoes" (smaller, semi-transparent copies) linger and move in random directions before disappearing. Add a "chaos mode" toggle that randomizes all parameters (colors, shapes, sounds, movement) for a wilder experience. The canvas must be full-screen and responsive, adapting to any device size.
A system prompt was added to support web rendering
Below is a complete HTML file that creates an interactive web canvas with the features you described. It uses the p5.js library for canvas drawing and the Tone.js library for sound generation. The code is fully responsive, works full-screen, and includes a chaos mode toggle for randomizing visuals and sounds. All external dependencies are loaded via CDN, and the entire implementation is contained in a single code block. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas of Echoes</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.9/Tone.js"></script> <style> body { margin: 0; padding: 0; overflow: hidden; background: #000; display: flex; flex-direction: column; align-items: center; font-family: Arial, sans-serif; } canvas { display: block; } .controls { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.7); padding: 10px 20px; border-radius: 10px; color: white; text-align: center; z-index: 10; } .controls label { cursor: pointer; font-size: 16px; } .controls input { cursor: pointer; } </style> </head> <body> <div class="controls"> <label for="chaosToggle">Chaos Mode</label> <input type="checkbox" id="chaosToggle" onchange="toggleChaosMode()"> </div> <script> let shapes = []; let echoes = []; let synth; let prevX, prevY; let isDrawing = false; let chaosMode = false; let baseFrequency = 220; let fadeTime = 2000; let echoFadeTime = 4000; function setup() { createCanvas(windowWidth, windowHeight); background(0); prevX = mouseX; prevY = mouseY; // Initialize Tone.js synth for sound generation synth = new Tone.PolySynth(Tone.Synth).toDestination(); } function windowResized() { resizeCanvas(windowWidth, windowHeight); } function draw() { background(0, 0, 0, 50); // Slightly transparent background for fade effect // Update and draw shapes updateAndDraw(shapes, fadeTime); // Update and draw echoes updateAndDraw(echoes, echoFadeTime); // Draw current stroke if mouse is pressed if (isDrawing) { let speed = dist(mouseX, mouseY, prevX, prevY); let size = map(speed, 0, 50, 5, 30, true); let color = chaosMode ? getRandomColor() : getSpeedColor(speed); let shapeType = chaosMode ? floor(random(3)) : 0; // 0: circle, 1: line, 2: rect let newShape = createShape(mouseX, mouseY, size, color, shapeType); shapes.push(newShape); // Play sound based on position and speed let freq = map(mouseY, 0, height, 880, 110) + (chaosMode ? random(-50, 50) : 0); playSound(freq, map(speed, 0, 50, 0.1, 0.5)); // Generate echoes createEchoes(newShape); prevX = mouseX; prevY = mouseY; } } function updateAndDraw(collection, maxAge) { for (let i = collection.length - 1; i >= 0; i--) { let s = collection[i]; s.age += deltaTime; s.alpha = map(s.age, 0, maxAge, 255, 0, true); if (s.age > maxAge) { collection.splice(i, 1); } else { // Echoes move randomly if (collection === echoes) { s.x += s.vx; s.y += s.vy; } drawShape(s); } } } function createShape(x, y, size, color, type) { return { x: x, y: y, size: size, color: color, type: type, age: 0, alpha: 255 }; } function drawShape(s) { noStroke(); fill(s.color[0], s.color[1], s.color[2], s.alpha); if (s.type === 0) { ellipse(s.x, s.y, s.size, s.size); } else if (s.type === 1) { stroke(s.color[0], s.color[1], s.color[2], s.alpha); strokeWeight(s.size / 5); line(s.x - s.size / 2, s.y, s.x + s.size / 2, s.y); } else if (s.type === 2) { rectMode(CENTER); rect(s.x, s.y, s.size, s.size); } } function createEchoes(original) { let numEchoes = chaosMode ? floor(random(3, 6)) : 3; for (let i = 0; i < numEchoes; i++) { let echoSize = original.size * 0.6; let echo = createShape( original.x + random(-20, 20), original.y + random(-20, 20), echoSize, original.color, original.type ); echo.vx = random(-2, 2) * (chaosMode ? 1.5 : 1); echo.vy = random(-2, 2) * (chaosMode ? 1.5 : 1); echo.alpha = 100; echoes.push(echo); } } function getSpeedColor(speed) { let r = map(speed, 0, 50, 50, 255); let g = map(speed, 0, 50, 50, 150); let b = map(speed, 0, 50, 150, 255); return [r, g, b]; } function getRandomColor() { return [random(50, 255), random(50, 255), random(50, 255)]; } function playSound(frequency, duration) { let synthOptions = { oscillator: { type: chaosMode ? random(['sine', 'square', 'triangle']) : 'sine' }, envelope: { attack: 0.01, decay: 0.1, sustain: 0.1, release: 0.5 } }; let noteSynth = new Tone.Synth(synthOptions).toDestination(); noteSynth.triggerAttackRelease(frequency, duration); } function mousePressed() { isDrawing = true; prevX = mouseX; prevY = mouseY; } function mouseReleased() { isDrawing = false; } function toggleChaosMode() { chaosMode = document.getElementById('chaosToggle').checked; } </script> </body> </html> ``` This code creates a full-screen, responsive canvas where: - Mouse movements draw shapes (circles by default, randomized in chaos mode) with colors based on speed. - Sounds are triggered with pitch based on vertical position and duration based on speed, using Tone.js for synthesis. - Echoes are smaller, semi-transparent copies of shapes that drift randomly and fade out. - Chaos mode, toggled via a checkbox, randomizes colors, shape types, sound waveforms, and echo behavior. - The experience adapts to any screen size with the `windowResized` function and CSS styling. You can save this as an HTML file or load it directly in a browser to experience the interactive canvas with synchronized visuals and sound.
Here is an interactive web canvas that generates dynamic visuals and sounds based on your mouse movements. Move your mouse across the screen to "paint" with sound and light. The speed of your movement affects the size and volume of the shapes. The position on the screen determines the pitch. For a more unpredictable and wild experience, activate "Chaos Mode". ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Audiovisual Echo Painter</title> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #000; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: white; } #interactiveCanvas { display: block; cursor: none; } #controls { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 10px 20px; background-color: rgba(0, 0, 0, 0.5); border-radius: 20px; display: flex; align-items: center; gap: 10px; user-select: none; transition: opacity 0.3s; } #controls label { cursor: pointer; } #startOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; text-align: center; flex-direction: column; cursor: pointer; z-index: 100; } #startOverlay h1 { font-size: 2.5em; margin: 0; } #startOverlay p { font-size: 1.2em; margin-top: 10px; } </style> </head> <body> <div id="startOverlay"> <h1>Echo Painter</h1> <p>Click or tap to begin painting with sound and light.</p> </div> <canvas id="interactiveCanvas"></canvas> <div id="controls"> <input type="checkbox" id="chaosToggle"> <label for="chaosToggle">Chaos Mode</label> </div> <script> const canvas = document.getElementById('interactiveCanvas'); const ctx = canvas.getContext('2d'); const controls = document.getElementById('controls'); const startOverlay = document.getElementById('startOverlay'); const chaosToggle = document.getElementById('chaosToggle'); let audioCtx; let particles = []; let hue = 0; let isChaosMode = false; let lastMouse = { x: 0, y: 0, time: Date.now() }; // --- Core Classes --- class Particle { constructor(x, y, radius, color, isEcho = false, type = 'circle') { this.x = x; this.y = y; this.radius = radius; this.color = color; this.isEcho = isEcho; this.type = type; this.initialLife = isEcho ? 100 + Math.random() * 50 : 120; this.life = this.initialLife; this.opacity = isEcho ? 0.6 : 1.0; if (isEcho) { const angle = Math.random() * Math.PI * 2; const speed = 1 + Math.random() * 2; this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed; } else { this.vx = 0; this.vy = 0; } } isDead() { return this.life <= 0; } update() { this.life--; this.x += this.vx; this.y += this.vy; this.opacity = (this.life / this.initialLife) * (this.isEcho ? 0.6 : 1.0); } draw() { ctx.save(); ctx.globalAlpha = this.opacity; ctx.fillStyle = this.color; ctx.beginPath(); if (this.type === 'circle') { ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); } else { // square ctx.rect(this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2); } ctx.fill(); ctx.restore(); } } // --- Setup and Initialization --- function setup() { setCanvasSize(); chaosToggle.addEventListener('change', () => { isChaosMode = chaosToggle.checked; }); window.addEventListener('resize', setCanvasSize); canvas.addEventListener('mousemove', handleMouseMove); animate(); } function initAudio() { if (!audioCtx) { try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { console.error("Web Audio API is not supported in this browser"); controls.innerHTML = "Audio not supported"; } } if (audioCtx.state === 'suspended') { audioCtx.resume(); } } startOverlay.addEventListener('click', () => { initAudio(); startOverlay.style.display = 'none'; controls.style.opacity = '1'; }, { once: true }); function setCanvasSize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } // --- Sound Generation --- function playSound(x, y, size) { if (!audioCtx) return; const now = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); osc.connect(gainNode); gainNode.connect(audioCtx.destination); const maxFreq = isChaosMode ? 2000 : 1200; const minFreq = 100; const frequency = minFreq + ((canvas.height - y) / canvas.height) * (maxFreq - minFreq); const maxGain = 0.5; const minGain = 0.05; const volume = minGain + (size / (isChaosMode ? 100 : 50)) * (maxGain - minGain); osc.type = isChaosMode ? ['sine', 'square', 'sawtooth', 'triangle'][Math.floor(Math.random() * 4)] : 'sine'; osc.frequency.setValueAtTime(frequency, now); gainNode.gain.setValueAtTime(0, now); gainNode.gain.linearRampToValueAtTime(volume, now + 0.05); gainNode.gain.linearRampToValueAtTime(0, now + 1.0); osc.start(now); osc.stop(now + 1.0); } // --- Event Handling --- function handleMouseMove(e) { if (audioCtx && audioCtx.state === 'suspended') { audioCtx.resume(); } const now = Date.now(); const dt = now - lastMouse.time; const dx = e.clientX - lastMouse.x; const dy = e.clientY - lastMouse.y; const distance = Math.sqrt(dx * dx + dy * dy); const speed = Math.min(distance / dt, 10); // Only draw if moving fast enough to avoid clutter if (speed > 0.3) { const angle = Math.atan2(dy, dx); // Base parameters let radius = isChaosMode ? 5 + Math.random() * 50 : 5 + speed * 4; let particleColor; let particleType = (isChaosMode && Math.random() > 0.5) ? 'square' : 'circle'; if (isChaosMode) { particleColor = `hsl(${Math.random() * 360}, 100%, 70%)`; } else { hue = (angle * (180 / Math.PI) + 180) % 360; // Map direction to hue particleColor = `hsl(${hue}, 90%, 65%)`; } // Create main particle particles.push(new Particle(e.clientX, e.clientY, radius, particleColor, false, particleType)); // Create echoes const numEchoes = isChaosMode ? Math.floor(2 + Math.random() * 5) : 3; for (let i = 0; i < numEchoes; i++) { const echoRadius = radius * (0.2 + Math.random() * 0.4); particles.push(new Particle(e.clientX, e.clientY, echoRadius, particleColor, true, particleType)); } playSound(e.clientX, e.clientY, radius); } lastMouse = { x: e.clientX, y: e.clientY, time: now }; } // --- Animation Loop --- function animate() { // Semi-transparent background creates a trailing effect ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Loop backwards to safely remove items for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; if (p.isDead()) { particles.splice(i, 1); } else { p.update(); p.draw(); } } requestAnimationFrame(animate); } // --- Start the experience --- setup(); </script> </body> </html> ```
Here's a complete HTML file that creates an interactive web canvas with dynamic visual patterns synchronized with sound. This includes all necessary libraries and code within a single block: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sound and Light Painting</title> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #000; touch-action: none; } #chaosToggle { position: absolute; top: 10px; left: 10px; z-index: 100; padding: 10px; background: rgba(255, 255, 255, 0.2); color: white; border: none; border-radius: 5px; cursor: pointer; font-family: Arial, sans-serif; } </style> </head> <body> <button id="chaosToggle">Chaos Mode: OFF</button> <script src="https://cdn.jsdelivr.net/npm/tone@14.7.77/build/Tone.min.js"></script> <script> // Initialize Tone.js const synth = new Tone.Synth().toDestination(); const now = Tone.now(); // Canvas setup const canvas = document.createElement('canvas'); document.body.appendChild(canvas); const ctx = canvas.getContext('2d'); // Full-screen canvas function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); // Mouse and touch tracking let mouseX = 0, mouseY = 0; let prevMouseX = 0, prevMouseY = 0; let isDragging = false; let chaosMode = false; // Store shapes and their echoes let shapes = []; let echoes = []; // Chaos mode toggle const chaosToggle = document.getElementById('chaosToggle'); chaosToggle.addEventListener('click', () => { chaosMode = !chaosMode; chaosToggle.textContent = `Chaos Mode: ${chaosMode ? 'ON' : 'OFF'}`; }); // Event listeners for mouse and touch canvas.addEventListener('mousemove', handleMove); canvas.addEventListener('mousedown', () => isDragging = true); canvas.addEventListener('mouseup', () => isDragging = false); canvas.addEventListener('touchmove', (e) => { e.preventDefault(); handleMove({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }); }); canvas.addEventListener('touchstart', (e) => { e.preventDefault(); isDragging = true; }); canvas.addEventListener('touchend', () => isDragging = false); function handleMove(e) { prevMouseX = mouseX; prevMouseY = mouseY; mouseX = e.clientX; mouseY = e.clientY; if (isDragging) { const speed = Math.sqrt((mouseX - prevMouseX) ** 2 + (mouseY - prevMouseY) ** 2); const direction = Math.atan2(mouseY - prevMouseY, mouseX - prevMouseX); // Create a new shape const shape = { x: mouseX, y: mouseY, speed: speed, direction: direction, size: chaosMode ? Math.random() * 50 + 10 : speed / 2, color: chaosMode ? `hsl(${Math.random() * 360}, 100%, 50%)` : `hsl(${direction * 180 / Math.PI + 90}, 100%, 50%)`, opacity: 1, type: chaosMode ? Math.floor(Math.random() * 3) : 0, createdAt: Date.now() }; shapes.push(shape); // Play a sound playSound(shape); } } function playSound(shape) { const note = chaosMode ? ['C', 'D', 'E', 'F', 'G', 'A', 'B'][Math.floor(Math.random() * 7)] + Math.floor(Math.random() * 4 + 3) : `C${Math.floor(shape.size / 10 + 3)}`; synth.triggerAttackRelease(note, '8n'); } function drawShape(shape) { ctx.save(); ctx.globalAlpha = shape.opacity; ctx.fillStyle = shape.color; ctx.strokeStyle = shape.color; switch (shape.type) { case 0: // Circle ctx.beginPath(); ctx.arc(shape.x, shape.y, shape.size / 2, 0, Math.PI * 2); ctx.fill(); break; case 1: // Line ctx.beginPath(); ctx.moveTo(shape.x - shape.size / 2, shape.y - shape.size / 2); ctx.lineTo(shape.x + shape.size / 2, shape.y + shape.size / 2); ctx.stroke(); break; case 2: // Spline ctx.beginPath(); ctx.moveTo(shape.x - shape.size / 2, shape.y); ctx.quadraticCurveTo(shape.x, shape.y - shape.size / 2, shape.x + shape.size / 2, shape.y); ctx.quadraticCurveTo(shape.x, shape.y + shape.size / 2, shape.x - shape.size / 2, shape.y); ctx.fill(); break; } ctx.restore(); } function animate() { // Clear canvas ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Update and draw shapes shapes.forEach((shape, index) => { shape.opacity -= 0.01; if (shape.opacity <= 0) { shapes.splice(index, 1); } else { drawShape(shape); // Create echoes if (Math.random() < 0.1) { const echo = { ...shape, x: shape.x + (Math.random() - 0.5) * 20, y: shape.y + (Math.random() - 0.5) * 20, size: shape.size * 0.5, opacity: shape.opacity * 0.5 }; echoes.push(echo); } } }); // Update and draw echoes echoes.forEach((echo, index) => { echo.opacity -= 0.01; if (echo.opacity <= 0) { echoes.splice(index, 1); } else { echo.x += (Math.random() - 0.5) * 2; echo.y += (Math.random() - 0.5) * 2; drawShape(echo); } }); requestAnimationFrame(animate); } animate(); </script> </body> </html> ``` This code creates an interactive canvas where users can draw with their mouse or finger, generating dynamic visual patterns and sounds. The "Chaos Mode" toggle randomizes the shapes, colors, and sounds for a wilder experience. The canvas is full-screen and responsive, adapting to any device size. The visuals and sounds are synchronized to create an engaging, multisensory experience.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Sound & Light Echoes Canvas</title> <style> html, body { margin: 0; height: 100%; background: #111; overflow: hidden; user-select: none; } canvas { display: block; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; touch-action: none; cursor: crosshair; background: radial-gradient(ellipse at center, #111 0%, #000 90%); } #toggleChaos { position: fixed; z-index: 10; top: 12px; right: 12px; background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.3); border-radius: 6px; padding: 8px 14px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #eee; cursor: pointer; user-select: none; transition: background-color 0.3s ease; } #toggleChaos:hover { background: rgba(255,255,255,0.22); } </style> </head> <body> <button id="toggleChaos" aria-pressed="false" aria-label="Toggle Chaos Mode">Chaos Mode: OFF</button> <canvas id="canvas"></canvas> <script> (() => { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let w, h; function resize() { w = canvas.width = window.innerWidth * devicePixelRatio; h = canvas.height = window.innerHeight * devicePixelRatio; canvas.style.width = window.innerWidth + 'px'; canvas.style.height = window.innerHeight + 'px'; ctx.setTransform(1,0,0,1,0,0); ctx.scale(devicePixelRatio, devicePixelRatio); } resize(); window.addEventListener('resize', resize); // Audio setup - Web Audio API const AudioCtx = window.AudioContext || window.webkitAudioContext; const audioCtx = new AudioCtx(); // We'll create simple synth voices for sounds based on shape function playTone(freq, type = 'sine', duration = 0.2, volume = 0.12, pan = 0) { const osc = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); const panNode = audioCtx.createStereoPanner(); osc.type = type; osc.frequency.value = freq; gainNode.gain.setValueAtTime(volume, audioCtx.currentTime); panNode.pan.value = pan; osc.connect(gainNode); gainNode.connect(panNode); panNode.connect(audioCtx.destination); osc.start(); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration); osc.stop(audioCtx.currentTime + duration); osc.onended = () => { osc.disconnect(); gainNode.disconnect(); panNode.disconnect(); }; } // For chaotically random frequency and behavior, map some values randomly function randomChoice(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function lerp(a,b,t) { return a + (b - a) * t; } function clamp(x, min, max) { return x < min ? min : x > max ? max : x; } const shapes = ['circle', 'line', 'spline']; const waveTypes = ['sine', 'triangle', 'square', 'sawtooth']; let chaosMode = false; // Shape class (abstract) class Shape { constructor(x, y, dx, dy, size, color, shapeType, soundFreq, audioType) { this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.size = size; this.color = color; this.alpha = 1; this.shapeType = shapeType; this.soundFreq = soundFreq; this.audioType = audioType; this.echoes = []; this.echoMax = 3 + Math.floor(size/15); this.timeToFade = 3.5 + size*0.025; // seconds this.birthTime = performance.now() / 1000; this.lastEchoTime = 0; this.echoInterval = 0.15; this.alive = true; this.angle = Math.atan2(dy, dx); // Random vector for echoes this.echoVectors = []; } update(delta) { const now = performance.now() / 1000; // Fade out alpha linearly const age = now - this.birthTime; this.alpha = clamp(1 - age / this.timeToFade, 0, 1); if (this.alpha <= 0) { this.alive = false; } // Periodically generate echoes if (age - this.lastEchoTime > this.echoInterval && this.echoes.length < this.echoMax) { this.lastEchoTime = age; const echoSize = this.size * lerp(0.4, 0.7, Math.random()); const echoAlpha = this.alpha * lerp(0.3,0.7,Math.random()); const angle = Math.random() * Math.PI * 2; const speed = lerp(8, 20, Math.random()); const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; this.echoes.push(new Echo(this.x, this.y, vx, vy, echoSize, this.color, this.shapeType, echoAlpha)); } // Update echoes this.echoes = this.echoes.filter(e => e.alive); this.echoes.forEach(e => e.update(delta)); } draw(ctx) { ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.save(); ctx.globalAlpha = this.alpha; ctx.fillStyle = this.color; ctx.strokeStyle = this.color; ctx.lineWidth = clamp(this.size / 12, 1, 5); switch(this.shapeType) { case 'circle': ctx.beginPath(); ctx.arc(this.x, this.y, this.size/2, 0, Math.PI*2); ctx.fill(); break; case 'line': ctx.beginPath(); // line oriented along velocity vector const len = this.size; const ax = this.x - Math.cos(this.angle) * len * 0.5; const ay = this.y - Math.sin(this.angle) * len * 0.5; const bx = this.x + Math.cos(this.angle) * len * 0.5; const by = this.y + Math.sin(this.angle) * len * 0.5; ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke(); break; case 'spline': // Random bezier-like curve near (x,y) ctx.beginPath(); ctx.moveTo(this.x, this.y); let cp1x = this.x + this.dx*3 + (Math.random()-0.5)*this.size; let cp1y = this.y + this.dy*3 + (Math.random()-0.5)*this.size; let cp2x = this.x - this.dy*3 + (Math.random()-0.5)*this.size; let cp2y = this.y + this.dx*3 + (Math.random()-0.5)*this.size; let endx = this.x + (this.dx + Math.random()*2 -1)*this.size*0.7; let endy = this.y + (this.dy + Math.random()*2 -1)*this.size*0.7; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endx, endy); ctx.lineWidth = clamp(this.size / 8, 1, 6); ctx.stroke(); break; default: // fallback circle ctx.beginPath(); ctx.arc(this.x, this.y, this.size/2, 0, Math.PI*2); ctx.fill(); } ctx.restore(); // Draw echoes this.echoes.forEach(e => e.draw(ctx)); } } // Echo smaller fading shapes class Echo { constructor(x, y, vx, vy, size, color, shapeType, alpha) { this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.size = size; this.color = color; this.shapeType = shapeType; this.alpha = alpha; this.birthTime = performance.now() / 1000; this.lifetime = 1.4 + this.size * 0.06; this.alive = true; } update(delta) { this.x += this.vx * delta; this.y += this.vy * delta; // friction slows velocity this.vx *= 0.94; this.vy *= 0.94; const age = (performance.now() / 1000) - this.birthTime; this.alpha = clamp(this.alpha - delta / this.lifetime, 0, 1); if (this.alpha <= 0) this.alive = false; } draw(ctx) { ctx.save(); ctx.globalAlpha = this.alpha; ctx.fillStyle = this.color; ctx.strokeStyle = this.color; ctx.lineWidth = clamp(this.size / 15, 0.8, 3); switch(this.shapeType) { case 'circle': ctx.beginPath(); ctx.arc(this.x, this.y, this.size/2, 0, Math.PI*2); ctx.fill(); break; case 'line': ctx.beginPath(); const len = this.size; let angle = Math.atan2(this.vy, this.vx); let ax = this.x - Math.cos(angle) * len * 0.5; let ay = this.y - Math.sin(angle) * len * 0.5; let bx = this.x + Math.cos(angle) * len * 0.5; let by = this.y + Math.sin(angle) * len * 0.5; ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke(); break; case 'spline': ctx.beginPath(); ctx.moveTo(this.x, this.y); let cp1x = this.x + (Math.random()-0.5)*this.size; let cp1y = this.y + (Math.random()-0.5)*this.size; let cp2x = this.x + (Math.random()-0.5)*this.size; let cp2y = this.y + (Math.random()-0.5)*this.size; let endx = this.x + (this.vx + (Math.random()-0.5)*0.8) * this.size * 0.8; let endy = this.y + (this.vy + (Math.random()-0.5)*0.8) * this.size * 0.8; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endx, endy); ctx.lineWidth = clamp(this.size / 20, 0.8, 2); ctx.stroke(); break; default: ctx.beginPath(); ctx.arc(this.x, this.y, this.size/2, 0, Math.PI*2); ctx.fill(); } ctx.restore(); } } // Tracking user mouse movements and speed/direction let lastPos = null; let lastTime = null; let moving = false; const activeShapes = []; // Color palettes const basePalette = [ "#ff4466", "#44ffbb", "#6688ff", "#ffaa33", "#dd44ff", "#44ddff", ]; // Chaos mode full random palette generator function randomColor() { // HSL random with saturation and brightness modulated in chaos if (chaosMode) { const h = Math.floor(Math.random() * 360); const s = lerp(50,100, Math.random()); const l = lerp(45,75, Math.random()); return `hsl(${h},${s}%,${l}%)` } else { return basePalette[Math.floor(Math.random()*basePalette.length)]; } } // Map position to frequency: x from left to right = pitch; y to modulate timbre somewhat. function positionToFreq(x, y) { // Map x (0 to w) to freq between 100 to ~1000Hz const baseFreq = lerp(100, 900, clamp(x/w,0,1)); // y modulates small freq shift or wave type shift // We'll just return baseFreq for now (wave type modulated separately) return baseFreq; } // Map speed to size function speedToSize(speed) { return clamp(speed * 8, 10, 60); } // Pan stereo based on horizontal position (-1 left, 1 right) function positionToPan(x) { return lerp(-1, 1, clamp(x/w,0,1)); } // Map direction to wave type (angle divided in quadrants) function angleToWaveType(angle) { if (chaosMode) return randomChoice(waveTypes); let deg = angle * 180/Math.PI; if (deg < 0) deg += 360; if (deg < 90) return 'sine'; else if (deg < 180) return 'triangle'; else if (deg < 270) return 'square'; else return 'sawtooth'; } // On mousemove/touchmove function pointerMove(x, y) { const now = performance.now(); if (lastPos && lastTime) { const dt = (now - lastTime) / 1000; if (dt < 0.04 && dt > 0) { const dx = x - lastPos.x; const dy = y - lastPos.y; const dist = Math.sqrt(dx*dx + dy*dy); const speed = dist / dt; if (speed > 15) { // create shape let shapeType = chaosMode ? randomChoice(shapes) : shapes[ (Math.floor((now/100) % shapes.length)) ]; let size = chaosMode ? speedToSize(speed) * lerp(0.5, 1.8, Math.random()) : speedToSize(speed); if(size < 12) size = 12; const color = randomColor(); const angle = Math.atan2(dy, dx); // Frequency and wave type based on position and motion angle const freq = chaosMode ? lerp(200,1500, Math.random()) : positionToFreq(x, y); const pan = chaosMode ? lerp(-1,1, Math.random()) : positionToPan(x); const waveType = chaosMode ? randomChoice(waveTypes) : angleToWaveType(angle); const shape = new Shape(x, y, dx/dt, dy/dt, size, color, shapeType, freq, waveType); shape.alive = true; activeShapes.push(shape); // Play sound, small random jitter in freq & duration if chaos playTone(freq * lerp(0.85, 1.15, Math.random()), waveType, lerp(0.12, 0.25, Math.random()), 0.1, pan); } } } lastPos = { x, y }; lastTime = now; } // Touch support normalized function onMouseMove(e) { // prevent simultaneous mouse+touch for mobile if(e.pointerType && e.pointerType !== 'mouse') return; pointerMove(e.clientX, e.clientY); } function onTouchMove(e) { e.preventDefault(); if(e.touches.length > 0) { const t = e.touches[0]; pointerMove(t.clientX, t.clientY); } } // For pointer events (better unified than mouse/touch) function onPointerMove(e) { pointerMove(e.clientX, e.clientY); } // Animation let lastFrame = performance.now(); function animate() { const now = performance.now(); const delta = (now - lastFrame) / 1000; lastFrame = now; // Very dark translucent fill - creates smooth fading trails ctx.fillStyle = chaosMode ? 'rgba(16,16,16,0.14)' : 'rgba(0,0,0,0.09)'; ctx.fillRect(0,0,w / devicePixelRatio, h / devicePixelRatio); // Update and draw shapes activeShapes.forEach(shape => { shape.update(delta); shape.draw(ctx); }); // Remove dead shapes for(let i = activeShapes.length-1; i >= 0; i--) { if(!activeShapes[i].alive) activeShapes.splice(i, 1); } requestAnimationFrame(animate); } animate(); // Chaos mode toggle const toggleBtn = document.getElementById('toggleChaos'); toggleBtn.addEventListener('click', () => { chaosMode = !chaosMode; toggleBtn.textContent = 'Chaos Mode: ' + (chaosMode ? 'ON' : 'OFF'); toggleBtn.setAttribute('aria-pressed', chaosMode); }); // Enable interaction to resume AudioContext on some browsers (mobile/Chrome) function resumeAudio() { if(audioCtx.state === 'suspended') { audioCtx.resume(); } window.removeEventListener('pointerdown', resumeAudio); } window.addEventListener('pointerdown', resumeAudio, { once: true }); // Pointer events setup window.addEventListener('pointermove', onPointerMove, { passive: true }); })(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sound Canvas</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #0a0a0a; overflow: hidden; font-family: Arial, sans-serif; } #canvas { cursor: crosshair; display: block; } #controls { position: fixed; top: 20px; left: 20px; z-index: 1000; display: flex; gap: 10px; align-items: center; } .control-btn { padding: 10px 20px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.3); color: white; cursor: pointer; border-radius: 5px; font-size: 14px; transition: all 0.3s; } .control-btn:hover { background: rgba(255, 255, 255, 0.2); } .control-btn.active { background: rgba(100, 255, 100, 0.3); border-color: rgba(100, 255, 100, 0.6); } #info { position: fixed; bottom: 20px; left: 20px; color: rgba(255, 255, 255, 0.6); font-size: 12px; z-index: 1000; } </style> </head> <body> <canvas id="canvas"></canvas> <div id="controls"> <button id="chaosToggle" class="control-btn">Chaos Mode: OFF</button> <button id="clearCanvas" class="control-btn">Clear</button> <button id="muteToggle" class="control-btn">Mute: OFF</button> </div> <div id="info"> Move your mouse to paint with sound and light </div> <script> class SoundCanvas { constructor() { this.canvas = document.getElementById('canvas'); this.ctx = this.canvas.getContext('2d'); this.shapes = []; this.echoes = []; this.mousePos = { x: 0, y: 0 }; this.lastMousePos = { x: 0, y: 0 }; this.mouseSpeed = 0; this.chaosMode = false; this.muted = false; // Audio setup this.audioContext = null; this.initAudio(); this.resize(); this.setupEventListeners(); this.animate(); } initAudio() { try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { console.log('Audio not supported'); } } playSound(frequency, duration = 0.1, type = 'sine') { if (!this.audioContext || this.muted) return; try { const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); oscillator.type = type; oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); oscillator.start(); oscillator.stop(this.audioContext.currentTime + duration); } catch (e) { console.log('Audio playback failed'); } } resize() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; } setupEventListeners() { window.addEventListener('resize', () => this.resize()); this.canvas.addEventListener('mousemove', (e) => { this.lastMousePos = { ...this.mousePos }; this.mousePos = { x: e.clientX, y: e.clientY }; const dx = this.mousePos.x - this.lastMousePos.x; const dy = this.mousePos.y - this.lastMousePos.y; this.mouseSpeed = Math.sqrt(dx * dx + dy * dy); this.createShape(); }); // Handle touch events for mobile this.canvas.addEventListener('touchmove', (e) => { e.preventDefault(); const touch = e.touches[0]; this.lastMousePos = { ...this.mousePos }; this.mousePos = { x: touch.clientX, y: touch.clientY }; const dx = this.mousePos.x - this.lastMousePos.x; const dy = this.mousePos.y - this.lastMousePos.y; this.mouseSpeed = Math.sqrt(dx * dx + dy * dy); this.createShape(); }); document.getElementById('chaosToggle').addEventListener('click', () => { this.chaosMode = !this.chaosMode; const btn = document.getElementById('chaosToggle'); btn.textContent = `Chaos Mode: ${this.chaosMode ? 'ON' : 'OFF'}`; btn.classList.toggle('active', this.chaosMode); }); document.getElementById('clearCanvas').addEventListener('click', () => { this.shapes = []; this.echoes = []; }); document.getElementById('muteToggle').addEventListener('click', () => { this.muted = !this.muted; const btn = document.getElementById('muteToggle'); btn.textContent = `Mute: ${this.muted ? 'ON' : 'OFF'}`; btn.classList.toggle('active', this.muted); }); // Initialize audio context on first user interaction document.addEventListener('click', () => { if (this.audioContext && this.audioContext.state === 'suspended') { this.audioContext.resume(); } }, { once: true }); } createShape() { const speed = Math.min(this.mouseSpeed, 50); const size = this.chaosMode ? Math.random() * 100 + 10 : Math.max(speed * 2, 5); const hue = this.chaosMode ? Math.random() * 360 : (this.mousePos.x / this.canvas.width * 360 + Date.now() * 0.01) % 360; const saturation = this.chaosMode ? Math.random() * 100 + 50 : Math.min(speed * 2 + 50, 100); const lightness = this.chaosMode ? Math.random() * 50 + 50 : Math.max(70 - speed, 30); const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`; const shapeType = this.chaosMode ? Math.floor(Math.random() * 4) : Math.floor(speed / 10) % 4; const shape = { x: this.mousePos.x, y: this.mousePos.y, size: size, color: color, alpha: 1, type: shapeType, age: 0, maxAge: this.chaosMode ? Math.random() * 200 + 50 : 150, rotation: this.chaosMode ? Math.random() * Math.PI * 2 : 0, rotationSpeed: this.chaosMode ? (Math.random() - 0.5) * 0.2 : 0 }; this.shapes.push(shape); // Create sound const frequency = this.chaosMode ? Math.random() * 800 + 200 : 200 + (this.mousePos.y / this.canvas.height * 600) + (speed * 10); const waveType = this.chaosMode ? ['sine', 'square', 'sawtooth', 'triangle'][Math.floor(Math.random() * 4)] : 'sine'; this.playSound(frequency, 0.05 + speed * 0.002, waveType); // Create echoes if (Math.random() < 0.3 || this.chaosMode) { this.createEchoes(shape); } } createEchoes(originalShape) { const numEchoes = this.chaosMode ? Math.floor(Math.random() * 8) + 2 : 3; for (let i = 0; i < numEchoes; i++) { const echo = { x: originalShape.x + (Math.random() - 0.5) * 50, y: originalShape.y + (Math.random() - 0.5) * 50, size: originalShape.size * (0.3 + Math.random() * 0.4), color: originalShape.color, alpha: 0.3 + Math.random() * 0.3, type: originalShape.type, age: 0, maxAge: this.chaosMode ? Math.random() * 300 + 100 : 200, vx: (Math.random() - 0.5) * (this.chaosMode ? 4 : 2), vy: (Math.random() - 0.5) * (this.chaosMode ? 4 : 2), rotation: originalShape.rotation, rotationSpeed: originalShape.rotationSpeed }; this.echoes.push(echo); } } drawShape(shape) { this.ctx.save(); this.ctx.globalAlpha = shape.alpha; this.ctx.fillStyle = shape.color; this.ctx.strokeStyle = shape.color; this.ctx.lineWidth = 2; this.ctx.translate(shape.x, shape.y); this.ctx.rotate(shape.rotation); switch (shape.type) { case 0: // Circle this.ctx.beginPath(); this.ctx.arc(0, 0, shape.size / 2, 0, Math.PI * 2); this.ctx.fill(); break; case 1: // Square this.ctx.fillRect(-shape.size / 2, -shape.size / 2, shape.size, shape.size); break; case 2: // Triangle this.ctx.beginPath(); this.ctx.moveTo(0, -shape.size / 2); this.ctx.lineTo(-shape.size / 2, shape.size / 2); this.ctx.lineTo(shape.size / 2, shape.size / 2); this.ctx.closePath(); this.ctx.fill(); break; case 3: // Star this.drawStar(shape.size / 2); break; } this.ctx.restore(); } drawStar(radius) { this.ctx.beginPath(); for (let i = 0; i < 10; i++) { const angle = (i * Math.PI) / 5; const r = i % 2 === 0 ? radius : radius * 0.5; const x = Math.cos(angle) * r; const y = Math.sin(angle) * r; if (i === 0) { this.ctx.moveTo(x, y); } else { this.ctx.lineTo(x, y); } } this.ctx.closePath(); this.ctx.fill(); } updateShapes() { // Update main shapes this.shapes = this.shapes.filter(shape => { shape.age++; shape.rotation += shape.rotationSpeed; shape.alpha = 1 - (shape.age / shape.maxAge); if (this.chaosMode) { shape.x += (Math.random() - 0.5) * 2; shape.y += (Math.random() - 0.5) * 2; } return shape.age < shape.maxAge; }); // Update echoes this.echoes = this.echoes.filter(echo => { echo.age++; echo.x += echo.vx; echo.y += echo.vy; echo.rotation += echo.rotationSpeed; echo.alpha = (0.6 - (echo.age / echo.maxAge)) * 0.5; if (this.chaosMode) { echo.vx += (Math.random() - 0.5) * 0.5; echo.vy += (Math.random() - 0.5) * 0.5; } return echo.age < echo.maxAge && echo.alpha > 0; }); } animate() { this.ctx.fillStyle = 'rgba(10, 10, 10, 0.1)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.updateShapes(); // Draw echoes first (background) this.echoes.forEach(echo => this.drawShape(echo)); // Draw main shapes (foreground) this.shapes.forEach(shape => this.drawShape(shape)); requestAnimationFrame(() => this.animate()); } } // Initialize the canvas when the page loads window.addEventListener('load', () => { new SoundCanvas(); }); </script> </body> </html> ``` This interactive sound canvas creates an immersive audio-visual experience with the following features: **Core Functionality:** - Mouse movements generate dynamic shapes with colors and sizes based on movement speed - Each shape triggers unique tones based on position and properties - Shapes fade over time while their "echoes" drift randomly before disappearing - Full responsive design that adapts to any screen size **Interactive Elements:** - **Chaos Mode**: Randomizes all parameters for wild, unpredictable patterns - **Clear Button**: Instantly clears all shapes and echoes - **Mute Toggle**: Disables sound while keeping visuals **Visual Features:** - 4 different shape types (circles, squares, triangles, stars) - Dynamic colors based on mouse position and speed - Smooth alpha blending and rotation effects - Echo system creates trailing visual effects **Audio System:** - Web Audio API for real-time sound generation - Frequency mapped to mouse Y position and movement speed - Different waveforms in chaos mode (sine, square, sawtooth, triangle) - Touch device compatibility The experience feels like painting with light and sound, where every gesture creates both visual and auditory feedback that evolves and fades naturally, creating an organic, meditative interaction.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Painting with Echoes</title> <style> /* Make the canvas full-screen and responsive */ canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; padding: 0; margin: 0; } /* Add some basic styling to the chaos mode toggle */ #chaos-mode-toggle { position: absolute; top: 20px; left: 20px; font-size: 24px; cursor: pointer; } </style> </head> <body> <canvas id="canvas"></canvas> <div id="chaos-mode-toggle">Toggle Chaos Mode</div> <script> // Get the canvas and context const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Set the canvas to be full-screen canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Define some constants const SHAPE_SIZE = 50; const ECHO_SIZE = 10; const ECHO_COUNT = 10; const CHAOS_MODE = false; // Define the shapes and their properties const shapes = []; // Define the sounds const sounds = []; for (let i = 0; i < 10; i++) { sounds.push(new AudioContext()); } // Function to generate a random color function getRandomColor() { const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); return `rgba(${r}, ${g}, ${b}, 1)`; } // Function to generate a random shape function getRandomShape(x, y) { const type = Math.floor(Math.random() * 3); let shape; switch (type) { case 0: shape = { type: 'circle', x: x, y: y, radius: SHAPE_SIZE, color: getRandomColor() }; break; case 1: shape = { type: 'line', x1: x, y1: y, x2: x + SHAPE_SIZE, y2: y + SHAPE_SIZE, color: getRandomColor() }; break; case 2: shape = { type: 'spline', x: x, y: y, controlX: x + SHAPE_SIZE, controlY: y + SHAPE_SIZE, endX: x + SHAPE_SIZE * 2, endY: y + SHAPE_SIZE * 2, color: getRandomColor() }; break; } return shape; } // Function to draw a shape function drawShape(shape) { ctx.beginPath(); switch (shape.type) { case 'circle': ctx.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI); break; case 'line': ctx.moveTo(shape.x1, shape.y1); ctx.lineTo(shape.x2, shape.y2); break; case 'spline': ctx.moveTo(shape.x, shape.y); ctx.quadraticCurveTo(shape.controlX, shape.controlY, shape.endX, shape.endY); break; } ctx.strokeStyle = shape.color; ctx.lineWidth = 2; ctx.stroke(); } // Function to play a sound function playSound(x, y) { const sound = sounds[Math.floor(Math.random() * sounds.length)]; const osc = sound.createOscillator(); const gain = sound.createGain(); osc.type = 'sine'; osc.frequency.value = x + y; gain.gain.value = 0.1; osc.connect(gain); gain.connect(sound.destination); osc.start(); setTimeout(() => { osc.stop(); }, 1000); } // Function to update the shapes function update() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = shapes.length - 1; i >= 0; i--) { const shape = shapes[i]; drawShape(shape); shape.radius -= 0.1; if (shape.radius < 0) { shapes.splice(i, 1); } } requestAnimationFrame(update); } // Function to handle mouse movement function handleMouseMove(event) { const x = event.clientX; const y = event.clientY; const shape = getRandomShape(x, y); shapes.push(shape); playSound(x, y); for (let i = 0; i < ECHO_COUNT; i++) { const echo = { type: shape.type, x: x + Math.random() * ECHO_SIZE - ECHO_SIZE / 2, y: y + Math.random() * ECHO_SIZE - ECHO_SIZE / 2, radius: ECHO_SIZE, color: shape.color }; shapes.push(echo); } } // Add event listener for mouse movement canvas.addEventListener('mousemove', handleMouseMove); // Add event listener for chaos mode toggle document.getElementById('chaos-mode-toggle').addEventListener('click', () => { CHAOS_MODE = !CHAOS_MODE; if (CHAOS_MODE) { document.getElementById('chaos-mode-toggle').style.color = 'red'; } else { document.getElementById('chaos-mode-toggle').style.color = 'black'; } }); // Start the animation update(); </script> </body> </html> ```
# Interactive Audio-Visual Canvas Experience I'll create a full-screen interactive canvas where mouse movements generate visual patterns synchronized with dynamic sounds, including fading echoes and chaos mode. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sound Light Canvas</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; background: #000; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; height: 100vh; width: 100vw; } canvas { position: fixed; top: 0; left: 0; z-index: 1; } .ui-container { position: fixed; top: 0; left: 0; z-index: 10; padding: 20px; display: flex; gap: 15px; flex-wrap: wrap; } .control-btn { background: rgba(0, 0, 0, 0.6); color: white; border: 1px solid rgba(255, 255, 255, 0.3); padding: 12px 24px; border-radius: 30px; font-size: 16px; cursor: pointer; backdrop-filter: blur(5px); transition: all 0.3s ease; display: flex; align-items: center; gap: 8px; } .control-btn:hover { background: rgba(50, 50, 50, 0.7); } .control-btn.active { background: rgba(100, 0, 150, 0.7); border-color: rgba(200, 50, 255, 0.5); box-shadow: 0 0 20px rgba(175, 50, 255, 0.6); } .instruction { position: fixed; bottom: 40px; left: 0; width: 100%; text-align: center; color: rgba(255, 255, 255, 0.7); font-size: 18px; z-index: 10; padding: 0 20px; background: rgba(0, 0, 0, 0.4); padding: 15px; backdrop-filter: blur(3px); } .title { position: fixed; top: 40px; right: 40px; color: white; font-size: 3rem; font-weight: 700; text-transform: uppercase; letter-spacing: 6px; text-align: right; text-shadow: 0 0 15px rgba(135, 60, 255, 0.8); z-index: 5; line-height: 1.1; } @media (max-width: 768px) { .title { font-size: 2rem; top: 20px; right: 20px; } .ui-container { padding: 10px; } .control-btn { padding: 10px 18px; font-size: 14px; } } </style> </head> <body> <canvas id="canvas"></canvas> <div class="title">Sound Light<br>Canvas</div> <div class="ui-container"> <button id="clearButton" class="control-btn">🧹 Clear Canvas</button> <button id="chaosButton" class="control-btn">🌀 Normal</button> <button id="soundButton" class="control-btn active">🔊 Sound On</button> <button id="infoButton" class="control-btn">❓ Info</button> </div> <div id="infoPanel" class="instruction" style="display: none;"> Move your mouse across the canvas to create unique visual patterns synchronized with sound<br> <br> Each movement creates shapes whose color and size are determined by speed and direction<br><br> Shapes fade over time and spawn smaller "echo" visuals that drift away<br><br> Toggle Chaos Mode for a truly unpredictable experience! </div> <div id="instruction" class="instruction"> Move your mouse/touch to create visual patterns with synchronized sounds... </div> <script> // Canvas setup const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // UI elements const clearButton = document.getElementById('clearButton'); const chaosButton = document.getElementById('chaosButton'); const soundButton = document.getElementById('soundButton'); const infoButton = document.getElementById('infoButton'); const infoPanel = document.getElementById('infoPanel'); // Set canvas to full window size function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); // State variables let chaosMode = false; let soundEnabled = true; let lastPoint = { x: 0, y: 0 }; let lastTimestamp = 0; let audioContext; let particles = []; // Web Audio Setup function initAudio() { // Create audio context only when needed if (!audioContext && soundEnabled) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } } // Particle types const SHAPE_TYPES = ['circle', 'triangle', 'square', 'line', 'ring', 'burst']; // Particle system class Particle { constructor(x, y, velocity, angle, timestamp, chaos) { this.x = x; this.y = y; this.velocity = Math.min(velocity * 0.1, 10); this.angle = angle; this.size = Math.min(5 + velocity * 0.05, 60); this.life = 1.0; this.branching = chaosMode ? 6 : 3; // Color based on velocity and angle this.hue = (Math.abs(angle * 180 / Math.PI) % 360) + (velocity * 0.5); this.saturation = 50 + (velocity * 0.1); this.lightness = 50; this.alpha = 0.7; // Shape type this.shape = chaosMode ? SHAPE_TYPES[Math.floor(Math.random() * SHAPE_TYPES.length)] : SHAPE_TYPES[Math.floor(Math.random() * 3)]; // If chaos mode is on, randomize many properties if (chaos) { this.size *= (0.5 + Math.random() * 1.5); this.hue = Math.random() * 360; this.saturation = 30 + Math.random() * 50; this.angularVelocity = (Math.random() - 0.5) * 0.05; this.driftAngle = Math.random() * Math.PI * 2; } else { this.angularVelocity = 0; this.driftAngle = angle; } this.driftSpeed = Math.random() * 0.5; } update() { this.life -= 0.01; this.driftAngle += this.angularVelocity; // Move the particle based on its drift angle this.x += Math.cos(this.driftAngle) * this.driftSpeed; this.y += Math.sin(this.driftAngle) * this.driftSpeed; // Scale down over time this.size *= 0.98; // Check if particle is still alive or should spawn echoes if (this.life <= 0) { if (this.branching > 0) { this.spawnEchoes(); return false; // Original particle dies } return false; } return true; } spawnEchoes() { // Spawn smaller particles (echoes) const echoCount = chaosMode ? Math.floor(3 + Math.random() * 5) : 3; for (let i = 0; i < echoCount; i++) { const velocity = this.velocity * (0.3 + Math.random() * 0.7); const angle = i * (Math.PI * 2 / echoCount); const echo = new Particle( this.x, this.y, velocity, angle, Date.now(), chaosMode ); // Make echoes smaller and transparent echo.size = this.size * (0.3 + Math.random() * 0.2); echo.life = this.life * (0.5 + Math.random() * 0.5); echo.branching = Math.max(0, this.branching - 1); echo.alpha = 0.3; particles.push(echo); } } draw(ctx) { const color = `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha * this.life})`; ctx.fillStyle = color; ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.save(); ctx.translate(this.x, this.y); switch (this.shape) { case 'circle': ctx.beginPath(); ctx.arc(0, 0, this.size/2, 0, Math.PI * 2); ctx.fill(); break; case 'triangle': ctx.beginPath(); ctx.moveTo(0, -this.size/2); ctx.lineTo(-this.size/2, this.size/2); ctx.lineTo(this.size/2, this.size/2); ctx.closePath(); ctx.fill(); break; case 'square': ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size); break; case 'line': ctx.beginPath(); ctx.moveTo(-this.size/2, 0); ctx.lineTo(this.size/2, 0); ctx.stroke(); break; case 'ring': ctx.beginPath(); ctx.arc(0, 0, this.size/2, 0, Math.PI * 2); ctx.stroke(); break; case 'burst': ctx.beginPath(); for (let i = 0; i < 6; i++) { const angle = i * Math.PI/3; ctx.moveTo(0, 0); ctx.lineTo(Math.cos(angle) * this.size/2, Math.sin(angle) * this.size/2); } ctx.stroke(); break; } ctx.restore(); } } // Play a tone based on particle position and velocity function playTone(x, y, velocity, angle) { if (!soundEnabled || !audioContext) return; const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); // Configure tone based on position and movement const pitch = (x / window.innerWidth) * 800 + 100; // 100-900Hz range const speedFactor = Math.min(1.0, velocity / 100); oscillator.type = chaosMode ? ['sine', 'square', 'triangle', 'sawtooth'][Math.floor(Math.random() * 4)] : (velocity > 100 ? 'square' : 'sine'); oscillator.frequency.setValueAtTime(pitch, audioContext.currentTime); // Create stereo effect based on position const panner = audioContext.createStereoPanner(); panner.pan.setValueAtTime((x / window.innerWidth) * 2 - 1, audioContext.currentTime); // Volume based on particle size and velocity gainNode.gain.setValueAtTime(0.2 * Math.min(1.0, velocity * 0.05), audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.5); oscillator.connect(panner); panner.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); } // Handle mouse/touch movement function handleInteraction(x, y) { // Initialize audio on first interaction initAudio(); const now = Date.now(); const timeDiff = now - lastTimestamp; // Calculate movement direction and velocity const deltaX = x - lastPoint.x; const deltaY = y - lastPoint.y; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const velocity = timeDiff > 0 ? distance / (timeDiff / 1000) : 0; // pixels per second const angle = Math.atan2(deltaY, deltaX); // Create new particle if movement detected if (distance > 1 && velocity > 1) { particles.push(new Particle(x, y, velocity, angle, now, chaosMode)); playTone(x, y, velocity, angle); } lastPoint = {x, y}; lastTimestamp = now; } // Event listeners canvas.addEventListener('mousemove', e => { handleInteraction(e.clientX, e.clientY); }); canvas.addEventListener('touchmove', e => { e.preventDefault(); if (e.touches.length > 0) { handleInteraction(e.touches[0].clientX, e.touches[0].clientY); } }); // Control handlers clearButton.addEventListener('click', () => { particles = []; }); chaosButton.addEventListener('click', () => { chaosMode = !chaosMode; chaosButton.textContent = chaosMode ? '🌀 Chaos Mode' : '🌀 Normal'; chaosButton.classList.toggle('active', chaosMode); }); soundButton.addEventListener('click', () => { soundEnabled = !soundEnabled; soundButton.classList.toggle('active', soundEnabled); soundButton.textContent = soundEnabled ? '🔊 Sound On' : '🔇 Sound Off'; // Typewriter effect info instruction document.getElementById('instruction').textContent = soundEnabled ? "Sound enabled! Move your mouse to play..." : "Silent mode activated. Draw quietly..."; }); infoButton.addEventListener('click', () => { infoPanel.style.display = infoPanel.style.display === 'block' ? 'none' : 'block'; }); // Animation loop function animate() { // Clear canvas with a trailing effect ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Update and draw particles for (let i = particles.length - 1; i >= 0; i--) { if (particles[i].update()) { particles[i].draw(ctx); } else { particles.splice(i, 1); } } requestAnimationFrame(animate); } // Start the animation animate(); </script> </body> </html> ``` ## Features of this Interactive Canvas: 1. **Sound-Synced Visuals**: Creates geometric shapes that trigger unique tones based on movement speed and position 2. **Dynamic Echo System**: Each shape spawns smaller, transparent echoes that drift away before fading 3. **Chaos Mode**: When activated, randomizes shape types, colors, movement patterns, and sounds 4. **Full Responsiveness**: Adapts seamlessly to any device or screen size 5. **Multiple Shape Types**: Circles, triangles, squares, lines, rings, and bursts 6. **Audio Controls**: Toggle sound on/off based on preference 7. **Interactive UI**: Clean controls with clear visual state indicators Move your mouse across the canvas to create evolving patterns that produce chord progressions based on your movement patterns. The chaos mode toggle will transform this into a completely unpredictable audio-visual experience!
**Interactive Web Canvas with Dynamic Visual Patterns and Sound** ================================================================= Below is a basic implementation of the interactive web canvas using HTML, CSS, and JavaScript. This example uses the HTML5 Canvas API for rendering and the Web Audio API for sound generation. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Echo Canvas</title> <style> /* Make the canvas full-screen and responsive */ body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; } #canvas { width: 100%; height: 100%; display: block; } </style> </head> <body> <canvas id="canvas"></canvas> <button id="chaos-mode-toggle">Chaos Mode</button> <script> // Get the canvas and its context const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Set the canvas to full-screen and responsive canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Define some colors const colors = ['#ff69b4', '#33cc33', '#6666ff', '#cc3333', '#cccc33']; // Define some shapes const shapes = ['circle', 'line', 'spline']; // Define some sounds const audioContext = new AudioContext(); const sounds = [ () => playTone(audioContext, 200), () => playTone(audioContext, 400), () => playTone(audioContext, 600), () => playChime(audioContext, [200, 400, 600]), () => playSynthWave(audioContext, 200) ]; // Initialize chaos mode let chaosMode = false; // Add event listener for chaos mode toggle document.getElementById('chaos-mode-toggle').addEventListener('click', () => { chaosMode = !chaosMode; if (chaosMode) { document.getElementById('chaos-mode-toggle').style.backgroundColor = 'green'; } else { document.getElementById('chaos-mode-toggle').style.backgroundColor = ''; } }); // Add event listener for mouse movement canvas.addEventListener('mousemove', (event) => { const x = event.clientX; const y = event.clientY; const speed = event.movementX + event.movementY; const direction = Math.atan2(event.movementY, event.movementX); // Draw a shape drawShape(ctx, x, y, speed, direction, chaosMode); // Play a sound playSound(sounds, x, y, speed, direction, chaosMode); }); // Function to draw a shape function drawShape(ctx, x, y, speed, direction, chaosMode) { const shape = chaosMode ? shapes[Math.floor(Math.random() * shapes.length)] : 'circle'; const color = chaosMode ? colors[Math.floor(Math.random() * colors.length)] : '#ffffff'; const size = chaosMode ? Math.random() * 50 + 10 : speed / 5 + 10; switch (shape) { case 'circle': ctx.beginPath(); ctx.arc(x, y, size, 0, 2 * Math.PI); ctx.fillStyle = color; ctx.fill(); break; case 'line': ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(direction) * size, y + Math.sin(direction) * size); ctx.strokeStyle = color; ctx.stroke(); break; case 'spline': ctx.beginPath(); ctx.moveTo(x, y); ctx.quadraticCurveTo(x + Math.cos(direction) * size, y + Math.sin(direction) * size, x + Math.cos(direction + Math.PI / 2) * size, y + Math.sin(direction + Math.PI / 2) * size); ctx.strokeStyle = color; ctx.stroke(); break; } // Create an echo createEcho(ctx, x, y, size, color, chaosMode); } // Function to create an echo function createEcho(ctx, x, y, size, color, chaosMode) { const echo = { x: x, y: y, size: size, color: color, vx: chaosMode ? Math.random() * 2 - 1 : 0, vy: chaosMode ? Math.random() * 2 - 1 : 0, alpha: 0.5 }; // Update and draw echoes function updateEchoes() { ctx.globalAlpha = echo.alpha; ctx.fillStyle = echo.color; ctx.beginPath(); ctx.arc(echo.x, echo.y, echo.size, 0, 2 * Math.PI); ctx.fill(); echo.x += echo.vx; echo.y += echo.vy; echo.size *= 0.9; echo.alpha *= 0.9; if (echo.alpha < 0.01) { return; } requestAnimationFrame(updateEchoes); } updateEchoes(); } // Function to play a sound function playSound(sounds, x, y, speed, direction, chaosMode) { const sound = chaosMode ? sounds[Math.floor(Math.random() * sounds.length)] : sounds[0]; sound(); } // Function to play a tone function playTone(audioContext, frequency) { const oscillator = audioContext.createOscillator(); oscillator.type = 'sine'; oscillator.frequency.value = frequency; oscillator.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); } // Function to play a chime function playChime(audioContext, frequencies) { frequencies.forEach((frequency) => { const oscillator = audioContext.createOscillator(); oscillator.type = 'sine'; oscillator.frequency.value = frequency; oscillator.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); }); } // Function to play a synth wave function playSynthWave(audioContext, frequency) { const oscillator = audioContext.createOscillator(); oscillator.type = 'sawtooth'; oscillator.frequency.value = frequency; oscillator.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); } </script> </body> </html> ``` This code creates a full-screen canvas that responds to mouse movements by drawing shapes and playing sounds. The shapes and sounds are influenced by the speed and direction of the mouse movement. The "chaos mode" toggle randomizes all parameters for a wilder experience. The echoes of the shapes linger and move in random directions before disappearing. **Example Use Case:** * Open the HTML file in a web browser. * Move the mouse around the canvas to create shapes and play sounds. * Click the "Chaos Mode" toggle to randomize the parameters. * Observe how the echoes of the shapes linger and move in random directions. Note that this is a basic implementation and can be improved and customized further.
```html <!DOCTYPE html> <html> <head> <title>Interactive Canvas with Sound</title> <style> body { margin: 0; overflow: hidden; /* Hide scrollbars */ background-color: #111; /* Dark background */ } canvas { display: block; width: 100vw; height: 100vh; } #controls { position: absolute; top: 20px; left: 20px; background-color: rgba(255, 255, 255, 0.2); padding: 10px; border-radius: 5px; } #chaosModeButton { background-color: #4CAF50; color: white; padding: 8px 12px; border: none; border-radius: 4px; cursor: pointer; } #chaosModeButton:hover { background-color: #3e8e41; } </style> </head> <body> <canvas id="canvas"></canvas> <div id="controls"> <button id="chaosModeButton">Chaos Mode</button> </div> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const chaosModeButton = document.getElementById('chaosModeButton'); let chaosMode = false; canvas.width = window.innerWidth; canvas.height = window.innerHeight; let shapes = []; const numShapes = 50; // Limit number of shapes to prevent performance issues let soundEnabled = true; // allow user to disable sound. // Audio context const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); // Function to generate a random float between min and max function randomFloat(min, max) { return Math.random() * (max - min) + min; } function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // Shape class class Shape { constructor(x, y, speedX, speedY) { this.x = x; this.y = y; this.speedX = speedX; this.speedY = speedY; this.size = randomFloat(10, 40); this.color = this.generateRandomColor(); this.alpha = 1; this.type = randomInt(0, 3); // 0: circle, 1: line, 2: spline, 3: square this.soundNode = null; } generateRandomColor() { const hue = randomFloat(0, 360); return `hsl(${hue}, ${randomFloat(50, 100)}%, ${randomFloat(50, 100)}%)`; } playShapeSound() { if (!soundEnabled) return; const oscillator = audioCtx.createOscillator(); oscillator.type = 'sine'; // You can experiment with other types oscillator.frequency.setValueAtTime(this.calculateFrequency(), audioCtx.currentTime); const gainNode = audioCtx.createGain(); gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); oscillator.start(); this.soundNode = oscillator; setTimeout(() => { if(this.soundNode){ this.soundNode.stop(); this.soundNode.disconnect(); this.soundNode = null; } }, 1000); } calculateFrequency() { // Frequency based on shape size and position. return 220 + (this.size * 5) + Math.sin(this.x * 0.02) * 100; // Example calculation } update() { this.x += this.speedX; this.y += this.speedY; // Bounce off the walls if (this.x + this.size > canvas.width || this.x - this.size < 0) { this.speedX = -this.speedX; } if (this.y + this.size > canvas.height || this.y - this.size < 0) { this.speedY = -this.speedY; } this.alpha -= 0.02; // Fade out if (this.alpha <= 0) { return false; // Remove shape } if (this.type === 0) { // Circle this.drawCircle(); } else if (this.type === 1) { // Line this.drawLine(); } else if (this.type === 2) { // Spline this.drawSpline(); } else { // Square this.drawSquare(); } return true; } drawCircle() { ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.closePath(); ctx.globalAlpha = 1; } drawLine() { ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.moveTo(this.x - this.size, this.y); ctx.lineTo(this.x + this.size, this.y); ctx.strokeStyle = this.color; ctx.lineWidth = 2; ctx.stroke(); ctx.closePath(); ctx.globalAlpha = 1; } drawSpline() { ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.moveTo(this.x - this.size, this.y); const controlPointX = this.x + randomFloat(-this.size, this.size); const controlPointY = this.y + randomFloat(-this.size, this.size); ctx.quadraticCurveTo(controlPointX, controlPointY, this.x + this.size, this.y); ctx.strokeStyle = this.color; ctx.lineWidth = 3; ctx.stroke(); ctx.closePath(); ctx.globalAlpha = 1; } drawSquare() { ctx.globalAlpha = this.alpha; ctx.fillStyle = this.color; ctx.fillRect(this.x - this.size, this.y - this.size, this.size * 2, this.size * 2); ctx.globalAlpha = 1; } } // Chaos mode toggle chaosModeButton.addEventListener('click', () => { chaosMode = !chaosMode; if (chaosMode) { chaosModeButton.textContent = 'Normal Mode'; } else { chaosModeButton.textContent = 'Chaos Mode'; } }); function init() { for (let i = 0; i < numShapes; i++) { shapes.push(new Shape( randomFloat(0, canvas.width), randomFloat(0, canvas.height), randomFloat(-2, 2), randomFloat(-2, 2) )); } } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); let newShapes = []; for (let i = 0; i < shapes.length; i++) { const shape = shapes[i]; if (shape.update()) { newShapes.push(shape); shape.playShapeSound(); } } shapes = newShapes; if (chaosMode) { // Randomly create new shapes if (Math.random() < 0.1) { shapes.push(new Shape( randomFloat(0, canvas.width), randomFloat(0, canvas.height), randomFloat(-5, 5), randomFloat(-5, 5) )); } } requestAnimationFrame(animate); } // Event listener for mouse movement canvas.addEventListener('mousemove', (event) => { if (chaosMode) return; // Don't add shapes in chaos mode const mouseX = event.clientX; const mouseY = event.clientY; const speedX = (mouseX - canvas.width / 2) * 0.02; const speedY = (mouseY - canvas.height / 2) * 0.02; shapes.push(new Shape( mouseX, mouseY, speedX, speedY )); }); // Resize canvas on window resize window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }); canvas.addEventListener('click', () => { soundEnabled = !soundEnabled; if (soundEnabled) { console.log("Sound Enabled"); } else { console.log("Sound Disabled"); } }); init(); animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Echoes of Sound and Light</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; } body { background: #181a1b; width: 100vw; height: 100vh; position: relative; user-select: none; touch-action: none; } #chaosBtn { position: fixed; top: 16px; right: 16px; z-index: 10; padding: 12px 28px; background: #232b2b; color: #fff; border: none; border-radius: 24px; font-size: 1.2em; cursor: pointer; opacity: 0.85; box-shadow: 0 3px 16px 0 rgba(0,0,0,0.2); transition: background 0.3s; } #chaosBtn.active { background: linear-gradient(90deg, #ff306d 0%, #4df7c8 100%); color: #181a1b; font-weight: bold; opacity: 0.96; box-shadow: 0 6px 32px 0 rgba(255,48,109,0.19); } canvas { display: block; position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; } </style> </head> <body> <button id="chaosBtn">Chaos Mode: Off</button> <canvas id="canvas"></canvas> <script> //---- Canvas Setup ---- const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let dpr = window.devicePixelRatio || 1; function resizeCanvas() { dpr = window.devicePixelRatio || 1; canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; canvas.style.width = window.innerWidth + "px"; canvas.style.height = window.innerHeight + "px"; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); //---- Globals ---- let shapes = []; let chaosMode = false; const chaosBtn = document.getElementById('chaosBtn'); // Utility: HSL color generator function hsl(h, s, l, a=1) { return `hsla(${h},${s}%,${l}%,${a})`; } //---- Sound Setup ---- const AudioCtx = window.AudioContext || window.webkitAudioContext; const audioCtx = new AudioCtx(); let lastSoundTime = 0; function playShapeSound(shape) { const now = audioCtx.currentTime; // Throttle: max 60 sounds per second if (now - lastSoundTime < 1/60) return; lastSoundTime = now; // Frequency is mapped from y position let y = shape.y / canvas.height; // Tone options let freq = 220 + (1-y) * 1600; // 220Hz-1820Hz let panVal = (shape.x / canvas.width) * 2 - 1; // -1 (L) - 1 (R) let duration = 0.15 + shape.size * 0.0035; // 0.15-0.45 s if (chaosMode) { freq += Math.random() * 1600 - 800; duration = 0.07 + Math.random() * 0.35; } // Random waveform for chaos, else synth or bell-like let type = chaosMode ? (["sine","square","triangle","sawtooth"])[Math.floor(Math.random()*4)] : (shape.shapeType === 'circle' ? 'triangle' : shape.shapeType === 'line' ? 'sine' : 'square'); // Main oscillator const osc = audioCtx.createOscillator(); osc.type = type; osc.frequency.value = freq; // Envelope const gain = audioCtx.createGain(); gain.gain.value = 0.0001; gain.gain.setValueAtTime(0.07, now); gain.gain.linearRampToValueAtTime(0.31, now + 0.03); gain.gain.linearRampToValueAtTime(0.005, now + duration); // Optional delay/echo let output = gain; if(Math.random() > 0.7 || chaosMode) { const delay = audioCtx.createDelay(0.5); delay.delayTime.value = 0.07 + Math.random()*0.21; const feedback = audioCtx.createGain(); feedback.gain.value = 0.22 + Math.random()*0.17; delay.connect(feedback); feedback.connect(delay); osc.connect(delay); delay.connect(gain); } // Panning let pan = audioCtx.createStereoPanner ? audioCtx.createStereoPanner() : null; if(pan) { pan.pan.value = panVal; osc.connect(pan); pan.connect(gain); } else { osc.connect(gain); } // Connect to destination gain.connect(audioCtx.destination); osc.start(now); osc.stop(now + duration + 0.06); osc.onended = () => { gain.disconnect(); if (pan) pan.disconnect(); osc.disconnect(); }; // Add chime for strong movements (optional) if (!chaosMode && shape.size > 80 && Math.random()>0.5) { const bell = audioCtx.createOscillator(); bell.type = 'sine'; bell.frequency.value = freq*2.62; const bellGain = audioCtx.createGain(); bellGain.gain.value = 0.13; bellGain.gain.linearRampToValueAtTime(0.01, now + 0.12); bell.connect(bellGain).connect(audioCtx.destination); bell.start(now); bell.stop(now + 0.15); } } //---- Shape Factory ---- // Shapes: circle, line, spline (Bezier curve) function createShape(x, y, speed, dir, chaos=false) { // chaos: randomize types/colors/params let baseType = chaos ? (["circle", "line", "spline"])[Math.floor(Math.random() * 3)] : ((speed > 140) ? 'line' : (Math.random()>0.4 ? 'circle' : 'spline')); let size = chaos ? 20 + Math.random() * 120 : 16 + Math.min(128, speed * 0.62 + Math.random()*30); // Color base: hue by direction, sat/luminance by speed let baseHue = (chaos ? Math.random()*360 : (dir*180/Math.PI)%360); let sat = chaos ? (60+Math.random()*40) : Math.min(90, 60+speed*0.22); let lum = chaos ? (30+Math.random()*60) : Math.min(85, 52+speed*0.1); return { x, y, speed, dir, size, alpha: 0.89, color: hsl(baseHue,sat,lum,0.82), h: baseHue, s: sat, l: lum, // for echo coloring shapeType: baseType, decay: chaos ? 0.008+Math.random()*0.012 : 0.012+size*0.00011, echoCount: 0, echoSchedule: [0.14+(chaos?Math.random()*0.2:0),0.3+(chaos?Math.random()*0.5:0)], echoes: [], chaos: chaos, shapeExtras: (()=> { if (baseType==='line'){ // random direction for line let ang = chaos ? Math.random()*2*Math.PI : dir; let len = chaos ? size + Math.random()*size : size*1.35+Math.random()*18; return {ang, len}; } else if (baseType==='spline') { // Control points for spline based on movement dir/speed let cp1 = { x: x + Math.cos(dir+Math.PI/3)*(size*0.7+Math.random()*33), y: y + Math.sin(dir+Math.PI/3)*(size*0.87) }; let cp2 = { x: x + Math.cos(dir-Math.PI/3)*(size*0.5+Math.random()*28), y: y + Math.sin(dir-Math.PI/3)*(size*0.65+Math.random()*0.8) }; return {cp1, cp2}; } else { return {}; } })() } } //---- Echo Mechanics ---- function spawnEcho(shape, echoIndex, chaos=false) { // Shrink, fade, move in offset random direction let echoDir = chaos ? Math.random()*2*Math.PI : shape.dir + (Math.random()-0.5)*Math.PI/2; let echoSpeed = (chaos ? Math.random()*40+10 : Math.random()*19+7); return { x: shape.x, y: shape.y, size: shape.size * (0.48 + Math.random()*0.35), alpha: 0.26 + Math.random()*0.22, decay: 0.02 + Math.random()*0.018, dx: Math.cos(echoDir)*echoSpeed, dy: Math.sin(echoDir)*echoSpeed, color: hsl( shape.h+Math.random()*30-15, Math.max(12,shape.s-18+Math.random()*30), shape.l-15+Math.random()*28, chaos? (0.73+Math.random()*0.19):(0.29+Math.random()*0.24) ), shapeType: shape.shapeType, shapeExtras: shape.shapeExtras // copy }; } //---- Interaction ---- let lastPoint = null; function pointerHandler(e) { let tx = 0, ty = 0; if (e.touches && e.touches.length>0) { tx = e.touches[0].clientX; ty = e.touches[0].clientY; } else { tx = e.clientX; ty = e.clientY; } tx *= dpr; ty *= dpr; if (lastPoint) { let dx = tx - lastPoint.x, dy = ty - lastPoint.y; let dist = Math.hypot(dx, dy); let dir = Math.atan2(dy, dx); if (dist>2.5) { // threshold for smoother trail let shape = createShape(tx, ty, dist, dir, chaosMode); shapes.push(shape); playShapeSound(shape); } } lastPoint = {x: tx, y: ty}; // Resume context (for iOS/safari) if (audioCtx.state === "suspended") audioCtx.resume(); } function pointerUpHandler(e) { lastPoint = null; } canvas.addEventListener('mousemove', pointerHandler, {passive:true}); canvas.addEventListener('touchmove', pointerHandler, {passive:true}); canvas.addEventListener('touchstart', pointerHandler, {passive:true}); canvas.addEventListener('mouseup', pointerUpHandler, {passive:true}); canvas.addEventListener('touchend', pointerUpHandler, {passive:true}); canvas.addEventListener('mouseleave', pointerUpHandler, {passive:true}); //---- Chaos Mode ---- chaosBtn.addEventListener('click', ()=>{ chaosMode = !chaosMode; chaosBtn.classList.toggle("active",chaosMode); chaosBtn.textContent = `Chaos Mode: ${chaosMode ? "On" : "Off"}`; }); //---- Draw Shapes ---- function drawShape(sh, forceAlpha) { ctx.save(); ctx.globalAlpha = typeof forceAlpha === "number" ? forceAlpha : sh.alpha; ctx.strokeStyle= ctx.fillStyle= sh.color; if (sh.shapeType === 'circle') { ctx.beginPath(); ctx.arc(sh.x, sh.y, sh.size, 0, 2*Math.PI); ctx.fill(); } else if (sh.shapeType === 'line') { ctx.lineWidth = sh.size * 0.18 + 2; let len = (sh.shapeExtras && sh.shapeExtras.len) || sh.size*1.1; let ang = (sh.shapeExtras && sh.shapeExtras.ang) || 0; ctx.beginPath(); ctx.moveTo(sh.x-len/2*Math.cos(ang), sh.y-len/2*Math.sin(ang)); ctx.lineTo(sh.x+len/2*Math.cos(ang), sh.y+len/2*Math.sin(ang)); ctx.stroke(); } else if (sh.shapeType === 'spline') { let cp1 = sh.shapeExtras && sh.shapeExtras.cp1; let cp2 = sh.shapeExtras && sh.shapeExtras.cp2; let x1 = sh.x, y1 = sh.y; let x2 = sh.x + (cp2 ? cp2.x - sh.x : sh.size), y2 = sh.y + (cp2 ? cp2.y - sh.y : sh.size*0.6); ctx.lineWidth = sh.size * 0.09+1.5; ctx.beginPath(); ctx.moveTo(x1,y1); ctx.bezierCurveTo( cp1?cp1.x:x1+sh.size*0.5, cp1?cp1.y:y1+sh.size*0.2, cp2?cp2.x:x1+sh.size*0.8, cp2?cp2.y:y1+sh.size*0.7, x2, y2 ); ctx.stroke(); } ctx.restore(); } //---- Animation Loop ---- function animate() { // Fading the canvas to leave subtle trails ctx.globalAlpha = chaosMode ? 0.14 : 0.18; ctx.fillStyle = chaosMode ? hsl(200+Math.random()*120,12+Math.random()*38,10+Math.random()*10) : "#181a1b"; ctx.fillRect(0,0,canvas.width,canvas.height); // Update and Draw all main shapes and echoes for (let i=shapes.length-1; i>=0; i--) { let sh = shapes[i]; // Draw the main shape drawShape(sh); // Spawn echo(s) at schedule let elapsed = (1-sh.alpha)/sh.decay * 1/60; //frames for (let e=0; e<sh.echoSchedule.length; ++e) { if (sh.echoCount<=e && 1-sh.alpha>sh.echoSchedule[e] && sh.alpha>0.05) { sh.echoes.push(spawnEcho(sh,e,sh.chaos)); sh.echoCount++; } } // Fade the main shape sh.alpha -= sh.decay; if (sh.alpha <= 0.06 && sh.echoes.length===0) { shapes.splice(i,1); continue; } // Echo management (draw & animate echoes) for (let j=sh.echoes.length-1;j>=0;j--) { let echo = sh.echoes[j]; echo.x += echo.dx * 1/60; echo.y += echo.dy * 1/60; echo.alpha -= echo.decay * (chaosMode?1.37:1); drawShape(echo, echo.alpha*0.92); if (echo.alpha<0.04) sh.echoes.splice(j,1); } } requestAnimationFrame(animate); } animate(); </script> </body> </html> ```
To create an interactive web canvas where mouse movements generate dynamic, evolving visual patterns synchronized with sound, you can use HTML5 Canvas for the visuals and the Web Audio API for the sounds. Below is a complete example, including all the necessary HTML, CSS, and JavaScript code: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Canvas</title> <style> body, html { margin: 0; overflow: hidden; height: 100%; width: 100%; display: flex; justify-content: center; align-items: center; background-color: #000; } canvas { display: block; } #controls { position: absolute; top: 10px; left: 10px; background: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; } #chaosMode { color: #fff; background: none; border: 1px solid #fff; padding: 5px 10px; border-radius: 3px; cursor: pointer; } </style> </head> <body> <div id="controls"> <button id="chaosMode">Chaos Mode</button> </div> <canvas id="canvas"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const chaosButton = document.getElementById('chaosMode'); let isChaosMode = false; window.addEventListener('resize', resizeCanvas); canvas.addEventListener('mousemove', drawShape); chaosButton.addEventListener('click', toggleChaosMode); const audioContext = new (window.AudioContext || window.webkitAudioContext)(); function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } function getRandomColor() { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; } function drawShape(event) { const x = event.clientX; const y = event.clientY; const size = isChaosMode ? Math.random() * 50 : Math.random() * 30 + 10; const color = isChaosMode ? getRandomColor() : `hsl(${event.movementX * 3 % 360}, 100%, 50%)`; ctx.beginPath(); if (isChaosMode && Math.random() > 0.5) { ctx.ellipse(x, y, size, size / 2, 0, 0, Math.PI * 2); } else { ctx.arc(x, y, size, 0, Math.PI * 2); } ctx.fillStyle = color; ctx.fill(); playSound(x, y); setTimeout(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); }, 2000); } function playSound(x, y) { const osc = audioContext.createOscillator(); const gainNode = audioContext.createGain(); osc.connect(gainNode); gainNode.connect(audioContext.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(x + y, audioContext.currentTime); gainNode.gain.setValueAtTime(0.5, audioContext.currentTime); osc.start(); setTimeout(() => { gainNode.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + 1); }, 1000); osc.stop(audioContext.currentTime + 2); } function toggleChaosMode() { isChaosMode = !isChaosMode; chaosButton.textContent = `Chaos Mode: ${isChaosMode ? 'On' : 'Off'}`; } resizeCanvas(); </script> </body> </html> ``` ### Explanation: 1. **HTML**: - A `canvas` element for drawing. - A `div` with a `button` to toggle chaos mode. 2. **CSS**: - Styles to make the canvas full-screen and responsive. - Basic styling for the controls. 3. **JavaScript**: - `resizeCanvas` function to adjust the canvas size on window resize. - `drawShape` function to draw shapes on the canvas based on mouse movements. - `playSound` function to generate sound using the Web Audio API. - `toggleChaosMode` function to switch between normal and chaos modes. - Event listeners for resizing, mouse movement, and button click. This setup provides an interactive and immersive experience that adapts to any device size.
### Interactive Web Canvas with Sound and Visuals Below is an example implementation using HTML, CSS, and JavaScript. The code utilizes the HTML5 Canvas API for rendering graphics and the Web Audio API for generating sound. #### HTML ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Painting with Echoes</title> <style> /* Make the canvas full-screen and responsive */ body { margin: 0; overflow: hidden; } #canvas { width: 100vw; height: 100vh; display: block; } </style> </head> <body> <canvas id="canvas"></canvas> <button id="chaos-mode-toggle">Toggle Chaos Mode</button> <script src="script.js"></script> </body> </html> ``` #### JavaScript (in script.js) ```javascript // Get the canvas and its 2D drawing context const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Set the canvas to full-screen canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Define some constants and variables const SHAPE_TYPES = ['circle', 'line', 'spline']; const COLORS = ['#FF69B4', '#33CC33', '#6666FF']; const MAX_SHAPE_SIZE = 50; const MIN_SHAPE_SIZE = 10; const MAX_ECHO_SIZE = 20; const ECHO_FADE_RATE = 0.05; const SOUND_FREQ_RANGE = [200, 800]; // Hz const SOUND_DURATION = 0.5; // seconds let shapes = []; let echoes = []; let chaosMode = false; let lastMouseX, lastMouseY; let audioContext; // Initialize the Web Audio API context try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { console.error('Web Audio API is not supported in this browser.'); } // Event listeners canvas.addEventListener('mousemove', (e) => { const mouseX = e.clientX; const mouseY = e.clientY; const speed = Math.sqrt(Math.pow(mouseX - lastMouseX, 2) + Math.pow(mouseY - lastMouseY, 2)); lastMouseX = mouseX; lastMouseY = mouseY; // Create a new shape based on mouse movement const shapeType = chaosMode ? SHAPE_TYPES[Math.floor(Math.random() * SHAPE_TYPES.length)] : 'circle'; const shapeSize = Math.min(MAX_SHAPE_SIZE, Math.max(MIN_SHAPE_SIZE, speed)); const shapeColor = chaosMode ? getRandomColor() : COLORS[Math.floor(Math.random() * COLORS.length)]; shapes.push({ x: mouseX, y: mouseY, size: shapeSize, color: shapeColor, type: shapeType, timestamp: Date.now() }); // Play a sound based on shape properties playSound(shapeSize, mouseX / canvas.width); }); document.getElementById('chaos-mode-toggle').addEventListener('click', () => { chaosMode = !chaosMode; }); // Animation loop function animate() { requestAnimationFrame(animate); // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Update and draw shapes shapes.forEach((shape, index) => { const age = (Date.now() - shape.timestamp) / 1000; if (age > 1) { shapes.splice(index, 1); // Create echoes for the shape for (let i = 0; i < 5; i++) { echoes.push({ x: shape.x, y: shape.y, size: Math.random() * MAX_ECHO_SIZE, color: shape.color, alpha: 1, vx: Math.random() * 2 - 1, vy: Math.random() * 2 - 1 }); } } else { drawShape(ctx, shape); } }); // Update and draw echoes echoes.forEach((echo, index) => { echo.x += echo.vx; echo.y += echo.vy; echo.alpha -= ECHO_FADE_RATE; if (echo.alpha <= 0) { echoes.splice(index, 1); } else { drawEcho(ctx, echo); } }); } animate(); // Helper functions function drawShape(ctx, shape) { ctx.fillStyle = shape.color; ctx.strokeStyle = shape.color; ctx.lineWidth = 2; switch (shape.type) { case 'circle': ctx.beginPath(); ctx.arc(shape.x, shape.y, shape.size / 2, 0, 2 * Math.PI); ctx.fill(); break; case 'line': ctx.beginPath(); ctx.moveTo(shape.x - shape.size / 2, shape.y); ctx.lineTo(shape.x + shape.size / 2, shape.y); ctx.stroke(); break; case 'spline': ctx.beginPath(); ctx.moveTo(shape.x - shape.size / 2, shape.y); ctx.quadraticCurveTo(shape.x, shape.y - shape.size / 2, shape.x + shape.size / 2, shape.y); ctx.stroke(); break; } } function drawEcho(ctx, echo) { ctx.fillStyle = echo.color; ctx.globalAlpha = echo.alpha; ctx.beginPath(); ctx.arc(echo.x, echo.y, echo.size / 2, 0, 2 * Math.PI); ctx.fill(); ctx.globalAlpha = 1; } function playSound(size, position) { const frequency = SOUND_FREQ_RANGE[0] + (SOUND_FREQ_RANGE[1] - SOUND_FREQ_RANGE[0]) * position; const oscillator = audioContext.createOscillator(); oscillator.type = 'sine'; oscillator.frequency.value = frequency; const gainNode = audioContext.createGain(); gainNode.gain.value = size / MAX_SHAPE_SIZE; oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + SOUND_DURATION); } function getRandomColor() { return `#${Math.floor(Math.random() * 16777215).toString(16)}`; } // Handle window resize window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }); ``` This code creates a full-screen canvas where mouse movements generate dynamic shapes and sounds. The shapes fade out over time, leaving behind echoes that move in random directions. The "chaos mode" toggle randomizes the shape types, colors, and sound frequencies for a wilder experience. The canvas adapts to any device size and is responsive.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Sonic Canvas</title> <style> html, body { margin: 0; padding: 0; overflow: hidden; background: black; } canvas { display: block; } #chaosToggle { position: absolute; top: 10px; left: 10px; z-index: 2; background: rgba(0,0,0,0.5); color: white; border: none; padding: 8px 12px; cursor: pointer; font-family: sans-serif; } </style> </head> <body> <button id="chaosToggle">Chaos Mode: OFF</button> <canvas id="canvas"></canvas> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const toggleBtn = document.getElementById('chaosToggle'); let width = window.innerWidth; let height = window.innerHeight; canvas.width = width; canvas.height = height; let shapes = []; let lastX = width / 2; let lastY = height / 2; let chaosMode = false; // Web Audio API setup const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); // Resize handling window.addEventListener('resize', () => { width = window.innerWidth; height = window.innerHeight; canvas.width = width; canvas.height = height; }); // Mouse movement handling window.addEventListener('pointermove', (e) => { const speed = Math.sqrt( Math.pow(e.clientX - lastX, 2) + Math.pow(e.clientY - lastY, 2) ); lastX = e.clientX; lastY = e.clientY; createShape(e.clientX, e.clientY, speed); }); function createShape(x, y, speed) { const baseSize = Math.max(10, Math.min(60, speed * 2)); // Randomize more in chaos mode const color = chaosMode ? `hsl(${Math.random()*360}, 100%, 50%)` : `hsl(${(x/width)*360}, 100%, 50%)`; const shapeType = chaosMode ? ['circle', 'line', 'triangle'][Math.floor(Math.random()*3)] : 'circle'; shapes.push({ x, y, radius: baseSize * (chaosMode ? Math.random() : 1), life: 100, color, shapeType, velocityX: (chaosMode ? (Math.random() - 0.5) : 0) * 0.3, velocityY: (chaosMode ? (Math.random() - 0.5) : 0) * 0.3, // Create echoes echoes: Array.from({length: 3}, () => ({ life: 80 + Math.random() * 20, opacity: 0.2 + Math.random() * 0.3, velocityX: (Math.random() - 0.5) * 0.1, velocityY: (Math.random() - 0.5) * 0.1, })) }); // Generate sound based on position and speed const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.type = chaosMode ? ['sine', 'square', 'sawtooth'][Math.floor(Math.random()*3)] : 'sine'; // Frequency based on x position const freq = 220 + (x / width) * 880; oscillator.frequency.value = freq; // Volume based on speed gainNode.gain.value = Math.min(0.5, speed / 300); oscillator.connect(gainNode).connect(audioCtx.destination); oscillator.start(); oscillator.stop(audioCtx.currentTime + 0.12); } // Animation loop function animate() { ctx.globalCompositeOperation = 'screen'; ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; ctx.fillRect(0, 0, width, height); ctx.globalCompositeOperation = 'lighter'; for (let i = shapes.length - 1; i >= 0; i--) { const shape = shapes[i]; shape.x += shape.velocityX; shape.y += shape.velocityY; shape.life--; // Draw main shape drawShape(shape); // Process echoes for (let j = shape.echoes.length - 1; j >= 0; j--) { const echo = shape.echoes[j]; echo.life--; // Draw echo ctx.save(); ctx.globalAlpha = echo.opacity * (echo.life/80); drawShape({ x: shape.x + echo.velocityX * 50, y: shape.y + echo.velocityY * 50, radius: shape.radius * 0.5, color: shape.color, shapeType: shape.shapeType }); ctx.restore(); } // Clean up expired elements if (shape.life <= 0) { shapes.splice(i, 1); } } // Limit maximum shapes if (shapes.length > 500) { shapes.splice(0, shapes.length - 300); } requestAnimationFrame(animate); } function drawShape(shape) { ctx.save(); ctx.globalAlpha = shape.life / 100; ctx.fillStyle = shape.color; switch(shape.shapeType) { case 'circle': ctx.beginPath(); ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI*2); ctx.fill(); break; case 'line': ctx.beginPath(); ctx.moveTo(shape.x, shape.y); ctx.lineTo(shape.x + shape.radius, shape.y + shape.radius); ctx.stroke(); break; case 'triangle': ctx.beginPath(); ctx.moveTo(shape.x, shape.y - shape.radius); ctx.lineTo(shape.x + shape.radius, shape.y + shape.radius); ctx.lineTo(shape.x - shape.radius, shape.y + shape.radius); ctx.closePath(); ctx.fill(); break; } ctx.restore(); } // Chaos mode toggle toggleBtn.addEventListener('click', () => { chaosMode = !chaosMode; toggleBtn.textContent = `Chaos Mode: ${chaosMode ? 'ON' : 'OFF'}`; }); animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sound & Visual Echo Canvas</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; overflow: hidden; } body, html { width: 100%; height: 100%; background-color: #000; color: white; font-family: Arial, sans-serif; } canvas { display: block; position: absolute; top: 0; left: 0; } .controls { position: fixed; bottom: 20px; right: 20px; z-index: 100; display: flex; gap: 10px; background-color: rgba(0, 0, 0, 0.6); padding: 10px; border-radius: 8px; } button { padding: 8px 12px; border: none; background-color: #4a4a4a; color: white; border-radius: 4px; cursor: pointer; transition: all 0.2s; } button:hover { background-color: #666; } button.active { background-color: #7d4ae8; } .intro { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 200; text-align: center; padding: 20px; } .intro h1 { margin-bottom: 20px; font-size: 2.5em; } .intro p { margin-bottom: 30px; max-width: 600px; line-height: 1.5; } #startButton { background-color: #7d4ae8; color: white; padding: 12px 24px; font-size: 1.2em; border-radius: 30px; border: none; cursor: pointer; transition: transform 0.2s; } #startButton:hover { transform: scale(1.05); } </style> </head> <body> <canvas id="canvas"></canvas> <div class="controls"> <button id="chaosToggle">Chaos Mode</button> <button id="clearCanvas">Clear Canvas</button> </div> <div class="intro" id="intro"> <h1>Sound & Visual Echo Canvas</h1> <p>Move your mouse or finger to create dynamic visual patterns synchronized with sound. Each movement generates unique shapes and tones that echo across the canvas.</p> <button id="startButton">Begin Experience</button> </div> <script> window.addEventListener('load', function() { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const chaosToggle = document.getElementById('chaosToggle'); const clearCanvasBtn = document.getElementById('clearCanvas'); const startButton = document.getElementById('startButton'); const intro = document.getElementById('intro'); // Set canvas to window size and update on resize function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resizeCanvas(); window.addEventListener('resize', resizeCanvas); // State variables let chaosMode = false; let isDrawing = false; let lastX = 0; let lastY = 0; let hue = 0; let particles = []; let lastTimestamp = 0; let synth; let reverb; let pingPong; let velocityHistory = []; let audioInitialized = false; // Initialize audio function initAudio() { if (audioInitialized) return; // Create effects reverb = new Tone.Reverb({ decay: 4, wet: 0.3 }).toDestination(); pingPong = new Tone.PingPongDelay({ delayTime: 0.25, feedback: 0.2, wet: 0.2 }).connect(reverb); // Create synth synth = new Tone.PolySynth(Tone.Synth).connect(pingPong); synth.set({ envelope: { attack: 0.02, decay: 0.2, sustain: 0.1, release: 1 } }); audioInitialized = true; Tone.start(); } // Particle class for visual echoes class Particle { constructor(x, y, hue, size, speedX, speedY, type) { this.x = x; this.y = y; this.originalX = x; this.originalY = y; this.size = size; this.hue = hue; this.alpha = 1; this.speedX = speedX; this.speedY = speedY; this.type = type || (Math.random() > 0.5 ? 'circle' : 'line'); this.rotation = Math.random() * Math.PI * 2; this.rotationSpeed = (Math.random() - 0.5) * 0.05; this.dissipationRate = Math.random() * 0.01 + 0.005; this.life = 100; // Life counter } update() { this.x += this.speedX; this.y += this.speedY; this.rotation += this.rotationSpeed; // Apply some gravity or resistance this.speedX *= 0.99; this.speedY *= 0.99; if (chaosMode) { this.speedX += (Math.random() - 0.5) * 0.5; this.speedY += (Math.random() - 0.5) * 0.5; this.hue += Math.random() * 2; } this.alpha -= this.dissipationRate; this.life--; // Bounce off edges if (this.x < 0 || this.x > canvas.width) this.speedX *= -0.8; if (this.y < 0 || this.y > canvas.height) this.speedY *= -0.8; } draw() { ctx.save(); ctx.globalAlpha = this.alpha; ctx.translate(this.x, this.y); ctx.rotate(this.rotation); let colorStyle; if (chaosMode) { colorStyle = `hsla(${this.hue}, 100%, 60%, ${this.alpha})`; } else { colorStyle = `hsla(${this.hue}, 80%, 60%, ${this.alpha})`; } ctx.fillStyle = colorStyle; ctx.strokeStyle = colorStyle; ctx.lineWidth = Math.max(1, this.size / 5); switch (this.type) { case 'circle': ctx.beginPath(); ctx.arc(0, 0, this.size, 0, Math.PI * 2); ctx.fill(); break; case 'line': ctx.beginPath(); ctx.moveTo(-this.size, 0); ctx.lineTo(this.size, 0); ctx.stroke(); break; case 'triangle': ctx.beginPath(); const s = this.size; ctx.moveTo(0, -s); ctx.lineTo(-s, s); ctx.lineTo(s, s); ctx.closePath(); ctx.fill(); break; case 'square': ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size); break; } // Draw connection to original position for some particles if (Math.random() > 0.8 && this.life > 80) { ctx.globalAlpha = this.alpha * 0.2; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(this.originalX - this.x, this.originalY - this.y); ctx.stroke(); } ctx.restore(); } isAlive() { return this.alpha > 0 && this.life > 0; } } // Calculate velocity from mouse movement function calculateVelocity(x1, y1, x2, y2, timeDelta) { const dx = x2 - x1; const dy = y2 - y1; const distance = Math.sqrt(dx * dx + dy * dy); return distance / (timeDelta || 16); // Default to 16ms if timeDelta is 0 } // Update velocity history function updateVelocityHistory(velocity) { velocityHistory.push(velocity); if (velocityHistory.length > 10) { velocityHistory.shift(); } } // Get average velocity function getAverageVelocity() { if (velocityHistory.length === 0) return 0; return velocityHistory.reduce((sum, v) => sum + v, 0) / velocityHistory.length; } // Play sound based on position and velocity function playSound(x, y, velocity) { if (!audioInitialized) return; // Map x position to pitch (note) const normalizedX = x / canvas.width; const normalizedY = y / canvas.height; // Use pentatonic scale for more harmonious sounds const pentatonicScale = ['C', 'D', 'E', 'G', 'A']; const octaves = [3, 4, 5]; const noteIndex = Math.floor(normalizedX * pentatonicScale.length); const octaveIndex = Math.floor(normalizedY * octaves.length); const note = pentatonicScale[noteIndex] + octaves[octaveIndex]; // Map velocity to volume and duration const volume = Math.min(-20 + velocity * 5, -8); const duration = 0.1 + Math.min(velocity * 0.2, 0.8); // In chaos mode, add randomness if (chaosMode) { const randomNote = pentatonicScale[Math.floor(Math.random() * pentatonicScale.length)] + octaves[Math.floor(Math.random() * octaves.length)]; const randomVolume = Math.random() * 10 - 30; synth.triggerAttackRelease(randomNote, duration, undefined, randomVolume); // Play a second note occasionally in chaos mode if (Math.random() > 0.7) { const secondNote = pentatonicScale[Math.floor(Math.random() * pentatonicScale.length)] + octaves[Math.floor(Math.random() * octaves.length)]; setTimeout(() => { synth.triggerAttackRelease(secondNote, duration * 0.5, undefined, randomVolume - 5); }, duration * 500); } } else { synth.triggerAttackRelease(note, duration, undefined, volume); } } // Create particles based on movement function createParticles(x, y, velocity) { const baseHue = chaosMode ? Math.random() * 360 : hue; const count = chaosMode ? Math.floor(velocity * 3) + 2 : Math.floor(velocity) + 1; const size = chaosMode ? Math.random() * 30 + 5 : Math.min(30, Math.max(5, velocity * 3)); for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = chaosMode ? Math.random() * 6 - 3 : Math.random() * 2 - 1; const speedX = Math.cos(angle) * speed; const speedY = Math.sin(angle) * speed; // In chaos mode, use random shapes let type; if (chaosMode) { const types = ['circle', 'line', 'triangle', 'square']; type = types[Math.floor(Math.random() * types.length)]; } else { type = velocity > 3 ? 'line' : 'circle'; } const particleHue = baseHue + (Math.random() * 20 - 10); const particleSize = size * (Math.random() * 0.5 + 0.5); particles.push(new Particle(x, y, particleHue, particleSize, speedX, speedY, type)); } } // Animation loop function animate(timestamp) { if (!lastTimestamp) lastTimestamp = timestamp; const deltaTime = timestamp - lastTimestamp; lastTimestamp = timestamp; // Fade out the canvas instead of clearing it ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Update and draw all particles particles = particles.filter(particle => { particle.update(); particle.draw(); return particle.isAlive(); }); // Increment hue for color cycling hue = (hue + 0.5) % 360; requestAnimationFrame(animate); } // Event listeners function startDrawing(e) { isDrawing = true; // Get initial position const pos = getEventPosition(e); lastX = pos.x; lastY = pos.y; // Initialize audio on first interaction if not already initialized if (!audioInitialized) { initAudio(); } } function stopDrawing() { isDrawing = false; } function draw(e) { if (!isDrawing) return; const pos = getEventPosition(e); const currentX = pos.x; const currentY = pos.y; const velocity = calculateVelocity(lastX, lastY, currentX, currentY, 16); updateVelocityHistory(velocity); if (velocity > 0.1) { // Only create particles if there's significant movement createParticles(currentX, currentY, velocity); // Play sound at most once every 50ms to avoid overwhelming audio if (timestamp - lastSoundTimestamp > 50) { playSound(currentX, currentY, getAverageVelocity()); lastSoundTimestamp = timestamp; } } lastX = currentX; lastY = currentY; } // Helper to get position from different event types function getEventPosition(e) { let x, y; if (e.type.includes('touch')) { x = e.touches[0].clientX; y = e.touches[0].clientY; } else { x = e.clientX; y = e.clientY; } return { x, y }; } // Toggle chaos mode chaosToggle.addEventListener('click', function() { chaosMode = !chaosMode; this.classList.toggle('active'); // Update effects based on chaos mode if (audioInitialized) { if (chaosMode) { reverb.wet.value = 0.6; pingPong.wet.value = 0.5; pingPong.feedback.value = 0.4; } else { reverb.wet.value = 0.3; pingPong.wet.value = 0.2; pingPong.feedback.value = 0.2; } } }); // Clear canvas clearCanvasBtn.addEventListener('click', function() { particles = []; ctx.fillStyle = 'black'; ctx.fillRect(0, 0, canvas.width, canvas.height); }); // Start button startButton.addEventListener('click', function() { intro.style.display = 'none'; initAudio(); if (!animationStarted) { requestAnimationFrame(animate); animationStarted = true; } }); // Mouse/touch event listeners canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); canvas.addEventListener('touchstart', startDrawing); canvas.addEventListener('touchmove', draw); canvas.addEventListener('touchend', stopDrawing); let animationStarted = false; let timestamp = 0; let lastSoundTimestamp = 0; }); </script> </body> </html> ```