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

scenic-draft-playwright

v0.15.0

Published

Render a scenic-draft scene to an image file from Node, by path-tracing it in a headless browser driven by Playwright.

Readme

scenic-draft-playwright

scenic-draft scenes, path-traced to image files from Node.

import { backgrounds, materials, plane, sphere } from 'scenic-draft-fluent';
import { renderToFile } from 'scenic-draft-playwright';

const SCENE = {
  scene: sphere(1).paint(materials.chrome).union(plane([0, 1, 0], -1)),
  background: backgrounds.dusk,
};

await renderToFile(SCENE, 'renders/chrome.png', { size: 1200, maxFrames: 900, seed: 7 });

One call opens a headless Chromium, traces the scene until the accumulation reaches its sample budget, writes the file and shuts the browser down again — from a script, in CI, on a machine with no screen.

There is no second renderer here. The copy of scenic-draft already in your project is served to the page and its own render() does the tracing, so a saved image and the same scene on a web page come out of the same code.

Install

npm install scenic-draft-playwright scenic-draft-fluent playwright
# or: pnpm add scenic-draft-playwright scenic-draft-fluent playwright
npx playwright install chromium

Both dependencies are peers, and both are bounded from below only:

| Peer | Range | Why | | --- | --- | --- | | scenic-draft-fluent | >= this package's version | It carries scenic-draft, and the copy it resolves is the one that gets served to the page. | | playwright | >=1.40 | Its browsers are your project's to install and version. |

Run the script with anything that runs TypeScript — tsx render.ts, node --experimental-strip-types render.ts — or write it in JavaScript and run it with nothing at all. The package ships ESM with TypeScript declarations.

renderToFile(spec, output, options?)

Traces one scene and writes it. The encoding comes from the file's extension — .png, .jpg, .jpeg or .webp — unless format says otherwise, and any directories in the path are created. Returns { path, format, width, height, frames, bytes }.

renderToBuffer(spec, options?) is the same render handed back as { data: Buffer, … }, for something that is going to upload it rather than keep it. format defaults to png there, there being no filename to read.

| Option | | | | --- | --- | --- | | size | number | Square backing resolution in pixels (default 1024). | | width height | number | The same, when the image is not square. | | maxFrames | number | Samples per pixel to accumulate (default 1200). | | bounces | number | Light-bounce budget (default 6); glass and metal reward 8–12. | | seed | number | Fix the sample sequence. Without one, two runs differ in their noise. | | format | 'png' \| 'jpeg' \| 'webp' | Overrides the extension. | | quality | number | Encoder quality in (0, 1], for jpeg and webp. | | timeout | number | Milliseconds before the render is abandoned (default 300000). | | onProgress | (frames, total) => void | After every accumulated frame, in your process. | | launch | LaunchOptions | Passed to chromium.launch(), on top of this package's own arguments. | | browser | Browser | A Playwright browser to draw in, instead of launching one. | | libraryRoot | string | The scenic-draft package root to serve. Resolved for you by default. |

Everything the scene itself can do is unchanged — this package adds no vocabulary and re-exports none. Build scenes with scenic-draft-fluent (or with scenic-draft directly) and hand them over; either dialect is accepted, and a fluent camera(...) is normalised on the way in.

openStudio(options?)

Launching the browser is most of the cost of the first image and all of the cost of the rest, so a script writing more than one should open a studio and keep it:

import { openStudio } from 'scenic-draft-playwright';

const studio = await openStudio();
try {
  for (const [index, scene] of SCENES.entries()) {
    await studio.renderToFile(scene, `renders/plate-${index}.png`, { size: 900, seed: 3 });
  }
} finally {
  await studio.close();
}

The studio takes the browser options (launch, browser, libraryRoot, timeout) and exposes the same renderToFile / renderToBuffer, each on a page of its own — so each render gets its own WebGL2 context and gives it back. Given an existing browser, the studio never launches or closes anything, which is what makes it usable from inside a Playwright test.

What it actually does

It launches Chromium, serves a page holding one canvas, and hands that page your copy of scenic-draft as ES modules, resolved off disk from the fluent package you installed. The page imports it and calls render(); when the accumulation finishes the canvas is read back and the bytes are written. There is no HTTP server and no temporary directory — the requests are intercepted by Playwright and answered from memory.

The browser is asked for software rasterisation (--use-angle=swiftshader) rather than whatever GPU happens to be there: a build machine usually has none, and one that does would otherwise produce a subtly different image from every machine that does not. Between that and seed, the same script gives the same file everywhere — which is what makes a rendered image something a repository can hold.

The cost is speed. Software path tracing is minutes, not seconds, for a large image at a high sample count, so raise timeout and report progress:

await renderToFile(SCENE, 'renders/poster.png', {
  size: 1600,
  maxFrames: 2000,
  timeout: 30 * 60 * 1000,
  onProgress: (frames, total) => process.stdout.write(`\r${frames}/${total}`),
});

If a machine really does have a GPU worth using, launch goes straight to Playwright and the last occurrence of a Chromium switch wins, so the defaults can be overridden one at a time:

await renderToFile(SCENE, 'renders/plate.png', { launch: { args: ['--use-gl=desktop'] } });

DEFAULT_BROWSER_ARGS and DEFAULT_TIMEOUT are exported, for a script that wants to say what it is changing.

Documentation

Licence

MIT