@nakednous/tree 0.0.20 → 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 +61 -7
- package/dist/index.js +333 -297
- 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,41 +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
|
|
1174
|
+
* CameraTrack — { eye, center, up, fov?, halfHeight? } lookat keyframes
|
|
1175
1175
|
*
|
|
1176
|
-
* ──
|
|
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
|
-
* ── Path samplers (public, zero-alloc, no cursor side effects) ──────────────
|
|
1186
|
-
* PoseTrack
|
|
1187
|
-
* samplePos(out, seg, t) writes interpolated pos at (seg, t∈[0,1])
|
|
1188
|
-
* sampleTangents(outIn, outOut, i) effective in/out tangents at keyframe i
|
|
1189
|
-
* CameraTrack
|
|
1190
|
-
* sampleEye(out, seg, t)
|
|
1191
|
-
* sampleCenter(out, seg, t)
|
|
1192
|
-
* sampleEyeTangents(outIn, outOut, i)
|
|
1193
|
-
* sampleCenterTangents(outIn, outOut, i)
|
|
1194
|
-
*
|
|
1195
|
-
* Samplers honour the corresponding interpolation mode (hermite/linear/step)
|
|
1196
|
-
* and the stored-tangent / auto-CR fallback chain used by eval().
|
|
1197
|
-
*
|
|
1198
|
-
* ── Hook architecture ─────────────────────────────────────────────────────────
|
|
1199
|
-
* Lib-space hooks (underscore prefix — reserved for host layer / UI layer):
|
|
1200
|
-
* _onActivate / _onDeactivate — fire on playing transitions false→true / true→false.
|
|
1201
|
-
* _onPlay / _onEnd / _onStop — mirror the user-space hooks; used by the UI layer
|
|
1202
|
-
* 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.
|
|
1203
1227
|
*
|
|
1204
|
-
* User-space hooks
|
|
1228
|
+
* User-space hooks:
|
|
1205
1229
|
* onPlay : fires in play() on false→true transition.
|
|
1206
1230
|
* onEnd : fires in tick() at natural boundary (once mode only).
|
|
1207
|
-
* onStop : fires in stop() / reset()
|
|
1208
|
-
* onEnd and onStop are mutually exclusive per event.
|
|
1231
|
+
* onStop : fires in stop() / reset().
|
|
1209
1232
|
*
|
|
1210
1233
|
* Firing order:
|
|
1211
1234
|
* play() → onPlay → _onPlay → _onActivate
|
|
@@ -1213,18 +1236,14 @@ function mat4ToRotation(out4, m) {
|
|
|
1213
1236
|
* stop() → onStop → _onStop → _onDeactivate
|
|
1214
1237
|
* reset() → onStop → _onStop → _onDeactivate
|
|
1215
1238
|
*
|
|
1216
|
-
* ── Loop modes
|
|
1239
|
+
* ── Loop modes ───────────────────────────────────────────────────────────
|
|
1217
1240
|
* loop:false, bounce:false — play once, stop at end (fires onEnd)
|
|
1218
1241
|
* loop:true, bounce:false — repeat, wrap back to start
|
|
1219
1242
|
* loop:true, bounce:true — bounce forever at boundaries
|
|
1220
1243
|
* loop:false, bounce:true — bounce once: flip at far boundary, stop at origin
|
|
1221
1244
|
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
* ── Playback semantics (rate + _dir) ─────────────────────────────────────────
|
|
1225
|
-
* rate > 0 forward
|
|
1226
|
-
* rate < 0 backward
|
|
1227
|
-
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
1245
|
+
* ── Playback semantics (rate + _dir) ─────────────────────────────────────
|
|
1246
|
+
* rate > 0 forward rate < 0 backward rate === 0 frozen
|
|
1228
1247
|
*
|
|
1229
1248
|
* play() is the sole setter of playing = true.
|
|
1230
1249
|
* stop() is the sole setter of playing = false.
|
|
@@ -1232,11 +1251,9 @@ function mat4ToRotation(out4, m) {
|
|
|
1232
1251
|
*
|
|
1233
1252
|
* _dir (internal, ±1) tracks the current bounce travel direction.
|
|
1234
1253
|
* tick() advances by rate * _dir and flips _dir at boundaries.
|
|
1235
|
-
*
|
|
1236
|
-
* _dir is reset to 1 only in reset() (keyframes cleared) — stop/replay
|
|
1237
|
-
* preserves the current travel direction.
|
|
1254
|
+
* _dir is reset to 1 only in reset().
|
|
1238
1255
|
*
|
|
1239
|
-
* ── One-keyframe behaviour
|
|
1256
|
+
* ── One-keyframe behaviour ───────────────────────────────────────────────
|
|
1240
1257
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
1241
1258
|
* setting playing = true and without firing hooks.
|
|
1242
1259
|
*/
|
|
@@ -1253,12 +1270,11 @@ function _dist3(a, b) {
|
|
|
1253
1270
|
|
|
1254
1271
|
/**
|
|
1255
1272
|
* Cubic Hermite interpolation between p0 and p1 with explicit tangents.
|
|
1256
|
-
* Catmull-Rom is a special case where m0/m1 are auto-computed from neighbors.
|
|
1257
1273
|
* @param {number[]} out 3-element result.
|
|
1258
1274
|
* @param {number[]} p0 Segment start.
|
|
1259
|
-
* @param {number[]} m0 Outgoing tangent at p0
|
|
1275
|
+
* @param {number[]} m0 Outgoing tangent at p0.
|
|
1260
1276
|
* @param {number[]} p1 Segment end.
|
|
1261
|
-
* @param {number[]} m1 Incoming tangent at p1
|
|
1277
|
+
* @param {number[]} m1 Incoming tangent at p1.
|
|
1262
1278
|
* @param {number} t Blend [0, 1].
|
|
1263
1279
|
* @returns {number[]} out
|
|
1264
1280
|
*/
|
|
@@ -1271,7 +1287,7 @@ const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
|
|
|
1271
1287
|
return out;
|
|
1272
1288
|
};
|
|
1273
1289
|
|
|
1274
|
-
// Centripetal CR outgoing tangent at p1 for segment p1→p2
|
|
1290
|
+
// Centripetal CR outgoing tangent at p1 for segment p1→p2.
|
|
1275
1291
|
// Signature: (out, p0, p1, p2). Returns tangent AT p1 (the middle point).
|
|
1276
1292
|
const _crTanOut = (out, p0, p1, p2) => {
|
|
1277
1293
|
const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1;
|
|
@@ -1279,7 +1295,7 @@ const _crTanOut = (out, p0, p1, p2) => {
|
|
|
1279
1295
|
return out;
|
|
1280
1296
|
};
|
|
1281
1297
|
|
|
1282
|
-
// Centripetal CR incoming tangent at p2 for segment p1→p2
|
|
1298
|
+
// Centripetal CR incoming tangent at p2 for segment p1→p2.
|
|
1283
1299
|
// Signature: (out, p1, p2, p3). Returns tangent AT p2 (the middle point).
|
|
1284
1300
|
const _crTanIn = (out, p1, p2, p3) => {
|
|
1285
1301
|
const dt1=Math.pow(_dist3(p1,p2),0.5)||1, dt2=Math.pow(_dist3(p2,p3),0.5)||1;
|
|
@@ -1287,16 +1303,13 @@ const _crTanIn = (out, p1, p2, p3) => {
|
|
|
1287
1303
|
return out;
|
|
1288
1304
|
};
|
|
1289
1305
|
|
|
1290
|
-
// Module-level scratch — shared across
|
|
1306
|
+
// Module-level scratch — shared across track instances (non-reentrant hot path).
|
|
1291
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] };
|
|
1292
1310
|
|
|
1293
1311
|
/**
|
|
1294
1312
|
* Linear interpolation between two vec3s.
|
|
1295
|
-
* @param {number[]} out
|
|
1296
|
-
* @param {number[]} a
|
|
1297
|
-
* @param {number[]} b
|
|
1298
|
-
* @param {number} t Blend [0, 1].
|
|
1299
|
-
* @returns {number[]} out
|
|
1300
1313
|
*/
|
|
1301
1314
|
const lerpVec3 = (out, a, b, t) => {
|
|
1302
1315
|
out[0]=a[0]+t*(b[0]-a[0]);
|
|
@@ -1308,27 +1321,8 @@ const lerpVec3 = (out, a, b, t) => {
|
|
|
1308
1321
|
// =========================================================================
|
|
1309
1322
|
// S2b Path samplers — shared core
|
|
1310
1323
|
// =========================================================================
|
|
1311
|
-
//
|
|
1312
|
-
// These helpers factor out the per-field interpolation from eval(), so that
|
|
1313
|
-
// the samplers (samplePos / sampleEye / sampleCenter / sampleTangents / ...)
|
|
1314
|
-
// can write into caller buffers without touching the cursor state.
|
|
1315
|
-
//
|
|
1316
|
-
// The field name and its associated tangent field names are passed as keys
|
|
1317
|
-
// so the same core serves 'pos'/'tanIn'/'tanOut' for PoseTrack and
|
|
1318
|
-
// 'eye'|'center' + matching tangent keys for CameraTrack.
|
|
1319
1324
|
|
|
1320
|
-
/**
|
|
1321
|
-
* Sample interpolated vec3 path at (seg, t) into out.
|
|
1322
|
-
* @private
|
|
1323
|
-
* @param {number[]} out
|
|
1324
|
-
* @param {Array} kfs keyframe array
|
|
1325
|
-
* @param {string} interp 'hermite' | 'linear' | 'step'
|
|
1326
|
-
* @param {string} field keyframe property holding the vec3 path point
|
|
1327
|
-
* @param {string} tanInName keyframe property for incoming tangent
|
|
1328
|
-
* @param {string} tanOutName keyframe property for outgoing tangent
|
|
1329
|
-
* @param {number} seg segment index
|
|
1330
|
-
* @param {number} t local parameter in [0,1]
|
|
1331
|
-
*/
|
|
1325
|
+
/** @private — sample interpolated vec3 path at (seg, t) into out. */
|
|
1332
1326
|
function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t) {
|
|
1333
1327
|
const n = kfs.length;
|
|
1334
1328
|
if (n === 0) { out[0]=0; out[1]=0; out[2]=0; return out; }
|
|
@@ -1338,7 +1332,9 @@ function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t)
|
|
|
1338
1332
|
return out;
|
|
1339
1333
|
}
|
|
1340
1334
|
const nSeg = n - 1;
|
|
1341
|
-
seg =
|
|
1335
|
+
seg = seg | 0;
|
|
1336
|
+
if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
|
|
1337
|
+
else if (seg < 0) { seg = 0; t = 0; }
|
|
1342
1338
|
t = _clamp01(t);
|
|
1343
1339
|
const k0 = kfs[seg];
|
|
1344
1340
|
const k1 = kfs[seg + 1];
|
|
@@ -1352,7 +1348,6 @@ function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t)
|
|
|
1352
1348
|
return lerpVec3(out, k0[field], k1[field], t);
|
|
1353
1349
|
}
|
|
1354
1350
|
|
|
1355
|
-
// hermite (default)
|
|
1356
1351
|
const p0 = seg > 0 ? kfs[seg - 1][field] : k0[field];
|
|
1357
1352
|
const p3 = seg + 2 < n ? kfs[seg + 2][field] : k1[field];
|
|
1358
1353
|
const m0 = k0[tanOutName] != null ? k0[tanOutName]
|
|
@@ -1364,19 +1359,7 @@ function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t)
|
|
|
1364
1359
|
return hermiteVec3(out, k0[field], m0, k1[field], m1, t);
|
|
1365
1360
|
}
|
|
1366
1361
|
|
|
1367
|
-
/**
|
|
1368
|
-
* Write the effective in/out tangents at keyframe i.
|
|
1369
|
-
*
|
|
1370
|
-
* Stored tanIn/tanOut take precedence, then each mirrors the other when only
|
|
1371
|
-
* one is stored, else centripetal Catmull-Rom tangents are auto-computed from
|
|
1372
|
-
* neighbours.
|
|
1373
|
-
*
|
|
1374
|
-
* At endpoints one side has no adjacent segment; that side mirrors the other
|
|
1375
|
-
* side's tangent. Callers drawing arrows at endpoints therefore see a vector
|
|
1376
|
-
* that matches the curve's derivative into / out of the curve's boundary.
|
|
1377
|
-
*
|
|
1378
|
-
* @private
|
|
1379
|
-
*/
|
|
1362
|
+
/** @private — write effective in/out tangents at keyframe i. */
|
|
1380
1363
|
function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i) {
|
|
1381
1364
|
const n = kfs.length;
|
|
1382
1365
|
if (n === 0) {
|
|
@@ -1388,7 +1371,6 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1388
1371
|
const hasTI = ki[tanInName] != null;
|
|
1389
1372
|
const hasTO = ki[tanOutName] != null;
|
|
1390
1373
|
|
|
1391
|
-
// ── outgoing tangent at keyframe i (for segment i → i+1) ──────────────
|
|
1392
1374
|
if (hasTO) {
|
|
1393
1375
|
outOut[0]=ki[tanOutName][0]; outOut[1]=ki[tanOutName][1]; outOut[2]=ki[tanOutName][2];
|
|
1394
1376
|
} else if (hasTI) {
|
|
@@ -1398,10 +1380,9 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1398
1380
|
const p0 = i > 0 ? kfs[i - 1][field] : ki[field];
|
|
1399
1381
|
_crTanOut(outOut, p0, ki[field], k1[field]);
|
|
1400
1382
|
} else {
|
|
1401
|
-
outOut[0]=0; outOut[1]=0; outOut[2]=0;
|
|
1383
|
+
outOut[0]=0; outOut[1]=0; outOut[2]=0;
|
|
1402
1384
|
}
|
|
1403
1385
|
|
|
1404
|
-
// ── incoming tangent at keyframe i (for segment i-1 → i) ──────────────
|
|
1405
1386
|
if (hasTI) {
|
|
1406
1387
|
outIn[0]=ki[tanInName][0]; outIn[1]=ki[tanInName][1]; outIn[2]=ki[tanInName][2];
|
|
1407
1388
|
} else if (hasTO) {
|
|
@@ -1414,7 +1395,6 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1414
1395
|
outIn[0]=outOut[0]; outIn[1]=outOut[1]; outIn[2]=outOut[2];
|
|
1415
1396
|
}
|
|
1416
1397
|
|
|
1417
|
-
// Boundary mirror the other way: last keyframe with no stored tangents.
|
|
1418
1398
|
if (i === n - 1 && !hasTO && !hasTI) {
|
|
1419
1399
|
outOut[0]=outIn[0]; outOut[1]=outIn[1]; outOut[2]=outIn[2];
|
|
1420
1400
|
}
|
|
@@ -1426,9 +1406,6 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1426
1406
|
|
|
1427
1407
|
/**
|
|
1428
1408
|
* Write a TRS transform into a column-major mat4.
|
|
1429
|
-
* @param {Float32Array|number[]} out 16-element column-major mat4.
|
|
1430
|
-
* @param {{ pos:number[], rot:number[], scl:number[] }} xform
|
|
1431
|
-
* @returns {Float32Array|number[]} out
|
|
1432
1409
|
*/
|
|
1433
1410
|
const transformToMat4 = (out, xform) => {
|
|
1434
1411
|
qToMat4(out, xform.rot);
|
|
@@ -1442,10 +1419,6 @@ const transformToMat4 = (out, xform) => {
|
|
|
1442
1419
|
|
|
1443
1420
|
/**
|
|
1444
1421
|
* Decompose a column-major mat4 into a TRS transform.
|
|
1445
|
-
* Assumes no shear. Scale extracted from column lengths.
|
|
1446
|
-
* @param {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1447
|
-
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1448
|
-
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1449
1422
|
*/
|
|
1450
1423
|
const mat4ToTransform = (out, m) => {
|
|
1451
1424
|
out.pos[0]=m[12]; out.pos[1]=m[13]; out.pos[2]=m[14];
|
|
@@ -1476,7 +1449,6 @@ function _parseVec3(v) {
|
|
|
1476
1449
|
return null;
|
|
1477
1450
|
}
|
|
1478
1451
|
|
|
1479
|
-
// Euler: unit axis vectors and the six valid intrinsic orderings.
|
|
1480
1452
|
const _EULER_AXES = { X:[1,0,0], Y:[0,1,0], Z:[0,0,1] };
|
|
1481
1453
|
const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
1482
1454
|
|
|
@@ -1484,14 +1456,13 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1484
1456
|
* Parse any rotation representation into a unit quaternion [x,y,z,w].
|
|
1485
1457
|
*
|
|
1486
1458
|
* Accepted forms:
|
|
1487
|
-
*
|
|
1488
1459
|
* [x,y,z,w] — raw quaternion array
|
|
1489
|
-
* { axis:[x,y,z], angle } — axis-angle
|
|
1460
|
+
* { axis:[x,y,z], angle } — axis-angle (angle in radians)
|
|
1490
1461
|
* { dir:[x,y,z], up?:[x,y,z] } — forward direction (−Z) with optional up
|
|
1491
1462
|
* { mat4Eye: mat4 } — rotation block of an eye matrix
|
|
1492
1463
|
* { mat3: mat3 } — column-major 3×3 rotation matrix
|
|
1493
1464
|
* { euler:[rx,ry,rz], order? } — intrinsic Euler (default order: YXZ)
|
|
1494
|
-
* { 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
|
|
1495
1466
|
*
|
|
1496
1467
|
* @param {*} v
|
|
1497
1468
|
* @returns {number[]|null} [x,y,z,w] or null if unparseable.
|
|
@@ -1499,7 +1470,7 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1499
1470
|
function _parseQuat(v) {
|
|
1500
1471
|
if (!v) return null;
|
|
1501
1472
|
|
|
1502
|
-
//
|
|
1473
|
+
// [x,y,z,w]
|
|
1503
1474
|
if (Array.isArray(v) && v.length === 4) return [v[0],v[1],v[2],v[3]];
|
|
1504
1475
|
if (ArrayBuffer.isView(v) && v.length >= 4) return [v[0],v[1],v[2],v[3]];
|
|
1505
1476
|
|
|
@@ -1528,8 +1499,7 @@ function _parseQuat(v) {
|
|
|
1528
1499
|
|
|
1529
1500
|
// { mat3 }
|
|
1530
1501
|
if (v.mat3 != null) {
|
|
1531
|
-
const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3))
|
|
1532
|
-
? v.mat3 : null;
|
|
1502
|
+
const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3)) ? v.mat3 : null;
|
|
1533
1503
|
if (!m || m.length < 9) return null;
|
|
1534
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]);
|
|
1535
1505
|
}
|
|
@@ -1539,8 +1509,7 @@ function _parseQuat(v) {
|
|
|
1539
1509
|
const e = v.euler;
|
|
1540
1510
|
if (!Array.isArray(e) || e.length < 3) return null;
|
|
1541
1511
|
const order = (v.order && _EULER_ORDERS.has(v.order)) ? v.order : 'YXZ';
|
|
1542
|
-
const q = [0,0,0,1];
|
|
1543
|
-
const s = [0,0,0,1];
|
|
1512
|
+
const q = [0,0,0,1], s = [0,0,0,1];
|
|
1544
1513
|
for (let i = 0; i < 3; i++) {
|
|
1545
1514
|
const ax = _EULER_AXES[order[i]];
|
|
1546
1515
|
qMul(q, q, qFromAxisAngle(s, ax[0],ax[1],ax[2], e[i]));
|
|
@@ -1575,22 +1544,22 @@ function _parseQuat(v) {
|
|
|
1575
1544
|
}
|
|
1576
1545
|
|
|
1577
1546
|
/**
|
|
1578
|
-
* Parse a PoseTrack keyframe spec.
|
|
1547
|
+
* Parse a PoseTrack keyframe spec into internal form.
|
|
1579
1548
|
*
|
|
1580
1549
|
* Accepted forms:
|
|
1581
|
-
*
|
|
1582
1550
|
* { mat4Model }
|
|
1583
1551
|
* Decompose a column-major mat4 into TRS via mat4ToTransform.
|
|
1584
1552
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1585
1553
|
*
|
|
1586
1554
|
* { pos?, rot?, scl?, tanIn?, tanOut? }
|
|
1587
|
-
* Explicit TRS.
|
|
1555
|
+
* Explicit TRS. pos and scl are vec3; rot accepts any form from _parseQuat.
|
|
1588
1556
|
* All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
|
|
1589
1557
|
* missing rot defaults to identity.
|
|
1590
1558
|
* tanIn/tanOut are optional vec3 tangents for Hermite interpolation.
|
|
1591
1559
|
*
|
|
1592
1560
|
* @param {Object} spec
|
|
1593
|
-
* @returns {{ pos:number[], rot:number[], scl:number[],
|
|
1561
|
+
* @returns {{ pos:number[], rot:number[], scl:number[],
|
|
1562
|
+
* tanIn:number[]|null, tanOut:number[]|null } | null}
|
|
1594
1563
|
*/
|
|
1595
1564
|
function _parseSpec(spec) {
|
|
1596
1565
|
if (!spec || typeof spec !== 'object') return null;
|
|
@@ -1605,6 +1574,7 @@ function _parseSpec(spec) {
|
|
|
1605
1574
|
return kf;
|
|
1606
1575
|
}
|
|
1607
1576
|
|
|
1577
|
+
// { pos?, rot?, scl?, tanIn?, tanOut? } — explicit TRS
|
|
1608
1578
|
const pos = _parseVec3(spec.pos) || [0,0,0];
|
|
1609
1579
|
const rot = _parseQuat(spec.rot) || [0,0,0,1];
|
|
1610
1580
|
const scl = _parseVec3(spec.scl) || [1,1,1];
|
|
@@ -1624,26 +1594,21 @@ function _sameTransform(a, b) {
|
|
|
1624
1594
|
// =========================================================================
|
|
1625
1595
|
|
|
1626
1596
|
/**
|
|
1627
|
-
* Parse a
|
|
1597
|
+
* Parse a CameraTrack keyframe spec into internal form.
|
|
1628
1598
|
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
*
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
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
|
|
1636
1606
|
*
|
|
1637
1607
|
* @param {Object} spec
|
|
1638
|
-
* @returns {
|
|
1639
|
-
* fov:number|null, halfHeight:number|null,
|
|
1640
|
-
* eyeTanIn:number[]|null, eyeTanOut:number[]|null,
|
|
1641
|
-
* centerTanIn:number[]|null, centerTanOut:number[]|null }|null}
|
|
1608
|
+
* @returns {Object|null} Parsed keyframe or null if eye is missing/malformed.
|
|
1642
1609
|
*/
|
|
1643
1610
|
function _parseCameraSpec(spec) {
|
|
1644
1611
|
if (!spec || typeof spec !== 'object') return null;
|
|
1645
|
-
|
|
1646
|
-
// { eye, center?, up? } — explicit lookat
|
|
1647
1612
|
const eye = _parseVec3(spec.eye);
|
|
1648
1613
|
if (!eye) return null;
|
|
1649
1614
|
const center = _parseVec3(spec.center) || [0,0,0];
|
|
@@ -1681,41 +1646,39 @@ class Track {
|
|
|
1681
1646
|
constructor() {
|
|
1682
1647
|
/** @type {Array} Keyframe array — shape depends on subclass. */
|
|
1683
1648
|
this.keyframes = [];
|
|
1684
|
-
/** Whether playback is active. @type {boolean} */
|
|
1649
|
+
/** Whether playback is currently active. @type {boolean} */
|
|
1685
1650
|
this.playing = false;
|
|
1686
1651
|
/** Loop at boundaries. @type {boolean} */
|
|
1687
1652
|
this.loop = false;
|
|
1688
|
-
/** Ping-pong bounce (independent of loop). @type {boolean} */
|
|
1653
|
+
/** Ping-pong bounce at boundaries (independent of loop). @type {boolean} */
|
|
1689
1654
|
this.bounce = false;
|
|
1690
1655
|
/** Frames per segment (≥1). @type {number} */
|
|
1691
1656
|
this.duration = 30;
|
|
1692
1657
|
/** Current segment index. @type {number} */
|
|
1693
1658
|
this.seg = 0;
|
|
1694
|
-
/** Frame offset within segment (can be fractional). @type {number} */
|
|
1659
|
+
/** Frame offset within current segment (can be fractional). @type {number} */
|
|
1695
1660
|
this.f = 0;
|
|
1696
1661
|
|
|
1697
|
-
//
|
|
1662
|
+
// Playback rate (signed: negative = reverse; 0 = frozen).
|
|
1698
1663
|
this._rate = 1;
|
|
1699
|
-
//
|
|
1664
|
+
// Current bounce travel direction (±1). Reset to 1 only on reset().
|
|
1700
1665
|
this._dir = 1;
|
|
1701
|
-
//
|
|
1666
|
+
// Whether a bounce-once has already flipped direction.
|
|
1702
1667
|
this._bounced = false;
|
|
1703
1668
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
/** @type {Function|null} */
|
|
1707
|
-
|
|
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;
|
|
1708
1675
|
|
|
1709
|
-
// Lib-space hooks (
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
// Lib-space event mirrors — set by UI layer (trackUI), never touched by user code
|
|
1713
|
-
/** @type {Function|null} */ this._onPlay = null;
|
|
1714
|
-
/** @type {Function|null} */ this._onEnd = null;
|
|
1715
|
-
/** @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;
|
|
1716
1679
|
}
|
|
1717
1680
|
|
|
1718
|
-
/** Playback rate. Assigning never starts
|
|
1681
|
+
/** Playback rate. Signed: negative reverses, 0 freezes. Assigning never starts or stops playback. @type {number} */
|
|
1719
1682
|
get rate() { return this._rate; }
|
|
1720
1683
|
set rate(v) { this._rate = (_isNum(v)) ? v : 1; }
|
|
1721
1684
|
|
|
@@ -1723,31 +1686,46 @@ class Track {
|
|
|
1723
1686
|
get segments() { return Math.max(0, this.keyframes.length - 1); }
|
|
1724
1687
|
|
|
1725
1688
|
/**
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1728
|
-
|
|
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.
|
|
1729
1712
|
* @returns {Track} this
|
|
1730
1713
|
*/
|
|
1731
1714
|
play(rateOrOpts) {
|
|
1732
1715
|
if (this.keyframes.length === 0) return this;
|
|
1733
|
-
|
|
1734
|
-
// One keyframe: snap cursor, no animation
|
|
1735
|
-
if (this.keyframes.length === 1) {
|
|
1736
|
-
this.seg = 0; this.f = 0;
|
|
1737
|
-
return this;
|
|
1738
|
-
}
|
|
1716
|
+
if (this.keyframes.length === 1) { this.seg = 0; this.f = 0; return this; }
|
|
1739
1717
|
|
|
1740
1718
|
if (typeof rateOrOpts === 'number' && Number.isFinite(rateOrOpts)) {
|
|
1741
1719
|
this._rate = rateOrOpts;
|
|
1742
1720
|
} else if (rateOrOpts && typeof rateOrOpts === 'object') {
|
|
1743
1721
|
const o = rateOrOpts;
|
|
1744
|
-
if (_isNum(o.duration)) this.duration
|
|
1722
|
+
if (_isNum(o.duration)) this.duration = Math.max(1, o.duration | 0);
|
|
1745
1723
|
if ('loop' in o) this.loop = !!o.loop;
|
|
1746
1724
|
if ('bounce' in o) this.bounce = !!o.bounce;
|
|
1747
|
-
if (typeof o.onPlay === 'function') this.onPlay
|
|
1748
|
-
if (typeof o.onEnd === 'function') this.onEnd
|
|
1749
|
-
if (typeof o.onStop === 'function') this.onStop
|
|
1750
|
-
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;
|
|
1751
1729
|
}
|
|
1752
1730
|
|
|
1753
1731
|
const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
|
|
@@ -1768,8 +1746,11 @@ class Track {
|
|
|
1768
1746
|
}
|
|
1769
1747
|
|
|
1770
1748
|
/**
|
|
1771
|
-
* Stop playback.
|
|
1772
|
-
*
|
|
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).
|
|
1773
1754
|
* @returns {Track} this
|
|
1774
1755
|
*/
|
|
1775
1756
|
stop(rewind) {
|
|
@@ -1786,7 +1767,8 @@ class Track {
|
|
|
1786
1767
|
}
|
|
1787
1768
|
|
|
1788
1769
|
/**
|
|
1789
|
-
* 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.
|
|
1790
1772
|
* @returns {Track} this
|
|
1791
1773
|
*/
|
|
1792
1774
|
reset() {
|
|
@@ -1803,9 +1785,10 @@ class Track {
|
|
|
1803
1785
|
}
|
|
1804
1786
|
|
|
1805
1787
|
/**
|
|
1806
|
-
* 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.
|
|
1807
1790
|
* @param {number} index
|
|
1808
|
-
* @returns {boolean}
|
|
1791
|
+
* @returns {boolean} true if removed; false if index was invalid.
|
|
1809
1792
|
*/
|
|
1810
1793
|
remove(index) {
|
|
1811
1794
|
if (!_isNum(index)) return false;
|
|
@@ -1819,9 +1802,15 @@ class Track {
|
|
|
1819
1802
|
}
|
|
1820
1803
|
|
|
1821
1804
|
/**
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
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]
|
|
1825
1814
|
* @returns {Track} this
|
|
1826
1815
|
*/
|
|
1827
1816
|
seek(t, segIndex) {
|
|
@@ -1838,8 +1827,8 @@ class Track {
|
|
|
1838
1827
|
}
|
|
1839
1828
|
|
|
1840
1829
|
/**
|
|
1841
|
-
* Normalised
|
|
1842
|
-
* @returns {number}
|
|
1830
|
+
* Normalised cursor position across the whole track.
|
|
1831
|
+
* @returns {number} ∈ [0, 1]; 0 when there are no segments.
|
|
1843
1832
|
*/
|
|
1844
1833
|
time() {
|
|
1845
1834
|
const nSeg = this.segments;
|
|
@@ -1849,8 +1838,11 @@ class Track {
|
|
|
1849
1838
|
}
|
|
1850
1839
|
|
|
1851
1840
|
/**
|
|
1852
|
-
* Snapshot of transport state.
|
|
1853
|
-
*
|
|
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}}
|
|
1854
1846
|
*/
|
|
1855
1847
|
info() {
|
|
1856
1848
|
return {
|
|
@@ -1868,16 +1860,19 @@ class Track {
|
|
|
1868
1860
|
}
|
|
1869
1861
|
|
|
1870
1862
|
/**
|
|
1871
|
-
* Advance cursor by rate frames.
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
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.
|
|
1874
1871
|
*/
|
|
1875
1872
|
tick() {
|
|
1876
1873
|
if (!this.playing) return false;
|
|
1877
1874
|
const nSeg = this.segments;
|
|
1878
|
-
if (nSeg === 0) {
|
|
1879
|
-
this.playing = false; this._onDeactivate?.(); return false;
|
|
1880
|
-
}
|
|
1875
|
+
if (nSeg === 0) { this.playing = false; this._onDeactivate?.(); return false; }
|
|
1881
1876
|
if (this._rate === 0) return true;
|
|
1882
1877
|
|
|
1883
1878
|
const dur = Math.max(1, this.duration | 0);
|
|
@@ -1885,7 +1880,6 @@ class Track {
|
|
|
1885
1880
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1886
1881
|
const next = s + this._rate * this._dir;
|
|
1887
1882
|
|
|
1888
|
-
// ── loop:true, bounce:true — bounce forever ───────────────────────────
|
|
1889
1883
|
if (this.loop && this.bounce) {
|
|
1890
1884
|
let pos = next, flips = 0;
|
|
1891
1885
|
while (pos < 0 || pos > total) {
|
|
@@ -1897,7 +1891,6 @@ class Track {
|
|
|
1897
1891
|
return true;
|
|
1898
1892
|
}
|
|
1899
1893
|
|
|
1900
|
-
// ── loop:false, bounce:true — bounce once, stop at origin ────────────
|
|
1901
1894
|
if (!this.loop && this.bounce) {
|
|
1902
1895
|
if (next >= total) {
|
|
1903
1896
|
this._setCursorFromScalar(Math.min(total, 2 * total - next));
|
|
@@ -1918,13 +1911,11 @@ class Track {
|
|
|
1918
1911
|
return true;
|
|
1919
1912
|
}
|
|
1920
1913
|
|
|
1921
|
-
// ── loop:true, bounce:false — repeat forever ──────────────────────────
|
|
1922
1914
|
if (this.loop) {
|
|
1923
1915
|
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1924
1916
|
return true;
|
|
1925
1917
|
}
|
|
1926
1918
|
|
|
1927
|
-
// ── loop:false, bounce:false — play once, stop at boundary ───────────
|
|
1928
1919
|
if (next <= 0) {
|
|
1929
1920
|
this._setCursorFromScalar(0);
|
|
1930
1921
|
this.playing = false;
|
|
@@ -1969,7 +1960,7 @@ class Track {
|
|
|
1969
1960
|
*
|
|
1970
1961
|
* tanIn — incoming position tangent at this keyframe (Hermite mode).
|
|
1971
1962
|
* tanOut — outgoing position tangent at this keyframe (Hermite mode).
|
|
1972
|
-
* When only one is supplied, the other mirrors it.
|
|
1963
|
+
* When only one is supplied, the other mirrors it at sample time.
|
|
1973
1964
|
* When neither is supplied, centripetal Catmull-Rom tangents are auto-computed.
|
|
1974
1965
|
*/
|
|
1975
1966
|
class PoseTrack extends Track {
|
|
@@ -1977,8 +1968,8 @@ class PoseTrack extends Track {
|
|
|
1977
1968
|
super();
|
|
1978
1969
|
/**
|
|
1979
1970
|
* Position interpolation mode.
|
|
1980
|
-
* - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom
|
|
1981
|
-
* when none are stored (default)
|
|
1971
|
+
* - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom
|
|
1972
|
+
* tangents when none are stored (default)
|
|
1982
1973
|
* - 'linear' — lerp
|
|
1983
1974
|
* - 'step' — snap to k0; useful for discrete state changes
|
|
1984
1975
|
* @type {'hermite'|'linear'|'step'}
|
|
@@ -1988,26 +1979,20 @@ class PoseTrack extends Track {
|
|
|
1988
1979
|
* Rotation interpolation mode.
|
|
1989
1980
|
* - 'slerp' — constant angular velocity (default)
|
|
1990
1981
|
* - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
|
|
1991
|
-
* - 'step' — snap to k0 quaternion
|
|
1982
|
+
* - 'step' — snap to k0 quaternion
|
|
1992
1983
|
* @type {'slerp'|'nlerp'|'step'}
|
|
1993
1984
|
*/
|
|
1994
1985
|
this.rotInterp = 'slerp';
|
|
1995
|
-
// Scratch for toMatrix() — avoids hot-path allocations
|
|
1996
|
-
this._pos = [0,0,0];
|
|
1997
|
-
this._rot = [0,0,0,1];
|
|
1998
|
-
this._scl = [1,1,1];
|
|
1999
1986
|
}
|
|
2000
1987
|
|
|
2001
1988
|
/**
|
|
2002
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.
|
|
2003
1991
|
* @param {Object|Object[]} spec
|
|
2004
1992
|
* @param {{ deduplicate?: boolean }} [opts]
|
|
2005
1993
|
*/
|
|
2006
1994
|
add(spec, opts) {
|
|
2007
|
-
if (Array.isArray(spec)) {
|
|
2008
|
-
for (const s of spec) this.add(s, opts);
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
1995
|
+
if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
|
|
2011
1996
|
const kf = _parseSpec(spec);
|
|
2012
1997
|
if (!kf) return;
|
|
2013
1998
|
const dedup = !opts || opts.deduplicate !== false;
|
|
@@ -2018,10 +2003,11 @@ class PoseTrack extends Track {
|
|
|
2018
2003
|
}
|
|
2019
2004
|
|
|
2020
2005
|
/**
|
|
2021
|
-
* Replace
|
|
2006
|
+
* Replace the keyframe at `index`, or append at the end if `index` equals
|
|
2007
|
+
* the current keyframe count.
|
|
2022
2008
|
* @param {number} index
|
|
2023
|
-
* @param {Object} spec
|
|
2024
|
-
* @returns {boolean}
|
|
2009
|
+
* @param {Object} spec Any spec form understood by _parseSpec.
|
|
2010
|
+
* @returns {boolean} true on success; false for invalid index or spec.
|
|
2025
2011
|
*/
|
|
2026
2012
|
set(index, spec) {
|
|
2027
2013
|
if (!_isNum(index)) return false;
|
|
@@ -2033,48 +2019,64 @@ class PoseTrack extends Track {
|
|
|
2033
2019
|
}
|
|
2034
2020
|
|
|
2035
2021
|
/**
|
|
2036
|
-
* Sample the position path
|
|
2022
|
+
* Sample the position path. Zero-alloc, no cursor side effects. Honours
|
|
2023
|
+
* posInterp and the stored-tangent → auto-CR fallback chain.
|
|
2037
2024
|
*
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2025
|
+
* Two signatures:
|
|
2026
|
+
* samplePos(out) cursor form — reads current seg/f
|
|
2027
|
+
* samplePos(out, seg, t) explicit (seg, t), continuous on the path
|
|
2041
2028
|
*
|
|
2042
2029
|
* @param {number[]} out 3-element result buffer.
|
|
2043
|
-
* @param {number} seg
|
|
2044
|
-
* @param {number} t
|
|
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.
|
|
2045
2032
|
* @returns {number[]} out
|
|
2046
2033
|
*/
|
|
2047
2034
|
samplePos(out, seg, t) {
|
|
2035
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2048
2036
|
return _samplePathCore(out, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
|
|
2049
2037
|
}
|
|
2050
2038
|
|
|
2051
2039
|
/**
|
|
2052
|
-
* Write the
|
|
2053
|
-
*
|
|
2054
|
-
* only one is stored, else centripetal Catmull-Rom tangents are auto-
|
|
2055
|
-
* computed from neighbours. Endpoint tangents mirror across the missing
|
|
2056
|
-
* side so drawing arrows at boundary keyframes produces a visible vector.
|
|
2040
|
+
* Write the TRS pose as a column-major model mat4. Zero-alloc, no cursor
|
|
2041
|
+
* side effects.
|
|
2057
2042
|
*
|
|
2058
|
-
*
|
|
2059
|
-
*
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
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
|
|
2062
2053
|
*/
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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);
|
|
2066
2058
|
}
|
|
2067
2059
|
|
|
2068
2060
|
/**
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2071
|
-
*
|
|
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
|
|
2072
2070
|
*/
|
|
2073
|
-
|
|
2074
|
-
|
|
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) {
|
|
2075
2078
|
const n = this.keyframes.length;
|
|
2076
2079
|
if (n === 0) return out;
|
|
2077
|
-
|
|
2078
2080
|
if (n === 1) {
|
|
2079
2081
|
const k = this.keyframes[0];
|
|
2080
2082
|
out.pos[0]=k.pos[0]; out.pos[1]=k.pos[1]; out.pos[2]=k.pos[2];
|
|
@@ -2082,18 +2084,16 @@ class PoseTrack extends Track {
|
|
|
2082
2084
|
out.scl[0]=k.scl[0]; out.scl[1]=k.scl[1]; out.scl[2]=k.scl[2];
|
|
2083
2085
|
return out;
|
|
2084
2086
|
}
|
|
2085
|
-
|
|
2086
2087
|
const nSeg = n - 1;
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const
|
|
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];
|
|
2092
2094
|
|
|
2093
|
-
// pos — shared sampler (respects posInterp + tangent fallback chain)
|
|
2094
2095
|
_samplePathCore(out.pos, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
|
|
2095
2096
|
|
|
2096
|
-
// rot — step, slerp, or nlerp
|
|
2097
2097
|
if (this.rotInterp === 'step') {
|
|
2098
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];
|
|
2099
2099
|
} else if (this.rotInterp === 'nlerp') {
|
|
@@ -2102,20 +2102,22 @@ class PoseTrack extends Track {
|
|
|
2102
2102
|
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
2103
2103
|
}
|
|
2104
2104
|
|
|
2105
|
-
// scl — lerp
|
|
2106
2105
|
lerpVec3(out.scl, k0.scl, k1.scl, t);
|
|
2107
|
-
|
|
2108
2106
|
return out;
|
|
2109
2107
|
}
|
|
2110
2108
|
|
|
2111
2109
|
/**
|
|
2112
|
-
* Evaluate
|
|
2113
|
-
* @param {
|
|
2114
|
-
* @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
|
|
2115
2113
|
*/
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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);
|
|
2119
2121
|
}
|
|
2120
2122
|
}
|
|
2121
2123
|
|
|
@@ -2134,21 +2136,18 @@ class PoseTrack extends Track {
|
|
|
2134
2136
|
* fov — vertical fov (radians) for perspective cameras; null for ortho.
|
|
2135
2137
|
* halfHeight — world-unit half-height of ortho frustum; null for perspective.
|
|
2136
2138
|
* Both are optional and nullable. eval() lerps each only when both adjacent
|
|
2137
|
-
* 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.
|
|
2138
2141
|
*
|
|
2139
2142
|
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite
|
|
2140
|
-
* interpolation of the eye and center paths respectively.
|
|
2141
|
-
*
|
|
2143
|
+
* interpolation of the eye and center paths respectively. When absent,
|
|
2144
|
+
* centripetal Catmull-Rom tangents are auto-computed at sample time.
|
|
2142
2145
|
*
|
|
2143
2146
|
* Missing fields default to: center → [0,0,0], up → [0,1,0].
|
|
2144
2147
|
*
|
|
2145
|
-
*
|
|
2146
|
-
*
|
|
2147
|
-
*
|
|
2148
|
-
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
2149
|
-
*
|
|
2150
|
-
* To capture a matrix-based pose, use PoseTrack.add({ mat4Model: mat4Eye })
|
|
2151
|
-
* 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.
|
|
2152
2151
|
*/
|
|
2153
2152
|
class CameraTrack extends Track {
|
|
2154
2153
|
constructor() {
|
|
@@ -2166,15 +2165,13 @@ class CameraTrack extends Track {
|
|
|
2166
2165
|
}
|
|
2167
2166
|
|
|
2168
2167
|
/**
|
|
2169
|
-
* 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.
|
|
2170
2170
|
* @param {Object|Object[]} spec
|
|
2171
2171
|
* @param {{ deduplicate?: boolean }} [opts]
|
|
2172
2172
|
*/
|
|
2173
2173
|
add(spec, opts) {
|
|
2174
|
-
if (Array.isArray(spec)) {
|
|
2175
|
-
for (const s of spec) this.add(s, opts);
|
|
2176
|
-
return;
|
|
2177
|
-
}
|
|
2174
|
+
if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
|
|
2178
2175
|
const kf = _parseCameraSpec(spec);
|
|
2179
2176
|
if (!kf) return;
|
|
2180
2177
|
const dedup = !opts || opts.deduplicate !== false;
|
|
@@ -2185,7 +2182,8 @@ class CameraTrack extends Track {
|
|
|
2185
2182
|
}
|
|
2186
2183
|
|
|
2187
2184
|
/**
|
|
2188
|
-
* Replace
|
|
2185
|
+
* Replace the camera keyframe at `index`, or append at the end if `index`
|
|
2186
|
+
* equals the current keyframe count.
|
|
2189
2187
|
* @param {number} index
|
|
2190
2188
|
* @param {Object} spec
|
|
2191
2189
|
* @returns {boolean}
|
|
@@ -2200,91 +2198,129 @@ class CameraTrack extends Track {
|
|
|
2200
2198
|
}
|
|
2201
2199
|
|
|
2202
2200
|
/**
|
|
2203
|
-
* Sample the eye path
|
|
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
|
|
2206
|
+
*
|
|
2204
2207
|
* @param {number[]} out
|
|
2205
|
-
* @param {number} seg
|
|
2206
|
-
* @param {number} t
|
|
2208
|
+
* @param {number} [seg]
|
|
2209
|
+
* @param {number} [t]
|
|
2207
2210
|
* @returns {number[]} out
|
|
2208
2211
|
*/
|
|
2209
2212
|
sampleEye(out, seg, t) {
|
|
2213
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2210
2214
|
return _samplePathCore(out, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
|
|
2211
2215
|
}
|
|
2212
2216
|
|
|
2213
2217
|
/**
|
|
2214
|
-
* Sample the center path
|
|
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
|
+
*
|
|
2215
2224
|
* @param {number[]} out
|
|
2216
|
-
* @param {number} seg
|
|
2217
|
-
* @param {number} t
|
|
2225
|
+
* @param {number} [seg]
|
|
2226
|
+
* @param {number} [t]
|
|
2218
2227
|
* @returns {number[]} out
|
|
2219
2228
|
*/
|
|
2220
2229
|
sampleCenter(out, seg, t) {
|
|
2230
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2221
2231
|
return _samplePathCore(out, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
|
|
2222
2232
|
}
|
|
2223
2233
|
|
|
2224
2234
|
/**
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
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
|
|
2230
2246
|
*/
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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]);
|
|
2234
2255
|
}
|
|
2235
2256
|
|
|
2236
2257
|
/**
|
|
2237
|
-
* Effective in/out
|
|
2238
|
-
* @param {number[]} outIn
|
|
2239
|
-
* @param {number[]} outOut
|
|
2240
|
-
* @param {number} i
|
|
2241
|
-
* @returns {CameraTrack} this
|
|
2258
|
+
* Effective in/out eye tangents at keyframe `index`.
|
|
2242
2259
|
*/
|
|
2243
|
-
|
|
2244
|
-
_sampleTangentsCore(outIn, outOut, this.keyframes, '
|
|
2260
|
+
eyeTangents(outIn, outOut, index) {
|
|
2261
|
+
_sampleTangentsCore(outIn, outOut, this.keyframes, 'eye', 'eyeTanIn', 'eyeTanOut', index);
|
|
2245
2262
|
return this;
|
|
2246
2263
|
}
|
|
2247
2264
|
|
|
2248
2265
|
/**
|
|
2249
|
-
*
|
|
2250
|
-
*
|
|
2251
|
-
* @param {{ eye:number[], center:number[], up:number[] }} [out]
|
|
2252
|
-
* @returns {{ eye:number[], center:number[], up:number[] }} out
|
|
2266
|
+
* Effective in/out center tangents at keyframe `index`.
|
|
2253
2267
|
*/
|
|
2254
|
-
|
|
2255
|
-
|
|
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) {
|
|
2256
2275
|
const n = this.keyframes.length;
|
|
2257
2276
|
if (n === 0) return out;
|
|
2258
|
-
|
|
2259
2277
|
if (n === 1) {
|
|
2260
2278
|
const k = this.keyframes[0];
|
|
2261
2279
|
out.eye[0]=k.eye[0]; out.eye[1]=k.eye[1]; out.eye[2]=k.eye[2];
|
|
2262
2280
|
out.center[0]=k.center[0]; out.center[1]=k.center[1]; out.center[2]=k.center[2];
|
|
2263
2281
|
out.up[0]=k.up[0]; out.up[1]=k.up[1]; out.up[2]=k.up[2];
|
|
2264
|
-
out.fov = k.fov;
|
|
2265
|
-
out.halfHeight = k.halfHeight;
|
|
2266
2282
|
return out;
|
|
2267
2283
|
}
|
|
2268
|
-
|
|
2269
2284
|
const nSeg = n - 1;
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
_samplePathCore(out.eye,
|
|
2278
|
-
|
|
2279
|
-
// center — shared sampler
|
|
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];
|
|
2291
|
+
|
|
2292
|
+
_samplePathCore(out.eye, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
|
|
2280
2293
|
_samplePathCore(out.center, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
|
|
2281
2294
|
|
|
2282
|
-
// up — nlerp on unit sphere
|
|
2283
2295
|
lerpVec3(out.up, k0.up, k1.up, t);
|
|
2284
|
-
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;
|
|
2285
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);
|
|
2286
2323
|
|
|
2287
|
-
// fov / halfHeight — lerp when both keyframes carry non-null values
|
|
2288
2324
|
out.fov = (k0.fov != null && k1.fov != null)
|
|
2289
2325
|
? k0.fov + t * (k1.fov - k0.fov) : (k0.fov ?? k1.fov ?? null);
|
|
2290
2326
|
out.halfHeight = (k0.halfHeight != null && k1.halfHeight != null)
|