npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pts-render

v0.1.2

Published

Render portable and classic Pts scenes to PNG and SVG with a deterministic Node.js CLI and API

Readme

pts-render

Render Pts.js scenes to PNG, JPEG, WebP, raw RGBA, and SVG in Node.js. The package uses skia-canvas and provides:

  • the pts-render command for agents, scripts, and build pipelines;
  • a deterministic renderScene() API;
  • a portable JavaScript format that runs in Node and a normal browser CanvasSpace;
  • a compatibility loader for a tested subset of unchanged Pts demos; and
  • the lower-level SkiaCanvasSpace and SkiaCanvasForm adapter.

Pts itself is never patched. All Node integration lives in this repository.

Status

This is the 0.1.2 release for published Pts 1.0.3. Both the npm package and the installed command are named pts-render.

Command line

Node.js 20 or later is required. You can render a file without installing the package globally:

npx pts-render drawing.mjs

For unattended or reproducible use, suppress npm's install prompt and pin the version:

npx --yes [email protected] drawing.mjs --json

The package installs one executable named pts-render, so the persistent form is:

npm install --global pts-render
pts-render drawing.mjs

skia-canvas downloads its prebuilt native binary in an install script. npm 11 warns that the script is "not yet covered by allowScripts"; it still runs, and npm install --global --allow-scripts=skia-canvas pts-render silences the warning. Without the script, rendering fails at startup with a missing skia.node.

With npx or a global install, the runner's copy of Pts lives in the npm cache or global prefix, so a render file cannot import it directly. Use the Pts namespace passed to run instead; it is the runner's own instance. Direct imports work only when the file resolves pts to the same installation, such as a project that installed pts-render locally.

From this checkout

pnpm install --frozen-lockfile
pnpm build

node dist/cli.mjs examples/basic-card.mjs \
  --json

In a shell without a TTY, such as an agent or CI job, set CI=true so pnpm can replace an existing node_modules without prompting.

With no --out, this creates a unique PNG such as pts-output/basic-card-550e8400-e29b-41d4-a716-446655440000.png. To choose names or generate multiple formats explicitly:

node dist/cli.mjs examples/basic-card.mjs \
  --out out/basic-card.png \
  --out out/basic-card.svg \
  --json

Output directories are created as needed. Existing files are protected unless --force is supplied. A trailing slash selects a directory and asks the CLI to generate the filename; an existing directory passed without a trailing slash is rejected with OUTPUT_TARGET_INVALID and a hint about the trailing-slash form. Symlinks, other non-regular targets, and duplicate canonical paths are also rejected before scene execution. --background and --matte must be colors that skia-canvas can parse (named colors, transparent, hex, rgb(), hsl(), or hwb()); anything else is a usage error rather than a silently wrong color. Running pts-render with no arguments is also a usage error; --help prints the option list.

Write a render file

A render file is ordinary JavaScript with one default-exported function. The CLI calls run once so the file can register its normal Pts drawing callback:

function run({ Pts, space, form, params }) {
  const { Circle } = Pts;
  const radius = Number(params.radius ?? 40);

  space.add((time) => {
    form
      .fillOnly("#67e8f9")
      .circle(Circle.fromCenter(space.pointer, radius + time / 1000));
  });
}

export default run;

Render it in Node:

pts-render circles.mjs \
  --size 640x360 \
  --pointer 320,180 \
  --time 1000 \
  --param radius=48 \
  --out circles.png

--param values are parsed as JSON when they are valid JSON and kept as strings otherwise, so --param radius=48 yields the number 48, --param flag=true a boolean, --param 'obj={"a":1}' an object, and --param label=hello the string "hello". The example above still coerces with Number() so the same file also accepts string parameters from a --params file or a browser mount.

The canvas is 800x600 with a transparent background when neither the file nor the command specifies them. --size and --background override file values. To keep defaults in the file, export a configured object instead:

export default {
  width: 640,
  height: 360,
  background: "#10131a",
  run,
};

pts-render/scene exports defineScene(), a type-level identity helper. It gives the configured object editor completion and type checking (for example, width and height must appear together and unknown keys are flagged) and returns it unchanged; runtime validation still happens when the file is rendered or mounted:

import { defineScene } from "pts-render/scene";

export default defineScene({ width: 640, height: 360, run });

It has no Node or skia-canvas dependency, so the same file works in the browser. A render file that imports it must be able to resolve pts-render from its own location; with npx or a global install, no local package exists, so prefer a plain object or function export there.

Quick testing in browser

Mount the exact same function or configured object in a browser:

import { mountScene } from "pts-render/browser";
import run from "./circles.mjs";

const mounted = await mountScene(run, {
  target: "#pt",
  params: { radius: 48 },
});

// Later:
await mounted.dispose();

pts-render/browser has no Node built-in, skia-canvas, or native dependency edge in its module graph. It creates a real Pts CanvasSpace and CanvasForm, preserves the same initial resize/start ordering as the Node runner, and owns input binding and playback. It intentionally rejects SVGSpace targets, and it rejects a target selector that matches no element instead of letting Pts append a new container to the page.

The module imports the bare specifiers pts-render/browser and pts, so the page needs a bundler, an import map, or a CDN that resolves them. Without a build step, an import map over a local install works:

<script type="importmap">
  {
    "imports": {
      "pts-render/browser": "./node_modules/pts-render/dist/browser.mjs",
      "pts": "./node_modules/pts/dist/index.mjs"
    }
  }
</script>

A CDN that rewrites dependencies, for example https://esm.sh/[email protected]/browser, also resolves pts on its own.

mountScene(scene, options) accepts:

| Option | Purpose | | ------------------------ | ------------------------------------------------------------------------ | | target | Selector or element for the container (required) | | size | { width, height } for a fixed canvas; overrides file values | | background | Canvas background; overrides the file value | | resize | Follow the container size; defaults to true only when no size is known | | retina | Scale for the device pixel ratio; default true | | params | Parameter object passed to run | | assetBaseURL | Base for assets.*; defaults to the page | | bindMouse, bindTouch | Pointer input; default true | | autoplay | Start playback after mounting; default true | | signal | AbortSignal that cancels mounting or disposes the scene | | onWarning | Callback for warnings that the result also collects |

It resolves to { space, form, warnings, dispose }. When the options or the file specify a size, the canvas keeps that size like the Node renderer's; an unsized mount fills its container and follows it. Pts measures the container to size the canvas, so give each mount its own container that holds nothing else. The created canvas is styled display: block so that a container without an explicit height does not grow a few pixels per resize.

Delta from a standard web demo

Most drawing and player code can stay unchanged. The meaningful differences are at the edges:

| Standard browser demo | Render file | | --------------------------------------------- | ------------------------------------------------- | | Pts.quickStart("#pt", bg) | file or CLI background | | container determines initial size | file, --size, or the 800x600 default | | globals such as Circle, space, and form | destructure Pts, space, and form in run | | space.bindMouse().bindTouch().play() | browser mount or CLI runner owns scheduling/input | | browser-relative Img or font loading | assets.image() and assets.font() | | live browser clock | explicit --time or --frame simulation |

The callback itself remains normal Pts code. See examples/basic-card.mjs for the canonical example.

CLI

pts-render <source> [--out <destination>] [options]
pts-render render <source> [--out <destination>] [options]

Core options:

| Option | Purpose | | ---------------------------------- | ---------------------------------------------------------------------- | | -o, --out <path> | Exact file or trailing-/ directory; repeatable; - writes to stdout | | --format <name> | Generated, ambiguous, or stdout format; required with --out - | | --loader <auto\|scene\|pts-demo> | Source interpretation | | --size <width>x<height> | Canvas size; overrides file values; otherwise defaults to 800x600 | | --background <color> | Canvas background; overrides file value; otherwise transparent | | --pointer <x>,<y> | Initial pointer without dispatching an action | | --time <ms> | One direct frame at a timestamp | | --frame <index> --fps <rate> | Simulate frames 0 through the index | | --events <file.json> | Replay a deterministic pointer/action/resize timeline | | --seed <string> | Seed Pts and isolated JavaScript random streams | | --params <file.json> | Base parameter object | | --param <key=value> | Parameter override; repeatable | | --density <integer> | Raster output scale | | --quality <0..1> | JPEG/WebP quality | | --matte <color> | Color beneath raster transparency | | --text-mode <preserve\|outline> | SVG text policy | | --asset-root <path\|URL> | Base for scene/legacy assets | | --allow-net | Allow HTTP(S) through the asset service | | --font <family=path> | Register a font; repeatable by family | | --renderer <cpu\|auto\|gpu> | Native renderer policy | | --timeout <ms> | Hard worker deadline | | --limit <name=value> | Resource-limit override; repeatable | | --force | Replace existing outputs | | --json | Emit exactly one machine-readable result record | | --quiet | Suppress human diagnostics and captured scene logs | | --debug | Include stack information in JSON errors |

Defaults and generated outputs

Only the source is required. When --out is omitted, the CLI creates one PNG under pts-output/ in the current working directory:

pts-output/<sanitized-source-stem>-<render-id>.png

The render ID is a full random UUID, so simultaneous agent processes do not choose the same path. The absolute committed path and the same renderId are reported in --json output. Human-readable mode prints the path to stderr. --quiet intentionally suppresses that message, so callers using a generated name should use --json when they need to discover it programmatically.

--format selects the generated extension:

pts-render scene.mjs --format svg --text-mode outline --json
pts-render scene.mjs --out renders/ --format webp --json

The second form creates a unique file inside renders/. A bare --out remains an error; omit it to use pts-output/. Multiple outputs still require repeated explicit --out destinations. --out - streams one output to stdout and has no extension to infer from, so it always needs --format:

pts-render scene.mjs --out - --format png > scene.png

Other effective defaults are:

| Setting | Default or fallback | | ---------------------------- | ------------------------------------------------------ | | loader | Syntax-aware auto selection | | logical size | File dimensions, otherwise 800x600 | | background | File/classic-demo background, otherwise transparent | | pointer | Canvas center | | clock | One direct frame at 0ms with a zero delta | | simulated frame rate | 60fps when --frame is selected | | events | Empty timeline | | random seed | Unset; randomness is not made deterministic implicitly | | parameters | Empty object | | raster density | 1 | | JPEG/WebP quality | 0.92 from skia-canvas | | raster matte | Unset | | SVG text mode | preserve | | assets | Scene-relative, with network access disabled | | additional fonts | None | | renderer | CPU | | timeout | 30000ms | | overwrite | Disabled | | JSON, quiet, and debug modes | Disabled |

--limit inherits the exported DEFAULT_RENDER_RESOURCE_LIMITS values. Flags such as --background, --seed, and --font are optional overrides rather than arbitrary implicit values.

--time 5000 invokes one frame with (time, delta) = (5000, 0). It does not invent intermediate frames. Stateful sketches should use, for example, --frame 300 --fps 60, which invokes frames 0 through 300 with timestamps computed from their integer indexes.

Deterministic input

An event file is versioned and strictly validated:

{
  "schemaVersion": 1,
  "events": [
    { "at": 0, "type": "move", "x": 100, "y": 100 },
    { "at": 500, "type": "down", "x": 100, "y": 100, "buttons": 1 },
    { "at": 800, "type": "drag", "x": 220, "y": 160, "buttons": 1 },
    { "at": 900, "type": "up", "x": 220, "y": 160 },
    { "at": 1000, "type": "resize", "width": 800, "height": 600 }
  ]
}

Events are stably ordered by at and source order. --pointer changes initial state only; it does not synthesize a move callback. Each at is a millisecond timestamp on the selected render clock. Before a frame, the runner dispatches all events whose timestamp is at or before that frame and then calls the normal Pts action(type, x, y, event) handlers. Pointer actions update the synthetic pointer state; resize events update the canvas and invoke its resize lifecycle.

With --time 1000, applicable events are replayed through 1000ms and then one frame is rendered. With --frame 60 --fps 60, intermediate frames are rendered and each event is applied before the first frame boundary that reaches it. Events after the final render time are not dispatched. The timeline supports Pts pointer/action types and resize, not keyboard input or an emulated browser event loop.

Agent-facing JSON

Success writes one JSON object to stdout:

{
  "ok": true,
  "schemaVersion": 1,
  "renderId": "550e8400-e29b-41d4-a716-446655440000",
  "loader": "scene",
  "width": 640,
  "height": 360,
  "outputs": [
    {
      "format": "png",
      "bytes": 12345,
      "sha256": "...",
      "path": "/absolute/path/pts-output/circles-550e8400-e29b-41d4-a716-446655440000.png"
    }
  ]
}

The example is abridged: the real record also carries source, render (clock facts), random (seed facts), runtime (Node, Pts, skia-canvas, and renderer facts), warnings, captured logs, and durationMs. renderScene() returns the same fields except renderId, which the CLI assigns for its generated filenames. Failures use { "ok": false, "error": { "code", "phase", "message", "cause"? } } plus schemaVersion and, once assigned, renderId, and retain a bounded cause chain without stacks by default. --debug adds worker and cause stacks. Exit groups are stable: 2 for usage/validation, 3 for source or compatibility, 4 for render-file initialization/input/frame/cleanup, 5 for export/commit, 6 for native/environment, 124 for timeout, and 130 for SIGINT. --json cannot be combined with --out -.

Programmatic one-shot API

import { renderScene } from "pts-render";

const result = await renderScene("./circles.mjs", {
  pointer: [320, 180],
  seed: "release-card",
  render: { mode: "frame", frame: 60, fps: 60 },
  outputs: [
    { format: "png" }, // returned as Buffer
    {
      format: "svg",
      path: "out/circles.svg",
      textMode: "outline",
    },
  ],
  timeoutMs: 30_000,
});

const png = result.outputs[0].buffer;

Each call starts a fresh worker. Outputs are encoded in request order from the same final canvas. All artifacts and Buffer limits are verified before any path is committed. Each path commit is atomic, but multiple paths are not a single filesystem transaction: a later commit failure reports already committed paths in the error details. Omitted paths become bounded Buffers in the parent. The result includes dimensions, clock facts, random facts, renderer facts, warnings, hashes, captured logs, and optional scene metadata.

Render files must have a default export containing either a run function or a configured object with a run property. .mjs, package-appropriate .js, and .cjs (module.exports or exports.default) are supported. Direct ESM imports from pts must resolve to the runner's exact peer installation and module instance; otherwise rendering fails with PTS_INSTANCE_MISMATCH. A scene's require("pts") selects Pts's separate CommonJS implementation and is rejected, even from the same installation, because it would split Pts state and seeded randomness. CommonJS scenes should use context.Pts or await import("pts"). Avoid separate Pts bundles or transitive dependencies that introduce another Pts instance; the preflight checks direct literal references, not an arbitrary dependency graph. Using context.Pts is the simplest portable choice.

Assets and fonts

Portable code uses one API in Node and the browser:

export default {
  assetBaseURL: import.meta.url,
  async run({ space, form, assets }) {
    const image = await assets.image("./artwork.webp");
    await assets.font({
      family: "Project Sans",
      sources: ["./project-sans.woff2"],
    });

    space.add(() => {
      form.image(
        [
          [0, 0],
          [320, 180],
        ],
        image,
      );
      form.font(24, "normal", "normal", 1.2, "Project Sans");
    });
  },
};

Node resolves assets relative to the source module unless assetBaseURL or --asset-root overrides it. Network access is denied by default, redirects and bytes are bounded, and registered resources live only for the render worker. Local files and data: URLs work without network permission. An explicit asset root confines file resolution through assets.* both lexically and after symlink resolution; explicit --font paths still resolve from the caller's working directory. Failed image loads can be retried; identical font registrations are idempotent, while reusing a family with different sources is an error. Diagnostics redact URL credentials, query strings, and data payloads. The browser uses HTMLImageElement, FontFace, and the same scene-relative URL rules; browser image failures also identify loading/CORS policy without exposing URL credentials.

SVG output

SVG is available at both levels:

await space.toFile("drawing.svg", { outline: false });
const svg = await space.toBuffer("svg", { outline: true });

In the CLI/API, textMode: "preserve" retains text where Skia can do so; "outline" converts glyphs to paths for more self-contained geometry. Embedded raster images remain raster data inside the SVG. Preserved text names the font Skia actually used, not the CSS family the scene asked for: a font registered as "Project Sans" from a DejaVu file is written as font-family="DejaVu Serif", and a generic sans-serif becomes the resolved system face. A viewer without that font substitutes its own, so outline is the portable choice when the SVG leaves the machine that rendered it.

Skia's SVG encoder omits Canvas-filtered drawing commands. If a scene sets a non-none Canvas filter, the adapter preserves the complete drawing by embedding a PNG snapshot in the SVG at logical resolution (density 1). The CLI/API reports SVG_RASTER_FALLBACK; the low-level adapter exposes space.svgRasterFallback. All content, including text, is rasterized in this case regardless of textMode. Unfiltered scenes retain normal vector SVG output. This conservative fallback remains enabled for the canvas lifetime, even if the scene later resets the filter.

Filters, compositing, text, fonts, and backend details can differ from a browser Canvas implementation, so visual compatibility is tested semantically rather than by byte equality.

Low-level adapter

Use SkiaCanvasSpace directly when a worker and scene schema are unnecessary:

import { Circle } from "pts";
import { SkiaCanvasSpace } from "pts-render";

const space = new SkiaCanvasSpace(640, 360, {
  background: "#10131a",
  renderer: "cpu",
});
const form = space.getForm();

space.setPointer([320, 180]);
space.add((time) => {
  form.fillOnly("#67e8f9").circle(Circle.fromCenter(space.pointer, 40));
});
space.renderFrame(1000);

await space.toFile("circle.png", { density: 2 });
await space.toFile("circle.svg", { outline: false });
space.dispose();

The adapter is intentionally explicit:

  • renderFrame() is deterministic and non-reentrant;
  • setPointer() and dispatchAction() provide synthetic input;
  • resize() preserves explicitly supplied pointer state;
  • exports do not advance the scene;
  • mutation is blocked while asynchronous export is pending; and
  • browser scheduling methods such as play() throw with a renderFrame() alternative.

CPU is the default. renderer: "auto" permits Skia fallback; "gpu" is strict and fails if the actual engine is not GPU-backed. Allocation limits are checked before native Canvas construction.

Trust and resource boundary

Render files and classic demos execute arbitrary JavaScript. The worker and VM improve lifecycle control, diagnostics, and timeout behavior; they are not a security sandbox for hostile code. Render only trusted sources, especially when network assets are enabled.

Default limits cover source/input bytes, logical and raster pixels, frames, events, outputs, assets, artifact bytes, returned Buffers, metadata, logs, and seed size. A timeout or SIGINT kills the worker. Scene code cannot choose output paths because output policy remains in the parent process.

Verification

pnpm check

The release gate includes formatting, lint, source types, coverage-enforced runtime tests, real Chromium mounting/abort/image tests, CLI black-box tests, all 24 compatibility cases, strict Node16/NodeNext consumer types, a clean-source package build and fresh npm consumer install, and package/export validation. The packed consumer installs the registry peer and native runtime dependencies without borrowing development symlinks.

pnpm test:coverage writes HTML and JSON reports to coverage/. Its thresholds are 82% lines, 80% statements, 85% functions, and 70% branches for instrumented runtime modules. Browser and process entry points are exercised by separate end-to-end suites, not included in those percentages.

Compatibility fixtures are vendored unchanged from the exact release source, so the compatibility suite needs neither network access nor a neighboring Pts checkout. To verify an optional external checkout of the same release:

pnpm build
pnpm test:compatibility /path/to/pts-at-6f666fa

The compatibility verifier checks the commit when Git metadata is present, checks source and asset hashes, decodes output to verify dimensions and visible content, verifies required SVG image content, writes output to its own temporary directory, and compares the Pts checkout's status/diff fingerprints before and after. The installed runtime remains npm [email protected].

Publishing checklist

pnpm install --frozen-lockfile
pnpm check
pnpm audit --prod
pnpm pack

Require green CI results for Linux, macOS, and Windows on Node 20, 22, and 24 before publishing. Confirm the package name/version, npm ownership, and release notes, then publish from this checkout with npm publish. prepublishOnly reruns pnpm check; prepack always rebuilds the distributed files, including when packing a clean checkout. Browser tests require Chromium; install it with pnpm exec playwright install chromium if it is not already available. No package is published by the verification commands.

Rendering unchanged Pts demos

The classic compatibility loader evaluates a browser demo once in an isolated worker VM and supplies Pts globals, Pts.quickStart, a private CanvasSpace facade, deterministic lifecycle methods, and narrowly supported Canvas globals:

pts-render path/to/pts/demo/circle.intersectCircle2D.js \
  --loader auto \
  --size 640x360 \
  --pointer 320,180 \
  --out circle.png \
  --out circle.svg

Auto-selection is syntax-aware. It parses without evaluation and chooses the classic loader only for strong markers such as Pts.quickStart, window.demoDescription, or a known Space constructor. A source containing both module exports and classic markers is rejected as ambiguous; choose --loader scene or --loader pts-demo explicitly.

Classic support is manifest-based, not a blanket “all demos” claim. For Pts 1.0.3, the checked matrix contains:

  • 17 supported demos;
  • 4 supported-with-input demos;
  • 1 partial editable/pixel Img demo; and
  • 2 browser-only/audio demos marked not applicable.

The supported set covers geometry, gradients, text, compositing, resize, actions, Tempo, physics, UI, direct CanvasSpace construction, non-editable image loading, and image patterns. Every source and image asset is SHA-256 pinned; every supported entry produces both PNG and SVG with visible content. The filtered-image demo uses the explicit SVG raster fallback described below. See compatibility/pts-revamp.json.

Root-relative legacy image paths require an explicit root instead of guessing the machine filesystem:

pts-render path/to/pts/demo/guide.image_load.js \
  --asset-root path/to/pts \
  --out image-demo.png

Editable Img canvases, pixel manipulation, audio, HTMLSpace, and SVGSpace fail at a named compatibility boundary. SVG file export remains available because it comes from the Canvas recording, not SVGSpace.

Current limitations

  • Classic compatibility is manifest-scoped; it is not a DOM emulator.
  • Editable/pixel-oriented legacy Img, offscreen Canvas helpers, HTMLSpace, SVGSpace, audio, microphone, video, and arbitrary browser timers are not supported.
  • SVG records Canvas output and does not provide semantic SVGSpace nodes.
  • Native deployment, fonts, and platform rendering differences still need to be treated like any other skia-canvas application.