Artistic Practice

Playful intersection of sound and human-machine collaboration

Geospatial Manipulation

Active Research • Interactive System
Live
Click & drag to deform terrain

Real-time terrain deformation system. Click and drag to sculpt the landscape. Audio feedback represents elevation changes.

GISGesture ControlSonification

Stigmergic Fields

Research Phase • Multi-Agent
Beta
Agents: 0 | Coherence: 0.00

Multi-agent stigmergy simulation. Bots (teal) and humans (pink) leave pheromone trails. Watch emergent paths form.

EmergenceCollective IntelligenceStigmergy

AI Improvisation

Performance Ready • Live Electronics
Live
Click to trigger • Double-click for chord

Real-time RNN improvisation. Click neurons to trigger activations. The network generates responsive sonic patterns via Web Audio API.

RNNLive ElectronicsImprovisation

Phase Transitions

Sonic Installation • Physics
Install
State: SOLID | Temp: 100K

Matter state simulation with sonification. Control temperature to observe phase transitions. Audio represents molecular vibration energy.

Granular SynthesisThermodynamicsPhysics Models

Algorithmic Composition

Series • Generative
Series
L-System: Botanical • Gen: 4

Rule-based generative music systems. L-systems grow musical structures. Click branches to grow new ones. Each generation applies transformation rules.

L-SystemsCellular AutomataGenerative

Extended Portfolio on Cargo Collective

Documentation, exhibition history, and process archives

Visit Cargo Collective
// 3. STIGMERGY let stigCanvas, stigCtx, stigAgents = [], stigTraces = [], stigObstacles = []; function initStigmergy() { stigCanvas = document.getElementById('canvas-stigmergy'); if (!stigCanvas) return; stigCtx = stigCanvas.getContext('2d'); stigCanvas.width = stigCanvas.offsetWidth; stigCanvas.height = stigCanvas.offsetHeight; for (let i = 0; i < 6; i++) addAgent('bot'); for (let i = 0; i < 4; i++) addAgent('human'); stigCanvas.addEventListener('click', addObstacle); animateStigmergy(); } function addAgent(type) { stigAgents.push({ x: Math.random() * stigCanvas.width, y: Math.random() * stigCanvas.height, vx: 0, vy: 0, angle: Math.random() * Math.PI * 2, type: type }); updateAgentCount(); } function addBots() { for (let i = 0; i < 3; i++) addAgent('bot'); } function addHumans() { for (let i = 0; i < 2; i++) addAgent('human'); } function addObstacle(e) { const rect = stigCanvas.getBoundingClientRect(); stigObstacles.push({ x: e.clientX - rect.left, y: e.clientY - rect.top, radius: 20 }); } function clearTraces() { stigTraces = []; } function updateAgentCount() { const el = document.getElementById('agent-count'); if (el) el.textContent = stigAgents.length; } function animateStigmergy() { const ctx = stigCtx, w = stigCanvas.width, h = stigCanvas.height; ctx.fillStyle = 'rgba(250, 250, 248, 0.08)'; ctx.fillRect(0, 0, w, h); stigObstacles.forEach(obs => { ctx.beginPath(); ctx.arc(obs.x, obs.y, obs.radius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(120, 113, 108, 0.3)'; ctx.fill(); }); stigAgents.forEach(agent => { let bestAngle = agent.angle, strongest = 0; stigTraces.forEach(trace => { if (trace.type !== agent.type) return; const dx = trace.x - agent.x, dy = trace.y - agent.y, dist = Math.sqrt(dx * dx + dy * dy); if (dist < 60 && trace.strength > strongest) { strongest = trace.strength; bestAngle = Math.atan2(dy, dx); } }); stigObstacles.forEach(obs => { const dx = agent.x - obs.x, dy = agent.y - obs.y, dist = Math.sqrt(dx * dx + dy * dy); if (dist < obs.radius + 15) bestAngle = Math.atan2(dy, dx); }); agent.angle += (bestAngle - agent.angle) * 0.15; agent.angle += (Math.random() - 0.5) * 0.4; const speed = agent.type === 'bot' ? 1.2 : 0.8; agent.vx = Math.cos(agent.angle) * speed; agent.vy = Math.sin(agent.angle) * speed; agent.x += agent.vx; agent.y += agent.vy; if (agent.x < 0) agent.x = w; if (agent.x > w) agent.x = 0; if (agent.y < 0) agent.y = h; if (agent.y > h) agent.y = 0; if (Math.random() < 0.4) stigTraces.push({ x: agent.x, y: agent.y, type: agent.type, strength: 1, age: 0 }); ctx.beginPath(); ctx.arc(agent.x, agent.y, agent.type === 'bot' ? 3 : 4, 0, Math.PI * 2); ctx.fillStyle = agent.type === 'bot' ? '#0d9488' : '#db2777'; ctx.fill(); }); stigTraces = stigTraces.filter(t => { t.age++; t.strength *= 0.985; if (t.strength < 0.01) return false; ctx.beginPath(); ctx.arc(t.x, t.y, 2.5, 0, Math.PI * 2); ctx.fillStyle = t.type === 'bot' ? `rgba(13, 148, 136, ${t.strength * 0.6})` : `rgba(219, 39, 119, ${t.strength * 0.6})`; ctx.fill(); return true; }); const coherence = stigTraces.length > 0 ? (stigTraces.filter(t => t.strength > 0.5).length / stigTraces.length).toFixed(2) : '0.00'; const cohEl = document.getElementById('coherence-val'); if (cohEl) cohEl.textContent = coherence; requestAnimationFrame(animateStigmergy); } function toggleParameters() { alert('Parameters:\nMemory: 0.5\nFading: 0.3\nAttention: 0.7\nInfluence: 0.4'); } function saveSimulation() { alert('State saved!'); } function loadSimulation() { alert('State loaded!'); } // 4. AI AUDIO let audioCanvas, audioCtx, neurons = [], connections = [], audioRunning = false, currentScale = [220, 261.63, 293.66, 329.63, 392, 440, 523.25]; function initAudioImprov() { audioCanvas = document.getElementById('canvas-audio'); if (!audioCanvas) return; audioCtx = audioCanvas.getContext('2d'); audioCanvas.width = audioCanvas.offsetWidth; audioCanvas.height = audioCanvas.offsetHeight; const layers = [3, 5, 5, 3]; let x = 0.15; layers.forEach((count, li) => { for (let i = 0; i < count; i++) { neurons.push({ x: x, y: 0.15 + (i / (count - 1 || 1)) * 0.7, layer: li, activation: 0, targetActivation: 0, freq: currentScale[Math.floor(Math.random() * currentScale.length)] }); } x += 0.23; }); neurons.forEach((n1, i) => { neurons.forEach((n2, j) => { if (n2.layer === n1.layer + 1 && Math.random() > 0.3) connections.push({ from: n1, to: n2, weight: Math.random() }); }); }); audioCanvas.addEventListener('click', triggerNeuron); audioCanvas.addEventListener('dblclick', triggerChord); animateAudioImprov(); } function triggerNeuron(e) { const rect = audioCanvas.getBoundingClientRect(), x = (e.clientX - rect.left) / rect.width, y = (e.clientY - rect.top) / rect.height; neurons.forEach(n => { const dx = n.x - x, dy = n.y - y, dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.12) { n.targetActivation = 1; if (audioRunning) { const { osc } = createOscillator(n.freq, 'sine', 0.3); osc.start(); osc.stop(initAudio().currentTime + 0.3); } } }); } function triggerChord() { if (!audioRunning) return; const chord = currentScale.slice(0, 3); chord.forEach((freq, i) => { setTimeout(() => { const { osc } = createOscillator(freq, 'triangle', 0.5); osc.start(); osc.stop(initAudio().currentTime + 0.5); }, i * 50); }); } function animateAudioImprov() { const ctx = audioCtx, w = audioCanvas.width, h = audioCanvas.height; ctx.fillStyle = '#f5f5f4'; ctx.fillRect(0, 0, w, h); neurons.forEach(n => { n.activation += (n.targetActivation - n.activation) * 0.12; n.targetActivation *= 0.94; }); connections.forEach(c => { const alpha = c.from.activation * c.to.activation * c.weight; if (alpha > 0.01) { ctx.strokeStyle = `rgba(13, 148, 136, ${alpha * 0.6})`; ctx.lineWidth = alpha * 3; ctx.beginPath(); ctx.moveTo(c.from.x * w, c.from.y * h); ctx.lineTo(c.to.x * w, c.to.y * h); ctx.stroke(); } }); neurons.forEach(n => { const radius = 6 + n.activation * 10; ctx.beginPath(); ctx.arc(n.x * w, n.y * h, radius, 0, Math.PI * 2); ctx.fillStyle = `rgba(13, 148, 136, ${0.2 + n.activation * 0.8})`; ctx.fill(); ctx.strokeStyle = '#0d9488'; ctx.lineWidth = 2; ctx.stroke(); if (n.activation > 0.5) { ctx.fillStyle = 'white'; ctx.font = '10px IBM Plex Mono'; ctx.textAlign = 'center'; ctx.fillText(Math.round(n.freq) + 'Hz', n.x * w, n.y * h + 4); } }); if (audioRunning && Math.random() < 0.02) { const n = neurons[Math.floor(Math.random() * neurons.length)]; n.targetActivation = 0.5 + Math.random() * 0.5; const { osc } = createOscillator(n.freq, 'sine', 0.2); osc.start(); osc.stop(initAudio().currentTime + 0.2); } requestAnimationFrame(animateAudioImprov); } function toggleAudioMain() { audioRunning = !audioRunning; const btn = document.getElementById('btn-audio-main'); btn.textContent = audioRunning ? 'Stop Audio' : 'Start Audio'; btn.classList.toggle('active', audioRunning); if (audioRunning) { initAudio(); document.getElementById('audio-status').textContent = 'Audio running • Click neurons'; } else { document.getElementById('audio-status').textContent = 'Click to trigger • Double-click chord'; } } function resetNetwork() { neurons.forEach(n => { n.activation = 0; n.targetActivation = 0; }); } function setScale(type) { if (type === 'major') currentScale = [261.63, 293.66, 329.63, 349.23, 392, 440, 493.88]; else currentScale = [220, 261.63, 293.66, 329.63, 392, 440, 523.25]; neurons.forEach(n => { n.freq = currentScale[Math.floor(Math.random() * currentScale.length)]; }); alert('Scale: ' + type); } // 5. PHASE TRANSITIONS let phaseCanvas, phaseCtx, phaseParticles = [], temperature = 100, targetTemp = 100, phaseAudio = false, autoCycling = false; function initPhase() { phaseCanvas = document.getElementById('canvas-phase'); if (!phaseCanvas) return; phaseCtx = phaseCanvas.getContext('2d'); phaseCanvas.width = phaseCanvas.offsetWidth; phaseCanvas.height = phaseCanvas.offsetHeight; for (let i = 0; i < 100; i++) phaseParticles.push({ x: Math.random() * phaseCanvas.width, y: Math.random() * phaseCanvas.height, vx: 0, vy: 0, bonds: [], type: 'normal' }); animatePhase(); } function animatePhase() { const ctx = phaseCtx, w = phaseCanvas.width, h = phaseCanvas.height; ctx.fillStyle = '#f5f5f4'; ctx.fillRect(0, 0, w, h); temperature += (targetTemp - temperature) * 0.05; let state, color, vibration; if (temperature < 150) { state = 'SOLID'; color = '#7c3aed'; vibration = 0.5; } else if (temperature < 300) { state = 'LIQUID'; color = '#0d9488'; vibration = 2; } else { state = 'GAS'; color = '#db2777'; vibration = 5; } document.getElementById('phase-state').textContent = state; document.getElementById('temp-val').textContent = Math.floor(temperature); const speed = (temperature / 100) * 0.8, bondDist = temperature < 200 ? 35 : temperature < 350 ? 20 : 8; if (phaseAudio && Math.random() < 0.1) { const freq = 100 + temperature * 2; const { osc } = createOscillator(freq, state === 'SOLID' ? 'square' : state === 'LIQUID' ? 'sawtooth' : 'sine', 0.05); osc.start(); osc.stop(initAudio().currentTime + 0.05); } phaseParticles.forEach((p, i) => { p.vx += (Math.random() - 0.5) * speed * vibration; p.vy += (Math.random() - 0.5) * speed * vibration; if (state === 'LIQUID' && p.y < h - 10) p.vy += 0.08; p.vx *= 0.92; p.vy *= 0.92; p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > w) p.vx *= -1; if (p.y < 0 || p.y > h) p.vy *= -1; p.x = Math.max(0, Math.min(w, p.x)); p.y = Math.max(0, Math.min(h, p.y)); p.bonds = []; phaseParticles.forEach((p2, j) => { if (i === j) return; const dx = p.x - p2.x, dy = p.y - p2.y, dist = Math.sqrt(dx * dx + dy * dy); if (dist < bondDist) p.bonds.push(p2); }); const radius = state === 'SOLID' ? 5 : state === 'LIQUID' ? 4 : 2.5; ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); ctx.fillStyle = p.type === 'impurity' ? '#f59e0b' : color; ctx.fill(); p.bonds.forEach(b => { ctx.strokeStyle = color + '30'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(b.x, b.y); ctx.stroke(); }); }); if (autoCycling) { targetTemp += Math.sin(Date.now() * 0.001) * 2; targetTemp = Math.max(50, Math.min(450, targetTemp)); } requestAnimationFrame(animatePhase); } function heatUp() { targetTemp = Math.min(500, targetTemp + 40); } function coolDown() { targetTemp = Math.max(0, targetTemp - 40); } function toggleAudioPhase() { phaseAudio = !phaseAudio; const btn = document.getElementById('btn-audio-phase'); btn.textContent = phaseAudio ? 'Audio: ON' : 'Audio'; btn.classList.toggle('active', phaseAudio); if (phaseAudio) initAudio(); } function autoCycle() { autoCycling = !autoCycling; alert(autoCycling ? 'Auto cycling ON' : 'Auto cycling OFF'); } function addImpurity() { const p = phaseParticles[Math.floor(Math.random() * phaseParticles.length)]; p.type = 'impurity'; alert('Impurity added!'); } function measureDiffusion() { const avgX = phaseParticles.reduce((sum, p) => sum + p.x, 0) / phaseParticles.length, avgY = phaseParticles.reduce((sum, p) => sum + p.y, 0) / phaseParticles.length; alert(`Diffusion: Center at (${Math.round(avgX)}, ${Math.round(avgY)})`); } // 6. ALGORITHMIC COMPOSITION - FIXED VERSION let algoCanvas, algoCtx; let treeBranches = []; let generation = 4; let algoAudioEnabled = false; let isGenerating = false; // Prevent multiple simultaneous generations function initAlgo() { algoCanvas = document.getElementById('canvas-algo'); if (!algoCanvas) return; algoCtx = algoCanvas.getContext('2d'); resizeAlgoCanvas(); generateTree(); animateAlgo(); // Single click to grow, but with debounce algoCanvas.addEventListener('click', handleAlgoClick); } function resizeAlgoCanvas() { if (!algoCanvas) return; algoCanvas.width = algoCanvas.offsetWidth; algoCanvas.height = algoCanvas.offsetHeight; } function generateTree() { if (isGenerating) return; isGenerating = true; treeBranches = []; const startX = algoCanvas.width / 2; const startY = algoCanvas.height - 20; // Limit recursion depth to prevent freezing const maxDepth = Math.min(generation, 6); try { growBranchSafe(startX, startY, -Math.PI / 2, 35, maxDepth, 0); } catch (e) { console.log('Generation stopped early to prevent freeze'); } isGenerating = false; updateGenCount(); } function growBranchSafe(x, y, angle, len, depth, currentDepth) { // Hard limit on total branches to prevent freezing if (treeBranches.length > 500 || currentDepth > 6) return; if (depth <= 0) return; const endX = x + Math.cos(angle) * len; const endY = y + Math.sin(angle) * len; treeBranches.push({ x1: x, y1: y, x2: endX, y2: endY, angle: angle, len: len, depth: currentDepth, progress: 0, fullyGrown: false }); // Smaller branching factor for higher generations const newLen = len * 0.7; const angleOffset = 0.4; growBranchSafe(endX, endY, angle - angleOffset, newLen, depth - 1, currentDepth + 1); growBranchSafe(endX, endY, angle + angleOffset, newLen, depth - 1, currentDepth + 1); } function handleAlgoClick(e) { if (isGenerating) return; // Prevent clicks during generation const rect = algoCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; let clicked = false; // Find clicked branch and grow from it for (let i = treeBranches.length - 1; i >= 0; i--) { const b = treeBranches[i]; const midX = (b.x1 + b.x2) / 2; const midY = (b.y1 + b.y2) / 2; const dist = Math.sqrt((x - midX) ** 2 + (y - midY) ** 2); if (dist < 20 && b.depth < 5 && treeBranches.length < 400) { // Add two new branches const newLen = b.len * 0.7; const newDepth = b.depth + 1; treeBranches.push({ x1: b.x2, y1: b.y2, x2: b.x2 + Math.cos(b.angle - 0.4) * newLen, y2: b.y2 + Math.sin(b.angle - 0.4) * newLen, angle: b.angle - 0.4, len: newLen, depth: newDepth, progress: 0, fullyGrown: false }); treeBranches.push({ x1: b.x2, y1: b.y2, x2: b.x2 + Math.cos(b.angle + 0.4) * newLen, y2: b.y2 + Math.sin(b.angle + 0.4) * newLen, angle: b.angle + 0.4, len: newLen, depth: newDepth, progress: 0, fullyGrown: false }); if (algoAudioEnabled) { const freq = 300 + newDepth * 100; const { osc } = createOscillator(freq, 'sine', 0.15); osc.start(); osc.stop(initAudio().currentTime + 0.15); } clicked = true; break; // Only grow one branch per click } } if (!clicked && treeBranches.length < 300) { // If didn't click on branch, add random small branch const randomBranch = treeBranches[Math.floor(Math.random() * treeBranches.length)]; if (randomBranch && randomBranch.depth < 4) { const newLen = randomBranch.len * 0.6; treeBranches.push({ x1: randomBranch.x2, y1: randomBranch.y2, x2: randomBranch.x2 + Math.cos(randomBranch.angle + 0.5) * newLen, y2: randomBranch.y2 + Math.sin(randomBranch.angle + 0.5) * newLen, angle: randomBranch.angle + 0.5, len: newLen, depth: randomBranch.depth + 1, progress: 0, fullyGrown: false }); } } } function animateAlgo() { if (!algoCtx) return; const ctx = algoCtx; const w = algoCanvas.width; const h = algoCanvas.height; ctx.fillStyle = '#f5f5f4'; ctx.fillRect(0, 0, w, h); let growing = false; treeBranches.forEach(b => { if (b.progress < 1) { b.progress += 0.08; growing = true; } else { b.progress = 1; b.fullyGrown = true; } const curX2 = b.x1 + (b.x2 - b.x1) * Math.min(b.progress, 1); const curY2 = b.y1 + (b.y2 - b.y1) * Math.min(b.progress, 1); const hue = 160 + (generation - b.depth) * 25; const lightness = 35 + b.depth * 6; ctx.strokeStyle = `hsl(${hue}, 60%, ${lightness}%)`; ctx.lineWidth = Math.max(0.5, (6 - b.depth) * 0.8); ctx.beginPath(); ctx.moveTo(b.x1, b.y1); ctx.lineTo(curX2, curY2); ctx.stroke(); // Draw leaf at end if fully grown and at max depth if (b.fullyGrown && b.depth >= 3) { ctx.fillStyle = '#059669'; ctx.beginPath(); ctx.arc(b.x2, b.y2, 2.5, 0, Math.PI * 2); ctx.fill(); } }); requestAnimationFrame(animateAlgo); } function regenerate() { if (isGenerating) return; generation = 4; generateTree(); } function iterate() { if (isGenerating || generation >= 6) return; // Cap at 6 to prevent freeze generation++; generateTree(); } function updateGenCount() { const el = document.getElementById('gen-count'); if (el) el.textContent = generation; } function showRules() { alert('L-System Rules:\n\nAxiom: F\nF → F[+F]F[-F]F\n\n[ = Push state\n] = Pop state\n+ = Turn left 25°\n- = Turn right 25°\n\nClick on branches to grow new ones.\nCurrent Generation: ' + generation); } function playMelody() { if (!algoAudioEnabled) { algoAudioEnabled = true; initAudio(); } // Play a simple melody based on branch depths const melody = treeBranches.slice(0, 20); melody.forEach((b, i) => { setTimeout(() => { if (!algoAudioEnabled) return; const freq = 200 + b.depth * 80 + (b.x2 % 100); const { osc } = createOscillator(freq, 'triangle', 0.12); osc.start(); osc.stop(initAudio().currentTime + 0.12); }, i * 80); }); } // Category filtering document.querySelectorAll('.category-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); const filter = this.dataset.filter; document.querySelectorAll('.project-card').forEach(card => { card.style.display = (filter === 'all' || card.dataset.category.includes(filter)) ? 'block' : 'none'; }); }); }); // Initialize all on load window.addEventListener('load', () => { initLandscapes(); initGeospatial(); initStigmergy(); initAudioImprov(); initPhase(); initAlgo(); }); // Handle resize window.addEventListener('resize', () => { if (landscapesCanvas) { landscapesCanvas.width = landscapesCanvas.offsetWidth; landscapesCanvas.height = landscapesCanvas.offsetHeight; } if (geoCanvas) { geoCanvas.width = geoCanvas.offsetWidth; geoCanvas.height = geoCanvas.offsetHeight; } if (stigCanvas) { stigCanvas.width = stigCanvas.offsetWidth; stigCanvas.height = stigCanvas.offsetHeight; } if (audioCanvas) { audioCanvas.width = audioCanvas.offsetWidth; audioCanvas.height = audioCanvas.offsetHeight; } if (phaseCanvas) { phaseCanvas.width = phaseCanvas.offsetWidth; phaseCanvas.height = phaseCanvas.offsetHeight; } if (algoCanvas) { resizeAlgoCanvas(); // Don't regenerate on resize, just clear and redraw } });