Sync all skills and memories 2026-04-14 07:27
This commit is contained in:
895
skills/creative/p5js/references/visual-effects.md
Normal file
895
skills/creative/p5js/references/visual-effects.md
Normal file
@@ -0,0 +1,895 @@
|
||||
# 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
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5.brush@latest/dist/p5.brush.js"></script>
|
||||
```
|
||||
|
||||
```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
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5.grain@0.7.0/p5.grain.min.js"></script>
|
||||
```
|
||||
|
||||
```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
|
||||
<script src="https://cdn.jsdelivr.net/npm/ccapture.js-npmfixed/build/CCapture.all.min.js"></script>
|
||||
```
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user