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.
Maintainers
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-reactThat 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-fluentRendering 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
- Using it in React — this package, with live examples in both dialects
scenic-draftREADME — what the scene vocabulary meansscenic-draft-fluent— the chain, in full- The guide — worked examples with live renders
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.
