Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [1.2.1]

### Bug Fixes

- Fixed `windowWidth`/`windowHeight` being undefined when accessed at top-level in `sketchPath` mode

### Examples

- Added terminal renderer CLI example (`ex-terminal-cli.js`)

## [1.2.0]

### Breaking Changes
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ npm install @10k24/p5b

## Quick Start

**Inline mode** — define setup/draw callbacks directly:

```javascript
const { P5b } = require("@10k24/p5b");

Expand All @@ -40,6 +42,25 @@ p5b.on("frame", (buffer) => {
p5b.run();
```

**Sketch file mode** — load a `.js` sketch file (defines `setup`/`draw` as globals):

```javascript
const { P5b } = require("@10k24/p5b");

const p5b = new P5b({
width: 400,
height: 400,
fps: 60,
sketchPath: "./my-sketch.js"
});

p5b.on("frame", (buffer) => {
// Process frame buffer
});

p5b.run();
```

## API

### Constructor
Expand Down Expand Up @@ -139,6 +160,7 @@ See [examples/](examples/) for runnable examples:
- [examples/ex-file-based.js](examples/ex-file-based.js) — Loading sketch from file
- [examples/ex-inline.js](examples/ex-inline.js) — Using setup/draw callbacks
- [examples/ex-p5b-zmq.js](examples/ex-p5b-zmq.js) — ZeroMQ frame transport
- [examples/ex-terminal-cli.js](examples/ex-terminal-cli.js) — Render a p5.js sketch in the terminal using truecolor ANSI half-block characters.


## Buffer Format
Expand Down
25 changes: 22 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,26 @@ The sketch draws an animated checkerboard pattern that scales from 400x400 (draw

Press `Ctrl+C` to close the connection and exit.

## Notes
## Terminal Renderer (CLI) Examples

- Examples require dependencies installed separately and do NOT affect the core p5b library installation
- Each example is self-contained and can be run independently
Renders a p5.js sketch from a file in the terminal using truecolor ANSI half-block characters.

**Usage:**

```bash
node ex-terminal-cli.js <sketch-path>
```

**Example:**

```bash
node ex-terminal-cli.js sketch-rings.js
```

**Requirements:**
- Truecolor terminal (Ghostty, Kitty, iTerm2, WezTerm, etc.)
- A sketch file that defines `setup()` and `draw()` functions

**Included Sketch:** `sketch-rings.js` - Animated concentric rings pattern

Press `Ctrl+C` to exit.
54 changes: 54 additions & 0 deletions examples/ex-terminal-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Render a p5.js sketch in the terminal using truecolor ANSI half-block characters.
// Works in any truecolor terminal (Ghostty, Kitty, iTerm2, WezTerm, etc.)
const { P5b } = require("../p5b.js");

const sketchPath = process.argv[2];

if (!sketchPath) {
console.error("Usage: node ex-terminal-cli.js <sketch-path>");
process.exit(1);
}

// Set sketch size based on terminal dimensions
// Each character cell = 1 col × 2 rows of pixels, so windowHeight must be even.
// Reserve 1 row to prevent scroll (which breaks cursor-home positioning).
//
// Note: we must bootstrap these values here, which typically
// are defined in the p5b wrapped version of createCanvas(w, h)
global.windowWidth = process.stdout.columns || 80;
global.windowHeight = ((process.stdout.rows || 30) - 1) * 2;

process.stdout.write("\x1b[?25l");
process.on("exit", () => process.stdout.write("\x1b[?25h"));
process.on("SIGINT", () => {
process.stdout.write("\x1b[?25h");
process.exit();
});

const p5b = new P5b({
width: windowWidth,
height: windowHeight,
framerate: 60,
sketchPath: sketchPath
});

function frameToAnsi(buf, w, h) {
const parts = ["\x1b[H"];
for (let y = 0; y < h; y += 2) {
for (let x = 0; x < w; x++) {
const t = (y * w + x) * 4;
const b = ((y + 1) * w + x) * 4;
parts.push(
`\x1b[48;2;${buf[t]};${buf[t+1]};${buf[t+2]}m\x1b[38;2;${buf[b]};${buf[b+1]};${buf[b+2]}m▄`
);
}
parts.push("\x1b[0m\n");
}
return parts.join("");
}

p5b.on("frame", (buffer) => {
process.stdout.write(frameToAnsi(buffer, windowWidth, windowHeight));
});

p5b.run();
62 changes: 62 additions & 0 deletions examples/sketch-rings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Concentric rings

let hexColors = [
"#95E06C",
"#FF785A",
"#094D92",
"#BBACC1"
];

let bgIdx = 0;
let fgIdx = 1;

function setup() {
createCanvas(windowWidth, windowHeight);
noStroke();
}

function draw() {
background(hexColors[bgIdx]);

const maxRadius = max(
sqrt(2 * windowWidth * windowWidth),
sqrt(2 * windowHeight * windowHeight)
);

const radius = (2 * frameCount) % maxRadius;

const cX = windowWidth / 2;
const cY = windowHeight / 2;

fill(hexColors[fgIdx]);
ellipse(cX, cY, radius * 1.5);

fill(hexColors[bgIdx]);
ellipse(cX, cY, radius * 1.2);

fill(hexColors[fgIdx]);
ellipse(cX, cY, radius);

fill(hexColors[bgIdx]);
ellipse(cX, cY, radius * 0.8);

fill(hexColors[fgIdx]);
ellipse(cX, cY, radius * 0.6);

fill(hexColors[bgIdx]);
ellipse(cX, cY, radius / 2);

fill(hexColors[fgIdx]);
ellipse(cX, cY, radius / 4);

fill(hexColors[bgIdx]);
ellipse(cX, cY, radius / 8);

fill(hexColors[fgIdx]);
ellipse(cX, cY, radius / 16);

if (radius <= 1) {
bgIdx = fgIdx;
fgIdx = (fgIdx + 1) % hexColors.length;
}
}
26 changes: 19 additions & 7 deletions p5b.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,25 @@ class P5b extends EventEmitter {
}
}

// Bind windowWidth/windowHeight explicitly - they may not exist on p5 instance
// until createCanvas() is called, but should still be accessible
// Use ?? 0 fallback to match p5.js behavior before createCanvas()
Object.defineProperty(global, "windowWidth", {
get: () => this._myP5?.windowWidth ?? 0,
configurable: true
});
Object.defineProperty(global, "windowHeight", {
get: () => this._myP5?.windowHeight ?? 0,
configurable: true
});

// Execute sketch if provided (overwrites globals)
if (this.sketchPath) {
const absoluteSketchPath = path.resolve(this.sketchPath);
const code = fs.readFileSync(absoluteSketchPath, "utf8");
vm.runInThisContext(code, { filename: absoluteSketchPath });
}

global._resolveAssetPath = function(sketchPath, filePath) {
const assetDir = sketchPath
? path.dirname(path.resolve(sketchPath))
Expand Down Expand Up @@ -665,13 +684,6 @@ class P5b extends EventEmitter {
global.preload = this.preload;
global.setup = this.setup;
global.draw = this.draw;

// Execute sketch if provided (overwrites globals)
if (this.sketchPath) {
const absoluteSketchPath = path.resolve(this.sketchPath);
const code = fs.readFileSync(absoluteSketchPath, "utf8");
vm.runInThisContext(code, { filename: absoluteSketchPath });
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@10k24/p5b",
"version": "1.2.0",
"version": "1.2.1",
"description": "Run p5.js sketches in Node.js and stream RGBA pixel buffers",
"author": "10k24",
"main": "p5b.js",
Expand Down
8 changes: 8 additions & 0 deletions templates/README.dot
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ npm install @10k24/p5b

## Quick Start

**Inline mode** — define setup/draw callbacks directly:

```javascript
{{=it.stubs.constructor}}
```

**Sketch file mode** — load a `.js` sketch file (defines `setup`/`draw` as globals):

```javascript
{{=it.stubs['constructor-sketchpath']}}
```

## API

### Constructor
Expand Down
14 changes: 14 additions & 0 deletions templates/stubs/readme-constructor-sketchpath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { P5b } = require("@10k24/p5b");

const p5b = new P5b({
width: 400,
height: 400,
fps: 60,
sketchPath: "./my-sketch.js"
});

p5b.on("frame", (buffer) => {
// Process frame buffer
});

p5b.run();
9 changes: 9 additions & 0 deletions test/fixtures/sketches/window-size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Test sketch: access windowWidth at top-level
const w = windowWidth;
const h = windowHeight;
global.window_width_at_top_level = w;
global.window_height_at_top_level = h;

function setup() {
global.canvas_width = createCanvas(w || 100, h || 100).width;
}
22 changes: 13 additions & 9 deletions test/integration/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -574,15 +574,19 @@ describe("P5b Integration - Buffer Analysis", () => {
});
});

it("should throw when sketchPath file does not exist", () => {
expect(() => {
new P5b({
width: 32,
height: 32,
fps: 30,
sketchPath: "/nonexistent/path/sketch.js"
});
}).toThrow();
it("should throw when sketchPath file does not exist", (done) => {
const p5b = new P5b({
width: 32,
height: 32,
fps: 30,
sketchPath: "/nonexistent/path/sketch.js"
});
p5b.on("error", (err) => {
expect(err.error.message).toContain("ENOENT");
p5b.stop();
done();
});
p5b.run();
});

it("should emit error event when setup throws", (done) => {
Expand Down
25 changes: 9 additions & 16 deletions test/integration/sketches.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ describe("P5b Real Sketch - Shapes", () => {

p5b.run();
});
});

it("should render non-black pixels in shapes sketch", (done) => {
describe("P5b Real Sketch - window dimensions", () => {
it("windowWidth and windowHeight should be accessible at top-level in sketchPath mode", (done) => {
const p5b = new P5b({
sketchPath: path.join(sketchesDir, "shapes.js"),
width: 64,
height: 64,
sketchPath: path.join(sketchesDir, "window-size.js"),
width: 200,
height: 200,
fps: 30
});

Expand All @@ -48,17 +50,8 @@ describe("P5b Real Sketch - Shapes", () => {
});

p5b.on("frame", (buffer) => {
let hasColor = false;
for (let i = 0; i < Math.min(buffer.length, 256); i += 4) {
const r = buffer[i];
const g = buffer[i + 1];
const b = buffer[i + 2];
if (r > 0 || g > 0 || b > 0) {
hasColor = true;
break;
}
}
expect(hasColor).toBe(true);
expect(global.window_width_at_top_level).toBe(0);
expect(global.canvas_width).toBe(100);
p5b.stop();
done();
});
Expand All @@ -67,7 +60,7 @@ describe("P5b Real Sketch - Shapes", () => {
});
});

describe("P5b Real Sketch - Graphics Pooling", () => {
describe("P5b Real Sketch - Shapes (colored)", () => {
it("should render graphics sketch with createGraphics successfully", (done) => {
const p5b = new P5b({
sketchPath: path.join(sketchesDir, "graphics.js"),
Expand Down
Loading