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".
Scanned 5/27/2026
Install via CLI
openskills install sacredvoid/skillkit---
name: creative-code-lab
description: 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](https://lab.xubh.top/) (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.
```javascript
// 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.
```javascript
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.
```html
<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.
```javascript
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.
```javascript
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.
```javascript
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.
```javascript
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.
```javascript
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
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
| 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 |
No comments yet. Be the first to comment!