@nakednous/tree 0.0.19 → 0.0.21
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 +78 -0
- package/dist/index.js +459 -261
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1153,15 +1153,15 @@ function mat4ToRotation(out4, m) {
|
|
|
1153
1153
|
* @module tree/track
|
|
1154
1154
|
* @license AGPL-3.0-only
|
|
1155
1155
|
*
|
|
1156
|
-
* Quaternion algebra is provided by quat.js
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1156
|
+
* Quaternion algebra is provided by quat.js; projection matrix construction
|
|
1157
|
+
* by form.js. Spline helpers (hermiteVec3, lerpVec3) and TRS↔mat4 conversions
|
|
1158
|
+
* (transformToMat4, mat4ToTransform) live here because they are tightly
|
|
1159
|
+
* coupled to the PoseTrack keyframe shape.
|
|
1160
1160
|
*
|
|
1161
1161
|
* Zero dependencies on p5, DOM, WebGL, or WebGPU.
|
|
1162
1162
|
*
|
|
1163
|
-
* ── Exports
|
|
1164
|
-
* Quaternion helpers
|
|
1163
|
+
* ── Exports ──────────────────────────────────────────────────────────────
|
|
1164
|
+
* Quaternion helpers (re-exported from quat.js)
|
|
1165
1165
|
* qSet qCopy qDot qNormalize qNegate qMul qSlerp qNlerp
|
|
1166
1166
|
* qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
1167
1167
|
* qToAxisAngle
|
|
@@ -1171,28 +1171,64 @@ function mat4ToRotation(out4, m) {
|
|
|
1171
1171
|
* transformToMat4 mat4ToTransform
|
|
1172
1172
|
* Tracks
|
|
1173
1173
|
* PoseTrack — { pos, rot, scl } TRS keyframes
|
|
1174
|
-
* CameraTrack — { eye, center, up } lookat keyframes
|
|
1175
|
-
*
|
|
1176
|
-
* ──
|
|
1174
|
+
* CameraTrack — { eye, center, up, fov?, halfHeight? } lookat keyframes
|
|
1175
|
+
*
|
|
1176
|
+
* ── Public path access (all zero-alloc, no cursor side effects) ──────────
|
|
1177
|
+
* PoseTrack
|
|
1178
|
+
* samplePos (out) | (out, seg, t) vec3
|
|
1179
|
+
* mat4Model (out) | (out, seg, t) mat4 — TRS model
|
|
1180
|
+
* tangents (outIn, outOut, index) vec3 × 2 at keyframe
|
|
1181
|
+
* eval (out?) TRS object at cursor
|
|
1182
|
+
*
|
|
1183
|
+
* CameraTrack
|
|
1184
|
+
* sampleEye (out) | (out, seg, t) vec3
|
|
1185
|
+
* sampleCenter (out) | (out, seg, t) vec3
|
|
1186
|
+
* mat4Eye (out) | (out, seg, t) mat4 — lookat frame
|
|
1187
|
+
* eyeTangents (outIn, outOut, index) vec3 × 2 at keyframe
|
|
1188
|
+
* centerTangents (outIn, outOut, index) vec3 × 2 at keyframe
|
|
1189
|
+
* eval (out?) { eye, center, up,
|
|
1190
|
+
* fov, halfHeight }
|
|
1191
|
+
*
|
|
1192
|
+
* Two arities for the continuous family:
|
|
1193
|
+
* (out) cursor form — reads track.seg / track.f. Useful when the
|
|
1194
|
+
* track's own transport is driving the animation.
|
|
1195
|
+
* (out, seg, t) explicit form — continuous (seg, t) coordinate, no cursor
|
|
1196
|
+
* side effects. seg ∈ [0, segments−1] integer, t ∈ [0, 1]
|
|
1197
|
+
* local to that segment. As a convenience, seg === segments
|
|
1198
|
+
* is accepted and rewritten to (segments−1, 1) — so the
|
|
1199
|
+
* idiom sampleX(out, i, 0) uniformly addresses keyframe i
|
|
1200
|
+
* for every i ∈ [0, keyframes.length−1].
|
|
1201
|
+
*
|
|
1202
|
+
* All honour the track's interpolation mode (hermite / linear / step) and
|
|
1203
|
+
* the same stored-tangent → centripetal-CR fallback chain that eval() uses.
|
|
1204
|
+
*
|
|
1205
|
+
* `tangents` / `eyeTangents` / `centerTangents` are keyframe-indexed — a
|
|
1206
|
+
* keyframe's incoming tangent belongs to the previous segment, outgoing to
|
|
1207
|
+
* the next. At boundary keyframes the missing side mirrors the present one
|
|
1208
|
+
* so drawing arrows at endpoints always yields a visible vector.
|
|
1209
|
+
*
|
|
1210
|
+
* Projection matrices are deliberately not exposed as a track method. Each
|
|
1211
|
+
* CameraTrack keyframe stores `fov` (perspective) or `halfHeight` (ortho)
|
|
1212
|
+
* directly on track.keyframes[i] — callers wanting a projection build one
|
|
1213
|
+
* with the free mat4Persp / mat4Ortho constructors. Animated fov in
|
|
1214
|
+
* sketches flows through the bridge's camera-binding via eval().fov and
|
|
1215
|
+
* eval().halfHeight (the bridge calls cam.perspective() / cam.ortho() each
|
|
1216
|
+
* frame).
|
|
1217
|
+
*
|
|
1218
|
+
* ── Class hierarchy ──────────────────────────────────────────────────────
|
|
1177
1219
|
* Track (unexported, never instantiated directly)
|
|
1178
1220
|
* └── PoseTrack (exported)
|
|
1179
1221
|
* └── CameraTrack (exported)
|
|
1180
1222
|
*
|
|
1181
|
-
*
|
|
1182
|
-
* hooks
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
* ── Hook architecture ─────────────────────────────────────────────────────────
|
|
1186
|
-
* Lib-space hooks (underscore prefix — reserved for host layer / UI layer):
|
|
1187
|
-
* _onActivate / _onDeactivate — fire on playing transitions false→true / true→false.
|
|
1188
|
-
* _onPlay / _onEnd / _onStop — mirror the user-space hooks; used by the UI layer
|
|
1189
|
-
* so it can sync without chaining the public slots.
|
|
1223
|
+
* ── Hook architecture ────────────────────────────────────────────────────
|
|
1224
|
+
* Lib-space hooks (underscore prefix — host layer / UI layer):
|
|
1225
|
+
* _onActivate / _onDeactivate — fire on playing false→true / true→false.
|
|
1226
|
+
* _onPlay / _onEnd / _onStop — mirror the user-space hooks.
|
|
1190
1227
|
*
|
|
1191
|
-
* User-space hooks
|
|
1228
|
+
* User-space hooks:
|
|
1192
1229
|
* onPlay : fires in play() on false→true transition.
|
|
1193
1230
|
* onEnd : fires in tick() at natural boundary (once mode only).
|
|
1194
|
-
* onStop : fires in stop() / reset()
|
|
1195
|
-
* onEnd and onStop are mutually exclusive per event.
|
|
1231
|
+
* onStop : fires in stop() / reset().
|
|
1196
1232
|
*
|
|
1197
1233
|
* Firing order:
|
|
1198
1234
|
* play() → onPlay → _onPlay → _onActivate
|
|
@@ -1200,18 +1236,14 @@ function mat4ToRotation(out4, m) {
|
|
|
1200
1236
|
* stop() → onStop → _onStop → _onDeactivate
|
|
1201
1237
|
* reset() → onStop → _onStop → _onDeactivate
|
|
1202
1238
|
*
|
|
1203
|
-
* ── Loop modes
|
|
1239
|
+
* ── Loop modes ───────────────────────────────────────────────────────────
|
|
1204
1240
|
* loop:false, bounce:false — play once, stop at end (fires onEnd)
|
|
1205
1241
|
* loop:true, bounce:false — repeat, wrap back to start
|
|
1206
1242
|
* loop:true, bounce:true — bounce forever at boundaries
|
|
1207
1243
|
* loop:false, bounce:true — bounce once: flip at far boundary, stop at origin
|
|
1208
1244
|
*
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
* ── Playback semantics (rate + _dir) ─────────────────────────────────────────
|
|
1212
|
-
* rate > 0 forward
|
|
1213
|
-
* rate < 0 backward
|
|
1214
|
-
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
1245
|
+
* ── Playback semantics (rate + _dir) ─────────────────────────────────────
|
|
1246
|
+
* rate > 0 forward rate < 0 backward rate === 0 frozen
|
|
1215
1247
|
*
|
|
1216
1248
|
* play() is the sole setter of playing = true.
|
|
1217
1249
|
* stop() is the sole setter of playing = false.
|
|
@@ -1219,11 +1251,9 @@ function mat4ToRotation(out4, m) {
|
|
|
1219
1251
|
*
|
|
1220
1252
|
* _dir (internal, ±1) tracks the current bounce travel direction.
|
|
1221
1253
|
* tick() advances by rate * _dir and flips _dir at boundaries.
|
|
1222
|
-
*
|
|
1223
|
-
* _dir is reset to 1 only in reset() (keyframes cleared) — stop/replay
|
|
1224
|
-
* preserves the current travel direction.
|
|
1254
|
+
* _dir is reset to 1 only in reset().
|
|
1225
1255
|
*
|
|
1226
|
-
* ── One-keyframe behaviour
|
|
1256
|
+
* ── One-keyframe behaviour ───────────────────────────────────────────────
|
|
1227
1257
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
1228
1258
|
* setting playing = true and without firing hooks.
|
|
1229
1259
|
*/
|
|
@@ -1240,12 +1270,11 @@ function _dist3(a, b) {
|
|
|
1240
1270
|
|
|
1241
1271
|
/**
|
|
1242
1272
|
* Cubic Hermite interpolation between p0 and p1 with explicit tangents.
|
|
1243
|
-
* Catmull-Rom is a special case where m0/m1 are auto-computed from neighbors.
|
|
1244
1273
|
* @param {number[]} out 3-element result.
|
|
1245
1274
|
* @param {number[]} p0 Segment start.
|
|
1246
|
-
* @param {number[]} m0 Outgoing tangent at p0
|
|
1275
|
+
* @param {number[]} m0 Outgoing tangent at p0.
|
|
1247
1276
|
* @param {number[]} p1 Segment end.
|
|
1248
|
-
* @param {number[]} m1 Incoming tangent at p1
|
|
1277
|
+
* @param {number[]} m1 Incoming tangent at p1.
|
|
1249
1278
|
* @param {number} t Blend [0, 1].
|
|
1250
1279
|
* @returns {number[]} out
|
|
1251
1280
|
*/
|
|
@@ -1258,29 +1287,29 @@ const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
|
|
|
1258
1287
|
return out;
|
|
1259
1288
|
};
|
|
1260
1289
|
|
|
1261
|
-
// Centripetal CR outgoing tangent at p1 for segment p1→p2
|
|
1290
|
+
// Centripetal CR outgoing tangent at p1 for segment p1→p2.
|
|
1291
|
+
// Signature: (out, p0, p1, p2). Returns tangent AT p1 (the middle point).
|
|
1262
1292
|
const _crTanOut = (out, p0, p1, p2) => {
|
|
1263
1293
|
const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1;
|
|
1264
1294
|
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;
|
|
1265
1295
|
return out;
|
|
1266
1296
|
};
|
|
1267
1297
|
|
|
1298
|
+
// Centripetal CR incoming tangent at p2 for segment p1→p2.
|
|
1299
|
+
// Signature: (out, p1, p2, p3). Returns tangent AT p2 (the middle point).
|
|
1268
1300
|
const _crTanIn = (out, p1, p2, p3) => {
|
|
1269
1301
|
const dt1=Math.pow(_dist3(p1,p2),0.5)||1, dt2=Math.pow(_dist3(p2,p3),0.5)||1;
|
|
1270
1302
|
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;
|
|
1271
1303
|
return out;
|
|
1272
1304
|
};
|
|
1273
1305
|
|
|
1274
|
-
// Module-level scratch — shared
|
|
1306
|
+
// Module-level scratch — shared across track instances (non-reentrant hot path).
|
|
1275
1307
|
const _m0=[0,0,0], _m1=[0,0,0];
|
|
1308
|
+
const _trsScratch = { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
|
|
1309
|
+
const _eyeScratch = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] };
|
|
1276
1310
|
|
|
1277
1311
|
/**
|
|
1278
1312
|
* Linear interpolation between two vec3s.
|
|
1279
|
-
* @param {number[]} out
|
|
1280
|
-
* @param {number[]} a
|
|
1281
|
-
* @param {number[]} b
|
|
1282
|
-
* @param {number} t Blend [0, 1].
|
|
1283
|
-
* @returns {number[]} out
|
|
1284
1313
|
*/
|
|
1285
1314
|
const lerpVec3 = (out, a, b, t) => {
|
|
1286
1315
|
out[0]=a[0]+t*(b[0]-a[0]);
|
|
@@ -1289,15 +1318,94 @@ const lerpVec3 = (out, a, b, t) => {
|
|
|
1289
1318
|
return out;
|
|
1290
1319
|
};
|
|
1291
1320
|
|
|
1321
|
+
// =========================================================================
|
|
1322
|
+
// S2b Path samplers — shared core
|
|
1323
|
+
// =========================================================================
|
|
1324
|
+
|
|
1325
|
+
/** @private — sample interpolated vec3 path at (seg, t) into out. */
|
|
1326
|
+
function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t) {
|
|
1327
|
+
const n = kfs.length;
|
|
1328
|
+
if (n === 0) { out[0]=0; out[1]=0; out[2]=0; return out; }
|
|
1329
|
+
if (n === 1) {
|
|
1330
|
+
const p = kfs[0][field];
|
|
1331
|
+
out[0]=p[0]; out[1]=p[1]; out[2]=p[2];
|
|
1332
|
+
return out;
|
|
1333
|
+
}
|
|
1334
|
+
const nSeg = n - 1;
|
|
1335
|
+
seg = seg | 0;
|
|
1336
|
+
if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
|
|
1337
|
+
else if (seg < 0) { seg = 0; t = 0; }
|
|
1338
|
+
t = _clamp01(t);
|
|
1339
|
+
const k0 = kfs[seg];
|
|
1340
|
+
const k1 = kfs[seg + 1];
|
|
1341
|
+
|
|
1342
|
+
if (interp === 'step') {
|
|
1343
|
+
const p = k0[field];
|
|
1344
|
+
out[0]=p[0]; out[1]=p[1]; out[2]=p[2];
|
|
1345
|
+
return out;
|
|
1346
|
+
}
|
|
1347
|
+
if (interp === 'linear') {
|
|
1348
|
+
return lerpVec3(out, k0[field], k1[field], t);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const p0 = seg > 0 ? kfs[seg - 1][field] : k0[field];
|
|
1352
|
+
const p3 = seg + 2 < n ? kfs[seg + 2][field] : k1[field];
|
|
1353
|
+
const m0 = k0[tanOutName] != null ? k0[tanOutName]
|
|
1354
|
+
: k0[tanInName] != null ? k0[tanInName]
|
|
1355
|
+
: _crTanOut(_m0, p0, k0[field], k1[field]);
|
|
1356
|
+
const m1 = k1[tanInName] != null ? k1[tanInName]
|
|
1357
|
+
: k1[tanOutName] != null ? k1[tanOutName]
|
|
1358
|
+
: _crTanIn(_m1, k0[field], k1[field], p3);
|
|
1359
|
+
return hermiteVec3(out, k0[field], m0, k1[field], m1, t);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** @private — write effective in/out tangents at keyframe i. */
|
|
1363
|
+
function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i) {
|
|
1364
|
+
const n = kfs.length;
|
|
1365
|
+
if (n === 0) {
|
|
1366
|
+
outIn[0]=outIn[1]=outIn[2]=0; outOut[0]=outOut[1]=outOut[2]=0;
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
i = _clampS(i | 0, 0, n - 1);
|
|
1370
|
+
const ki = kfs[i];
|
|
1371
|
+
const hasTI = ki[tanInName] != null;
|
|
1372
|
+
const hasTO = ki[tanOutName] != null;
|
|
1373
|
+
|
|
1374
|
+
if (hasTO) {
|
|
1375
|
+
outOut[0]=ki[tanOutName][0]; outOut[1]=ki[tanOutName][1]; outOut[2]=ki[tanOutName][2];
|
|
1376
|
+
} else if (hasTI) {
|
|
1377
|
+
outOut[0]=ki[tanInName][0]; outOut[1]=ki[tanInName][1]; outOut[2]=ki[tanInName][2];
|
|
1378
|
+
} else if (i < n - 1) {
|
|
1379
|
+
const k1 = kfs[i + 1];
|
|
1380
|
+
const p0 = i > 0 ? kfs[i - 1][field] : ki[field];
|
|
1381
|
+
_crTanOut(outOut, p0, ki[field], k1[field]);
|
|
1382
|
+
} else {
|
|
1383
|
+
outOut[0]=0; outOut[1]=0; outOut[2]=0;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (hasTI) {
|
|
1387
|
+
outIn[0]=ki[tanInName][0]; outIn[1]=ki[tanInName][1]; outIn[2]=ki[tanInName][2];
|
|
1388
|
+
} else if (hasTO) {
|
|
1389
|
+
outIn[0]=ki[tanOutName][0]; outIn[1]=ki[tanOutName][1]; outIn[2]=ki[tanOutName][2];
|
|
1390
|
+
} else if (i > 0) {
|
|
1391
|
+
const k0 = kfs[i - 1];
|
|
1392
|
+
const p3 = i + 1 < n ? kfs[i + 1][field] : ki[field];
|
|
1393
|
+
_crTanIn(outIn, k0[field], ki[field], p3);
|
|
1394
|
+
} else {
|
|
1395
|
+
outIn[0]=outOut[0]; outIn[1]=outOut[1]; outIn[2]=outOut[2];
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (i === n - 1 && !hasTO && !hasTI) {
|
|
1399
|
+
outOut[0]=outIn[0]; outOut[1]=outIn[1]; outOut[2]=outIn[2];
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1292
1403
|
// =========================================================================
|
|
1293
1404
|
// S3 Transform <-> Mat4
|
|
1294
1405
|
// =========================================================================
|
|
1295
1406
|
|
|
1296
1407
|
/**
|
|
1297
1408
|
* Write a TRS transform into a column-major mat4.
|
|
1298
|
-
* @param {Float32Array|number[]} out 16-element column-major mat4.
|
|
1299
|
-
* @param {{ pos:number[], rot:number[], scl:number[] }} xform
|
|
1300
|
-
* @returns {Float32Array|number[]} out
|
|
1301
1409
|
*/
|
|
1302
1410
|
const transformToMat4 = (out, xform) => {
|
|
1303
1411
|
qToMat4(out, xform.rot);
|
|
@@ -1311,10 +1419,6 @@ const transformToMat4 = (out, xform) => {
|
|
|
1311
1419
|
|
|
1312
1420
|
/**
|
|
1313
1421
|
* Decompose a column-major mat4 into a TRS transform.
|
|
1314
|
-
* Assumes no shear. Scale extracted from column lengths.
|
|
1315
|
-
* @param {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1316
|
-
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1317
|
-
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1318
1422
|
*/
|
|
1319
1423
|
const mat4ToTransform = (out, m) => {
|
|
1320
1424
|
out.pos[0]=m[12]; out.pos[1]=m[13]; out.pos[2]=m[14];
|
|
@@ -1345,7 +1449,6 @@ function _parseVec3(v) {
|
|
|
1345
1449
|
return null;
|
|
1346
1450
|
}
|
|
1347
1451
|
|
|
1348
|
-
// Euler: unit axis vectors and the six valid intrinsic orderings.
|
|
1349
1452
|
const _EULER_AXES = { X:[1,0,0], Y:[0,1,0], Z:[0,0,1] };
|
|
1350
1453
|
const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
1351
1454
|
|
|
@@ -1353,14 +1456,13 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1353
1456
|
* Parse any rotation representation into a unit quaternion [x,y,z,w].
|
|
1354
1457
|
*
|
|
1355
1458
|
* Accepted forms:
|
|
1356
|
-
*
|
|
1357
1459
|
* [x,y,z,w] — raw quaternion array
|
|
1358
|
-
* { axis:[x,y,z], angle } — axis-angle
|
|
1460
|
+
* { axis:[x,y,z], angle } — axis-angle (angle in radians)
|
|
1359
1461
|
* { dir:[x,y,z], up?:[x,y,z] } — forward direction (−Z) with optional up
|
|
1360
1462
|
* { mat4Eye: mat4 } — rotation block of an eye matrix
|
|
1361
1463
|
* { mat3: mat3 } — column-major 3×3 rotation matrix
|
|
1362
1464
|
* { euler:[rx,ry,rz], order? } — intrinsic Euler (default order: YXZ)
|
|
1363
|
-
* { from:[x,y,z], to:[x,y,z] } — shortest-arc rotation
|
|
1465
|
+
* { from:[x,y,z], to:[x,y,z] } — shortest-arc rotation between two vectors
|
|
1364
1466
|
*
|
|
1365
1467
|
* @param {*} v
|
|
1366
1468
|
* @returns {number[]|null} [x,y,z,w] or null if unparseable.
|
|
@@ -1368,7 +1470,7 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1368
1470
|
function _parseQuat(v) {
|
|
1369
1471
|
if (!v) return null;
|
|
1370
1472
|
|
|
1371
|
-
//
|
|
1473
|
+
// [x,y,z,w]
|
|
1372
1474
|
if (Array.isArray(v) && v.length === 4) return [v[0],v[1],v[2],v[3]];
|
|
1373
1475
|
if (ArrayBuffer.isView(v) && v.length >= 4) return [v[0],v[1],v[2],v[3]];
|
|
1374
1476
|
|
|
@@ -1397,8 +1499,7 @@ function _parseQuat(v) {
|
|
|
1397
1499
|
|
|
1398
1500
|
// { mat3 }
|
|
1399
1501
|
if (v.mat3 != null) {
|
|
1400
|
-
const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3))
|
|
1401
|
-
? v.mat3 : null;
|
|
1502
|
+
const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3)) ? v.mat3 : null;
|
|
1402
1503
|
if (!m || m.length < 9) return null;
|
|
1403
1504
|
return qFromRotMat3x3([0,0,0,1], m[0],m[3],m[6], m[1],m[4],m[7], m[2],m[5],m[8]);
|
|
1404
1505
|
}
|
|
@@ -1408,8 +1509,7 @@ function _parseQuat(v) {
|
|
|
1408
1509
|
const e = v.euler;
|
|
1409
1510
|
if (!Array.isArray(e) || e.length < 3) return null;
|
|
1410
1511
|
const order = (v.order && _EULER_ORDERS.has(v.order)) ? v.order : 'YXZ';
|
|
1411
|
-
const q = [0,0,0,1];
|
|
1412
|
-
const s = [0,0,0,1];
|
|
1512
|
+
const q = [0,0,0,1], s = [0,0,0,1];
|
|
1413
1513
|
for (let i = 0; i < 3; i++) {
|
|
1414
1514
|
const ax = _EULER_AXES[order[i]];
|
|
1415
1515
|
qMul(q, q, qFromAxisAngle(s, ax[0],ax[1],ax[2], e[i]));
|
|
@@ -1444,22 +1544,22 @@ function _parseQuat(v) {
|
|
|
1444
1544
|
}
|
|
1445
1545
|
|
|
1446
1546
|
/**
|
|
1447
|
-
* Parse a PoseTrack keyframe spec.
|
|
1547
|
+
* Parse a PoseTrack keyframe spec into internal form.
|
|
1448
1548
|
*
|
|
1449
1549
|
* Accepted forms:
|
|
1450
|
-
*
|
|
1451
1550
|
* { mat4Model }
|
|
1452
1551
|
* Decompose a column-major mat4 into TRS via mat4ToTransform.
|
|
1453
1552
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1454
1553
|
*
|
|
1455
1554
|
* { pos?, rot?, scl?, tanIn?, tanOut? }
|
|
1456
|
-
* Explicit TRS.
|
|
1555
|
+
* Explicit TRS. pos and scl are vec3; rot accepts any form from _parseQuat.
|
|
1457
1556
|
* All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
|
|
1458
1557
|
* missing rot defaults to identity.
|
|
1459
1558
|
* tanIn/tanOut are optional vec3 tangents for Hermite interpolation.
|
|
1460
1559
|
*
|
|
1461
1560
|
* @param {Object} spec
|
|
1462
|
-
* @returns {{ pos:number[], rot:number[], scl:number[],
|
|
1561
|
+
* @returns {{ pos:number[], rot:number[], scl:number[],
|
|
1562
|
+
* tanIn:number[]|null, tanOut:number[]|null } | null}
|
|
1463
1563
|
*/
|
|
1464
1564
|
function _parseSpec(spec) {
|
|
1465
1565
|
if (!spec || typeof spec !== 'object') return null;
|
|
@@ -1474,6 +1574,7 @@ function _parseSpec(spec) {
|
|
|
1474
1574
|
return kf;
|
|
1475
1575
|
}
|
|
1476
1576
|
|
|
1577
|
+
// { pos?, rot?, scl?, tanIn?, tanOut? } — explicit TRS
|
|
1477
1578
|
const pos = _parseVec3(spec.pos) || [0,0,0];
|
|
1478
1579
|
const rot = _parseQuat(spec.rot) || [0,0,0,1];
|
|
1479
1580
|
const scl = _parseVec3(spec.scl) || [1,1,1];
|
|
@@ -1493,30 +1594,21 @@ function _sameTransform(a, b) {
|
|
|
1493
1594
|
// =========================================================================
|
|
1494
1595
|
|
|
1495
1596
|
/**
|
|
1496
|
-
* Parse a
|
|
1497
|
-
*
|
|
1498
|
-
* Accepted forms:
|
|
1499
|
-
*
|
|
1500
|
-
* { eye, center?, up?, fov?, halfHeight?,
|
|
1501
|
-
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
1502
|
-
* Explicit lookat. center defaults to [0,0,0], up defaults to [0,1,0].
|
|
1503
|
-
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite.
|
|
1504
|
-
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1597
|
+
* Parse a CameraTrack keyframe spec into internal form.
|
|
1505
1598
|
*
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1599
|
+
* Required: eye (vec3). Everything else is optional.
|
|
1600
|
+
* center defaults to [0, 0, 0]
|
|
1601
|
+
* up defaults to [0, 1, 0] and is normalised
|
|
1602
|
+
* fov vertical fov (radians), perspective only — null if absent
|
|
1603
|
+
* halfHeight world-unit half-height of ortho frustum — null if absent
|
|
1604
|
+
* eyeTanIn/Out optional Hermite tangents for the eye path
|
|
1605
|
+
* centerTanIn/Out optional Hermite tangents for the center path
|
|
1509
1606
|
*
|
|
1510
1607
|
* @param {Object} spec
|
|
1511
|
-
* @returns {
|
|
1512
|
-
* fov:number|null, halfHeight:number|null,
|
|
1513
|
-
* eyeTanIn:number[]|null, eyeTanOut:number[]|null,
|
|
1514
|
-
* centerTanIn:number[]|null, centerTanOut:number[]|null }|null}
|
|
1608
|
+
* @returns {Object|null} Parsed keyframe or null if eye is missing/malformed.
|
|
1515
1609
|
*/
|
|
1516
1610
|
function _parseCameraSpec(spec) {
|
|
1517
1611
|
if (!spec || typeof spec !== 'object') return null;
|
|
1518
|
-
|
|
1519
|
-
// { eye, center?, up? } — explicit lookat
|
|
1520
1612
|
const eye = _parseVec3(spec.eye);
|
|
1521
1613
|
if (!eye) return null;
|
|
1522
1614
|
const center = _parseVec3(spec.center) || [0,0,0];
|
|
@@ -1526,12 +1618,12 @@ function _parseCameraSpec(spec) {
|
|
|
1526
1618
|
return {
|
|
1527
1619
|
eye, center,
|
|
1528
1620
|
up: [up[0]/ul, up[1]/ul, up[2]/ul],
|
|
1529
|
-
fov:
|
|
1530
|
-
halfHeight:
|
|
1531
|
-
eyeTanIn:
|
|
1532
|
-
eyeTanOut:
|
|
1533
|
-
centerTanIn:
|
|
1534
|
-
centerTanOut:_parseVec3(spec.centerTanOut)|| null,
|
|
1621
|
+
fov: typeof spec.fov === 'number' ? spec.fov : null,
|
|
1622
|
+
halfHeight: typeof spec.halfHeight === 'number' ? spec.halfHeight : null,
|
|
1623
|
+
eyeTanIn: _parseVec3(spec.eyeTanIn) || null,
|
|
1624
|
+
eyeTanOut: _parseVec3(spec.eyeTanOut) || null,
|
|
1625
|
+
centerTanIn: _parseVec3(spec.centerTanIn) || null,
|
|
1626
|
+
centerTanOut: _parseVec3(spec.centerTanOut)|| null,
|
|
1535
1627
|
};
|
|
1536
1628
|
}
|
|
1537
1629
|
|
|
@@ -1554,41 +1646,39 @@ class Track {
|
|
|
1554
1646
|
constructor() {
|
|
1555
1647
|
/** @type {Array} Keyframe array — shape depends on subclass. */
|
|
1556
1648
|
this.keyframes = [];
|
|
1557
|
-
/** Whether playback is active. @type {boolean} */
|
|
1649
|
+
/** Whether playback is currently active. @type {boolean} */
|
|
1558
1650
|
this.playing = false;
|
|
1559
1651
|
/** Loop at boundaries. @type {boolean} */
|
|
1560
1652
|
this.loop = false;
|
|
1561
|
-
/** Ping-pong bounce (
|
|
1653
|
+
/** Ping-pong bounce at boundaries (independent of loop). @type {boolean} */
|
|
1562
1654
|
this.bounce = false;
|
|
1563
1655
|
/** Frames per segment (≥1). @type {number} */
|
|
1564
1656
|
this.duration = 30;
|
|
1565
1657
|
/** Current segment index. @type {number} */
|
|
1566
1658
|
this.seg = 0;
|
|
1567
|
-
/** Frame offset within segment (can be fractional). @type {number} */
|
|
1659
|
+
/** Frame offset within current segment (can be fractional). @type {number} */
|
|
1568
1660
|
this.f = 0;
|
|
1569
1661
|
|
|
1570
|
-
//
|
|
1662
|
+
// Playback rate (signed: negative = reverse; 0 = frozen).
|
|
1571
1663
|
this._rate = 1;
|
|
1572
|
-
//
|
|
1664
|
+
// Current bounce travel direction (±1). Reset to 1 only on reset().
|
|
1573
1665
|
this._dir = 1;
|
|
1574
|
-
//
|
|
1666
|
+
// Whether a bounce-once has already flipped direction.
|
|
1575
1667
|
this._bounced = false;
|
|
1576
1668
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
/** @type {Function|null} */
|
|
1580
|
-
|
|
1669
|
+
/** User hook: fires on play() false→true transition. @type {Function|null} */
|
|
1670
|
+
this.onPlay = null;
|
|
1671
|
+
/** User hook: fires on natural boundary in once mode. @type {Function|null} */
|
|
1672
|
+
this.onEnd = null;
|
|
1673
|
+
/** User hook: fires on stop() / reset() / end-of-bounce-once. @type {Function|null} */
|
|
1674
|
+
this.onStop = null;
|
|
1581
1675
|
|
|
1582
|
-
// Lib-space hooks (
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
// Lib-space event mirrors — set by UI layer (trackUI), never touched by user code
|
|
1586
|
-
/** @type {Function|null} */ this._onPlay = null;
|
|
1587
|
-
/** @type {Function|null} */ this._onEnd = null;
|
|
1588
|
-
/** @type {Function|null} */ this._onStop = null;
|
|
1676
|
+
// Lib-space hooks (underscore prefix — host layer / UI layer only).
|
|
1677
|
+
this._onActivate = null; this._onDeactivate = null;
|
|
1678
|
+
this._onPlay = null; this._onEnd = null; this._onStop = null;
|
|
1589
1679
|
}
|
|
1590
1680
|
|
|
1591
|
-
/** Playback rate. Assigning never starts
|
|
1681
|
+
/** Playback rate. Signed: negative reverses, 0 freezes. Assigning never starts or stops playback. @type {number} */
|
|
1592
1682
|
get rate() { return this._rate; }
|
|
1593
1683
|
set rate(v) { this._rate = (_isNum(v)) ? v : 1; }
|
|
1594
1684
|
|
|
@@ -1596,31 +1686,46 @@ class Track {
|
|
|
1596
1686
|
get segments() { return Math.max(0, this.keyframes.length - 1); }
|
|
1597
1687
|
|
|
1598
1688
|
/**
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
|
|
1689
|
+
* @private — resolve cursor (seg, f) into continuous (seg, t).
|
|
1690
|
+
* Shared backing of every cursor-form sampler / matrix method.
|
|
1691
|
+
*/
|
|
1692
|
+
_cursorSegT() {
|
|
1693
|
+
const nSeg = this.segments;
|
|
1694
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1695
|
+
const seg = nSeg > 0 ? _clampS(this.seg, 0, nSeg - 1) : 0;
|
|
1696
|
+
const t = _clamp01(this.f / dur);
|
|
1697
|
+
return [seg, t];
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Start or update playback. Sole setter of `playing = true`.
|
|
1702
|
+
*
|
|
1703
|
+
* Fires `onPlay → _onPlay → _onActivate` only on a false→true transition.
|
|
1704
|
+
* Zero keyframes: no-op. Exactly one keyframe: snaps eval() to it but does
|
|
1705
|
+
* not set `playing` and fires no hooks.
|
|
1706
|
+
*
|
|
1707
|
+
* @param {number|{duration?:number,loop?:boolean,bounce?:boolean,
|
|
1708
|
+
* rate?:number,onPlay?:Function,onEnd?:Function,
|
|
1709
|
+
* onStop?:Function}} [rateOrOpts]
|
|
1710
|
+
* A bare number is taken as `rate`; an object configures multiple
|
|
1711
|
+
* fields in one call.
|
|
1602
1712
|
* @returns {Track} this
|
|
1603
1713
|
*/
|
|
1604
1714
|
play(rateOrOpts) {
|
|
1605
1715
|
if (this.keyframes.length === 0) return this;
|
|
1606
|
-
|
|
1607
|
-
// One keyframe: snap cursor, no animation
|
|
1608
|
-
if (this.keyframes.length === 1) {
|
|
1609
|
-
this.seg = 0; this.f = 0;
|
|
1610
|
-
return this;
|
|
1611
|
-
}
|
|
1716
|
+
if (this.keyframes.length === 1) { this.seg = 0; this.f = 0; return this; }
|
|
1612
1717
|
|
|
1613
1718
|
if (typeof rateOrOpts === 'number' && Number.isFinite(rateOrOpts)) {
|
|
1614
1719
|
this._rate = rateOrOpts;
|
|
1615
1720
|
} else if (rateOrOpts && typeof rateOrOpts === 'object') {
|
|
1616
1721
|
const o = rateOrOpts;
|
|
1617
|
-
if (_isNum(o.duration)) this.duration
|
|
1722
|
+
if (_isNum(o.duration)) this.duration = Math.max(1, o.duration | 0);
|
|
1618
1723
|
if ('loop' in o) this.loop = !!o.loop;
|
|
1619
1724
|
if ('bounce' in o) this.bounce = !!o.bounce;
|
|
1620
|
-
if (typeof o.onPlay === 'function') this.onPlay
|
|
1621
|
-
if (typeof o.onEnd === 'function') this.onEnd
|
|
1622
|
-
if (typeof o.onStop === 'function') this.onStop
|
|
1623
|
-
if (_isNum(o.rate)) this._rate
|
|
1725
|
+
if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
|
|
1726
|
+
if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
|
|
1727
|
+
if (typeof o.onStop === 'function') this.onStop = o.onStop;
|
|
1728
|
+
if (_isNum(o.rate)) this._rate = o.rate;
|
|
1624
1729
|
}
|
|
1625
1730
|
|
|
1626
1731
|
const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
|
|
@@ -1641,8 +1746,11 @@ class Track {
|
|
|
1641
1746
|
}
|
|
1642
1747
|
|
|
1643
1748
|
/**
|
|
1644
|
-
* Stop playback.
|
|
1645
|
-
*
|
|
1749
|
+
* Stop playback. Sole setter of `playing = false`. Fires
|
|
1750
|
+
* `onStop → _onStop → _onDeactivate` on a true→false transition.
|
|
1751
|
+
*
|
|
1752
|
+
* @param {boolean} [rewind] If true, seek to the origin end after stopping
|
|
1753
|
+
* (0 when playing forward, 1 when playing backward).
|
|
1646
1754
|
* @returns {Track} this
|
|
1647
1755
|
*/
|
|
1648
1756
|
stop(rewind) {
|
|
@@ -1659,7 +1767,8 @@ class Track {
|
|
|
1659
1767
|
}
|
|
1660
1768
|
|
|
1661
1769
|
/**
|
|
1662
|
-
* Clear all keyframes and stop.
|
|
1770
|
+
* Clear all keyframes and stop. Fires stop-side hooks if it was playing.
|
|
1771
|
+
* Unlike stop(), this also resets `_dir` to +1.
|
|
1663
1772
|
* @returns {Track} this
|
|
1664
1773
|
*/
|
|
1665
1774
|
reset() {
|
|
@@ -1676,9 +1785,10 @@ class Track {
|
|
|
1676
1785
|
}
|
|
1677
1786
|
|
|
1678
1787
|
/**
|
|
1679
|
-
* Remove the keyframe at index
|
|
1788
|
+
* Remove the keyframe at `index`. Adjusts the cursor if the removal shrinks
|
|
1789
|
+
* the track below the current segment.
|
|
1680
1790
|
* @param {number} index
|
|
1681
|
-
* @returns {boolean}
|
|
1791
|
+
* @returns {boolean} true if removed; false if index was invalid.
|
|
1682
1792
|
*/
|
|
1683
1793
|
remove(index) {
|
|
1684
1794
|
if (!_isNum(index)) return false;
|
|
@@ -1692,9 +1802,15 @@ class Track {
|
|
|
1692
1802
|
}
|
|
1693
1803
|
|
|
1694
1804
|
/**
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1805
|
+
* Move the cursor.
|
|
1806
|
+
* seek(t) Scrub to normalised position t ∈ [0, 1] across the
|
|
1807
|
+
* whole track.
|
|
1808
|
+
* seek(t, segIndex) Position within a specific segment — t is local to
|
|
1809
|
+
* that segment.
|
|
1810
|
+
* Does not affect `playing`.
|
|
1811
|
+
*
|
|
1812
|
+
* @param {number} t
|
|
1813
|
+
* @param {number} [segIndex]
|
|
1698
1814
|
* @returns {Track} this
|
|
1699
1815
|
*/
|
|
1700
1816
|
seek(t, segIndex) {
|
|
@@ -1711,8 +1827,8 @@ class Track {
|
|
|
1711
1827
|
}
|
|
1712
1828
|
|
|
1713
1829
|
/**
|
|
1714
|
-
* Normalised
|
|
1715
|
-
* @returns {number}
|
|
1830
|
+
* Normalised cursor position across the whole track.
|
|
1831
|
+
* @returns {number} ∈ [0, 1]; 0 when there are no segments.
|
|
1716
1832
|
*/
|
|
1717
1833
|
time() {
|
|
1718
1834
|
const nSeg = this.segments;
|
|
@@ -1722,8 +1838,11 @@ class Track {
|
|
|
1722
1838
|
}
|
|
1723
1839
|
|
|
1724
1840
|
/**
|
|
1725
|
-
* Snapshot of transport state.
|
|
1726
|
-
*
|
|
1841
|
+
* Snapshot of transport state. Allocates a new object per call — intended
|
|
1842
|
+
* for UI / debugging, not hot loops.
|
|
1843
|
+
* @returns {{keyframes:number, segments:number, seg:number, f:number,
|
|
1844
|
+
* playing:boolean, loop:boolean, bounce:boolean, rate:number,
|
|
1845
|
+
* duration:number, time:number}}
|
|
1727
1846
|
*/
|
|
1728
1847
|
info() {
|
|
1729
1848
|
return {
|
|
@@ -1741,16 +1860,19 @@ class Track {
|
|
|
1741
1860
|
}
|
|
1742
1861
|
|
|
1743
1862
|
/**
|
|
1744
|
-
* Advance cursor by rate frames.
|
|
1745
|
-
*
|
|
1746
|
-
*
|
|
1863
|
+
* Advance the cursor by `rate * _dir` in frames. Handles loop / bounce /
|
|
1864
|
+
* once modes per the table in the module header. Fires `onEnd → _onEnd →
|
|
1865
|
+
* _onDeactivate` at a natural boundary in once mode.
|
|
1866
|
+
*
|
|
1867
|
+
* Intended to be called once per animation frame by the bridge / UI layer.
|
|
1868
|
+
* rate === 0 freezes the cursor but keeps `playing` unchanged.
|
|
1869
|
+
*
|
|
1870
|
+
* @returns {boolean} Current `playing` state after advancing.
|
|
1747
1871
|
*/
|
|
1748
1872
|
tick() {
|
|
1749
1873
|
if (!this.playing) return false;
|
|
1750
1874
|
const nSeg = this.segments;
|
|
1751
|
-
if (nSeg === 0) {
|
|
1752
|
-
this.playing = false; this._onDeactivate?.(); return false;
|
|
1753
|
-
}
|
|
1875
|
+
if (nSeg === 0) { this.playing = false; this._onDeactivate?.(); return false; }
|
|
1754
1876
|
if (this._rate === 0) return true;
|
|
1755
1877
|
|
|
1756
1878
|
const dur = Math.max(1, this.duration | 0);
|
|
@@ -1758,7 +1880,6 @@ class Track {
|
|
|
1758
1880
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1759
1881
|
const next = s + this._rate * this._dir;
|
|
1760
1882
|
|
|
1761
|
-
// ── loop:true, bounce:true — bounce forever ───────────────────────────
|
|
1762
1883
|
if (this.loop && this.bounce) {
|
|
1763
1884
|
let pos = next, flips = 0;
|
|
1764
1885
|
while (pos < 0 || pos > total) {
|
|
@@ -1770,17 +1891,14 @@ class Track {
|
|
|
1770
1891
|
return true;
|
|
1771
1892
|
}
|
|
1772
1893
|
|
|
1773
|
-
// ── loop:false, bounce:true — bounce once, stop at origin ────────────
|
|
1774
1894
|
if (!this.loop && this.bounce) {
|
|
1775
1895
|
if (next >= total) {
|
|
1776
|
-
// far boundary: reflect and flip direction once
|
|
1777
1896
|
this._setCursorFromScalar(Math.min(total, 2 * total - next));
|
|
1778
1897
|
this._dir = -this._dir;
|
|
1779
1898
|
this._bounced = true;
|
|
1780
1899
|
return true;
|
|
1781
1900
|
}
|
|
1782
1901
|
if (next <= 0) {
|
|
1783
|
-
// origin: stop (whether we bounced or started backward)
|
|
1784
1902
|
this._setCursorFromScalar(0);
|
|
1785
1903
|
this.playing = false;
|
|
1786
1904
|
this._dir = 1; this._bounced = false;
|
|
@@ -1793,13 +1911,11 @@ class Track {
|
|
|
1793
1911
|
return true;
|
|
1794
1912
|
}
|
|
1795
1913
|
|
|
1796
|
-
// ── loop:true, bounce:false — repeat forever ──────────────────────────
|
|
1797
1914
|
if (this.loop) {
|
|
1798
1915
|
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1799
1916
|
return true;
|
|
1800
1917
|
}
|
|
1801
1918
|
|
|
1802
|
-
// ── loop:false, bounce:false — play once, stop at boundary ───────────
|
|
1803
1919
|
if (next <= 0) {
|
|
1804
1920
|
this._setCursorFromScalar(0);
|
|
1805
1921
|
this.playing = false;
|
|
@@ -1844,7 +1960,7 @@ class Track {
|
|
|
1844
1960
|
*
|
|
1845
1961
|
* tanIn — incoming position tangent at this keyframe (Hermite mode).
|
|
1846
1962
|
* tanOut — outgoing position tangent at this keyframe (Hermite mode).
|
|
1847
|
-
* When only one is supplied, the other mirrors it.
|
|
1963
|
+
* When only one is supplied, the other mirrors it at sample time.
|
|
1848
1964
|
* When neither is supplied, centripetal Catmull-Rom tangents are auto-computed.
|
|
1849
1965
|
*/
|
|
1850
1966
|
class PoseTrack extends Track {
|
|
@@ -1852,8 +1968,8 @@ class PoseTrack extends Track {
|
|
|
1852
1968
|
super();
|
|
1853
1969
|
/**
|
|
1854
1970
|
* Position interpolation mode.
|
|
1855
|
-
* - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom
|
|
1856
|
-
* when none are stored (default)
|
|
1971
|
+
* - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom
|
|
1972
|
+
* tangents when none are stored (default)
|
|
1857
1973
|
* - 'linear' — lerp
|
|
1858
1974
|
* - 'step' — snap to k0; useful for discrete state changes
|
|
1859
1975
|
* @type {'hermite'|'linear'|'step'}
|
|
@@ -1863,26 +1979,20 @@ class PoseTrack extends Track {
|
|
|
1863
1979
|
* Rotation interpolation mode.
|
|
1864
1980
|
* - 'slerp' — constant angular velocity (default)
|
|
1865
1981
|
* - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
|
|
1866
|
-
* - 'step' — snap to k0 quaternion
|
|
1982
|
+
* - 'step' — snap to k0 quaternion
|
|
1867
1983
|
* @type {'slerp'|'nlerp'|'step'}
|
|
1868
1984
|
*/
|
|
1869
1985
|
this.rotInterp = 'slerp';
|
|
1870
|
-
// Scratch for toMatrix() — avoids hot-path allocations
|
|
1871
|
-
this._pos = [0,0,0];
|
|
1872
|
-
this._rot = [0,0,0,1];
|
|
1873
|
-
this._scl = [1,1,1];
|
|
1874
1986
|
}
|
|
1875
1987
|
|
|
1876
1988
|
/**
|
|
1877
1989
|
* Append one or more keyframes. Adjacent duplicates are skipped by default.
|
|
1990
|
+
* Accepts any spec form understood by _parseSpec, or an array of them.
|
|
1878
1991
|
* @param {Object|Object[]} spec
|
|
1879
1992
|
* @param {{ deduplicate?: boolean }} [opts]
|
|
1880
1993
|
*/
|
|
1881
1994
|
add(spec, opts) {
|
|
1882
|
-
if (Array.isArray(spec)) {
|
|
1883
|
-
for (const s of spec) this.add(s, opts);
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1995
|
+
if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
|
|
1886
1996
|
const kf = _parseSpec(spec);
|
|
1887
1997
|
if (!kf) return;
|
|
1888
1998
|
const dedup = !opts || opts.deduplicate !== false;
|
|
@@ -1893,10 +2003,11 @@ class PoseTrack extends Track {
|
|
|
1893
2003
|
}
|
|
1894
2004
|
|
|
1895
2005
|
/**
|
|
1896
|
-
* Replace
|
|
2006
|
+
* Replace the keyframe at `index`, or append at the end if `index` equals
|
|
2007
|
+
* the current keyframe count.
|
|
1897
2008
|
* @param {number} index
|
|
1898
|
-
* @param {Object} spec
|
|
1899
|
-
* @returns {boolean}
|
|
2009
|
+
* @param {Object} spec Any spec form understood by _parseSpec.
|
|
2010
|
+
* @returns {boolean} true on success; false for invalid index or spec.
|
|
1900
2011
|
*/
|
|
1901
2012
|
set(index, spec) {
|
|
1902
2013
|
if (!_isNum(index)) return false;
|
|
@@ -1908,15 +2019,64 @@ class PoseTrack extends Track {
|
|
|
1908
2019
|
}
|
|
1909
2020
|
|
|
1910
2021
|
/**
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
2022
|
+
* Sample the position path. Zero-alloc, no cursor side effects. Honours
|
|
2023
|
+
* posInterp and the stored-tangent → auto-CR fallback chain.
|
|
2024
|
+
*
|
|
2025
|
+
* Two signatures:
|
|
2026
|
+
* samplePos(out) cursor form — reads current seg/f
|
|
2027
|
+
* samplePos(out, seg, t) explicit (seg, t), continuous on the path
|
|
2028
|
+
*
|
|
2029
|
+
* @param {number[]} out 3-element result buffer.
|
|
2030
|
+
* @param {number} [seg] Segment index in [0, segments−1]. Omit for cursor.
|
|
2031
|
+
* @param {number} [t] Local parameter in [0, 1]. Omit for cursor.
|
|
2032
|
+
* @returns {number[]} out
|
|
1914
2033
|
*/
|
|
1915
|
-
|
|
1916
|
-
|
|
2034
|
+
samplePos(out, seg, t) {
|
|
2035
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2036
|
+
return _samplePathCore(out, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Write the TRS pose as a column-major model mat4. Zero-alloc, no cursor
|
|
2041
|
+
* side effects.
|
|
2042
|
+
*
|
|
2043
|
+
* Two signatures:
|
|
2044
|
+
* mat4Model(out) cursor form — reads current seg/f
|
|
2045
|
+
* mat4Model(out, seg, t) explicit (seg, t), continuous on the path
|
|
2046
|
+
*
|
|
2047
|
+
* Replaces the previous toMatrix() method.
|
|
2048
|
+
*
|
|
2049
|
+
* @param {Float32Array|number[]} out 16-element result buffer.
|
|
2050
|
+
* @param {number} [seg] Segment index in [0, segments−1]. Omit for cursor.
|
|
2051
|
+
* @param {number} [t] Local parameter in [0, 1]. Omit for cursor.
|
|
2052
|
+
* @returns {Float32Array|number[]} out
|
|
2053
|
+
*/
|
|
2054
|
+
mat4Model(out, seg, t) {
|
|
2055
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2056
|
+
this._sampleTRS(_trsScratch, seg, t);
|
|
2057
|
+
return transformToMat4(out, _trsScratch);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* Effective incoming / outgoing position tangents at keyframe `index`.
|
|
2062
|
+
* Stored tanIn/tanOut take precedence; each mirrors the other when only
|
|
2063
|
+
* one is stored; else centripetal Catmull-Rom tangents are auto-computed
|
|
2064
|
+
* from neighbours. Boundary keyframes mirror across the missing side.
|
|
2065
|
+
*
|
|
2066
|
+
* @param {number[]} outIn 3-element result — incoming tangent.
|
|
2067
|
+
* @param {number[]} outOut 3-element result — outgoing tangent.
|
|
2068
|
+
* @param {number} index Keyframe index.
|
|
2069
|
+
* @returns {PoseTrack} this
|
|
2070
|
+
*/
|
|
2071
|
+
tangents(outIn, outOut, index) {
|
|
2072
|
+
_sampleTangentsCore(outIn, outOut, this.keyframes, 'pos', 'tanIn', 'tanOut', index);
|
|
2073
|
+
return this;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/** @private — write interpolated TRS at (seg, t). */
|
|
2077
|
+
_sampleTRS(out, seg, t) {
|
|
1917
2078
|
const n = this.keyframes.length;
|
|
1918
2079
|
if (n === 0) return out;
|
|
1919
|
-
|
|
1920
2080
|
if (n === 1) {
|
|
1921
2081
|
const k = this.keyframes[0];
|
|
1922
2082
|
out.pos[0]=k.pos[0]; out.pos[1]=k.pos[1]; out.pos[2]=k.pos[2];
|
|
@@ -1924,32 +2084,16 @@ class PoseTrack extends Track {
|
|
|
1924
2084
|
out.scl[0]=k.scl[0]; out.scl[1]=k.scl[1]; out.scl[2]=k.scl[2];
|
|
1925
2085
|
return out;
|
|
1926
2086
|
}
|
|
1927
|
-
|
|
1928
2087
|
const nSeg = n - 1;
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
out.pos[0]=k0.pos[0]; out.pos[1]=k0.pos[1]; out.pos[2]=k0.pos[2];
|
|
1938
|
-
} else if (this.posInterp === 'linear') {
|
|
1939
|
-
lerpVec3(out.pos, k0.pos, k1.pos, t);
|
|
1940
|
-
} else {
|
|
1941
|
-
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1942
|
-
seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1943
|
-
const m0 = k0.tanOut != null ? k0.tanOut
|
|
1944
|
-
: k0.tanIn != null ? k0.tanIn
|
|
1945
|
-
: _crTanOut(_m0, p0, k0.pos, k1.pos);
|
|
1946
|
-
const m1 = k1.tanIn != null ? k1.tanIn
|
|
1947
|
-
: k1.tanOut != null ? k1.tanOut
|
|
1948
|
-
: _crTanIn(_m1, p0, k0.pos, k1.pos);
|
|
1949
|
-
hermiteVec3(out.pos, k0.pos, m0, k1.pos, m1, t);
|
|
1950
|
-
}
|
|
2088
|
+
seg = seg | 0;
|
|
2089
|
+
if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
|
|
2090
|
+
else if (seg < 0) { seg = 0; t = 0; }
|
|
2091
|
+
t = _clamp01(t);
|
|
2092
|
+
const k0 = this.keyframes[seg];
|
|
2093
|
+
const k1 = this.keyframes[seg + 1];
|
|
2094
|
+
|
|
2095
|
+
_samplePathCore(out.pos, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
|
|
1951
2096
|
|
|
1952
|
-
// rot — step, slerp, or nlerp
|
|
1953
2097
|
if (this.rotInterp === 'step') {
|
|
1954
2098
|
out.rot[0]=k0.rot[0]; out.rot[1]=k0.rot[1]; out.rot[2]=k0.rot[2]; out.rot[3]=k0.rot[3];
|
|
1955
2099
|
} else if (this.rotInterp === 'nlerp') {
|
|
@@ -1958,20 +2102,22 @@ class PoseTrack extends Track {
|
|
|
1958
2102
|
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
1959
2103
|
}
|
|
1960
2104
|
|
|
1961
|
-
// scl — lerp
|
|
1962
2105
|
lerpVec3(out.scl, k0.scl, k1.scl, t);
|
|
1963
|
-
|
|
1964
2106
|
return out;
|
|
1965
2107
|
}
|
|
1966
2108
|
|
|
1967
2109
|
/**
|
|
1968
|
-
* Evaluate
|
|
1969
|
-
* @param {
|
|
1970
|
-
* @returns {
|
|
2110
|
+
* Evaluate interpolated TRS pose at current cursor.
|
|
2111
|
+
* @param {{ pos:number[], rot:number[], scl:number[] }} [out]
|
|
2112
|
+
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1971
2113
|
*/
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2114
|
+
eval(out) {
|
|
2115
|
+
out = out || { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
|
|
2116
|
+
const n = this.keyframes.length;
|
|
2117
|
+
if (n === 0) return out;
|
|
2118
|
+
if (n === 1) return this._sampleTRS(out, 0, 0);
|
|
2119
|
+
const [seg, t] = this._cursorSegT();
|
|
2120
|
+
return this._sampleTRS(out, seg, t);
|
|
1975
2121
|
}
|
|
1976
2122
|
}
|
|
1977
2123
|
|
|
@@ -1990,21 +2136,18 @@ class PoseTrack extends Track {
|
|
|
1990
2136
|
* fov — vertical fov (radians) for perspective cameras; null for ortho.
|
|
1991
2137
|
* halfHeight — world-unit half-height of ortho frustum; null for perspective.
|
|
1992
2138
|
* Both are optional and nullable. eval() lerps each only when both adjacent
|
|
1993
|
-
* keyframes carry a non-null value for that field
|
|
2139
|
+
* keyframes carry a non-null value for that field; mixed or missing entries
|
|
2140
|
+
* pass `null` through.
|
|
1994
2141
|
*
|
|
1995
2142
|
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite
|
|
1996
|
-
* interpolation of the eye and center paths respectively.
|
|
1997
|
-
*
|
|
2143
|
+
* interpolation of the eye and center paths respectively. When absent,
|
|
2144
|
+
* centripetal Catmull-Rom tangents are auto-computed at sample time.
|
|
1998
2145
|
*
|
|
1999
2146
|
* Missing fields default to: center → [0,0,0], up → [0,1,0].
|
|
2000
2147
|
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
2005
|
-
*
|
|
2006
|
-
* To capture a matrix-based pose, use PoseTrack.add({ mat4Model: mat4Eye })
|
|
2007
|
-
* for full-fidelity including roll, or cam.capturePose() for lookat-style.
|
|
2148
|
+
* For matrix-based capture of a camera-like pose use
|
|
2149
|
+
* PoseTrack.add({ mat4Model: mat4Eye }) for full TRS fidelity including roll,
|
|
2150
|
+
* or a lookat spec here for camera-style interpolation.
|
|
2008
2151
|
*/
|
|
2009
2152
|
class CameraTrack extends Track {
|
|
2010
2153
|
constructor() {
|
|
@@ -2022,15 +2165,13 @@ class CameraTrack extends Track {
|
|
|
2022
2165
|
}
|
|
2023
2166
|
|
|
2024
2167
|
/**
|
|
2025
|
-
* Append one or more camera keyframes. Adjacent duplicates are skipped by
|
|
2168
|
+
* Append one or more camera keyframes. Adjacent duplicates are skipped by
|
|
2169
|
+
* default.
|
|
2026
2170
|
* @param {Object|Object[]} spec
|
|
2027
2171
|
* @param {{ deduplicate?: boolean }} [opts]
|
|
2028
2172
|
*/
|
|
2029
2173
|
add(spec, opts) {
|
|
2030
|
-
if (Array.isArray(spec)) {
|
|
2031
|
-
for (const s of spec) this.add(s, opts);
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2174
|
+
if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
|
|
2034
2175
|
const kf = _parseCameraSpec(spec);
|
|
2035
2176
|
if (!kf) return;
|
|
2036
2177
|
const dedup = !opts || opts.deduplicate !== false;
|
|
@@ -2041,7 +2182,8 @@ class CameraTrack extends Track {
|
|
|
2041
2182
|
}
|
|
2042
2183
|
|
|
2043
2184
|
/**
|
|
2044
|
-
* Replace
|
|
2185
|
+
* Replace the camera keyframe at `index`, or append at the end if `index`
|
|
2186
|
+
* equals the current keyframe count.
|
|
2045
2187
|
* @param {number} index
|
|
2046
2188
|
* @param {Object} spec
|
|
2047
2189
|
* @returns {boolean}
|
|
@@ -2056,73 +2198,129 @@ class CameraTrack extends Track {
|
|
|
2056
2198
|
}
|
|
2057
2199
|
|
|
2058
2200
|
/**
|
|
2059
|
-
*
|
|
2201
|
+
* Sample the eye path. Zero-alloc, no cursor side effects.
|
|
2202
|
+
*
|
|
2203
|
+
* Two signatures:
|
|
2204
|
+
* sampleEye(out) cursor form — reads current seg/f
|
|
2205
|
+
* sampleEye(out, seg, t) explicit (seg, t), continuous on the path
|
|
2060
2206
|
*
|
|
2061
|
-
* @param {
|
|
2062
|
-
* @
|
|
2207
|
+
* @param {number[]} out
|
|
2208
|
+
* @param {number} [seg]
|
|
2209
|
+
* @param {number} [t]
|
|
2210
|
+
* @returns {number[]} out
|
|
2063
2211
|
*/
|
|
2064
|
-
|
|
2065
|
-
|
|
2212
|
+
sampleEye(out, seg, t) {
|
|
2213
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2214
|
+
return _samplePathCore(out, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Sample the center path. Zero-alloc, no cursor side effects.
|
|
2219
|
+
*
|
|
2220
|
+
* Two signatures:
|
|
2221
|
+
* sampleCenter(out) cursor form — reads current seg/f
|
|
2222
|
+
* sampleCenter(out, seg, t) explicit (seg, t)
|
|
2223
|
+
*
|
|
2224
|
+
* @param {number[]} out
|
|
2225
|
+
* @param {number} [seg]
|
|
2226
|
+
* @param {number} [t]
|
|
2227
|
+
* @returns {number[]} out
|
|
2228
|
+
*/
|
|
2229
|
+
sampleCenter(out, seg, t) {
|
|
2230
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2231
|
+
return _samplePathCore(out, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
/**
|
|
2235
|
+
* Write the interpolated lookat eye matrix as a column-major mat4
|
|
2236
|
+
* (eye→world rigid frame). Zero-alloc, no cursor side effects.
|
|
2237
|
+
*
|
|
2238
|
+
* Two signatures:
|
|
2239
|
+
* mat4Eye(out) cursor form — reads current seg/f
|
|
2240
|
+
* mat4Eye(out, seg, t) explicit (seg, t), continuous on the path
|
|
2241
|
+
*
|
|
2242
|
+
* @param {Float32Array|number[]} out 16-element result buffer.
|
|
2243
|
+
* @param {number} [seg]
|
|
2244
|
+
* @param {number} [t]
|
|
2245
|
+
* @returns {Float32Array|number[]} out
|
|
2246
|
+
*/
|
|
2247
|
+
mat4Eye(out, seg, t) {
|
|
2248
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2249
|
+
this._sampleEyePose(_eyeScratch, seg, t);
|
|
2250
|
+
const e = _eyeScratch;
|
|
2251
|
+
return mat4Eye(out,
|
|
2252
|
+
e.eye[0], e.eye[1], e.eye[2],
|
|
2253
|
+
e.center[0], e.center[1], e.center[2],
|
|
2254
|
+
e.up[0], e.up[1], e.up[2]);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/**
|
|
2258
|
+
* Effective in/out eye tangents at keyframe `index`.
|
|
2259
|
+
*/
|
|
2260
|
+
eyeTangents(outIn, outOut, index) {
|
|
2261
|
+
_sampleTangentsCore(outIn, outOut, this.keyframes, 'eye', 'eyeTanIn', 'eyeTanOut', index);
|
|
2262
|
+
return this;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
/**
|
|
2266
|
+
* Effective in/out center tangents at keyframe `index`.
|
|
2267
|
+
*/
|
|
2268
|
+
centerTangents(outIn, outOut, index) {
|
|
2269
|
+
_sampleTangentsCore(outIn, outOut, this.keyframes, 'center', 'centerTanIn', 'centerTanOut', index);
|
|
2270
|
+
return this;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/** @private — write interpolated { eye, center, up } at (seg, t). */
|
|
2274
|
+
_sampleEyePose(out, seg, t) {
|
|
2066
2275
|
const n = this.keyframes.length;
|
|
2067
2276
|
if (n === 0) return out;
|
|
2068
|
-
|
|
2069
2277
|
if (n === 1) {
|
|
2070
2278
|
const k = this.keyframes[0];
|
|
2071
2279
|
out.eye[0]=k.eye[0]; out.eye[1]=k.eye[1]; out.eye[2]=k.eye[2];
|
|
2072
2280
|
out.center[0]=k.center[0]; out.center[1]=k.center[1]; out.center[2]=k.center[2];
|
|
2073
2281
|
out.up[0]=k.up[0]; out.up[1]=k.up[1]; out.up[2]=k.up[2];
|
|
2074
|
-
out.fov = k.fov;
|
|
2075
|
-
out.halfHeight = k.halfHeight;
|
|
2076
2282
|
return out;
|
|
2077
2283
|
}
|
|
2078
|
-
|
|
2079
2284
|
const nSeg = n - 1;
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
// eye — Hermite (auto-CR tangents when none stored), linear, or step
|
|
2087
|
-
if (this.eyeInterp === 'step') {
|
|
2088
|
-
out.eye[0]=k0.eye[0]; out.eye[1]=k0.eye[1]; out.eye[2]=k0.eye[2];
|
|
2089
|
-
} else if (this.eyeInterp === 'linear') {
|
|
2090
|
-
lerpVec3(out.eye, k0.eye, k1.eye, t);
|
|
2091
|
-
} else {
|
|
2092
|
-
const p0 = seg > 0 ? this.keyframes[seg - 1].eye : k0.eye;
|
|
2093
|
-
seg + 2 < n ? this.keyframes[seg + 2].eye : k1.eye;
|
|
2094
|
-
const m0 = k0.eyeTanOut != null ? k0.eyeTanOut
|
|
2095
|
-
: k0.eyeTanIn != null ? k0.eyeTanIn
|
|
2096
|
-
: _crTanOut(_m0, p0, k0.eye, k1.eye);
|
|
2097
|
-
const m1 = k1.eyeTanIn != null ? k1.eyeTanIn
|
|
2098
|
-
: k1.eyeTanOut != null ? k1.eyeTanOut
|
|
2099
|
-
: _crTanIn(_m1, p0, k0.eye, k1.eye);
|
|
2100
|
-
hermiteVec3(out.eye, k0.eye, m0, k1.eye, m1, t);
|
|
2101
|
-
}
|
|
2285
|
+
seg = seg | 0;
|
|
2286
|
+
if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
|
|
2287
|
+
else if (seg < 0) { seg = 0; t = 0; }
|
|
2288
|
+
t = _clamp01(t);
|
|
2289
|
+
const k0 = this.keyframes[seg];
|
|
2290
|
+
const k1 = this.keyframes[seg + 1];
|
|
2102
2291
|
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
out.center[0]=k0.center[0]; out.center[1]=k0.center[1]; out.center[2]=k0.center[2];
|
|
2106
|
-
} else if (this.centerInterp === 'hermite') {
|
|
2107
|
-
const c0 = seg > 0 ? this.keyframes[seg - 1].center : k0.center;
|
|
2108
|
-
seg + 2 < n ? this.keyframes[seg + 2].center : k1.center;
|
|
2109
|
-
const m0 = k0.centerTanOut != null ? k0.centerTanOut
|
|
2110
|
-
: k0.centerTanIn != null ? k0.centerTanIn
|
|
2111
|
-
: _crTanOut(_m0, c0, k0.center, k1.center);
|
|
2112
|
-
const m1 = k1.centerTanIn != null ? k1.centerTanIn
|
|
2113
|
-
: k1.centerTanOut != null ? k1.centerTanOut
|
|
2114
|
-
: _crTanIn(_m1, c0, k0.center, k1.center);
|
|
2115
|
-
hermiteVec3(out.center, k0.center, m0, k1.center, m1, t);
|
|
2116
|
-
} else {
|
|
2117
|
-
lerpVec3(out.center, k0.center, k1.center, t);
|
|
2118
|
-
}
|
|
2292
|
+
_samplePathCore(out.eye, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
|
|
2293
|
+
_samplePathCore(out.center, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
|
|
2119
2294
|
|
|
2120
|
-
// up — nlerp on unit sphere
|
|
2121
2295
|
lerpVec3(out.up, k0.up, k1.up, t);
|
|
2122
|
-
const ul=Math.sqrt(out.up[0]*out.up[0]+out.up[1]*out.up[1]+out.up[2]*out.up[2])||1;
|
|
2296
|
+
const ul = Math.sqrt(out.up[0]*out.up[0]+out.up[1]*out.up[1]+out.up[2]*out.up[2]) || 1;
|
|
2123
2297
|
out.up[0]/=ul; out.up[1]/=ul; out.up[2]/=ul;
|
|
2298
|
+
return out;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* Evaluate interpolated camera pose at current cursor.
|
|
2303
|
+
* @param {{ eye:number[], center:number[], up:number[],
|
|
2304
|
+
* fov:number|null, halfHeight:number|null }} [out]
|
|
2305
|
+
* @returns {{ eye:number[], center:number[], up:number[],
|
|
2306
|
+
* fov:number|null, halfHeight:number|null }} out
|
|
2307
|
+
*/
|
|
2308
|
+
eval(out) {
|
|
2309
|
+
out = out || { eye:[0,0,0], center:[0,0,0], up:[0,1,0], fov:null, halfHeight:null };
|
|
2310
|
+
const n = this.keyframes.length;
|
|
2311
|
+
if (n === 0) return out;
|
|
2312
|
+
if (n === 1) {
|
|
2313
|
+
const k = this.keyframes[0];
|
|
2314
|
+
this._sampleEyePose(out, 0, 0);
|
|
2315
|
+
out.fov = k.fov; out.halfHeight = k.halfHeight;
|
|
2316
|
+
return out;
|
|
2317
|
+
}
|
|
2318
|
+
const [seg, t] = this._cursorSegT();
|
|
2319
|
+
const k0 = this.keyframes[seg];
|
|
2320
|
+
const k1 = this.keyframes[seg + 1];
|
|
2321
|
+
|
|
2322
|
+
this._sampleEyePose(out, seg, t);
|
|
2124
2323
|
|
|
2125
|
-
// fov / halfHeight — lerp when both keyframes carry non-null values
|
|
2126
2324
|
out.fov = (k0.fov != null && k1.fov != null)
|
|
2127
2325
|
? k0.fov + t * (k1.fov - k0.fov) : (k0.fov ?? k1.fov ?? null);
|
|
2128
2326
|
out.halfHeight = (k0.halfHeight != null && k1.halfHeight != null)
|