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-react

v0.15.0

Published

scenic-draft as a React component: <SceneRenderer spec={scene} /> path-traces a scene onto a canvas you style yourself.

Readme

scenic-draft-react

scenic-draft as a React component.

import { backgrounds, materials, plane, SceneRenderer, sphere } from 'scenic-draft-react';

// Module scope: a stable spec is a stable render.
const SPEC = {
  scene: sphere(1).paint(materials.chrome).union(plane([0, 1, 0], -1)),
  background: backgrounds.dusk,
};

export function Plate() {
  return <SceneRenderer spec={SPEC} size={720} lazy className="aspect-square w-full" />;
}

One element, path-traced progressively on a WebGL2 canvas, started when it scrolls into view and stopped when it unmounts.

Install

npm install scenic-draft-react   # or: pnpm add scenic-draft-react

That is the whole install. scenic-draft and scenic-draft-fluent come with it as exact-version dependencies, and everything both of them export is re-exported here — every primitive, operator, material, background, entry point and type. You never import from more than one package.

React is a peer dependency with a lower bound only (>=18), so the component runs on whatever React the app is already on, including versions that do not exist yet. The package ships ESM with TypeScript declarations, and needs the same browser support as the core library: WebGL2 with EXT_color_buffer_float.

<SceneRenderer />

The component renders a single <canvas> and nothing else — no wrapper, no class names, no styling of its own. Every canvas prop is passed straight through, so size and appearance are entirely yours:

<SceneRenderer spec={SPEC} style={{ width: '100%', aspectRatio: '16 / 9' }} />

| Prop | | | | --- | --- | --- | | spec | SceneSpec \| Draft | The scene. Keep it stable — see below. | | size | number | Square backing resolution in pixels (default 1024). | | width height | number | Backing resolution, when it is not square. | | maxFrames | number | Samples per pixel to accumulate (default 1200). | | bounces | number | Light-bounce budget (default 6). | | seed | number | Fix the sample sequence for a reproducible image. | | lazy | boolean | Wait until the canvas is near the viewport. | | rootMargin | string | How early that is (default '300px'). | | onProgress | (frames, total) => void | After every accumulated frame. | | onDone | (frames) => void | When the accumulation finishes or stops. | | onError | (error) => void | The scene could not be traced at all. | | onContextLost | () => void | The browser reclaimed the context. |

Anything else — className, style, id, aria-*, onClick, a ref to the canvas itself — goes to the element. width and height are the backing store in pixels, not a CSS size; the canvas is displayed scaled to fit whatever box your CSS gives it, so a wide backing store renders a wide image.

Two rules

Give it a stable spec. Hoist it to module scope or wrap it in useMemo. A new object is a new scene: the shader is recompiled and the accumulation restarts from noise.

const spec = useMemo(() => draft(sphere(radius), backgrounds.dusk), [radius]);

Set lazy on a page with several scenes. Each render holds its own WebGL2 context and browsers cap how many can live at once. lazy defers taking one until an IntersectionObserver says the canvas is near the viewport. For a very long page, unmount canvases that have scrolled away — the component frees the GPU resources on unmount and re-accumulates when it comes back.

The callbacks are read as they are at the time they fire, so passing them inline is fine and does not restart the render.

When it cannot render

onError fires when the scene cannot be traced at all — no WebGL2, no EXT_color_buffer_float, a nonsensical frame budget. There is no fallback element, because anything drawn in place of the canvas would be this package's styling rather than yours:

const [failed, setFailed] = useState(false);

return failed ? (
  <p>This browser can’t render the scene.</p>
) : (
  <SceneRenderer spec={SPEC} onError={() => setFailed(true)} />
);

Both dialects, one component

spec takes a plain SceneSpec, a fluent Draft, or an object literal holding a fluent camera(...) — the last is normalised on the way in. That is why there is no SceneRendererFluent: a Draft already is a SceneSpec, so there was never a second thing to render.

import { backgrounds, camera, draft, materials, SceneRenderer, sphere } from 'scenic-draft-react';

// Nested calls — scenic-draft's own style.
const NESTED = {
  scene: paint(translate(sphere(1), [0, 1, 0]), materials.gold),
  background: backgrounds.studio,
  camera: { position: [0, 1.2, -4], zoom: 2.3 },
};

// The chain — scenic-draft-fluent's style. The same value, either way.
const CHAINED = draft(sphere(1).translate([0, 1, 0]).paint(materials.gold), backgrounds.studio)
  .withCamera(camera([0, 1.2, -4]).zoom(2.3));

<SceneRenderer spec={NESTED} />;
<SceneRenderer spec={CHAINED} />;

Both styles are exported from scenic-draft-react under one set of names: the fluent builders, whose return values are the core library's plain scene nodes, and which include every free function the core package exports. If you would rather have exactly one package's surface, two subpaths give it unmixed:

import * as core from 'scenic-draft-react/core'; // exactly scenic-draft
import * as fluent from 'scenic-draft-react/fluent'; // exactly scenic-draft-fluent

Rendering without the component

render(canvas, spec, options) is re-exported unchanged, for a canvas this component does not own. It returns a handle whose stop() you must call on teardown — which is all the component does:

useEffect(() => {
  const handle = render(canvasRef.current, spec, { size: 720 });
  return () => handle.stop();
}, [spec]);

buildShader(spec) is here too, and needs no GPU at all: it returns the whole generated fragment shader as a deterministic string.

Documentation

Versioning

This package is released in lockstep with scenic-draft and scenic-draft-fluent, and depends on the exact versions it was published alongside. The three are one library with three front doors.