Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
820bd4a
align readme template with reality
shakeelmohamed Apr 10, 2026
7f8b8a3
Add support for loadJSON() + API cleanup + test coverage
shakeelmohamed Apr 10, 2026
e6072ee
add loadImage() support, some test are broken due to broken functiona…
shakeelmohamed Apr 12, 2026
8c45c55
bun pre-load script + fix lint errors
shakeelmohamed Apr 12, 2026
3604487
fix out of bounds array access in tests
shakeelmohamed Apr 12, 2026
7feee95
fix non-stretching issue to output canvas
shakeelmohamed Apr 12, 2026
2cebfa5
test hardening
shakeelmohamed Apr 12, 2026
50aee1d
nit
shakeelmohamed Apr 12, 2026
11f546f
add test coverage and some cleanup
shakeelmohamed Apr 14, 2026
bd4eb50
add missing globals and basic tests
shakeelmohamed Apr 14, 2026
004f480
add open_issues file for WIP
shakeelmohamed Apr 14, 2026
4534680
add coverage for more built in tests
shakeelmohamed Apr 14, 2026
549e50b
add failing test for unbound global vars
shakeelmohamed Apr 14, 2026
e8632e5
add test coverage; mark images as supported
shakeelmohamed Apr 14, 2026
2d8a286
add test coverage
shakeelmohamed Apr 14, 2026
67eb2c8
add broken tests around loop lifecycle
shakeelmohamed Apr 14, 2026
0c5af16
fix loop() and noLoop() lifecycle tests
shakeelmohamed Apr 14, 2026
d88a4ff
complete test coverage for draw/redraw/loop/noloop lifecycle
shakeelmohamed Apr 14, 2026
0f66cce
Add noops for p5 accessibility functions
shakeelmohamed Apr 15, 2026
72e1944
p5b-dom add detached parent pollyfill
shakeelmohamed Apr 15, 2026
4f879b9
add data support + test coverage
shakeelmohamed Apr 15, 2026
816b795
add image mode tests
shakeelmohamed Apr 15, 2026
43e79fe
expose drawingContext
shakeelmohamed Apr 15, 2026
9b692ae
more test coverage for common p5.js functions
shakeelmohamed Apr 15, 2026
99205c5
Add happy path 2x optimization when p5 dimensions match p5b dimensions
shakeelmohamed Apr 15, 2026
72893fe
revise open issues
shakeelmohamed Apr 15, 2026
76a42c9
update open issues
shakeelmohamed Apr 15, 2026
b17d9ee
add a perf test for memory leaks
shakeelmohamed Apr 16, 2026
727b2a8
some cleanup
shakeelmohamed Apr 16, 2026
9efa7eb
update readme
shakeelmohamed Apr 16, 2026
d9934da
remove _cleanupGlobals() as it doesn't impact perf enough
shakeelmohamed Apr 16, 2026
0a524d6
Update open issues
shakeelmohamed Apr 16, 2026
c1bbb7f
draft changelog for v1.2.0 + bump version number
shakeelmohamed Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## [1.2.0]

### Breaking Changes

- `stop()` now pauses the sketch (`noLoop()`); call `run()` to resume
- `remove()` / `clear()` fully tears down the p5 instance and frees resources

### New Features

- `loadImage()` — local files and HTTP URLs, works in `preload` with `image()` in draw
- `loadJSON()` — local and remote JSON files
- `loadStrings()` — local text files as array of lines
- `loadTable()` — CSV/TSV/SSV files as `p5.Table` with optional header parsing
- `drawingContext` — direct Canvas 2D API access after `createCanvas()`
- Math constants and functions (`PI`, `TWO_PI`, `abs`, `sin`, `cos`, etc.) explicitly bound
- p5.js constants (`CORNER`, `CENTER`, `RGB`, `HSB`, blend modes, key codes, etc.) explicitly bound
- Accessibility, save, audio, and input event functions stubbed to prevent crashes

### Performance

- ~2× faster frame reads when sketch canvas dimensions match p5b output dimensions
- Fixed memory leak in `stop()`/`run()` cycles by reusing the sketch canvas

### Bug Fixes

- Fixed p5.js initialization crashes (`parentNode`, `mediaDevices`, `navigator`)
- `loadFont()` now throws a descriptive error on missing files
- WEBGL mode now throws a clear unsupported error instead of crashing

## [1.1.1]

- Fix unbounded memory leak when running sketches
Expand Down
142 changes: 142 additions & 0 deletions OPEN_ISSUES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Open Issues - p5b.js

## Completed & Tested

The following are implemented and have passing test coverage:

- **Math Constants**: PI, TWO_PI, HALF_PI, QUARTER_PI, TAU, DEGREES, RADIANS
- **Math Functions**: abs, ceil, floor, round, pow, sqrt, exp, log, max, min, sin, cos, tan, asin, acos, atan, atan2, sq, mag, fract
- **Random/Noise**: random, randomSeed, randomGaussian, noise, noiseSeed, noiseDetail
- **Utility Functions**: map, lerp, lerpColor, constrain, dist, createVector
- **String Functions**: nf, nfc, nfp, nfs, join, split, splitTokens, trim
- **Shape Constants**: CORNER, CORNERS, RADIUS, CENTER, LEFT, RIGHT, TOP, BOTTOM, BASELINE, CLOSE, OPEN, CHORD, PIE, POINTS, LINES, TRIANGLES, etc.
- **Blend Mode Constants**: BLEND, ADD, REMOVE, DARKEST, LIGHTEST, DIFFERENCE, SUBTRACT, EXCLUSION, MULTIPLY, SCREEN, REPLACE, OVERLAY, HARD_LIGHT, SOFT_LIGHT, DODGE, BURN
- **Cursor/Input Constants**: ARROW, CROSS, HAND, MOVE, TEXT, WAIT, key codes, arrow keys
- **Typography Constants**: NORMAL, ITALIC, BOLD, BOLDITALIC, CHAR, WORD
- **Other Constants**: AUTO, STROKE, FILL, TEXTURE, IMMEDIATE, NEAREST, REPEAT, CLAMP, MIRROR, FLAT, SMOOTH, LANDSCAPE, PORTRAIT
- **Time Functions**: year(), month(), day(), hour(), minute(), second()
- **Environment**: frameRate(), loop(), noLoop(), isLooping(), redraw()
- **Loop lifecycle**: noLoop() in setup, noLoop() in draw, redraw() while stopped, loop()/noLoop() toggle, external control, frameCount preservation
- **Mode/Style**: rectMode(), ellipseMode(), strokeCap(), strokeJoin()
- **Typography**: textLeading(), textStyle(), textWidth(), textAlign(), textWrap()
- **Data/IO**: loadStrings(), loadTable()
- **Environment (Extended)**: cursor(), noCursor(), pixelDensity(), windowWidth, windowHeight
- **Accessibility**: describe(), describeElement(), textOutput(), gridOutput() (all noops in headless)
- **imageMode()**: CORNER, CENTER, CORNERS
- **drawingContext**: direct Canvas 2D API access after createCanvas()

---

## Next Up (v1.3.0)

These are the top priorities for the next release.

### 1. Graphics Pool State on Reuse

Pooled graphics retain previous state (draw settings, transformations, pixel data) between frames.

**Location:** `p5b.js` — `createGraphics` pool checkout (~line 310)

**Fix:** Reset graphics state (transform, fill, stroke, etc.) when pulling from pool.

---

### 2. Graphics Pool Unbounded Growth

If a sketch creates graphics of many different sizes, the pool map grows indefinitely.

**Location:** `p5b.js` — pool management in `_initSketch` (~line 185)

**Fix:** Cap bucket size per key, or add LRU eviction across the pool map.

### 3. `global:` Config Option

Shared sketch-scope variables across `preload`/`setup`/`draw` when using inline config functions (no `sketchPath`).

**Root cause:** When config supplies `{preload, setup, draw}` as functions, p5b assigns each to `global.*`. These functions are defined in the caller's closure — a `let x` inside `preload` is invisible to `setup`. Users must write `global.x = ...` explicitly to share state across lifecycle functions.

By contrast, `sketchPath` sketches run via `vm.runInThisContext`, so top-level variables in the sketch file are shared naturally.

**Fix:** Add a `global:` function to config that runs before `preload` and declares shared variables into global scope:

```js
new P5b({
global: () => { myImage = null; },
preload: () => { myImage = loadImage('img.png'); },
setup: () => { image(myImage, 0, 0); },
draw: () => {},
});
```

---

## Backlog

Lower priority issues identified during code review. Not scoped to any specific release.

### Code Quality

#### Asset Path / URL Duplication
`filePath.startsWith("http")` and `file://` URL construction duplicated across `loadImage`, `loadJSON`, `loadStrings`, `loadTable`. Extract to a shared helper.

#### Preload Counter Duplication
`p5._incrementPreload()` / `setImmediate(p5._decrementPreload())` pattern repeated across `loadImage`, `loadStrings`, `loadTable`. Extract to a helper.

#### `fetch` Bound at Init Time
`p5b-dom.js` sets `fetch: global.fetch` at construction time. If `fetch` isn't available yet (Node < 18 without polyfill), it's permanently `undefined`. Should be a getter: `get fetch() { return global.fetch; }`.

#### `loadFont()` vs `loadJSON()` Inconsistency
`loadFont()` is synchronous (blocking file I/O). `loadJSON()` is async. Surprising difference for users familiar with p5.js where both use the same callback/preload pattern.

#### `async preload()` Silently Broken
If a sketch uses `async function preload() { await loadJSON(...) }`, p5.js never awaits the returned promise. Assets will not be loaded before `setup()` runs. Should detect and warn.

### API Gaps

#### `loadJSON()` Callback Compatibility
p5.js `loadJSON()` supports `loadJSON(path, successCallback, errorCallback)`. p5b's implementation is async-only. Sketches using the callback pattern will silently get no data.

#### `loadStrings()` HTTP Support
`loadStrings()` supports local files only. `loadImage()` and `loadJSON()` both support HTTP URLs. Inconsistent.

#### `loadBytes()` Missing
`loadBytes()` is not implemented. Calls will throw `"loadBytes is not defined"` with no helpful error.

#### `loadXML()` Missing
`loadXML()` is not implemented. Calls will throw `"loadXML is not defined"` with no helpful error.

#### DOM Functions Behavior Unverified
p5.js may auto-bind DOM functions (`createButton()`, `createCheckbox()`, `createRadio()`, `createSlider()`, `createColorPicker()`, `createInput()`, `createFileInput()`, `createSelect()`, `createDiv()`, `createP()`, `createSpan()`, `createImg()`, `createA()`, `createVideo()`, `createCapture()`, `createTextarea()`) via `_bindGlobals()`. Their actual behavior in headless has not been tested. Need to audit what p5.js exposes and whether calls succeed, silently fail, or crash.

#### `select()`, `selectAll()`, `removeElements()` Not Implemented
These query and manipulate p5-created DOM elements. In headless, all elements live in the DOM shim — these functions should query/manipulate the shim's tracked elements rather than a real browser DOM. Non-trivial to implement correctly.

---

## Known Unsupported (By Design)

These require browser APIs unavailable in Node.js:

### Sound (p5.sound)

| Missing |
|---------|
| `loadSound`, `loadAudio`, `createAudio` |
| `Oscillator`, `p5.AudioIn`, `p5.FFT`, `p5.Amplitude` |
| `play`, `pause`, `loop`, `stop`, `jump`, `rate`, `amp` |

### Video/Capture

| Missing |
|---------|
| `createCapture(VIDEO/AUDIO)`, `createVideo()` |

### 3D/WebGL

WEBGL renderer throws by design.

| Missing |
|---------|
| `createCanvas(w, h, WEBGL)` |
| `plane`, `box`, `sphere`, `cylinder`, `cone`, `torus` |
| `loadModel`, `ambientLight`, `directionalLight`, `camera`, `orbitControl` |
49 changes: 44 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ NOTE: several features are untested or unsupported, including the following:
- p5.js v2.x
- webgl
- shaders
- images
- video
- sound
- third party plugins or extensions
Expand Down Expand Up @@ -65,22 +64,30 @@ Creates a new P5b instance with the given options.

#### `run()`

Start sketch execution and begin rendering frames.
Start or resume sketch execution. On first call, initializes the p5 instance. After `stop()`, resumes the draw loop. Throws if called after `remove()`.

```javascript
p5b.run();
```

Throws if already running.

#### `stop()`

Stop sketch execution and clean up resources.
Pause sketch execution. The p5 instance and canvas are kept alive. Call `run()` to resume.

```javascript
p5b.stop();
```

#### `remove()`

Fully tear down the p5 instance and free all resources. Calling `run()` after `remove()` throws.

```javascript
p5b.remove(); // or p5b.clear()
```

`clear()` is an alias for `remove()`.

#### `toFrame()`

Get current canvas as a Uint8Array RGBA buffer.
Expand Down Expand Up @@ -163,6 +170,38 @@ const [r, g, b, a] = buffer.slice(idx, idx + 4);
- Reducing `width` / `height`
- Optimizing `draw()` logic

### Happy Path Optimization

When your sketch calls `createCanvas(w, h)` with dimensions that exactly match the p5b `width` and `height` config, p5b reads pixels directly from the canvas without any resizing step. This is ~2× faster per frame.

```javascript
// Fast: canvas matches p5b output dimensions — no resize
const p5b = new P5b({ width: 512, height: 512, ... });
// In sketch: createCanvas(512, 512)

// Slower: canvas is larger than p5b output — resized every frame
const p5b = new P5b({ width: 256, height: 256, ... });
// In sketch: createCanvas(512, 512)
```

### Browser Preview (p5.js Web Editor)

p5b sets `navigator.userAgent` to `"p5b-dom/<version>"` so sketches can detect the headless environment. Use this to scale up the canvas for a readable preview when running in the browser, while keeping the output dimensions small for p5b:

```javascript
function setup() {
createCanvas(64, 64);
if (!navigator.userAgent.includes('p5b')) {
resizeCanvas(
floor(min(windowWidth, windowHeight) / width) * width,
floor(min(windowWidth, windowHeight) / height) * height
);
}
}
```

This scales the canvas to the largest integer multiple that fits the window — no CSS, no interpolation artifacts.

## Transport Layer

For streaming frames to external systems, see [examples/ex-p5b-zmq.js](examples/ex-p5b-zmq.js) for a ZeroMQ adapter reference.
Expand Down
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
preload = ["./test-preload.js"]
45 changes: 42 additions & 3 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ module.exports = [
clearImmediate: "readonly",
Buffer: "readonly",
setInterval: "readonly",
clearInterval: "readonly"
clearInterval: "readonly",
setTimeout: "readonly",
createCanvas: "readonly",
background: "readonly",
createGraphics: "readonly",
fill: "readonly",
noStroke: "readonly",
rect: "readonly"
}
},
rules: {
Expand Down Expand Up @@ -50,9 +57,26 @@ module.exports = [
height: "readonly",
createGraphics: "readonly",
loadFont: "readonly",
loadJSON: "readonly",
noStroke: "readonly",
ellipse: "readonly",
image: "readonly"
image: "readonly",
saveCanvas: "readonly",
saveJSON: "readonly",
print: "readonly",
mouseX: "readonly",
mouseY: "readonly",
key: "readonly",
keyCode: "readonly",
mousePressed: "readonly",
keyPressed: "readonly",
touchStarted: "readonly",
accelerationX: "readonly",
accelerationY: "readonly",
accelerationZ: "readonly",
loadImage: "readonly",
noLoop: "readonly",
path: "readonly"
}
},
rules: {
Expand All @@ -79,9 +103,24 @@ module.exports = [
height: "readonly",
createGraphics: "readonly",
loadFont: "readonly",
loadJSON: "readonly",
noStroke: "readonly",
ellipse: "readonly",
image: "readonly"
image: "readonly",
saveCanvas: "readonly",
saveJSON: "readonly",
print: "readonly",
mouseX: "readonly",
mouseY: "readonly",
key: "readonly",
keyCode: "readonly",
mousePressed: "readonly",
keyPressed: "readonly",
touchStarted: "readonly",
accelerationX: "readonly",
accelerationY: "readonly",
accelerationZ: "readonly",
loadImage: "readonly"
}
},
rules: {
Expand Down
Loading
Loading