
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>
```