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 curvesspeed(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:
| Name | Axiom | Rules | Angle |
|---|---|---|---|
| Binary tree | F | F -> FF+[+F-F]-[-F+F] | 25deg |
| Fern | X | X -> F+[[X]-X]-F[-FX]+X, F -> FF | 25deg |
| Bush | F | F -> F[+FF][-FF]F[-F][+F]F | 20deg |
| Seaweed | F | F -> 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 wants | Output format |
|---|---|
| Quick standalone demo | Single .html file, no dependencies |
| React/Next.js component | .tsx with useRef + useEffect + canvas |
| p5.js sketch | sketch.js with setup() / draw() |
| Background for existing site | CSS + minimal JS, or shader-only |
| npm package / reusable | ES module with config options |
| Three.js scene | Module 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
- Start simple, add complexity - Get a dot moving before building a particle system
- Color is emotion - HSL is your friend. Map data to hue for instant beauty
- Trails create history -
fillRectwith low alpha instead ofclearRect - Noise is nature - Perlin/simplex noise makes anything look organic
- Interaction creates connection - Mouse influence makes it personal
- Frame rate is sacred - Profile early, use
requestAnimationFrame, offload to GPU - Constraints breed creativity - "What can I make with just circles?" beats "anything goes"
Performance Tips
- Canvas 2D: Good for 1-5K particles. Use
fillRectoverarcfor 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 displayswill-change: transform: Hint to browser for composited layers
Common Mistakes
| Mistake | Fix |
|---|---|
| Clearing canvas every frame (no trails) | Use semi-transparent fill instead of clearRect |
| Noise that looks random, not smooth | Lower the scale factor (0.001-0.01 range) |
| Too many particles, low FPS | Start with 500, increase until you hit 30fps |
| Shader compiles but shows black | Check gl.getShaderInfoLog() for errors |
| Mouse coords wrong on canvas | Account for canvas offset and devicePixelRatio |
| Animation jerky on resize | Debounce resize handler, recalculate dimensions |
| Colors look muddy | Use HSL with fixed saturation/lightness, vary only hue |
Inspiration Mapping
When the user describes a vibe, map to technique:
| Vibe | Technique |
|---|---|
| "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 |