scenic-draft-extras
v0.22.1
Published
Extra scenic-draft primitives and materials — hexagonal and triangular prisms, chain links, quadrilateral faces and fifty-odd more surface presets — written against the library's own extension points.
Maintainers
Readme
scenic-draft-extras
Five shapes scenic-draft no
longer carries and fifty-odd materials it never did — both written against what
the core library already offers rather than against a change to it.
import { backgrounds, materials, paint, render, rotateY, translate, union } from 'scenic-draft';
import { hexPrism, link, triPrism } from 'scenic-draft-extras';
render(canvas, {
background: backgrounds.dusk,
scene: union(
paint(hexPrism(0.7, 0.9), materials.brass),
translate(rotateY(triPrism(0.5, 0.4), Math.PI / 6), [1.8, 0, 0]),
translate(link(0.5, 0.15, 0.35), [-1.8, 0, 0]),
),
});Each of these was a primitive of the core library's own, with a case in its
compiler and a variant in its SceneNode union. They live here instead because
the shapes a scene might want are endless and a compiler's switch is not: a
hexagonal prism, a triangular prism, a chain link and a quadrilateral face are
all worth having and none of them is load-bearing. So they are also the proof
that custom() is enough to add a primitive with — the distance function, the
validation and the name, in a package a scene reaches for only if it wants one.
Nothing about them is special from the core library's side. Every builder
returns a plain SceneNode, so the whole vocabulary above it works untouched:
translate, rotate, repeat, paint, the smooth booleans,
scenic-draft/probe's checkField, and scenic-draft-standalone's exporters.
extraMaterials is the same argument made about surfaces. materials in the
core library is short on purpose — every name in it is one a reader has to be
able to hold in their head — and a preset is plain data, so a second library of
them needs no change to anything:
import { paint, sphere } from 'scenic-draft';
import { extraMaterials } from 'scenic-draft-extras';
paint(sphere(1), extraMaterials.malachite);Install
npm install scenic-draft-extras
# or: pnpm add scenic-draft-extrasThe package carries scenic-draft and scenic-draft-fluent at exact versions,
so the compiler that reads these fields is the one they were written for. It
ships ESM with TypeScript declarations.
The shapes
| | |
| --- | --- |
| hexPrism(radius, height) | A hexagonal prism along the y axis. radius is the apothem — centre to the middle of a flat — and height the half-length along the axis. |
| triPrism(radius, height) | An equilateral triangular prism along the y axis. radius is the circumradius, so the sides are radius * √3 long; the cross-section sits in the x–z plane with one corner along +z. |
| link(major, minor, height) | A chain link: a torus stood on edge in the x–y plane and drawn apart along y. The straight part is 2 * height long and the whole link 2 * (height + major + minor). |
| quad(a, b, c, d, thickness) | A quadrilateral face through four corners given in order around it, thickness out from their plane on each side. |
| quads(faces, thickness) | A whole list of those as one shape: a folded plate, a set of panels, a quad mesh. |
Every measure is a half-measure, as everywhere in scenic-draft: a height is
the half-length, a thickness the half-thickness. A face is a sheet 2 *
thickness thick with its edges and corners rounded to that radius, since what
is measured is the distance to the face itself.
A quad has three promises to keep that a triangle does not — its corners go
round it in order, they lie in one plane, and it turns the same way at every
corner. All three are checked when you build it, because Quilez's quad field
assumes rather than handles them. A face that fails any of them is two
triangles, and scenic-draft's own triangles takes it with the field still
exact.
The materials
extraMaterials is a second preset library, in the shape of the first one: an
object of finished Material values that paint() takes.
| | |
| --- | --- |
| Metals | bronze, titanium, platinum, roseGold, gunmetal, mercury, bluedSteel, tarnishedSilver, rustedIron, verdigris |
| Stone | granite, slate, sandstone, limestone, basalt, onyx, lapisLazuli, malachite, turquoise |
| Gemstones | ruby, sapphire, amethyst, aquamarine, opal |
| Grown | bone, ivory, coral, bark, cork, moss |
| Cloth | wool, silk, corduroy, suede |
| Manufactured | bakelite, acrylic, vinyl, carbonFibre, brick, asphalt, paper |
| Cold and edible | ice, snow, milk, honey, chocolate |
| Lights | neon, ember, lava |
| Thin films | soapBubble, beetle, compactDisc |
Each one is a complete Required<Material>, exactly as a core preset is: every
optional behaviour is spelled out, including the ones switched off, so a preset
dropped inside a patterned or transmissive paint() comes out as itself rather
than inheriting something it never asked for. They mix with the core library's
own — materials.mix(extraMaterials.bronze, materials.iron, 0.4) — and spreading
one to change a field is the expected way to depart from it.
There are no builders here that take a colour. materials.metal, .lacquer,
.wax, .velvet and the rest already cover that half of the problem, and a
bronze that took a colour would be metal with a longer name. What a library
can add is names.
Anything with transmission or subsurface above 0 — the gemstones, the ice,
the honey, the milk — wants a bounces budget of 8 to 12 rather than the
default 6, like materials.glass and materials.jade before them. And the
films (beetle, soapBubble, compactDisc, bluedSteel, opal) have their
colour in the reflection rather than in the pigment, so they want an environment
with some structure to reflect: a gradient with a sun rather than a flat colour.
EXTRA_MATERIAL_NAMES is the names in the order they are declared, for a menu
or an index.
Two entry points
scenic-draft-extras returns plain nodes, for a scene written in
scenic-draft's nested calls. scenic-draft-extras/fluent returns chainable
shapes, for one written in
scenic-draft-fluent's
chain:
import { backgrounds, draft, materials, sphere } from 'scenic-draft-fluent';
import { hexPrism } from 'scenic-draft-extras/fluent';
draft(hexPrism(0.7, 0.9).rotateY(Math.PI / 6).paint(materials.brass).union(sphere(0.4)))
.render(canvas);They build the identical node — a fluent Shape is a SceneNode, with the
chaining methods on its prototype — so the two mix freely in one scene. There is
little else in the fluent entry point: the vocabulary a scene is written in
stays scenic-draft-fluent's, and this package adds five leaves to it rather
than wrapping it again. extraMaterials is re-exported from it unchanged, since
a material is data either dialect paints with and there is no chain to put it
on.
Reading one back
By the time a shape is in a tree it is a custom node like any other, which is
what makes it compile and export like one — and what makes it opaque to anything
that wants to show a scene rather than trace it. readExtra(node) says which
of these shapes a node is and what it was built with, or null for anything
else:
import { hexPrism, readExtra } from 'scenic-draft-extras';
readExtra(hexPrism(0.7, 0.9)); // { name: 'hexPrism', radius: 0.7, height: 0.9 }
readExtra(sphere(1)); // nullIt is recognition rather than a guess: the function's name and its source have
to be this package's, so somebody's own sdLink is not mistaken for a link.
That is what a source printer or a scene editor needs, and it is why the site's
guide can show hexPrism(0.7, 0.9) beside a render of one rather than fourteen
lines of GLSL.
The GLSL
HEX_PRISM_GLSL, TRI_PRISM_GLSL, LINK_GLSL and quadFacesGlsl(count) are
the source each shape is made of, exported because it is the interesting part.
They are Inigo Quilez's distance functions, unchanged from the ones
scenic-draft's compiler used to write inline, and they are a working example
of what a custom primitive looks like — including, in quadFacesGlsl, one
that takes a variable number of arguments by naming a function per arity.
Licence
PolyForm Noncommercial 1.0.0, like every package in this repository. Free for
noncommercial use; a commercial licence is a separate conversation. See
LICENSE in the package.
