Patterns
Generator functions
A generator is just a function that returns a Mesh:
function tree(options?: { height?: number; seed?: number }): Mesh {
const rand = createRng(options?.seed ?? 1)
const trunk = cylinder({ radius: 0.05, height: height * 0.4 })
.translate(0, height * 0.2, 0)
.vertexColor([0.35, 0.22, 0.1])
const canopy = icosphere({ radius: 0.5 })
.translate(0, height * 0.6, 0)
.faceColor(...)
return merge(trunk, canopy)
}
Seeded randomness
Use createRng (wraps alea PRNG) for deterministic generation:
import { createRng } from 'shapecraft'
const rand = createRng(42) // seed → deterministic sequence
rand() // 0-1
rand() // next value
// Derive sub-seeds for noise, jitter, etc.
function subSeed() { return Math.floor(rand() * 2147483647) }
const noise = new UberNoise({ seed: subSeed() })
mesh.jitter(0.05, { seed: subSeed() })
Important: always consume rand() / subSeed() calls in the same order regardless of options, so changing one parameter doesn’t shift the entire random sequence.
Option schemas & presets
Define configurable generators with typed schemas:
import { resolveOptions, type OptionSchema } from 'shapecraft'
const treeSchema = {
height: { type: 'range', default: 2.5, min: 0.5, max: 6, step: 0.1 },
seed: { type: 'integer', default: 1, min: 1, max: 100 },
lean: { type: 'range', default: [0.1, 0.4], min: 0, max: 1 }, // randomized range
showCanopy: { type: 'boolean', default: true },
colors: { type: 'color-array', default: ['#1a5a10', '#2a7518'] },
} satisfies OptionSchema
const presets = {
default: {},
autumn: { colors: ['#c44422', '#d48825'] },
winter: { colors: ['#4a4a4a'], snowColors: ['#e8e8f0'] },
}
function tree(options = {}) {
const rand = createRng(options.seed ?? 1)
const o = resolveOptions(treeSchema, options, presets, rand)
// o.height is a number, o.lean is resolved from [min,max] via rand
}
Numeric defaults can be [min, max] — resolved deterministically from the seed. The editor shows the midpoint.
Scatter points
import { scatterOnSphere } from 'shapecraft'
// N random points on a sphere surface
const points = scatterOnSphere(5, seed, {
radius: 1,
polarMin: Math.PI * 0.3, // avoid top
polarMax: Math.PI * 0.7, // avoid bottom
})
// → [[x,y,z], [x,y,z], ...]
Terrain workflow
const terrain = plane({ size: 20, segments: 100 })
.displaceNoise(fbm({ seed: 42, scale: 0.2, octaves: 5 }), 3)
.vertexColor(heightGradient([
[0, [0.2, 0.5, 0.1]],
[1.5, [0.5, 0.35, 0.2]],
[3, [1, 1, 1]],
]))
Organic shapes with icosphere
// Subdivide → spherize → displace radially (no gaps)
icosphere({ radius: r, subdivisions: 0 })
.subdivideAdaptive(edgeLen)
.spherize(r)
.displaceNoise(noise, r * 0.5, { direction: 'radial' })
.jitter(r * 0.04, { seed })
Use direction: 'radial' instead of 'normal' on non-indexed geometry to avoid gaps between faces.
Branches & tubes
import { tube } from 'shapecraft'
// Tapered branch along a curved path
const path = [[0,0,0], [0.1,0.5,0], [0.3,1,0.1], [0.2,1.5,0]]
const branch = tube(path, (t) => 0.05 * (1 - t * 0.8), 5)
Leaves with loft + thicken
import { loft, thicken } from 'shapecraft'
const leaf = loft({
path: leafPath,
shape: (t) => {
const w = 0.2 * Math.sin(t * Math.PI) // wide in middle
return [[-w, 0], [0, w*0.2], [w, 0]]
},
closedShape: false,
up: [0, 1, 0],
})
const solidLeaf = thicken(leaf, 0.01)