# 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: - `0.001` — very smooth, large features - `0.005` — smooth, medium features - `0.01` — standard generative art scale - `0.05` — detailed, small features - `0.1` — near-random, grainy ### Fractal Brownian Motion (fBM) Layered noise octaves for natural-looking texture. Each octave adds detail at smaller scale. ```javascript function fbm(x, y, octaves = 6, lacunarity = 2.0, gain = 0.5) { let value = 0; let amplitude = 1.0; let frequency = 1.0; let maxValue = 0; for (let i = 0; i < octaves; i++) { value += noise(x * frequency, y * frequency) * amplitude; maxValue += amplitude; amplitude *= gain; frequency *= lacunarity; } return value / maxValue; } ``` ### Domain Warping Feed noise output back as input coordinates for flowing organic distortion. ```javascript function domainWarp(x, y, scale, strength, time) { // First warp pass let qx = fbm(x + 0.0, y + 0.0); let qy = fbm(x + 5.2, y + 1.3); // Second warp pass (feed back) let rx = fbm(x + strength * qx + 1.7, y + strength * qy + 9.2, 4, 2, 0.5); let ry = fbm(x + strength * qx + 8.3, y + strength * qy + 2.8, 4, 2, 0.5); return fbm(x + strength * rx + time, y + strength * ry + time); } ``` ### Curl Noise Divergence-free noise field. Particles following curl noise never converge or diverge — they flow in smooth, swirling patterns. ```javascript function curlNoise(x, y, scale, time) { let eps = 0.001; // Partial derivatives via finite differences let dndx = (noise(x * scale + eps, y * scale, time) - noise(x * scale - eps, y * scale, time)) / (2 * eps); let dndy = (noise(x * scale, y * scale + eps, time) - noise(x * scale, y * scale - eps, time)) / (2 * eps); // Curl = perpendicular to gradient return createVector(dndy, -dndx); } ``` ## Flow Fields A grid of vectors that steer particles. The foundational generative art technique. ```javascript class FlowField { constructor(resolution, noiseScale) { this.resolution = resolution; this.cols = ceil(width / resolution); this.rows = ceil(height / resolution); this.field = new Array(this.cols * this.rows); this.noiseScale = noiseScale; } update(time) { for (let i = 0; i < this.cols; i++) { for (let j = 0; j < this.rows; j++) { let angle = noise(i * this.noiseScale, j * this.noiseScale, time) * TWO_PI * 2; this.field[i + j * this.cols] = p5.Vector.fromAngle(angle); } } } lookup(x, y) { let col = constrain(floor(x / this.resolution), 0, this.cols - 1); let row = constrain(floor(y / this.resolution), 0, this.rows - 1); return this.field[col + row * this.cols].copy(); } } ``` ### Flow Field Particle ```javascript class FlowParticle { constructor(x, y) { this.pos = createVector(x, y); this.vel = createVector(0, 0); this.acc = createVector(0, 0); this.prev = this.pos.copy(); this.maxSpeed = 2; this.life = 1.0; } follow(field) { let force = field.lookup(this.pos.x, this.pos.y); force.mult(0.5); // force magnitude this.acc.add(force); } update() { this.prev = this.pos.copy(); this.vel.add(this.acc); this.vel.limit(this.maxSpeed); this.pos.add(this.vel); this.acc.mult(0); this.life -= 0.001; } edges() { if (this.pos.x > width) this.pos.x = 0; if (this.pos.x < 0) this.pos.x = width; if (this.pos.y > height) this.pos.y = 0; if (this.pos.y < 0) this.pos.y = height; this.prev = this.pos.copy(); // prevent wrap line } display(buffer) { buffer.stroke(255, this.life * 30); buffer.strokeWeight(0.5); buffer.line(this.prev.x, this.prev.y, this.pos.x, this.pos.y); } } ``` ## Particle Systems ### Basic Physics Particle ```javascript class Particle { constructor(x, y) { this.pos = createVector(x, y); this.vel = p5.Vector.random2D().mult(random(1, 3)); this.acc = createVector(0, 0); this.life = 255; this.decay = random(1, 5); this.size = random(3, 8); } applyForce(f) { this.acc.add(f); } update() { this.vel.add(this.acc); this.pos.add(this.vel); this.acc.mult(0); this.life -= this.decay; } display() { noStroke(); fill(255, this.life); ellipse(this.pos.x, this.pos.y, this.size); } isDead() { return this.life <= 0; } } ``` ### Attractor-Driven Particles ```javascript class Attractor { constructor(x, y, strength) { this.pos = createVector(x, y); this.strength = strength; } attract(particle) { let force = p5.Vector.sub(this.pos, particle.pos); let d = constrain(force.mag(), 5, 200); force.normalize(); force.mult(this.strength / (d * d)); particle.applyForce(force); } } ``` ### Boid Flocking ```javascript class Boid { constructor(x, y) { this.pos = createVector(x, y); this.vel = p5.Vector.random2D().mult(random(2, 4)); this.acc = createVector(0, 0); this.maxForce = 0.2; this.maxSpeed = 4; this.perceptionRadius = 50; } flock(boids) { let alignment = createVector(0, 0); let cohesion = createVector(0, 0); let separation = createVector(0, 0); let total = 0; for (let other of boids) { let d = this.pos.dist(other.pos); if (other !== this && d < this.perceptionRadius) { alignment.add(other.vel); cohesion.add(other.pos); let diff = p5.Vector.sub(this.pos, other.pos); diff.div(d * d); separation.add(diff); total++; } } if (total > 0) { alignment.div(total).setMag(this.maxSpeed).sub(this.vel).limit(this.maxForce); cohesion.div(total).sub(this.pos).setMag(this.maxSpeed).sub(this.vel).limit(this.maxForce); separation.div(total).setMag(this.maxSpeed).sub(this.vel).limit(this.maxForce); } this.acc.add(alignment.mult(1.0)); this.acc.add(cohesion.mult(1.0)); this.acc.add(separation.mult(1.5)); } update() { this.vel.add(this.acc); this.vel.limit(this.maxSpeed); this.pos.add(this.vel); this.acc.mult(0); } } ``` ## Pixel Manipulation ### Reading and Writing Pixels ```javascript loadPixels(); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let idx = 4 * (y * width + x); let r = pixels[idx]; let g = pixels[idx + 1]; let b = pixels[idx + 2]; let a = pixels[idx + 3]; // Modify pixels[idx] = 255 - r; // invert red pixels[idx + 1] = 255 - g; // invert green pixels[idx + 2] = 255 - b; // invert blue } } updatePixels(); ``` ### Pixel-Level Noise Texture ```javascript loadPixels(); for (let i = 0; i < pixels.length; i += 4) { let x = (i / 4) % width; let y = floor((i / 4) / width); let n = noise(x * 0.01, y * 0.01, frameCount * 0.02); let c = n * 255; pixels[i] = c; pixels[i + 1] = c; pixels[i + 2] = c; pixels[i + 3] = 255; } updatePixels(); ``` ### Built-in Filters ```javascript filter(BLUR, 3); // Gaussian blur (radius) filter(THRESHOLD, 0.5); // Black/white threshold filter(INVERT); // Color inversion filter(POSTERIZE, 4); // Reduce color levels filter(GRAY); // Desaturate filter(ERODE); // Thin bright areas filter(DILATE); // Expand bright areas filter(OPAQUE); // Remove transparency ``` ## Texture Generation ### Stippling / Pointillism ```javascript function stipple(buffer, density, minSize, maxSize) { buffer.loadPixels(); for (let i = 0; i < density; i++) { let x = floor(random(width)); let y = floor(random(height)); let idx = 4 * (y * width + x); let brightness = (buffer.pixels[idx] + buffer.pixels[idx+1] + buffer.pixels[idx+2]) / 3; let size = map(brightness, 0, 255, maxSize, minSize); if (random() < map(brightness, 0, 255, 0.8, 0.1)) { noStroke(); fill(buffer.pixels[idx], buffer.pixels[idx+1], buffer.pixels[idx+2]); ellipse(x, y, size); } } } ``` ### Halftone ```javascript function halftone(sourceBuffer, dotSpacing, maxDotSize) { sourceBuffer.loadPixels(); background(255); fill(0); noStroke(); for (let y = 0; y < height; y += dotSpacing) { for (let x = 0; x < width; x += dotSpacing) { let idx = 4 * (y * width + x); let brightness = (sourceBuffer.pixels[idx] + sourceBuffer.pixels[idx+1] + sourceBuffer.pixels[idx+2]) / 3; let dotSize = map(brightness, 0, 255, maxDotSize, 0); ellipse(x + dotSpacing/2, y + dotSpacing/2, dotSize); } } } ``` ### Cross-Hatching ```javascript function crossHatch(x, y, w, h, value, spacing) { // value: 0 (dark) to 1 (light) let numLayers = floor(map(value, 0, 1, 4, 0)); let angles = [PI/4, -PI/4, 0, PI/2]; for (let layer = 0; layer < numLayers; layer++) { push(); translate(x + w/2, y + h/2); rotate(angles[layer]); let s = spacing + layer * 2; for (let i = -max(w, h); i < max(w, h); i += s) { line(i, -max(w, h), i, max(w, h)); } pop(); } } ``` ## Feedback Loops ### Frame Feedback (Echo/Trail) ```javascript let feedback; function setup() { createCanvas(800, 800); feedback = createGraphics(width, height); } function draw() { // Copy current feedback, slightly zoomed and rotated let temp = feedback.get(); feedback.push(); feedback.translate(width/2, height/2); feedback.scale(1.005); // slow zoom feedback.rotate(0.002); // slow rotation feedback.translate(-width/2, -height/2); feedback.tint(255, 245); // slight fade feedback.image(temp, 0, 0); feedback.pop(); // Draw new content to feedback feedback.noStroke(); feedback.fill(255); feedback.ellipse(mouseX, mouseY, 20); // Show image(feedback, 0, 0); } ``` ### Bloom / Glow (Post-Processing) Downsample the scene to a small buffer, blur it, overlay additively. Creates soft glow around bright areas. This is the standard generative art bloom technique. ```javascript let scene, bloomBuf; function setup() { createCanvas(1080, 1080); scene = createGraphics(width, height); bloomBuf = createGraphics(width, height); } function draw() { // 1. Render scene to offscreen buffer scene.background(0); scene.fill(255, 200, 100); scene.noStroke(); // ... draw bright elements to scene ... // 2. Build bloom: downsample → blur → upscale bloomBuf.clear(); bloomBuf.image(scene, 0, 0, width / 4, height / 4); // 4x downsample bloomBuf.filter(BLUR, 6); // blur the small version // 3. Composite: scene + additive bloom background(0); image(scene, 0, 0); // base layer blendMode(ADD); // additive = glow tint(255, 80); // control bloom intensity (0-255) image(bloomBuf, 0, 0, width, height); // upscale back to full size noTint(); blendMode(BLEND); // ALWAYS reset blend mode } ``` **Tuning:** - Downsample ratio (1/4 is standard, 1/8 for softer, 1/2 for tighter) - Blur radius (4-8 typical, higher = wider glow) - Tint alpha (40-120, controls glow intensity) - Update bloom every N frames to save perf: `if (frameCount % 2 === 0) { ... }` **Common mistake:** Forgetting `blendMode(BLEND)` after the ADD pass — everything drawn after will be additive. ### Trail Buffer Brightness Trail accumulation via `createGraphics()` + semi-transparent fade rect is the standard technique for particle trails, but **trails are always dimmer than you expect**. The fade rect's alpha compounds multiplicatively every frame. ```javascript // The fade rect alpha controls trail length AND brightness: trailBuf.fill(0, 0, 0, alpha); trailBuf.rect(0, 0, width, height); // alpha=5 → very long trails, very dim (content fades to 50% in ~35 frames) // alpha=10 → long trails, dim // alpha=20 → medium trails, visible // alpha=40 → short trails, bright // alpha=80 → very short trails, crisp ``` **The trap:** You set alpha=5 for long trails, but particle strokes at alpha=30 are invisible because they fade before accumulating enough density. Either: - **Boost stroke alpha** to 80-150 (not the intuitive 20-40) - **Reduce fade alpha** but accept shorter trails - **Use additive blending** for the strokes: bright particles accumulate, dim ones stay dark ```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[y*w+(x-1)] + arr[y*w+(x+1)] - 4 * arr[y*w+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 } ```