@nakednous/tree 0.0.2 → 0.0.3
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 +217 -57
- package/dist/index.js +550 -356
- 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,276 @@ 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
|
+
```
|
|
42
57
|
|
|
43
|
-
|
|
58
|
+
Interpolation modes:
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
```js
|
|
61
|
+
track.posInterp = 'catmullrom' // default — centripetal Catmull-Rom
|
|
62
|
+
track.posInterp = 'linear'
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
track.rotInterp = 'slerp' // default — constant angular velocity
|
|
65
|
+
track.rotInterp = 'nlerp' // normalised lerp; cheaper, slightly non-constant
|
|
66
|
+
```
|
|
48
67
|
|
|
49
|
-
|
|
50
|
-
* rate control
|
|
51
|
-
* seeking
|
|
52
|
-
* looping and ping-pong modes
|
|
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.
|
|
53
69
|
|
|
54
|
-
|
|
70
|
+
Keyframe `rot` input is flexible — the parser normalises all forms:
|
|
71
|
+
- raw `[x,y,z,w]` quaternion
|
|
72
|
+
- `{ axis: [x,y,z], angle }` axis-angle
|
|
73
|
+
- `{ dir: [x,y,z], up? }` look-direction
|
|
74
|
+
- `{ view: mat4 }` from view matrix rotation block
|
|
75
|
+
- `{ eye, center, up? }` lookat shorthand
|
|
55
76
|
|
|
56
77
|
---
|
|
57
78
|
|
|
58
|
-
###
|
|
79
|
+
### CameraTrack — lookat keyframe animation
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { CameraTrack } from '@nakednous/tree'
|
|
85
|
+
|
|
86
|
+
const track = new CameraTrack()
|
|
87
|
+
track.add({ eye:[0,0,500], center:[0,0,0] })
|
|
88
|
+
track.add({ eye:[300,-150,0], center:[0,0,0] })
|
|
89
|
+
track.play({ loop: true, duration: 90 })
|
|
90
|
+
|
|
91
|
+
// per-frame — zero allocation
|
|
92
|
+
const out = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] }
|
|
93
|
+
track.tick()
|
|
94
|
+
track.eval(out)
|
|
95
|
+
// apply: cam.camera(out.eye[0],out.eye[1],out.eye[2],
|
|
96
|
+
// out.center[0],out.center[1],out.center[2],
|
|
97
|
+
// out.up[0],out.up[1],out.up[2])
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Interpolation modes:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
track.eyeInterp = 'catmullrom' // default
|
|
104
|
+
track.eyeInterp = 'linear'
|
|
105
|
+
|
|
106
|
+
track.centerInterp = 'linear' // default — suits fixed lookat targets
|
|
107
|
+
track.centerInterp = 'catmullrom' // smoother when center is also flying
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`add()` accepts:
|
|
111
|
+
- `{ eye, center, up? }` explicit lookat; `up` defaults to `[0,1,0]`
|
|
112
|
+
- `{ view: mat4 }` column-major view matrix; eye and forward extracted
|
|
113
|
+
|
|
114
|
+
---
|
|
59
115
|
|
|
60
|
-
|
|
116
|
+
### Shared Track transport
|
|
117
|
+
|
|
118
|
+
Both `PoseTrack` and `CameraTrack` extend `Track`, which holds all transport machinery:
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
track.play({ duration, loop, pingPong, rate, onPlay, onEnd, onStop })
|
|
122
|
+
track.stop([rewind]) // rewind=true seeks to origin on stop
|
|
123
|
+
track.reset() // clear all keyframes and stop
|
|
124
|
+
track.seek(t) // normalised position [0, 1]
|
|
125
|
+
track.time() // → number ∈ [0, 1]
|
|
126
|
+
track.info() // → { keyframes, segments, seg, f, playing, loop, ... }
|
|
127
|
+
track.tick() // advance cursor by rate — returns playing state
|
|
128
|
+
track.add(spec) // append keyframe(s)
|
|
129
|
+
track.set(i, spec) // replace keyframe at index
|
|
130
|
+
track.remove(i) // remove keyframe at index
|
|
131
|
+
|
|
132
|
+
track.playing // boolean
|
|
133
|
+
track.rate // get/set — never starts/stops playback
|
|
134
|
+
track.duration // frames per segment
|
|
135
|
+
track.keyframes // raw array
|
|
136
|
+
```
|
|
61
137
|
|
|
62
|
-
|
|
138
|
+
Hook firing order:
|
|
139
|
+
```
|
|
140
|
+
play() → onPlay → _onActivate
|
|
141
|
+
tick() → onEnd → _onDeactivate (once mode, at boundary)
|
|
142
|
+
stop() → onStop → _onDeactivate
|
|
143
|
+
reset() → onStop → _onDeactivate
|
|
144
|
+
```
|
|
63
145
|
|
|
64
|
-
|
|
65
|
-
* mapping directions between spaces
|
|
66
|
-
* deriving projection parameters
|
|
146
|
+
One-keyframe behaviour: `play()` with exactly one keyframe snaps `eval()` to that keyframe without setting `playing = true` and without firing hooks.
|
|
67
147
|
|
|
68
148
|
---
|
|
69
149
|
|
|
70
|
-
###
|
|
150
|
+
### Coordinate-space mapping
|
|
151
|
+
|
|
152
|
+
`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.
|
|
71
153
|
|
|
72
|
-
|
|
154
|
+
**Spaces:** `WORLD`, `EYE`, `SCREEN`, `NDC`, `MODEL`, `MATRIX` (custom frame).
|
|
73
155
|
|
|
74
|
-
|
|
75
|
-
* spheres
|
|
76
|
-
* axis-aligned boxes
|
|
156
|
+
**NDC convention:** `WEBGL = -1` (z ∈ [−1,1]), `WEBGPU = 0` (z ∈ [0,1]).
|
|
77
157
|
|
|
78
|
-
|
|
158
|
+
```js
|
|
159
|
+
import { mapLocation, mapDirection, WORLD, SCREEN, WEBGL } from '@nakednous/tree'
|
|
160
|
+
|
|
161
|
+
const out = new Float32Array(3)
|
|
162
|
+
const m = {
|
|
163
|
+
proj: /* Float32Array(16) */,
|
|
164
|
+
view: /* Float32Array(16) */,
|
|
165
|
+
pv: /* proj × view */,
|
|
166
|
+
ipv: /* inv(pv) */,
|
|
167
|
+
}
|
|
168
|
+
const vp = [0, height, width, -height]
|
|
169
|
+
|
|
170
|
+
mapLocation(out, worldX, worldY, worldZ, WORLD, SCREEN, m, vp, WEBGL)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
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.
|
|
79
174
|
|
|
80
175
|
---
|
|
81
176
|
|
|
82
|
-
###
|
|
177
|
+
### Visibility testing
|
|
178
|
+
|
|
179
|
+
[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.
|
|
83
180
|
|
|
84
|
-
|
|
181
|
+
```js
|
|
182
|
+
import { frustumPlanes, pointVisibility, sphereVisibility, boxVisibility,
|
|
183
|
+
VISIBLE, SEMIVISIBLE, INVISIBLE } from '@nakednous/tree'
|
|
184
|
+
|
|
185
|
+
const planes = new Float64Array(24)
|
|
186
|
+
frustumPlanes(planes, posX, posY, posZ, vdX, vdY, vdZ,
|
|
187
|
+
upX, upY, upZ, rtX, rtY, rtZ,
|
|
188
|
+
ortho, near, far, left, right, top, bottom)
|
|
189
|
+
|
|
190
|
+
sphereVisibility(planes, cx, cy, cz, radius) // → VISIBLE | SEMIVISIBLE | INVISIBLE
|
|
191
|
+
boxVisibility(planes, x0,y0,z0, x1,y1,z1)
|
|
192
|
+
pointVisibility(planes, px, py, pz)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INVISIBLE` (fully outside).
|
|
85
196
|
|
|
86
197
|
---
|
|
87
198
|
|
|
88
|
-
|
|
199
|
+
### Quaternion and matrix math
|
|
200
|
+
|
|
201
|
+
Exported individually for use in hot paths.
|
|
89
202
|
|
|
90
|
-
|
|
203
|
+
**Quaternions** — `[x,y,z,w]` w-last:
|
|
91
204
|
|
|
92
|
-
|
|
205
|
+
```
|
|
206
|
+
qSet qCopy qDot qNormalize qNegate qMul
|
|
207
|
+
qSlerp qNlerp
|
|
208
|
+
qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
209
|
+
quatToAxisAngle
|
|
210
|
+
```
|
|
93
211
|
|
|
94
|
-
|
|
95
|
-
* interpolating transforms over time
|
|
96
|
-
* determining whether objects are visible
|
|
212
|
+
**Spline / vector:** `catmullRomVec3`, `lerpVec3`
|
|
97
213
|
|
|
98
|
-
|
|
214
|
+
**Mat4:**
|
|
215
|
+
```
|
|
216
|
+
mat4Mul mat4Invert mat4Transpose mat4MulPoint
|
|
217
|
+
mat3NormalFromMat4 mat4Location mat3Direction
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**TRS ↔ mat4:** `transformToMat4`, `mat4ToTransform`
|
|
221
|
+
|
|
222
|
+
**Projection queries** (read from a projection mat4 — no renderer needed):
|
|
223
|
+
```
|
|
224
|
+
projIsOrtho projNear projFar projFov projHfov
|
|
225
|
+
projLeft projRight projTop projBottom
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Pixel ratio:** `pixelRatio(proj, vpH, eyeZ, ndcZMin)` — world-units-per-pixel at a given depth, handles both perspective and orthographic.
|
|
99
229
|
|
|
100
230
|
---
|
|
101
231
|
|
|
102
|
-
|
|
232
|
+
### Constants
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
// Coordinate spaces
|
|
236
|
+
WORLD, EYE, NDC, SCREEN, MODEL, MATRIX
|
|
237
|
+
|
|
238
|
+
// NDC Z convention
|
|
239
|
+
WEBGL // −1 (z ∈ [−1, 1])
|
|
240
|
+
WEBGPU // 0 (z ∈ [0, 1])
|
|
103
241
|
|
|
104
|
-
|
|
242
|
+
// Visibility results
|
|
243
|
+
INVISIBLE, VISIBLE, SEMIVISIBLE
|
|
105
244
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
245
|
+
// Basis vectors (frozen)
|
|
246
|
+
ORIGIN, i, j, k, _i, _j, _k
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Performance contract
|
|
252
|
+
|
|
253
|
+
All hot-path functions follow an **out-first, zero-allocation** contract:
|
|
254
|
+
|
|
255
|
+
- `out` is the first parameter — the caller owns the buffer
|
|
256
|
+
- the function writes into `out` and returns it
|
|
257
|
+
- `null` is returned on degeneracy (singular matrix, etc.)
|
|
258
|
+
- no heap allocations per call
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
// allocate once
|
|
262
|
+
const out = new Float32Array(3)
|
|
263
|
+
const pv = new Float32Array(16)
|
|
264
|
+
const ipv = new Float32Array(16)
|
|
265
|
+
|
|
266
|
+
// per frame — zero allocation
|
|
267
|
+
mat4Mul(pv, proj, view)
|
|
268
|
+
mat4Invert(ipv, pv)
|
|
269
|
+
mapLocation(out, px, py, pz, WORLD, SCREEN, { proj, view, pv, ipv }, vp, WEBGL)
|
|
270
|
+
```
|
|
111
271
|
|
|
112
272
|
---
|
|
113
273
|
|
|
114
274
|
## Relationship to `p5.tree`
|
|
115
275
|
|
|
116
|
-
|
|
276
|
+
[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
277
|
|
|
118
|
-
|
|
278
|
+
`@nakednous/tree` provides the algorithms. The bridge provides the wiring.
|
|
119
279
|
|
|
120
280
|
---
|
|
121
281
|
|
|
122
282
|
## License
|
|
123
283
|
|
|
124
|
-
GPL-3.0-only
|
|
284
|
+
GPL-3.0-only
|
|
125
285
|
© JP Charalambos
|