scenic-draft-fluent
v0.19.0
Published
A chainable API over scenic-draft: sphere(1).translate([0, 1, 0]).paint(materials.chrome), producing the same plain scene data.
Downloads
653
Maintainers
Readme
scenic-draft-fluent
The scenic-draft scene
vocabulary, as a chain.
import { backgrounds, camera, draft, materials, plane, sphere } from 'scenic-draft-fluent';
draft(
sphere(0.62).translate([1, 2, 3]).paint(materials.chrome).union(plane([0, 1, 0], -1)),
backgrounds.dusk,
)
.withCamera(camera([0, 1.35, -4.9], [0, 0.05, 0]).zoom(2.3).focus(10))
.render(canvas);Same library, same renderer, same compiled shader — read left to right instead of inside out.
Install
npm install scenic-draft-fluent # or: pnpm add scenic-draft-fluentThat is the whole install. scenic-draft comes with it as an exact-version
dependency, and everything it exports is re-exported here — materials,
backgrounds, the background builders, render, buildShader, every type. You
never need to import from both.
The package ships ESM with TypeScript declarations, and needs the same browser
support as the core library: WebGL2 with EXT_color_buffer_float.
The idea
A Shape is a SceneNode. The builders copy the plain node's own
properties onto an object whose prototype holds the chaining methods, so what
you get back has exactly the properties the core builder's return value has:
import { sphere } from 'scenic-draft-fluent';
JSON.stringify(sphere(1)); // {"kind":"sphere","radius":1}
Object.keys(sphere(1)); // ['kind', 'radius']That is what makes the two styles interchangeable rather than merely similar.
A fluent shape can go straight into a core builder, a core node can be picked
up mid-chain, and anything that reads a SceneSpec — including scenic-draft's
own render() — takes either without unwrapping:
import { render, union } from 'scenic-draft'; // the core package
import { fluent, sphere } from 'scenic-draft-fluent';
union(sphere(1).translate([1, 0, 0]), box([1, 1, 1])); // fluent inside core
fluent(someCoreNode).paint(materials.gold); // core inside fluentNothing is validated twice, either. Every method delegates to the matching core builder, so the errors are the same errors, thrown at the same call.
Shapes
Every CSG builder is re-cut to return a chainable Shape, and every operator
that takes a subtree is also available as a method on one. a.union(b) is
union(a, b); a.subtract(b) is subtract(a, b); a.smoothUnion(k, b) is
smoothUnion(k, a, b).
| Method | Equivalent |
| --- | --- |
| .translate(offset) | translate(node, offset) |
| .rotateX(a) .rotateY(a) .rotateZ(a) | rotateX(node, a), … |
| .rotate([x, y, z]) | rotate(node, [x, y, z]) |
| .scale(factor) | scale(node, factor) |
| .elongate([x, y, z]) | elongate(node, [x, y, z]) |
| .mirrorX() .mirrorY() .mirrorZ() | mirrorX(node), … |
| .repeat(spacing, counts?) | repeat(node, spacing, counts) |
| .repeatRadial(count) | repeatRadial(node, count) |
| .displace(amplitude, [x, y, z]) | displace(node, amplitude, [x, y, z]) |
| .twist(rate) .bend(curvature) | twist(node, rate), … |
| .shell(thickness) .grow(amount) | shell(node, thickness), … |
| .paint(material) | paint(node, material) |
| .union(...others) | union(node, ...others) |
| .intersect(...others) | intersect(node, ...others) |
| .subtract(...cuts) | subtract(node, ...cuts) |
| .smoothUnion(k, ...others) | smoothUnion(k, node, ...others) |
| .smoothIntersect(k, ...others) | smoothIntersect(k, node, ...others) |
| .smoothSubtract(k, ...cuts) | smoothSubtract(k, node, ...cuts) |
fluent(node) gives an existing plain node the methods, and isShape(node)
says whether it already has them.
Cameras
camera([0, 1.35, -4.9], [0, 0.05, 0]).zoom(2.3).focus(10);camera(position, target?) starts the chain; .position(), .target(),
.projection(), .perspective(), .orthographic(), .zoom(), .height(),
.aperture() and .focus() each return a new camera, so a base camera can be
framed several ways without being copied by hand.
A Camera is the one thing here that is not structurally its spec, and it
cannot be: every method is named after the field it sets, and a property cannot
be both a number and a function. Rather than rename zoom to something
clumsier, the chain stays readable and the spec is fetched with .toSpec().
In practice you rarely need it — everything in this package that takes a camera
takes either form:
draft(scene, backgrounds.studio).withCamera(camera([0, 1, -5]).zoom(2));
buildShader({ scene, background, camera: camera([0, 1, -5]).zoom(2) });
render(canvas, { scene, background, camera: camera([0, 1, -5]).zoom(2) });scenic-draft's own render and buildShader want the plain spec, so pass
camera(...).toSpec() when you call those directly.
Drafts
draft(scene, background) starts a whole spec. Like Shape, a Draft is a
SceneSpec — the same four fields as own properties — so it can be handed to
anything that reads one:
import { draft, backgrounds, camera, fog, materials, sphere } from 'scenic-draft-fluent';
const plate = draft(sphere(1).paint(materials.glass), backgrounds.sunset)
.withCamera(camera([0, 0.6, -4], [0, 0, 0]).zoom(2.4))
.withFog(fog([0.35, 0.2, 0.12], 0.05));
plate.render(canvas, { size: 900, bounces: 12 }); // or: render(canvas, plate)
plate.shader(); // the generated GLSL, no WebGL neededThe setters are with…-prefixed because, unlike the shape methods, the plain
names would collide with the fields they set. .withScene(),
.withBackground(), .withCamera() and .withFog() each return a new draft,
which makes variants of one scene cheap:
const lit = (background) => plate.withBackground(background);
[backgrounds.noon, backgrounds.dusk, backgrounds.nebula].map(lit);Everything else
materials, patterns, bumps, backgrounds, solid, gradient, noise,
customBackground, sun, fog, DEFAULT_MATERIAL, checkField and every type
are re-exported from scenic-draft unchanged — they are plain data with nothing
to chain, or, in checkField's case, a function that already takes one (a
Shape is a SceneNode). render and buildShader are re-exported widened,
so they also accept a fluent camera.
See the scenic-draft README for
what the scene vocabulary means, and
the fluent guide for
worked examples with live renders.
Versioning
This package is released in lockstep with scenic-draft and depends on the
exact version it was published at. The two are one library with two
front doors; a mismatched pair would be two different scene vocabularies.
