@nakednous/tree 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +243 -57
- package/dist/index.js +838 -622
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# `@nakednous/tree`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
The package is **renderer-agnostic**, has **no dependencies**, and can run in both browser and server environments.
|
|
3
|
+
Pure numeric core for animation, coordinate-space mapping, and visibility — **zero dependencies**, runs anywhere.
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
@@ -12,114 +10,302 @@ The package is **renderer-agnostic**, has **no dependencies**, and can run in bo
|
|
|
12
10
|
npm install @nakednous/tree
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
```
|
|
13
|
+
```js
|
|
16
14
|
import * as tree from '@nakednous/tree'
|
|
17
15
|
```
|
|
18
16
|
|
|
19
17
|
---
|
|
20
18
|
|
|
21
|
-
##
|
|
19
|
+
## Architecture
|
|
22
20
|
|
|
23
|
-
`@nakednous/tree` is
|
|
21
|
+
`@nakednous/tree` is the bottom layer of a three-package stack. It knows nothing about renderers, the DOM, or p5 — it operates on plain arrays and `Float32Array` buffers throughout.
|
|
24
22
|
|
|
25
|
-
```
|
|
26
|
-
application
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
```
|
|
24
|
+
application
|
|
25
|
+
│
|
|
26
|
+
▼
|
|
27
|
+
p5.tree.js ← bridge: wires tree + ui into p5.js v2
|
|
28
|
+
│
|
|
29
|
+
├── @nakednous/ui ← DOM param panels, transport controls
|
|
30
|
+
│
|
|
31
|
+
└── @nakednous/tree ← this package: math, spaces, animation, visibility
|
|
34
32
|
```
|
|
35
33
|
|
|
36
|
-
The
|
|
37
|
-
`@nakednous/tree` performs the underlying computations: animation interpolation, spatial mapping, and visibility testing.
|
|
34
|
+
The dependency direction is strict: `@nakednous/tree` never imports from the bridge or the DOM layer. This is what lets the same `PoseTrack` that drives a camera path also animate any object — headless, server-side, or in a future renderer.
|
|
38
35
|
|
|
39
36
|
---
|
|
40
37
|
|
|
41
|
-
##
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
### PoseTrack — TRS keyframe animation
|
|
41
|
+
|
|
42
|
+
A renderer-agnostic state machine for `{ pos, rot, scl }` keyframe sequences. Rotation is stored as `[x,y,z,w]` quaternions (w-last, glTF layout).
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { PoseTrack } from '@nakednous/tree'
|
|
46
|
+
|
|
47
|
+
const track = new PoseTrack()
|
|
48
|
+
track.add({ pos: [0, 0, 0], rot: [0,0,0,1], scl: [1,1,1] })
|
|
49
|
+
track.add({ pos: [100, 50, 0], rot: [0,0,0,1], scl: [2,1,1] })
|
|
50
|
+
track.play({ duration: 60, loop: true })
|
|
51
|
+
|
|
52
|
+
// per-frame — zero allocation
|
|
53
|
+
const out = { pos: [0,0,0], rot: [0,0,0,1], scl: [1,1,1] }
|
|
54
|
+
track.tick()
|
|
55
|
+
track.eval(out) // writes interpolated TRS into out
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Interpolation modes:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
track.posInterp = 'catmullrom' // default — centripetal Catmull-Rom
|
|
62
|
+
track.posInterp = 'linear'
|
|
63
|
+
|
|
64
|
+
track.rotInterp = 'slerp' // default — constant angular velocity
|
|
65
|
+
track.rotInterp = 'nlerp' // normalised lerp; cheaper, slightly non-constant
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Playback features: signed `rate` (negative reverses), `loop`, `pingPong`, `seek(t)` scrubbing, and lifecycle hooks (`onPlay`, `onEnd`, `onStop`). `_onActivate` / `_onDeactivate` are lib-space hooks for the host layer's draw-loop registry — not for user code.
|
|
69
|
+
|
|
70
|
+
`add()` accepts flexible specs. Top-level forms:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
track.add({ pos, rot, scl }) // explicit TRS — rot accepts any form below
|
|
74
|
+
track.add({ mMatrix: mat4 }) // decompose a column-major model matrix into TRS
|
|
75
|
+
track.add([ spec, spec, ... ]) // bulk
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`rot` sub-forms — all normalised internally:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
rot: [x,y,z,w] // raw quaternion
|
|
82
|
+
rot: { axis:[x,y,z], angle } // axis-angle
|
|
83
|
+
rot: { dir:[x,y,z], up?:[x,y,z] } // look direction (−Z forward)
|
|
84
|
+
rot: { euler:[rx,ry,rz], order?:'YXZ' } // intrinsic Euler angles (radians)
|
|
85
|
+
// orders: YXZ (default), XYZ, ZYX,
|
|
86
|
+
// ZXY, XZY, YZX
|
|
87
|
+
// extrinsic ABC = intrinsic CBA
|
|
88
|
+
rot: { from:[x,y,z], to:[x,y,z] } // shortest-arc between directions
|
|
89
|
+
rot: { mat3: Float32Array|Array } // column-major 3×3 rotation matrix
|
|
90
|
+
rot: { eMatrix: mat4 } // rotation block of an eye matrix
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### CameraTrack — lookat keyframe animation
|
|
96
|
+
|
|
97
|
+
A renderer-agnostic state machine for `{ eye, center, up }` lookat keyframes. Each field is independently interpolated — eye and center along their own paths, up nlerped on the unit sphere.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { CameraTrack } from '@nakednous/tree'
|
|
101
|
+
|
|
102
|
+
const track = new CameraTrack()
|
|
103
|
+
track.add({ eye:[0,0,500], center:[0,0,0] })
|
|
104
|
+
track.add({ eye:[300,-150,0], center:[0,0,0] })
|
|
105
|
+
track.play({ loop: true, duration: 90 })
|
|
106
|
+
|
|
107
|
+
// per-frame — zero allocation
|
|
108
|
+
const out = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] }
|
|
109
|
+
track.tick()
|
|
110
|
+
track.eval(out)
|
|
111
|
+
// apply: cam.camera(out.eye[0],out.eye[1],out.eye[2],
|
|
112
|
+
// out.center[0],out.center[1],out.center[2],
|
|
113
|
+
// out.up[0],out.up[1],out.up[2])
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Interpolation modes:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
track.eyeInterp = 'catmullrom' // default
|
|
120
|
+
track.eyeInterp = 'linear'
|
|
121
|
+
|
|
122
|
+
track.centerInterp = 'linear' // default — suits fixed lookat targets
|
|
123
|
+
track.centerInterp = 'catmullrom' // smoother when center is also moving freely
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`add()` accepts:
|
|
42
127
|
|
|
43
|
-
|
|
128
|
+
```js
|
|
129
|
+
track.add({ eye, center, up? }) // explicit lookat; up defaults to [0,1,0]
|
|
130
|
+
track.add({ vMatrix: mat4 }) // view matrix (world→eye); eye reconstructed
|
|
131
|
+
track.add({ eMatrix: mat4 }) // eye matrix (eye→world); eye read from col3
|
|
132
|
+
track.add([ spec, spec, ... ]) // bulk
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Note: both matrix forms default `up` to `[0,1,0]`. The matrix col1 (up_ortho) is intentionally not used — it differs from the hint for upright cameras and would shift orbitControl's orbit reference. Use `capturePose()` (p5.tree bridge) when the real up hint is needed.
|
|
44
136
|
|
|
45
|
-
|
|
137
|
+
---
|
|
46
138
|
|
|
47
|
-
|
|
139
|
+
### Shared Track transport
|
|
140
|
+
|
|
141
|
+
Both `PoseTrack` and `CameraTrack` extend `Track`, which holds all transport machinery:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
track.play({ duration, loop, pingPong, rate, onPlay, onEnd, onStop })
|
|
145
|
+
track.stop([rewind]) // rewind=true seeks to origin on stop
|
|
146
|
+
track.reset() // clear all keyframes and stop
|
|
147
|
+
track.seek(t) // normalised position [0, 1]
|
|
148
|
+
track.time() // → number ∈ [0, 1]
|
|
149
|
+
track.info() // → { keyframes, segments, seg, f, playing, loop, ... }
|
|
150
|
+
track.tick() // advance cursor by rate — returns playing state
|
|
151
|
+
track.add(spec) // append keyframe(s)
|
|
152
|
+
track.set(i, spec) // replace keyframe at index
|
|
153
|
+
track.remove(i) // remove keyframe at index
|
|
154
|
+
|
|
155
|
+
track.playing // boolean
|
|
156
|
+
track.loop // boolean
|
|
157
|
+
track.pingPong // boolean
|
|
158
|
+
track.rate // get/set — never starts/stops playback
|
|
159
|
+
track.duration // frames per segment
|
|
160
|
+
track.keyframes // raw array
|
|
161
|
+
```
|
|
48
162
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
163
|
+
Hook firing order:
|
|
164
|
+
```
|
|
165
|
+
play() → onPlay → _onActivate
|
|
166
|
+
tick() → onEnd → _onDeactivate (once mode, at boundary)
|
|
167
|
+
stop() → onStop → _onDeactivate
|
|
168
|
+
reset() → onStop → _onDeactivate
|
|
169
|
+
```
|
|
53
170
|
|
|
54
|
-
|
|
171
|
+
One-keyframe behaviour: `play()` with exactly one keyframe snaps `eval()` to that keyframe without setting `playing = true` and without firing hooks.
|
|
55
172
|
|
|
56
173
|
---
|
|
57
174
|
|
|
58
|
-
###
|
|
175
|
+
### Coordinate-space mapping
|
|
176
|
+
|
|
177
|
+
`mapLocation` and `mapDirection` convert points and vectors between any pair of named spaces. All work is done in flat scalar arithmetic — no objects created per call.
|
|
178
|
+
|
|
179
|
+
**Spaces:** `WORLD`, `EYE`, `SCREEN`, `NDC`, `MODEL`, `MATRIX` (custom frame).
|
|
59
180
|
|
|
60
|
-
|
|
181
|
+
**NDC convention:** `WEBGL = -1` (z ∈ [−1,1]), `WEBGPU = 0` (z ∈ [0,1]).
|
|
61
182
|
|
|
62
|
-
|
|
183
|
+
```js
|
|
184
|
+
import { mapLocation, mapDirection, WORLD, SCREEN, WEBGL } from '@nakednous/tree'
|
|
63
185
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
186
|
+
const out = new Float32Array(3)
|
|
187
|
+
const m = {
|
|
188
|
+
pMatrix: /* Float32Array(16) — projection */,
|
|
189
|
+
vMatrix: /* Float32Array(16) — view (world→eye) */,
|
|
190
|
+
pvMatrix: /* pMatrix × vMatrix — optional, computed if absent */,
|
|
191
|
+
ipvMatrix: /* inv(pvMatrix) — optional, computed if absent */,
|
|
192
|
+
}
|
|
193
|
+
const vp = [0, height, width, -height]
|
|
194
|
+
|
|
195
|
+
mapLocation(out, worldX, worldY, worldZ, WORLD, SCREEN, m, vp, WEBGL)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The matrices bag `m` is assembled by the host (p5.tree reads live renderer state into it). All pairs are supported: WORLD↔EYE, WORLD↔SCREEN, WORLD↔NDC, EYE↔SCREEN, SCREEN↔NDC, WORLD↔MATRIX, and their reverses.
|
|
67
199
|
|
|
68
200
|
---
|
|
69
201
|
|
|
70
|
-
### Visibility
|
|
202
|
+
### Visibility testing
|
|
203
|
+
|
|
204
|
+
[Frustum culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) against six planes. All functions take scalar inputs and a pre-filled `Float64Array(24)` planes buffer — zero allocations per test.
|
|
71
205
|
|
|
72
|
-
|
|
206
|
+
```js
|
|
207
|
+
import { frustumPlanes, pointVisibility, sphereVisibility, boxVisibility,
|
|
208
|
+
VISIBLE, SEMIVISIBLE, INVISIBLE } from '@nakednous/tree'
|
|
73
209
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
210
|
+
const planes = new Float64Array(24)
|
|
211
|
+
frustumPlanes(planes, posX, posY, posZ, vdX, vdY, vdZ,
|
|
212
|
+
upX, upY, upZ, rtX, rtY, rtZ,
|
|
213
|
+
ortho, near, far, left, right, top, bottom)
|
|
77
214
|
|
|
78
|
-
|
|
215
|
+
sphereVisibility(planes, cx, cy, cz, radius) // → VISIBLE | SEMIVISIBLE | INVISIBLE
|
|
216
|
+
boxVisibility(planes, x0,y0,z0, x1,y1,z1)
|
|
217
|
+
pointVisibility(planes, px, py, pz)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INVISIBLE` (fully outside).
|
|
79
221
|
|
|
80
222
|
---
|
|
81
223
|
|
|
82
|
-
###
|
|
224
|
+
### Quaternion and matrix math
|
|
83
225
|
|
|
84
|
-
|
|
226
|
+
Exported individually for use in hot paths.
|
|
227
|
+
|
|
228
|
+
**Quaternions** — `[x,y,z,w]` w-last:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
qSet qCopy qDot qNormalize qNegate qMul
|
|
232
|
+
qSlerp qNlerp
|
|
233
|
+
qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
234
|
+
quatToAxisAngle
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Spline / vector:** `catmullRomVec3`, `lerpVec3`
|
|
238
|
+
|
|
239
|
+
**Mat4:**
|
|
240
|
+
```
|
|
241
|
+
mat4Mul mat4Invert mat4Transpose mat4MulPoint
|
|
242
|
+
mat3NormalFromMat4 mat4Location mat3Direction
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**TRS ↔ mat4:** `transformToMat4`, `mat4ToTransform`
|
|
246
|
+
|
|
247
|
+
**Projection queries** (read from a projection mat4 — no renderer needed):
|
|
248
|
+
```
|
|
249
|
+
projIsOrtho projNear projFar projFov projHfov
|
|
250
|
+
projLeft projRight projTop projBottom
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Pixel ratio:** `pixelRatio(proj, vpH, eyeZ, ndcZMin)` — world-units-per-pixel at a given depth, handles both perspective and orthographic.
|
|
85
254
|
|
|
86
255
|
---
|
|
87
256
|
|
|
88
|
-
|
|
257
|
+
### Constants
|
|
89
258
|
|
|
90
|
-
|
|
259
|
+
```js
|
|
260
|
+
// Coordinate spaces
|
|
261
|
+
WORLD, EYE, NDC, SCREEN, MODEL, MATRIX
|
|
91
262
|
|
|
92
|
-
|
|
263
|
+
// NDC Z convention
|
|
264
|
+
WEBGL // −1 (z ∈ [−1, 1])
|
|
265
|
+
WEBGPU // 0 (z ∈ [0, 1])
|
|
93
266
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* determining whether objects are visible
|
|
267
|
+
// Visibility results
|
|
268
|
+
INVISIBLE, VISIBLE, SEMIVISIBLE
|
|
97
269
|
|
|
98
|
-
|
|
270
|
+
// Basis vectors (frozen)
|
|
271
|
+
ORIGIN, i, j, k, _i, _j, _k
|
|
272
|
+
```
|
|
99
273
|
|
|
100
274
|
---
|
|
101
275
|
|
|
102
|
-
##
|
|
276
|
+
## Performance contract
|
|
103
277
|
|
|
104
|
-
|
|
278
|
+
All hot-path functions follow an **out-first, zero-allocation** contract:
|
|
105
279
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
280
|
+
- `out` is the first parameter — the caller owns the buffer
|
|
281
|
+
- the function writes into `out` and returns it
|
|
282
|
+
- `null` is returned on degeneracy (singular matrix, etc.)
|
|
283
|
+
- no heap allocations per call
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
// allocate once
|
|
287
|
+
const out = new Float32Array(3)
|
|
288
|
+
const pvMatrix = new Float32Array(16)
|
|
289
|
+
const ipvMatrix= new Float32Array(16)
|
|
290
|
+
|
|
291
|
+
// per frame — zero allocation
|
|
292
|
+
mat4Mul(pvMatrix, proj, view)
|
|
293
|
+
mat4Invert(ipvMatrix, pvMatrix)
|
|
294
|
+
mapLocation(out, px, py, pz, WORLD, SCREEN,
|
|
295
|
+
{ pMatrix: proj, vMatrix: view, pvMatrix, ipvMatrix }, vp, WEBGL)
|
|
296
|
+
```
|
|
111
297
|
|
|
112
298
|
---
|
|
113
299
|
|
|
114
300
|
## Relationship to `p5.tree`
|
|
115
301
|
|
|
116
|
-
|
|
302
|
+
[p5.tree](https://github.com/VisualComputing/p5.tree) is the bridge layer. It reads live renderer state (camera matrices, viewport dimensions, NDC convention) and passes it to `@nakednous/tree` functions. It wires `PoseTrack` and `CameraTrack` to the p5 draw loop, exposes `createTrack` / `getCamera`, and provides `createPanel` for transport and parameter UIs.
|
|
117
303
|
|
|
118
|
-
|
|
304
|
+
`@nakednous/tree` provides the algorithms. The bridge provides the wiring.
|
|
119
305
|
|
|
120
306
|
---
|
|
121
307
|
|
|
122
308
|
## License
|
|
123
309
|
|
|
124
|
-
GPL-3.0-only
|
|
310
|
+
GPL-3.0-only
|
|
125
311
|
© JP Charalambos
|