📄 visual-effects.md

← Vault

Visual Effects

Noise

Perlin Noise Basics

`javascript

noiseSeed(42);

noiseDetail(4, 0.5); // octaves, falloff

// 1D noise — smooth undulation

let y = noise(x * 0.01); // returns 0.0 to 1.0

// 2D noise — terrain/texture

let v = noise(x 0.005, y 0.005);

// 3D noise — animated 2D field (z = time)

let v = noise(x 0.005, y 0.005, frameCount * 0.005);

`

The scale factor (0.005 etc.) is critical:

`javascript

// WRONG: low fade + low stroke = invisible

trailBuf.fill(0, 0, 0, 5); // long trails

trailBuf.rect(0, 0, W, H);

trailBuf.stroke(255, 30); // too dim to ever accumulate

trailBuf.line(px, py, x, y);

// RIGHT: low fade + high stroke = visible long trails

trailBuf.fill(0, 0, 0, 5);

trailBuf.rect(0, 0, W, H);

trailBuf.stroke(255, 100); // bright enough to persist through fade

trailBuf.line(px, py, x, y);

`

Reaction-Diffusion (Gray-Scott)

`javascript

class ReactionDiffusion {

constructor(w, h) {

this.w = w;

this.h = h;

this.a = new Float32Array(w * h).fill(1);

this.b = new Float32Array(w * h).fill(0);

this.nextA = new Float32Array(w * h);

this.nextB = new Float32Array(w * h);

this.dA = 1.0;

this.dB = 0.5;

this.feed = 0.055;

this.kill = 0.062;

}

seed(cx, cy, r) {

for (let y = cy - r; y < cy + r; y++) {

for (let x = cx - r; x < cx + r; x++) {

if (dist(x, y, cx, cy) < r) {

let idx = y * this.w + x;

this.b[idx] = 1;

}

}

}

}

step() {

for (let y = 1; y < this.h - 1; y++) {

for (let x = 1; x < this.w - 1; x++) {

let idx = y * this.w + x;

let a = this.a[idx], b = this.b[idx];

let lapA = this.laplacian(this.a, x, y);

let lapB = this.laplacian(this.b, x, y);

let abb = a b b;

this.nextA[idx] = constrain(a + this.dA lapA - abb + this.feed (1 - a), 0, 1);

this.nextB[idx] = constrain(b + this.dB lapB + abb - (this.kill + this.feed) b, 0, 1);

}

}

[this.a, this.nextA] = [this.nextA, this.a];

[this.b, this.nextB] = [this.nextB, this.b];

}

laplacian(arr, x, y) {

let w = this.w;

return arr[(y-1)w+x] + arr[(y+1)w+x] + arr[yw+(x-1)] + arr[yw+(x+1)]

- 4 arr[yw+x];

}

}

`

Pixel Sorting

`javascript

function pixelSort(buffer, threshold, direction = 'horizontal') {

buffer.loadPixels();

let px = buffer.pixels;

if (direction === 'horizontal') {

for (let y = 0; y < height; y++) {

let spans = findSpans(px, y, width, threshold, true);

for (let span of spans) {

sortSpan(px, span.start, span.end, y, true);

}

}

}

buffer.updatePixels();

}

function findSpans(px, row, w, threshold, horizontal) {

let spans = [];

let start = -1;

for (let i = 0; i < w; i++) {

let idx = horizontal ? 4 (row w + i) : 4 (i w + row);

let brightness = (px[idx] + px[idx+1] + px[idx+2]) / 3;

if (brightness > threshold && start === -1) {

start = i;

} else if (brightness <= threshold && start !== -1) {

spans.push({ start, end: i });

start = -1;

}

}

if (start !== -1) spans.push({ start, end: w });

return spans;

}

`

Advanced Generative Techniques

L-Systems (Lindenmayer Systems)

Grammar-based recursive growth for trees, plants, fractals.

`javascript

class LSystem {

constructor(axiom, rules) {

this.axiom = axiom;

this.rules = rules; // { 'F': 'F[+F]F[-F]F' }

this.sentence = axiom;

}

generate(iterations) {

for (let i = 0; i < iterations; i++) {

let next = '';

for (let ch of this.sentence) {

next += this.rules[ch] || ch;

}

this.sentence = next;

}

}

draw(len, angle) {

for (let ch of this.sentence) {

switch (ch) {

case 'F': line(0, 0, 0, -len); translate(0, -len); break;

case '+': rotate(angle); break;

case '-': rotate(-angle); break;

case '[': push(); break;

case ']': pop(); break;

}

}

}

}

// Usage: fractal plant

let lsys = new LSystem('X', {

'X': 'F+[[X]-X]-F[-FX]+X',

'F': 'FF'

});

lsys.generate(5);

translate(width/2, height);

lsys.draw(4, radians(25));

`

Circle Packing

Fill a space with non-overlapping circles of varying size.

`javascript

class PackedCircle {

constructor(x, y, r) {

this.x = x; this.y = y; this.r = r;

this.growing = true;

}

grow() { if (this.growing) this.r += 0.5; }

overlaps(other) {

let d = dist(this.x, this.y, other.x, other.y);

return d < this.r + other.r + 2; // +2 gap

}

atEdge() {

return this.x - this.r < 0 || this.x + this.r > width ||

this.y - this.r < 0 || this.y + this.r > height;

}

}

let circles = [];

function packStep() {

// Try to place new circle

for (let attempts = 0; attempts < 100; attempts++) {

let x = random(width), y = random(height);

let valid = true;

for (let c of circles) {

if (dist(x, y, c.x, c.y) < c.r + 2) { valid = false; break; }

}

if (valid) { circles.push(new PackedCircle(x, y, 1)); break; }

}

// Grow existing circles

for (let c of circles) {

if (!c.growing) continue;

c.grow();

if (c.atEdge()) { c.growing = false; continue; }

for (let other of circles) {

if (c !== other && c.overlaps(other)) { c.growing = false; break; }

}

}

}

`

Voronoi Diagram (Fortune's Algorithm Approximation)

`javascript

// Simple brute-force Voronoi (for small point counts)

function drawVoronoi(points, colors) {

loadPixels();

for (let y = 0; y < height; y++) {

for (let x = 0; x < width; x++) {

let minDist = Infinity;

let closest = 0;

for (let i = 0; i < points.length; i++) {

let d = (x - points[i].x) 2 + (y - points[i].y) 2; // magSq

if (d < minDist) { minDist = d; closest = i; }

}

let idx = 4 (y width + x);

let c = colors[closest % colors.length];

pixels[idx] = red(c);

pixels[idx+1] = green(c);

pixels[idx+2] = blue(c);

pixels[idx+3] = 255;

}

}

updatePixels();

}

`

Fractal Trees

`javascript

function fractalTree(x, y, len, angle, depth, branchAngle) {

if (depth <= 0 || len < 2) return;

let x2 = x + Math.cos(angle) * len;

let y2 = y + Math.sin(angle) * len;

strokeWeight(map(depth, 0, 10, 0.5, 4));

line(x, y, x2, y2);

let shrink = 0.67 + noise(x 0.01, y 0.01) * 0.15;

fractalTree(x2, y2, len * shrink, angle - branchAngle, depth - 1, branchAngle);

fractalTree(x2, y2, len * shrink, angle + branchAngle, depth - 1, branchAngle);

}

// Usage

fractalTree(width/2, height, 120, -HALF_PI, 10, PI/6);

`

Strange Attractors

`javascript

// Clifford Attractor

function cliffordAttractor(a, b, c, d, iterations) {

let x = 0, y = 0;

beginShape(POINTS);

for (let i = 0; i < iterations; i++) {

let nx = Math.sin(a y) + c Math.cos(a * x);

let ny = Math.sin(b x) + d Math.cos(b * y);

x = nx; y = ny;

let px = map(x, -3, 3, 0, width);

let py = map(y, -3, 3, 0, height);

vertex(px, py);

}

endShape();

}

// De Jong Attractor

function deJongAttractor(a, b, c, d, iterations) {

let x = 0, y = 0;

beginShape(POINTS);

for (let i = 0; i < iterations; i++) {

let nx = Math.sin(a y) - Math.cos(b x);

let ny = Math.sin(c x) - Math.cos(d y);

x = nx; y = ny;

let px = map(x, -2.5, 2.5, 0, width);

let py = map(y, -2.5, 2.5, 0, height);

vertex(px, py);

}

endShape();

}

`

Poisson Disk Sampling

Even distribution that looks natural — better than pure random for placing elements.

`javascript

function poissonDiskSampling(r, k = 30) {

let cellSize = r / Math.sqrt(2);

let cols = Math.ceil(width / cellSize);

let rows = Math.ceil(height / cellSize);

let grid = new Array(cols * rows).fill(-1);

let points = [];

let active = [];

function gridIndex(x, y) {

return Math.floor(x / cellSize) + Math.floor(y / cellSize) * cols;

}

// Seed

let p0 = createVector(random(width), random(height));

points.push(p0);

active.push(p0);

grid[gridIndex(p0.x, p0.y)] = 0;

while (active.length > 0) {

let idx = Math.floor(Math.random() * active.length);

let pos = active[idx];

let found = false;

for (let n = 0; n < k; n++) {

let angle = Math.random() * TWO_PI;

let mag = r + Math.random() * r;

let sample = createVector(pos.x + Math.cos(angle) mag, pos.y + Math.sin(angle) mag);

if (sample.x < 0 || sample.x >= width || sample.y < 0 || sample.y >= height) continue;

let col = Math.floor(sample.x / cellSize);

let row = Math.floor(sample.y / cellSize);

let ok = true;

for (let dy = -2; dy <= 2; dy++) {

for (let dx = -2; dx <= 2; dx++) {

let nc = col + dx, nr = row + dy;

if (nc >= 0 && nc < cols && nr >= 0 && nr < rows) {

let gi = nc + nr * cols;

if (grid[gi] !== -1 && points[grid[gi]].dist(sample) < r) { ok = false; }

}

}

}

if (ok) {

points.push(sample);

active.push(sample);

grid[gridIndex(sample.x, sample.y)] = points.length - 1;

found = true;

break;

}

}

if (!found) active.splice(idx, 1);

}

return points;

}

`

Addon Libraries

p5.brush — Natural Media

Hand-drawn, organic aesthetics. Watercolor, charcoal, pen, marker. Requires p5.js 2.x + WEBGL.

`html

`

`javascript

function setup() {

createCanvas(1200, 1200, WEBGL);

brush.scaleBrushes(3); // essential for proper sizing

translate(-width/2, -height/2); // WEBGL origin is center

brush.pick('2B'); // pencil brush

brush.stroke(50, 50, 50);

brush.strokeWeight(2);

brush.line(100, 100, 500, 500);

brush.pick('watercolor');

brush.fill('#4a90d9', 150);

brush.circle(400, 400, 200);

}

`

Built-in brushes: 2B, HB, 2H, cpencil, pen, rotring, spray, marker, charcoal, hatch_brush.

Built-in vector fields: hand, curved, zigzag, waves, seabed, spiral, columns.

p5.grain — Film Grain & Texture

`html

`

`javascript

function draw() {

// ... render scene ...

applyMonochromaticGrain(42); // uniform grain

// or: applyChromaticGrain(42); // per-channel randomization

}

`

CCapture.js — Deterministic Video Capture

Records canvas at fixed framerate regardless of actual render speed. Essential for complex generative art.

`html

`

`javascript

let capturer;

function setup() {

createCanvas(1920, 1080);

capturer = new CCapture({

format: 'webm',

framerate: 60,

quality: 99,

// timeLimit: 10, // auto-stop after N seconds

// motionBlurFrames: 4 // supersampled motion blur

});

}

function startRecording() {

capturer.start();

}

function draw() {

// ... render frame ...

if (capturer) capturer.capture(document.querySelector('canvas'));

}

function stopRecording() {

capturer.stop();

capturer.save(); // triggers download

}

`