creative-code-lab

Use when user asks to create generative art, creative coding sketches, visual experiments, particle systems, shader effects, procedural graphics, interactive canvases, or any code-as-art project. Use when building animated backgrounds, flow fields, fractals, L-systems, noise-based visuals, text effects, fluid simulations, or interactive 3D scenes. Triggers on words like "generative", "sketch", "particles", "shader", "creative coding", "visual experiment", "procedural", "noise", "flow field".

Creative Code Lab

Generate interactive generative art and creative coding sketches. Inspired by Leisure Lab (43 sketches by KainXu).

Overview

Creative coding = algorithms made visible. Every sketch transforms math into something you can see, touch, and feel.

The output format is flexible: single self-contained HTML file, React component, Next.js page, p5.js sketch, or raw Canvas/WebGL - whatever fits the user's project.

When to Use

  • User wants generative/procedural art
  • Building animated backgrounds or hero sections
  • Creating interactive visual experiments
  • Implementing particle systems, flow fields, shaders, fractals
  • "Make something cool/beautiful with code"
  • Image-to-art transformations
  • Creative 404 pages, loading screens, visual flourishes

Technique Catalog

Pick the right approach for the visual:

What visual are you building?
|
+-- Flowing, organic motion?
|   +-- Particles following forces --> Flow Field (Perlin noise)
|   +-- Smooth color blending --> Fluid Shader (WebGL fragment)
|   +-- Swirling paint effect --> Fluid Swirl (iterative sine + spiral distortion)
|
+-- Nature / biology?
|   +-- Trees, plants, corals --> L-System (recursive branching)
|   +-- Cell division, growth --> Cellular Automata (neighbor rules)
|   +-- Veins, rivers, cracks --> Diffusion-Limited Aggregation
|
+-- Geometric / mathematical?
|   +-- Mondrian, grids --> Recursive Subdivision
|   +-- Space-filling patterns --> Hilbert/Peano Curves
|   +-- Spirals, roses --> Polar Coordinate Math
|   +-- Vortex patterns --> Parametric equations in (r, theta)
|
+-- Text effects?
|   +-- Text from particles --> Canvas pixel sampling + particle system
|   +-- Neon glow --> CSS text-shadow stacking or shader
|   +-- Glitch effect --> Random clip-path + color channel offset
|   +-- Blur/reveal --> CSS filter animation or shader blur
|
+-- Interactive / mouse-driven?
|   +-- Particles scatter from cursor --> Distance-based force repulsion
|   +-- Drawing / painting --> Canvas stroke with velocity-based width
|   +-- Cursor trail effects --> Ring buffer of positions + fade
|
+-- 3D scenes?
|   +-- Product showcase --> Three.js + GLTF + orbit controls
|   +-- Abstract geometry --> Three.js + custom shaders
|   +-- Camera depth blur --> Three.js postprocessing (DOF)
|
+-- Image transformation?
    +-- Photo to painting --> Algorithmic brush strokes on canvas
    +-- Pointillism / dots --> Pixel sampling + circle rendering
    +-- Pixel decomposition --> Grid sampling + animated scatter

Core Algorithms Reference

1. Perlin Noise Flow Field

The foundation of organic-looking particle motion. Particles follow a vector field generated by noise.

// Core concept: noise(x, y) returns smooth random value 0-1
// Map to angle: angle = noise(x * scale, y * scale) * TWO_PI
// Each particle follows the angle at its grid position

class FlowField {
  constructor(canvas, particleCount = 2000) {
    this.ctx = canvas.getContext('2d');
    this.w = canvas.width;
    this.h = canvas.height;
    this.scale = 0.005;       // Noise zoom (smaller = smoother)
    this.speed = 2;
    this.particles = Array.from({ length: particleCount }, () => ({
      x: Math.random() * this.w,
      y: Math.random() * this.h,
      prevX: 0, prevY: 0
    }));
  }

  // Attempt at simplex-like noise (for self-contained sketches)
  // For production, use a proper noise library
  noise2D(x, y) {
    const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
    return n - Math.floor(n);
  }

  update(time) {
    this.ctx.fillStyle = 'rgba(0, 0, 0, 0.02)'; // Trail fade
    this.ctx.fillRect(0, 0, this.w, this.h);

    for (const p of this.particles) {
      p.prevX = p.x;
      p.prevY = p.y;

      const angle = this.noise2D(
        p.x * this.scale + time * 0.0001,
        p.y * this.scale
      ) * Math.PI * 4;

      p.x += Math.cos(angle) * this.speed;
      p.y += Math.sin(angle) * this.speed;

      // Wrap around edges
      if (p.x < 0) p.x = this.w;
      if (p.x > this.w) p.x = 0;
      if (p.y < 0) p.y = this.h;
      if (p.y > this.h) p.y = 0;

      this.ctx.strokeStyle = `hsla(${angle * 30}, 70%, 60%, 0.3)`;
      this.ctx.beginPath();
      this.ctx.moveTo(p.prevX, p.prevY);
      this.ctx.lineTo(p.x, p.y);
      this.ctx.stroke();
    }
  }
}

Key parameters to tune:

  • scale (0.001-0.01): Noise zoom. Smaller = wider, smoother curves
  • speed (1-5): How fast particles move
  • Trail fade alpha (0.01-0.1): Lower = longer trails
  • particleCount: More = denser field, watch performance
  • Color mapping: Map angle, position, or velocity to hue

2. L-System (Procedural Trees/Plants)

Recursive string rewriting that draws botanical structures.

function lSystem(axiom, rules, iterations) {
  let current = axiom;
  for (let i = 0; i < iterations; i++) {
    current = current.split('').map(c => rules[c] || c).join('');
  }
  return current;
}

function drawLSystem(ctx, instructions, len, angle) {
  const stack = [];
  for (const char of instructions) {
    switch (char) {
      case 'F': // Draw forward
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(0, -len);
        ctx.stroke();
        ctx.translate(0, -len);
        break;
      case '+': ctx.rotate(angle); break;   // Turn right
      case '-': ctx.rotate(-angle); break;   // Turn left
      case '[': // Save state (branch start)
        stack.push(ctx.getTransform());
        break;
      case ']': // Restore state (branch end)
        ctx.setTransform(stack.pop());
        break;
    }
  }
}

// Classic tree
const tree = lSystem('F', { 'F': 'FF+[+F-F-F]-[-F+F+F]' }, 4);
// Fern
const fern = lSystem('X', { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' }, 6);

Common L-System presets:

NameAxiomRulesAngle
Binary treeFF -> FF+[+F-F]-[-F+F]25deg
FernXX -> F+[[X]-X]-F[-FX]+X, F -> FF25deg
BushFF -> F[+FF][-FF]F[-F][+F]F20deg
SeaweedFF -> FF-[-F+F+F]+[+F-F-F]22deg

3. WebGL Fragment Shader (Fluid/Gradient Effects)

For GPU-accelerated visuals. Single HTML file with inline shader.

<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl');
canvas.width = innerWidth;
canvas.height = innerHeight;

const vertSrc = `attribute vec2 p; void main(){gl_Position=vec4(p,0,1);}`;
const fragSrc = `
precision mediump float;
uniform float t;
uniform vec2 r;    // resolution
uniform vec2 m;    // mouse

void main() {
  vec2 uv = gl_FragCoord.xy / r;
  vec2 mouse = m / r;

  // Fluid swirl: iterative sine distortion
  for (int i = 0; i < 8; i++) {
    uv = vec2(
      sin(uv.y * 4.0 + t + float(i)) * 0.4 + uv.x,
      cos(uv.x * 4.0 + t + float(i)) * 0.4 + uv.y
    );
  }

  // Distance from mouse adds interaction
  float d = length(uv - mouse) * 2.0;

  vec3 col = vec3(
    sin(uv.x * 3.0 + t) * 0.5 + 0.5,
    sin(uv.y * 3.0 + t * 1.3) * 0.5 + 0.5,
    sin((uv.x + uv.y) * 2.0 + t * 0.7) * 0.5 + 0.5
  );

  gl_FragColor = vec4(col * (1.0 - d * 0.3), 1.0);
}`;

// Boilerplate: compile, link, draw fullscreen quad
function compile(type, src) {
  const s = gl.createShader(type);
  gl.shaderSource(s, src);
  gl.compileShader(s);
  return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vertSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc));
gl.linkProgram(prog);
gl.useProgram(prog);

const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
const p = gl.getAttribLocation(prog, 'p');
gl.enableVertexAttribArray(p);
gl.vertexAttribPointer(p, 2, gl.FLOAT, false, 0, 0);

const tLoc = gl.getUniformLocation(prog, 't');
const rLoc = gl.getUniformLocation(prog, 'r');
const mLoc = gl.getUniformLocation(prog, 'm');

let mx = 0, my = 0;
canvas.addEventListener('mousemove', e => { mx = e.clientX; my = canvas.height - e.clientY; });

(function loop(now) {
  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.uniform1f(tLoc, now * 0.001);
  gl.uniform2f(rLoc, canvas.width, canvas.height);
  gl.uniform2f(mLoc, mx, my);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(loop);
})(0);
</script>

Shader effect recipes (modify the main() body):

  • Plasma: sin(uv.x*10+t) + sin(uv.y*10+t) + sin((uv.x+uv.y)*10+t)
  • Voronoi cells: Distance to nearest random point in grid
  • Fractal noise: Layered sin() at increasing frequencies (octaves)
  • Distortion: texture2D(tex, uv + vec2(sin(uv.y*10+t)*0.02))

4. Particle Text Effect

Sample pixels from text rendered on a hidden canvas, spawn particles at dark pixel positions.

function createTextParticles(text, fontSize, canvas) {
  const ctx = canvas.getContext('2d');
  const w = canvas.width, h = canvas.height;

  // Render text to hidden canvas to sample pixels
  const offscreen = new OffscreenCanvas(w, h);
  const offCtx = offscreen.getContext('2d');
  offCtx.fillStyle = 'white';
  offCtx.font = `bold ${fontSize}px sans-serif`;
  offCtx.textAlign = 'center';
  offCtx.textBaseline = 'middle';
  offCtx.fillText(text, w / 2, h / 2);

  // Sample pixels at grid intervals
  const imageData = offCtx.getImageData(0, 0, w, h).data;
  const particles = [];
  const gap = 4; // Density: lower = more particles

  for (let y = 0; y < h; y += gap) {
    for (let x = 0; x < w; x += gap) {
      const alpha = imageData[(y * w + x) * 4 + 3];
      if (alpha > 128) {
        particles.push({
          targetX: x, targetY: y,
          x: Math.random() * w, y: Math.random() * h, // Start scattered
          vx: 0, vy: 0
        });
      }
    }
  }

  // Animate: spring toward target, repel from mouse
  function animate(mouseX, mouseY) {
    ctx.clearRect(0, 0, w, h);
    for (const p of particles) {
      const dx = mouseX - p.x, dy = mouseY - p.y;
      const dist = Math.sqrt(dx * dx + dy * dy);

      // Repel from mouse
      if (dist < 100) {
        p.vx -= dx / dist * 5;
        p.vy -= dy / dist * 5;
      }

      // Spring back to target
      p.vx += (p.targetX - p.x) * 0.05;
      p.vy += (p.targetY - p.y) * 0.05;
      p.vx *= 0.9; // Damping
      p.vy *= 0.9;
      p.x += p.vx;
      p.y += p.vy;

      ctx.fillStyle = '#fff';
      ctx.fillRect(p.x, p.y, 2, 2);
    }
  }

  return { animate, particles };
}

5. Cellular Automata / Vector Field

Emergent patterns from simple local rules.

function cellularField(canvas, gridSize = 100) {
  const ctx = canvas.getContext('2d');
  const cols = gridSize, rows = gridSize;
  const cellW = canvas.width / cols, cellH = canvas.height / rows;

  // Initialize grid with random values
  let grid = Array.from({ length: cols }, () =>
    Array.from({ length: rows }, () => Math.random())
  );

  // Random locked anchor points
  const anchors = Array.from({ length: 20 }, () => ({
    x: Math.floor(Math.random() * cols),
    y: Math.floor(Math.random() * rows),
    value: Math.random()
  }));

  function step() {
    const next = grid.map(row => [...row]);
    for (let x = 1; x < cols - 1; x++) {
      for (let y = 1; y < rows - 1; y++) {
        // Average neighbors
        next[x][y] = (
          grid[x-1][y] + grid[x+1][y] +
          grid[x][y-1] + grid[x][y+1]
        ) / 4 + (Math.random() - 0.5) * 0.01;
      }
    }
    // Re-apply anchors
    for (const a of anchors) next[a.x][a.y] = a.value;
    grid = next;
  }

  function draw() {
    for (let x = 0; x < cols; x++) {
      for (let y = 0; y < rows; y++) {
        const v = grid[x][y];
        ctx.fillStyle = `hsl(${v * 360}, 70%, 50%)`;
        ctx.fillRect(x * cellW, y * cellH, cellW, cellH);
      }
    }
  }

  return { step, draw };
}

6. Polar Coordinate Art (Vortex/Spiral)

Mathematical beauty from (r, theta) parametric equations.

function polarArt(canvas) {
  const ctx = canvas.getContext('2d');
  const cx = canvas.width / 2, cy = canvas.height / 2;

  function draw(time) {
    ctx.fillStyle = 'rgba(0,0,0,0.05)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    for (let theta = 0; theta < Math.PI * 20; theta += 0.02) {
      // The art is in this formula - experiment!
      const r = 150 * Math.sin(theta * 3 + time * 0.001)
              + 50 * Math.cos(theta * 7 - time * 0.002);

      const x = cx + r * Math.cos(theta);
      const y = cy + r * Math.sin(theta);

      ctx.fillStyle = `hsl(${theta * 20 + time * 0.05}, 80%, 60%)`;
      ctx.fillRect(x, y, 2, 2);
    }
  }

  return { draw };
}

Formula playground (swap into the r = ... line):

  • Rose: r = 200 * cos(n * theta) (n controls petals)
  • Spiral: r = theta * 5
  • Lissajous: x = A*sin(a*t+d), y = B*sin(b*t) (use cartesian)
  • Butterfly: r = exp(sin(theta)) - 2*cos(4*theta) + sin((2*theta-PI)/24)^5

7. Three.js 3D Scene (Quick Setup)

For 3D creative coding with models, lighting, and camera.

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

function create3DScene(container) {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  renderer.setSize(innerWidth, innerHeight);
  renderer.setPixelRatio(devicePixelRatio);
  container.appendChild(renderer.domElement);

  // Lighting
  scene.add(new THREE.AmbientLight(0xffffff, 0.5));
  const dirLight = new THREE.DirectionalLight(0xffffff, 1);
  dirLight.position.set(5, 5, 5);
  scene.add(dirLight);

  // Controls
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  camera.position.z = 5;

  // Animation loop
  function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
  }
  animate();

  // Handle resize
  window.addEventListener('resize', () => {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });

  return { scene, camera, renderer };
}

8. Image-to-Art Transform

Upload an image, decompose into artistic representation.

function imageToPointillism(sourceCanvas, outputCanvas, dotSize = 6) {
  const src = sourceCanvas.getContext('2d');
  const out = outputCanvas.getContext('2d');
  const w = sourceCanvas.width, h = sourceCanvas.height;
  const imageData = src.getImageData(0, 0, w, h).data;

  out.fillStyle = '#000';
  out.fillRect(0, 0, w, h);

  const dots = [];
  for (let y = 0; y < h; y += dotSize) {
    for (let x = 0; x < w; x += dotSize) {
      const i = (y * w + x) * 4;
      const r = imageData[i], g = imageData[i+1], b = imageData[i+2];
      const brightness = (r + g + b) / 3;
      dots.push({
        x: x + (Math.random() - 0.5) * dotSize * 0.5,
        y: y + (Math.random() - 0.5) * dotSize * 0.5,
        radius: (brightness / 255) * dotSize * 0.5 + 1,
        color: `rgb(${r},${g},${b})`
      });
    }
  }

  // Animate dots appearing
  let drawn = 0;
  function drawBatch() {
    const batch = Math.min(drawn + 200, dots.length);
    for (let i = drawn; i < batch; i++) {
      const d = dots[i];
      out.beginPath();
      out.arc(d.x, d.y, d.radius, 0, Math.PI * 2);
      out.fillStyle = d.color;
      out.fill();
    }
    drawn = batch;
    if (drawn < dots.length) requestAnimationFrame(drawBatch);
  }
  drawBatch();
}

Art style variants (change the rendering loop):

  • Pointillism: Colored circles (above)
  • ASCII art: Map brightness to character set .:-=+*#%@
  • Mosaic: Colored rectangles at grid positions
  • Brush strokes: Short angled lines using local gradient direction

Output Formats

Adapt output to the user's needs:

User wantsOutput format
Quick standalone demoSingle .html file, no dependencies
React/Next.js component.tsx with useRef + useEffect + canvas
p5.js sketchsketch.js with setup() / draw()
Background for existing siteCSS + minimal JS, or shader-only
npm package / reusableES module with config options
Three.js sceneModule with importmap or bundler setup

For self-contained HTML: Inline all JS/CSS, use CDN imports via <script type="importmap"> for Three.js, or raw Canvas/WebGL with no dependencies.

For React: Use useRef for canvas, useEffect for animation loop, cleanup on unmount. Use 'use client' directive for Next.js.

Design Principles

  1. Start simple, add complexity - Get a dot moving before building a particle system
  2. Color is emotion - HSL is your friend. Map data to hue for instant beauty
  3. Trails create history - fillRect with low alpha instead of clearRect
  4. Noise is nature - Perlin/simplex noise makes anything look organic
  5. Interaction creates connection - Mouse influence makes it personal
  6. Frame rate is sacred - Profile early, use requestAnimationFrame, offload to GPU
  7. Constraints breed creativity - "What can I make with just circles?" beats "anything goes"

Performance Tips

  • Canvas 2D: Good for 1-5K particles. Use fillRect over arc for dots.
  • WebGL shaders: Good for fullscreen effects, millions of calculations per frame
  • Three.js: Good for 3D scenes, use instanced meshes for many objects
  • OffscreenCanvas: Use web workers for heavy computation
  • devicePixelRatio: Always set for sharp rendering on retina displays
  • will-change: transform: Hint to browser for composited layers

Common Mistakes

MistakeFix
Clearing canvas every frame (no trails)Use semi-transparent fill instead of clearRect
Noise that looks random, not smoothLower the scale factor (0.001-0.01 range)
Too many particles, low FPSStart with 500, increase until you hit 30fps
Shader compiles but shows blackCheck gl.getShaderInfoLog() for errors
Mouse coords wrong on canvasAccount for canvas offset and devicePixelRatio
Animation jerky on resizeDebounce resize handler, recalculate dimensions
Colors look muddyUse HSL with fixed saturation/lightness, vary only hue

Inspiration Mapping

When the user describes a vibe, map to technique:

VibeTechnique
"Organic, flowing"Perlin noise flow field
"Geometric, structured"Recursive subdivision, Mondrian
"Natural, growing"L-systems, fractal trees
"Psychedelic, trippy"WebGL shader with iterative distortion
"Minimal, elegant"Single curve, polar coordinates
"Chaotic, energetic"High-count particles with physics
"Retro, pixelated"Low-res canvas + nearest-neighbor scaling
"Dreamy, soft"Gaussian blur + slow float + pastels
"Interactive, playful"Mouse-reactive particles or deformation
"Data-driven"Map dataset values to visual properties
creative-code-lab — skill by sacredvoid | Shared Context