Skip to content

Latest commit

 

History

History
720 lines (553 loc) · 26.6 KB

File metadata and controls

720 lines (553 loc) · 26.6 KB

p5.tree

npm version

Render pipeline for p5.js v2pose and camera interpolation, space transforms, frustum visibility, HUD, post-processing pipe, picking, and declarative control panels.

A non-Euclidean geometry cube with faces showcasing teapot, bunny, and Buddha models.


Tracks

A unified factory creates either a PoseTrack (object animation) or a CameraTrack (camera keyframe path).

const track = createPoseTrack()       // PoseTrack — animates any object
const track = createCameraTrack()     // CameraTrack — binds to the current camera
const track = createCameraTrack(cam)  // CameraTrack — binds to a specific camera

PoseTrack — object animation

Stores { pos, rot, scl } keyframes. Interpolates position with cubic Hermite (auto-computed centripetal Catmull-Rom tangents by default), rotation with slerp or nlerp, scale with linear.

const track = createPoseTrack()
const out   = { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }

track.add({ pos:[-150, 0, 0], rot:[0,0,0,1], scl:[1,1,1] })
track.add({ pos:[ 150, 0, 0], rot:[0,0,0,1], scl:[1,1,1] })
track.play({ loop: true, duration: 60 })

function draw() {
  background(20)
  if (track.playing) {
    push()
    applyPose(track.eval(out))
    box(60)
    pop()
  }
}

add() accepts flexible specs. Top-level forms:

track.add({ pos, rot, scl })                      // explicit TRS — rot accepts any form below
track.add({ pos, rot, scl, tanIn, tanOut })        // with Hermite tangents (vec3, optional)
track.add({ mMatrix: mat4 })                       // decompose model matrix into TRS
track.add([ spec, spec, ... ])                     // bulk

tanIn is the incoming position tangent at this keyframe; tanOut is the outgoing tangent. When only one is given, the other mirrors it. When neither is given, centripetal Catmull-Rom tangents are auto-computed — identical to the default smooth behavior.

track.add({ pos:[0,0,0] })                                      // auto tangents
track.add({ pos:[100,0,0], tanOut:[0,50,0] })                   // leave heading +Y
track.add({ pos:[200,0,0], tanIn:[0,50,0], tanOut:[-30,0,0] })  // arrive from +Y, leave heading -X
track.add({ pos:[300,0,0] })                                    // auto tangents

rot sub-forms — all normalised internally, no pre-processing needed:

track.add({ pos:[0,0,0], rot: [x,y,z,w] })                          // raw quaternion
track.add({ pos:[0,0,0], rot: { axis:[0,1,0], angle: PI/4 } })      // axis-angle
track.add({ pos:[0,0,0], rot: { dir:[1,0,0] } })                    // look direction
track.add({ pos:[0,0,0], rot: { euler:[rx,ry,rz] } })               // intrinsic YXZ (default)
track.add({ pos:[0,0,0], rot: { euler:[rx,ry,rz], order:'XYZ' } })  // explicit order
track.add({ pos:[0,0,0], rot: { from:[0,0,1], to:[1,0,0] } })       // shortest arc
track.add({ pos:[0,0,0], rot: { mat3: rotationMatrix } })           // 3×3 col-major
track.add({ pos:[0,0,0], rot: { eMatrix: eyeMat } })                // from eye matrix

Supported Euler orders: YXZ (default, matches p5 Y-up), XYZ, ZYX, ZXY, XZY, YZX. All are intrinsic — extrinsic ABC equals intrinsic CBA with the same angles.

Interpolation modes:

track.posInterp = 'hermite'  // default — Hermite; auto-CR tangents when none stored
track.posInterp = 'linear'
track.posInterp = 'step'     // snap to k0; useful for discrete state changes

track.rotInterp = 'slerp'    // default — constant angular velocity
track.rotInterp = 'nlerp'    // faster, slightly non-constant speed
track.rotInterp = 'step'     // snap to k0 quaternion

eval(out) writes into a pre-allocated buffer — zero heap allocation per frame. Use toMatrix(outMat4) to evaluate directly into a column-major mat4.

CameraTrack — camera keyframe paths

Stores { eye, center, up } lookat keyframes. Playback applies automatically each frame via cam.camera() — no draw-loop guard needed.

let track

function setup() {
  createCanvas(600, 400, WEBGL)
  track = createCameraTrack()   // binds to the default camera

  track.add({ eye:[0,0,500], center:[0,0,0] })
  track.add({ eye:[300,-150,0], center:[0,0,0] })
  track.add({ eye:[-200,100,-300], center:[0,0,0] })
  track.play({ loop: true, duration: 90 })
}

function draw() {
  background(20)
  orbitControl()   // works freely when track is stopped
  axes(); grid()
}

add() accepts explicit lookat specs or a bulk array:

track.add({ eye, center?, up?, fov?, halfHeight?,
            eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? })
                               // explicit lookat; center defaults to [0,0,0], up to [0,1,0]
                               // eyeTanIn/Out — Hermite tangents for eye path
                               // centerTanIn/Out — Hermite tangents for center path
track.add(cam.capturePose())   // capture live camera state (zero-alloc with pre-allocated out)
track.add()                    // shortcut — captures track's bound camera
track.add([ spec, spec, ... ]) // bulk

For matrix-based capture use track.add({ mMatrix: eMatrix }) on a PoseTrack for full-fidelity TRS including roll, or cam.capturePose() for lookat-style capture.

fov (radians) animates perspective field of view. halfHeight (world units) animates the vertical extent of an ortho frustum — width is derived from aspect ratio at apply time, preserving image proportions. Both fields are captured automatically by track.add() and cam.capturePose().

Interpolation modes:

track.eyeInterp    = 'hermite'  // default — auto-CR tangents when none stored
track.eyeInterp    = 'linear'
track.eyeInterp    = 'step'

track.centerInterp = 'linear'   // default — suits fixed lookat targets
track.centerInterp = 'hermite'  // smoother when center is also flying
track.centerInterp = 'step'

Playback options

All tracks share the same transport API:

track.play({ duration, loop, bounce, rate, onPlay, onEnd, onStop })
track.stop([rewind])   // rewind=true seeks to origin
track.reset()          // clear all keyframes and stop
track.seek(t)          // t ∈ [0, 1]
track.time()           // → number ∈ [0, 1]
track.info()           // → { keyframes, segments, playing, loop, ... }
track.add(spec)        // append keyframe(s)
track.set(i, spec)     // replace keyframe at index
track.remove(i)        // remove keyframe at index
Option Default Description
duration 30 Frames per segment.
loop false Repeat — wrap back to start at end.
bounce false Bounce at boundaries (independent of loop).
rate 1 Playback speed (negative reverses direction).
onPlay Fires when playback starts.
onEnd Fires at natural end (once mode only).
onStop Fires on explicit stop() or reset().

Loop modesloop and bounce are fully independent flags:

loop bounce behaviour
false false play once — stop at end (fires onEnd)
true false repeat — wrap back to start
true true bounce forever — reverse direction at each boundary
false true bounce once — flip at far boundary, stop at origin

The internal _dir field (±1) tracks bounce travel direction — rate is never mutated at boundaries.

Hook firing order:

play()  → onPlay → _onActivate
tick()  → onEnd  → _onDeactivate   (once mode, at boundary)
stop()  → onStop → _onDeactivate
reset() → onStop → _onDeactivate

track.playing, track.loop, track.bounce, track.rate, track.duration, track.keyframes — readable at any time.

Camera helpers

getCamera()              // current p5.Camera (curCamera)
cam.capturePose([out])   // → { eye, center, up, fov, halfHeight }
cam.applyPose(pose)      // write pose back to camera

Space transformations

Matrix operations

All matrix queries share the same contract:

  • out is the first parameter — the caller owns the buffer
  • returns out (or null on a singular matrix)
  • no allocations — pass the same buffer every frame

Accepted types for out and override params: Float32Array | ArrayLike | p5.Matrix

Simple queries — read from live renderer state:

mat4Eye(out)    // eye matrix (inverse view) — eye→world
mat4Proj(out)   // projection matrix
mat4View(out)   // view matrix — world→eye
mat4Model(out)  // model matrix — local→world

Composite queriesout first, optional overrides in an opts object:

mat4PV(out,    [{ mat4Proj, mat4View }])
mat4PVInv(out, [{ mat4Proj, mat4View, mat4PV }])
mat4MV(out,    [{ mat4Model, mat4View }])
mat4PMV(out,   [{ mat4Proj, mat4Model, mat4View }])
mat3Normal(out,[{ mat4Model, mat4View, mat4MV }])  // 9-element out
mat4Location(out, from, to)   // location transform: inv(to) · from
mat3Direction(out, from, to)  // direction transform: to₃ · inv(from₃), 9-element out

Raw matrix math — forwarded from @nakednous/tree, same out-first contract:

mat4Mul(out, A, B)           // out = A · B  (column-major)
mat4Invert(out, src)         // out = inv(src), null if singular
mat4MulPoint(out, m, point)  // out = m · [x,y,z,1] perspective-divided
                             // point: Float32Array | ArrayLike | p5.Vector
mat4MulDir(out, m, dx,dy,dz) // out = 3×3 block of m applied to direction
                             // no translation, no perspective divide

Zero-allocation draw-loop pattern:

// setup — allocate once
const e   = new Float32Array(16)
const pm  = new Float32Array(16)
const pv  = new Float32Array(16)
const wlm = new Float32Array(16)   // e.g. bias · lightPV for shadow mapping
const pt  = new Float32Array(3)

// draw — zero allocations
mat4Eye(e)
mat4Proj(pm)
mat4PV(pv)
mat4Mul(wlm, biasMatrix, pv)
mat4MulPoint(pt, wlm, lightPosition)
viewFrustum({ mat4Eye: e, mat4Proj: pm })
mouseHit({ mat4PV: pv, mat4Eye: e })

Frustum queries

Scalars read directly from the projection matrix — no buffer needed:

projLeft()   projRight()   projBottom()   projTop()   // side planes
projNear()   projFar()                                // near / far
projFov()    projHfov()                               // field of view (radians)
projIsOrtho()                                         // true for orthographic

pixelRatio([worldPos], [{ mat4Proj, mat4View }])
// world-units-per-pixel at worldPos (defaults to camera position)

Coordinate space conversions

out is opt-in. When provided via opts.out the result is written into it (zero-alloc hot path). When omitted a fresh p5.Vector is allocated and returned. Return type matches opts.out.

mapLocation([point], [opts])   // map a point between spaces
mapLocation([opts])            // input defaults to p5.Tree.ORIGIN
mapLocation()                  // ORIGIN, EYE → WORLD → p5.Vector

mapDirection([dir], [opts])    // map a direction between spaces
mapDirection([opts])           // input defaults to p5.Tree._k
mapDirection()                 // _k, EYE → WORLD → p5.Vector

point / dir accept Float32Array | ArrayLike | p5.Vector.

Option Default Description
out new p5.Vector() Destination buffer — omit to allocate p5.Vector.
from p5.Tree.EYE Source space (constant or matrix).
to p5.Tree.WORLD Target space (constant or matrix).
mat4Eye current eye Pre-computed eye matrix.
mat4Proj current proj Override projection matrix.
mat4View current view Override view matrix.
mat4PV P·V Pre-computed PV — skips multiply.
mat4PVInv inv(PV) Pre-computed IPV — skips inversion.

from / to accept: p5.Tree.WORLD, EYE, SCREEN, NDC, MODEL, or a mat4 for a custom local frame.

// ergonomic — allocates p5.Vector
const eye = mapLocation()                                      // camera world position
const fwd = mapDirection()                                     // camera look direction
const scr = mapLocation([100,0,0], { from: p5.Tree.WORLD,
                                     to:   p5.Tree.SCREEN })

// hot path — zero allocation
const loc = new Float32Array(3)
const pv  = new Float32Array(16)
mat4PV(pv)
mapLocation([100,0,0], { from: p5.Tree.WORLD, to: p5.Tree.SCREEN,
                         out: loc, mat4PV: pv })

Constants: p5.Tree.ORIGIN, p5.Tree.i, p5.Tree.j, p5.Tree.k, p5.Tree._i, p5.Tree._j, p5.Tree._k.

Heads Up Display

Draw directly in screen space — independent of the current camera and 3D transforms.

beginHUD()
text('FPS: ' + frameRate().toFixed(1), 10, 20)
endHUD()

Coordinates: (x, y) ∈ [0, width] × [0, height], origin top-left, y increasing downward.


Panels

A unified createPanel factory covers parameter bindings and track transport controls. The first argument determines the panel type.

Parameter panel

Binds named schema keys to DOM sliders, checkboxes, color pickers, dropdowns, and buttons. Target receives (name, value) on each dirty tick.

const panel = createPanel({
  speed:     { min: 0, max: 0.05, value: 0.012, step: 0.001 },
  shininess: { min: 1, max: 200,  value: 80,    step: 1,    type: 'int' },
  showGrid:  { value: true },
  tint:      { value: '#ff8844' },
  fxOrder:   { type: 'select', options: [
                 { label: 'noise → dof', value: '1' },
                 { label: 'dof → noise', value: '2' }
               ], value: '1' }
}, { x: 10, y: 10, width: 160, labels: true, title: 'Scene', color: 'white',
     target: (name, value) => shader.setUniform(name, value) })

// call every frame
panel.tick()
Option Default Description
target fn(name, value) or object with .set(name, value).
x / y 0 Position (px).
width 120 Slider width (px).
labels false Show parameter name labels.
title Optional title row.
collapsible false Title row becomes a collapse toggle.
collapsed false Start collapsed (implies collapsible).
color Container text color.
hidden false Start hidden.
parent document.body Mount target (HTMLElement).

Track transport panel

Controls playback of any PoseTrack or CameraTrack.

const ui = createPanel(track, {
  x: 10, y: 10, width: 170,
  loop: false, rate: 1,
  seek: true, props: true, info: true,
  color: 'white'
})

// Suppress + button
createPanel(track, { camera: null, x: 10, y: 10 })

// Suppress reset button (e.g. when keyframes are hardcoded and cannot be re-added)
createPanel(track, { reset: false, x: 10, y: 10 })

// call every frame
ui.tick()
Option Default Description
seek true Show seek slider.
props true Show rate slider + loop controls.
info false Show time/keyframe readout.
rate track.rate Initial rate.
loop track.loop Initial loop state.
bounce track.bounce Initial bounce state.
depth 0.5 Initial + button depth [0..1].
camera track.camera (CameraTrack), curCamera (PoseTrack) Camera for + button. null suppresses it.
reset true Show reset button. false suppresses it.

Lifecycle hooks can be passed directly in opt:

createPanel(track, {
  onPlay: t => console.log('playing'),
  onEnd:  t => console.log('done'),
  onStop: t => console.log('stopped'),
  x: 10, y: 10
})

Returned handle (both panel types):

panel.el            // HTMLElement container
panel.visible       // get/set boolean
panel.collapsed     // get/set boolean (requires collapsible + title)
panel.parent(el)    // re-mount into a different HTMLElement
panel.tick()        // called automatically — no need to call manually
panel.dispose()     // remove from DOM

Collapsible panels

Any panel with a title can be made collapsible. Clicking the title row toggles the content.

createPanel(schema, { title: 'Noise', collapsible: true, collapsed: true })
createPanel(track,  { title: 'Camera path', collapsible: true })

Programmatic control:

panel.collapsed = true
panel.collapsed = false

Post-processing

A lightweight multi-pass pipeline for p5.Framebuffer, p5.strands, and standard WebGL rendering. pipe() chains filter shaders, reuses internal ping/pong framebuffers, and optionally displays the result. Framebuffers are lazily allocated and released on sketch removal.

pipe

pipe(source, passes, options)
Parameter Description
source p5.Framebuffer, texture, image, or graphics.
passes Array of filters, or a single filter instance.
options See table below.
Option Default Description
display true Draw final output to the main canvas.
allocate true Auto-allocate and cache internal ping/pong.
key 'default' Cache key for multiple independent pipelines.
ping / pong User-provided framebuffers (advanced override).
clear true Clear each pass target before drawing.
clearDisplay true Clear main canvas before final blit.
clearFn background(0) Custom clear strategy for passes.
clearDisplayFn clearFn Custom clear strategy for display stage.
draw full blit Custom draw strategy for placing texture on target.

releasePipe

releasePipe()         // release default pipeline
releasePipe(true)     // release all pipelines
releasePipe('key')    // release a named pipeline

Picking

Two complementary strategies — GPU color-ID for whole-scene picking, CPU proximity for per-object hit testing.

GPU color-ID picking

Renders the scene into a cached 1×1 framebuffer with a pick-matrix projection aligned to the query pixel, reads back one RGBA pixel, and decodes a 24-bit integer id. Supports up to 16 777 215 unique ids. id 0 is reserved for background / miss.

// tag(id) encodes an integer as a CSS hex string — works with fill() regardless of colorMode()
fill(tag(1)); box(60)
fill(tag(2)); sphere(40)
// colorPick — explicit coordinates
const hit = colorPick(mouseX, mouseY, () => {
  push(); fill(tag(1)); box(60);    pop()
  push(); fill(tag(2)); sphere(40); pop()
})
if (hit === 1) console.log('box!')
if (hit === 2) console.log('sphere!')

// mousePick — shorthand for colorPick(mouseX, mouseY, fn)
const hit = mousePick(() => {
  push(); fill(tag(1)); box(60);    pop()
  push(); fill(tag(2)); sphere(40); pop()
})

Before drawFn is called, the library unconditionally sets noLights(), noStroke(), resetShader(). Stroke is excluded from the pick buffer by default — call stroke(tag(id)) inside drawFn to include it, skipping the stroke render passes when precision or performance warrants it. When stroke is included, both fill and stroke must carry the same tag(id). The FBO is lazily allocated on first use and released on sketch removal.

CPU proximity picking

Tests whether a pointer position falls within a radius of the current model's projected screen-space origin. Zero GPU round-trip — call inside push()/pop() for each pickable object.

// mouseHit — test against mouseX/mouseY
push()
translate(x, y, z)
if (mouseHit()) { fill('red') } else { fill('white') }
box(60)
pop()

// pointerHit — explicit coordinates (base form)
push()
translate(x, y, z)
if (pointerHit(touchX, touchY)) { fill('red') } else { fill('white') }
box(60)
pop()

Both accept the same options object:

Option Default Description
mat4Model current model Override model matrix.
size 50 Hit radius (world units, auto-scaled by depth).
shape p5.Tree.CIRCLE CIRCLE or SQUARE.
mat4Eye current eye Pre-computed eye matrix.
mat4Proj current proj Override projection.
mat4View current view Override view.
mat4PV P·V Pre-computed PV.

Utilities

p5.Tree.VERSION   // '0.0.34'

Shader helpers

screenSize()
// Returns physical canvas size in pixels:
// [pixelDensity * width, pixelDensity * height].
// Use as `u_resolution` when working with gl_FragCoord.xy.
// Not required for createFilterShader() — filter shaders receive `canvasSize` automatically.

shader.setUniform('u_resolution', screenSize())
texelSize(img)
// Returns texel size: [1 / width, 1 / height].
// Accepts p5.Image, p5.Framebuffer, p5.Graphics,
// or any object with { width, height }.

shader.setUniform('texOffset', texelSize(myFbo))

Visibility testing

Frustum culling against the current camera:

visibility({ corner1, corner2 })   // axis-aligned box
visibility({ center, radius })     // sphere
visibility({ center })             // point

Returns:

p5.Tree.VISIBLE
p5.Tree.SEMIVISIBLE
p5.Tree.INVISIBLE

Gizmos

Scene-space diagnostic helpers — drawn to understand the scene, not to build it.

axes([{ size, bits, mat4Model, mat4Eye, mat4Proj, mat4View, mat4PV }])
grid([{ size, subdivisions }])
bullsEye([{ size, shape }])
cross([{ size }])
viewFrustum({ pg, mat4Eye, mat4Proj, mat4View, bits, viewer })

axes bits: p5.Tree.X, p5.Tree._X, p5.Tree.Y, p5.Tree._Y, p5.Tree.Z, p5.Tree._Z, p5.Tree.LABELS.

viewFrustum bits: p5.Tree.NEAR, p5.Tree.FAR, p5.Tree.BODY, p5.Tree.APEX.

Matrix params accept Float32Array(16) | ArrayLike | p5.Matrix throughout.


Releases

Latest:

Tagged:


Usage

CDN

<script src="https://cdn.jsdelivr.net/npm/p5/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.js"></script>

<script>
  function setup() {
    createCanvas(600, 400, WEBGL)
    axes()
  }

  function draw() {
    background(0.15)
    orbitControl()
  }
</script>

Works in global and instance mode.

npm (ESM)

npm i p5 p5.tree
import p5 from 'p5'
import 'p5.tree'

const sketch = p => {
  p.setup = () => {
    p.createCanvas(600, 400, p.WEBGL)
    p.axes()
  }

  p.draw = () => {
    p.background(0.15)
    p.orbitControl()
  }
}

new p5(sketch)