@nakednous/tree 0.0.8 → 0.0.9
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 +30 -14
- package/dist/index.js +178 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,11 +58,14 @@ track.eval(out) // writes interpolated TRS into out
|
|
|
58
58
|
Interpolation modes:
|
|
59
59
|
|
|
60
60
|
```js
|
|
61
|
-
track.posInterp = '
|
|
61
|
+
track.posInterp = 'hermite' // default — cubic Hermite; auto-computes centripetal
|
|
62
|
+
// Catmull-Rom tangents when none are stored
|
|
62
63
|
track.posInterp = 'linear'
|
|
64
|
+
track.posInterp = 'step' // snap to k0; useful for discrete state changes
|
|
63
65
|
|
|
64
|
-
track.rotInterp = 'slerp'
|
|
65
|
-
track.rotInterp = 'nlerp'
|
|
66
|
+
track.rotInterp = 'slerp' // default — constant angular velocity
|
|
67
|
+
track.rotInterp = 'nlerp' // normalised lerp; cheaper, slightly non-constant speed
|
|
68
|
+
track.rotInterp = 'step' // snap to k0 quaternion
|
|
66
69
|
```
|
|
67
70
|
|
|
68
71
|
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.
|
|
@@ -70,9 +73,19 @@ Playback features: signed `rate` (negative reverses), `loop`, `pingPong`, `seek(
|
|
|
70
73
|
`add()` accepts flexible specs. Top-level forms:
|
|
71
74
|
|
|
72
75
|
```js
|
|
73
|
-
track.add({ pos, rot, scl })
|
|
74
|
-
track.add({
|
|
75
|
-
track.add(
|
|
76
|
+
track.add({ pos, rot, scl }) // explicit TRS — rot accepts any form below
|
|
77
|
+
track.add({ pos, rot, scl, tanIn, tanOut }) // with Hermite tangents (vec3, optional)
|
|
78
|
+
track.add({ mMatrix: mat4 }) // decompose a column-major model matrix into TRS
|
|
79
|
+
track.add([ spec, spec, ... ]) // bulk
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`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 from neighboring keyframes — identical to prior default behavior.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
track.add({ pos:[0,0,0] }) // auto tangents
|
|
86
|
+
track.add({ pos:[100,0,0], tanOut:[0,50,0] }) // leave heading +Y
|
|
87
|
+
track.add({ pos:[200,0,0], tanIn:[0,50,0], tanOut:[-30,0,0] }) // arrive from +Y, leave heading -X
|
|
88
|
+
track.add({ pos:[300,0,0] }) // auto tangents
|
|
76
89
|
```
|
|
77
90
|
|
|
78
91
|
`rot` sub-forms — all normalised internally:
|
|
@@ -116,20 +129,25 @@ track.eval(out)
|
|
|
116
129
|
Interpolation modes:
|
|
117
130
|
|
|
118
131
|
```js
|
|
119
|
-
track.eyeInterp = '
|
|
132
|
+
track.eyeInterp = 'hermite' // default — auto-CR tangents when none stored
|
|
120
133
|
track.eyeInterp = 'linear'
|
|
134
|
+
track.eyeInterp = 'step'
|
|
121
135
|
|
|
122
|
-
track.centerInterp = 'linear'
|
|
123
|
-
track.centerInterp = '
|
|
136
|
+
track.centerInterp = 'linear' // default — suits fixed lookat targets
|
|
137
|
+
track.centerInterp = 'hermite' // smoother when center is also moving freely
|
|
138
|
+
track.centerInterp = 'step'
|
|
124
139
|
```
|
|
125
140
|
|
|
126
141
|
`add()` accepts:
|
|
127
142
|
|
|
128
143
|
```js
|
|
129
|
-
track.add({ eye, center?, up?, fov?, halfHeight
|
|
144
|
+
track.add({ eye, center?, up?, fov?, halfHeight?,
|
|
145
|
+
eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? })
|
|
130
146
|
// fov — vertical fov (radians) for perspective
|
|
131
147
|
// halfHeight — world-unit half-height for ortho
|
|
132
148
|
// both nullable; omit to leave projection unchanged
|
|
149
|
+
// eyeTanIn/Out — Hermite tangents for eye path
|
|
150
|
+
// centerTanIn/Out — Hermite tangents for center path
|
|
133
151
|
track.add({ vMatrix: mat4 }) // view matrix (world→eye); eye reconstructed
|
|
134
152
|
track.add({ eMatrix: mat4 }) // eye matrix (eye→world); eye read from col3
|
|
135
153
|
track.add([ spec, spec, ... ]) // bulk
|
|
@@ -137,9 +155,7 @@ track.add([ spec, spec, ... ]) // bulk
|
|
|
137
155
|
|
|
138
156
|
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.
|
|
139
157
|
|
|
140
|
-
`fov` and `halfHeight` are lerped between keyframes only when both adjacent
|
|
141
|
-
keyframes carry a non-null value for that field. Mixed or null entries pass
|
|
142
|
-
`null` through — the bridge leaves the projection unchanged.
|
|
158
|
+
`fov` and `halfHeight` are lerped between keyframes only when both adjacent keyframes carry a non-null value for that field. Mixed or null entries pass `null` through — the bridge leaves the projection unchanged.
|
|
143
159
|
|
|
144
160
|
---
|
|
145
161
|
|
|
@@ -241,7 +257,7 @@ qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
|
241
257
|
quatToAxisAngle
|
|
242
258
|
```
|
|
243
259
|
|
|
244
|
-
**Spline / vector:** `
|
|
260
|
+
**Spline / vector:** `hermiteVec3`, `lerpVec3`
|
|
245
261
|
|
|
246
262
|
**Mat4:**
|
|
247
263
|
```
|
package/dist/index.js
CHANGED
|
@@ -822,7 +822,7 @@ function applyPickMatrix(proj, px, py, W, H) {
|
|
|
822
822
|
* qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
823
823
|
* quatToAxisAngle
|
|
824
824
|
* Spline / vector helpers
|
|
825
|
-
*
|
|
825
|
+
* hermiteVec3 lerpVec3
|
|
826
826
|
* Transform / mat4 helpers
|
|
827
827
|
* transformToMat4 mat4ToTransform
|
|
828
828
|
* Tracks
|
|
@@ -839,20 +839,22 @@ function applyPickMatrix(proj, px, py, W, H) {
|
|
|
839
839
|
* add() / eval() for their respective data shape.
|
|
840
840
|
*
|
|
841
841
|
* ── Hook architecture ─────────────────────────────────────────────────────────
|
|
842
|
-
*
|
|
843
|
-
*
|
|
842
|
+
* Lib-space hooks (underscore prefix — reserved for host layer / UI layer):
|
|
843
|
+
* _onActivate / _onDeactivate — fire on playing transitions false→true / true→false.
|
|
844
|
+
* _onPlay / _onEnd / _onStop — mirror the user-space hooks; used by the UI layer
|
|
845
|
+
* so it can sync without chaining the public slots.
|
|
844
846
|
*
|
|
845
|
-
*
|
|
847
|
+
* User-space hooks (public):
|
|
846
848
|
* onPlay : fires in play() on false→true transition.
|
|
847
849
|
* onEnd : fires in tick() at natural boundary (once mode only).
|
|
848
850
|
* onStop : fires in stop() / reset() — explicit deactivation.
|
|
849
851
|
* onEnd and onStop are mutually exclusive per event.
|
|
850
852
|
*
|
|
851
853
|
* Firing order:
|
|
852
|
-
* play() → onPlay → _onActivate
|
|
853
|
-
* tick() → onEnd → _onDeactivate
|
|
854
|
-
* stop() → onStop → _onDeactivate
|
|
855
|
-
* reset() → onStop → _onDeactivate
|
|
854
|
+
* play() → onPlay → _onPlay → _onActivate
|
|
855
|
+
* tick() → onEnd → _onEnd → _onDeactivate
|
|
856
|
+
* stop() → onStop → _onStop → _onDeactivate
|
|
857
|
+
* reset() → onStop → _onStop → _onDeactivate
|
|
856
858
|
*
|
|
857
859
|
* ── Playback semantics (rate) ─────────────────────────────────────────────────
|
|
858
860
|
* rate > 0 forward
|
|
@@ -1048,33 +1050,42 @@ function _dist3(a, b) {
|
|
|
1048
1050
|
}
|
|
1049
1051
|
|
|
1050
1052
|
/**
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
* Boundary: p0===p1 or p2===p3 clamps the end tangent.
|
|
1053
|
+
* Cubic Hermite interpolation between p0 and p1 with explicit tangents.
|
|
1054
|
+
* Catmull-Rom is a special case where m0/m1 are auto-computed from neighbors.
|
|
1054
1055
|
* @param {number[]} out 3-element result.
|
|
1055
|
-
* @param {number[]} p0
|
|
1056
|
-
* @param {number[]}
|
|
1057
|
-
* @param {number[]}
|
|
1058
|
-
* @param {number[]}
|
|
1059
|
-
* @param {number} t
|
|
1056
|
+
* @param {number[]} p0 Segment start.
|
|
1057
|
+
* @param {number[]} m0 Outgoing tangent at p0 (world-space, dp/dt scaled to segment).
|
|
1058
|
+
* @param {number[]} p1 Segment end.
|
|
1059
|
+
* @param {number[]} m1 Incoming tangent at p1 (world-space, dp/dt scaled to segment).
|
|
1060
|
+
* @param {number} t Blend [0, 1].
|
|
1060
1061
|
* @returns {number[]} out
|
|
1061
1062
|
*/
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1063
|
+
const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
|
|
1064
|
+
const t2=t*t, t3=t2*t;
|
|
1065
|
+
const h00=2*t3-3*t2+1, h10=t3-2*t2+t, h01=-2*t3+3*t2, h11=t3-t2;
|
|
1066
|
+
out[0]=h00*p0[0]+h10*m0[0]+h01*p1[0]+h11*m1[0];
|
|
1067
|
+
out[1]=h00*p0[1]+h10*m0[1]+h01*p1[1]+h11*m1[1];
|
|
1068
|
+
out[2]=h00*p0[2]+h10*m0[2]+h01*p1[2]+h11*m1[2];
|
|
1069
|
+
return out;
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// Centripetal CR outgoing tangent at p1 for segment p1→p2, scaled by dt1.
|
|
1073
|
+
const _crTanOut = (out, p0, p1, p2, p3) => {
|
|
1074
|
+
const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1; Math.pow(_dist3(p2,p3),0.5)||1;
|
|
1075
|
+
for (let i=0;i<3;i++) out[i]=((p1[i]-p0[i])/dt0-(p2[i]-p0[i])/(dt0+dt1)+(p2[i]-p1[i])/dt1)*dt1;
|
|
1076
|
+
return out;
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// Centripetal CR incoming tangent at p2 for segment p1→p2, scaled by dt1.
|
|
1080
|
+
const _crTanIn = (out, p0, p1, p2, p3) => {
|
|
1081
|
+
Math.pow(_dist3(p0,p1),0.5)||1; const dt1=Math.pow(_dist3(p1,p2),0.5)||1, dt2=Math.pow(_dist3(p2,p3),0.5)||1;
|
|
1082
|
+
for (let i=0;i<3;i++) out[i]=((p2[i]-p1[i])/dt1-(p3[i]-p1[i])/(dt1+dt2)+(p3[i]-p2[i])/dt2)*dt1;
|
|
1075
1083
|
return out;
|
|
1076
1084
|
};
|
|
1077
1085
|
|
|
1086
|
+
// Module-level scratch — shared by eval() across all track instances (non-reentrant hot path).
|
|
1087
|
+
const _m0=[0,0,0], _m1=[0,0,0];
|
|
1088
|
+
|
|
1078
1089
|
/**
|
|
1079
1090
|
* Linear interpolation between two vec3s.
|
|
1080
1091
|
* @param {number[]} out
|
|
@@ -1188,7 +1199,7 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1188
1199
|
*/
|
|
1189
1200
|
function _parseQuat(v) {
|
|
1190
1201
|
if (!v) return null;
|
|
1191
|
-
|
|
1202
|
+
|
|
1192
1203
|
// raw [x,y,z,w] — plain array or typed array
|
|
1193
1204
|
if ((Array.isArray(v) || ArrayBuffer.isView(v)) && v.length === 4) return [v[0], v[1], v[2], v[3]];
|
|
1194
1205
|
|
|
@@ -1280,13 +1291,15 @@ function _parseQuat(v) {
|
|
|
1280
1291
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1281
1292
|
* pos from col3, scl from column lengths, rot from normalised rotation block.
|
|
1282
1293
|
*
|
|
1283
|
-
* { pos
|
|
1294
|
+
* { pos?, rot?, scl?, tanIn?, tanOut? }
|
|
1284
1295
|
* Explicit TRS. pos and scl are vec3, rot accepts any form from _parseQuat.
|
|
1285
1296
|
* All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
|
|
1286
1297
|
* missing rot defaults to identity.
|
|
1298
|
+
* tanIn/tanOut are optional vec3 tangents for Hermite interpolation.
|
|
1299
|
+
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1287
1300
|
*
|
|
1288
1301
|
* @param {Object} spec
|
|
1289
|
-
* @returns {{ pos:number[], rot:number[], scl:number[] }|null}
|
|
1302
|
+
* @returns {{ pos:number[], rot:number[], scl:number[], tanIn:number[]|null, tanOut:number[]|null }|null}
|
|
1290
1303
|
*/
|
|
1291
1304
|
function _parseSpec(spec) {
|
|
1292
1305
|
if (!spec || typeof spec !== 'object') return null;
|
|
@@ -1296,13 +1309,17 @@ function _parseSpec(spec) {
|
|
|
1296
1309
|
const m = (ArrayBuffer.isView(spec.mMatrix) || Array.isArray(spec.mMatrix))
|
|
1297
1310
|
? spec.mMatrix : (spec.mMatrix.mat4 ?? null);
|
|
1298
1311
|
if (!m || m.length < 16) return null;
|
|
1299
|
-
|
|
1312
|
+
const kf = mat4ToTransform({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }, m);
|
|
1313
|
+
kf.tanIn = null; kf.tanOut = null;
|
|
1314
|
+
return kf;
|
|
1300
1315
|
}
|
|
1301
1316
|
|
|
1302
|
-
const pos
|
|
1303
|
-
const rot
|
|
1304
|
-
const scl
|
|
1305
|
-
|
|
1317
|
+
const pos = _parseVec3(spec.pos) || [0,0,0];
|
|
1318
|
+
const rot = _parseQuat(spec.rot) || [0,0,0,1];
|
|
1319
|
+
const scl = _parseVec3(spec.scl) || [1,1,1];
|
|
1320
|
+
const tanIn = _parseVec3(spec.tanIn) || null;
|
|
1321
|
+
const tanOut = _parseVec3(spec.tanOut) || null;
|
|
1322
|
+
return { pos, rot, scl, tanIn, tanOut };
|
|
1306
1323
|
}
|
|
1307
1324
|
|
|
1308
1325
|
function _sameTransform(a, b) {
|
|
@@ -1320,9 +1337,12 @@ function _sameTransform(a, b) {
|
|
|
1320
1337
|
*
|
|
1321
1338
|
* Accepted forms:
|
|
1322
1339
|
*
|
|
1323
|
-
* { eye, center?, up
|
|
1340
|
+
* { eye, center?, up?, fov?, halfHeight?,
|
|
1341
|
+
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
1324
1342
|
* Explicit lookat. center defaults to [0,0,0], up defaults to [0,1,0].
|
|
1325
1343
|
* Both are normalised/stored as-is. eye must be a vec3.
|
|
1344
|
+
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite.
|
|
1345
|
+
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1326
1346
|
*
|
|
1327
1347
|
* { vMatrix: mat4 }
|
|
1328
1348
|
* Column-major view matrix (world→eye).
|
|
@@ -1338,7 +1358,10 @@ function _sameTransform(a, b) {
|
|
|
1338
1358
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1339
1359
|
*
|
|
1340
1360
|
* @param {Object} spec
|
|
1341
|
-
* @returns {{ eye:number[], center:number[], up:number[]
|
|
1361
|
+
* @returns {{ eye:number[], center:number[], up:number[],
|
|
1362
|
+
* fov:number|null, halfHeight:number|null,
|
|
1363
|
+
* eyeTanIn:number[]|null, eyeTanOut:number[]|null,
|
|
1364
|
+
* centerTanIn:number[]|null, centerTanOut:number[]|null }|null}
|
|
1342
1365
|
*/
|
|
1343
1366
|
function _parseCameraSpec(spec) {
|
|
1344
1367
|
if (!spec || typeof spec !== 'object') return null;
|
|
@@ -1354,7 +1377,8 @@ function _parseCameraSpec(spec) {
|
|
|
1354
1377
|
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1355
1378
|
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1356
1379
|
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0],
|
|
1357
|
-
fov:null, halfHeight:null
|
|
1380
|
+
fov:null, halfHeight:null,
|
|
1381
|
+
eyeTanIn:null, eyeTanOut:null, centerTanIn:null, centerTanOut:null };
|
|
1358
1382
|
}
|
|
1359
1383
|
|
|
1360
1384
|
// { eMatrix } — eye matrix (eye→world); eye = col3, forward = -col2
|
|
@@ -1366,21 +1390,26 @@ function _parseCameraSpec(spec) {
|
|
|
1366
1390
|
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1367
1391
|
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1368
1392
|
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0],
|
|
1369
|
-
fov:null, halfHeight:null
|
|
1393
|
+
fov:null, halfHeight:null,
|
|
1394
|
+
eyeTanIn:null, eyeTanOut:null, centerTanIn:null, centerTanOut:null };
|
|
1370
1395
|
}
|
|
1371
1396
|
|
|
1372
1397
|
// { eye, center?, up? } — explicit lookat (eye is a vec3, not a mat4)
|
|
1373
1398
|
const eye = _parseVec3(spec.eye);
|
|
1374
1399
|
if (!eye) return null;
|
|
1375
1400
|
const center = _parseVec3(spec.center) || [0,0,0];
|
|
1376
|
-
const upRaw
|
|
1377
|
-
const up
|
|
1378
|
-
const ul
|
|
1401
|
+
const upRaw = spec.up ? _parseVec3(spec.up) : null;
|
|
1402
|
+
const up = upRaw || [0,1,0];
|
|
1403
|
+
const ul = Math.sqrt(up[0]*up[0]+up[1]*up[1]+up[2]*up[2]) || 1;
|
|
1379
1404
|
return {
|
|
1380
1405
|
eye, center,
|
|
1381
1406
|
up: [up[0]/ul, up[1]/ul, up[2]/ul],
|
|
1382
|
-
fov:
|
|
1383
|
-
halfHeight:
|
|
1407
|
+
fov: typeof spec.fov === 'number' ? spec.fov : null,
|
|
1408
|
+
halfHeight: typeof spec.halfHeight === 'number' ? spec.halfHeight : null,
|
|
1409
|
+
eyeTanIn: _parseVec3(spec.eyeTanIn) || null,
|
|
1410
|
+
eyeTanOut: _parseVec3(spec.eyeTanOut) || null,
|
|
1411
|
+
centerTanIn: _parseVec3(spec.centerTanIn) || null,
|
|
1412
|
+
centerTanOut:_parseVec3(spec.centerTanOut)|| null,
|
|
1384
1413
|
};
|
|
1385
1414
|
}
|
|
1386
1415
|
|
|
@@ -1427,6 +1456,10 @@ class Track {
|
|
|
1427
1456
|
// Lib-space hooks (set by host layer, e.g. p5 bridge)
|
|
1428
1457
|
/** @type {Function|null} */ this._onActivate = null;
|
|
1429
1458
|
/** @type {Function|null} */ this._onDeactivate = null;
|
|
1459
|
+
// Lib-space event mirrors — set by UI layer (trackUI), never touched by user code
|
|
1460
|
+
/** @type {Function|null} */ this._onPlay = null;
|
|
1461
|
+
/** @type {Function|null} */ this._onEnd = null;
|
|
1462
|
+
/** @type {Function|null} */ this._onStop = null;
|
|
1430
1463
|
}
|
|
1431
1464
|
|
|
1432
1465
|
/** Playback rate. Assigning never starts/stops playback. @type {number} */
|
|
@@ -1474,6 +1507,7 @@ class Track {
|
|
|
1474
1507
|
this.playing = true;
|
|
1475
1508
|
if (!wasPlaying) {
|
|
1476
1509
|
if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
|
|
1510
|
+
this._onPlay?.();
|
|
1477
1511
|
this._onActivate?.();
|
|
1478
1512
|
}
|
|
1479
1513
|
return this;
|
|
@@ -1489,6 +1523,7 @@ class Track {
|
|
|
1489
1523
|
this.playing = false;
|
|
1490
1524
|
if (wasPlaying) {
|
|
1491
1525
|
if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
|
|
1526
|
+
this._onStop?.();
|
|
1492
1527
|
this._onDeactivate?.();
|
|
1493
1528
|
if (rewind && this.keyframes.length > 1) this.seek(this._rate < 0 ? 1 : 0);
|
|
1494
1529
|
}
|
|
@@ -1504,6 +1539,7 @@ class Track {
|
|
|
1504
1539
|
this.playing = false;
|
|
1505
1540
|
if (wasPlaying) {
|
|
1506
1541
|
if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
|
|
1542
|
+
this._onStop?.();
|
|
1507
1543
|
this._onDeactivate?.();
|
|
1508
1544
|
}
|
|
1509
1545
|
this.keyframes.length = 0;
|
|
@@ -1614,6 +1650,7 @@ class Track {
|
|
|
1614
1650
|
this._setCursorFromScalar(0);
|
|
1615
1651
|
this.playing = false;
|
|
1616
1652
|
if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
|
|
1653
|
+
this._onEnd?.();
|
|
1617
1654
|
this._onDeactivate?.();
|
|
1618
1655
|
return false;
|
|
1619
1656
|
}
|
|
@@ -1621,6 +1658,7 @@ class Track {
|
|
|
1621
1658
|
this._setCursorFromScalar(total);
|
|
1622
1659
|
this.playing = false;
|
|
1623
1660
|
if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
|
|
1661
|
+
this._onEnd?.();
|
|
1624
1662
|
this._onDeactivate?.();
|
|
1625
1663
|
return false;
|
|
1626
1664
|
}
|
|
@@ -1647,12 +1685,18 @@ class Track {
|
|
|
1647
1685
|
/**
|
|
1648
1686
|
* Renderer-agnostic TRS keyframe track.
|
|
1649
1687
|
*
|
|
1650
|
-
* Keyframe shape: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z]
|
|
1688
|
+
* Keyframe shape: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z],
|
|
1689
|
+
* tanIn?:[x,y,z], tanOut?:[x,y,z] }
|
|
1690
|
+
*
|
|
1691
|
+
* tanIn — incoming position tangent at this keyframe (Hermite mode).
|
|
1692
|
+
* tanOut — outgoing position tangent at this keyframe (Hermite mode).
|
|
1693
|
+
* When only one is supplied, the other mirrors it.
|
|
1694
|
+
* When neither is supplied, centripetal Catmull-Rom tangents are auto-computed.
|
|
1651
1695
|
*
|
|
1652
1696
|
* add() accepts individual specs or a bulk array of specs:
|
|
1653
1697
|
*
|
|
1654
1698
|
* { mMatrix } — full TRS from model matrix
|
|
1655
|
-
* { pos?, rot?, scl? }
|
|
1699
|
+
* { pos?, rot?, scl?, tanIn?, tanOut? } — direct TRS; all fields optional
|
|
1656
1700
|
* { pos?, rot: [x,y,z,w] } — explicit quaternion
|
|
1657
1701
|
* { pos?, rot: { axis, angle } } — axis-angle
|
|
1658
1702
|
* { pos?, rot: { dir, up? } } — look direction
|
|
@@ -1665,16 +1709,15 @@ class Track {
|
|
|
1665
1709
|
* Missing fields default to: pos → [0,0,0], rot → [0,0,0,1], scl → [1,1,1].
|
|
1666
1710
|
*
|
|
1667
1711
|
* eval() writes { pos, rot, scl }:
|
|
1668
|
-
* pos —
|
|
1669
|
-
* rot — slerp (rotInterp='slerp') or nlerp
|
|
1712
|
+
* pos — Hermite (tanIn/tanOut per keyframe; auto-CR when absent) or linear or step
|
|
1713
|
+
* rot — slerp (rotInterp='slerp') or nlerp or step
|
|
1670
1714
|
* scl — lerp
|
|
1671
1715
|
*
|
|
1672
1716
|
* @example
|
|
1673
1717
|
* const track = new PoseTrack()
|
|
1674
|
-
* track.add({ pos:[0,0,0] })
|
|
1675
|
-
* track.add({ pos:[100,0,0],
|
|
1676
|
-
* track.add({
|
|
1677
|
-
* track.add({ mMatrix: someModelMatrix })
|
|
1718
|
+
* track.add({ pos:[0,0,0] })
|
|
1719
|
+
* track.add({ pos:[100,0,0], tanOut:[0,50,0] }) // leave heading +Y
|
|
1720
|
+
* track.add({ pos:[200,0,0] })
|
|
1678
1721
|
* track.play({ loop: true })
|
|
1679
1722
|
* // per frame:
|
|
1680
1723
|
* track.tick()
|
|
@@ -1686,14 +1729,19 @@ class PoseTrack extends Track {
|
|
|
1686
1729
|
super();
|
|
1687
1730
|
/**
|
|
1688
1731
|
* Position interpolation mode.
|
|
1689
|
-
*
|
|
1732
|
+
* - 'hermite' — cubic Hermite; uses tanIn/tanOut per keyframe when present,
|
|
1733
|
+
* auto-computes centripetal Catmull-Rom tangents when absent (default)
|
|
1734
|
+
* - 'linear' — lerp
|
|
1735
|
+
* - 'step' — snap to k0 value; useful for discrete state changes
|
|
1736
|
+
* @type {'hermite'|'linear'|'step'}
|
|
1690
1737
|
*/
|
|
1691
|
-
this.posInterp = '
|
|
1738
|
+
this.posInterp = 'hermite';
|
|
1692
1739
|
/**
|
|
1693
1740
|
* Rotation interpolation mode.
|
|
1694
1741
|
* - 'slerp' — constant angular velocity (default)
|
|
1695
1742
|
* - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
|
|
1696
|
-
*
|
|
1743
|
+
* - 'step' — snap to k0 quaternion; useful for discrete state changes
|
|
1744
|
+
* @type {'slerp'|'nlerp'|'step'}
|
|
1697
1745
|
*/
|
|
1698
1746
|
this.rotInterp = 'slerp';
|
|
1699
1747
|
// Scratch for toMatrix() — avoids hot-path allocations
|
|
@@ -1761,17 +1809,29 @@ class PoseTrack extends Track {
|
|
|
1761
1809
|
const k0 = this.keyframes[seg];
|
|
1762
1810
|
const k1 = this.keyframes[seg + 1];
|
|
1763
1811
|
|
|
1764
|
-
// pos —
|
|
1765
|
-
if (this.posInterp === '
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
catmullRomVec3(out.pos, p0, k0.pos, k1.pos, p3, t);
|
|
1769
|
-
} else {
|
|
1812
|
+
// pos — Hermite (auto-CR tangents when none stored), linear, or step
|
|
1813
|
+
if (this.posInterp === 'step') {
|
|
1814
|
+
out.pos[0]=k0.pos[0]; out.pos[1]=k0.pos[1]; out.pos[2]=k0.pos[2];
|
|
1815
|
+
} else if (this.posInterp === 'linear') {
|
|
1770
1816
|
lerpVec3(out.pos, k0.pos, k1.pos, t);
|
|
1817
|
+
} else {
|
|
1818
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1819
|
+
const p3 = seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1820
|
+
// tanOut on k0: use stored, else symmetric from tanIn, else auto-CR
|
|
1821
|
+
const m0 = k0.tanOut != null ? k0.tanOut
|
|
1822
|
+
: k0.tanIn != null ? k0.tanIn
|
|
1823
|
+
: _crTanOut(_m0, p0, k0.pos, k1.pos, p3);
|
|
1824
|
+
// tanIn on k1: use stored, else symmetric from tanOut, else auto-CR
|
|
1825
|
+
const m1 = k1.tanIn != null ? k1.tanIn
|
|
1826
|
+
: k1.tanOut != null ? k1.tanOut
|
|
1827
|
+
: _crTanIn(_m1, p0, k0.pos, k1.pos, p3);
|
|
1828
|
+
hermiteVec3(out.pos, k0.pos, m0, k1.pos, m1, t);
|
|
1771
1829
|
}
|
|
1772
1830
|
|
|
1773
|
-
// rot — slerp or nlerp
|
|
1774
|
-
if (this.rotInterp === '
|
|
1831
|
+
// rot — step, slerp, or nlerp
|
|
1832
|
+
if (this.rotInterp === 'step') {
|
|
1833
|
+
out.rot[0]=k0.rot[0]; out.rot[1]=k0.rot[1]; out.rot[2]=k0.rot[2]; out.rot[3]=k0.rot[3];
|
|
1834
|
+
} else if (this.rotInterp === 'nlerp') {
|
|
1775
1835
|
qNlerp(out.rot, k0.rot, k1.rot, t);
|
|
1776
1836
|
} else {
|
|
1777
1837
|
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
@@ -1801,23 +1861,31 @@ class PoseTrack extends Track {
|
|
|
1801
1861
|
/**
|
|
1802
1862
|
* Lookat camera keyframe track.
|
|
1803
1863
|
*
|
|
1804
|
-
* Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z],
|
|
1864
|
+
* Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z],
|
|
1865
|
+
* fov?:number, halfHeight?:number,
|
|
1866
|
+
* eyeTanIn?:[x,y,z], eyeTanOut?:[x,y,z],
|
|
1867
|
+
* centerTanIn?:[x,y,z], centerTanOut?:[x,y,z] }
|
|
1805
1868
|
*
|
|
1806
1869
|
* fov — vertical fov (radians) for perspective cameras; null for ortho.
|
|
1807
1870
|
* halfHeight — world-unit half-height of ortho frustum; null for perspective.
|
|
1808
1871
|
* Both are optional and nullable. eval() lerps each only when both adjacent
|
|
1809
1872
|
* keyframes carry a non-null value for that field.
|
|
1810
1873
|
*
|
|
1874
|
+
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite
|
|
1875
|
+
* interpolation of the eye and center paths respectively.
|
|
1876
|
+
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1877
|
+
*
|
|
1811
1878
|
* Each field is independently interpolated — eye and center along their
|
|
1812
1879
|
* own paths, up nlerped on the unit sphere. This correctly handles cameras
|
|
1813
1880
|
* that always look at a fixed target (center stays at origin throughout)
|
|
1814
1881
|
* as well as free-fly paths where center moves independently.
|
|
1815
|
-
*
|
|
1882
|
+
*
|
|
1816
1883
|
* Missing fields default to: center → [0,0,0], up → [0,1,0].
|
|
1817
1884
|
*
|
|
1818
1885
|
* add() accepts individual specs or a bulk array of specs:
|
|
1819
1886
|
*
|
|
1820
|
-
* { eye, center?, up?, fov?, halfHeight
|
|
1887
|
+
* { eye, center?, up?, fov?, halfHeight?,
|
|
1888
|
+
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
1821
1889
|
* explicit lookat; center defaults to [0,0,0], up to [0,1,0].
|
|
1822
1890
|
* fov and halfHeight are mutually exclusive nullable scalars.
|
|
1823
1891
|
* { vMatrix: mat4 } view matrix (world→eye); eye reconstructed via -R^T·t
|
|
@@ -1831,8 +1899,8 @@ class PoseTrack extends Track {
|
|
|
1831
1899
|
* Use capturePose() (p5.tree bridge) when the real up hint is needed.
|
|
1832
1900
|
*
|
|
1833
1901
|
* eval() writes { eye, center, up, fov, halfHeight }:
|
|
1834
|
-
* eye —
|
|
1835
|
-
* center —
|
|
1902
|
+
* eye — Hermite (auto-CR when no tangents stored) or linear or step
|
|
1903
|
+
* center — Hermite (auto-CR when no tangents stored) or linear or step
|
|
1836
1904
|
* up — nlerp (normalize-after-lerp on unit sphere)
|
|
1837
1905
|
* fov — lerp when both keyframes carry non-null fov; else null
|
|
1838
1906
|
* halfHeight — lerp when both keyframes carry non-null halfHeight; else null
|
|
@@ -1857,14 +1925,20 @@ class CameraTrack extends Track {
|
|
|
1857
1925
|
super();
|
|
1858
1926
|
/**
|
|
1859
1927
|
* Eye position interpolation mode.
|
|
1860
|
-
*
|
|
1928
|
+
* - 'hermite' — cubic Hermite; auto-CR tangents when none stored (default)
|
|
1929
|
+
* - 'linear' — lerp
|
|
1930
|
+
* - 'step' — snap to k0 eye
|
|
1931
|
+
* @type {'hermite'|'linear'|'step'}
|
|
1861
1932
|
*/
|
|
1862
|
-
this.eyeInterp = '
|
|
1933
|
+
this.eyeInterp = 'hermite';
|
|
1863
1934
|
/**
|
|
1864
1935
|
* Center (lookat target) interpolation mode.
|
|
1865
|
-
* 'linear' suits fixed or predictably moving targets.
|
|
1866
|
-
* '
|
|
1867
|
-
*
|
|
1936
|
+
* 'linear' suits fixed or predictably moving targets (default).
|
|
1937
|
+
* 'hermite' gives smoother paths when center is also flying freely.
|
|
1938
|
+
* - 'hermite' — cubic Hermite; auto-CR tangents when none stored
|
|
1939
|
+
* - 'linear' — lerp
|
|
1940
|
+
* - 'step' — snap to k0 center
|
|
1941
|
+
* @type {'hermite'|'linear'|'step'}
|
|
1868
1942
|
*/
|
|
1869
1943
|
this.centerInterp = 'linear';
|
|
1870
1944
|
// Scratch for toCamera() — avoids hot-path allocations
|
|
@@ -1937,22 +2011,38 @@ class CameraTrack extends Track {
|
|
|
1937
2011
|
const k0 = this.keyframes[seg];
|
|
1938
2012
|
const k1 = this.keyframes[seg + 1];
|
|
1939
2013
|
|
|
1940
|
-
// eye —
|
|
1941
|
-
if (this.eyeInterp === '
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
catmullRomVec3(out.eye, p0, k0.eye, k1.eye, p3, t);
|
|
1945
|
-
} else {
|
|
2014
|
+
// eye — Hermite (auto-CR tangents when none stored), linear, or step
|
|
2015
|
+
if (this.eyeInterp === 'step') {
|
|
2016
|
+
out.eye[0]=k0.eye[0]; out.eye[1]=k0.eye[1]; out.eye[2]=k0.eye[2];
|
|
2017
|
+
} else if (this.eyeInterp === 'linear') {
|
|
1946
2018
|
lerpVec3(out.eye, k0.eye, k1.eye, t);
|
|
2019
|
+
} else {
|
|
2020
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].eye : k0.eye;
|
|
2021
|
+
const p3 = seg + 2 < n ? this.keyframes[seg + 2].eye : k1.eye;
|
|
2022
|
+
const m0 = k0.eyeTanOut != null ? k0.eyeTanOut
|
|
2023
|
+
: k0.eyeTanIn != null ? k0.eyeTanIn
|
|
2024
|
+
: _crTanOut(_m0, p0, k0.eye, k1.eye, p3);
|
|
2025
|
+
const m1 = k1.eyeTanIn != null ? k1.eyeTanIn
|
|
2026
|
+
: k1.eyeTanOut != null ? k1.eyeTanOut
|
|
2027
|
+
: _crTanIn(_m1, p0, k0.eye, k1.eye, p3);
|
|
2028
|
+
hermiteVec3(out.eye, k0.eye, m0, k1.eye, m1, t);
|
|
1947
2029
|
}
|
|
1948
2030
|
|
|
1949
|
-
// center —
|
|
1950
|
-
if (this.centerInterp === '
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
catmullRomVec3(out.center, c0, k0.center, k1.center, c3, t);
|
|
1954
|
-
} else {
|
|
2031
|
+
// center — Hermite, linear, or step (independent lookat target)
|
|
2032
|
+
if (this.centerInterp === 'step') {
|
|
2033
|
+
out.center[0]=k0.center[0]; out.center[1]=k0.center[1]; out.center[2]=k0.center[2];
|
|
2034
|
+
} else if (this.centerInterp === 'linear') {
|
|
1955
2035
|
lerpVec3(out.center, k0.center, k1.center, t);
|
|
2036
|
+
} else {
|
|
2037
|
+
const c0 = seg > 0 ? this.keyframes[seg - 1].center : k0.center;
|
|
2038
|
+
const c3 = seg + 2 < n ? this.keyframes[seg + 2].center : k1.center;
|
|
2039
|
+
const m0 = k0.centerTanOut != null ? k0.centerTanOut
|
|
2040
|
+
: k0.centerTanIn != null ? k0.centerTanIn
|
|
2041
|
+
: _crTanOut(_m0, c0, k0.center, k1.center, c3);
|
|
2042
|
+
const m1 = k1.centerTanIn != null ? k1.centerTanIn
|
|
2043
|
+
: k1.centerTanOut != null ? k1.centerTanOut
|
|
2044
|
+
: _crTanIn(_m1, c0, k0.center, k1.center, c3);
|
|
2045
|
+
hermiteVec3(out.center, k0.center, m0, k1.center, m1, t);
|
|
1956
2046
|
}
|
|
1957
2047
|
|
|
1958
2048
|
// up — nlerp (normalize after lerp; correct for typical near-upright cameras)
|
|
@@ -2130,5 +2220,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
2130
2220
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
2131
2221
|
}
|
|
2132
2222
|
|
|
2133
|
-
export { CameraTrack, EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, applyPickMatrix, boxVisibility,
|
|
2223
|
+
export { CameraTrack, EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, applyPickMatrix, boxVisibility, distanceToPlane, frustumPlanes, hermiteVec3, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulPoint, mat4PV, mat4ToTransform, mat4Transpose, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNlerp, qNormalize, qSet, qSlerp, qToMat4, quatToAxisAngle, sphereVisibility, transformToMat4 };
|
|
2134
2224
|
//# sourceMappingURL=index.js.map
|