Sync all skills and memories 2026-04-14 07:27
This commit is contained in:
64
skills/creative/p5js/README.md
Normal file
64
skills/creative/p5js/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# p5.js Skill
|
||||
|
||||
Production pipeline for interactive and generative visual art using [p5.js](https://p5js.org/).
|
||||
|
||||
## What it does
|
||||
|
||||
Creates browser-based visual art from text prompts. The agent handles the full pipeline: creative concept, code generation, preview, export, and iterative refinement. Output is a single self-contained HTML file that runs in any browser — no build step, no server, no dependencies beyond a CDN script tag.
|
||||
|
||||
The output is real interactive art. Not tutorial exercises. Generative systems, particle physics, noise fields, shader effects, kinetic typography — composed with intentional color palettes, layered composition, and visual hierarchy.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Input | Output |
|
||||
|------|-------|--------|
|
||||
| **Generative art** | Seed / parameters | Procedural visual composition |
|
||||
| **Data visualization** | Dataset / API | Interactive charts, custom data displays |
|
||||
| **Interactive experience** | None (user drives) | Mouse/keyboard/touch-driven sketch |
|
||||
| **Animation / motion graphics** | Timeline / storyboard | Timed sequences, kinetic typography |
|
||||
| **3D scene** | Concept description | WebGL geometry, lighting, shaders |
|
||||
| **Image processing** | Image file(s) | Pixel manipulation, filters, pointillism |
|
||||
| **Audio-reactive** | Audio file / mic | Sound-driven generative visuals |
|
||||
|
||||
## Export Formats
|
||||
|
||||
| Format | Method |
|
||||
|--------|--------|
|
||||
| **HTML** | Self-contained file, opens in any browser |
|
||||
| **PNG** | `saveCanvas()` — press 's' to capture |
|
||||
| **GIF** | `saveGif()` — press 'g' to capture |
|
||||
| **MP4** | Frame sequence + ffmpeg via `scripts/render.sh` |
|
||||
| **SVG** | p5.js-svg renderer for vector output |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A modern browser. That's it for basic use.
|
||||
|
||||
For headless export: Node.js, Puppeteer, ffmpeg.
|
||||
|
||||
```bash
|
||||
bash skills/creative/p5js/scripts/setup.sh
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
├── SKILL.md # Modes, workflow, creative direction, critical notes
|
||||
├── README.md # This file
|
||||
├── references/
|
||||
│ ├── core-api.md # Canvas, draw loop, transforms, offscreen buffers, math
|
||||
│ ├── shapes-and-geometry.md # Primitives, vertices, curves, vectors, SDFs, clipping
|
||||
│ ├── visual-effects.md # Noise, flow fields, particles, pixels, textures, feedback
|
||||
│ ├── animation.md # Easing, springs, state machines, timelines, transitions
|
||||
│ ├── typography.md # Fonts, textToPoints, kinetic text, text masks
|
||||
│ ├── color-systems.md # HSB/RGB, palettes, gradients, blend modes, curated colors
|
||||
│ ├── webgl-and-3d.md # 3D primitives, camera, lighting, shaders, framebuffers
|
||||
│ ├── interaction.md # Mouse, keyboard, touch, DOM, audio, scroll
|
||||
│ ├── export-pipeline.md # PNG, GIF, MP4, SVG, headless, tiling, batch export
|
||||
│ └── troubleshooting.md # Performance, common mistakes, browser issues, debugging
|
||||
└── scripts/
|
||||
├── setup.sh # Dependency verification
|
||||
├── serve.sh # Local dev server (for loading local assets)
|
||||
├── render.sh # Headless render pipeline (HTML → frames → MP4)
|
||||
└── export-frames.js # Puppeteer frame capture (Node.js)
|
||||
```
|
||||
547
skills/creative/p5js/SKILL.md
Normal file
547
skills/creative/p5js/SKILL.md
Normal file
@@ -0,0 +1,547 @@
|
||||
---
|
||||
name: p5js
|
||||
description: "Production pipeline for interactive and generative visual art using p5.js. Creates browser-based sketches, generative art, data visualizations, interactive experiences, 3D scenes, audio-reactive visuals, and motion graphics — exported as HTML, PNG, GIF, MP4, or SVG. Covers: 2D/3D rendering, noise and particle systems, flow fields, shaders (GLSL), pixel manipulation, kinetic typography, WebGL scenes, audio analysis, mouse/keyboard interaction, and headless high-res export. Use when users request: p5.js sketches, creative coding, generative art, interactive visualizations, canvas animations, browser-based visual art, data viz, shader effects, or any p5.js project."
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [creative-coding, generative-art, p5js, canvas, interactive, visualization, webgl, shaders, animation]
|
||||
related_skills: [ascii-video, manim-video, excalidraw]
|
||||
---
|
||||
|
||||
# p5.js Production Pipeline
|
||||
|
||||
## Creative Standard
|
||||
|
||||
This is visual art rendered in the browser. The canvas is the medium; the algorithm is the brush.
|
||||
|
||||
**Before writing a single line of code**, articulate the creative concept. What does this piece communicate? What makes the viewer stop scrolling? What separates this from a code tutorial example? The user's prompt is a starting point — interpret it with creative ambition.
|
||||
|
||||
**First-render excellence is non-negotiable.** The output must be visually striking on first load. If it looks like a p5.js tutorial exercise, a default configuration, or "AI-generated creative coding," it is wrong. Rethink before shipping.
|
||||
|
||||
**Go beyond the reference vocabulary.** The noise functions, particle systems, color palettes, and shader effects in the references are a starting vocabulary. For every project, combine, layer, and invent. The catalog is a palette of paints — you write the painting.
|
||||
|
||||
**Be proactively creative.** If the user asks for "a particle system," deliver a particle system with emergent flocking behavior, trailing ghost echoes, palette-shifted depth fog, and a background noise field that breathes. Include at least one visual detail the user didn't ask for but will appreciate.
|
||||
|
||||
**Dense, layered, considered.** Every frame should reward viewing. Never flat white backgrounds. Always compositional hierarchy. Always intentional color. Always micro-detail that only appears on close inspection.
|
||||
|
||||
**Cohesive aesthetic over feature count.** All elements must serve a unified visual language — shared color temperature, consistent stroke weight vocabulary, harmonious motion speeds. A sketch with ten unrelated effects is worse than one with three that belong together.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Input | Output | Reference |
|
||||
|------|-------|--------|-----------|
|
||||
| **Generative art** | Seed / parameters | Procedural visual composition (still or animated) | `references/visual-effects.md` |
|
||||
| **Data visualization** | Dataset / API | Interactive charts, graphs, custom data displays | `references/interaction.md` |
|
||||
| **Interactive experience** | None (user drives) | Mouse/keyboard/touch-driven sketch | `references/interaction.md` |
|
||||
| **Animation / motion graphics** | Timeline / storyboard | Timed sequences, kinetic typography, transitions | `references/animation.md` |
|
||||
| **3D scene** | Concept description | WebGL geometry, lighting, camera, materials | `references/webgl-and-3d.md` |
|
||||
| **Image processing** | Image file(s) | Pixel manipulation, filters, mosaic, pointillism | `references/visual-effects.md` § Pixel Manipulation |
|
||||
| **Audio-reactive** | Audio file / mic | Sound-driven generative visuals | `references/interaction.md` § Audio Input |
|
||||
|
||||
## Stack
|
||||
|
||||
Single self-contained HTML file per project. No build step required.
|
||||
|
||||
| Layer | Tool | Purpose |
|
||||
|-------|------|---------|
|
||||
| Core | p5.js 1.11.3 (CDN) | Canvas rendering, math, transforms, event handling |
|
||||
| 3D | p5.js WebGL mode | 3D geometry, camera, lighting, GLSL shaders |
|
||||
| Audio | p5.sound.js (CDN) | FFT analysis, amplitude, mic input, oscillators |
|
||||
| Export | Built-in `saveCanvas()` / `saveGif()` / `saveFrames()` | PNG, GIF, frame sequence output |
|
||||
| Capture | CCapture.js (optional) | Deterministic framerate video capture (WebM, GIF) |
|
||||
| Headless | Puppeteer + Node.js (optional) | Automated high-res rendering, MP4 via ffmpeg |
|
||||
| SVG | p5.js-svg 1.6.0 (optional) | Vector output for print — requires p5.js 1.x |
|
||||
| Natural media | p5.brush (optional) | Watercolor, charcoal, pen — requires p5.js 2.x + WEBGL |
|
||||
| Texture | p5.grain (optional) | Film grain, texture overlays |
|
||||
| Fonts | Google Fonts / `loadFont()` | Custom typography via OTF/TTF/WOFF2 |
|
||||
|
||||
### Version Note
|
||||
|
||||
**p5.js 1.x** (1.11.3) is the default — stable, well-documented, broadest library compatibility. Use this unless a project requires 2.x features.
|
||||
|
||||
**p5.js 2.x** (2.2+) adds: `async setup()` replacing `preload()`, OKLCH/OKLAB color modes, `splineVertex()`, shader `.modify()` API, variable fonts, `textToContours()`, pointer events. Required for p5.brush. See `references/core-api.md` § p5.js 2.0.
|
||||
|
||||
## Pipeline
|
||||
|
||||
Every project follows the same 6-stage path:
|
||||
|
||||
```
|
||||
CONCEPT → DESIGN → CODE → PREVIEW → EXPORT → VERIFY
|
||||
```
|
||||
|
||||
1. **CONCEPT** — Articulate the creative vision: mood, color world, motion vocabulary, what makes this unique
|
||||
2. **DESIGN** — Choose mode, canvas size, interaction model, color system, export format. Map concept to technical decisions
|
||||
3. **CODE** — Write single HTML file with inline p5.js. Structure: globals → `preload()` → `setup()` → `draw()` → helpers → classes → event handlers
|
||||
4. **PREVIEW** — Open in browser, verify visual quality. Test at target resolution. Check performance
|
||||
5. **EXPORT** — Capture output: `saveCanvas()` for PNG, `saveGif()` for GIF, `saveFrames()` + ffmpeg for MP4, Puppeteer for headless batch
|
||||
6. **VERIFY** — Does the output match the concept? Is it visually striking at the intended display size? Would you frame it?
|
||||
|
||||
## Creative Direction
|
||||
|
||||
### Aesthetic Dimensions
|
||||
|
||||
| Dimension | Options | Reference |
|
||||
|-----------|---------|-----------|
|
||||
| **Color system** | HSB/HSL, RGB, named palettes, procedural harmony, gradient interpolation | `references/color-systems.md` |
|
||||
| **Noise vocabulary** | Perlin noise, simplex, fractal (octaved), domain warping, curl noise | `references/visual-effects.md` § Noise |
|
||||
| **Particle systems** | Physics-based, flocking, trail-drawing, attractor-driven, flow-field following | `references/visual-effects.md` § Particles |
|
||||
| **Shape language** | Geometric primitives, custom vertices, bezier curves, SVG paths | `references/shapes-and-geometry.md` |
|
||||
| **Motion style** | Eased, spring-based, noise-driven, physics sim, lerped, stepped | `references/animation.md` |
|
||||
| **Typography** | System fonts, loaded OTF, `textToPoints()` particle text, kinetic | `references/typography.md` |
|
||||
| **Shader effects** | GLSL fragment/vertex, filter shaders, post-processing, feedback loops | `references/webgl-and-3d.md` § Shaders |
|
||||
| **Composition** | Grid, radial, golden ratio, rule of thirds, organic scatter, tiled | `references/core-api.md` § Composition |
|
||||
| **Interaction model** | Mouse follow, click spawn, drag, keyboard state, scroll-driven, mic input | `references/interaction.md` |
|
||||
| **Blend modes** | `BLEND`, `ADD`, `MULTIPLY`, `SCREEN`, `DIFFERENCE`, `EXCLUSION`, `OVERLAY` | `references/color-systems.md` § Blend Modes |
|
||||
| **Layering** | `createGraphics()` offscreen buffers, alpha compositing, masking | `references/core-api.md` § Offscreen Buffers |
|
||||
| **Texture** | Perlin surface, stippling, hatching, halftone, pixel sorting | `references/visual-effects.md` § Texture Generation |
|
||||
|
||||
### Per-Project Variation Rules
|
||||
|
||||
Never use default configurations. For every project:
|
||||
- **Custom color palette** — never raw `fill(255, 0, 0)`. Always a designed palette with 3-7 colors
|
||||
- **Custom stroke weight vocabulary** — thin accents (0.5), medium structure (1-2), bold emphasis (3-5)
|
||||
- **Background treatment** — never plain `background(0)` or `background(255)`. Always textured, gradient, or layered
|
||||
- **Motion variety** — different speeds for different elements. Primary at 1x, secondary at 0.3x, ambient at 0.1x
|
||||
- **At least one invented element** — a custom particle behavior, a novel noise application, a unique interaction response
|
||||
|
||||
### Project-Specific Invention
|
||||
|
||||
For every project, invent at least one of:
|
||||
- A custom color palette matching the mood (not a preset)
|
||||
- A novel noise field combination (e.g., curl noise + domain warp + feedback)
|
||||
- A unique particle behavior (custom forces, custom trails, custom spawning)
|
||||
- An interaction mechanic the user didn't request but that elevates the piece
|
||||
- A compositional technique that creates visual hierarchy
|
||||
|
||||
### Parameter Design Philosophy
|
||||
|
||||
Parameters should emerge from the algorithm, not from a generic menu. Ask: "What properties of *this* system should be tunable?"
|
||||
|
||||
**Good parameters** expose the algorithm's character:
|
||||
- **Quantities** — how many particles, branches, cells (controls density)
|
||||
- **Scales** — noise frequency, element size, spacing (controls texture)
|
||||
- **Rates** — speed, growth rate, decay (controls energy)
|
||||
- **Thresholds** — when does behavior change? (controls drama)
|
||||
- **Ratios** — proportions, balance between forces (controls harmony)
|
||||
|
||||
**Bad parameters** are generic controls unrelated to the algorithm:
|
||||
- "color1", "color2", "size" — meaningless without context
|
||||
- Toggle switches for unrelated effects
|
||||
- Parameters that only change cosmetics, not behavior
|
||||
|
||||
Every parameter should change how the algorithm *thinks*, not just how it *looks*. A "turbulence" parameter that changes noise octaves is good. A "particle size" slider that only changes `ellipse()` radius is shallow.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Creative Vision
|
||||
|
||||
Before any code, articulate:
|
||||
|
||||
- **Mood / atmosphere**: What should the viewer feel? Contemplative? Energized? Unsettled? Playful?
|
||||
- **Visual story**: What happens over time (or on interaction)? Build? Decay? Transform? Oscillate?
|
||||
- **Color world**: Warm/cool? Monochrome? Complementary? What's the dominant hue? The accent?
|
||||
- **Shape language**: Organic curves? Sharp geometry? Dots? Lines? Mixed?
|
||||
- **Motion vocabulary**: Slow drift? Explosive burst? Breathing pulse? Mechanical precision?
|
||||
- **What makes THIS different**: What is the one thing that makes this sketch unique?
|
||||
|
||||
Map the user's prompt to aesthetic choices. "Relaxing generative background" demands different everything from "glitch data visualization."
|
||||
|
||||
### Step 2: Technical Design
|
||||
|
||||
- **Mode** — which of the 7 modes from the table above
|
||||
- **Canvas size** — landscape 1920x1080, portrait 1080x1920, square 1080x1080, or responsive `windowWidth/windowHeight`
|
||||
- **Renderer** — `P2D` (default) or `WEBGL` (for 3D, shaders, advanced blend modes)
|
||||
- **Frame rate** — 60fps (interactive), 30fps (ambient animation), or `noLoop()` (static generative)
|
||||
- **Export target** — browser display, PNG still, GIF loop, MP4 video, SVG vector
|
||||
- **Interaction model** — passive (no input), mouse-driven, keyboard-driven, audio-reactive, scroll-driven
|
||||
- **Viewer UI** — for interactive generative art, start from `templates/viewer.html` which provides seed navigation, parameter sliders, and download. For simple sketches or video export, use bare HTML
|
||||
|
||||
### Step 3: Code the Sketch
|
||||
|
||||
For **interactive generative art** (seed exploration, parameter tuning): start from `templates/viewer.html`. Read the template first, keep the fixed sections (seed nav, actions), replace the algorithm and parameter controls. This gives the user seed prev/next/random/jump, parameter sliders with live update, and PNG download — all wired up.
|
||||
|
||||
For **animations, video export, or simple sketches**: use bare HTML:
|
||||
|
||||
Single HTML file. Structure:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Project Name</title>
|
||||
<script>p5.disableFriendlyErrors = true;</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/p5.min.js"></script>
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/addons/p5.sound.min.js"></script> -->
|
||||
<!-- <script src="https://unpkg.com/p5.js-svg@1.6.0"></script> --> <!-- SVG export -->
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/ccapture.js-npmfixed/build/CCapture.all.min.js"></script> --> <!-- video capture -->
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; overflow: hidden; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// === Configuration ===
|
||||
const CONFIG = {
|
||||
seed: 42,
|
||||
// ... project-specific params
|
||||
};
|
||||
|
||||
// === Color Palette ===
|
||||
const PALETTE = {
|
||||
bg: '#0a0a0f',
|
||||
primary: '#e8d5b7',
|
||||
// ...
|
||||
};
|
||||
|
||||
// === Global State ===
|
||||
let particles = [];
|
||||
|
||||
// === Preload (fonts, images, data) ===
|
||||
function preload() {
|
||||
// font = loadFont('...');
|
||||
}
|
||||
|
||||
// === Setup ===
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
randomSeed(CONFIG.seed);
|
||||
noiseSeed(CONFIG.seed);
|
||||
colorMode(HSB, 360, 100, 100, 100);
|
||||
// Initialize state...
|
||||
}
|
||||
|
||||
// === Draw Loop ===
|
||||
function draw() {
|
||||
// Render frame...
|
||||
}
|
||||
|
||||
// === Helper Functions ===
|
||||
// ...
|
||||
|
||||
// === Classes ===
|
||||
class Particle {
|
||||
// ...
|
||||
}
|
||||
|
||||
// === Event Handlers ===
|
||||
function mousePressed() { /* ... */ }
|
||||
function keyPressed() { /* ... */ }
|
||||
function windowResized() { resizeCanvas(windowWidth, windowHeight); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Key implementation patterns:
|
||||
- **Seeded randomness**: Always `randomSeed()` + `noiseSeed()` for reproducibility
|
||||
- **Color mode**: Use `colorMode(HSB, 360, 100, 100, 100)` for intuitive color control
|
||||
- **State separation**: CONFIG for parameters, PALETTE for colors, globals for mutable state
|
||||
- **Class-based entities**: Particles, agents, shapes as classes with `update()` + `display()` methods
|
||||
- **Offscreen buffers**: `createGraphics()` for layered composition, trails, masks
|
||||
|
||||
### Step 4: Preview & Iterate
|
||||
|
||||
- Open HTML file directly in browser — no server needed for basic sketches
|
||||
- For `loadImage()`/`loadFont()` from local files: use `scripts/serve.sh` or `python3 -m http.server`
|
||||
- Chrome DevTools Performance tab to verify 60fps
|
||||
- Test at target export resolution, not just the window size
|
||||
- Adjust parameters until the visual matches the concept from Step 1
|
||||
|
||||
### Step 5: Export
|
||||
|
||||
| Format | Method | Command |
|
||||
|--------|--------|---------|
|
||||
| **PNG** | `saveCanvas('output', 'png')` in `keyPressed()` | Press 's' to save |
|
||||
| **High-res PNG** | Puppeteer headless capture | `node scripts/export-frames.js sketch.html --width 3840 --height 2160 --frames 1` |
|
||||
| **GIF** | `saveGif('output', 5)` — captures N seconds | Press 'g' to save |
|
||||
| **Frame sequence** | `saveFrames('frame', 'png', 10, 30)` — 10s at 30fps | Then `ffmpeg -i frame-%04d.png -c:v libx264 output.mp4` |
|
||||
| **MP4** | Puppeteer frame capture + ffmpeg | `bash scripts/render.sh sketch.html output.mp4 --duration 30 --fps 30` |
|
||||
| **SVG** | `createCanvas(w, h, SVG)` with p5.js-svg | `save('output.svg')` |
|
||||
|
||||
### Step 6: Quality Verification
|
||||
|
||||
- **Does it match the vision?** Compare output to the creative concept. If it looks generic, go back to Step 1
|
||||
- **Resolution check**: Is it sharp at the target display size? No aliasing artifacts?
|
||||
- **Performance check**: Does it hold 60fps in browser? (30fps minimum for animations)
|
||||
- **Color check**: Do the colors work together? Test on both light and dark monitors
|
||||
- **Edge cases**: What happens at canvas edges? On resize? After running for 10 minutes?
|
||||
|
||||
## Critical Implementation Notes
|
||||
|
||||
### Performance — Disable FES First
|
||||
|
||||
The Friendly Error System (FES) adds up to 10x overhead. Disable it in every production sketch:
|
||||
|
||||
```javascript
|
||||
p5.disableFriendlyErrors = true; // BEFORE setup()
|
||||
|
||||
function setup() {
|
||||
pixelDensity(1); // prevent 2x-4x overdraw on retina
|
||||
createCanvas(1920, 1080);
|
||||
}
|
||||
```
|
||||
|
||||
In hot loops (particles, pixel ops), use `Math.*` instead of p5 wrappers — measurably faster:
|
||||
|
||||
```javascript
|
||||
// In draw() or update() hot paths:
|
||||
let a = Math.sin(t); // not sin(t)
|
||||
let r = Math.sqrt(dx*dx+dy*dy); // not dist() — or better: skip sqrt, compare magSq
|
||||
let v = Math.random(); // not random() — when seed not needed
|
||||
let m = Math.min(a, b); // not min(a, b)
|
||||
```
|
||||
|
||||
Never `console.log()` inside `draw()`. Never manipulate DOM in `draw()`. See `references/troubleshooting.md` § Performance.
|
||||
|
||||
### Seeded Randomness — Always
|
||||
|
||||
Every generative sketch must be reproducible. Same seed, same output.
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
randomSeed(CONFIG.seed);
|
||||
noiseSeed(CONFIG.seed);
|
||||
// All random() and noise() calls now deterministic
|
||||
}
|
||||
```
|
||||
|
||||
Never use `Math.random()` for generative content — only for performance-critical non-visual code. Always `random()` for visual elements. If you need a random seed: `CONFIG.seed = floor(random(99999))`.
|
||||
|
||||
### Generative Art Platform Support (fxhash / Art Blocks)
|
||||
|
||||
For generative art platforms, replace p5's PRNG with the platform's deterministic random:
|
||||
|
||||
```javascript
|
||||
// fxhash convention
|
||||
const SEED = $fx.hash; // unique per mint
|
||||
const rng = $fx.rand; // deterministic PRNG
|
||||
$fx.features({ palette: 'warm', complexity: 'high' });
|
||||
|
||||
// In setup():
|
||||
randomSeed(SEED); // for p5's noise()
|
||||
noiseSeed(SEED);
|
||||
|
||||
// Replace random() with rng() for platform determinism
|
||||
let x = rng() * width; // instead of random(width)
|
||||
```
|
||||
|
||||
See `references/export-pipeline.md` § Platform Export.
|
||||
|
||||
### Color Mode — Use HSB
|
||||
|
||||
HSB (Hue, Saturation, Brightness) is dramatically easier to work with than RGB for generative art:
|
||||
|
||||
```javascript
|
||||
colorMode(HSB, 360, 100, 100, 100);
|
||||
// Now: fill(hue, sat, bri, alpha)
|
||||
// Rotate hue: fill((baseHue + offset) % 360, 80, 90)
|
||||
// Desaturate: fill(hue, sat * 0.3, bri)
|
||||
// Darken: fill(hue, sat, bri * 0.5)
|
||||
```
|
||||
|
||||
Never hardcode raw RGB values. Define a palette object, derive variations procedurally. See `references/color-systems.md`.
|
||||
|
||||
### Noise — Multi-Octave, Not Raw
|
||||
|
||||
Raw `noise(x, y)` looks like smooth blobs. Layer octaves for natural texture:
|
||||
|
||||
```javascript
|
||||
function fbm(x, y, octaves = 4) {
|
||||
let val = 0, amp = 1, freq = 1, sum = 0;
|
||||
for (let i = 0; i < octaves; i++) {
|
||||
val += noise(x * freq, y * freq) * amp;
|
||||
sum += amp;
|
||||
amp *= 0.5;
|
||||
freq *= 2;
|
||||
}
|
||||
return val / sum;
|
||||
}
|
||||
```
|
||||
|
||||
For flowing organic forms, use **domain warping**: feed noise output back as noise input coordinates. See `references/visual-effects.md`.
|
||||
|
||||
### createGraphics() for Layers — Not Optional
|
||||
|
||||
Flat single-pass rendering looks flat. Use offscreen buffers for composition:
|
||||
|
||||
```javascript
|
||||
let bgLayer, fgLayer, trailLayer;
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
bgLayer = createGraphics(width, height);
|
||||
fgLayer = createGraphics(width, height);
|
||||
trailLayer = createGraphics(width, height);
|
||||
}
|
||||
function draw() {
|
||||
renderBackground(bgLayer);
|
||||
renderTrails(trailLayer); // persistent, fading
|
||||
renderForeground(fgLayer); // cleared each frame
|
||||
image(bgLayer, 0, 0);
|
||||
image(trailLayer, 0, 0);
|
||||
image(fgLayer, 0, 0);
|
||||
}
|
||||
```
|
||||
|
||||
### Performance — Vectorize Where Possible
|
||||
|
||||
p5.js draw calls are expensive. For thousands of particles:
|
||||
|
||||
```javascript
|
||||
// SLOW: individual shapes
|
||||
for (let p of particles) {
|
||||
ellipse(p.x, p.y, p.size);
|
||||
}
|
||||
|
||||
// FAST: single shape with beginShape()
|
||||
beginShape(POINTS);
|
||||
for (let p of particles) {
|
||||
vertex(p.x, p.y);
|
||||
}
|
||||
endShape();
|
||||
|
||||
// FASTEST: pixel buffer for massive counts
|
||||
loadPixels();
|
||||
for (let p of particles) {
|
||||
let idx = 4 * (floor(p.y) * width + floor(p.x));
|
||||
pixels[idx] = r; pixels[idx+1] = g; pixels[idx+2] = b; pixels[idx+3] = 255;
|
||||
}
|
||||
updatePixels();
|
||||
```
|
||||
|
||||
See `references/troubleshooting.md` § Performance.
|
||||
|
||||
### Instance Mode for Multiple Sketches
|
||||
|
||||
Global mode pollutes `window`. For production, use instance mode:
|
||||
|
||||
```javascript
|
||||
const sketch = (p) => {
|
||||
p.setup = function() {
|
||||
p.createCanvas(800, 800);
|
||||
};
|
||||
p.draw = function() {
|
||||
p.background(0);
|
||||
p.ellipse(p.mouseX, p.mouseY, 50);
|
||||
};
|
||||
};
|
||||
new p5(sketch, 'canvas-container');
|
||||
```
|
||||
|
||||
Required when embedding multiple sketches on one page or integrating with frameworks.
|
||||
|
||||
### WebGL Mode Gotchas
|
||||
|
||||
- `createCanvas(w, h, WEBGL)` — origin is center, not top-left
|
||||
- Y-axis is inverted (positive Y goes up in WEBGL, down in P2D)
|
||||
- `translate(-width/2, -height/2)` to get P2D-like coordinates
|
||||
- `push()`/`pop()` around every transform — matrix stack overflows silently
|
||||
- `texture()` before `rect()`/`plane()` — not after
|
||||
- Custom shaders: `createShader(vert, frag)` — test on multiple browsers
|
||||
|
||||
### Export — Key Bindings Convention
|
||||
|
||||
Every sketch should include these in `keyPressed()`:
|
||||
|
||||
```javascript
|
||||
function keyPressed() {
|
||||
if (key === 's' || key === 'S') saveCanvas('output', 'png');
|
||||
if (key === 'g' || key === 'G') saveGif('output', 5);
|
||||
if (key === 'r' || key === 'R') { randomSeed(millis()); noiseSeed(millis()); }
|
||||
if (key === ' ') CONFIG.paused = !CONFIG.paused;
|
||||
}
|
||||
```
|
||||
|
||||
### Headless Video Export — Use noLoop()
|
||||
|
||||
For headless rendering via Puppeteer, the sketch **must** use `noLoop()` in setup. Without it, p5's draw loop runs freely while screenshots are slow — the sketch races ahead and you get skipped/duplicate frames.
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(1920, 1080);
|
||||
pixelDensity(1);
|
||||
noLoop(); // capture script controls frame advance
|
||||
window._p5Ready = true; // signal readiness to capture script
|
||||
}
|
||||
```
|
||||
|
||||
The bundled `scripts/export-frames.js` detects `_p5Ready` and calls `redraw()` once per capture for exact 1:1 frame correspondence. See `references/export-pipeline.md` § Deterministic Capture.
|
||||
|
||||
For multi-scene videos, use the per-clip architecture: one HTML per scene, render independently, stitch with `ffmpeg -f concat`. See `references/export-pipeline.md` § Per-Clip Architecture.
|
||||
|
||||
### Agent Workflow
|
||||
|
||||
When building p5.js sketches:
|
||||
|
||||
1. **Write the HTML file** — single self-contained file, all code inline
|
||||
2. **Open in browser** — `open sketch.html` (macOS) or `xdg-open sketch.html` (Linux)
|
||||
3. **Local assets** (fonts, images) require a server: `python3 -m http.server 8080` in the project directory, then open `http://localhost:8080/sketch.html`
|
||||
4. **Export PNG/GIF** — add `keyPressed()` shortcuts as shown above, tell the user which key to press
|
||||
5. **Headless export** — `node scripts/export-frames.js sketch.html --frames 300` for automated frame capture (sketch must use `noLoop()` + `_p5Ready`)
|
||||
6. **MP4 rendering** — `bash scripts/render.sh sketch.html output.mp4 --duration 30`
|
||||
7. **Iterative refinement** — edit the HTML file, user refreshes browser to see changes
|
||||
8. **Load references on demand** — use `skill_view(name="p5js", file_path="references/...")` to load specific reference files as needed during implementation
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Frame rate (interactive) | 60fps sustained |
|
||||
| Frame rate (animated export) | 30fps minimum |
|
||||
| Particle count (P2D shapes) | 5,000-10,000 at 60fps |
|
||||
| Particle count (pixel buffer) | 50,000-100,000 at 60fps |
|
||||
| Canvas resolution | Up to 3840x2160 (export), 1920x1080 (interactive) |
|
||||
| File size (HTML) | < 100KB (excluding CDN libraries) |
|
||||
| Load time | < 2s to first frame |
|
||||
|
||||
## References
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `references/core-api.md` | Canvas setup, coordinate system, draw loop, `push()`/`pop()`, offscreen buffers, composition patterns, `pixelDensity()`, responsive design |
|
||||
| `references/shapes-and-geometry.md` | 2D primitives, `beginShape()`/`endShape()`, Bezier/Catmull-Rom curves, `vertex()` systems, custom shapes, `p5.Vector`, signed distance fields, SVG path conversion |
|
||||
| `references/visual-effects.md` | Noise (Perlin, fractal, domain warp, curl), flow fields, particle systems (physics, flocking, trails), pixel manipulation, texture generation (stipple, hatch, halftone), feedback loops, reaction-diffusion |
|
||||
| `references/animation.md` | Frame-based animation, easing functions, `lerp()`/`map()`, spring physics, state machines, timeline sequencing, `millis()`-based timing, transition patterns |
|
||||
| `references/typography.md` | `text()`, `loadFont()`, `textToPoints()`, kinetic typography, text masks, font metrics, responsive text sizing |
|
||||
| `references/color-systems.md` | `colorMode()`, HSB/HSL/RGB, `lerpColor()`, `paletteLerp()`, procedural palettes, color harmony, `blendMode()`, gradient rendering, curated palette library |
|
||||
| `references/webgl-and-3d.md` | WEBGL renderer, 3D primitives, camera, lighting, materials, custom geometry, GLSL shaders (`createShader()`, `createFilterShader()`), framebuffers, post-processing |
|
||||
| `references/interaction.md` | Mouse events, keyboard state, touch input, DOM elements, `createSlider()`/`createButton()`, audio input (p5.sound FFT/amplitude), scroll-driven animation, responsive events |
|
||||
| `references/export-pipeline.md` | `saveCanvas()`, `saveGif()`, `saveFrames()`, deterministic headless capture, ffmpeg frame-to-video, CCapture.js, SVG export, per-clip architecture, platform export (fxhash), video gotchas |
|
||||
| `references/troubleshooting.md` | Performance profiling, per-pixel budgets, common mistakes, browser compatibility, WebGL debugging, font loading issues, pixel density traps, memory leaks, CORS |
|
||||
| `templates/viewer.html` | Interactive viewer template: seed navigation (prev/next/random/jump), parameter sliders, download PNG, responsive canvas. Start from this for explorable generative art |
|
||||
|
||||
---
|
||||
|
||||
## Creative Divergence (use only when user requests experimental/creative/unique output)
|
||||
|
||||
If the user asks for creative, experimental, surprising, or unconventional output, select the strategy that best fits and reason through its steps BEFORE generating code.
|
||||
|
||||
- **Conceptual Blending** — when the user names two things to combine or wants hybrid aesthetics
|
||||
- **SCAMPER** — when the user wants a twist on a known generative art pattern
|
||||
- **Distance Association** — when the user gives a single concept and wants exploration ("make something about time")
|
||||
|
||||
### Conceptual Blending
|
||||
1. Name two distinct visual systems (e.g., particle physics + handwriting)
|
||||
2. Map correspondences (particles = ink drops, forces = pen pressure, fields = letterforms)
|
||||
3. Blend selectively — keep mappings that produce interesting emergent visuals
|
||||
4. Code the blend as a unified system, not two systems side-by-side
|
||||
|
||||
### SCAMPER Transformation
|
||||
Take a known generative pattern (flow field, particle system, L-system, cellular automata) and systematically transform it:
|
||||
- **Substitute**: replace circles with text characters, lines with gradients
|
||||
- **Combine**: merge two patterns (flow field + voronoi)
|
||||
- **Adapt**: apply a 2D pattern to a 3D projection
|
||||
- **Modify**: exaggerate scale, warp the coordinate space
|
||||
- **Purpose**: use a physics sim for typography, a sorting algorithm for color
|
||||
- **Eliminate**: remove the grid, remove color, remove symmetry
|
||||
- **Reverse**: run the simulation backward, invert the parameter space
|
||||
|
||||
### Distance Association
|
||||
1. Anchor on the user's concept (e.g., "loneliness")
|
||||
2. Generate associations at three distances:
|
||||
- Close (obvious): empty room, single figure, silence
|
||||
- Medium (interesting): one fish in a school swimming the wrong way, a phone with no notifications, the gap between subway cars
|
||||
- Far (abstract): prime numbers, asymptotic curves, the color of 3am
|
||||
3. Develop the medium-distance associations — they're specific enough to visualize but unexpected enough to be interesting
|
||||
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);
|
||||
}
|
||||
```
|
||||
179
skills/creative/p5js/scripts/export-frames.js
Executable file
179
skills/creative/p5js/scripts/export-frames.js
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* p5.js Skill — Headless Frame Export
|
||||
*
|
||||
* Captures frames from a p5.js sketch using Puppeteer (headless Chrome).
|
||||
* Uses noLoop() + redraw() for DETERMINISTIC frame-by-frame control.
|
||||
*
|
||||
* IMPORTANT: Your sketch must call noLoop() in setup() and set
|
||||
* window._p5Ready = true when initialized. This script calls redraw()
|
||||
* for each frame capture, ensuring exact 1:1 correspondence between
|
||||
* frameCount and captured frames.
|
||||
*
|
||||
* If the sketch does NOT set window._p5Ready, the script falls back to
|
||||
* a timed capture mode (less precise, may drop/duplicate frames).
|
||||
*
|
||||
* Usage:
|
||||
* node export-frames.js sketch.html [options]
|
||||
*
|
||||
* Options:
|
||||
* --output <dir> Output directory (default: ./frames)
|
||||
* --width <px> Canvas width (default: 1920)
|
||||
* --height <px> Canvas height (default: 1080)
|
||||
* --frames <n> Number of frames to capture (default: 1)
|
||||
* --fps <n> Target FPS for timed fallback mode (default: 30)
|
||||
* --wait <ms> Wait before first capture (default: 2000)
|
||||
* --selector <sel> Canvas CSS selector (default: canvas)
|
||||
*
|
||||
* Examples:
|
||||
* node export-frames.js sketch.html --frames 1 # single PNG
|
||||
* node export-frames.js sketch.html --frames 300 --fps 30 # 10s at 30fps
|
||||
* node export-frames.js sketch.html --width 3840 --height 2160 # 4K still
|
||||
*
|
||||
* Sketch template for deterministic capture:
|
||||
* function setup() {
|
||||
* createCanvas(1920, 1080);
|
||||
* pixelDensity(1);
|
||||
* noLoop(); // REQUIRED for deterministic capture
|
||||
* window._p5Ready = true; // REQUIRED to signal readiness
|
||||
* }
|
||||
* function draw() { ... }
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Parse CLI arguments
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const opts = {
|
||||
input: null,
|
||||
output: './frames',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frames: 1,
|
||||
fps: 30,
|
||||
wait: 2000,
|
||||
selector: 'canvas',
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith('--')) {
|
||||
const key = args[i].slice(2);
|
||||
const val = args[i + 1];
|
||||
if (key in opts && val !== undefined) {
|
||||
opts[key] = isNaN(Number(val)) ? val : Number(val);
|
||||
i++;
|
||||
}
|
||||
} else if (!opts.input) {
|
||||
opts.input = args[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.input) {
|
||||
console.error('Usage: node export-frames.js <sketch.html> [options]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs();
|
||||
const inputPath = path.resolve(opts.input);
|
||||
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
console.error(`File not found: ${inputPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
fs.mkdirSync(opts.output, { recursive: true });
|
||||
|
||||
console.log(`Capturing ${opts.frames} frame(s) from ${opts.input}`);
|
||||
console.log(`Resolution: ${opts.width}x${opts.height}`);
|
||||
console.log(`Output: ${opts.output}/`);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-web-security',
|
||||
'--allow-file-access-from-files',
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setViewport({
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
|
||||
// Navigate to sketch
|
||||
const fileUrl = `file://${inputPath}`;
|
||||
await page.goto(fileUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||
|
||||
// Wait for canvas to appear
|
||||
await page.waitForSelector(opts.selector, { timeout: 10000 });
|
||||
|
||||
// Detect capture mode: deterministic (noLoop+redraw) vs timed (fallback)
|
||||
let deterministic = false;
|
||||
try {
|
||||
await page.waitForFunction('window._p5Ready === true', { timeout: 5000 });
|
||||
deterministic = true;
|
||||
console.log(`Mode: deterministic (noLoop + redraw)`);
|
||||
} catch {
|
||||
console.log(`Mode: timed fallback (sketch does not set window._p5Ready)`);
|
||||
console.log(` For frame-perfect capture, add noLoop() and window._p5Ready=true to setup()`);
|
||||
await new Promise(r => setTimeout(r, opts.wait));
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < opts.frames; i++) {
|
||||
if (deterministic) {
|
||||
// Advance exactly one frame
|
||||
await page.evaluate(() => { redraw(); });
|
||||
// Brief settle time for render to complete
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
}
|
||||
|
||||
const frameName = `frame-${String(i).padStart(4, '0')}.png`;
|
||||
const framePath = path.join(opts.output, frameName);
|
||||
|
||||
// Capture the canvas element
|
||||
const canvas = await page.$(opts.selector);
|
||||
if (!canvas) {
|
||||
console.error('Canvas element not found');
|
||||
break;
|
||||
}
|
||||
|
||||
await canvas.screenshot({ path: framePath, type: 'png' });
|
||||
|
||||
// Progress
|
||||
if (i % 30 === 0 || i === opts.frames - 1) {
|
||||
const pct = ((i + 1) / opts.frames * 100).toFixed(1);
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
process.stdout.write(`\r Frame ${i + 1}/${opts.frames} (${pct}%) — ${elapsed}s`);
|
||||
}
|
||||
|
||||
// In timed mode, wait between frames
|
||||
if (!deterministic && i < opts.frames - 1) {
|
||||
await new Promise(r => setTimeout(r, 1000 / opts.fps));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n Done.');
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
108
skills/creative/p5js/scripts/render.sh
Executable file
108
skills/creative/p5js/scripts/render.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
# p5.js Skill — Headless Render Pipeline
|
||||
# Renders a p5.js sketch to MP4 video via Puppeteer + ffmpeg
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/render.sh sketch.html output.mp4 [options]
|
||||
#
|
||||
# Options:
|
||||
# --width Canvas width (default: 1920)
|
||||
# --height Canvas height (default: 1080)
|
||||
# --fps Frames per second (default: 30)
|
||||
# --duration Duration in seconds (default: 10)
|
||||
# --quality CRF value 0-51 (default: 18, lower = better)
|
||||
# --frames-only Only export frames, skip MP4 encoding
|
||||
#
|
||||
# Examples:
|
||||
# bash scripts/render.sh sketch.html output.mp4
|
||||
# bash scripts/render.sh sketch.html output.mp4 --duration 30 --fps 60
|
||||
# bash scripts/render.sh sketch.html output.mp4 --width 3840 --height 2160
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Defaults
|
||||
WIDTH=1920
|
||||
HEIGHT=1080
|
||||
FPS=30
|
||||
DURATION=10
|
||||
CRF=18
|
||||
FRAMES_ONLY=false
|
||||
|
||||
# Parse arguments
|
||||
INPUT="${1:?Usage: render.sh <input.html> <output.mp4> [options]}"
|
||||
OUTPUT="${2:?Usage: render.sh <input.html> <output.mp4> [options]}"
|
||||
shift 2
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--width) WIDTH="$2"; shift 2 ;;
|
||||
--height) HEIGHT="$2"; shift 2 ;;
|
||||
--fps) FPS="$2"; shift 2 ;;
|
||||
--duration) DURATION="$2"; shift 2 ;;
|
||||
--quality) CRF="$2"; shift 2 ;;
|
||||
--frames-only) FRAMES_ONLY=true; shift ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TOTAL_FRAMES=$((FPS * DURATION))
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
FRAME_DIR=$(mktemp -d)
|
||||
|
||||
echo "=== p5.js Render Pipeline ==="
|
||||
echo "Input: $INPUT"
|
||||
echo "Output: $OUTPUT"
|
||||
echo "Resolution: ${WIDTH}x${HEIGHT}"
|
||||
echo "FPS: $FPS"
|
||||
echo "Duration: ${DURATION}s (${TOTAL_FRAMES} frames)"
|
||||
echo "Quality: CRF $CRF"
|
||||
echo "Frame dir: $FRAME_DIR"
|
||||
echo ""
|
||||
|
||||
# Check dependencies
|
||||
command -v node >/dev/null 2>&1 || { echo "Error: Node.js required"; exit 1; }
|
||||
if [ "$FRAMES_ONLY" = false ]; then
|
||||
command -v ffmpeg >/dev/null 2>&1 || { echo "Error: ffmpeg required for MP4"; exit 1; }
|
||||
fi
|
||||
|
||||
# Step 1: Capture frames via Puppeteer
|
||||
echo "Step 1/2: Capturing ${TOTAL_FRAMES} frames..."
|
||||
node "$SCRIPT_DIR/export-frames.js" \
|
||||
"$INPUT" \
|
||||
--output "$FRAME_DIR" \
|
||||
--width "$WIDTH" \
|
||||
--height "$HEIGHT" \
|
||||
--frames "$TOTAL_FRAMES" \
|
||||
--fps "$FPS"
|
||||
|
||||
echo "Frames captured to $FRAME_DIR"
|
||||
|
||||
if [ "$FRAMES_ONLY" = true ]; then
|
||||
echo "Frames saved to: $FRAME_DIR"
|
||||
echo "To encode manually:"
|
||||
echo " ffmpeg -framerate $FPS -i $FRAME_DIR/frame-%04d.png -c:v libx264 -crf $CRF -pix_fmt yuv420p $OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Step 2: Encode to MP4
|
||||
echo "Step 2/2: Encoding MP4..."
|
||||
ffmpeg -y \
|
||||
-framerate "$FPS" \
|
||||
-i "$FRAME_DIR/frame-%04d.png" \
|
||||
-c:v libx264 \
|
||||
-preset slow \
|
||||
-crf "$CRF" \
|
||||
-pix_fmt yuv420p \
|
||||
-movflags +faststart \
|
||||
"$OUTPUT" \
|
||||
2>"$FRAME_DIR/ffmpeg.log"
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$FRAME_DIR"
|
||||
|
||||
# Report
|
||||
FILE_SIZE=$(ls -lh "$OUTPUT" | awk '{print $5}')
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Output: $OUTPUT ($FILE_SIZE)"
|
||||
echo "Duration: ${DURATION}s at ${FPS}fps, ${WIDTH}x${HEIGHT}"
|
||||
28
skills/creative/p5js/scripts/serve.sh
Executable file
28
skills/creative/p5js/scripts/serve.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# p5.js Skill — Local Development Server
|
||||
# Serves the current directory over HTTP for loading local assets (fonts, images)
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/serve.sh [port] [directory]
|
||||
#
|
||||
# Examples:
|
||||
# bash scripts/serve.sh # serve CWD on port 8080
|
||||
# bash scripts/serve.sh 3000 # serve CWD on port 3000
|
||||
# bash scripts/serve.sh 8080 ./my-project # serve specific directory
|
||||
|
||||
PORT="${1:-8080}"
|
||||
DIR="${2:-.}"
|
||||
|
||||
echo "=== p5.js Dev Server ==="
|
||||
echo "Serving: $(cd "$DIR" && pwd)"
|
||||
echo "URL: http://localhost:$PORT"
|
||||
echo "Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
cd "$DIR" && python3 -m http.server "$PORT" 2>/dev/null || {
|
||||
echo "Python3 not found. Trying Node.js..."
|
||||
npx serve -l "$PORT" "$DIR" 2>/dev/null || {
|
||||
echo "Error: Need python3 or npx (Node.js) for local server"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
87
skills/creative/p5js/scripts/setup.sh
Executable file
87
skills/creative/p5js/scripts/setup.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# p5.js Skill — Dependency Verification
|
||||
# Run: bash skills/creative/p5js/scripts/setup.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
fail() { echo -e "${RED}[FAIL]${NC} $1"; }
|
||||
|
||||
echo "=== p5.js Skill — Setup Check ==="
|
||||
echo ""
|
||||
|
||||
# Required: Node.js (for Puppeteer headless export)
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_VER=$(node -v)
|
||||
ok "Node.js $NODE_VER"
|
||||
else
|
||||
warn "Node.js not found — optional, needed for headless export"
|
||||
echo " Install: https://nodejs.org/ or 'brew install node'"
|
||||
fi
|
||||
|
||||
# Required: npm (for Puppeteer install)
|
||||
if command -v npm &>/dev/null; then
|
||||
NPM_VER=$(npm -v)
|
||||
ok "npm $NPM_VER"
|
||||
else
|
||||
warn "npm not found — optional, needed for headless export"
|
||||
fi
|
||||
|
||||
# Optional: Puppeteer
|
||||
if node -e "require('puppeteer')" 2>/dev/null; then
|
||||
ok "Puppeteer installed"
|
||||
else
|
||||
warn "Puppeteer not installed — needed for headless export"
|
||||
echo " Install: npm install puppeteer"
|
||||
fi
|
||||
|
||||
# Optional: ffmpeg (for MP4 encoding from frame sequences)
|
||||
if command -v ffmpeg &>/dev/null; then
|
||||
FFMPEG_VER=$(ffmpeg -version 2>&1 | head -1 | awk '{print $3}')
|
||||
ok "ffmpeg $FFMPEG_VER"
|
||||
else
|
||||
warn "ffmpeg not found — needed for MP4 export"
|
||||
echo " Install: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"
|
||||
fi
|
||||
|
||||
# Optional: Python3 (for local server)
|
||||
if command -v python3 &>/dev/null; then
|
||||
PY_VER=$(python3 --version 2>&1 | awk '{print $2}')
|
||||
ok "Python $PY_VER (for local server: python3 -m http.server)"
|
||||
else
|
||||
warn "Python3 not found — needed for local file serving"
|
||||
fi
|
||||
|
||||
# Browser check (macOS)
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if open -Ra "Google Chrome" 2>/dev/null; then
|
||||
ok "Google Chrome found"
|
||||
elif open -Ra "Safari" 2>/dev/null; then
|
||||
ok "Safari found"
|
||||
else
|
||||
warn "No browser detected"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Core Requirements ==="
|
||||
echo " A modern browser (Chrome/Firefox/Safari/Edge)"
|
||||
echo " p5.js loaded via CDN — no local install needed"
|
||||
echo ""
|
||||
echo "=== Optional (for export) ==="
|
||||
echo " Node.js + Puppeteer — headless frame capture"
|
||||
echo " ffmpeg — frame sequence to MP4"
|
||||
echo " Python3 — local development server"
|
||||
echo ""
|
||||
echo "=== Quick Start ==="
|
||||
echo " 1. Create an HTML file with inline p5.js sketch"
|
||||
echo " 2. Open in browser: open sketch.html"
|
||||
echo " 3. Press 's' to save PNG, 'g' to save GIF"
|
||||
echo ""
|
||||
echo "Setup check complete."
|
||||
395
skills/creative/p5js/templates/viewer.html
Normal file
395
skills/creative/p5js/templates/viewer.html
Normal file
@@ -0,0 +1,395 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
p5.js Interactive Viewer Template
|
||||
=================================
|
||||
USE THIS AS THE STARTING POINT for interactive generative art sketches.
|
||||
|
||||
FIXED (keep as-is):
|
||||
✓ Layout structure (sidebar + canvas)
|
||||
✓ Seed navigation (prev/next/random/jump)
|
||||
✓ Action buttons (regenerate, reset, download PNG)
|
||||
✓ Responsive canvas sizing
|
||||
✓ Parameter update + regeneration wiring
|
||||
|
||||
VARIABLE (replace for each project):
|
||||
✗ The p5.js algorithm (setup/draw/classes)
|
||||
✗ The PARAMS object (define what your art needs)
|
||||
✗ The parameter controls in the sidebar (sliders, pickers)
|
||||
✗ The color palette
|
||||
✗ The title and description
|
||||
|
||||
For headless export: add noLoop() and window._p5Ready=true in setup().
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Generative Art Viewer</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/p5.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: #0a0a0f;
|
||||
color: #c8c8d0;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Sidebar --- */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
background: #12121a;
|
||||
border-right: 1px solid #1e1e2a;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.sidebar h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e8e8f0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.sidebar .subtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* --- Seed Controls --- */
|
||||
.seed-display {
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #e8e8f0;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
background: #1a1a25;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.seed-nav {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.seed-nav button {
|
||||
flex: 1;
|
||||
padding: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.seed-jump {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.seed-jump input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
background: #1a1a25;
|
||||
border: 1px solid #2a2a35;
|
||||
border-radius: 4px;
|
||||
color: #c8c8d0;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.seed-jump button { padding: 6px 12px; font-size: 12px; }
|
||||
|
||||
/* --- Parameter Controls --- */
|
||||
.control-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.control-group label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.control-group .value {
|
||||
color: #aaa;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
.control-group input[type="range"] {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: #2a2a35;
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
.control-group input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px; height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #6a9bcc;
|
||||
cursor: pointer;
|
||||
}
|
||||
.control-group input[type="color"] {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
border: 1px solid #2a2a35;
|
||||
border-radius: 4px;
|
||||
background: #1a1a25;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* --- Buttons --- */
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
background: #1e1e2a;
|
||||
border: 1px solid #2a2a35;
|
||||
border-radius: 4px;
|
||||
color: #c8c8d0;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
button:hover { background: #2a2a3a; }
|
||||
button.primary { background: #2a4a6a; border-color: #3a5a7a; }
|
||||
button.primary:hover { background: #3a5a7a; }
|
||||
|
||||
.actions { display: flex; flex-direction: column; gap: 6px; }
|
||||
.actions button { width: 100%; }
|
||||
|
||||
/* --- Canvas Area --- */
|
||||
.canvas-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: #08080c;
|
||||
}
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- === SIDEBAR === -->
|
||||
<div class="sidebar">
|
||||
<!-- FIXED: Title (customize text, keep structure) -->
|
||||
<div>
|
||||
<h1 id="art-title">Generative Sketch</h1>
|
||||
<div class="subtitle" id="art-subtitle">p5.js generative art</div>
|
||||
</div>
|
||||
|
||||
<!-- FIXED: Seed Navigation -->
|
||||
<div>
|
||||
<div class="section-title">Seed</div>
|
||||
<div class="seed-display" id="seed-display">42</div>
|
||||
<div class="seed-nav">
|
||||
<button onclick="changeSeed(-1)">◀ Prev</button>
|
||||
<button onclick="changeSeed(1)">Next ▶</button>
|
||||
<button onclick="randomizeSeed()">Random</button>
|
||||
</div>
|
||||
<div class="seed-jump">
|
||||
<input type="number" id="seed-input" placeholder="Seed #" min="0">
|
||||
<button onclick="jumpToSeed()">Go</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VARIABLE: Parameters (customize for each project) -->
|
||||
<div id="params-section">
|
||||
<div class="section-title">Parameters</div>
|
||||
|
||||
<!-- === REPLACE THESE WITH YOUR PARAMETERS === -->
|
||||
<div class="control-group">
|
||||
<label>Count <span class="value" id="count-val">500</span></label>
|
||||
<input type="range" id="count" min="50" max="2000" step="50" value="500"
|
||||
oninput="updateParam('count', +this.value)">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Scale <span class="value" id="scale-val">0.005</span></label>
|
||||
<input type="range" id="scale" min="0.001" max="0.02" step="0.001" value="0.005"
|
||||
oninput="updateParam('scale', +this.value)">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Speed <span class="value" id="speed-val">2.0</span></label>
|
||||
<input type="range" id="speed" min="0.5" max="5" step="0.1" value="2.0"
|
||||
oninput="updateParam('speed', +this.value)">
|
||||
</div>
|
||||
<!-- === END PARAMETER CONTROLS === -->
|
||||
</div>
|
||||
|
||||
<!-- VARIABLE: Colors (optional — include if art needs adjustable palette) -->
|
||||
<!--
|
||||
<div>
|
||||
<div class="section-title">Colors</div>
|
||||
<div class="control-group">
|
||||
<label>Background</label>
|
||||
<input type="color" id="bg-color" value="#0a0a14"
|
||||
oninput="updateParam('bgColor', this.value)">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Primary</label>
|
||||
<input type="color" id="primary-color" value="#6a9bcc"
|
||||
oninput="updateParam('primaryColor', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- FIXED: Actions -->
|
||||
<div class="actions">
|
||||
<div class="section-title">Actions</div>
|
||||
<button class="primary" onclick="regenerate()">Regenerate</button>
|
||||
<button onclick="resetDefaults()">Reset Defaults</button>
|
||||
<button onclick="downloadPNG()">Download PNG</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === CANVAS === -->
|
||||
<div class="canvas-area" id="canvas-container"></div>
|
||||
|
||||
<script>
|
||||
// ====================================================================
|
||||
// CONFIGURATION — REPLACE FOR EACH PROJECT
|
||||
// ====================================================================
|
||||
const DEFAULTS = {
|
||||
seed: 42,
|
||||
count: 500,
|
||||
scale: 0.005,
|
||||
speed: 2.0,
|
||||
// Add your parameters here
|
||||
};
|
||||
|
||||
let PARAMS = { ...DEFAULTS };
|
||||
|
||||
// ====================================================================
|
||||
// SEED NAVIGATION — FIXED (do not modify)
|
||||
// ====================================================================
|
||||
function changeSeed(delta) {
|
||||
PARAMS.seed = Math.max(0, PARAMS.seed + delta);
|
||||
document.getElementById('seed-display').textContent = PARAMS.seed;
|
||||
regenerate();
|
||||
}
|
||||
|
||||
function randomizeSeed() {
|
||||
PARAMS.seed = Math.floor(Math.random() * 99999);
|
||||
document.getElementById('seed-display').textContent = PARAMS.seed;
|
||||
regenerate();
|
||||
}
|
||||
|
||||
function jumpToSeed() {
|
||||
let v = parseInt(document.getElementById('seed-input').value);
|
||||
if (!isNaN(v) && v >= 0) {
|
||||
PARAMS.seed = v;
|
||||
document.getElementById('seed-display').textContent = PARAMS.seed;
|
||||
document.getElementById('seed-input').value = '';
|
||||
regenerate();
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// PARAMETER UPDATES — CUSTOMIZE updateParam body as needed
|
||||
// ====================================================================
|
||||
function updateParam(name, value) {
|
||||
PARAMS[name] = value;
|
||||
let el = document.getElementById(name + '-val');
|
||||
if (el) el.textContent = typeof value === 'number' && value < 1 ? value.toFixed(3) : value;
|
||||
regenerate();
|
||||
}
|
||||
|
||||
function resetDefaults() {
|
||||
PARAMS = { ...DEFAULTS };
|
||||
// Reset all sliders to default values
|
||||
for (let [key, val] of Object.entries(DEFAULTS)) {
|
||||
let el = document.getElementById(key);
|
||||
if (el) el.value = val;
|
||||
let valEl = document.getElementById(key + '-val');
|
||||
if (valEl) valEl.textContent = typeof val === 'number' && val < 1 ? val.toFixed(3) : val;
|
||||
}
|
||||
document.getElementById('seed-display').textContent = PARAMS.seed;
|
||||
regenerate();
|
||||
}
|
||||
|
||||
function regenerate() {
|
||||
randomSeed(PARAMS.seed);
|
||||
noiseSeed(PARAMS.seed);
|
||||
// Clear and redraw
|
||||
clear();
|
||||
initializeArt();
|
||||
redraw();
|
||||
}
|
||||
|
||||
function downloadPNG() {
|
||||
saveCanvas('generative-art-seed-' + PARAMS.seed, 'png');
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// P5.JS SKETCH — REPLACE ENTIRELY FOR EACH PROJECT
|
||||
// ====================================================================
|
||||
|
||||
// Your state variables
|
||||
let particles = [];
|
||||
|
||||
function initializeArt() {
|
||||
// Initialize your generative system using PARAMS
|
||||
// This is called on every regenerate()
|
||||
particles = [];
|
||||
for (let i = 0; i < PARAMS.count; i++) {
|
||||
particles.push({
|
||||
x: random(width),
|
||||
y: random(height),
|
||||
vx: 0, vy: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// Size canvas to fit container
|
||||
let container = document.getElementById('canvas-container');
|
||||
let size = Math.min(container.clientWidth - 40, container.clientHeight - 40, 1080);
|
||||
let cnv = createCanvas(size, size);
|
||||
cnv.parent('canvas-container');
|
||||
pixelDensity(1);
|
||||
colorMode(HSB, 360, 100, 100, 100);
|
||||
|
||||
randomSeed(PARAMS.seed);
|
||||
noiseSeed(PARAMS.seed);
|
||||
initializeArt();
|
||||
|
||||
// For interactive/animated sketches: remove noLoop()
|
||||
// For static generation: keep noLoop()
|
||||
noLoop();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
background(0, 0, 5);
|
||||
|
||||
// === YOUR ALGORITHM HERE ===
|
||||
// Use PARAMS.count, PARAMS.scale, PARAMS.speed, etc.
|
||||
noStroke();
|
||||
for (let p of particles) {
|
||||
let n = noise(p.x * PARAMS.scale, p.y * PARAMS.scale);
|
||||
let hue = (n * 200 + PARAMS.seed * 0.1) % 360;
|
||||
fill(hue, 70, 80, 60);
|
||||
circle(p.x, p.y, n * 10 + 2);
|
||||
}
|
||||
// === END ALGORITHM ===
|
||||
}
|
||||
|
||||
function windowResized() {
|
||||
let container = document.getElementById('canvas-container');
|
||||
let size = Math.min(container.clientWidth - 40, container.clientHeight - 40, 1080);
|
||||
resizeCanvas(size, size);
|
||||
regenerate();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user