Skip to content

Latest commit

 

History

History
926 lines (703 loc) · 28.8 KB

File metadata and controls

926 lines (703 loc) · 28.8 KB

Chapter 39: Live-Coding, Three.js, and Real-Time Visuals

Part 7 — Creative Coding and Shaders | Prerequisites: Part 0 + Part 1 + Ch 36 | Difficulty: Intermediate | Language: JS/GLSL

In This Chapter

  • Live-coding as performance: Algorave, Hydra, and the live-coding scene
  • Three.js fundamentals: scene, camera, renderer, mesh, material
  • Instanced meshes for rendering thousands of objects
  • Custom shader materials in Three.js
  • Audio-reactive visuals: connecting Web Audio API to shader uniforms
  • canvas-sketch: a framework for creative coding with export
  • Exporting GIFs, videos, and still frames for sharing
  • The VJ workflow: real-time visuals for live events

Related Chapters

This chapter is about making visuals that MOVE -- in real-time, on stage, in the browser, as GIFs that stop people scrolling. We'll build 3D scenes with Three.js, animate them with trigonometry and easing, make shaders that react to music, and export everything as shareable media. This is creative coding at its most performative: visuals as instrument, code as performance.


39.1 Live-Coding as Performance

Live-coding means writing code in front of an audience while the output is projected. You type, the visuals change. Mistakes are part of the show. The audience sees your process, your thinking, your creative decisions in real-time.

This isn't theoretical -- live-coding has a thriving scene:

  • Algorave: dance events where all music and visuals are live-coded
  • TOPLAP: the live-coding organization (toplap.org)
  • Live-coding tools: Hydra (video synth), Sonic Pi (music), TidalCycles (patterns), Strudel (browser-based), KodeLife (shaders)

The philosophy: show your screen. Make your process visible. Embrace errors as creative opportunities. The constraint of real-time creation forces a different kind of creativity -- intuitive, improvisational, performative.

Hydra: The Browser Video Synth

Hydra (hydra.ojack.xyz) by Olivia Jack lets you live-code video synthesis in one line:

// In Hydra's browser editor:
osc(10, 0.1, 1.2).rotate(0.5).modulate(noise(3, 0.1)).out()

// Feedback loop:
src(o0).rotate(0.01).modulate(osc(3), 0.01).blend(osc(10, 0.1, 1), 0.3).out()

// Audio reactive:
a.show()  // show audio waveform
osc(10, 0, () => a.fft[0] * 4).out()  // oscillator frequency driven by bass

Hydra chains transformations like Unix pipes. Each function takes an input and produces an output. The result is projected live. It's the fastest path from idea to visual.


39.2 Three.js Fundamentals for Creative Coding

Three.js is a JavaScript library that wraps WebGL, making 3D accessible without writing raw GPU code. For creative coding, Three.js provides the geometry, materials, and rendering pipeline so you can focus on composition, animation, and aesthetics.

Scene, Camera, Renderer -- The Three Pillars

import * as THREE from 'three';

// Scene: the container for everything
const scene = new THREE.Scene();

// Camera: what you see through
const camera = new THREE.PerspectiveCamera(
  50,                              // field of view (degrees)
  window.innerWidth / window.innerHeight,  // aspect ratio
  0.1,                             // near clipping plane
  100                              // far clipping plane
);
camera.position.set(0, 0, 5);     // pull camera back

// Renderer: draws the scene
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

Geometries: The Shapes

Three.js provides parametric geometries out of the box:

const boxGeo = new THREE.BoxGeometry(1, 1, 1);
const sphereGeo = new THREE.SphereGeometry(0.5, 32, 32);
const torusGeo = new THREE.TorusGeometry(0.5, 0.2, 16, 100);
const torusKnotGeo = new THREE.TorusKnotGeometry(0.5, 0.15, 100, 16);
const icosaGeo = new THREE.IcosahedronGeometry(0.5, 0);
const planeGeo = new THREE.PlaneGeometry(2, 2, 32, 32);

Materials: The Surface Appearance

// Flat color, no lighting needed
const basicMat = new THREE.MeshBasicMaterial({ color: 0x4488ff });

// Reacts to light (diffuse + specular)
const standardMat = new THREE.MeshStandardMaterial({
  color: 0xff6644,
  roughness: 0.4,
  metalness: 0.1,
});

// Physical material (more accurate)
const physicalMat = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  roughness: 0.1,
  metalness: 0.9,
  clearcoat: 1.0,
  clearcoatRoughness: 0.1,
});

// Wireframe
const wireMat = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x00ffaa });

// Custom shader
const shaderMat = new THREE.ShaderMaterial({
  vertexShader: `...`,
  fragmentShader: `...`,
  uniforms: { u_time: { value: 0 } },
});

Meshes: Geometry + Material

const mesh = new THREE.Mesh(sphereGeo, standardMat);
scene.add(mesh);

That's it. Geometry defines shape, material defines appearance, mesh combines them and places them in the scene. Everything in Three.js follows this pattern.


39.3 Applying Images to Meshes: Texture Mapping

Wrapping images around 3D objects is one of the most satisfying creative coding techniques:

const textureLoader = new THREE.TextureLoader();

// Load an image as a texture
const texture = textureLoader.load('earth.jpg');

const earthMat = new THREE.MeshStandardMaterial({
  map: texture,          // color texture
  roughness: 0.8,
});

const earth = new THREE.Mesh(
  new THREE.SphereGeometry(1, 64, 64),
  earthMat
);
scene.add(earth);

With a good Earth texture and rotation, you've got a globe in 10 lines. Add a starfield background (scene.background = new THREE.CubeTextureLoader().load(...)) and you're in space.

Canvas as Texture

You can also use a 2D canvas as a texture -- draw anything with Canvas2D and map it onto 3D geometry:

const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');

// Draw something on the canvas
ctx.fillStyle = '#222';
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = '#fff';
ctx.font = 'bold 80px monospace';
ctx.fillText('HELLO', 100, 280);

const canvasTexture = new THREE.CanvasTexture(canvas);
const mat = new THREE.MeshBasicMaterial({ map: canvasTexture });

Update the canvas each frame and call canvasTexture.needsUpdate = true to animate.


39.4 Grouping and Hierarchy

Three.js uses a scene graph. A Group is a container whose transform applies to all children:

const group = new THREE.Group();

for (let i = 0; i < 12; i++) {
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(0.2, 0.2, 0.2),
    new THREE.MeshStandardMaterial({ color: new THREE.Color().setHSL(i / 12, 0.7, 0.6) })
  );

  // Position in a circle
  const angle = (i / 12) * Math.PI * 2;
  mesh.position.set(Math.cos(angle) * 1.5, Math.sin(angle) * 1.5, 0);

  group.add(mesh);
}

scene.add(group);

// Now rotating the group rotates ALL children
function animate(time) {
  group.rotation.z = time * 0.001;
  group.rotation.x = Math.sin(time * 0.0005) * 0.5;
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate(0);

Groups are how you build complex hierarchical structures. A solar system: sun group contains planet groups, planet groups contain moon meshes. Rotate a planet group and its moons orbit with it.


39.5 Lighting

Lighting makes the difference between flat shapes and dimensionality:

// Ambient: uniform fill light (prevents pure-black shadows)
const ambient = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambient);

// Directional: sunlight (parallel rays from a direction)
const directional = new THREE.DirectionalLight(0xffffff, 1.0);
directional.position.set(5, 5, 5);
scene.add(directional);

// Point: light bulb (radiates in all directions from a point)
const point = new THREE.PointLight(0xff4400, 1.0, 10);
point.position.set(-2, 2, 1);
scene.add(point);

// Hemisphere: sky/ground gradient
const hemi = new THREE.HemisphereLight(0x88ccff, 0x444422, 0.6);
scene.add(hemi);

For creative coding, hemisphere light + one directional light is a reliable setup. It gives you dimension without harsh shadows.


39.6 Isometric Perspective

An orthographic camera creates the isometric look popular in creative coding -- no perspective distortion, clean geometric aesthetics:

const frustumSize = 5;
const aspect = window.innerWidth / window.innerHeight;

const camera = new THREE.OrthographicCamera(
  -frustumSize * aspect / 2,
  frustumSize * aspect / 2,
  frustumSize / 2,
  -frustumSize / 2,
  0.1, 100
);

// Classic isometric angle
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);

Isometric scenes have a toy-like, architectural quality. Perfect for grid-based compositions, voxel art, and abstract geometric pieces.

What happens if... you combine isometric camera with a grid of objects that have noise-driven heights? You get a rolling landscape of geometric blocks that looks like a miniature world.


39.7 Instancing: Many Meshes, One Draw Call

Drawing 10,000 individual meshes is slow. Instancing draws them all in a single GPU call:

const count = 10000;
const geometry = new THREE.IcosahedronGeometry(0.05, 0);
const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
const instanced = new THREE.InstancedMesh(geometry, material, count);

const dummy = new THREE.Object3D();
const color = new THREE.Color();

for (let i = 0; i < count; i++) {
  // Random position in a sphere
  const theta = Math.random() * Math.PI * 2;
  const phi = Math.acos(Math.random() * 2 - 1);
  const r = Math.cbrt(Math.random()) * 3;

  dummy.position.set(
    r * Math.sin(phi) * Math.cos(theta),
    r * Math.sin(phi) * Math.sin(theta),
    r * Math.cos(phi)
  );
  dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
  dummy.scale.setScalar(0.5 + Math.random() * 1.5);
  dummy.updateMatrix();
  instanced.setMatrixAt(i, dummy.matrix);

  // Vary color per instance
  color.setHSL(Math.random() * 0.1 + 0.55, 0.6, 0.4 + Math.random() * 0.3);
  instanced.setColorAt(i, color);
}

instanced.instanceMatrix.needsUpdate = true;
instanced.instanceColor.needsUpdate = true;
scene.add(instanced);

10,000 icosahedra in a spherical cloud, each with unique color, position, and scale. Runs at 60fps. This is how you build dense, rich creative scenes.

Animating Instances

function animate(time) {
  const t = time * 0.001;

  for (let i = 0; i < count; i++) {
    instanced.getMatrixAt(i, dummy.matrix);
    dummy.matrix.decompose(dummy.position, dummy.quaternion, dummy.scale);

    // Oscillate each instance based on its index
    dummy.position.y += Math.sin(t + i * 0.1) * 0.001;
    dummy.rotation.x = t + i * 0.01;

    dummy.updateMatrix();
    instanced.setMatrixAt(i, dummy.matrix);
  }

  instanced.instanceMatrix.needsUpdate = true;
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

39.8 Color Variation and Composition

Matt DesLauriers emphasizes that color and composition separate amateur work from exhibition-quality pieces.

Color Strategies

// 1. Monochromatic with value variation
const hue = 0.6;  // blue
for (let i = 0; i < 100; i++) {
  const lightness = 0.3 + Math.random() * 0.4;
  const saturation = 0.4 + Math.random() * 0.3;
  color.setHSL(hue, saturation, lightness);
}

// 2. Analogous (neighboring hues)
for (let i = 0; i < 100; i++) {
  const h = 0.6 + (Math.random() - 0.5) * 0.1;  // blue +/- 10%
  color.setHSL(h, 0.5 + Math.random() * 0.3, 0.3 + Math.random() * 0.4);
}

// 3. From a curated palette (best approach)
const palette = ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'];
const c = new THREE.Color(palette[Math.floor(Math.random() * palette.length)]);

Composition Tips

  • Margins matter. Never fill edge-to-edge. Leave breathing room.
  • Vary density. Dense clusters + empty space is more interesting than uniform distribution.
  • Use Gaussian distribution for size/position -- cluster near center, thin at edges.
  • Limit your palette to 3-5 colors. Fewer colors = more cohesion.
  • Add one accent color used sparingly -- it draws the eye.

39.9 Easing and Trigonometry for Animation

Smooth animation is the difference between "programmer art" and "art."

Easing Functions

// Linear (boring)
const linear = t => t;

// Ease in (slow start)
const easeIn = t => t * t;

// Ease out (slow end)
const easeOut = t => 1 - (1 - t) * (1 - t);

// Ease in-out (smooth both ends)
const easeInOut = t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;

// Elastic (bouncy overshoot)
const elastic = t => {
  const c4 = (2 * Math.PI) / 3;
  return t === 0 ? 0 : t === 1 ? 1 :
    Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
};

Trigonometric Animation Patterns

function animate(time) {
  const t = time * 0.001;

  // Smooth oscillation
  mesh.position.y = Math.sin(t) * 0.5;

  // Circular orbit
  mesh.position.x = Math.cos(t) * 2;
  mesh.position.z = Math.sin(t) * 2;

  // Figure-8 (Lissajous)
  mesh.position.x = Math.sin(t) * 2;
  mesh.position.z = Math.sin(t * 2) * 1;

  // Breathing scale
  const breath = 1.0 + Math.sin(t * 2) * 0.1;
  mesh.scale.setScalar(breath);

  // Staggered animation (each object offset in time)
  for (let i = 0; i < group.children.length; i++) {
    const child = group.children[i];
    const offset = i * 0.2;
    child.position.y = Math.sin(t * 2 + offset) * 0.3;
    child.rotation.y = t + offset;
  }

  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

The staggered offset is everything. Without it, all objects move in lockstep -- robotic. With it, a wave ripples through the group -- organic, alive.

What happens if... you use Math.sin(t + i * 0.2) vs Math.sin(t * 2 + i * 0.1)? The first creates a spatial wave (each object offset in phase). The second creates a faster wave with tighter spacing. Try Math.sin(t + i * i * 0.01) for an accelerating wave.


39.10 canvas-sketch + Three.js Integration

Matt DesLauriers' canvas-sketch provides the infrastructure for looping animations and export:

const canvasSketch = require('canvas-sketch');
const THREE = require('three');

const settings = {
  animate: true,
  dimensions: [1024, 1024],
  fps: 30,
  duration: 4,  // loop duration in seconds
  context: 'webgl',
};

const sketch = ({ context }) => {
  const renderer = new THREE.WebGLRenderer({ canvas: context.canvas });
  renderer.setClearColor('#1a1a2e', 1);

  const scene = new THREE.Scene();
  const camera = new THREE.OrthographicCamera(-2, 2, 2, -2, 0.1, 100);
  camera.position.set(4, 4, 4);
  camera.lookAt(0, 0, 0);

  // Lighting
  scene.add(new THREE.AmbientLight('#555'));
  const light = new THREE.DirectionalLight('#fff', 1);
  light.position.set(3, 5, 2);
  scene.add(light);

  // Grid of cubes
  const meshes = [];
  const gridSize = 5;
  for (let x = 0; x < gridSize; x++) {
    for (let z = 0; z < gridSize; z++) {
      const geo = new THREE.BoxGeometry(0.3, 0.3, 0.3);
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(
          (x + z) / (gridSize * 2), 0.6, 0.5
        ),
        roughness: 0.5,
      });
      const mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(
        x - gridSize / 2 + 0.5,
        0,
        z - gridSize / 2 + 0.5
      );
      scene.add(mesh);
      meshes.push({ mesh, x, z });
    }
  }

  return {
    render({ playhead }) {
      // playhead goes 0 -> 1 over the duration, then loops
      const t = playhead * Math.PI * 2;

      for (let { mesh, x, z } of meshes) {
        // Wave animation: cubes rise and fall in a wave
        const delay = (x + z) * 0.3;
        const height = Math.sin(t + delay) * 0.5 + 0.5;
        mesh.position.y = height * 1.5;
        mesh.scale.y = 0.5 + height;

        // Color shift
        mesh.material.color.setHSL(
          (playhead + (x + z) / (gridSize * 2)) % 1, 0.6, 0.3 + height * 0.3
        );
      }

      renderer.render(scene, camera);
    },
  };
};

canvasSketch(sketch, settings);

Exporting GIFs and MP4s

canvas-sketch makes export simple:

# Export frames as PNG sequence
canvas-sketch my-sketch.js --output=frames

# Then convert to GIF:
gifski --fps 30 --width 512 frames/*.png -o output.gif

# Or to MP4 with ffmpeg:
ffmpeg -framerate 30 -i frames/%05d.png -c:v libx264 -pix_fmt yuv420p output.mp4

Or use the built-in Cmd+Shift+S during canvas-sketch playback to capture a single frame.

For looping animations, set duration in settings and use playhead (0 to 1) instead of raw time. When playhead reaches 1, it loops back to 0 seamlessly.


39.11 Audio-Reactive Visuals

Connecting sound to visuals is the ultimate creative coding experience. The core idea: analyze audio frequencies with FFT, pass them as uniforms to your shader or animation.

Web Audio API: FFT Analysis

let analyser, dataArray;

async function initAudio() {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const audioCtx = new AudioContext();
  const source = audioCtx.createMediaStreamSource(stream);

  analyser = audioCtx.createAnalyser();
  analyser.fftSize = 256;
  const bufferLength = analyser.frequencyBinCount;  // 128
  dataArray = new Uint8Array(bufferLength);

  source.connect(analyser);
}

function getFrequencyData() {
  analyser.getByteFrequencyData(dataArray);

  // Frequency bands (roughly)
  let bass = 0, mid = 0, treble = 0;
  for (let i = 0; i < 10; i++) bass += dataArray[i];       // 0-400Hz
  for (let i = 10; i < 60; i++) mid += dataArray[i];       // 400Hz-5kHz
  for (let i = 60; i < 128; i++) treble += dataArray[i];   // 5kHz+

  return {
    bass: bass / (10 * 255),      // normalized 0-1
    mid: mid / (50 * 255),
    treble: treble / (68 * 255),
    raw: dataArray,
  };
}

Audio-Reactive Shader

// Fragment shader
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_bass;
uniform float u_mid;
uniform float u_treble;

void main() {
    vec2 uv = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    uv.x *= u_resolution.x / u_resolution.y;

    // Bass drives overall scale
    float d = length(uv) / (0.5 + u_bass * 1.5);

    // Treble drives detail frequency
    float detail = sin(d * (10.0 + u_treble * 40.0) - u_time * 3.0);
    detail = smoothstep(-0.1, 0.1, detail);

    // Mid drives color cycling speed
    float hue = u_time * 0.2 * (1.0 + u_mid * 3.0);

    // Color
    vec3 col;
    col.r = sin(hue + d * 3.0) * 0.5 + 0.5;
    col.g = sin(hue + d * 3.0 + 2.094) * 0.5 + 0.5;
    col.b = sin(hue + d * 3.0 + 4.189) * 0.5 + 0.5;

    col *= detail;

    // Bass pulse: bright flash on beat
    col += vec3(1.0) * pow(u_bass, 4.0) * 0.3;

    fragColor = vec4(col, 1.0);
}

JavaScript Animation Loop with Audio

function animate(time) {
  if (analyser) {
    const { bass, mid, treble } = getFrequencyData();

    // Smooth the values (avoid jitter)
    smoothBass += (bass - smoothBass) * 0.3;
    smoothMid += (mid - smoothMid) * 0.2;
    smoothTreble += (treble - smoothTreble) * 0.2;

    shader.uniforms.u_bass = smoothBass;
    shader.uniforms.u_mid = smoothMid;
    shader.uniforms.u_treble = smoothTreble;
  }

  shader.uniforms.u_time = time * 0.001;
  shader.render();
  requestAnimationFrame(animate);
}

The smoothing is critical. Raw FFT data is noisy. Without smoothing, visuals jitter and flicker. The 0.3 lerp factor is a good starting point -- smaller values for smoother, more delayed response; larger values for more reactive, jittery response.

What happens if... you drive the Three.js instanced mesh positions with audio? Each instance's Y position modulated by its frequency bin creates a 3D equalizer. Or drive the scale of objects with bass for a "breathing" effect.


39.12 Audio-Reactive Three.js Scene

A complete example combining Three.js, instancing, and audio:

import * as THREE from 'three';

const scene = new THREE.Scene();
scene.background = new THREE.Color('#0a0a15');

const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

scene.add(new THREE.AmbientLight('#333'));
const light = new THREE.PointLight('#4488ff', 2, 20);
light.position.set(0, 5, 0);
scene.add(light);

// Ring of torus knots
const count = 32;
const group = new THREE.Group();
const meshes = [];

for (let i = 0; i < count; i++) {
  const geo = new THREE.TorusKnotGeometry(0.15, 0.05, 64, 8);
  const mat = new THREE.MeshStandardMaterial({
    color: new THREE.Color().setHSL(i / count, 0.7, 0.5),
    roughness: 0.3,
    metalness: 0.6,
  });
  const mesh = new THREE.Mesh(geo, mat);

  const angle = (i / count) * Math.PI * 2;
  mesh.position.set(Math.cos(angle) * 3, 0, Math.sin(angle) * 3);
  mesh.lookAt(0, 0, 0);

  group.add(mesh);
  meshes.push(mesh);
}
scene.add(group);

// Audio setup (click to start)
let analyser, dataArray;
let smoothBass = 0;

document.addEventListener('click', async () => {
  const audioCtx = new AudioContext();
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const source = audioCtx.createMediaStreamSource(stream);
  analyser = audioCtx.createAnalyser();
  analyser.fftSize = 256;
  dataArray = new Uint8Array(analyser.frequencyBinCount);
  source.connect(analyser);
});

function animate(time) {
  const t = time * 0.001;

  // Get audio data
  let bass = 0;
  if (analyser) {
    analyser.getByteFrequencyData(dataArray);
    for (let i = 0; i < 8; i++) bass += dataArray[i];
    bass /= (8 * 255);
    smoothBass += (bass - smoothBass) * 0.3;
  }

  // Animate: rotation + audio-driven scale
  group.rotation.y = t * 0.3;

  for (let i = 0; i < meshes.length; i++) {
    const mesh = meshes[i];
    const binIndex = Math.floor(i / count * 128);
    const freqVal = analyser ? dataArray[binIndex] / 255 : 0;

    // Each mesh reacts to its frequency band
    const scale = 0.5 + freqVal * 2;
    mesh.scale.setScalar(scale);

    // Vertical bounce
    mesh.position.y = Math.sin(t * 2 + i * 0.3) * 0.3 + freqVal * 1.5;

    // Spin
    mesh.rotation.x = t + i * 0.1;
    mesh.rotation.z = t * 0.5 + i * 0.05;

    // Color intensity with bass
    mesh.material.emissive = new THREE.Color().setHSL(
      (i / count + t * 0.1) % 1, 0.8, smoothBass * 0.3
    );
  }

  // Light pulses with bass
  light.intensity = 1 + smoothBass * 5;
  light.color.setHSL((t * 0.05) % 1, 0.8, 0.5);

  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate(0);

Click the page, play music, and watch the torus knots dance. Each one reacts to a different frequency band. The bass drives overall light intensity. The ring rotates smoothly while individual elements bounce and scale.


39.13 Live-Coding Tool Reference

Tool What It Does Language Platform
Shadertoy Fragment shader playground GLSL Browser
p5.js editor Creative coding IDE JavaScript Browser
Hydra Live-coded video synth JavaScript Browser
KodeLife Real-time shader editor GLSL Desktop
canvas-sketch Generative art framework JavaScript Node.js
Three.js editor 3D scene builder JavaScript Browser
Strudel Live-coded music patterns JavaScript Browser
TidalCycles Algorithmic music Haskell Desktop
Sonic Pi Music live-coding Ruby-like Desktop
TouchDesigner Node-based visual programming Python Desktop

VJ Culture

VJs (visual jockeys) create real-time visuals for concerts, clubs, and festivals. The workflow:

  1. Build a library of scenes/shaders/loops
  2. Map MIDI controllers to parameters (brightness, speed, color, effect intensity)
  3. Trigger and mix visuals live, synchronized to music
  4. Layer multiple sources with blend modes

Many VJs use Resolume, VDMX, or TouchDesigner. But a growing number use code: Hydra, custom GLSL, or Three.js with MIDI input.

// WebMIDI for controller input
navigator.requestMIDIAccess().then((access) => {
  for (let input of access.inputs.values()) {
    input.onmidimessage = (msg) => {
      const [status, control, value] = msg.data;
      if (status === 176) {  // CC message
        // Map control number to parameter
        params[control] = value / 127;  // normalize to 0-1
      }
    };
  }
});

Map a knob to noise scale. Map a fader to color hue. Map a button to trigger a flash. Physical controls make live-coding performative and expressive.


39.14 Build: Complete Animated Scene with Export

Putting it all together -- a canvas-sketch + Three.js scene ready for GIF export:

const canvasSketch = require('canvas-sketch');
const THREE = require('three');

const settings = {
  dimensions: [800, 800],
  fps: 30,
  duration: 3,
  animate: true,
  context: 'webgl',
};

const sketch = ({ context }) => {
  const renderer = new THREE.WebGLRenderer({ canvas: context.canvas });
  renderer.setClearColor('#120818', 1);

  const scene = new THREE.Scene();
  const camera = new THREE.OrthographicCamera(-3, 3, 3, -3, 0.1, 100);
  camera.position.set(5, 5, 5);
  camera.lookAt(0, 0, 0);

  scene.add(new THREE.AmbientLight('#2a1a3a', 0.6));
  const dir = new THREE.DirectionalLight('#eeddff', 0.8);
  dir.position.set(3, 5, 2);
  scene.add(dir);

  const palette = ['#ff6b6b', '#ffa07a', '#98d8c8', '#6c5ce7', '#fdcb6e'];
  const meshes = [];
  const gridSize = 7;

  for (let x = 0; x < gridSize; x++) {
    for (let z = 0; z < gridSize; z++) {
      const types = [
        new THREE.BoxGeometry(0.35, 0.35, 0.35),
        new THREE.SphereGeometry(0.2, 16, 16),
        new THREE.ConeGeometry(0.2, 0.4, 8),
        new THREE.TorusGeometry(0.15, 0.06, 8, 20),
      ];
      const geo = types[Math.floor(Math.random() * types.length)];
      const col = palette[Math.floor(Math.random() * palette.length)];
      const mat = new THREE.MeshStandardMaterial({
        color: col,
        roughness: 0.5,
        metalness: 0.2,
      });
      const mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(x - gridSize / 2 + 0.5, 0, z - gridSize / 2 + 0.5);

      scene.add(mesh);
      meshes.push({ mesh, x, z, baseY: 0 });
    }
  }

  return {
    render({ playhead }) {
      const t = playhead * Math.PI * 2;

      for (let { mesh, x, z } of meshes) {
        // Radial wave from center
        const dx = x - gridSize / 2 + 0.5;
        const dz = z - gridSize / 2 + 0.5;
        const dist = Math.sqrt(dx * dx + dz * dz);

        const wave = Math.sin(t - dist * 0.8) * 0.5 + 0.5;
        mesh.position.y = wave * 2;
        mesh.rotation.y = t + dist * 0.3;
        mesh.rotation.x = Math.sin(t + dist) * 0.5;

        // Scale pulse
        const s = 0.8 + wave * 0.4;
        mesh.scale.setScalar(s);
      }

      renderer.render(scene, camera);
    },
  };
};

canvasSketch(sketch, settings);

Run with canvas-sketch my-scene.js --open. Press Cmd+Shift+S to export the loop as a PNG sequence, then convert to GIF. The playhead-based animation guarantees a perfect loop.


Key Takeaways:

  • Live-coding is performance art -- code as instrument, visuals as music
  • Three.js provides Scene + Camera + Renderer; Mesh = Geometry + Material
  • Instancing (InstancedMesh) renders thousands of objects efficiently
  • Isometric cameras (orthographic) create clean, architectural aesthetics
  • Easing functions and staggered offsets make animation feel organic, not robotic
  • canvas-sketch + Three.js + playhead creates perfect looping animations exportable as GIF/MP4
  • Audio-reactive visuals: FFT frequency data drives shader uniforms and animation parameters
  • Smooth audio input with lerp to avoid jitter; separate bass/mid/treble for different visual effects
  • Hydra, Shadertoy, KodeLife, and p5.js editor are the essential live-coding playgrounds
  • VJ culture: MIDI controllers mapped to visual parameters for live performance