TRS keyframe animation with
createPoseTrack. Four{ pos, rot }keyframes, cubic Hermite interpolation of position with auto-Catmull-Rom tangents, and slerp on rotations. A transport panel scrubs the track; a second panel toggles thetrackPathoverlay bits — PATH, CONTROLS, TANGENTS_IN, TANGENTS_OUT — and switches interpolation modes live. UnlikecreateCameraTrack, which applies its result to a camera automatically,PoseTrackproduces an interpolated pose you fold into the transform stack yourself withapplyPose(track.eval(out)).
Four keyframes, one object
createPoseTrack returns a PoseTrack — a renderer-agnostic state machine for { pos, rot, scl } keyframes. track.add(spec) appends one; adjacent duplicates are skipped by default:
const track = createPoseTrack()
track.add({ pos: [-220, -40, 0], rot: { axis: [0, 1, 0], angle: 0 } })
track.add({ pos: [ -70, -40, 80], rot: { axis: [0, 1, 0], angle: Math.PI/3 } })
rot accepts every sensible form — raw quaternion [x,y,z,w], axis-angle, look direction, intrinsic Euler angles, shortest-arc from/to, or a rotation matrix. All get normalised to a unit quaternion internally, so you pick whichever form is cheapest to author for the keyframe at hand. Position uses cubic Hermite with centripetal Catmull-Rom tangents when none are supplied, so four waypoints produce a smooth curve without any hand-set control vectors.
Applying the pose
A PoseTrack does not bind to anything — it just interpolates. Each frame, track.eval(out) writes the current TRS into a pre-allocated buffer, and applyPose folds it into the current matrix stack:
const out = { pos: [0,0,0], rot: [0,0,0,1], scl: [1,1,1] }
function draw() {
track.eval(out)
push()
applyPose(out)
box(42)
pop()
}
This is the core contrast with createCameraTrack, whose predraw hook calls cam.applyPose(...) on the bound camera automatically. PoseTrack is explicit on purpose — the same track can drive any number of objects, each with its own local transform layered above applyPose, and the object’s orientation can be composed with the scene’s own push/pop discipline without the track fighting for control of the view matrix.
For a zero-allocation direct-to-mat4 path, use track.mat4Model(mat) instead — it writes a column-major 4×4 into the provided Float32Array(16), ready for applyMatrix(...mat). Same math, no intermediate TRS object.
Interpolation modes
Position and rotation have independent interpolation modes, toggled live from the right-hand panel:
track.posInterp = 'hermite' // 'hermite' | 'linear' | 'step'
track.rotInterp = 'slerp' // 'slerp' | 'nlerp' | 'step'
hermite uses auto-Catmull-Rom tangents when keyframes carry none; linear lerps between adjacent keyframes, producing a straight polyline that coincides with the control polygon; step snaps to k0 until the next boundary — handy for game-state transitions or tile-based motion. On rotations, slerp is Ken Shoemake’s constant-angular-velocity interpolation, nlerp is a cheaper normalised lerp with slightly non-constant speed, and step snaps to k0’s quaternion. Modes compose — smooth position with stepped rotation is a valid, visually distinct combination, and switching mid-playback doesn’t break the transport: the cursor keeps its (seg, t) and the sampler just produces different output next frame.
Visualising the path with trackPath
trackPath is the gizmo for any track — PoseTrack or CameraTrack. Four bits cover the pose-track case:
const { PATH, CONTROLS, TANGENTS_IN, TANGENTS_OUT } = p5.Tree
stroke('#e8ecf1'); trackPath(track, { bits: PATH, marker: null })
stroke('#4a5566'); trackPath(track, { bits: CONTROLS, marker: null })
stroke('#5cd0ff'); trackPath(track, { bits: TANGENTS_IN, marker: null })
stroke('#ff6ec7'); trackPath(track, { bits: TANGENTS_OUT, marker: null })
PATH samples the interpolated position polyline. CONTROLS is the straight control polygon joining adjacent keyframes — the chord that hermite and linear agree on at the endpoints but diverge between. TANGENTS_IN / TANGENTS_OUT draw arrows at each keyframe showing the effective incoming and outgoing tangents, either the stored tanIn/tanOut or the auto-CR fallback, which makes it easy to see why a curve bulges the way it does.
There is no colour argument. Every bit inherits the ambient stroke(...), so composing a multi-coloured path is just multiple calls — the same convention axes and viewFrustum follow. Passing marker: null suppresses the per-keyframe marker so overlay calls don’t stack redundant gizmos on top of each other.
A final pass with bits: 0 draws only the default marker — a six-axis cross oriented by each keyframe’s pose:
stroke(180)
trackPath(track, { bits: 0 })
Custom markers are a callback (kf, index, track, ctx) => { ... } called once per keyframe — useful for drawing a wireframe proxy, a thumbnail model, or a labelled index at each keyframe.
Two panels, two modes
createPanel has two call shapes. Passing a track yields a transport panel — play/stop, seek slider, rate, loop, bounce, and a + button that captures the current camera pose as a new keyframe:
uiTrack = createPanel(track, {
x: 10, y: 10, width: 130,
title: 'PoseTrack', info: true,
reset: false, camera: null, // suppress reset + '+' buttons
color: 'white',
})
Passing a schema object yields a parameter panel — checkboxes, sliders, dropdowns, colour pickers — with values read either through a target sink or polled manually via panel.<key>.value():
uiViz = createPanel({
path: { value: true }, // inferred as checkbox
posInterp: { type: 'select', value: 'hermite',
options: [{ label: 'hermite', value: 'hermite' },
{ label: 'linear', value: 'linear' },
{ label: 'step', value: 'step' }] },
}, { x: 460, y: 10, width: 130, labels: true, title: 'Path viz', color: 'white' })
// in draw():
track.posInterp = uiViz.posInterp.value()
if (uiViz.path.value()) trackPath(track, { bits: p5.Tree.PATH, marker: null })
Both panels self-tick — no panel.tick() needed in the draw loop. The transport panel polls track.playing / track.loop / track.bounce live, so poking the track from keyboard shortcuts or external code stays in sync with the UI, and the parameter panel’s dirty-flag model collapses rapid slider drags into a single push per frame.
References
- p5.tree README —
PoseTrack,CameraTrack,trackPath,createPanel - Centripetal Catmull–Rom spline — the tangent-free Hermite default
- Cubic Hermite spline — the math behind
tanIn/tanOut - Slerp — Animating rotation with quaternions — Ken Shoemake’s constant-speed rotational interpolation
- Understanding Slerp, Then Not Using It — Jonathan Blow on when
nlerpis the right call