Sync all skills and memories 2026-04-14 07:27
This commit is contained in:
439
skills/creative/p5js/references/animation.md
Normal file
439
skills/creative/p5js/references/animation.md
Normal file
@@ -0,0 +1,439 @@
|
||||
# Animation
|
||||
|
||||
## Frame-Based Animation
|
||||
|
||||
### The Draw Loop
|
||||
|
||||
```javascript
|
||||
function draw() {
|
||||
// Called ~60 times/sec by default
|
||||
// frameCount — integer, starts at 1
|
||||
// deltaTime — ms since last frame (use for framerate-independent motion)
|
||||
// millis() — ms since sketch start
|
||||
}
|
||||
```
|
||||
|
||||
### Time-Based vs Frame-Based
|
||||
|
||||
```javascript
|
||||
// Frame-based (speed varies with framerate)
|
||||
x += speed;
|
||||
|
||||
// Time-based (consistent speed regardless of framerate)
|
||||
x += speed * (deltaTime / 16.67); // normalized to 60fps
|
||||
```
|
||||
|
||||
### Normalized Time
|
||||
|
||||
```javascript
|
||||
// Progress from 0 to 1 over N seconds
|
||||
let duration = 5000; // 5 seconds in ms
|
||||
let t = constrain(millis() / duration, 0, 1);
|
||||
|
||||
// Looping progress (0 → 1 → 0 → 1...)
|
||||
let period = 3000; // 3 second loop
|
||||
let t = (millis() % period) / period;
|
||||
|
||||
// Ping-pong (0 → 1 → 0 → 1...)
|
||||
let raw = (millis() % (period * 2)) / period;
|
||||
let t = raw <= 1 ? raw : 2 - raw;
|
||||
```
|
||||
|
||||
## Easing Functions
|
||||
|
||||
### Built-in Lerp
|
||||
|
||||
```javascript
|
||||
// Linear interpolation — smooth but mechanical
|
||||
let x = lerp(startX, endX, t);
|
||||
|
||||
// Map for non-0-1 ranges
|
||||
let y = map(t, 0, 1, startY, endY);
|
||||
```
|
||||
|
||||
### Common Easing Curves
|
||||
|
||||
```javascript
|
||||
// Ease in (slow start)
|
||||
function easeInQuad(t) { return t * t; }
|
||||
function easeInCubic(t) { return t * t * t; }
|
||||
function easeInExpo(t) { return t === 0 ? 0 : pow(2, 10 * (t - 1)); }
|
||||
|
||||
// Ease out (slow end)
|
||||
function easeOutQuad(t) { return 1 - (1 - t) * (1 - t); }
|
||||
function easeOutCubic(t) { return 1 - pow(1 - t, 3); }
|
||||
function easeOutExpo(t) { return t === 1 ? 1 : 1 - pow(2, -10 * t); }
|
||||
|
||||
// Ease in-out (slow both ends)
|
||||
function easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
function easeInOutQuint(t) {
|
||||
return t < 0.5 ? 16 * t * t * t * t * t : 1 - pow(-2 * t + 2, 5) / 2;
|
||||
}
|
||||
|
||||
// Elastic (spring overshoot)
|
||||
function easeOutElastic(t) {
|
||||
if (t === 0 || t === 1) return t;
|
||||
return pow(2, -10 * t) * sin((t * 10 - 0.75) * (2 * PI / 3)) + 1;
|
||||
}
|
||||
|
||||
// Bounce
|
||||
function easeOutBounce(t) {
|
||||
if (t < 1/2.75) return 7.5625 * t * t;
|
||||
else if (t < 2/2.75) { t -= 1.5/2.75; return 7.5625 * t * t + 0.75; }
|
||||
else if (t < 2.5/2.75) { t -= 2.25/2.75; return 7.5625 * t * t + 0.9375; }
|
||||
else { t -= 2.625/2.75; return 7.5625 * t * t + 0.984375; }
|
||||
}
|
||||
|
||||
// Smooth step (Hermite interpolation — great default)
|
||||
function smoothstep(t) { return t * t * (3 - 2 * t); }
|
||||
|
||||
// Smoother step (Ken Perlin)
|
||||
function smootherstep(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
|
||||
```
|
||||
|
||||
### Applying Easing
|
||||
|
||||
```javascript
|
||||
// Animate from startVal to endVal over duration ms
|
||||
function easedValue(startVal, endVal, startTime, duration, easeFn) {
|
||||
let t = constrain((millis() - startTime) / duration, 0, 1);
|
||||
return lerp(startVal, endVal, easeFn(t));
|
||||
}
|
||||
|
||||
// Usage
|
||||
let x = easedValue(100, 700, animStartTime, 2000, easeOutCubic);
|
||||
```
|
||||
|
||||
## Spring Physics
|
||||
|
||||
More natural than easing — responds to force, overshoots, settles.
|
||||
|
||||
```javascript
|
||||
class Spring {
|
||||
constructor(value, target, stiffness = 0.1, damping = 0.7) {
|
||||
this.value = value;
|
||||
this.target = target;
|
||||
this.velocity = 0;
|
||||
this.stiffness = stiffness;
|
||||
this.damping = damping;
|
||||
}
|
||||
|
||||
update() {
|
||||
let force = (this.target - this.value) * this.stiffness;
|
||||
this.velocity += force;
|
||||
this.velocity *= this.damping;
|
||||
this.value += this.velocity;
|
||||
return this.value;
|
||||
}
|
||||
|
||||
setTarget(t) { this.target = t; }
|
||||
isSettled(threshold = 0.01) {
|
||||
return abs(this.velocity) < threshold && abs(this.value - this.target) < threshold;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
let springX = new Spring(0, 0, 0.08, 0.85);
|
||||
function draw() {
|
||||
springX.setTarget(mouseX);
|
||||
let x = springX.update();
|
||||
ellipse(x, height/2, 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 2D Spring
|
||||
|
||||
```javascript
|
||||
class Spring2D {
|
||||
constructor(x, y) {
|
||||
this.pos = createVector(x, y);
|
||||
this.target = createVector(x, y);
|
||||
this.vel = createVector(0, 0);
|
||||
this.stiffness = 0.08;
|
||||
this.damping = 0.85;
|
||||
}
|
||||
|
||||
update() {
|
||||
let force = p5.Vector.sub(this.target, this.pos).mult(this.stiffness);
|
||||
this.vel.add(force).mult(this.damping);
|
||||
this.pos.add(this.vel);
|
||||
return this.pos;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## State Machines
|
||||
|
||||
For complex multi-phase animations.
|
||||
|
||||
```javascript
|
||||
const STATES = { IDLE: 0, ENTER: 1, ACTIVE: 2, EXIT: 3 };
|
||||
let state = STATES.IDLE;
|
||||
let stateStart = 0;
|
||||
|
||||
function setState(newState) {
|
||||
state = newState;
|
||||
stateStart = millis();
|
||||
}
|
||||
|
||||
function stateTime() {
|
||||
return millis() - stateStart;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
switch (state) {
|
||||
case STATES.IDLE:
|
||||
// waiting...
|
||||
break;
|
||||
case STATES.ENTER:
|
||||
let t = constrain(stateTime() / 1000, 0, 1);
|
||||
let alpha = easeOutCubic(t) * 255;
|
||||
// fade in...
|
||||
if (t >= 1) setState(STATES.ACTIVE);
|
||||
break;
|
||||
case STATES.ACTIVE:
|
||||
// main animation...
|
||||
break;
|
||||
case STATES.EXIT:
|
||||
let t2 = constrain(stateTime() / 500, 0, 1);
|
||||
// fade out...
|
||||
if (t2 >= 1) setState(STATES.IDLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Timeline Sequencing
|
||||
|
||||
For timed multi-scene animations (motion graphics, title sequences).
|
||||
|
||||
```javascript
|
||||
class Timeline {
|
||||
constructor() {
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
at(timeMs, duration, fn) {
|
||||
this.events.push({ start: timeMs, end: timeMs + duration, fn });
|
||||
return this;
|
||||
}
|
||||
|
||||
update() {
|
||||
let now = millis();
|
||||
for (let e of this.events) {
|
||||
if (now >= e.start && now < e.end) {
|
||||
let t = (now - e.start) / (e.end - e.start);
|
||||
e.fn(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
let timeline = new Timeline();
|
||||
timeline
|
||||
.at(0, 2000, (t) => {
|
||||
// Scene 1: title fade in (0-2s)
|
||||
let alpha = easeOutCubic(t) * 255;
|
||||
fill(255, alpha);
|
||||
textSize(48);
|
||||
text("Hello", width/2, height/2);
|
||||
})
|
||||
.at(2000, 1000, (t) => {
|
||||
// Scene 2: title fade out (2-3s)
|
||||
let alpha = (1 - easeInCubic(t)) * 255;
|
||||
fill(255, alpha);
|
||||
textSize(48);
|
||||
text("Hello", width/2, height/2);
|
||||
})
|
||||
.at(3000, 5000, (t) => {
|
||||
// Scene 3: main content (3-8s)
|
||||
renderMainContent(t);
|
||||
});
|
||||
|
||||
function draw() {
|
||||
background(0);
|
||||
timeline.update();
|
||||
}
|
||||
```
|
||||
|
||||
## Noise-Driven Motion
|
||||
|
||||
More organic than deterministic animation.
|
||||
|
||||
```javascript
|
||||
// Smooth wandering position
|
||||
let x = map(noise(frameCount * 0.005, 0), 0, 1, 0, width);
|
||||
let y = map(noise(0, frameCount * 0.005), 0, 1, 0, height);
|
||||
|
||||
// Noise-driven rotation
|
||||
let angle = noise(frameCount * 0.01) * TWO_PI;
|
||||
|
||||
// Noise-driven scale (breathing effect)
|
||||
let s = map(noise(frameCount * 0.02), 0, 1, 0.8, 1.2);
|
||||
|
||||
// Noise-driven color shift
|
||||
let hue = map(noise(frameCount * 0.003), 0, 1, 0, 360);
|
||||
```
|
||||
|
||||
## Transition Patterns
|
||||
|
||||
### Fade In/Out
|
||||
|
||||
```javascript
|
||||
function fadeIn(t) { return constrain(t, 0, 1); }
|
||||
function fadeOut(t) { return constrain(1 - t, 0, 1); }
|
||||
```
|
||||
|
||||
### Slide
|
||||
|
||||
```javascript
|
||||
function slideIn(t, direction = 'left') {
|
||||
let et = easeOutCubic(t);
|
||||
switch (direction) {
|
||||
case 'left': return lerp(-width, 0, et);
|
||||
case 'right': return lerp(width, 0, et);
|
||||
case 'up': return lerp(-height, 0, et);
|
||||
case 'down': return lerp(height, 0, et);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scale Reveal
|
||||
|
||||
```javascript
|
||||
function scaleReveal(t) {
|
||||
let et = easeOutElastic(constrain(t, 0, 1));
|
||||
push();
|
||||
translate(width/2, height/2);
|
||||
scale(et);
|
||||
translate(-width/2, -height/2);
|
||||
// draw content...
|
||||
pop();
|
||||
}
|
||||
```
|
||||
|
||||
### Staggered Entry
|
||||
|
||||
```javascript
|
||||
// N elements appear one after another
|
||||
let staggerDelay = 100; // ms between each
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let itemStart = baseTime + i * staggerDelay;
|
||||
let t = constrain((millis() - itemStart) / 500, 0, 1);
|
||||
let alpha = easeOutCubic(t) * 255;
|
||||
let yOffset = lerp(30, 0, easeOutCubic(t));
|
||||
// draw element with alpha and yOffset
|
||||
}
|
||||
```
|
||||
|
||||
## Recording Deterministic Animations
|
||||
|
||||
For frame-perfect export, use frame count instead of millis():
|
||||
|
||||
```javascript
|
||||
const TOTAL_FRAMES = 300; // 10 seconds at 30fps
|
||||
const FPS = 30;
|
||||
|
||||
function draw() {
|
||||
let t = frameCount / TOTAL_FRAMES; // 0 to 1 over full duration
|
||||
if (t > 1) { noLoop(); return; }
|
||||
|
||||
// Use t for all animation timing — deterministic
|
||||
renderFrame(t);
|
||||
|
||||
// Export
|
||||
if (CONFIG.recording) {
|
||||
saveCanvas('frame-' + nf(frameCount, 4), 'png');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scene Fade Envelopes (Video)
|
||||
|
||||
Every scene in a multi-scene video needs fade-in and fade-out. Hard cuts between visually different generative scenes are jarring.
|
||||
|
||||
```javascript
|
||||
const SCENE_FRAMES = 150; // 5 seconds at 30fps
|
||||
const FADE = 15; // half-second fade
|
||||
|
||||
function draw() {
|
||||
let lf = frameCount - 1; // 0-indexed local frame
|
||||
let t = lf / SCENE_FRAMES; // 0..1 normalized progress
|
||||
|
||||
// Fade envelope: ramp up at start, ramp down at end
|
||||
let fade = 1;
|
||||
if (lf < FADE) fade = lf / FADE;
|
||||
if (lf > SCENE_FRAMES - FADE) fade = (SCENE_FRAMES - lf) / FADE;
|
||||
fade = fade * fade * (3 - 2 * fade); // smoothstep for organic feel
|
||||
|
||||
// Apply fade to all visual output
|
||||
// Option 1: multiply alpha values by fade
|
||||
fill(r, g, b, alpha * fade);
|
||||
|
||||
// Option 2: tint entire composited image
|
||||
tint(255, fade * 255);
|
||||
image(sceneBuffer, 0, 0);
|
||||
noTint();
|
||||
|
||||
// Option 3: multiply pixel brightness (for pixel-level scenes)
|
||||
pixels[i] = r * fade;
|
||||
}
|
||||
```
|
||||
|
||||
## Animating Static Algorithms
|
||||
|
||||
Some generative algorithms produce a single static result (attractors, circle packing, Voronoi). In video, static content reads as frozen/broken. Techniques to add motion:
|
||||
|
||||
### Progressive Reveal
|
||||
|
||||
Expand a mask from center outward to reveal the precomputed result:
|
||||
|
||||
```javascript
|
||||
let revealRadius = easeOutCubic(min(t * 1.5, 1)) * (width * 0.8);
|
||||
// In the render loop, skip pixels beyond revealRadius from center
|
||||
let dx = x - width/2, dy = y - height/2;
|
||||
if (sqrt(dx*dx + dy*dy) > revealRadius) continue;
|
||||
// Soft edge:
|
||||
let edgeFade = constrain((revealRadius - dist) / 40, 0, 1);
|
||||
```
|
||||
|
||||
### Parameter Sweep
|
||||
|
||||
Slowly change a parameter to show the algorithm evolving:
|
||||
|
||||
```javascript
|
||||
// Attractor with drifting parameters
|
||||
let a = -1.7 + sin(t * 0.5) * 0.2; // oscillate around base value
|
||||
let b = 1.3 + cos(t * 0.3) * 0.15;
|
||||
```
|
||||
|
||||
### Slow Camera Motion
|
||||
|
||||
Apply subtle zoom or rotation to the final image:
|
||||
|
||||
```javascript
|
||||
push();
|
||||
translate(width/2, height/2);
|
||||
scale(1 + t * 0.05); // slow 5% zoom over scene duration
|
||||
rotate(t * 0.1); // gentle rotation
|
||||
translate(-width/2, -height/2);
|
||||
image(precomputedResult, 0, 0);
|
||||
pop();
|
||||
```
|
||||
|
||||
### Overlay Dynamic Elements
|
||||
|
||||
Add particles, grain, or subtle noise on top of static content:
|
||||
|
||||
```javascript
|
||||
// Static background
|
||||
image(staticResult, 0, 0);
|
||||
// Dynamic overlay
|
||||
for (let p of ambientParticles) {
|
||||
p.update();
|
||||
p.display(); // slow-moving specks add life
|
||||
}
|
||||
```
|
||||
352
skills/creative/p5js/references/color-systems.md
Normal file
352
skills/creative/p5js/references/color-systems.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# Color Systems
|
||||
|
||||
## Color Modes
|
||||
|
||||
### HSB (Recommended for Generative Art)
|
||||
|
||||
```javascript
|
||||
colorMode(HSB, 360, 100, 100, 100);
|
||||
// Hue: 0-360 (color wheel position)
|
||||
// Saturation: 0-100 (gray to vivid)
|
||||
// Brightness: 0-100 (black to full)
|
||||
// Alpha: 0-100
|
||||
|
||||
fill(200, 80, 90); // blue, vivid, bright
|
||||
fill(200, 80, 90, 50); // 50% transparent
|
||||
```
|
||||
|
||||
HSB advantages:
|
||||
- Rotate hue: `(baseHue + offset) % 360`
|
||||
- Desaturate: reduce S
|
||||
- Darken: reduce B
|
||||
- Monochrome variations: fix H, vary S and B
|
||||
- Complementary: `(hue + 180) % 360`
|
||||
- Analogous: `hue +/- 30`
|
||||
|
||||
### HSL
|
||||
|
||||
```javascript
|
||||
colorMode(HSL, 360, 100, 100, 100);
|
||||
// Lightness 50 = pure color, 0 = black, 100 = white
|
||||
// More intuitive for tints (L > 50) and shades (L < 50)
|
||||
```
|
||||
|
||||
### RGB
|
||||
|
||||
```javascript
|
||||
colorMode(RGB, 255, 255, 255, 255); // default
|
||||
// Direct channel control, less intuitive for procedural palettes
|
||||
```
|
||||
|
||||
## Color Objects
|
||||
|
||||
```javascript
|
||||
let c = color(200, 80, 90); // create color object
|
||||
fill(c);
|
||||
|
||||
// Extract components
|
||||
let h = hue(c);
|
||||
let s = saturation(c);
|
||||
let b = brightness(c);
|
||||
let r = red(c);
|
||||
let g = green(c);
|
||||
let bl = blue(c);
|
||||
let a = alpha(c);
|
||||
|
||||
// Hex colors work everywhere
|
||||
fill('#e8d5b7');
|
||||
fill('#e8d5b7cc'); // with alpha
|
||||
|
||||
// Modify via setters
|
||||
c.setAlpha(128);
|
||||
c.setRed(200);
|
||||
```
|
||||
|
||||
## Color Interpolation
|
||||
|
||||
### lerpColor
|
||||
|
||||
```javascript
|
||||
let c1 = color(0, 80, 100); // red
|
||||
let c2 = color(200, 80, 100); // blue
|
||||
let mixed = lerpColor(c1, c2, 0.5); // midpoint blend
|
||||
// Works in current colorMode
|
||||
```
|
||||
|
||||
### paletteLerp (p5.js 1.11+)
|
||||
|
||||
Interpolate through multiple colors at once.
|
||||
|
||||
```javascript
|
||||
let colors = [
|
||||
color('#2E0854'),
|
||||
color('#850E35'),
|
||||
color('#EE6C4D'),
|
||||
color('#F5E663')
|
||||
];
|
||||
let c = paletteLerp(colors, t); // t = 0..1, interpolates through all
|
||||
```
|
||||
|
||||
### Manual Multi-Stop Gradient
|
||||
|
||||
```javascript
|
||||
function multiLerp(colors, t) {
|
||||
t = constrain(t, 0, 1);
|
||||
let segment = t * (colors.length - 1);
|
||||
let idx = floor(segment);
|
||||
let frac = segment - idx;
|
||||
idx = min(idx, colors.length - 2);
|
||||
return lerpColor(colors[idx], colors[idx + 1], frac);
|
||||
}
|
||||
```
|
||||
|
||||
## Gradient Rendering
|
||||
|
||||
### Linear Gradient
|
||||
|
||||
```javascript
|
||||
function linearGradient(x1, y1, x2, y2, c1, c2) {
|
||||
let steps = dist(x1, y1, x2, y2);
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
let t = i / steps;
|
||||
let c = lerpColor(c1, c2, t);
|
||||
stroke(c);
|
||||
let x = lerp(x1, x2, t);
|
||||
let y = lerp(y1, y2, t);
|
||||
// Draw perpendicular line at each point
|
||||
let dx = -(y2 - y1) / steps * 1000;
|
||||
let dy = (x2 - x1) / steps * 1000;
|
||||
line(x - dx, y - dy, x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Radial Gradient
|
||||
|
||||
```javascript
|
||||
function radialGradient(cx, cy, r, innerColor, outerColor) {
|
||||
noStroke();
|
||||
for (let i = r; i > 0; i--) {
|
||||
let t = 1 - i / r;
|
||||
fill(lerpColor(innerColor, outerColor, t));
|
||||
ellipse(cx, cy, i * 2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Noise-Based Gradient
|
||||
|
||||
```javascript
|
||||
function noiseGradient(colors, noiseScale, time) {
|
||||
loadPixels();
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let n = noise(x * noiseScale, y * noiseScale, time);
|
||||
let c = multiLerp(colors, n);
|
||||
let idx = 4 * (y * width + x);
|
||||
pixels[idx] = red(c);
|
||||
pixels[idx+1] = green(c);
|
||||
pixels[idx+2] = blue(c);
|
||||
pixels[idx+3] = 255;
|
||||
}
|
||||
}
|
||||
updatePixels();
|
||||
}
|
||||
```
|
||||
|
||||
## Procedural Palette Generation
|
||||
|
||||
### Complementary
|
||||
|
||||
```javascript
|
||||
function complementary(baseHue) {
|
||||
return [baseHue, (baseHue + 180) % 360];
|
||||
}
|
||||
```
|
||||
|
||||
### Analogous
|
||||
|
||||
```javascript
|
||||
function analogous(baseHue, spread = 30) {
|
||||
return [
|
||||
(baseHue - spread + 360) % 360,
|
||||
baseHue,
|
||||
(baseHue + spread) % 360
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Triadic
|
||||
|
||||
```javascript
|
||||
function triadic(baseHue) {
|
||||
return [baseHue, (baseHue + 120) % 360, (baseHue + 240) % 360];
|
||||
}
|
||||
```
|
||||
|
||||
### Split Complementary
|
||||
|
||||
```javascript
|
||||
function splitComplementary(baseHue) {
|
||||
return [baseHue, (baseHue + 150) % 360, (baseHue + 210) % 360];
|
||||
}
|
||||
```
|
||||
|
||||
### Tetradic (Rectangle)
|
||||
|
||||
```javascript
|
||||
function tetradic(baseHue) {
|
||||
return [baseHue, (baseHue + 60) % 360, (baseHue + 180) % 360, (baseHue + 240) % 360];
|
||||
}
|
||||
```
|
||||
|
||||
### Monochromatic Variations
|
||||
|
||||
```javascript
|
||||
function monoVariations(hue, count = 5) {
|
||||
let colors = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
let s = map(i, 0, count - 1, 20, 90);
|
||||
let b = map(i, 0, count - 1, 95, 40);
|
||||
colors.push(color(hue, s, b));
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
```
|
||||
|
||||
## Curated Palette Library
|
||||
|
||||
### Warm Palettes
|
||||
|
||||
```javascript
|
||||
const SUNSET = ['#2E0854', '#850E35', '#EE6C4D', '#F5E663'];
|
||||
const EMBER = ['#1a0000', '#4a0000', '#8b2500', '#cd5c00', '#ffd700'];
|
||||
const PEACH = ['#fff5eb', '#ffdab9', '#ff9a76', '#ff6b6b', '#c94c4c'];
|
||||
const COPPER = ['#1c1108', '#3d2b1f', '#7b4b2a', '#b87333', '#daa06d'];
|
||||
```
|
||||
|
||||
### Cool Palettes
|
||||
|
||||
```javascript
|
||||
const OCEAN = ['#0a0e27', '#1a1b4b', '#2a4a7f', '#3d7cb8', '#87ceeb'];
|
||||
const ARCTIC = ['#0d1b2a', '#1b263b', '#415a77', '#778da9', '#e0e1dd'];
|
||||
const FOREST = ['#0b1a0b', '#1a3a1a', '#2d5a2d', '#4a8c4a', '#90c990'];
|
||||
const DEEP_SEA = ['#000814', '#001d3d', '#003566', '#006d77', '#83c5be'];
|
||||
```
|
||||
|
||||
### Neutral Palettes
|
||||
|
||||
```javascript
|
||||
const GRAPHITE = ['#1a1a1a', '#333333', '#555555', '#888888', '#cccccc'];
|
||||
const CREAM = ['#f4f0e8', '#e8dcc8', '#c9b99a', '#a89070', '#7a6450'];
|
||||
const SLATE = ['#1e293b', '#334155', '#475569', '#64748b', '#94a3b8'];
|
||||
```
|
||||
|
||||
### Vivid Palettes
|
||||
|
||||
```javascript
|
||||
const NEON = ['#ff00ff', '#00ffff', '#ff0080', '#80ff00', '#0080ff'];
|
||||
const RAINBOW = ['#ff0000', '#ff8000', '#ffff00', '#00ff00', '#0000ff', '#8000ff'];
|
||||
const VAPOR = ['#ff71ce', '#01cdfe', '#05ffa1', '#b967ff', '#fffb96'];
|
||||
const CYBER = ['#0f0f0f', '#00ff41', '#ff0090', '#00d4ff', '#ffd000'];
|
||||
```
|
||||
|
||||
### Earth Tones
|
||||
|
||||
```javascript
|
||||
const TERRA = ['#2c1810', '#5c3a2a', '#8b6b4a', '#c4a672', '#e8d5b7'];
|
||||
const MOSS = ['#1a1f16', '#3d4a2e', '#6b7c4f', '#9aab7a', '#c8d4a9'];
|
||||
const CLAY = ['#3b2f2f', '#6b4c4c', '#9e7676', '#c9a0a0', '#e8caca'];
|
||||
```
|
||||
|
||||
## Blend Modes
|
||||
|
||||
```javascript
|
||||
blendMode(BLEND); // default — alpha compositing
|
||||
blendMode(ADD); // additive — bright glow effects
|
||||
blendMode(MULTIPLY); // darkening — shadows, texture overlay
|
||||
blendMode(SCREEN); // lightening — soft glow
|
||||
blendMode(OVERLAY); // contrast boost — high/low emphasis
|
||||
blendMode(DIFFERENCE); // color subtraction — psychedelic
|
||||
blendMode(EXCLUSION); // softer difference
|
||||
blendMode(REPLACE); // overwrite (no alpha blending)
|
||||
blendMode(REMOVE); // subtract alpha
|
||||
blendMode(LIGHTEST); // keep brighter pixel
|
||||
blendMode(DARKEST); // keep darker pixel
|
||||
blendMode(BURN); // darken + saturate
|
||||
blendMode(DODGE); // lighten + saturate
|
||||
blendMode(SOFT_LIGHT); // subtle overlay
|
||||
blendMode(HARD_LIGHT); // strong overlay
|
||||
|
||||
// ALWAYS reset after use
|
||||
blendMode(BLEND);
|
||||
```
|
||||
|
||||
### Blend Mode Recipes
|
||||
|
||||
| Effect | Mode | Use case |
|
||||
|--------|------|----------|
|
||||
| Additive glow | `ADD` | Light beams, fire, particles |
|
||||
| Shadow overlay | `MULTIPLY` | Texture, vignette |
|
||||
| Soft light mix | `SCREEN` | Fog, mist, backlight |
|
||||
| High contrast | `OVERLAY` | Dramatic compositing |
|
||||
| Color negative | `DIFFERENCE` | Glitch, psychedelic |
|
||||
| Layer compositing | `BLEND` | Standard alpha layering |
|
||||
|
||||
## Background Techniques
|
||||
|
||||
### Textured Background
|
||||
|
||||
```javascript
|
||||
function texturedBackground(baseColor, noiseScale, noiseAmount) {
|
||||
loadPixels();
|
||||
let r = red(baseColor), g = green(baseColor), b = blue(baseColor);
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
let x = (i / 4) % width;
|
||||
let y = floor((i / 4) / width);
|
||||
let n = (noise(x * noiseScale, y * noiseScale) - 0.5) * noiseAmount;
|
||||
pixels[i] = constrain(r + n, 0, 255);
|
||||
pixels[i+1] = constrain(g + n, 0, 255);
|
||||
pixels[i+2] = constrain(b + n, 0, 255);
|
||||
pixels[i+3] = 255;
|
||||
}
|
||||
updatePixels();
|
||||
}
|
||||
```
|
||||
|
||||
### Vignette
|
||||
|
||||
```javascript
|
||||
function vignette(strength = 0.5, radius = 0.7) {
|
||||
loadPixels();
|
||||
let cx = width / 2, cy = height / 2;
|
||||
let maxDist = dist(0, 0, cx, cy);
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
let x = (i / 4) % width;
|
||||
let y = floor((i / 4) / width);
|
||||
let d = dist(x, y, cx, cy) / maxDist;
|
||||
let factor = 1.0 - smoothstep(constrain((d - radius) / (1 - radius), 0, 1)) * strength;
|
||||
pixels[i] *= factor;
|
||||
pixels[i+1] *= factor;
|
||||
pixels[i+2] *= factor;
|
||||
}
|
||||
updatePixels();
|
||||
}
|
||||
|
||||
function smoothstep(t) { return t * t * (3 - 2 * t); }
|
||||
```
|
||||
|
||||
### Film Grain
|
||||
|
||||
```javascript
|
||||
function filmGrain(amount = 30) {
|
||||
loadPixels();
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
let grain = random(-amount, amount);
|
||||
pixels[i] = constrain(pixels[i] + grain, 0, 255);
|
||||
pixels[i+1] = constrain(pixels[i+1] + grain, 0, 255);
|
||||
pixels[i+2] = constrain(pixels[i+2] + grain, 0, 255);
|
||||
}
|
||||
updatePixels();
|
||||
}
|
||||
```
|
||||
410
skills/creative/p5js/references/core-api.md
Normal file
410
skills/creative/p5js/references/core-api.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# Core API Reference
|
||||
|
||||
## Canvas Setup
|
||||
|
||||
### createCanvas()
|
||||
|
||||
```javascript
|
||||
// 2D (default renderer)
|
||||
createCanvas(1920, 1080);
|
||||
|
||||
// WebGL (3D, shaders)
|
||||
createCanvas(1920, 1080, WEBGL);
|
||||
|
||||
// Responsive
|
||||
createCanvas(windowWidth, windowHeight);
|
||||
```
|
||||
|
||||
### Pixel Density
|
||||
|
||||
High-DPI displays render at 2x by default. This doubles memory usage and halves performance.
|
||||
|
||||
```javascript
|
||||
// Force 1x for consistent export and performance
|
||||
pixelDensity(1);
|
||||
|
||||
// Match display (default) — sharp on retina but expensive
|
||||
pixelDensity(displayDensity());
|
||||
|
||||
// ALWAYS call before createCanvas()
|
||||
function setup() {
|
||||
pixelDensity(1); // first
|
||||
createCanvas(1920, 1080); // second
|
||||
}
|
||||
```
|
||||
|
||||
For export, always `pixelDensity(1)` and use the exact target resolution. Never rely on device scaling for final output.
|
||||
|
||||
### Responsive Resize
|
||||
|
||||
```javascript
|
||||
function windowResized() {
|
||||
resizeCanvas(windowWidth, windowHeight);
|
||||
// Recreate offscreen buffers at new size
|
||||
bgLayer = createGraphics(width, height);
|
||||
// Reinitialize any size-dependent state
|
||||
}
|
||||
```
|
||||
|
||||
## Coordinate System
|
||||
|
||||
### P2D (Default)
|
||||
- Origin: top-left (0, 0)
|
||||
- X increases rightward
|
||||
- Y increases downward
|
||||
- Angles: radians by default, `angleMode(DEGREES)` to switch
|
||||
|
||||
### WEBGL
|
||||
- Origin: center of canvas
|
||||
- X increases rightward, Y increases **upward**, Z increases toward viewer
|
||||
- To get P2D-like coordinates in WEBGL: `translate(-width/2, -height/2)`
|
||||
|
||||
## Draw Loop
|
||||
|
||||
```javascript
|
||||
function preload() {
|
||||
// Load assets before setup — fonts, images, JSON, CSV
|
||||
// Blocks execution until all loads complete
|
||||
font = loadFont('font.otf');
|
||||
img = loadImage('texture.png');
|
||||
data = loadJSON('data.json');
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// Runs once. Create canvas, initialize state.
|
||||
createCanvas(1920, 1080);
|
||||
colorMode(HSB, 360, 100, 100, 100);
|
||||
randomSeed(CONFIG.seed);
|
||||
noiseSeed(CONFIG.seed);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Runs every frame (default 60fps).
|
||||
// Set frameRate(30) in setup() to change.
|
||||
// Call noLoop() for static sketches (render once).
|
||||
}
|
||||
```
|
||||
|
||||
### Frame Control
|
||||
|
||||
```javascript
|
||||
frameRate(30); // set target FPS
|
||||
noLoop(); // stop draw loop (static pieces)
|
||||
loop(); // restart draw loop
|
||||
redraw(); // call draw() once (manual refresh)
|
||||
frameCount // frames since start (integer)
|
||||
deltaTime // milliseconds since last frame (float)
|
||||
millis() // milliseconds since sketch started
|
||||
```
|
||||
|
||||
## Transform Stack
|
||||
|
||||
Every transform is cumulative. Use `push()`/`pop()` to isolate.
|
||||
|
||||
```javascript
|
||||
push();
|
||||
translate(width / 2, height / 2);
|
||||
rotate(angle);
|
||||
scale(1.5);
|
||||
// draw something at transformed position
|
||||
ellipse(0, 0, 100, 100);
|
||||
pop();
|
||||
// back to original coordinate system
|
||||
```
|
||||
|
||||
### Transform Functions
|
||||
|
||||
| Function | Effect |
|
||||
|----------|--------|
|
||||
| `translate(x, y)` | Move origin |
|
||||
| `rotate(angle)` | Rotate around origin (radians) |
|
||||
| `scale(s)` / `scale(sx, sy)` | Scale from origin |
|
||||
| `shearX(angle)` | Skew X axis |
|
||||
| `shearY(angle)` | Skew Y axis |
|
||||
| `applyMatrix(a, b, c, d, e, f)` | Arbitrary 2D affine transform |
|
||||
| `resetMatrix()` | Clear all transforms |
|
||||
|
||||
### Composition Pattern: Rotate Around Center
|
||||
|
||||
```javascript
|
||||
push();
|
||||
translate(cx, cy); // move origin to center
|
||||
rotate(angle); // rotate around that center
|
||||
translate(-cx, -cy); // move origin back
|
||||
// draw at original coordinates, but rotated around (cx, cy)
|
||||
rect(cx - 50, cy - 50, 100, 100);
|
||||
pop();
|
||||
```
|
||||
|
||||
## Offscreen Buffers (createGraphics)
|
||||
|
||||
Offscreen buffers are separate canvases you can draw to and composite. Essential for:
|
||||
- **Layered composition** — background, midground, foreground
|
||||
- **Persistent trails** — draw to buffer, fade with semi-transparent rect, never clear
|
||||
- **Masking** — draw mask to buffer, apply with `image()` or pixel operations
|
||||
- **Post-processing** — render scene to buffer, apply effects, draw to main canvas
|
||||
|
||||
```javascript
|
||||
let layer;
|
||||
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
layer = createGraphics(width, height);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Draw to offscreen buffer
|
||||
layer.background(0, 10); // semi-transparent clear = trails
|
||||
layer.fill(255);
|
||||
layer.ellipse(mouseX, mouseY, 20);
|
||||
|
||||
// Composite to main canvas
|
||||
image(layer, 0, 0);
|
||||
}
|
||||
```
|
||||
|
||||
### Trail Effect Pattern
|
||||
|
||||
```javascript
|
||||
let trailBuffer;
|
||||
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
trailBuffer = createGraphics(width, height);
|
||||
trailBuffer.background(0);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Fade previous frame (lower alpha = longer trails)
|
||||
trailBuffer.noStroke();
|
||||
trailBuffer.fill(0, 0, 0, 15); // RGBA — 15/255 alpha
|
||||
trailBuffer.rect(0, 0, width, height);
|
||||
|
||||
// Draw new content
|
||||
trailBuffer.fill(255);
|
||||
trailBuffer.ellipse(mouseX, mouseY, 10);
|
||||
|
||||
// Show
|
||||
image(trailBuffer, 0, 0);
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Layer Composition
|
||||
|
||||
```javascript
|
||||
let bgLayer, contentLayer, fxLayer;
|
||||
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
bgLayer = createGraphics(width, height);
|
||||
contentLayer = createGraphics(width, height);
|
||||
fxLayer = createGraphics(width, height);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Background — drawn once or slowly evolving
|
||||
renderBackground(bgLayer);
|
||||
|
||||
// Content — main visual elements
|
||||
contentLayer.clear();
|
||||
renderContent(contentLayer);
|
||||
|
||||
// FX — overlays, vignettes, grain
|
||||
fxLayer.clear();
|
||||
renderEffects(fxLayer);
|
||||
|
||||
// Composite with blend modes
|
||||
image(bgLayer, 0, 0);
|
||||
blendMode(ADD);
|
||||
image(contentLayer, 0, 0);
|
||||
blendMode(MULTIPLY);
|
||||
image(fxLayer, 0, 0);
|
||||
blendMode(BLEND); // reset
|
||||
}
|
||||
```
|
||||
|
||||
## Composition Patterns
|
||||
|
||||
### Grid Layout
|
||||
|
||||
```javascript
|
||||
let cols = 10, rows = 10;
|
||||
let cellW = width / cols;
|
||||
let cellH = height / rows;
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
let cx = cellW * (i + 0.5);
|
||||
let cy = cellH * (j + 0.5);
|
||||
// draw element at (cx, cy) within cell size (cellW, cellH)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Radial Layout
|
||||
|
||||
```javascript
|
||||
let n = 12;
|
||||
for (let i = 0; i < n; i++) {
|
||||
let angle = TWO_PI * i / n;
|
||||
let r = 300;
|
||||
let x = width/2 + cos(angle) * r;
|
||||
let y = height/2 + sin(angle) * r;
|
||||
// draw element at (x, y)
|
||||
}
|
||||
```
|
||||
|
||||
### Golden Ratio Spiral
|
||||
|
||||
```javascript
|
||||
let phi = (1 + sqrt(5)) / 2;
|
||||
let n = 500;
|
||||
for (let i = 0; i < n; i++) {
|
||||
let angle = i * TWO_PI / (phi * phi);
|
||||
let r = sqrt(i) * 10;
|
||||
let x = width/2 + cos(angle) * r;
|
||||
let y = height/2 + sin(angle) * r;
|
||||
let size = map(i, 0, n, 8, 2);
|
||||
ellipse(x, y, size);
|
||||
}
|
||||
```
|
||||
|
||||
### Margin-Aware Composition
|
||||
|
||||
```javascript
|
||||
const MARGIN = 80; // pixels from edge
|
||||
const drawW = width - 2 * MARGIN;
|
||||
const drawH = height - 2 * MARGIN;
|
||||
|
||||
// Map normalized [0,1] coordinates to drawable area
|
||||
function mapX(t) { return MARGIN + t * drawW; }
|
||||
function mapY(t) { return MARGIN + t * drawH; }
|
||||
```
|
||||
|
||||
## Random and Noise
|
||||
|
||||
### Seeded Random
|
||||
|
||||
```javascript
|
||||
randomSeed(42);
|
||||
let x = random(100); // always same value for seed 42
|
||||
let y = random(-1, 1); // range
|
||||
let item = random(myArray); // random element
|
||||
```
|
||||
|
||||
### Gaussian Random
|
||||
|
||||
```javascript
|
||||
let x = randomGaussian(0, 1); // mean=0, stddev=1
|
||||
// Useful for natural-looking distributions
|
||||
```
|
||||
|
||||
### Perlin Noise
|
||||
|
||||
```javascript
|
||||
noiseSeed(42);
|
||||
noiseDetail(4, 0.5); // 4 octaves, 0.5 falloff
|
||||
|
||||
let v = noise(x * 0.01, y * 0.01); // returns 0.0 to 1.0
|
||||
// Scale factor (0.01) controls feature size — smaller = smoother
|
||||
```
|
||||
|
||||
## Math Utilities
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `map(v, lo1, hi1, lo2, hi2)` | Remap value between ranges |
|
||||
| `constrain(v, lo, hi)` | Clamp to range |
|
||||
| `lerp(a, b, t)` | Linear interpolation |
|
||||
| `norm(v, lo, hi)` | Normalize to 0-1 |
|
||||
| `dist(x1, y1, x2, y2)` | Euclidean distance |
|
||||
| `mag(x, y)` | Vector magnitude |
|
||||
| `abs()`, `ceil()`, `floor()`, `round()` | Standard math |
|
||||
| `sq(n)`, `sqrt(n)`, `pow(b, e)` | Powers |
|
||||
| `sin()`, `cos()`, `tan()`, `atan2()` | Trig (radians) |
|
||||
| `degrees(r)`, `radians(d)` | Angle conversion |
|
||||
| `fract(n)` | Fractional part |
|
||||
|
||||
## p5.js 2.0 Changes
|
||||
|
||||
p5.js 2.0 (released Apr 2025, current: 2.2) introduces breaking changes. The p5.js editor defaults to 1.x until Aug 2026. Use 2.x only when you need its features.
|
||||
|
||||
### async setup() replaces preload()
|
||||
|
||||
```javascript
|
||||
// p5.js 1.x
|
||||
let img;
|
||||
function preload() { img = loadImage('cat.jpg'); }
|
||||
function setup() { createCanvas(800, 800); }
|
||||
|
||||
// p5.js 2.x
|
||||
let img;
|
||||
async function setup() {
|
||||
createCanvas(800, 800);
|
||||
img = await loadImage('cat.jpg');
|
||||
}
|
||||
```
|
||||
|
||||
### New Color Modes
|
||||
|
||||
```javascript
|
||||
colorMode(OKLCH); // perceptually uniform — better gradients
|
||||
// L: 0-1 (lightness), C: 0-0.4 (chroma), H: 0-360 (hue)
|
||||
fill(0.7, 0.15, 200); // medium-bright saturated blue
|
||||
|
||||
colorMode(OKLAB); // perceptually uniform, no hue angle
|
||||
colorMode(HWB); // Hue-Whiteness-Blackness
|
||||
```
|
||||
|
||||
### splineVertex() replaces curveVertex()
|
||||
|
||||
No more doubling first/last control points:
|
||||
|
||||
```javascript
|
||||
// p5.js 1.x — must repeat first and last
|
||||
beginShape();
|
||||
curveVertex(pts[0].x, pts[0].y); // doubled
|
||||
for (let p of pts) curveVertex(p.x, p.y);
|
||||
curveVertex(pts[pts.length-1].x, pts[pts.length-1].y); // doubled
|
||||
endShape();
|
||||
|
||||
// p5.js 2.x — clean
|
||||
beginShape();
|
||||
for (let p of pts) splineVertex(p.x, p.y);
|
||||
endShape();
|
||||
```
|
||||
|
||||
### Shader .modify() API
|
||||
|
||||
Modify built-in shaders without writing full GLSL:
|
||||
|
||||
```javascript
|
||||
let myShader = baseMaterialShader().modify({
|
||||
vertexDeclarations: 'uniform float uTime;',
|
||||
'vec4 getWorldPosition': `(vec4 pos) {
|
||||
pos.y += sin(pos.x * 0.1 + uTime) * 20.0;
|
||||
return pos;
|
||||
}`
|
||||
});
|
||||
```
|
||||
|
||||
### Variable Fonts
|
||||
|
||||
```javascript
|
||||
textWeight(700); // dynamic weight without loading multiple files
|
||||
```
|
||||
|
||||
### textToContours() and textToModel()
|
||||
|
||||
```javascript
|
||||
let contours = font.textToContours('HELLO', 0, 0, 200);
|
||||
// Returns array of contour arrays (closed paths)
|
||||
|
||||
let geo = font.textToModel('HELLO', 0, 0, 200);
|
||||
// Returns p5.Geometry for 3D extruded text
|
||||
```
|
||||
|
||||
### CDN for p5.js 2.x
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5@2/lib/p5.min.js"></script>
|
||||
```
|
||||
566
skills/creative/p5js/references/export-pipeline.md
Normal file
566
skills/creative/p5js/references/export-pipeline.md
Normal file
@@ -0,0 +1,566 @@
|
||||
# Export Pipeline
|
||||
|
||||
## PNG Export
|
||||
|
||||
### In-Sketch (Keyboard Shortcut)
|
||||
|
||||
```javascript
|
||||
function keyPressed() {
|
||||
if (key === 's' || key === 'S') {
|
||||
saveCanvas('output', 'png');
|
||||
// Downloads output.png immediately
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timed Export (Static Generative)
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(3840, 2160);
|
||||
pixelDensity(1);
|
||||
randomSeed(CONFIG.seed);
|
||||
noiseSeed(CONFIG.seed);
|
||||
noLoop();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// ... render everything ...
|
||||
saveCanvas('output-seed-' + CONFIG.seed, 'png');
|
||||
}
|
||||
```
|
||||
|
||||
### High-Resolution Export
|
||||
|
||||
For resolutions beyond screen size, use `pixelDensity()` or a large offscreen buffer:
|
||||
|
||||
```javascript
|
||||
function exportHighRes(scale) {
|
||||
let buffer = createGraphics(width * scale, height * scale);
|
||||
buffer.scale(scale);
|
||||
// Re-render everything to buffer at higher resolution
|
||||
renderScene(buffer);
|
||||
buffer.save('highres-output.png');
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Seed Export
|
||||
|
||||
```javascript
|
||||
function exportBatch(startSeed, count) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
CONFIG.seed = startSeed + i;
|
||||
randomSeed(CONFIG.seed);
|
||||
noiseSeed(CONFIG.seed);
|
||||
// Render
|
||||
background(0);
|
||||
renderScene();
|
||||
saveCanvas('seed-' + nf(CONFIG.seed, 5), 'png');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GIF Export
|
||||
|
||||
### saveGif()
|
||||
|
||||
```javascript
|
||||
function keyPressed() {
|
||||
if (key === 'g' || key === 'G') {
|
||||
saveGif('output', 5);
|
||||
// Captures 5 seconds of animation
|
||||
// Options: saveGif(filename, duration, options)
|
||||
}
|
||||
}
|
||||
|
||||
// With options
|
||||
saveGif('output', 5, {
|
||||
delay: 0, // delay before starting capture (seconds)
|
||||
units: 'seconds' // or 'frames'
|
||||
});
|
||||
```
|
||||
|
||||
Limitations:
|
||||
- GIF is 256 colors max — dithering artifacts on gradients
|
||||
- Large canvases produce huge files
|
||||
- Use a smaller canvas (640x360) for GIF, higher for PNG/MP4
|
||||
- Frame rate is approximate
|
||||
|
||||
### Optimal GIF Settings
|
||||
|
||||
```javascript
|
||||
// For GIF output, use smaller canvas and lower framerate
|
||||
function setup() {
|
||||
createCanvas(640, 360);
|
||||
frameRate(15); // GIF standard
|
||||
pixelDensity(1);
|
||||
}
|
||||
```
|
||||
|
||||
## Frame Sequence Export
|
||||
|
||||
### saveFrames()
|
||||
|
||||
```javascript
|
||||
function keyPressed() {
|
||||
if (key === 'f') {
|
||||
saveFrames('frame', 'png', 10, 30);
|
||||
// 10 seconds, 30 fps → 300 PNG files
|
||||
// Downloads as individual files (browser may block bulk downloads)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Frame Export (More Control)
|
||||
|
||||
```javascript
|
||||
let recording = false;
|
||||
let frameNum = 0;
|
||||
const TOTAL_FRAMES = 300;
|
||||
|
||||
function keyPressed() {
|
||||
if (key === 'r') recording = !recording;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// ... render frame ...
|
||||
|
||||
if (recording) {
|
||||
saveCanvas('frame-' + nf(frameNum, 4), 'png');
|
||||
frameNum++;
|
||||
if (frameNum >= TOTAL_FRAMES) {
|
||||
recording = false;
|
||||
noLoop();
|
||||
console.log('Recording complete: ' + frameNum + ' frames');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deterministic Capture (Critical for Video)
|
||||
|
||||
The `noLoop()` + `redraw()` pattern is **required** for frame-perfect headless capture. Without it, p5's draw loop runs freely in Chrome while Puppeteer screenshots are slow — the sketch runs ahead and you get duplicate/missing frames.
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
pixelDensity(1);
|
||||
noLoop(); // STOP the automatic draw loop
|
||||
window._p5Ready = true; // Signal to capture script
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// This only runs when redraw() is called by the capture script
|
||||
// frameCount increments exactly once per redraw()
|
||||
}
|
||||
```
|
||||
|
||||
The bundled `scripts/export-frames.js` detects `window._p5Ready` and switches to deterministic mode automatically. Without it, falls back to timed capture (less precise).
|
||||
|
||||
### ffmpeg: Frames to MP4
|
||||
|
||||
```bash
|
||||
# Basic encoding
|
||||
ffmpeg -framerate 30 -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p output.mp4
|
||||
|
||||
# High quality
|
||||
ffmpeg -framerate 30 -i frame-%04d.png \
|
||||
-c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
|
||||
output.mp4
|
||||
|
||||
# With audio
|
||||
ffmpeg -framerate 30 -i frame-%04d.png -i audio.mp3 \
|
||||
-c:v libx264 -c:a aac -shortest \
|
||||
output.mp4
|
||||
|
||||
# Loop for social media (3 loops)
|
||||
ffmpeg -stream_loop 2 -i output.mp4 -c copy output-looped.mp4
|
||||
```
|
||||
|
||||
### Video Export Gotchas
|
||||
|
||||
**YUV420 clips dark values.** H.264 encodes in YUV420 color space, which rounds dark RGB values. Content below RGB(8,8,8) may become pure black. Subtle dark details (dim particle trails, faint noise textures) disappear in the encoded video even though they're visible in the PNG frames.
|
||||
|
||||
**Fix:** Ensure minimum brightness of ~10 for any visible content. Test by encoding a few frames and comparing the MP4 frame vs the source PNG.
|
||||
|
||||
```bash
|
||||
# Extract a frame from MP4 for comparison
|
||||
ffmpeg -i output.mp4 -vf "select=eq(n\,100)" -vframes 1 check.png
|
||||
```
|
||||
|
||||
**Static frames look broken in video.** If an algorithm produces a single static image (like a pre-computed attractor heatmap), it reads as a freeze/glitch in video. Always add animation even to static content:
|
||||
- Progressive reveal (expand from center, sweep across)
|
||||
- Slow parameter drift (rotate color mapping, shift noise offset)
|
||||
- Camera-like motion (slow zoom, slight pan)
|
||||
- Overlay animated particles or grain
|
||||
|
||||
**Scene transitions are mandatory.** Hard cuts between visually different scenes are jarring. Use fade envelopes:
|
||||
|
||||
```javascript
|
||||
const FADE_FRAMES = 15; // half-second at 30fps
|
||||
let fade = 1;
|
||||
if (localFrame < FADE_FRAMES) fade = localFrame / FADE_FRAMES;
|
||||
if (localFrame > SCENE_FRAMES - FADE_FRAMES) fade = (SCENE_FRAMES - localFrame) / FADE_FRAMES;
|
||||
fade = fade * fade * (3 - 2 * fade); // smoothstep
|
||||
// Apply: multiply all alpha/brightness by fade
|
||||
```
|
||||
|
||||
### Per-Clip Architecture (Multi-Scene Videos)
|
||||
|
||||
For videos with multiple scenes, render each as a separate HTML file + MP4 clip, then stitch with ffmpeg. This enables re-rendering individual scenes without touching the rest.
|
||||
|
||||
**Directory structure:**
|
||||
```
|
||||
project/
|
||||
├── capture-scene.js # Shared: node capture-scene.js <html> <outdir> <frames>
|
||||
├── render-all.sh # Renders all + stitches
|
||||
├── scenes/
|
||||
│ ├── 00-intro.html # Each scene is self-contained
|
||||
│ ├── 01-particles.html
|
||||
│ ├── 02-noise.html
|
||||
│ └── 03-outro.html
|
||||
└── clips/
|
||||
├── 00-intro.mp4 # Each clip rendered independently
|
||||
├── 01-particles.mp4
|
||||
├── 02-noise.mp4
|
||||
├── 03-outro.mp4
|
||||
└── concat.txt
|
||||
```
|
||||
|
||||
**Stitch clips with ffmpeg concat:**
|
||||
```bash
|
||||
# concat.txt (order determines final sequence)
|
||||
file '00-intro.mp4'
|
||||
file '01-particles.mp4'
|
||||
file '02-noise.mp4'
|
||||
file '03-outro.mp4'
|
||||
|
||||
# Lossless stitch (all clips must have same codec/resolution/fps)
|
||||
ffmpeg -f concat -safe 0 -i concat.txt -c copy final.mp4
|
||||
```
|
||||
|
||||
**Re-render a single scene:**
|
||||
```bash
|
||||
node capture-scene.js scenes/01-particles.html clips/01-particles 150
|
||||
ffmpeg -y -framerate 30 -i clips/01-particles/frame-%04d.png \
|
||||
-c:v libx264 -preset slow -crf 16 -pix_fmt yuv420p clips/01-particles.mp4
|
||||
# Then re-stitch
|
||||
ffmpeg -y -f concat -safe 0 -i clips/concat.txt -c copy final.mp4
|
||||
```
|
||||
|
||||
**Re-order without re-rendering:** Just change the order in concat.txt and re-stitch. No frames need re-rendering.
|
||||
|
||||
**Each scene HTML must:**
|
||||
- Call `noLoop()` in setup and set `window._p5Ready = true`
|
||||
- Use `frameCount`-based timing (not `millis()`) for deterministic output
|
||||
- Handle its own fade-in/fade-out envelope
|
||||
- Be fully self-contained (no shared state between scenes)
|
||||
|
||||
### ffmpeg: Frames to GIF (Better Quality)
|
||||
|
||||
```bash
|
||||
# Generate palette first for optimal colors
|
||||
ffmpeg -i frame-%04d.png -vf "fps=15,palettegen=max_colors=256" palette.png
|
||||
|
||||
# Render GIF using palette
|
||||
ffmpeg -i frame-%04d.png -i palette.png \
|
||||
-lavfi "fps=15 [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=3" \
|
||||
output.gif
|
||||
```
|
||||
|
||||
## Headless Export (Puppeteer)
|
||||
|
||||
For automated, server-side, or CI rendering. Uses a headless Chrome browser to run the sketch.
|
||||
|
||||
### export-frames.js (Node.js Script)
|
||||
|
||||
See `scripts/export-frames.js` for the full implementation. Basic pattern:
|
||||
|
||||
```javascript
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
async function captureFrames(htmlPath, outputDir, options) {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setViewport({
|
||||
width: options.width || 1920,
|
||||
height: options.height || 1080,
|
||||
deviceScaleFactor: 1
|
||||
});
|
||||
|
||||
await page.goto(`file://${path.resolve(htmlPath)}`, {
|
||||
waitUntil: 'networkidle0'
|
||||
});
|
||||
|
||||
// Wait for sketch to initialize
|
||||
await page.waitForSelector('canvas');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
for (let i = 0; i < options.frames; i++) {
|
||||
const canvas = await page.$('canvas');
|
||||
await canvas.screenshot({
|
||||
path: path.join(outputDir, `frame-${String(i).padStart(4, '0')}.png`)
|
||||
});
|
||||
|
||||
// Advance one frame
|
||||
await page.evaluate(() => { redraw(); });
|
||||
await page.waitForTimeout(1000 / options.fps);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
```
|
||||
|
||||
### render.sh (Full Pipeline)
|
||||
|
||||
See `scripts/render.sh` for the complete render script. Pipeline:
|
||||
|
||||
```
|
||||
1. Launch Puppeteer → open sketch HTML
|
||||
2. Capture N frames as PNG sequence
|
||||
3. Pipe to ffmpeg → encode H.264 MP4
|
||||
4. Optional: add audio track
|
||||
5. Clean up temp frames
|
||||
```
|
||||
|
||||
## SVG Export
|
||||
|
||||
### Using p5.js-svg Library
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/p5.js-svg@1.5.1"></script>
|
||||
```
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(1920, 1080, SVG); // SVG renderer
|
||||
noLoop();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Only vector operations (no pixels, no blend modes)
|
||||
stroke(0);
|
||||
noFill();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
let x = random(width);
|
||||
let y = random(height);
|
||||
ellipse(x, y, random(10, 50));
|
||||
}
|
||||
save('output.svg');
|
||||
}
|
||||
```
|
||||
|
||||
Limitations:
|
||||
- No `loadPixels()`, `updatePixels()`, `filter()`, `blendMode()`
|
||||
- No WebGL
|
||||
- No pixel-level effects
|
||||
- Great for: line art, geometric patterns, plots
|
||||
|
||||
### Hybrid: Raster Background + SVG Overlay
|
||||
|
||||
Render background effects to PNG, then SVG for crisp vector elements on top.
|
||||
|
||||
## Export Format Decision Guide
|
||||
|
||||
| Need | Format | Method |
|
||||
|------|--------|--------|
|
||||
| Single still image | PNG | `saveCanvas()` or `keyPressed()` |
|
||||
| Print-quality still | PNG (high-res) | `pixelDensity(1)` + large canvas |
|
||||
| Short animated loop | GIF | `saveGif()` |
|
||||
| Long animation | MP4 | Frame sequence + ffmpeg |
|
||||
| Social media video | MP4 | `scripts/render.sh` |
|
||||
| Vector/print | SVG | p5.js-svg renderer |
|
||||
| Batch variations | PNG sequence | Seed loop + `saveCanvas()` |
|
||||
| Interactive deployment | HTML | Single self-contained file |
|
||||
| Headless rendering | PNG/MP4 | Puppeteer + ffmpeg |
|
||||
|
||||
## Tiling for Ultra-High-Resolution
|
||||
|
||||
For resolutions too large for a single canvas (e.g., 10000x10000 for print):
|
||||
|
||||
```javascript
|
||||
function renderTiled(totalW, totalH, tileSize) {
|
||||
let cols = ceil(totalW / tileSize);
|
||||
let rows = ceil(totalH / tileSize);
|
||||
|
||||
for (let ty = 0; ty < rows; ty++) {
|
||||
for (let tx = 0; tx < cols; tx++) {
|
||||
let buffer = createGraphics(tileSize, tileSize);
|
||||
buffer.push();
|
||||
buffer.translate(-tx * tileSize, -ty * tileSize);
|
||||
renderScene(buffer, totalW, totalH);
|
||||
buffer.pop();
|
||||
buffer.save(`tile-${tx}-${ty}.png`);
|
||||
buffer.remove(); // free memory
|
||||
}
|
||||
}
|
||||
// Stitch with ImageMagick:
|
||||
// montage tile-*.png -tile 4x4 -geometry +0+0 final.png
|
||||
}
|
||||
```
|
||||
|
||||
## CCapture.js — Deterministic Video Capture
|
||||
|
||||
The built-in `saveFrames()` has limitations: small frame counts, memory issues, browser download blocking. CCapture.js solves all of these by hooking into the browser's timing functions to simulate constant time steps regardless of actual render speed.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/ccapture.js-npmfixed/build/CCapture.all.min.js"></script>
|
||||
```
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```javascript
|
||||
let capturer;
|
||||
let recording = false;
|
||||
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
pixelDensity(1);
|
||||
|
||||
capturer = new CCapture({
|
||||
format: 'webm', // 'webm', 'gif', 'png', 'jpg'
|
||||
framerate: 30,
|
||||
quality: 99, // 0-100 for webm/jpg
|
||||
// timeLimit: 10, // auto-stop after N seconds
|
||||
// motionBlurFrames: 4 // supersampled motion blur
|
||||
});
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// ... render frame ...
|
||||
|
||||
if (recording) {
|
||||
capturer.capture(document.querySelector('canvas'));
|
||||
}
|
||||
}
|
||||
|
||||
function keyPressed() {
|
||||
if (key === 'c') {
|
||||
if (!recording) {
|
||||
capturer.start();
|
||||
recording = true;
|
||||
console.log('Recording started');
|
||||
} else {
|
||||
capturer.stop();
|
||||
capturer.save(); // triggers download
|
||||
recording = false;
|
||||
console.log('Recording saved');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Format Comparison
|
||||
|
||||
| Format | Quality | Size | Browser Support |
|
||||
|--------|---------|------|-----------------|
|
||||
| **WebM** | High | Medium | Chrome only |
|
||||
| **GIF** | 256 colors | Large | All (via gif.js worker) |
|
||||
| **PNG sequence** | Lossless | Very large (TAR) | All |
|
||||
| **JPEG sequence** | Lossy | Large (TAR) | All |
|
||||
|
||||
### Important: Timing Hook
|
||||
|
||||
CCapture.js overrides `Date.now()`, `setTimeout`, `requestAnimationFrame`, and `performance.now()`. This means:
|
||||
- `millis()` returns simulated time (perfect for recording)
|
||||
- `deltaTime` is constant (1000/framerate)
|
||||
- Complex sketches that take 500ms per frame still record at smooth 30fps
|
||||
- **Caveat**: Audio sync breaks (audio plays in real-time, not simulated time)
|
||||
|
||||
## Programmatic Export (canvas API)
|
||||
|
||||
For custom export workflows beyond `saveCanvas()`:
|
||||
|
||||
```javascript
|
||||
// Canvas to Blob (for upload, processing)
|
||||
document.querySelector('canvas').toBlob((blob) => {
|
||||
// Upload to server, process, etc.
|
||||
let url = URL.createObjectURL(blob);
|
||||
console.log('Blob URL:', url);
|
||||
}, 'image/png');
|
||||
|
||||
// Canvas to Data URL (for inline embedding)
|
||||
let dataUrl = document.querySelector('canvas').toDataURL('image/png');
|
||||
// Use in <img src="..."> or send as base64
|
||||
```
|
||||
|
||||
## SVG Export (p5.js-svg)
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/p5.js-svg@1.6.0"></script>
|
||||
```
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(1920, 1080, SVG); // SVG renderer
|
||||
noLoop();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Only vector operations work (no pixel ops, no blendMode)
|
||||
stroke(0);
|
||||
noFill();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
ellipse(random(width), random(height), random(10, 50));
|
||||
}
|
||||
save('output.svg');
|
||||
}
|
||||
```
|
||||
|
||||
**Critical SVG caveats:**
|
||||
- **Must call `clear()` in `draw()`** for animated sketches — SVG DOM accumulates child elements, causing memory bloat
|
||||
- `blendMode()` is **not implemented** in SVG renderer
|
||||
- `filter()`, `loadPixels()`, `updatePixels()` don't work
|
||||
- Requires **p5.js 1.11.x** — not compatible with p5.js 2.x
|
||||
- Perfect for: line art, geometric patterns, pen plotter output
|
||||
|
||||
## Platform Export
|
||||
|
||||
### fxhash Conventions
|
||||
|
||||
```javascript
|
||||
// Replace p5's random with fxhash's deterministic PRNG
|
||||
const rng = $fx.rand;
|
||||
|
||||
// Declare features for rarity/filtering
|
||||
$fx.features({
|
||||
'Palette': paletteName,
|
||||
'Complexity': complexity > 0.7 ? 'High' : 'Low',
|
||||
'Has Particles': particleCount > 0
|
||||
});
|
||||
|
||||
// Declare on-chain parameters
|
||||
$fx.params([
|
||||
{ id: 'density', name: 'Density', type: 'number',
|
||||
options: { min: 1, max: 100, step: 1 } },
|
||||
{ id: 'palette', name: 'Palette', type: 'select',
|
||||
options: { options: ['Warm', 'Cool', 'Mono'] } },
|
||||
{ id: 'accent', name: 'Accent Color', type: 'color' }
|
||||
]);
|
||||
|
||||
// Read params
|
||||
let density = $fx.getParam('density');
|
||||
|
||||
// Build: npx fxhash build → upload.zip
|
||||
// Dev: npx fxhash dev → localhost:3300
|
||||
```
|
||||
|
||||
### Art Blocks / Generic Platform
|
||||
|
||||
```javascript
|
||||
// Platform provides a hash string
|
||||
const hash = tokenData.hash; // Art Blocks convention
|
||||
|
||||
// Build deterministic PRNG from hash
|
||||
function prngFromHash(hash) {
|
||||
let seed = parseInt(hash.slice(0, 16), 16);
|
||||
// xoshiro128** or similar
|
||||
return function() { /* ... */ };
|
||||
}
|
||||
|
||||
const rng = prngFromHash(hash);
|
||||
```
|
||||
398
skills/creative/p5js/references/interaction.md
Normal file
398
skills/creative/p5js/references/interaction.md
Normal file
@@ -0,0 +1,398 @@
|
||||
# Interaction
|
||||
|
||||
## Mouse Events
|
||||
|
||||
### Continuous State
|
||||
|
||||
```javascript
|
||||
mouseX, mouseY // current position (relative to canvas)
|
||||
pmouseX, pmouseY // previous frame position
|
||||
mouseIsPressed // boolean
|
||||
mouseButton // LEFT, RIGHT, CENTER (during press)
|
||||
movedX, movedY // delta since last frame
|
||||
winMouseX, winMouseY // relative to window (not canvas)
|
||||
```
|
||||
|
||||
### Event Callbacks
|
||||
|
||||
```javascript
|
||||
function mousePressed() {
|
||||
// fires once on press
|
||||
// mouseButton tells you which button
|
||||
}
|
||||
|
||||
function mouseReleased() {
|
||||
// fires once on release
|
||||
}
|
||||
|
||||
function mouseClicked() {
|
||||
// fires after press+release (same element)
|
||||
}
|
||||
|
||||
function doubleClicked() {
|
||||
// fires on double-click
|
||||
}
|
||||
|
||||
function mouseMoved() {
|
||||
// fires when mouse moves (no button pressed)
|
||||
}
|
||||
|
||||
function mouseDragged() {
|
||||
// fires when mouse moves WITH button pressed
|
||||
}
|
||||
|
||||
function mouseWheel(event) {
|
||||
// event.delta: positive = scroll down, negative = scroll up
|
||||
zoom += event.delta * -0.01;
|
||||
return false; // prevent page scroll
|
||||
}
|
||||
```
|
||||
|
||||
### Mouse Interaction Patterns
|
||||
|
||||
**Spawn on click:**
|
||||
```javascript
|
||||
function mousePressed() {
|
||||
particles.push(new Particle(mouseX, mouseY));
|
||||
}
|
||||
```
|
||||
|
||||
**Mouse follow with spring:**
|
||||
```javascript
|
||||
let springX, springY;
|
||||
function setup() {
|
||||
springX = new Spring(width/2, width/2);
|
||||
springY = new Spring(height/2, height/2);
|
||||
}
|
||||
function draw() {
|
||||
springX.setTarget(mouseX);
|
||||
springY.setTarget(mouseY);
|
||||
let x = springX.update();
|
||||
let y = springY.update();
|
||||
ellipse(x, y, 50);
|
||||
}
|
||||
```
|
||||
|
||||
**Drag interaction:**
|
||||
```javascript
|
||||
let dragging = false;
|
||||
let dragObj = null;
|
||||
let offsetX, offsetY;
|
||||
|
||||
function mousePressed() {
|
||||
for (let obj of objects) {
|
||||
if (dist(mouseX, mouseY, obj.x, obj.y) < obj.radius) {
|
||||
dragging = true;
|
||||
dragObj = obj;
|
||||
offsetX = mouseX - obj.x;
|
||||
offsetY = mouseY - obj.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mouseDragged() {
|
||||
if (dragging && dragObj) {
|
||||
dragObj.x = mouseX - offsetX;
|
||||
dragObj.y = mouseY - offsetY;
|
||||
}
|
||||
}
|
||||
|
||||
function mouseReleased() {
|
||||
dragging = false;
|
||||
dragObj = null;
|
||||
}
|
||||
```
|
||||
|
||||
**Mouse repulsion (particles flee cursor):**
|
||||
```javascript
|
||||
function draw() {
|
||||
let mousePos = createVector(mouseX, mouseY);
|
||||
for (let p of particles) {
|
||||
let d = p.pos.dist(mousePos);
|
||||
if (d < 150) {
|
||||
let repel = p5.Vector.sub(p.pos, mousePos);
|
||||
repel.normalize();
|
||||
repel.mult(map(d, 0, 150, 5, 0));
|
||||
p.applyForce(repel);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Keyboard Events
|
||||
|
||||
### State
|
||||
|
||||
```javascript
|
||||
keyIsPressed // boolean
|
||||
key // last key as string ('a', 'A', ' ')
|
||||
keyCode // numeric code (LEFT_ARROW, UP_ARROW, etc.)
|
||||
```
|
||||
|
||||
### Event Callbacks
|
||||
|
||||
```javascript
|
||||
function keyPressed() {
|
||||
// fires once on press
|
||||
if (keyCode === LEFT_ARROW) { /* ... */ }
|
||||
if (key === 's') saveCanvas('output', 'png');
|
||||
if (key === ' ') CONFIG.paused = !CONFIG.paused;
|
||||
return false; // prevent default browser behavior
|
||||
}
|
||||
|
||||
function keyReleased() {
|
||||
// fires once on release
|
||||
}
|
||||
|
||||
function keyTyped() {
|
||||
// fires for printable characters only (not arrows, shift, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
### Continuous Key State (Multiple Keys)
|
||||
|
||||
```javascript
|
||||
let keys = {};
|
||||
|
||||
function keyPressed() { keys[keyCode] = true; }
|
||||
function keyReleased() { keys[keyCode] = false; }
|
||||
|
||||
function draw() {
|
||||
if (keys[LEFT_ARROW]) player.x -= 5;
|
||||
if (keys[RIGHT_ARROW]) player.x += 5;
|
||||
if (keys[UP_ARROW]) player.y -= 5;
|
||||
if (keys[DOWN_ARROW]) player.y += 5;
|
||||
}
|
||||
```
|
||||
|
||||
### Key Constants
|
||||
|
||||
```
|
||||
LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW
|
||||
BACKSPACE, DELETE, ENTER, RETURN, TAB, ESCAPE
|
||||
SHIFT, CONTROL, OPTION, ALT
|
||||
```
|
||||
|
||||
## Touch Events
|
||||
|
||||
```javascript
|
||||
touches // array of { x, y, id } — all current touches
|
||||
|
||||
function touchStarted() {
|
||||
// fires on first touch
|
||||
return false; // prevent default (stops scroll on mobile)
|
||||
}
|
||||
|
||||
function touchMoved() {
|
||||
// fires on touch drag
|
||||
return false;
|
||||
}
|
||||
|
||||
function touchEnded() {
|
||||
// fires on touch release
|
||||
}
|
||||
```
|
||||
|
||||
### Pinch Zoom
|
||||
|
||||
```javascript
|
||||
let prevDist = 0;
|
||||
let zoomLevel = 1;
|
||||
|
||||
function touchMoved() {
|
||||
if (touches.length === 2) {
|
||||
let d = dist(touches[0].x, touches[0].y, touches[1].x, touches[1].y);
|
||||
if (prevDist > 0) {
|
||||
zoomLevel *= d / prevDist;
|
||||
}
|
||||
prevDist = d;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function touchEnded() {
|
||||
prevDist = 0;
|
||||
}
|
||||
```
|
||||
|
||||
## DOM Elements
|
||||
|
||||
### Creating Controls
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(800, 800);
|
||||
|
||||
// Slider
|
||||
let slider = createSlider(0, 255, 100, 1); // min, max, default, step
|
||||
slider.position(10, height + 10);
|
||||
slider.input(() => { CONFIG.value = slider.value(); });
|
||||
|
||||
// Button
|
||||
let btn = createButton('Reset');
|
||||
btn.position(10, height + 40);
|
||||
btn.mousePressed(() => { resetSketch(); });
|
||||
|
||||
// Checkbox
|
||||
let check = createCheckbox('Show grid', false);
|
||||
check.position(10, height + 70);
|
||||
check.changed(() => { CONFIG.showGrid = check.checked(); });
|
||||
|
||||
// Select / dropdown
|
||||
let sel = createSelect();
|
||||
sel.position(10, height + 100);
|
||||
sel.option('Mode A');
|
||||
sel.option('Mode B');
|
||||
sel.changed(() => { CONFIG.mode = sel.value(); });
|
||||
|
||||
// Color picker
|
||||
let picker = createColorPicker('#ff0000');
|
||||
picker.position(10, height + 130);
|
||||
picker.input(() => { CONFIG.color = picker.value(); });
|
||||
|
||||
// Text input
|
||||
let inp = createInput('Hello');
|
||||
inp.position(10, height + 160);
|
||||
inp.input(() => { CONFIG.text = inp.value(); });
|
||||
}
|
||||
```
|
||||
|
||||
### Styling DOM Elements
|
||||
|
||||
```javascript
|
||||
let slider = createSlider(0, 100, 50);
|
||||
slider.position(10, 10);
|
||||
slider.style('width', '200px');
|
||||
slider.class('my-slider');
|
||||
slider.parent('controls-div'); // attach to specific DOM element
|
||||
```
|
||||
|
||||
## Audio Input (p5.sound)
|
||||
|
||||
Requires `p5.sound.min.js` addon.
|
||||
|
||||
```html
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/addons/p5.sound.min.js"></script>
|
||||
```
|
||||
|
||||
### Microphone Input
|
||||
|
||||
```javascript
|
||||
let mic, fft, amplitude;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800);
|
||||
userStartAudio(); // required — user gesture to enable audio
|
||||
|
||||
mic = new p5.AudioIn();
|
||||
mic.start();
|
||||
|
||||
fft = new p5.FFT(0.8, 256); // smoothing, bins
|
||||
fft.setInput(mic);
|
||||
|
||||
amplitude = new p5.Amplitude();
|
||||
amplitude.setInput(mic);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
let level = amplitude.getLevel(); // 0.0 to 1.0 (overall volume)
|
||||
let spectrum = fft.analyze(); // array of 256 frequency values (0-255)
|
||||
let waveform = fft.waveform(); // array of 256 time-domain samples (-1 to 1)
|
||||
|
||||
// Get energy in frequency bands
|
||||
let bass = fft.getEnergy('bass'); // 20-140 Hz
|
||||
let lowMid = fft.getEnergy('lowMid'); // 140-400 Hz
|
||||
let mid = fft.getEnergy('mid'); // 400-2600 Hz
|
||||
let highMid = fft.getEnergy('highMid'); // 2600-5200 Hz
|
||||
let treble = fft.getEnergy('treble'); // 5200-14000 Hz
|
||||
// Each returns 0-255
|
||||
}
|
||||
```
|
||||
|
||||
### Audio File Playback
|
||||
|
||||
```javascript
|
||||
let song, fft;
|
||||
|
||||
function preload() {
|
||||
song = loadSound('track.mp3');
|
||||
}
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800);
|
||||
fft = new p5.FFT(0.8, 512);
|
||||
fft.setInput(song);
|
||||
}
|
||||
|
||||
function mousePressed() {
|
||||
if (song.isPlaying()) {
|
||||
song.pause();
|
||||
} else {
|
||||
song.play();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Beat Detection (Simple)
|
||||
|
||||
```javascript
|
||||
let prevBass = 0;
|
||||
let beatThreshold = 30;
|
||||
let beatCooldown = 0;
|
||||
|
||||
function detectBeat() {
|
||||
let bass = fft.getEnergy('bass');
|
||||
let isBeat = bass - prevBass > beatThreshold && beatCooldown <= 0;
|
||||
prevBass = bass;
|
||||
if (isBeat) beatCooldown = 10; // frames
|
||||
beatCooldown--;
|
||||
return isBeat;
|
||||
}
|
||||
```
|
||||
|
||||
## Scroll-Driven Animation
|
||||
|
||||
```javascript
|
||||
let scrollProgress = 0;
|
||||
|
||||
function setup() {
|
||||
let canvas = createCanvas(windowWidth, windowHeight);
|
||||
canvas.style('position', 'fixed');
|
||||
// Make page scrollable
|
||||
document.body.style.height = '500vh';
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let maxScroll = document.body.scrollHeight - window.innerHeight;
|
||||
scrollProgress = window.scrollY / maxScroll;
|
||||
});
|
||||
|
||||
function draw() {
|
||||
background(0);
|
||||
// Use scrollProgress (0 to 1) to drive animation
|
||||
let x = lerp(0, width, scrollProgress);
|
||||
ellipse(x, height/2, 50);
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Events
|
||||
|
||||
```javascript
|
||||
function windowResized() {
|
||||
resizeCanvas(windowWidth, windowHeight);
|
||||
// Recreate buffers
|
||||
bgLayer = createGraphics(width, height);
|
||||
// Recalculate layout
|
||||
recalculateLayout();
|
||||
}
|
||||
|
||||
// Visibility change (tab switching)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
noLoop(); // pause when tab not visible
|
||||
} else {
|
||||
loop();
|
||||
}
|
||||
});
|
||||
```
|
||||
300
skills/creative/p5js/references/shapes-and-geometry.md
Normal file
300
skills/creative/p5js/references/shapes-and-geometry.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Shapes and Geometry
|
||||
|
||||
## 2D Primitives
|
||||
|
||||
```javascript
|
||||
point(x, y);
|
||||
line(x1, y1, x2, y2);
|
||||
rect(x, y, w, h); // default: corner mode
|
||||
rect(x, y, w, h, r); // rounded corners
|
||||
rect(x, y, w, h, tl, tr, br, bl); // per-corner radius
|
||||
square(x, y, size);
|
||||
ellipse(x, y, w, h);
|
||||
circle(x, y, d); // diameter, not radius
|
||||
triangle(x1, y1, x2, y2, x3, y3);
|
||||
quad(x1, y1, x2, y2, x3, y3, x4, y4);
|
||||
arc(x, y, w, h, start, stop, mode); // mode: OPEN, CHORD, PIE
|
||||
```
|
||||
|
||||
### Drawing Modes
|
||||
|
||||
```javascript
|
||||
rectMode(CENTER); // x,y is center (default: CORNER)
|
||||
rectMode(CORNERS); // x1,y1 to x2,y2
|
||||
ellipseMode(CORNER); // x,y is top-left corner
|
||||
ellipseMode(CENTER); // default — x,y is center
|
||||
```
|
||||
|
||||
## Stroke and Fill
|
||||
|
||||
```javascript
|
||||
fill(r, g, b, a); // or fill(gray), fill('#hex'), fill(h, s, b) in HSB mode
|
||||
noFill();
|
||||
stroke(r, g, b, a);
|
||||
noStroke();
|
||||
strokeWeight(2);
|
||||
strokeCap(ROUND); // ROUND, SQUARE, PROJECT
|
||||
strokeJoin(ROUND); // ROUND, MITER, BEVEL
|
||||
```
|
||||
|
||||
## Custom Shapes with Vertices
|
||||
|
||||
### Basic vertex shape
|
||||
|
||||
```javascript
|
||||
beginShape();
|
||||
vertex(100, 100);
|
||||
vertex(200, 50);
|
||||
vertex(300, 100);
|
||||
vertex(250, 200);
|
||||
vertex(150, 200);
|
||||
endShape(CLOSE); // CLOSE connects last vertex to first
|
||||
```
|
||||
|
||||
### Shape modes
|
||||
|
||||
```javascript
|
||||
beginShape(); // default: polygon connecting all vertices
|
||||
beginShape(POINTS); // individual points
|
||||
beginShape(LINES); // pairs of vertices as lines
|
||||
beginShape(TRIANGLES); // triplets as triangles
|
||||
beginShape(TRIANGLE_FAN);
|
||||
beginShape(TRIANGLE_STRIP);
|
||||
beginShape(QUADS); // groups of 4
|
||||
beginShape(QUAD_STRIP);
|
||||
```
|
||||
|
||||
### Contours (holes in shapes)
|
||||
|
||||
```javascript
|
||||
beginShape();
|
||||
// outer shape
|
||||
vertex(100, 100);
|
||||
vertex(300, 100);
|
||||
vertex(300, 300);
|
||||
vertex(100, 300);
|
||||
// inner hole
|
||||
beginContour();
|
||||
vertex(150, 150);
|
||||
vertex(150, 250);
|
||||
vertex(250, 250);
|
||||
vertex(250, 150);
|
||||
endContour();
|
||||
endShape(CLOSE);
|
||||
```
|
||||
|
||||
## Bezier Curves
|
||||
|
||||
### Cubic Bezier
|
||||
|
||||
```javascript
|
||||
bezier(x1, y1, cx1, cy1, cx2, cy2, x2, y2);
|
||||
// x1,y1 = start point
|
||||
// cx1,cy1 = first control point
|
||||
// cx2,cy2 = second control point
|
||||
// x2,y2 = end point
|
||||
```
|
||||
|
||||
### Bezier in custom shapes
|
||||
|
||||
```javascript
|
||||
beginShape();
|
||||
vertex(100, 200);
|
||||
bezierVertex(150, 50, 250, 50, 300, 200);
|
||||
// control1, control2, endpoint
|
||||
endShape();
|
||||
```
|
||||
|
||||
### Quadratic Bezier
|
||||
|
||||
```javascript
|
||||
beginShape();
|
||||
vertex(100, 200);
|
||||
quadraticVertex(200, 50, 300, 200);
|
||||
// single control point + endpoint
|
||||
endShape();
|
||||
```
|
||||
|
||||
### Interpolation along Bezier
|
||||
|
||||
```javascript
|
||||
let x = bezierPoint(x1, cx1, cx2, x2, t); // t = 0..1
|
||||
let y = bezierPoint(y1, cy1, cy2, y2, t);
|
||||
let tx = bezierTangent(x1, cx1, cx2, x2, t); // tangent
|
||||
```
|
||||
|
||||
## Catmull-Rom Splines
|
||||
|
||||
```javascript
|
||||
curve(cpx1, cpy1, x1, y1, x2, y2, cpx2, cpy2);
|
||||
// cpx1,cpy1 = control point before start
|
||||
// x1,y1 = start point (visible)
|
||||
// x2,y2 = end point (visible)
|
||||
// cpx2,cpy2 = control point after end
|
||||
|
||||
curveVertex(x, y); // in beginShape() — smooth curve through all points
|
||||
curveTightness(0); // 0 = Catmull-Rom, 1 = straight lines, -1 = loose
|
||||
```
|
||||
|
||||
### Smooth curve through points
|
||||
|
||||
```javascript
|
||||
let points = [/* array of {x, y} */];
|
||||
beginShape();
|
||||
curveVertex(points[0].x, points[0].y); // repeat first for tangent
|
||||
for (let p of points) {
|
||||
curveVertex(p.x, p.y);
|
||||
}
|
||||
curveVertex(points[points.length-1].x, points[points.length-1].y); // repeat last
|
||||
endShape();
|
||||
```
|
||||
|
||||
## p5.Vector
|
||||
|
||||
Essential for physics, particle systems, and geometric computation.
|
||||
|
||||
```javascript
|
||||
let v = createVector(x, y);
|
||||
|
||||
// Arithmetic (modifies in place)
|
||||
v.add(other); // vector addition
|
||||
v.sub(other); // subtraction
|
||||
v.mult(scalar); // scale
|
||||
v.div(scalar); // inverse scale
|
||||
v.normalize(); // unit vector (length 1)
|
||||
v.limit(max); // cap magnitude
|
||||
v.setMag(len); // set exact magnitude
|
||||
|
||||
// Queries (non-destructive)
|
||||
v.mag(); // magnitude (length)
|
||||
v.magSq(); // squared magnitude (faster, no sqrt)
|
||||
v.heading(); // angle in radians
|
||||
v.dist(other); // distance to other vector
|
||||
v.dot(other); // dot product
|
||||
v.cross(other); // cross product (3D)
|
||||
v.angleBetween(other); // angle between vectors
|
||||
|
||||
// Static methods (return new vector)
|
||||
p5.Vector.add(a, b); // a + b → new vector
|
||||
p5.Vector.sub(a, b); // a - b → new vector
|
||||
p5.Vector.fromAngle(a); // unit vector at angle
|
||||
p5.Vector.random2D(); // random unit vector
|
||||
p5.Vector.lerp(a, b, t); // interpolate
|
||||
|
||||
// Copy
|
||||
let copy = v.copy();
|
||||
```
|
||||
|
||||
## Signed Distance Fields (2D)
|
||||
|
||||
SDFs return the distance from a point to the nearest edge of a shape. Negative inside, positive outside. Useful for smooth shapes, glow effects, boolean operations.
|
||||
|
||||
```javascript
|
||||
// Circle SDF
|
||||
function sdCircle(px, py, cx, cy, r) {
|
||||
return dist(px, py, cx, cy) - r;
|
||||
}
|
||||
|
||||
// Box SDF
|
||||
function sdBox(px, py, cx, cy, hw, hh) {
|
||||
let dx = abs(px - cx) - hw;
|
||||
let dy = abs(py - cy) - hh;
|
||||
return sqrt(max(dx, 0) ** 2 + max(dy, 0) ** 2) + min(max(dx, dy), 0);
|
||||
}
|
||||
|
||||
// Line segment SDF
|
||||
function sdSegment(px, py, ax, ay, bx, by) {
|
||||
let pa = createVector(px - ax, py - ay);
|
||||
let ba = createVector(bx - ax, by - ay);
|
||||
let t = constrain(pa.dot(ba) / ba.dot(ba), 0, 1);
|
||||
let closest = p5.Vector.add(createVector(ax, ay), p5.Vector.mult(ba, t));
|
||||
return dist(px, py, closest.x, closest.y);
|
||||
}
|
||||
|
||||
// Smooth boolean union
|
||||
function opSmoothUnion(d1, d2, k) {
|
||||
let h = constrain(0.5 + 0.5 * (d2 - d1) / k, 0, 1);
|
||||
return lerp(d2, d1, h) - k * h * (1 - h);
|
||||
}
|
||||
|
||||
// Rendering SDF as glow
|
||||
let d = sdCircle(x, y, width/2, height/2, 200);
|
||||
let glow = exp(-abs(d) * 0.02); // exponential falloff
|
||||
fill(glow * 255);
|
||||
```
|
||||
|
||||
## Useful Geometry Patterns
|
||||
|
||||
### Regular Polygon
|
||||
|
||||
```javascript
|
||||
function regularPolygon(cx, cy, r, sides) {
|
||||
beginShape();
|
||||
for (let i = 0; i < sides; i++) {
|
||||
let a = TWO_PI * i / sides - HALF_PI;
|
||||
vertex(cx + cos(a) * r, cy + sin(a) * r);
|
||||
}
|
||||
endShape(CLOSE);
|
||||
}
|
||||
```
|
||||
|
||||
### Star Shape
|
||||
|
||||
```javascript
|
||||
function star(cx, cy, r1, r2, npoints) {
|
||||
beginShape();
|
||||
let angle = TWO_PI / npoints;
|
||||
let halfAngle = angle / 2;
|
||||
for (let a = -HALF_PI; a < TWO_PI - HALF_PI; a += angle) {
|
||||
vertex(cx + cos(a) * r2, cy + sin(a) * r2);
|
||||
vertex(cx + cos(a + halfAngle) * r1, cy + sin(a + halfAngle) * r1);
|
||||
}
|
||||
endShape(CLOSE);
|
||||
}
|
||||
```
|
||||
|
||||
### Rounded Line (Capsule)
|
||||
|
||||
```javascript
|
||||
function capsule(x1, y1, x2, y2, weight) {
|
||||
strokeWeight(weight);
|
||||
strokeCap(ROUND);
|
||||
line(x1, y1, x2, y2);
|
||||
}
|
||||
```
|
||||
|
||||
### Soft Body / Blob
|
||||
|
||||
```javascript
|
||||
function blob(cx, cy, baseR, noiseScale, noiseOffset, detail = 64) {
|
||||
beginShape();
|
||||
for (let i = 0; i < detail; i++) {
|
||||
let a = TWO_PI * i / detail;
|
||||
let r = baseR + noise(cos(a) * noiseScale + noiseOffset,
|
||||
sin(a) * noiseScale + noiseOffset) * baseR * 0.4;
|
||||
vertex(cx + cos(a) * r, cy + sin(a) * r);
|
||||
}
|
||||
endShape(CLOSE);
|
||||
}
|
||||
```
|
||||
|
||||
## Clipping and Masking
|
||||
|
||||
```javascript
|
||||
// Clip shape — everything drawn after is masked by the clip shape
|
||||
beginClip();
|
||||
circle(width/2, height/2, 400);
|
||||
endClip();
|
||||
// Only content inside the circle is visible
|
||||
image(myImage, 0, 0);
|
||||
|
||||
// Or functional form
|
||||
clip(() => {
|
||||
circle(width/2, height/2, 400);
|
||||
});
|
||||
|
||||
// Erase mode — cut holes
|
||||
erase();
|
||||
circle(mouseX, mouseY, 100); // this area becomes transparent
|
||||
noErase();
|
||||
```
|
||||
532
skills/creative/p5js/references/troubleshooting.md
Normal file
532
skills/creative/p5js/references/troubleshooting.md
Normal file
@@ -0,0 +1,532 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Performance
|
||||
|
||||
### Step Zero — Disable FES
|
||||
|
||||
The Friendly Error System (FES) adds massive overhead — up to 10x slowdown. Disable it in every production sketch:
|
||||
|
||||
```javascript
|
||||
// BEFORE any p5 code
|
||||
p5.disableFriendlyErrors = true;
|
||||
|
||||
// Or use p5.min.js instead of p5.js — FES is stripped from minified build
|
||||
```
|
||||
|
||||
### Step One — pixelDensity(1)
|
||||
|
||||
Retina/HiDPI displays default to 2x or 3x density, multiplying pixel count by 4-9x:
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
pixelDensity(1); // force 1:1 — always do this first
|
||||
createCanvas(1920, 1080);
|
||||
}
|
||||
```
|
||||
|
||||
### Use Math.* in Hot Loops
|
||||
|
||||
p5's `sin()`, `cos()`, `random()`, `min()`, `max()`, `abs()` are wrapper functions with overhead. In hot loops (thousands of iterations per frame), use native `Math.*`:
|
||||
|
||||
```javascript
|
||||
// SLOW — p5 wrappers
|
||||
for (let p of particles) {
|
||||
let a = sin(p.angle);
|
||||
let d = dist(p.x, p.y, mx, my);
|
||||
}
|
||||
|
||||
// FAST — native Math
|
||||
for (let p of particles) {
|
||||
let a = Math.sin(p.angle);
|
||||
let dx = p.x - mx, dy = p.y - my;
|
||||
let dSq = dx * dx + dy * dy; // skip sqrt entirely
|
||||
}
|
||||
```
|
||||
|
||||
Use `magSq()` instead of `mag()` for distance comparisons — avoids expensive `sqrt()`.
|
||||
|
||||
### Diagnosis
|
||||
|
||||
Open Chrome DevTools > Performance tab > Record while sketch runs.
|
||||
|
||||
Common bottlenecks:
|
||||
1. **FES enabled** — 10x overhead on every p5 function call
|
||||
2. **pixelDensity > 1** — 4x pixel count, 4x slower
|
||||
3. **Too many draw calls** — thousands of `ellipse()`, `rect()` per frame
|
||||
4. **Large canvas + pixel operations** — `loadPixels()`/`updatePixels()` on 4K canvas
|
||||
5. **Unoptimized particle systems** — checking all-vs-all distances (O(n^2))
|
||||
6. **Memory leaks** — creating objects every frame without cleanup
|
||||
7. **Shader compilation** — calling `createShader()` in `draw()` instead of `setup()`
|
||||
8. **console.log() in draw()** — DOM write per frame, destroys performance
|
||||
9. **DOM manipulation in draw()** — layout thrashing (400-500x slower than canvas ops)
|
||||
|
||||
### Solutions
|
||||
|
||||
**Reduce draw calls:**
|
||||
```javascript
|
||||
// BAD: 10000 individual circles
|
||||
for (let p of particles) {
|
||||
ellipse(p.x, p.y, p.size);
|
||||
}
|
||||
|
||||
// GOOD: single shape with vertices
|
||||
beginShape(POINTS);
|
||||
for (let p of particles) {
|
||||
vertex(p.x, p.y);
|
||||
}
|
||||
endShape();
|
||||
|
||||
// BEST: direct pixel manipulation
|
||||
loadPixels();
|
||||
for (let p of particles) {
|
||||
let idx = 4 * (floor(p.y) * width + floor(p.x));
|
||||
pixels[idx] = p.r;
|
||||
pixels[idx+1] = p.g;
|
||||
pixels[idx+2] = p.b;
|
||||
pixels[idx+3] = 255;
|
||||
}
|
||||
updatePixels();
|
||||
```
|
||||
|
||||
**Spatial hashing for neighbor queries:**
|
||||
```javascript
|
||||
class SpatialHash {
|
||||
constructor(cellSize) {
|
||||
this.cellSize = cellSize;
|
||||
this.cells = new Map();
|
||||
}
|
||||
|
||||
clear() { this.cells.clear(); }
|
||||
|
||||
_key(x, y) {
|
||||
return `${floor(x / this.cellSize)},${floor(y / this.cellSize)}`;
|
||||
}
|
||||
|
||||
insert(obj) {
|
||||
let key = this._key(obj.pos.x, obj.pos.y);
|
||||
if (!this.cells.has(key)) this.cells.set(key, []);
|
||||
this.cells.get(key).push(obj);
|
||||
}
|
||||
|
||||
query(x, y, radius) {
|
||||
let results = [];
|
||||
let minCX = floor((x - radius) / this.cellSize);
|
||||
let maxCX = floor((x + radius) / this.cellSize);
|
||||
let minCY = floor((y - radius) / this.cellSize);
|
||||
let maxCY = floor((y + radius) / this.cellSize);
|
||||
|
||||
for (let cx = minCX; cx <= maxCX; cx++) {
|
||||
for (let cy = minCY; cy <= maxCY; cy++) {
|
||||
let key = `${cx},${cy}`;
|
||||
let cell = this.cells.get(key);
|
||||
if (cell) {
|
||||
for (let obj of cell) {
|
||||
if (dist(x, y, obj.pos.x, obj.pos.y) <= radius) {
|
||||
results.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Object pooling:**
|
||||
```javascript
|
||||
class ParticlePool {
|
||||
constructor(maxSize) {
|
||||
this.pool = [];
|
||||
this.active = [];
|
||||
for (let i = 0; i < maxSize; i++) {
|
||||
this.pool.push(new Particle(0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
spawn(x, y) {
|
||||
let p = this.pool.pop();
|
||||
if (p) {
|
||||
p.reset(x, y);
|
||||
this.active.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
for (let i = this.active.length - 1; i >= 0; i--) {
|
||||
this.active[i].update();
|
||||
if (this.active[i].isDead()) {
|
||||
this.pool.push(this.active.splice(i, 1)[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Throttle heavy operations:**
|
||||
```javascript
|
||||
// Only update flow field every N frames
|
||||
if (frameCount % 5 === 0) {
|
||||
flowField.update(frameCount * 0.001);
|
||||
}
|
||||
```
|
||||
|
||||
### Frame Rate Targets
|
||||
|
||||
| Context | Target | Acceptable |
|
||||
|---------|--------|------------|
|
||||
| Interactive sketch | 60fps | 30fps |
|
||||
| Ambient animation | 30fps | 20fps |
|
||||
| Export/recording | 30fps render | Any (offline) |
|
||||
| Mobile | 30fps | 20fps |
|
||||
|
||||
### Per-Pixel Rendering Budgets
|
||||
|
||||
Pixel-level operations (`loadPixels()` loops) are the most expensive common pattern. Budget depends on canvas size and computation per pixel.
|
||||
|
||||
| Canvas | Pixels | Simple noise (1 call) | fBM (4 octave) | Domain warp (3-layer fBM) |
|
||||
|--------|--------|----------------------|----------------|--------------------------|
|
||||
| 540x540 | 291K | ~5ms | ~20ms | ~80ms |
|
||||
| 1080x1080 | 1.17M | ~20ms | ~80ms | ~300ms+ |
|
||||
| 1920x1080 | 2.07M | ~35ms | ~140ms | ~500ms+ |
|
||||
| 3840x2160 | 8.3M | ~140ms | ~560ms | WILL CRASH |
|
||||
|
||||
**Rules of thumb:**
|
||||
- 1 `noise()` call per pixel at 1080x1080 = ~20ms/frame (OK at 30fps)
|
||||
- 4-octave fBM per pixel at 1080x1080 = ~80ms/frame (borderline)
|
||||
- Multi-layer domain warp at 1080x1080 = 300ms+ (too slow for real-time, fine for `noLoop()` export)
|
||||
- **Headless Chrome is 2-5x slower** than desktop Chrome for pixel ops
|
||||
|
||||
**Solution: render at lower resolution, fill blocks:**
|
||||
```javascript
|
||||
let step = 3; // render 1/9 of pixels, fill 3x3 blocks
|
||||
loadPixels();
|
||||
for (let y = 0; y < H; y += step) {
|
||||
for (let x = 0; x < W; x += step) {
|
||||
let v = expensiveNoise(x, y);
|
||||
for (let dy = 0; dy < step && y+dy < H; dy++)
|
||||
for (let dx = 0; dx < step && x+dx < W; dx++) {
|
||||
let i = 4 * ((y+dy) * W + (x+dx));
|
||||
pixels[i] = v; pixels[i+1] = v; pixels[i+2] = v; pixels[i+3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePixels();
|
||||
```
|
||||
|
||||
Step=2 gives 4x speedup. Step=3 gives 9x. Visible at 1080p but acceptable for video (motion hides it).
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### 1. Forgetting to reset blend mode
|
||||
|
||||
```javascript
|
||||
blendMode(ADD);
|
||||
image(glowLayer, 0, 0);
|
||||
// WRONG: everything after this is ADD blended
|
||||
blendMode(BLEND); // ALWAYS reset
|
||||
```
|
||||
|
||||
### 2. Creating objects in draw()
|
||||
|
||||
```javascript
|
||||
// BAD: creates new font object every frame
|
||||
function draw() {
|
||||
let f = loadFont('font.otf'); // NEVER load in draw()
|
||||
}
|
||||
|
||||
// GOOD: load in preload, use in draw
|
||||
let f;
|
||||
function preload() { f = loadFont('font.otf'); }
|
||||
```
|
||||
|
||||
### 3. Not using push()/pop() with transforms
|
||||
|
||||
```javascript
|
||||
// BAD: transforms accumulate
|
||||
translate(100, 0);
|
||||
rotate(0.1);
|
||||
ellipse(0, 0, 50);
|
||||
// Everything after this is also translated and rotated
|
||||
|
||||
// GOOD: isolated transforms
|
||||
push();
|
||||
translate(100, 0);
|
||||
rotate(0.1);
|
||||
ellipse(0, 0, 50);
|
||||
pop();
|
||||
```
|
||||
|
||||
### 4. Integer coordinates for crisp lines
|
||||
|
||||
```javascript
|
||||
// BLURRY: sub-pixel rendering
|
||||
line(10.5, 20.3, 100.7, 80.2);
|
||||
|
||||
// CRISP: integer + 0.5 for 1px lines
|
||||
line(10.5, 20.5, 100.5, 80.5); // on pixel boundary
|
||||
```
|
||||
|
||||
### 5. Pixel density confusion
|
||||
|
||||
```javascript
|
||||
// WRONG: assuming pixel array matches canvas dimensions
|
||||
loadPixels();
|
||||
let idx = 4 * (y * width + x); // wrong if pixelDensity > 1
|
||||
|
||||
// RIGHT: account for pixel density
|
||||
let d = pixelDensity();
|
||||
loadPixels();
|
||||
let idx = 4 * ((y * d) * (width * d) + (x * d));
|
||||
|
||||
// SIMPLEST: set pixelDensity(1) at the start
|
||||
```
|
||||
|
||||
### 6. Color mode confusion
|
||||
|
||||
```javascript
|
||||
// In HSB mode, fill(255) is NOT white
|
||||
colorMode(HSB, 360, 100, 100);
|
||||
fill(255); // This is hue=255, sat=100, bri=100 = vivid purple
|
||||
|
||||
// White in HSB:
|
||||
fill(0, 0, 100); // any hue, 0 saturation, 100 brightness
|
||||
|
||||
// Black in HSB:
|
||||
fill(0, 0, 0);
|
||||
```
|
||||
|
||||
### 7. WebGL origin is center
|
||||
|
||||
```javascript
|
||||
// In WEBGL mode, (0,0) is CENTER, not top-left
|
||||
function draw() {
|
||||
// This draws at the center, not the corner
|
||||
rect(0, 0, 100, 100);
|
||||
|
||||
// For top-left behavior:
|
||||
translate(-width/2, -height/2);
|
||||
rect(0, 0, 100, 100); // now at top-left
|
||||
}
|
||||
```
|
||||
|
||||
### 8. createGraphics cleanup
|
||||
|
||||
```javascript
|
||||
// BAD: memory leak — buffer never freed
|
||||
function draw() {
|
||||
let temp = createGraphics(width, height); // new buffer every frame!
|
||||
// ...
|
||||
}
|
||||
|
||||
// GOOD: create once, reuse
|
||||
let temp;
|
||||
function setup() {
|
||||
temp = createGraphics(width, height);
|
||||
}
|
||||
function draw() {
|
||||
temp.clear();
|
||||
// ... reuse temp
|
||||
}
|
||||
|
||||
// If you must create/destroy:
|
||||
temp.remove(); // explicitly free
|
||||
```
|
||||
|
||||
### 9. noise() returns 0-1, not -1 to 1
|
||||
|
||||
```javascript
|
||||
let n = noise(x); // 0.0 to 1.0 (biased toward 0.5)
|
||||
|
||||
// For -1 to 1 range:
|
||||
let n = noise(x) * 2 - 1;
|
||||
|
||||
// For a specific range:
|
||||
let n = map(noise(x), 0, 1, -100, 100);
|
||||
```
|
||||
|
||||
### 10. saveCanvas() in draw() saves every frame
|
||||
|
||||
```javascript
|
||||
// BAD: saves a PNG every single frame
|
||||
function draw() {
|
||||
// ... render ...
|
||||
saveCanvas('output', 'png'); // DON'T DO THIS
|
||||
}
|
||||
|
||||
// GOOD: save once via keyboard
|
||||
function keyPressed() {
|
||||
if (key === 's') saveCanvas('output', 'png');
|
||||
}
|
||||
|
||||
// GOOD: save once after rendering static piece
|
||||
function draw() {
|
||||
// ... render ...
|
||||
saveCanvas('output', 'png');
|
||||
noLoop(); // stop after saving
|
||||
}
|
||||
```
|
||||
|
||||
### 11. console.log() in draw()
|
||||
|
||||
```javascript
|
||||
// BAD: writes to DOM console every frame — massive overhead
|
||||
function draw() {
|
||||
console.log(particles.length); // 60 DOM writes/second
|
||||
}
|
||||
|
||||
// GOOD: log periodically or conditionally
|
||||
function draw() {
|
||||
if (frameCount % 60 === 0) console.log('FPS:', frameRate().toFixed(1));
|
||||
}
|
||||
```
|
||||
|
||||
### 12. DOM manipulation in draw()
|
||||
|
||||
```javascript
|
||||
// BAD: layout thrashing — 400-500x slower than canvas ops
|
||||
function draw() {
|
||||
document.getElementById('counter').innerText = frameCount;
|
||||
let el = document.querySelector('.info'); // DOM query per frame
|
||||
}
|
||||
|
||||
// GOOD: cache DOM refs, update infrequently
|
||||
let counterEl;
|
||||
function setup() { counterEl = document.getElementById('counter'); }
|
||||
function draw() {
|
||||
if (frameCount % 30 === 0) counterEl.innerText = frameCount;
|
||||
}
|
||||
```
|
||||
|
||||
### 13. Not disabling FES in production
|
||||
|
||||
```javascript
|
||||
// BAD: every p5 function call has error-checking overhead (up to 10x slower)
|
||||
function setup() { createCanvas(800, 800); }
|
||||
|
||||
// GOOD: disable before any p5 code
|
||||
p5.disableFriendlyErrors = true;
|
||||
function setup() { createCanvas(800, 800); }
|
||||
|
||||
// ALSO GOOD: use p5.min.js (FES stripped from minified build)
|
||||
```
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
### Safari Issues
|
||||
- WebGL shader precision: always declare `precision mediump float;`
|
||||
- `AudioContext` requires user gesture (`userStartAudio()`)
|
||||
- Some `blendMode()` options behave differently
|
||||
|
||||
### Firefox Issues
|
||||
- `textToPoints()` may return slightly different point counts
|
||||
- WebGL extensions may differ from Chrome
|
||||
- Color profile handling can shift colors
|
||||
|
||||
### Mobile Issues
|
||||
- Touch events need `return false` to prevent scroll
|
||||
- `devicePixelRatio` can be 2x or 3x — use `pixelDensity(1)` for performance
|
||||
- Smaller canvas recommended (720p or less)
|
||||
- Audio requires explicit user gesture to start
|
||||
|
||||
## CORS Issues
|
||||
|
||||
```javascript
|
||||
// Loading images/fonts from external URLs requires CORS headers
|
||||
// Local files need a server:
|
||||
// python3 -m http.server 8080
|
||||
|
||||
// Or use a CORS proxy for external resources (not recommended for production)
|
||||
```
|
||||
|
||||
## Memory Leaks
|
||||
|
||||
### Symptoms
|
||||
- Framerate degrading over time
|
||||
- Browser tab memory growing unbounded
|
||||
- Page becomes unresponsive after minutes
|
||||
|
||||
### Common Causes
|
||||
|
||||
```javascript
|
||||
// 1. Growing arrays
|
||||
let history = [];
|
||||
function draw() {
|
||||
history.push(someData); // grows forever
|
||||
}
|
||||
// FIX: cap the array
|
||||
if (history.length > 1000) history.shift();
|
||||
|
||||
// 2. Creating p5 objects in draw()
|
||||
function draw() {
|
||||
let v = createVector(0, 0); // allocation every frame
|
||||
}
|
||||
// FIX: reuse pre-allocated objects
|
||||
|
||||
// 3. Unreleased graphics buffers
|
||||
let layers = [];
|
||||
function reset() {
|
||||
for (let l of layers) l.remove(); // free old buffers
|
||||
layers = [];
|
||||
}
|
||||
|
||||
// 4. Event listener accumulation
|
||||
function setup() {
|
||||
// BAD: adds new listener every time setup runs
|
||||
window.addEventListener('resize', handler);
|
||||
}
|
||||
// FIX: use p5's built-in windowResized()
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Console Logging
|
||||
|
||||
```javascript
|
||||
// Log once (not every frame)
|
||||
if (frameCount === 1) {
|
||||
console.log('Canvas:', width, 'x', height);
|
||||
console.log('Pixel density:', pixelDensity());
|
||||
console.log('Renderer:', drawingContext.constructor.name);
|
||||
}
|
||||
|
||||
// Log periodically
|
||||
if (frameCount % 60 === 0) {
|
||||
console.log('FPS:', frameRate().toFixed(1));
|
||||
console.log('Particles:', particles.length);
|
||||
}
|
||||
```
|
||||
|
||||
### Visual Debugging
|
||||
|
||||
```javascript
|
||||
// Show frame rate
|
||||
function draw() {
|
||||
// ... your sketch ...
|
||||
if (CONFIG.debug) {
|
||||
fill(255, 0, 0);
|
||||
noStroke();
|
||||
textSize(14);
|
||||
textAlign(LEFT, TOP);
|
||||
text('FPS: ' + frameRate().toFixed(1), 10, 10);
|
||||
text('Particles: ' + particles.length, 10, 28);
|
||||
text('Frame: ' + frameCount, 10, 46);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle debug with 'd' key
|
||||
function keyPressed() {
|
||||
if (key === 'd') CONFIG.debug = !CONFIG.debug;
|
||||
}
|
||||
```
|
||||
|
||||
### Isolating Issues
|
||||
|
||||
```javascript
|
||||
// Comment out layers to find the slow one
|
||||
function draw() {
|
||||
renderBackground(); // comment out to test
|
||||
// renderParticles(); // this might be slow
|
||||
// renderPostEffects(); // or this
|
||||
}
|
||||
```
|
||||
302
skills/creative/p5js/references/typography.md
Normal file
302
skills/creative/p5js/references/typography.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Typography
|
||||
|
||||
## Loading Fonts
|
||||
|
||||
### System Fonts
|
||||
|
||||
```javascript
|
||||
textFont('Helvetica');
|
||||
textFont('Georgia');
|
||||
textFont('monospace');
|
||||
```
|
||||
|
||||
### Custom Fonts (OTF/TTF/WOFF2)
|
||||
|
||||
```javascript
|
||||
let myFont;
|
||||
|
||||
function preload() {
|
||||
myFont = loadFont('path/to/font.otf');
|
||||
// Requires local server or CORS-enabled URL
|
||||
}
|
||||
|
||||
function setup() {
|
||||
textFont(myFont);
|
||||
}
|
||||
```
|
||||
|
||||
### Google Fonts via CSS
|
||||
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
function setup() {
|
||||
textFont('Inter');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Google Fonts work without `loadFont()` but only for `text()` — not for `textToPoints()`. For particle text, you need `loadFont()` with an OTF/TTF file.
|
||||
|
||||
## Text Rendering
|
||||
|
||||
### Basic Text
|
||||
|
||||
```javascript
|
||||
textSize(32);
|
||||
textAlign(CENTER, CENTER);
|
||||
text('Hello World', width/2, height/2);
|
||||
```
|
||||
|
||||
### Text Properties
|
||||
|
||||
```javascript
|
||||
textSize(48); // pixel size
|
||||
textAlign(LEFT, TOP); // horizontal: LEFT, CENTER, RIGHT
|
||||
// vertical: TOP, CENTER, BOTTOM, BASELINE
|
||||
textLeading(40); // line spacing (for multi-line text)
|
||||
textStyle(BOLD); // NORMAL, BOLD, ITALIC, BOLDITALIC
|
||||
textWrap(WORD); // WORD or CHAR (for text() with max width)
|
||||
```
|
||||
|
||||
### Text Metrics
|
||||
|
||||
```javascript
|
||||
let w = textWidth('Hello'); // pixel width of string
|
||||
let a = textAscent(); // height above baseline
|
||||
let d = textDescent(); // height below baseline
|
||||
let totalH = a + d; // full line height
|
||||
```
|
||||
|
||||
### Text Bounding Box
|
||||
|
||||
```javascript
|
||||
let bounds = myFont.textBounds('Hello', x, y, size);
|
||||
// bounds = { x, y, w, h }
|
||||
// Useful for positioning, collision, background rectangles
|
||||
```
|
||||
|
||||
### Multi-Line Text
|
||||
|
||||
```javascript
|
||||
// With max width — auto wraps
|
||||
textWrap(WORD);
|
||||
text('Long text that wraps within the given width', x, y, maxWidth);
|
||||
|
||||
// With max width AND height — clips
|
||||
text('Very long text', x, y, maxWidth, maxHeight);
|
||||
```
|
||||
|
||||
## textToPoints() — Text as Particles
|
||||
|
||||
Convert text outline to array of points. Requires a loaded font (OTF/TTF via `loadFont()`).
|
||||
|
||||
```javascript
|
||||
let font;
|
||||
let points;
|
||||
|
||||
function preload() {
|
||||
font = loadFont('font.otf'); // MUST be loadFont, not CSS
|
||||
}
|
||||
|
||||
function setup() {
|
||||
createCanvas(1200, 600);
|
||||
points = font.textToPoints('HELLO', 100, 400, 200, {
|
||||
sampleFactor: 0.1, // lower = more points (0.1-0.5 typical)
|
||||
simplifyThreshold: 0
|
||||
});
|
||||
}
|
||||
|
||||
function draw() {
|
||||
background(0);
|
||||
for (let pt of points) {
|
||||
let n = noise(pt.x * 0.01, pt.y * 0.01, frameCount * 0.01);
|
||||
fill(255, n * 255);
|
||||
noStroke();
|
||||
ellipse(pt.x + random(-2, 2), pt.y + random(-2, 2), 3);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Particle Text Class
|
||||
|
||||
```javascript
|
||||
class TextParticle {
|
||||
constructor(target) {
|
||||
this.target = createVector(target.x, target.y);
|
||||
this.pos = createVector(random(width), random(height));
|
||||
this.vel = createVector(0, 0);
|
||||
this.acc = createVector(0, 0);
|
||||
this.maxSpeed = 10;
|
||||
this.maxForce = 0.5;
|
||||
}
|
||||
|
||||
arrive() {
|
||||
let desired = p5.Vector.sub(this.target, this.pos);
|
||||
let d = desired.mag();
|
||||
let speed = d < 100 ? map(d, 0, 100, 0, this.maxSpeed) : this.maxSpeed;
|
||||
desired.setMag(speed);
|
||||
let steer = p5.Vector.sub(desired, this.vel);
|
||||
steer.limit(this.maxForce);
|
||||
this.acc.add(steer);
|
||||
}
|
||||
|
||||
flee(target, radius) {
|
||||
let d = this.pos.dist(target);
|
||||
if (d < radius) {
|
||||
let desired = p5.Vector.sub(this.pos, target);
|
||||
desired.setMag(this.maxSpeed);
|
||||
let steer = p5.Vector.sub(desired, this.vel);
|
||||
steer.limit(this.maxForce * 2);
|
||||
this.acc.add(steer);
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.vel.add(this.acc);
|
||||
this.vel.limit(this.maxSpeed);
|
||||
this.pos.add(this.vel);
|
||||
this.acc.mult(0);
|
||||
}
|
||||
|
||||
display() {
|
||||
fill(255);
|
||||
noStroke();
|
||||
ellipse(this.pos.x, this.pos.y, 3);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: particles form text, scatter from mouse
|
||||
let textParticles = [];
|
||||
for (let pt of points) {
|
||||
textParticles.push(new TextParticle(pt));
|
||||
}
|
||||
|
||||
function draw() {
|
||||
background(0);
|
||||
for (let p of textParticles) {
|
||||
p.arrive();
|
||||
p.flee(createVector(mouseX, mouseY), 80);
|
||||
p.update();
|
||||
p.display();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kinetic Typography
|
||||
|
||||
### Wave Text
|
||||
|
||||
```javascript
|
||||
function waveText(str, x, y, size, amplitude, frequency) {
|
||||
textSize(size);
|
||||
textAlign(LEFT, BASELINE);
|
||||
let xOff = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let yOff = sin(frameCount * 0.05 + i * frequency) * amplitude;
|
||||
text(str[i], x + xOff, y + yOff);
|
||||
xOff += textWidth(str[i]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Typewriter Effect
|
||||
|
||||
```javascript
|
||||
class Typewriter {
|
||||
constructor(str, x, y, speed = 50) {
|
||||
this.str = str;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.speed = speed; // ms per character
|
||||
this.startTime = millis();
|
||||
this.cursor = true;
|
||||
}
|
||||
|
||||
display() {
|
||||
let elapsed = millis() - this.startTime;
|
||||
let chars = min(floor(elapsed / this.speed), this.str.length);
|
||||
let visible = this.str.substring(0, chars);
|
||||
|
||||
textAlign(LEFT, TOP);
|
||||
text(visible, this.x, this.y);
|
||||
|
||||
// Blinking cursor
|
||||
if (chars < this.str.length && floor(millis() / 500) % 2 === 0) {
|
||||
let cursorX = this.x + textWidth(visible);
|
||||
line(cursorX, this.y, cursorX, this.y + textAscent() + textDescent());
|
||||
}
|
||||
}
|
||||
|
||||
isDone() { return millis() - this.startTime >= this.str.length * this.speed; }
|
||||
}
|
||||
```
|
||||
|
||||
### Character-by-Character Animation
|
||||
|
||||
```javascript
|
||||
function animatedText(str, x, y, size, delay = 50) {
|
||||
textSize(size);
|
||||
textAlign(LEFT, BASELINE);
|
||||
let xOff = 0;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let charStart = i * delay;
|
||||
let t = constrain((millis() - charStart) / 500, 0, 1);
|
||||
let et = easeOutElastic(t);
|
||||
|
||||
push();
|
||||
translate(x + xOff, y);
|
||||
scale(et);
|
||||
let alpha = t * 255;
|
||||
fill(255, alpha);
|
||||
text(str[i], 0, 0);
|
||||
pop();
|
||||
|
||||
xOff += textWidth(str[i]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Text as Mask
|
||||
|
||||
```javascript
|
||||
let textBuffer;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800);
|
||||
textBuffer = createGraphics(width, height);
|
||||
textBuffer.background(0);
|
||||
textBuffer.fill(255);
|
||||
textBuffer.textSize(200);
|
||||
textBuffer.textAlign(CENTER, CENTER);
|
||||
textBuffer.text('MASK', width/2, height/2);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Draw content
|
||||
background(0);
|
||||
// ... render something colorful
|
||||
|
||||
// Apply text mask (show content only where text is white)
|
||||
loadPixels();
|
||||
textBuffer.loadPixels();
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
let maskVal = textBuffer.pixels[i]; // white = show, black = hide
|
||||
pixels[i + 3] = maskVal; // set alpha from mask
|
||||
}
|
||||
updatePixels();
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Text Sizing
|
||||
|
||||
```javascript
|
||||
function responsiveTextSize(baseSize, baseWidth = 1920) {
|
||||
return baseSize * (width / baseWidth);
|
||||
}
|
||||
|
||||
// Usage
|
||||
textSize(responsiveTextSize(48));
|
||||
text('Scales with canvas', width/2, height/2);
|
||||
```
|
||||
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
|
||||
}
|
||||
```
|
||||
423
skills/creative/p5js/references/webgl-and-3d.md
Normal file
423
skills/creative/p5js/references/webgl-and-3d.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# WebGL and 3D
|
||||
|
||||
## WebGL Mode Setup
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(1920, 1080, WEBGL);
|
||||
// Origin is CENTER, not top-left
|
||||
// Y-axis points UP (opposite of 2D mode)
|
||||
// Z-axis points toward viewer
|
||||
}
|
||||
```
|
||||
|
||||
### Coordinate Conversion (WEBGL to P2D-like)
|
||||
|
||||
```javascript
|
||||
function draw() {
|
||||
translate(-width/2, -height/2); // shift origin to top-left
|
||||
// Now coordinates work like P2D
|
||||
}
|
||||
```
|
||||
|
||||
## 3D Primitives
|
||||
|
||||
```javascript
|
||||
box(w, h, d); // rectangular prism
|
||||
sphere(radius, detailX, detailY);
|
||||
cylinder(radius, height, detailX, detailY);
|
||||
cone(radius, height, detailX, detailY);
|
||||
torus(radius, tubeRadius, detailX, detailY);
|
||||
plane(width, height); // flat rectangle
|
||||
ellipsoid(rx, ry, rz); // stretched sphere
|
||||
```
|
||||
|
||||
### 3D Transforms
|
||||
|
||||
```javascript
|
||||
push();
|
||||
translate(x, y, z);
|
||||
rotateX(angleX);
|
||||
rotateY(angleY);
|
||||
rotateZ(angleZ);
|
||||
scale(s);
|
||||
box(100);
|
||||
pop();
|
||||
```
|
||||
|
||||
## Camera
|
||||
|
||||
### Default Camera
|
||||
|
||||
```javascript
|
||||
camera(
|
||||
eyeX, eyeY, eyeZ, // camera position
|
||||
centerX, centerY, centerZ, // look-at target
|
||||
upX, upY, upZ // up direction
|
||||
);
|
||||
|
||||
// Default: camera(0, 0, (height/2)/tan(PI/6), 0, 0, 0, 0, 1, 0)
|
||||
```
|
||||
|
||||
### Orbit Control
|
||||
|
||||
```javascript
|
||||
function draw() {
|
||||
orbitControl(); // mouse drag to rotate, scroll to zoom
|
||||
box(200);
|
||||
}
|
||||
```
|
||||
|
||||
### createCamera
|
||||
|
||||
```javascript
|
||||
let cam;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800, WEBGL);
|
||||
cam = createCamera();
|
||||
cam.setPosition(300, -200, 500);
|
||||
cam.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
// Camera methods
|
||||
cam.setPosition(x, y, z);
|
||||
cam.lookAt(x, y, z);
|
||||
cam.move(dx, dy, dz); // relative to camera orientation
|
||||
cam.pan(angle); // horizontal rotation
|
||||
cam.tilt(angle); // vertical rotation
|
||||
cam.roll(angle); // z-axis rotation
|
||||
cam.slerp(otherCam, t); // smooth interpolation between cameras
|
||||
```
|
||||
|
||||
### Perspective and Orthographic
|
||||
|
||||
```javascript
|
||||
// Perspective (default)
|
||||
perspective(fov, aspect, near, far);
|
||||
// fov: field of view in radians (PI/3 default)
|
||||
// aspect: width/height
|
||||
// near/far: clipping planes
|
||||
|
||||
// Orthographic (no depth foreshortening)
|
||||
ortho(-width/2, width/2, -height/2, height/2, 0, 2000);
|
||||
```
|
||||
|
||||
## Lighting
|
||||
|
||||
```javascript
|
||||
// Ambient (uniform, no direction)
|
||||
ambientLight(50, 50, 50); // dim fill light
|
||||
|
||||
// Directional (parallel rays, like sun)
|
||||
directionalLight(255, 255, 255, 0, -1, 0); // color + direction
|
||||
|
||||
// Point (radiates from position)
|
||||
pointLight(255, 200, 150, 200, -300, 400); // color + position
|
||||
|
||||
// Spot (cone from position toward target)
|
||||
spotLight(255, 255, 255, // color
|
||||
0, -300, 300, // position
|
||||
0, 1, -1, // direction
|
||||
PI / 4, 5); // angle, concentration
|
||||
|
||||
// Image-based lighting
|
||||
imageLight(myHDRI);
|
||||
|
||||
// No lights (flat shading)
|
||||
noLights();
|
||||
|
||||
// Quick default lighting
|
||||
lights();
|
||||
```
|
||||
|
||||
### Three-Point Lighting Setup
|
||||
|
||||
```javascript
|
||||
function setupLighting() {
|
||||
ambientLight(30, 30, 40); // dim blue fill
|
||||
|
||||
// Key light (main, warm)
|
||||
directionalLight(255, 240, 220, -1, -1, -1);
|
||||
|
||||
// Fill light (softer, cooler, opposite side)
|
||||
directionalLight(80, 100, 140, 1, -0.5, -1);
|
||||
|
||||
// Rim light (behind subject, for edge definition)
|
||||
pointLight(200, 200, 255, 0, -200, -400);
|
||||
}
|
||||
```
|
||||
|
||||
## Materials
|
||||
|
||||
```javascript
|
||||
// Normal material (debug — colors from surface normals)
|
||||
normalMaterial();
|
||||
|
||||
// Ambient (responds only to ambientLight)
|
||||
ambientMaterial(200, 100, 100);
|
||||
|
||||
// Emissive (self-lit, no shadows)
|
||||
emissiveMaterial(255, 0, 100);
|
||||
|
||||
// Specular (shiny reflections)
|
||||
specularMaterial(255);
|
||||
shininess(50); // 1-200 (higher = tighter highlight)
|
||||
metalness(100); // 0-200 (metallic reflection)
|
||||
|
||||
// Fill works too (no lighting response)
|
||||
fill(255, 0, 0);
|
||||
```
|
||||
|
||||
### Texture
|
||||
|
||||
```javascript
|
||||
let img;
|
||||
function preload() { img = loadImage('texture.jpg'); }
|
||||
|
||||
function draw() {
|
||||
texture(img);
|
||||
textureMode(NORMAL); // UV coords 0-1
|
||||
// textureMode(IMAGE); // UV coords in pixels
|
||||
textureWrap(REPEAT); // or CLAMP, MIRROR
|
||||
box(200);
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Geometry
|
||||
|
||||
### buildGeometry
|
||||
|
||||
```javascript
|
||||
let myShape;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800, WEBGL);
|
||||
myShape = buildGeometry(() => {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
push();
|
||||
translate(random(-200, 200), random(-200, 200), random(-200, 200));
|
||||
sphere(10);
|
||||
pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function draw() {
|
||||
model(myShape); // renders once-built geometry efficiently
|
||||
}
|
||||
```
|
||||
|
||||
### beginGeometry / endGeometry
|
||||
|
||||
```javascript
|
||||
beginGeometry();
|
||||
// draw shapes here
|
||||
box(50);
|
||||
translate(100, 0, 0);
|
||||
sphere(30);
|
||||
let geo = endGeometry();
|
||||
|
||||
model(geo); // reuse
|
||||
```
|
||||
|
||||
### Manual Geometry (p5.Geometry)
|
||||
|
||||
```javascript
|
||||
let geo = new p5.Geometry(detailX, detailY, function() {
|
||||
for (let i = 0; i <= detailX; i++) {
|
||||
for (let j = 0; j <= detailY; j++) {
|
||||
let u = i / detailX;
|
||||
let v = j / detailY;
|
||||
let x = cos(u * TWO_PI) * (100 + 30 * cos(v * TWO_PI));
|
||||
let y = sin(u * TWO_PI) * (100 + 30 * cos(v * TWO_PI));
|
||||
let z = 30 * sin(v * TWO_PI);
|
||||
this.vertices.push(createVector(x, y, z));
|
||||
this.uvs.push(u, v);
|
||||
}
|
||||
}
|
||||
this.computeFaces();
|
||||
this.computeNormals();
|
||||
});
|
||||
```
|
||||
|
||||
## GLSL Shaders
|
||||
|
||||
### createShader (Vertex + Fragment)
|
||||
|
||||
```javascript
|
||||
let myShader;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800, WEBGL);
|
||||
|
||||
let vert = `
|
||||
precision mediump float;
|
||||
attribute vec3 aPosition;
|
||||
attribute vec2 aTexCoord;
|
||||
varying vec2 vTexCoord;
|
||||
uniform mat4 uModelViewMatrix;
|
||||
uniform mat4 uProjectionMatrix;
|
||||
void main() {
|
||||
vTexCoord = aTexCoord;
|
||||
vec4 pos = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
|
||||
gl_Position = pos;
|
||||
}
|
||||
`;
|
||||
|
||||
let frag = `
|
||||
precision mediump float;
|
||||
varying vec2 vTexCoord;
|
||||
uniform float uTime;
|
||||
uniform vec2 uResolution;
|
||||
|
||||
void main() {
|
||||
vec2 uv = vTexCoord;
|
||||
vec3 col = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0, 2, 4));
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
myShader = createShader(vert, frag);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
shader(myShader);
|
||||
myShader.setUniform('uTime', millis() / 1000.0);
|
||||
myShader.setUniform('uResolution', [width, height]);
|
||||
rect(0, 0, width, height);
|
||||
resetShader();
|
||||
}
|
||||
```
|
||||
|
||||
### createFilterShader (Post-Processing)
|
||||
|
||||
Simpler — only needs a fragment shader. Automatically gets the canvas as a texture.
|
||||
|
||||
```javascript
|
||||
let blurShader;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800, WEBGL);
|
||||
|
||||
blurShader = createFilterShader(`
|
||||
precision mediump float;
|
||||
varying vec2 vTexCoord;
|
||||
uniform sampler2D tex0;
|
||||
uniform vec2 texelSize;
|
||||
|
||||
void main() {
|
||||
vec4 sum = vec4(0.0);
|
||||
for (int x = -2; x <= 2; x++) {
|
||||
for (int y = -2; y <= 2; y++) {
|
||||
sum += texture2D(tex0, vTexCoord + vec2(float(x), float(y)) * texelSize);
|
||||
}
|
||||
}
|
||||
gl_FragColor = sum / 25.0;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Draw scene normally
|
||||
background(0);
|
||||
fill(255, 0, 0);
|
||||
sphere(100);
|
||||
|
||||
// Apply post-processing filter
|
||||
filter(blurShader);
|
||||
}
|
||||
```
|
||||
|
||||
### Common Shader Uniforms
|
||||
|
||||
```javascript
|
||||
myShader.setUniform('uTime', millis() / 1000.0);
|
||||
myShader.setUniform('uResolution', [width, height]);
|
||||
myShader.setUniform('uMouse', [mouseX / width, mouseY / height]);
|
||||
myShader.setUniform('uTexture', myGraphics); // pass p5.Graphics as texture
|
||||
myShader.setUniform('uValue', 0.5); // float
|
||||
myShader.setUniform('uColor', [1.0, 0.0, 0.5, 1.0]); // vec4
|
||||
```
|
||||
|
||||
### Shader Recipes
|
||||
|
||||
**Chromatic Aberration:**
|
||||
```glsl
|
||||
vec4 r = texture2D(tex0, vTexCoord + vec2(0.005, 0.0));
|
||||
vec4 g = texture2D(tex0, vTexCoord);
|
||||
vec4 b = texture2D(tex0, vTexCoord - vec2(0.005, 0.0));
|
||||
gl_FragColor = vec4(r.r, g.g, b.b, 1.0);
|
||||
```
|
||||
|
||||
**Vignette:**
|
||||
```glsl
|
||||
float d = distance(vTexCoord, vec2(0.5));
|
||||
float v = smoothstep(0.7, 0.4, d);
|
||||
gl_FragColor = texture2D(tex0, vTexCoord) * v;
|
||||
```
|
||||
|
||||
**Scanlines:**
|
||||
```glsl
|
||||
float scanline = sin(vTexCoord.y * uResolution.y * 3.14159) * 0.04;
|
||||
vec4 col = texture2D(tex0, vTexCoord);
|
||||
gl_FragColor = col - scanline;
|
||||
```
|
||||
|
||||
## Framebuffers
|
||||
|
||||
```javascript
|
||||
let fbo;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800, WEBGL);
|
||||
fbo = createFramebuffer();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Render to framebuffer
|
||||
fbo.begin();
|
||||
clear();
|
||||
rotateY(frameCount * 0.01);
|
||||
box(200);
|
||||
fbo.end();
|
||||
|
||||
// Use framebuffer as texture
|
||||
texture(fbo.color);
|
||||
plane(width, height);
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Pass Rendering
|
||||
|
||||
```javascript
|
||||
let sceneBuffer, blurBuffer;
|
||||
|
||||
function setup() {
|
||||
createCanvas(800, 800, WEBGL);
|
||||
sceneBuffer = createFramebuffer();
|
||||
blurBuffer = createFramebuffer();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Pass 1: render scene
|
||||
sceneBuffer.begin();
|
||||
clear();
|
||||
lights();
|
||||
rotateY(frameCount * 0.01);
|
||||
box(200);
|
||||
sceneBuffer.end();
|
||||
|
||||
// Pass 2: blur
|
||||
blurBuffer.begin();
|
||||
shader(blurShader);
|
||||
blurShader.setUniform('uTexture', sceneBuffer.color);
|
||||
rect(0, 0, width, height);
|
||||
resetShader();
|
||||
blurBuffer.end();
|
||||
|
||||
// Final: composite
|
||||
texture(blurBuffer.color);
|
||||
plane(width, height);
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user