@nakednous/tree 0.0.8 → 0.0.10
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 +209 -100
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,22 +839,33 @@ 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
|
+
* ── Loop modes ────────────────────────────────────────────────────────────────
|
|
860
|
+
* once loop=false, bounce=false — stop at end (fires onEnd)
|
|
861
|
+
* repeat loop=true, bounce=false — wrap back to start
|
|
862
|
+
* bounce loop=true, bounce=true — bounce at boundaries
|
|
863
|
+
*
|
|
864
|
+
* Exclusivity enforced in play():
|
|
865
|
+
* bounce: true → loop is also set true
|
|
866
|
+
* loop: false → bounce is also cleared
|
|
867
|
+
*
|
|
868
|
+
* ── Playback semantics (rate + _dir) ─────────────────────────────────────────
|
|
858
869
|
* rate > 0 forward
|
|
859
870
|
* rate < 0 backward
|
|
860
871
|
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
@@ -863,6 +874,12 @@ function applyPickMatrix(proj, px, py, W, H) {
|
|
|
863
874
|
* stop() is the sole setter of playing = false.
|
|
864
875
|
* Assigning rate never starts or stops playback.
|
|
865
876
|
*
|
|
877
|
+
* _dir (internal, ±1) tracks the current bounce travel direction.
|
|
878
|
+
* tick() advances by rate * _dir and flips _dir at boundaries.
|
|
879
|
+
* rate always holds the user-set value — it is never mutated by bounce.
|
|
880
|
+
* _dir is reset to 1 only in reset() (keyframes cleared) — stop/replay
|
|
881
|
+
* preserves the current travel direction.
|
|
882
|
+
*
|
|
866
883
|
* ── One-keyframe behaviour ────────────────────────────────────────────────────
|
|
867
884
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
868
885
|
* setting playing = true and without firing hooks.
|
|
@@ -1048,33 +1065,42 @@ function _dist3(a, b) {
|
|
|
1048
1065
|
}
|
|
1049
1066
|
|
|
1050
1067
|
/**
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
* Boundary: p0===p1 or p2===p3 clamps the end tangent.
|
|
1068
|
+
* Cubic Hermite interpolation between p0 and p1 with explicit tangents.
|
|
1069
|
+
* Catmull-Rom is a special case where m0/m1 are auto-computed from neighbors.
|
|
1054
1070
|
* @param {number[]} out 3-element result.
|
|
1055
|
-
* @param {number[]} p0
|
|
1056
|
-
* @param {number[]}
|
|
1057
|
-
* @param {number[]}
|
|
1058
|
-
* @param {number[]}
|
|
1059
|
-
* @param {number} t
|
|
1071
|
+
* @param {number[]} p0 Segment start.
|
|
1072
|
+
* @param {number[]} m0 Outgoing tangent at p0 (world-space, dp/dt scaled to segment).
|
|
1073
|
+
* @param {number[]} p1 Segment end.
|
|
1074
|
+
* @param {number[]} m1 Incoming tangent at p1 (world-space, dp/dt scaled to segment).
|
|
1075
|
+
* @param {number} t Blend [0, 1].
|
|
1060
1076
|
* @returns {number[]} out
|
|
1061
1077
|
*/
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1078
|
+
const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
|
|
1079
|
+
const t2=t*t, t3=t2*t;
|
|
1080
|
+
const h00=2*t3-3*t2+1, h10=t3-2*t2+t, h01=-2*t3+3*t2, h11=t3-t2;
|
|
1081
|
+
out[0]=h00*p0[0]+h10*m0[0]+h01*p1[0]+h11*m1[0];
|
|
1082
|
+
out[1]=h00*p0[1]+h10*m0[1]+h01*p1[1]+h11*m1[1];
|
|
1083
|
+
out[2]=h00*p0[2]+h10*m0[2]+h01*p1[2]+h11*m1[2];
|
|
1084
|
+
return out;
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// Centripetal CR outgoing tangent at p1 for segment p1→p2, scaled by dt1.
|
|
1088
|
+
const _crTanOut = (out, p0, p1, p2, p3) => {
|
|
1089
|
+
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;
|
|
1090
|
+
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;
|
|
1075
1091
|
return out;
|
|
1076
1092
|
};
|
|
1077
1093
|
|
|
1094
|
+
// Centripetal CR incoming tangent at p2 for segment p1→p2, scaled by dt1.
|
|
1095
|
+
const _crTanIn = (out, p0, p1, p2, p3) => {
|
|
1096
|
+
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;
|
|
1097
|
+
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;
|
|
1098
|
+
return out;
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
// Module-level scratch — shared by eval() across all track instances (non-reentrant hot path).
|
|
1102
|
+
const _m0=[0,0,0], _m1=[0,0,0];
|
|
1103
|
+
|
|
1078
1104
|
/**
|
|
1079
1105
|
* Linear interpolation between two vec3s.
|
|
1080
1106
|
* @param {number[]} out
|
|
@@ -1188,7 +1214,7 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1188
1214
|
*/
|
|
1189
1215
|
function _parseQuat(v) {
|
|
1190
1216
|
if (!v) return null;
|
|
1191
|
-
|
|
1217
|
+
|
|
1192
1218
|
// raw [x,y,z,w] — plain array or typed array
|
|
1193
1219
|
if ((Array.isArray(v) || ArrayBuffer.isView(v)) && v.length === 4) return [v[0], v[1], v[2], v[3]];
|
|
1194
1220
|
|
|
@@ -1280,13 +1306,15 @@ function _parseQuat(v) {
|
|
|
1280
1306
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1281
1307
|
* pos from col3, scl from column lengths, rot from normalised rotation block.
|
|
1282
1308
|
*
|
|
1283
|
-
* { pos
|
|
1309
|
+
* { pos?, rot?, scl?, tanIn?, tanOut? }
|
|
1284
1310
|
* Explicit TRS. pos and scl are vec3, rot accepts any form from _parseQuat.
|
|
1285
1311
|
* All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
|
|
1286
1312
|
* missing rot defaults to identity.
|
|
1313
|
+
* tanIn/tanOut are optional vec3 tangents for Hermite interpolation.
|
|
1314
|
+
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1287
1315
|
*
|
|
1288
1316
|
* @param {Object} spec
|
|
1289
|
-
* @returns {{ pos:number[], rot:number[], scl:number[] }|null}
|
|
1317
|
+
* @returns {{ pos:number[], rot:number[], scl:number[], tanIn:number[]|null, tanOut:number[]|null }|null}
|
|
1290
1318
|
*/
|
|
1291
1319
|
function _parseSpec(spec) {
|
|
1292
1320
|
if (!spec || typeof spec !== 'object') return null;
|
|
@@ -1296,13 +1324,17 @@ function _parseSpec(spec) {
|
|
|
1296
1324
|
const m = (ArrayBuffer.isView(spec.mMatrix) || Array.isArray(spec.mMatrix))
|
|
1297
1325
|
? spec.mMatrix : (spec.mMatrix.mat4 ?? null);
|
|
1298
1326
|
if (!m || m.length < 16) return null;
|
|
1299
|
-
|
|
1327
|
+
const kf = mat4ToTransform({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }, m);
|
|
1328
|
+
kf.tanIn = null; kf.tanOut = null;
|
|
1329
|
+
return kf;
|
|
1300
1330
|
}
|
|
1301
1331
|
|
|
1302
|
-
const pos
|
|
1303
|
-
const rot
|
|
1304
|
-
const scl
|
|
1305
|
-
|
|
1332
|
+
const pos = _parseVec3(spec.pos) || [0,0,0];
|
|
1333
|
+
const rot = _parseQuat(spec.rot) || [0,0,0,1];
|
|
1334
|
+
const scl = _parseVec3(spec.scl) || [1,1,1];
|
|
1335
|
+
const tanIn = _parseVec3(spec.tanIn) || null;
|
|
1336
|
+
const tanOut = _parseVec3(spec.tanOut) || null;
|
|
1337
|
+
return { pos, rot, scl, tanIn, tanOut };
|
|
1306
1338
|
}
|
|
1307
1339
|
|
|
1308
1340
|
function _sameTransform(a, b) {
|
|
@@ -1320,9 +1352,12 @@ function _sameTransform(a, b) {
|
|
|
1320
1352
|
*
|
|
1321
1353
|
* Accepted forms:
|
|
1322
1354
|
*
|
|
1323
|
-
* { eye, center?, up
|
|
1355
|
+
* { eye, center?, up?, fov?, halfHeight?,
|
|
1356
|
+
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
1324
1357
|
* Explicit lookat. center defaults to [0,0,0], up defaults to [0,1,0].
|
|
1325
1358
|
* Both are normalised/stored as-is. eye must be a vec3.
|
|
1359
|
+
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite.
|
|
1360
|
+
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1326
1361
|
*
|
|
1327
1362
|
* { vMatrix: mat4 }
|
|
1328
1363
|
* Column-major view matrix (world→eye).
|
|
@@ -1338,7 +1373,10 @@ function _sameTransform(a, b) {
|
|
|
1338
1373
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1339
1374
|
*
|
|
1340
1375
|
* @param {Object} spec
|
|
1341
|
-
* @returns {{ eye:number[], center:number[], up:number[]
|
|
1376
|
+
* @returns {{ eye:number[], center:number[], up:number[],
|
|
1377
|
+
* fov:number|null, halfHeight:number|null,
|
|
1378
|
+
* eyeTanIn:number[]|null, eyeTanOut:number[]|null,
|
|
1379
|
+
* centerTanIn:number[]|null, centerTanOut:number[]|null }|null}
|
|
1342
1380
|
*/
|
|
1343
1381
|
function _parseCameraSpec(spec) {
|
|
1344
1382
|
if (!spec || typeof spec !== 'object') return null;
|
|
@@ -1354,7 +1392,8 @@ function _parseCameraSpec(spec) {
|
|
|
1354
1392
|
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1355
1393
|
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1356
1394
|
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0],
|
|
1357
|
-
fov:null, halfHeight:null
|
|
1395
|
+
fov:null, halfHeight:null,
|
|
1396
|
+
eyeTanIn:null, eyeTanOut:null, centerTanIn:null, centerTanOut:null };
|
|
1358
1397
|
}
|
|
1359
1398
|
|
|
1360
1399
|
// { eMatrix } — eye matrix (eye→world); eye = col3, forward = -col2
|
|
@@ -1366,21 +1405,26 @@ function _parseCameraSpec(spec) {
|
|
|
1366
1405
|
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1367
1406
|
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1368
1407
|
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0],
|
|
1369
|
-
fov:null, halfHeight:null
|
|
1408
|
+
fov:null, halfHeight:null,
|
|
1409
|
+
eyeTanIn:null, eyeTanOut:null, centerTanIn:null, centerTanOut:null };
|
|
1370
1410
|
}
|
|
1371
1411
|
|
|
1372
1412
|
// { eye, center?, up? } — explicit lookat (eye is a vec3, not a mat4)
|
|
1373
1413
|
const eye = _parseVec3(spec.eye);
|
|
1374
1414
|
if (!eye) return null;
|
|
1375
1415
|
const center = _parseVec3(spec.center) || [0,0,0];
|
|
1376
|
-
const upRaw
|
|
1377
|
-
const up
|
|
1378
|
-
const ul
|
|
1416
|
+
const upRaw = spec.up ? _parseVec3(spec.up) : null;
|
|
1417
|
+
const up = upRaw || [0,1,0];
|
|
1418
|
+
const ul = Math.sqrt(up[0]*up[0]+up[1]*up[1]+up[2]*up[2]) || 1;
|
|
1379
1419
|
return {
|
|
1380
1420
|
eye, center,
|
|
1381
1421
|
up: [up[0]/ul, up[1]/ul, up[2]/ul],
|
|
1382
|
-
fov:
|
|
1383
|
-
halfHeight:
|
|
1422
|
+
fov: typeof spec.fov === 'number' ? spec.fov : null,
|
|
1423
|
+
halfHeight: typeof spec.halfHeight === 'number' ? spec.halfHeight : null,
|
|
1424
|
+
eyeTanIn: _parseVec3(spec.eyeTanIn) || null,
|
|
1425
|
+
eyeTanOut: _parseVec3(spec.eyeTanOut) || null,
|
|
1426
|
+
centerTanIn: _parseVec3(spec.centerTanIn) || null,
|
|
1427
|
+
centerTanOut:_parseVec3(spec.centerTanOut)|| null,
|
|
1384
1428
|
};
|
|
1385
1429
|
}
|
|
1386
1430
|
|
|
@@ -1408,7 +1452,7 @@ class Track {
|
|
|
1408
1452
|
/** Loop at boundaries. @type {boolean} */
|
|
1409
1453
|
this.loop = false;
|
|
1410
1454
|
/** Ping-pong bounce (takes precedence over loop). @type {boolean} */
|
|
1411
|
-
this.
|
|
1455
|
+
this.bounce = false;
|
|
1412
1456
|
/** Frames per segment (≥1). @type {number} */
|
|
1413
1457
|
this.duration = 30;
|
|
1414
1458
|
/** Current segment index. @type {number} */
|
|
@@ -1418,6 +1462,10 @@ class Track {
|
|
|
1418
1462
|
|
|
1419
1463
|
// Internal rate — never directly starts/stops playback
|
|
1420
1464
|
this._rate = 1;
|
|
1465
|
+
// Internal bounce direction: +1 forward, -1 backward.
|
|
1466
|
+
// Flipped by tick() at boundaries. Never exposed publicly.
|
|
1467
|
+
// rate always holds the user-set value — only _dir changes.
|
|
1468
|
+
this._dir = 1;
|
|
1421
1469
|
|
|
1422
1470
|
// User-space hooks
|
|
1423
1471
|
/** @type {Function|null} */ this.onPlay = null;
|
|
@@ -1427,6 +1475,10 @@ class Track {
|
|
|
1427
1475
|
// Lib-space hooks (set by host layer, e.g. p5 bridge)
|
|
1428
1476
|
/** @type {Function|null} */ this._onActivate = null;
|
|
1429
1477
|
/** @type {Function|null} */ this._onDeactivate = null;
|
|
1478
|
+
// Lib-space event mirrors — set by UI layer (trackUI), never touched by user code
|
|
1479
|
+
/** @type {Function|null} */ this._onPlay = null;
|
|
1480
|
+
/** @type {Function|null} */ this._onEnd = null;
|
|
1481
|
+
/** @type {Function|null} */ this._onStop = null;
|
|
1430
1482
|
}
|
|
1431
1483
|
|
|
1432
1484
|
/** Playback rate. Assigning never starts/stops playback. @type {number} */
|
|
@@ -1439,7 +1491,7 @@ class Track {
|
|
|
1439
1491
|
/**
|
|
1440
1492
|
* Start or update playback.
|
|
1441
1493
|
* @param {number|Object} [rateOrOpts] Numeric rate or options object:
|
|
1442
|
-
* { rate, duration, loop,
|
|
1494
|
+
* { rate, duration, loop, bounce, onPlay, onEnd, onStop }
|
|
1443
1495
|
* @returns {Track} this
|
|
1444
1496
|
*/
|
|
1445
1497
|
play(rateOrOpts) {
|
|
@@ -1455,9 +1507,9 @@ class Track {
|
|
|
1455
1507
|
this._rate = rateOrOpts;
|
|
1456
1508
|
} else if (rateOrOpts && typeof rateOrOpts === 'object') {
|
|
1457
1509
|
const o = rateOrOpts;
|
|
1458
|
-
if (_isNum(o.duration)) this.duration
|
|
1459
|
-
if ('loop' in o)
|
|
1460
|
-
if ('
|
|
1510
|
+
if (_isNum(o.duration)) this.duration = Math.max(1, o.duration | 0);
|
|
1511
|
+
if ('loop' in o) { this.loop = !!o.loop; if (!this.loop) this.bounce = false; }
|
|
1512
|
+
if ('bounce' in o) { this.bounce = !!o.bounce; if (this.bounce) this.loop = true; }
|
|
1461
1513
|
if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
|
|
1462
1514
|
if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
|
|
1463
1515
|
if (typeof o.onStop === 'function') this.onStop = o.onStop;
|
|
@@ -1474,6 +1526,7 @@ class Track {
|
|
|
1474
1526
|
this.playing = true;
|
|
1475
1527
|
if (!wasPlaying) {
|
|
1476
1528
|
if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
|
|
1529
|
+
this._onPlay?.();
|
|
1477
1530
|
this._onActivate?.();
|
|
1478
1531
|
}
|
|
1479
1532
|
return this;
|
|
@@ -1489,8 +1542,9 @@ class Track {
|
|
|
1489
1542
|
this.playing = false;
|
|
1490
1543
|
if (wasPlaying) {
|
|
1491
1544
|
if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
|
|
1545
|
+
this._onStop?.();
|
|
1492
1546
|
this._onDeactivate?.();
|
|
1493
|
-
if (rewind && this.keyframes.length > 1) this.seek(this._rate < 0 ? 1 : 0);
|
|
1547
|
+
if (rewind && this.keyframes.length > 1) this.seek(this._rate * this._dir < 0 ? 1 : 0);
|
|
1494
1548
|
}
|
|
1495
1549
|
return this;
|
|
1496
1550
|
}
|
|
@@ -1504,10 +1558,11 @@ class Track {
|
|
|
1504
1558
|
this.playing = false;
|
|
1505
1559
|
if (wasPlaying) {
|
|
1506
1560
|
if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
|
|
1561
|
+
this._onStop?.();
|
|
1507
1562
|
this._onDeactivate?.();
|
|
1508
1563
|
}
|
|
1509
1564
|
this.keyframes.length = 0;
|
|
1510
|
-
this.seg = 0; this.f = 0;
|
|
1565
|
+
this.seg = 0; this.f = 0; this._dir = 1;
|
|
1511
1566
|
return this;
|
|
1512
1567
|
}
|
|
1513
1568
|
|
|
@@ -1569,7 +1624,7 @@ class Track {
|
|
|
1569
1624
|
f: this.f,
|
|
1570
1625
|
playing: this.playing,
|
|
1571
1626
|
loop: this.loop,
|
|
1572
|
-
|
|
1627
|
+
bounce: this.bounce,
|
|
1573
1628
|
rate: this._rate,
|
|
1574
1629
|
duration: this.duration,
|
|
1575
1630
|
time: this.segments > 0 ? this.time() : 0
|
|
@@ -1592,15 +1647,15 @@ class Track {
|
|
|
1592
1647
|
const dur = Math.max(1, this.duration | 0);
|
|
1593
1648
|
const total = nSeg * dur;
|
|
1594
1649
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1595
|
-
const next = s + this._rate;
|
|
1650
|
+
const next = s + this._rate * this._dir;
|
|
1596
1651
|
|
|
1597
|
-
if (this.
|
|
1652
|
+
if (this.bounce) {
|
|
1598
1653
|
let pos = next, flips = 0;
|
|
1599
1654
|
while (pos < 0 || pos > total) {
|
|
1600
1655
|
if (pos < 0) { pos = -pos; flips++; }
|
|
1601
1656
|
else { pos = 2 * total - pos; flips++; }
|
|
1602
1657
|
}
|
|
1603
|
-
if (flips & 1) this.
|
|
1658
|
+
if (flips & 1) this._dir = -this._dir;
|
|
1604
1659
|
this._setCursorFromScalar(pos);
|
|
1605
1660
|
return true;
|
|
1606
1661
|
}
|
|
@@ -1614,6 +1669,7 @@ class Track {
|
|
|
1614
1669
|
this._setCursorFromScalar(0);
|
|
1615
1670
|
this.playing = false;
|
|
1616
1671
|
if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
|
|
1672
|
+
this._onEnd?.();
|
|
1617
1673
|
this._onDeactivate?.();
|
|
1618
1674
|
return false;
|
|
1619
1675
|
}
|
|
@@ -1621,6 +1677,7 @@ class Track {
|
|
|
1621
1677
|
this._setCursorFromScalar(total);
|
|
1622
1678
|
this.playing = false;
|
|
1623
1679
|
if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
|
|
1680
|
+
this._onEnd?.();
|
|
1624
1681
|
this._onDeactivate?.();
|
|
1625
1682
|
return false;
|
|
1626
1683
|
}
|
|
@@ -1647,12 +1704,18 @@ class Track {
|
|
|
1647
1704
|
/**
|
|
1648
1705
|
* Renderer-agnostic TRS keyframe track.
|
|
1649
1706
|
*
|
|
1650
|
-
* Keyframe shape: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z]
|
|
1707
|
+
* Keyframe shape: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z],
|
|
1708
|
+
* tanIn?:[x,y,z], tanOut?:[x,y,z] }
|
|
1709
|
+
*
|
|
1710
|
+
* tanIn — incoming position tangent at this keyframe (Hermite mode).
|
|
1711
|
+
* tanOut — outgoing position tangent at this keyframe (Hermite mode).
|
|
1712
|
+
* When only one is supplied, the other mirrors it.
|
|
1713
|
+
* When neither is supplied, centripetal Catmull-Rom tangents are auto-computed.
|
|
1651
1714
|
*
|
|
1652
1715
|
* add() accepts individual specs or a bulk array of specs:
|
|
1653
1716
|
*
|
|
1654
1717
|
* { mMatrix } — full TRS from model matrix
|
|
1655
|
-
* { pos?, rot?, scl? }
|
|
1718
|
+
* { pos?, rot?, scl?, tanIn?, tanOut? } — direct TRS; all fields optional
|
|
1656
1719
|
* { pos?, rot: [x,y,z,w] } — explicit quaternion
|
|
1657
1720
|
* { pos?, rot: { axis, angle } } — axis-angle
|
|
1658
1721
|
* { pos?, rot: { dir, up? } } — look direction
|
|
@@ -1665,16 +1728,15 @@ class Track {
|
|
|
1665
1728
|
* Missing fields default to: pos → [0,0,0], rot → [0,0,0,1], scl → [1,1,1].
|
|
1666
1729
|
*
|
|
1667
1730
|
* eval() writes { pos, rot, scl }:
|
|
1668
|
-
* pos —
|
|
1669
|
-
* rot — slerp (rotInterp='slerp') or nlerp
|
|
1731
|
+
* pos — Hermite (tanIn/tanOut per keyframe; auto-CR when absent) or linear or step
|
|
1732
|
+
* rot — slerp (rotInterp='slerp') or nlerp or step
|
|
1670
1733
|
* scl — lerp
|
|
1671
1734
|
*
|
|
1672
1735
|
* @example
|
|
1673
1736
|
* 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 })
|
|
1737
|
+
* track.add({ pos:[0,0,0] })
|
|
1738
|
+
* track.add({ pos:[100,0,0], tanOut:[0,50,0] }) // leave heading +Y
|
|
1739
|
+
* track.add({ pos:[200,0,0] })
|
|
1678
1740
|
* track.play({ loop: true })
|
|
1679
1741
|
* // per frame:
|
|
1680
1742
|
* track.tick()
|
|
@@ -1686,14 +1748,19 @@ class PoseTrack extends Track {
|
|
|
1686
1748
|
super();
|
|
1687
1749
|
/**
|
|
1688
1750
|
* Position interpolation mode.
|
|
1689
|
-
*
|
|
1751
|
+
* - 'hermite' — cubic Hermite; uses tanIn/tanOut per keyframe when present,
|
|
1752
|
+
* auto-computes centripetal Catmull-Rom tangents when absent (default)
|
|
1753
|
+
* - 'linear' — lerp
|
|
1754
|
+
* - 'step' — snap to k0 value; useful for discrete state changes
|
|
1755
|
+
* @type {'hermite'|'linear'|'step'}
|
|
1690
1756
|
*/
|
|
1691
|
-
this.posInterp = '
|
|
1757
|
+
this.posInterp = 'hermite';
|
|
1692
1758
|
/**
|
|
1693
1759
|
* Rotation interpolation mode.
|
|
1694
1760
|
* - 'slerp' — constant angular velocity (default)
|
|
1695
1761
|
* - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
|
|
1696
|
-
*
|
|
1762
|
+
* - 'step' — snap to k0 quaternion; useful for discrete state changes
|
|
1763
|
+
* @type {'slerp'|'nlerp'|'step'}
|
|
1697
1764
|
*/
|
|
1698
1765
|
this.rotInterp = 'slerp';
|
|
1699
1766
|
// Scratch for toMatrix() — avoids hot-path allocations
|
|
@@ -1761,17 +1828,29 @@ class PoseTrack extends Track {
|
|
|
1761
1828
|
const k0 = this.keyframes[seg];
|
|
1762
1829
|
const k1 = this.keyframes[seg + 1];
|
|
1763
1830
|
|
|
1764
|
-
// pos —
|
|
1765
|
-
if (this.posInterp === '
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
catmullRomVec3(out.pos, p0, k0.pos, k1.pos, p3, t);
|
|
1769
|
-
} else {
|
|
1831
|
+
// pos — Hermite (auto-CR tangents when none stored), linear, or step
|
|
1832
|
+
if (this.posInterp === 'step') {
|
|
1833
|
+
out.pos[0]=k0.pos[0]; out.pos[1]=k0.pos[1]; out.pos[2]=k0.pos[2];
|
|
1834
|
+
} else if (this.posInterp === 'linear') {
|
|
1770
1835
|
lerpVec3(out.pos, k0.pos, k1.pos, t);
|
|
1836
|
+
} else {
|
|
1837
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1838
|
+
const p3 = seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1839
|
+
// tanOut on k0: use stored, else symmetric from tanIn, else auto-CR
|
|
1840
|
+
const m0 = k0.tanOut != null ? k0.tanOut
|
|
1841
|
+
: k0.tanIn != null ? k0.tanIn
|
|
1842
|
+
: _crTanOut(_m0, p0, k0.pos, k1.pos, p3);
|
|
1843
|
+
// tanIn on k1: use stored, else symmetric from tanOut, else auto-CR
|
|
1844
|
+
const m1 = k1.tanIn != null ? k1.tanIn
|
|
1845
|
+
: k1.tanOut != null ? k1.tanOut
|
|
1846
|
+
: _crTanIn(_m1, p0, k0.pos, k1.pos, p3);
|
|
1847
|
+
hermiteVec3(out.pos, k0.pos, m0, k1.pos, m1, t);
|
|
1771
1848
|
}
|
|
1772
1849
|
|
|
1773
|
-
// rot — slerp or nlerp
|
|
1774
|
-
if (this.rotInterp === '
|
|
1850
|
+
// rot — step, slerp, or nlerp
|
|
1851
|
+
if (this.rotInterp === 'step') {
|
|
1852
|
+
out.rot[0]=k0.rot[0]; out.rot[1]=k0.rot[1]; out.rot[2]=k0.rot[2]; out.rot[3]=k0.rot[3];
|
|
1853
|
+
} else if (this.rotInterp === 'nlerp') {
|
|
1775
1854
|
qNlerp(out.rot, k0.rot, k1.rot, t);
|
|
1776
1855
|
} else {
|
|
1777
1856
|
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
@@ -1801,23 +1880,31 @@ class PoseTrack extends Track {
|
|
|
1801
1880
|
/**
|
|
1802
1881
|
* Lookat camera keyframe track.
|
|
1803
1882
|
*
|
|
1804
|
-
* Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z],
|
|
1883
|
+
* Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z],
|
|
1884
|
+
* fov?:number, halfHeight?:number,
|
|
1885
|
+
* eyeTanIn?:[x,y,z], eyeTanOut?:[x,y,z],
|
|
1886
|
+
* centerTanIn?:[x,y,z], centerTanOut?:[x,y,z] }
|
|
1805
1887
|
*
|
|
1806
1888
|
* fov — vertical fov (radians) for perspective cameras; null for ortho.
|
|
1807
1889
|
* halfHeight — world-unit half-height of ortho frustum; null for perspective.
|
|
1808
1890
|
* Both are optional and nullable. eval() lerps each only when both adjacent
|
|
1809
1891
|
* keyframes carry a non-null value for that field.
|
|
1810
1892
|
*
|
|
1893
|
+
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite
|
|
1894
|
+
* interpolation of the eye and center paths respectively.
|
|
1895
|
+
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1896
|
+
*
|
|
1811
1897
|
* Each field is independently interpolated — eye and center along their
|
|
1812
1898
|
* own paths, up nlerped on the unit sphere. This correctly handles cameras
|
|
1813
1899
|
* that always look at a fixed target (center stays at origin throughout)
|
|
1814
1900
|
* as well as free-fly paths where center moves independently.
|
|
1815
|
-
*
|
|
1901
|
+
*
|
|
1816
1902
|
* Missing fields default to: center → [0,0,0], up → [0,1,0].
|
|
1817
1903
|
*
|
|
1818
1904
|
* add() accepts individual specs or a bulk array of specs:
|
|
1819
1905
|
*
|
|
1820
|
-
* { eye, center?, up?, fov?, halfHeight
|
|
1906
|
+
* { eye, center?, up?, fov?, halfHeight?,
|
|
1907
|
+
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
1821
1908
|
* explicit lookat; center defaults to [0,0,0], up to [0,1,0].
|
|
1822
1909
|
* fov and halfHeight are mutually exclusive nullable scalars.
|
|
1823
1910
|
* { vMatrix: mat4 } view matrix (world→eye); eye reconstructed via -R^T·t
|
|
@@ -1831,8 +1918,8 @@ class PoseTrack extends Track {
|
|
|
1831
1918
|
* Use capturePose() (p5.tree bridge) when the real up hint is needed.
|
|
1832
1919
|
*
|
|
1833
1920
|
* eval() writes { eye, center, up, fov, halfHeight }:
|
|
1834
|
-
* eye —
|
|
1835
|
-
* center —
|
|
1921
|
+
* eye — Hermite (auto-CR when no tangents stored) or linear or step
|
|
1922
|
+
* center — Hermite (auto-CR when no tangents stored) or linear or step
|
|
1836
1923
|
* up — nlerp (normalize-after-lerp on unit sphere)
|
|
1837
1924
|
* fov — lerp when both keyframes carry non-null fov; else null
|
|
1838
1925
|
* halfHeight — lerp when both keyframes carry non-null halfHeight; else null
|
|
@@ -1857,14 +1944,20 @@ class CameraTrack extends Track {
|
|
|
1857
1944
|
super();
|
|
1858
1945
|
/**
|
|
1859
1946
|
* Eye position interpolation mode.
|
|
1860
|
-
*
|
|
1947
|
+
* - 'hermite' — cubic Hermite; auto-CR tangents when none stored (default)
|
|
1948
|
+
* - 'linear' — lerp
|
|
1949
|
+
* - 'step' — snap to k0 eye
|
|
1950
|
+
* @type {'hermite'|'linear'|'step'}
|
|
1861
1951
|
*/
|
|
1862
|
-
this.eyeInterp = '
|
|
1952
|
+
this.eyeInterp = 'hermite';
|
|
1863
1953
|
/**
|
|
1864
1954
|
* Center (lookat target) interpolation mode.
|
|
1865
|
-
* 'linear' suits fixed or predictably moving targets.
|
|
1866
|
-
* '
|
|
1867
|
-
*
|
|
1955
|
+
* 'linear' suits fixed or predictably moving targets (default).
|
|
1956
|
+
* 'hermite' gives smoother paths when center is also flying freely.
|
|
1957
|
+
* - 'hermite' — cubic Hermite; auto-CR tangents when none stored
|
|
1958
|
+
* - 'linear' — lerp
|
|
1959
|
+
* - 'step' — snap to k0 center
|
|
1960
|
+
* @type {'hermite'|'linear'|'step'}
|
|
1868
1961
|
*/
|
|
1869
1962
|
this.centerInterp = 'linear';
|
|
1870
1963
|
// Scratch for toCamera() — avoids hot-path allocations
|
|
@@ -1937,22 +2030,38 @@ class CameraTrack extends Track {
|
|
|
1937
2030
|
const k0 = this.keyframes[seg];
|
|
1938
2031
|
const k1 = this.keyframes[seg + 1];
|
|
1939
2032
|
|
|
1940
|
-
// eye —
|
|
1941
|
-
if (this.eyeInterp === '
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
catmullRomVec3(out.eye, p0, k0.eye, k1.eye, p3, t);
|
|
1945
|
-
} else {
|
|
2033
|
+
// eye — Hermite (auto-CR tangents when none stored), linear, or step
|
|
2034
|
+
if (this.eyeInterp === 'step') {
|
|
2035
|
+
out.eye[0]=k0.eye[0]; out.eye[1]=k0.eye[1]; out.eye[2]=k0.eye[2];
|
|
2036
|
+
} else if (this.eyeInterp === 'linear') {
|
|
1946
2037
|
lerpVec3(out.eye, k0.eye, k1.eye, t);
|
|
2038
|
+
} else {
|
|
2039
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].eye : k0.eye;
|
|
2040
|
+
const p3 = seg + 2 < n ? this.keyframes[seg + 2].eye : k1.eye;
|
|
2041
|
+
const m0 = k0.eyeTanOut != null ? k0.eyeTanOut
|
|
2042
|
+
: k0.eyeTanIn != null ? k0.eyeTanIn
|
|
2043
|
+
: _crTanOut(_m0, p0, k0.eye, k1.eye, p3);
|
|
2044
|
+
const m1 = k1.eyeTanIn != null ? k1.eyeTanIn
|
|
2045
|
+
: k1.eyeTanOut != null ? k1.eyeTanOut
|
|
2046
|
+
: _crTanIn(_m1, p0, k0.eye, k1.eye, p3);
|
|
2047
|
+
hermiteVec3(out.eye, k0.eye, m0, k1.eye, m1, t);
|
|
1947
2048
|
}
|
|
1948
2049
|
|
|
1949
|
-
// center —
|
|
1950
|
-
if (this.centerInterp === '
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
catmullRomVec3(out.center, c0, k0.center, k1.center, c3, t);
|
|
1954
|
-
} else {
|
|
2050
|
+
// center — Hermite, linear, or step (independent lookat target)
|
|
2051
|
+
if (this.centerInterp === 'step') {
|
|
2052
|
+
out.center[0]=k0.center[0]; out.center[1]=k0.center[1]; out.center[2]=k0.center[2];
|
|
2053
|
+
} else if (this.centerInterp === 'linear') {
|
|
1955
2054
|
lerpVec3(out.center, k0.center, k1.center, t);
|
|
2055
|
+
} else {
|
|
2056
|
+
const c0 = seg > 0 ? this.keyframes[seg - 1].center : k0.center;
|
|
2057
|
+
const c3 = seg + 2 < n ? this.keyframes[seg + 2].center : k1.center;
|
|
2058
|
+
const m0 = k0.centerTanOut != null ? k0.centerTanOut
|
|
2059
|
+
: k0.centerTanIn != null ? k0.centerTanIn
|
|
2060
|
+
: _crTanOut(_m0, c0, k0.center, k1.center, c3);
|
|
2061
|
+
const m1 = k1.centerTanIn != null ? k1.centerTanIn
|
|
2062
|
+
: k1.centerTanOut != null ? k1.centerTanOut
|
|
2063
|
+
: _crTanIn(_m1, c0, k0.center, k1.center, c3);
|
|
2064
|
+
hermiteVec3(out.center, k0.center, m0, k1.center, m1, t);
|
|
1956
2065
|
}
|
|
1957
2066
|
|
|
1958
2067
|
// up — nlerp (normalize after lerp; correct for typical near-upright cameras)
|
|
@@ -2130,5 +2239,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
2130
2239
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
2131
2240
|
}
|
|
2132
2241
|
|
|
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,
|
|
2242
|
+
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
2243
|
//# sourceMappingURL=index.js.map
|