@nakednous/tree 0.0.20 → 0.0.22
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 +70 -12
- package/dist/index.js +378 -298
- 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,66 @@ function mat4ToRotation(out4, m) {
|
|
|
1171
1171
|
* transformToMat4 mat4ToTransform
|
|
1172
1172
|
* Tracks
|
|
1173
1173
|
* PoseTrack — { pos, rot, scl } TRS keyframes
|
|
1174
|
-
* CameraTrack — { eye, center, up
|
|
1174
|
+
* CameraTrack — { eye, center, up, fov?, halfHeight?, near, far }
|
|
1175
|
+
* lookat keyframes
|
|
1175
1176
|
*
|
|
1176
|
-
* ──
|
|
1177
|
+
* ── Public path access (all zero-alloc, no cursor side effects) ──────────
|
|
1178
|
+
* PoseTrack
|
|
1179
|
+
* samplePos (out) | (out, seg, t) vec3
|
|
1180
|
+
* mat4Model (out) | (out, seg, t) mat4 — TRS model
|
|
1181
|
+
* tangents (outIn, outOut, index) vec3 × 2 at keyframe
|
|
1182
|
+
* eval (out?) TRS object at cursor
|
|
1183
|
+
*
|
|
1184
|
+
* CameraTrack
|
|
1185
|
+
* sampleEye (out) | (out, seg, t) vec3
|
|
1186
|
+
* sampleCenter (out) | (out, seg, t) vec3
|
|
1187
|
+
* mat4Eye (out) | (out, seg, t) mat4 — lookat frame
|
|
1188
|
+
* eyeTangents (outIn, outOut, index) vec3 × 2 at keyframe
|
|
1189
|
+
* centerTangents (outIn, outOut, index) vec3 × 2 at keyframe
|
|
1190
|
+
* eval (out?) { eye, center, up,
|
|
1191
|
+
* fov, halfHeight,
|
|
1192
|
+
* near, far }
|
|
1193
|
+
*
|
|
1194
|
+
* Two arities for the continuous family:
|
|
1195
|
+
* (out) cursor form — reads track.seg / track.f. Useful when the
|
|
1196
|
+
* track's own transport is driving the animation.
|
|
1197
|
+
* (out, seg, t) explicit form — continuous (seg, t) coordinate, no cursor
|
|
1198
|
+
* side effects. seg ∈ [0, segments−1] integer, t ∈ [0, 1]
|
|
1199
|
+
* local to that segment. As a convenience, seg === segments
|
|
1200
|
+
* is accepted and rewritten to (segments−1, 1) — so the
|
|
1201
|
+
* idiom sampleX(out, i, 0) uniformly addresses keyframe i
|
|
1202
|
+
* for every i ∈ [0, keyframes.length−1].
|
|
1203
|
+
*
|
|
1204
|
+
* All honour the track's interpolation mode (hermite / linear / step) and
|
|
1205
|
+
* the same stored-tangent → centripetal-CR fallback chain that eval() uses.
|
|
1206
|
+
*
|
|
1207
|
+
* `tangents` / `eyeTangents` / `centerTangents` are keyframe-indexed — a
|
|
1208
|
+
* keyframe's incoming tangent belongs to the previous segment, outgoing to
|
|
1209
|
+
* the next. At boundary keyframes the missing side mirrors the present one
|
|
1210
|
+
* so drawing arrows at endpoints always yields a visible vector.
|
|
1211
|
+
*
|
|
1212
|
+
* Projection matrices are deliberately not exposed as a track method. Each
|
|
1213
|
+
* CameraTrack keyframe stores `fov` (perspective) or `halfHeight` (ortho)
|
|
1214
|
+
* directly on track.keyframes[i] — callers wanting a projection build one
|
|
1215
|
+
* with the free mat4Persp / mat4Ortho constructors. Animated fov in
|
|
1216
|
+
* sketches flows through the bridge's camera-binding via eval().fov and
|
|
1217
|
+
* eval().halfHeight (the bridge calls cam.perspective() / cam.ortho() each
|
|
1218
|
+
* frame).
|
|
1219
|
+
*
|
|
1220
|
+
* ── Class hierarchy ──────────────────────────────────────────────────────
|
|
1177
1221
|
* Track (unexported, never instantiated directly)
|
|
1178
1222
|
* └── PoseTrack (exported)
|
|
1179
1223
|
* └── CameraTrack (exported)
|
|
1180
1224
|
*
|
|
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.
|
|
1225
|
+
* ── Hook architecture ────────────────────────────────────────────────────
|
|
1226
|
+
* Lib-space hooks (underscore prefix — host layer / UI layer):
|
|
1227
|
+
* _onActivate / _onDeactivate — fire on playing false→true / true→false.
|
|
1228
|
+
* _onPlay / _onEnd / _onStop — mirror the user-space hooks.
|
|
1203
1229
|
*
|
|
1204
|
-
* User-space hooks
|
|
1230
|
+
* User-space hooks:
|
|
1205
1231
|
* onPlay : fires in play() on false→true transition.
|
|
1206
1232
|
* 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.
|
|
1233
|
+
* onStop : fires in stop() / reset().
|
|
1209
1234
|
*
|
|
1210
1235
|
* Firing order:
|
|
1211
1236
|
* play() → onPlay → _onPlay → _onActivate
|
|
@@ -1213,18 +1238,14 @@ function mat4ToRotation(out4, m) {
|
|
|
1213
1238
|
* stop() → onStop → _onStop → _onDeactivate
|
|
1214
1239
|
* reset() → onStop → _onStop → _onDeactivate
|
|
1215
1240
|
*
|
|
1216
|
-
* ── Loop modes
|
|
1241
|
+
* ── Loop modes ───────────────────────────────────────────────────────────
|
|
1217
1242
|
* loop:false, bounce:false — play once, stop at end (fires onEnd)
|
|
1218
1243
|
* loop:true, bounce:false — repeat, wrap back to start
|
|
1219
1244
|
* loop:true, bounce:true — bounce forever at boundaries
|
|
1220
1245
|
* loop:false, bounce:true — bounce once: flip at far boundary, stop at origin
|
|
1221
1246
|
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
* ── Playback semantics (rate + _dir) ─────────────────────────────────────────
|
|
1225
|
-
* rate > 0 forward
|
|
1226
|
-
* rate < 0 backward
|
|
1227
|
-
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
1247
|
+
* ── Playback semantics (rate + _dir) ─────────────────────────────────────
|
|
1248
|
+
* rate > 0 forward rate < 0 backward rate === 0 frozen
|
|
1228
1249
|
*
|
|
1229
1250
|
* play() is the sole setter of playing = true.
|
|
1230
1251
|
* stop() is the sole setter of playing = false.
|
|
@@ -1232,11 +1253,9 @@ function mat4ToRotation(out4, m) {
|
|
|
1232
1253
|
*
|
|
1233
1254
|
* _dir (internal, ±1) tracks the current bounce travel direction.
|
|
1234
1255
|
* 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.
|
|
1256
|
+
* _dir is reset to 1 only in reset().
|
|
1238
1257
|
*
|
|
1239
|
-
* ── One-keyframe behaviour
|
|
1258
|
+
* ── One-keyframe behaviour ───────────────────────────────────────────────
|
|
1240
1259
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
1241
1260
|
* setting playing = true and without firing hooks.
|
|
1242
1261
|
*/
|
|
@@ -1253,12 +1272,11 @@ function _dist3(a, b) {
|
|
|
1253
1272
|
|
|
1254
1273
|
/**
|
|
1255
1274
|
* 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
1275
|
* @param {number[]} out 3-element result.
|
|
1258
1276
|
* @param {number[]} p0 Segment start.
|
|
1259
|
-
* @param {number[]} m0 Outgoing tangent at p0
|
|
1277
|
+
* @param {number[]} m0 Outgoing tangent at p0.
|
|
1260
1278
|
* @param {number[]} p1 Segment end.
|
|
1261
|
-
* @param {number[]} m1 Incoming tangent at p1
|
|
1279
|
+
* @param {number[]} m1 Incoming tangent at p1.
|
|
1262
1280
|
* @param {number} t Blend [0, 1].
|
|
1263
1281
|
* @returns {number[]} out
|
|
1264
1282
|
*/
|
|
@@ -1271,7 +1289,7 @@ const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
|
|
|
1271
1289
|
return out;
|
|
1272
1290
|
};
|
|
1273
1291
|
|
|
1274
|
-
// Centripetal CR outgoing tangent at p1 for segment p1→p2
|
|
1292
|
+
// Centripetal CR outgoing tangent at p1 for segment p1→p2.
|
|
1275
1293
|
// Signature: (out, p0, p1, p2). Returns tangent AT p1 (the middle point).
|
|
1276
1294
|
const _crTanOut = (out, p0, p1, p2) => {
|
|
1277
1295
|
const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1;
|
|
@@ -1279,7 +1297,7 @@ const _crTanOut = (out, p0, p1, p2) => {
|
|
|
1279
1297
|
return out;
|
|
1280
1298
|
};
|
|
1281
1299
|
|
|
1282
|
-
// Centripetal CR incoming tangent at p2 for segment p1→p2
|
|
1300
|
+
// Centripetal CR incoming tangent at p2 for segment p1→p2.
|
|
1283
1301
|
// Signature: (out, p1, p2, p3). Returns tangent AT p2 (the middle point).
|
|
1284
1302
|
const _crTanIn = (out, p1, p2, p3) => {
|
|
1285
1303
|
const dt1=Math.pow(_dist3(p1,p2),0.5)||1, dt2=Math.pow(_dist3(p2,p3),0.5)||1;
|
|
@@ -1287,16 +1305,13 @@ const _crTanIn = (out, p1, p2, p3) => {
|
|
|
1287
1305
|
return out;
|
|
1288
1306
|
};
|
|
1289
1307
|
|
|
1290
|
-
// Module-level scratch — shared across
|
|
1308
|
+
// Module-level scratch — shared across track instances (non-reentrant hot path).
|
|
1291
1309
|
const _m0=[0,0,0], _m1=[0,0,0];
|
|
1310
|
+
const _trsScratch = { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
|
|
1311
|
+
const _eyeScratch = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] };
|
|
1292
1312
|
|
|
1293
1313
|
/**
|
|
1294
1314
|
* 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
1315
|
*/
|
|
1301
1316
|
const lerpVec3 = (out, a, b, t) => {
|
|
1302
1317
|
out[0]=a[0]+t*(b[0]-a[0]);
|
|
@@ -1308,27 +1323,8 @@ const lerpVec3 = (out, a, b, t) => {
|
|
|
1308
1323
|
// =========================================================================
|
|
1309
1324
|
// S2b Path samplers — shared core
|
|
1310
1325
|
// =========================================================================
|
|
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
1326
|
|
|
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
|
-
*/
|
|
1327
|
+
/** @private — sample interpolated vec3 path at (seg, t) into out. */
|
|
1332
1328
|
function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t) {
|
|
1333
1329
|
const n = kfs.length;
|
|
1334
1330
|
if (n === 0) { out[0]=0; out[1]=0; out[2]=0; return out; }
|
|
@@ -1338,7 +1334,9 @@ function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t)
|
|
|
1338
1334
|
return out;
|
|
1339
1335
|
}
|
|
1340
1336
|
const nSeg = n - 1;
|
|
1341
|
-
seg =
|
|
1337
|
+
seg = seg | 0;
|
|
1338
|
+
if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
|
|
1339
|
+
else if (seg < 0) { seg = 0; t = 0; }
|
|
1342
1340
|
t = _clamp01(t);
|
|
1343
1341
|
const k0 = kfs[seg];
|
|
1344
1342
|
const k1 = kfs[seg + 1];
|
|
@@ -1352,7 +1350,6 @@ function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t)
|
|
|
1352
1350
|
return lerpVec3(out, k0[field], k1[field], t);
|
|
1353
1351
|
}
|
|
1354
1352
|
|
|
1355
|
-
// hermite (default)
|
|
1356
1353
|
const p0 = seg > 0 ? kfs[seg - 1][field] : k0[field];
|
|
1357
1354
|
const p3 = seg + 2 < n ? kfs[seg + 2][field] : k1[field];
|
|
1358
1355
|
const m0 = k0[tanOutName] != null ? k0[tanOutName]
|
|
@@ -1364,19 +1361,7 @@ function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t)
|
|
|
1364
1361
|
return hermiteVec3(out, k0[field], m0, k1[field], m1, t);
|
|
1365
1362
|
}
|
|
1366
1363
|
|
|
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
|
-
*/
|
|
1364
|
+
/** @private — write effective in/out tangents at keyframe i. */
|
|
1380
1365
|
function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i) {
|
|
1381
1366
|
const n = kfs.length;
|
|
1382
1367
|
if (n === 0) {
|
|
@@ -1388,7 +1373,6 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1388
1373
|
const hasTI = ki[tanInName] != null;
|
|
1389
1374
|
const hasTO = ki[tanOutName] != null;
|
|
1390
1375
|
|
|
1391
|
-
// ── outgoing tangent at keyframe i (for segment i → i+1) ──────────────
|
|
1392
1376
|
if (hasTO) {
|
|
1393
1377
|
outOut[0]=ki[tanOutName][0]; outOut[1]=ki[tanOutName][1]; outOut[2]=ki[tanOutName][2];
|
|
1394
1378
|
} else if (hasTI) {
|
|
@@ -1398,10 +1382,9 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1398
1382
|
const p0 = i > 0 ? kfs[i - 1][field] : ki[field];
|
|
1399
1383
|
_crTanOut(outOut, p0, ki[field], k1[field]);
|
|
1400
1384
|
} else {
|
|
1401
|
-
outOut[0]=0; outOut[1]=0; outOut[2]=0;
|
|
1385
|
+
outOut[0]=0; outOut[1]=0; outOut[2]=0;
|
|
1402
1386
|
}
|
|
1403
1387
|
|
|
1404
|
-
// ── incoming tangent at keyframe i (for segment i-1 → i) ──────────────
|
|
1405
1388
|
if (hasTI) {
|
|
1406
1389
|
outIn[0]=ki[tanInName][0]; outIn[1]=ki[tanInName][1]; outIn[2]=ki[tanInName][2];
|
|
1407
1390
|
} else if (hasTO) {
|
|
@@ -1414,7 +1397,6 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1414
1397
|
outIn[0]=outOut[0]; outIn[1]=outOut[1]; outIn[2]=outOut[2];
|
|
1415
1398
|
}
|
|
1416
1399
|
|
|
1417
|
-
// Boundary mirror the other way: last keyframe with no stored tangents.
|
|
1418
1400
|
if (i === n - 1 && !hasTO && !hasTI) {
|
|
1419
1401
|
outOut[0]=outIn[0]; outOut[1]=outIn[1]; outOut[2]=outIn[2];
|
|
1420
1402
|
}
|
|
@@ -1426,9 +1408,6 @@ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i
|
|
|
1426
1408
|
|
|
1427
1409
|
/**
|
|
1428
1410
|
* 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
1411
|
*/
|
|
1433
1412
|
const transformToMat4 = (out, xform) => {
|
|
1434
1413
|
qToMat4(out, xform.rot);
|
|
@@ -1442,10 +1421,6 @@ const transformToMat4 = (out, xform) => {
|
|
|
1442
1421
|
|
|
1443
1422
|
/**
|
|
1444
1423
|
* 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
1424
|
*/
|
|
1450
1425
|
const mat4ToTransform = (out, m) => {
|
|
1451
1426
|
out.pos[0]=m[12]; out.pos[1]=m[13]; out.pos[2]=m[14];
|
|
@@ -1476,7 +1451,6 @@ function _parseVec3(v) {
|
|
|
1476
1451
|
return null;
|
|
1477
1452
|
}
|
|
1478
1453
|
|
|
1479
|
-
// Euler: unit axis vectors and the six valid intrinsic orderings.
|
|
1480
1454
|
const _EULER_AXES = { X:[1,0,0], Y:[0,1,0], Z:[0,0,1] };
|
|
1481
1455
|
const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
1482
1456
|
|
|
@@ -1484,14 +1458,13 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1484
1458
|
* Parse any rotation representation into a unit quaternion [x,y,z,w].
|
|
1485
1459
|
*
|
|
1486
1460
|
* Accepted forms:
|
|
1487
|
-
*
|
|
1488
1461
|
* [x,y,z,w] — raw quaternion array
|
|
1489
|
-
* { axis:[x,y,z], angle } — axis-angle
|
|
1462
|
+
* { axis:[x,y,z], angle } — axis-angle (angle in radians)
|
|
1490
1463
|
* { dir:[x,y,z], up?:[x,y,z] } — forward direction (−Z) with optional up
|
|
1491
1464
|
* { mat4Eye: mat4 } — rotation block of an eye matrix
|
|
1492
1465
|
* { mat3: mat3 } — column-major 3×3 rotation matrix
|
|
1493
1466
|
* { euler:[rx,ry,rz], order? } — intrinsic Euler (default order: YXZ)
|
|
1494
|
-
* { from:[x,y,z], to:[x,y,z] } — shortest-arc rotation
|
|
1467
|
+
* { from:[x,y,z], to:[x,y,z] } — shortest-arc rotation between two vectors
|
|
1495
1468
|
*
|
|
1496
1469
|
* @param {*} v
|
|
1497
1470
|
* @returns {number[]|null} [x,y,z,w] or null if unparseable.
|
|
@@ -1499,7 +1472,7 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1499
1472
|
function _parseQuat(v) {
|
|
1500
1473
|
if (!v) return null;
|
|
1501
1474
|
|
|
1502
|
-
//
|
|
1475
|
+
// [x,y,z,w]
|
|
1503
1476
|
if (Array.isArray(v) && v.length === 4) return [v[0],v[1],v[2],v[3]];
|
|
1504
1477
|
if (ArrayBuffer.isView(v) && v.length >= 4) return [v[0],v[1],v[2],v[3]];
|
|
1505
1478
|
|
|
@@ -1528,8 +1501,7 @@ function _parseQuat(v) {
|
|
|
1528
1501
|
|
|
1529
1502
|
// { mat3 }
|
|
1530
1503
|
if (v.mat3 != null) {
|
|
1531
|
-
const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3))
|
|
1532
|
-
? v.mat3 : null;
|
|
1504
|
+
const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3)) ? v.mat3 : null;
|
|
1533
1505
|
if (!m || m.length < 9) return null;
|
|
1534
1506
|
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
1507
|
}
|
|
@@ -1539,8 +1511,7 @@ function _parseQuat(v) {
|
|
|
1539
1511
|
const e = v.euler;
|
|
1540
1512
|
if (!Array.isArray(e) || e.length < 3) return null;
|
|
1541
1513
|
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];
|
|
1514
|
+
const q = [0,0,0,1], s = [0,0,0,1];
|
|
1544
1515
|
for (let i = 0; i < 3; i++) {
|
|
1545
1516
|
const ax = _EULER_AXES[order[i]];
|
|
1546
1517
|
qMul(q, q, qFromAxisAngle(s, ax[0],ax[1],ax[2], e[i]));
|
|
@@ -1575,22 +1546,22 @@ function _parseQuat(v) {
|
|
|
1575
1546
|
}
|
|
1576
1547
|
|
|
1577
1548
|
/**
|
|
1578
|
-
* Parse a PoseTrack keyframe spec.
|
|
1549
|
+
* Parse a PoseTrack keyframe spec into internal form.
|
|
1579
1550
|
*
|
|
1580
1551
|
* Accepted forms:
|
|
1581
|
-
*
|
|
1582
1552
|
* { mat4Model }
|
|
1583
1553
|
* Decompose a column-major mat4 into TRS via mat4ToTransform.
|
|
1584
1554
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1585
1555
|
*
|
|
1586
1556
|
* { pos?, rot?, scl?, tanIn?, tanOut? }
|
|
1587
|
-
* Explicit TRS.
|
|
1557
|
+
* Explicit TRS. pos and scl are vec3; rot accepts any form from _parseQuat.
|
|
1588
1558
|
* All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
|
|
1589
1559
|
* missing rot defaults to identity.
|
|
1590
1560
|
* tanIn/tanOut are optional vec3 tangents for Hermite interpolation.
|
|
1591
1561
|
*
|
|
1592
1562
|
* @param {Object} spec
|
|
1593
|
-
* @returns {{ pos:number[], rot:number[], scl:number[],
|
|
1563
|
+
* @returns {{ pos:number[], rot:number[], scl:number[],
|
|
1564
|
+
* tanIn:number[]|null, tanOut:number[]|null } | null}
|
|
1594
1565
|
*/
|
|
1595
1566
|
function _parseSpec(spec) {
|
|
1596
1567
|
if (!spec || typeof spec !== 'object') return null;
|
|
@@ -1605,6 +1576,7 @@ function _parseSpec(spec) {
|
|
|
1605
1576
|
return kf;
|
|
1606
1577
|
}
|
|
1607
1578
|
|
|
1579
|
+
// { pos?, rot?, scl?, tanIn?, tanOut? } — explicit TRS
|
|
1608
1580
|
const pos = _parseVec3(spec.pos) || [0,0,0];
|
|
1609
1581
|
const rot = _parseQuat(spec.rot) || [0,0,0,1];
|
|
1610
1582
|
const scl = _parseVec3(spec.scl) || [1,1,1];
|
|
@@ -1623,27 +1595,36 @@ function _sameTransform(a, b) {
|
|
|
1623
1595
|
// S4b Spec parser — CameraTrack
|
|
1624
1596
|
// =========================================================================
|
|
1625
1597
|
|
|
1598
|
+
// Lens defaults used when a spec omits near/far. Match the three.js /
|
|
1599
|
+
// Bevy conventions and are safe for typical p5 v2 scene scales.
|
|
1600
|
+
const _DEFAULT_NEAR = 0.1;
|
|
1601
|
+
const _DEFAULT_FAR = 1000;
|
|
1602
|
+
|
|
1626
1603
|
/**
|
|
1627
|
-
* Parse a
|
|
1604
|
+
* Parse a CameraTrack keyframe spec into internal form.
|
|
1628
1605
|
*
|
|
1629
|
-
*
|
|
1606
|
+
* Required: eye (vec3). Everything else is optional.
|
|
1607
|
+
* center defaults to [0, 0, 0]
|
|
1608
|
+
* up defaults to [0, 1, 0] and is normalised
|
|
1609
|
+
* fov vertical fov (radians), perspective only — null if absent
|
|
1610
|
+
* halfHeight world-unit half-height of ortho frustum — null if absent
|
|
1611
|
+
* near near clip distance (positive) — defaults to 0.1
|
|
1612
|
+
* far far clip distance (positive) — defaults to 1000
|
|
1613
|
+
* eyeTanIn/Out optional Hermite tangents for the eye path
|
|
1614
|
+
* centerTanIn/Out optional Hermite tangents for the center path
|
|
1630
1615
|
*
|
|
1631
|
-
*
|
|
1632
|
-
*
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1616
|
+
* fov and halfHeight are mutually exclusive (perspective xor ortho) and
|
|
1617
|
+
* therefore left nullable; eval() lerps each only when both adjacent
|
|
1618
|
+
* keyframes carry a non-null value, passing null through otherwise.
|
|
1619
|
+
*
|
|
1620
|
+
* near and far are always meaningful regardless of projection type, so
|
|
1621
|
+
* they receive real defaults and are linearly interpolated unconditionally.
|
|
1636
1622
|
*
|
|
1637
1623
|
* @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}
|
|
1624
|
+
* @returns {Object|null} Parsed keyframe or null if eye is missing/malformed.
|
|
1642
1625
|
*/
|
|
1643
1626
|
function _parseCameraSpec(spec) {
|
|
1644
1627
|
if (!spec || typeof spec !== 'object') return null;
|
|
1645
|
-
|
|
1646
|
-
// { eye, center?, up? } — explicit lookat
|
|
1647
1628
|
const eye = _parseVec3(spec.eye);
|
|
1648
1629
|
if (!eye) return null;
|
|
1649
1630
|
const center = _parseVec3(spec.center) || [0,0,0];
|
|
@@ -1655,6 +1636,8 @@ function _parseCameraSpec(spec) {
|
|
|
1655
1636
|
up: [up[0]/ul, up[1]/ul, up[2]/ul],
|
|
1656
1637
|
fov: typeof spec.fov === 'number' ? spec.fov : null,
|
|
1657
1638
|
halfHeight: typeof spec.halfHeight === 'number' ? spec.halfHeight : null,
|
|
1639
|
+
near: typeof spec.near === 'number' ? spec.near : _DEFAULT_NEAR,
|
|
1640
|
+
far: typeof spec.far === 'number' ? spec.far : _DEFAULT_FAR,
|
|
1658
1641
|
eyeTanIn: _parseVec3(spec.eyeTanIn) || null,
|
|
1659
1642
|
eyeTanOut: _parseVec3(spec.eyeTanOut) || null,
|
|
1660
1643
|
centerTanIn: _parseVec3(spec.centerTanIn) || null,
|
|
@@ -1670,6 +1653,8 @@ function _sameCameraKeyframe(a, b) {
|
|
|
1670
1653
|
}
|
|
1671
1654
|
if (a.fov !== b.fov) return false;
|
|
1672
1655
|
if (a.halfHeight !== b.halfHeight) return false;
|
|
1656
|
+
if (a.near !== b.near) return false;
|
|
1657
|
+
if (a.far !== b.far) return false;
|
|
1673
1658
|
return true;
|
|
1674
1659
|
}
|
|
1675
1660
|
|
|
@@ -1681,41 +1666,39 @@ class Track {
|
|
|
1681
1666
|
constructor() {
|
|
1682
1667
|
/** @type {Array} Keyframe array — shape depends on subclass. */
|
|
1683
1668
|
this.keyframes = [];
|
|
1684
|
-
/** Whether playback is active. @type {boolean} */
|
|
1669
|
+
/** Whether playback is currently active. @type {boolean} */
|
|
1685
1670
|
this.playing = false;
|
|
1686
1671
|
/** Loop at boundaries. @type {boolean} */
|
|
1687
1672
|
this.loop = false;
|
|
1688
|
-
/** Ping-pong bounce (independent of loop). @type {boolean} */
|
|
1673
|
+
/** Ping-pong bounce at boundaries (independent of loop). @type {boolean} */
|
|
1689
1674
|
this.bounce = false;
|
|
1690
1675
|
/** Frames per segment (≥1). @type {number} */
|
|
1691
1676
|
this.duration = 30;
|
|
1692
1677
|
/** Current segment index. @type {number} */
|
|
1693
1678
|
this.seg = 0;
|
|
1694
|
-
/** Frame offset within segment (can be fractional). @type {number} */
|
|
1679
|
+
/** Frame offset within current segment (can be fractional). @type {number} */
|
|
1695
1680
|
this.f = 0;
|
|
1696
1681
|
|
|
1697
|
-
//
|
|
1682
|
+
// Playback rate (signed: negative = reverse; 0 = frozen).
|
|
1698
1683
|
this._rate = 1;
|
|
1699
|
-
//
|
|
1684
|
+
// Current bounce travel direction (±1). Reset to 1 only on reset().
|
|
1700
1685
|
this._dir = 1;
|
|
1701
|
-
//
|
|
1686
|
+
// Whether a bounce-once has already flipped direction.
|
|
1702
1687
|
this._bounced = false;
|
|
1703
1688
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
/** @type {Function|null} */
|
|
1707
|
-
|
|
1689
|
+
/** User hook: fires on play() false→true transition. @type {Function|null} */
|
|
1690
|
+
this.onPlay = null;
|
|
1691
|
+
/** User hook: fires on natural boundary in once mode. @type {Function|null} */
|
|
1692
|
+
this.onEnd = null;
|
|
1693
|
+
/** User hook: fires on stop() / reset() / end-of-bounce-once. @type {Function|null} */
|
|
1694
|
+
this.onStop = null;
|
|
1708
1695
|
|
|
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;
|
|
1696
|
+
// Lib-space hooks (underscore prefix — host layer / UI layer only).
|
|
1697
|
+
this._onActivate = null; this._onDeactivate = null;
|
|
1698
|
+
this._onPlay = null; this._onEnd = null; this._onStop = null;
|
|
1716
1699
|
}
|
|
1717
1700
|
|
|
1718
|
-
/** Playback rate. Assigning never starts
|
|
1701
|
+
/** Playback rate. Signed: negative reverses, 0 freezes. Assigning never starts or stops playback. @type {number} */
|
|
1719
1702
|
get rate() { return this._rate; }
|
|
1720
1703
|
set rate(v) { this._rate = (_isNum(v)) ? v : 1; }
|
|
1721
1704
|
|
|
@@ -1723,31 +1706,46 @@ class Track {
|
|
|
1723
1706
|
get segments() { return Math.max(0, this.keyframes.length - 1); }
|
|
1724
1707
|
|
|
1725
1708
|
/**
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1728
|
-
|
|
1709
|
+
* @private — resolve cursor (seg, f) into continuous (seg, t).
|
|
1710
|
+
* Shared backing of every cursor-form sampler / matrix method.
|
|
1711
|
+
*/
|
|
1712
|
+
_cursorSegT() {
|
|
1713
|
+
const nSeg = this.segments;
|
|
1714
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1715
|
+
const seg = nSeg > 0 ? _clampS(this.seg, 0, nSeg - 1) : 0;
|
|
1716
|
+
const t = _clamp01(this.f / dur);
|
|
1717
|
+
return [seg, t];
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* Start or update playback. Sole setter of `playing = true`.
|
|
1722
|
+
*
|
|
1723
|
+
* Fires `onPlay → _onPlay → _onActivate` only on a false→true transition.
|
|
1724
|
+
* Zero keyframes: no-op. Exactly one keyframe: snaps eval() to it but does
|
|
1725
|
+
* not set `playing` and fires no hooks.
|
|
1726
|
+
*
|
|
1727
|
+
* @param {number|{duration?:number,loop?:boolean,bounce?:boolean,
|
|
1728
|
+
* rate?:number,onPlay?:Function,onEnd?:Function,
|
|
1729
|
+
* onStop?:Function}} [rateOrOpts]
|
|
1730
|
+
* A bare number is taken as `rate`; an object configures multiple
|
|
1731
|
+
* fields in one call.
|
|
1729
1732
|
* @returns {Track} this
|
|
1730
1733
|
*/
|
|
1731
1734
|
play(rateOrOpts) {
|
|
1732
1735
|
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
|
-
}
|
|
1736
|
+
if (this.keyframes.length === 1) { this.seg = 0; this.f = 0; return this; }
|
|
1739
1737
|
|
|
1740
1738
|
if (typeof rateOrOpts === 'number' && Number.isFinite(rateOrOpts)) {
|
|
1741
1739
|
this._rate = rateOrOpts;
|
|
1742
1740
|
} else if (rateOrOpts && typeof rateOrOpts === 'object') {
|
|
1743
1741
|
const o = rateOrOpts;
|
|
1744
|
-
if (_isNum(o.duration)) this.duration
|
|
1742
|
+
if (_isNum(o.duration)) this.duration = Math.max(1, o.duration | 0);
|
|
1745
1743
|
if ('loop' in o) this.loop = !!o.loop;
|
|
1746
1744
|
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
|
|
1745
|
+
if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
|
|
1746
|
+
if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
|
|
1747
|
+
if (typeof o.onStop === 'function') this.onStop = o.onStop;
|
|
1748
|
+
if (_isNum(o.rate)) this._rate = o.rate;
|
|
1751
1749
|
}
|
|
1752
1750
|
|
|
1753
1751
|
const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
|
|
@@ -1768,8 +1766,11 @@ class Track {
|
|
|
1768
1766
|
}
|
|
1769
1767
|
|
|
1770
1768
|
/**
|
|
1771
|
-
* Stop playback.
|
|
1772
|
-
*
|
|
1769
|
+
* Stop playback. Sole setter of `playing = false`. Fires
|
|
1770
|
+
* `onStop → _onStop → _onDeactivate` on a true→false transition.
|
|
1771
|
+
*
|
|
1772
|
+
* @param {boolean} [rewind] If true, seek to the origin end after stopping
|
|
1773
|
+
* (0 when playing forward, 1 when playing backward).
|
|
1773
1774
|
* @returns {Track} this
|
|
1774
1775
|
*/
|
|
1775
1776
|
stop(rewind) {
|
|
@@ -1786,7 +1787,8 @@ class Track {
|
|
|
1786
1787
|
}
|
|
1787
1788
|
|
|
1788
1789
|
/**
|
|
1789
|
-
* Clear all keyframes and stop.
|
|
1790
|
+
* Clear all keyframes and stop. Fires stop-side hooks if it was playing.
|
|
1791
|
+
* Unlike stop(), this also resets `_dir` to +1.
|
|
1790
1792
|
* @returns {Track} this
|
|
1791
1793
|
*/
|
|
1792
1794
|
reset() {
|
|
@@ -1803,9 +1805,10 @@ class Track {
|
|
|
1803
1805
|
}
|
|
1804
1806
|
|
|
1805
1807
|
/**
|
|
1806
|
-
* Remove the keyframe at index
|
|
1808
|
+
* Remove the keyframe at `index`. Adjusts the cursor if the removal shrinks
|
|
1809
|
+
* the track below the current segment.
|
|
1807
1810
|
* @param {number} index
|
|
1808
|
-
* @returns {boolean}
|
|
1811
|
+
* @returns {boolean} true if removed; false if index was invalid.
|
|
1809
1812
|
*/
|
|
1810
1813
|
remove(index) {
|
|
1811
1814
|
if (!_isNum(index)) return false;
|
|
@@ -1819,9 +1822,15 @@ class Track {
|
|
|
1819
1822
|
}
|
|
1820
1823
|
|
|
1821
1824
|
/**
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
+
* Move the cursor.
|
|
1826
|
+
* seek(t) Scrub to normalised position t ∈ [0, 1] across the
|
|
1827
|
+
* whole track.
|
|
1828
|
+
* seek(t, segIndex) Position within a specific segment — t is local to
|
|
1829
|
+
* that segment.
|
|
1830
|
+
* Does not affect `playing`.
|
|
1831
|
+
*
|
|
1832
|
+
* @param {number} t
|
|
1833
|
+
* @param {number} [segIndex]
|
|
1825
1834
|
* @returns {Track} this
|
|
1826
1835
|
*/
|
|
1827
1836
|
seek(t, segIndex) {
|
|
@@ -1838,8 +1847,8 @@ class Track {
|
|
|
1838
1847
|
}
|
|
1839
1848
|
|
|
1840
1849
|
/**
|
|
1841
|
-
* Normalised
|
|
1842
|
-
* @returns {number}
|
|
1850
|
+
* Normalised cursor position across the whole track.
|
|
1851
|
+
* @returns {number} ∈ [0, 1]; 0 when there are no segments.
|
|
1843
1852
|
*/
|
|
1844
1853
|
time() {
|
|
1845
1854
|
const nSeg = this.segments;
|
|
@@ -1849,8 +1858,11 @@ class Track {
|
|
|
1849
1858
|
}
|
|
1850
1859
|
|
|
1851
1860
|
/**
|
|
1852
|
-
* Snapshot of transport state.
|
|
1853
|
-
*
|
|
1861
|
+
* Snapshot of transport state. Allocates a new object per call — intended
|
|
1862
|
+
* for UI / debugging, not hot loops.
|
|
1863
|
+
* @returns {{keyframes:number, segments:number, seg:number, f:number,
|
|
1864
|
+
* playing:boolean, loop:boolean, bounce:boolean, rate:number,
|
|
1865
|
+
* duration:number, time:number}}
|
|
1854
1866
|
*/
|
|
1855
1867
|
info() {
|
|
1856
1868
|
return {
|
|
@@ -1868,16 +1880,19 @@ class Track {
|
|
|
1868
1880
|
}
|
|
1869
1881
|
|
|
1870
1882
|
/**
|
|
1871
|
-
* Advance cursor by rate frames.
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
1883
|
+
* Advance the cursor by `rate * _dir` in frames. Handles loop / bounce /
|
|
1884
|
+
* once modes per the table in the module header. Fires `onEnd → _onEnd →
|
|
1885
|
+
* _onDeactivate` at a natural boundary in once mode.
|
|
1886
|
+
*
|
|
1887
|
+
* Intended to be called once per animation frame by the bridge / UI layer.
|
|
1888
|
+
* rate === 0 freezes the cursor but keeps `playing` unchanged.
|
|
1889
|
+
*
|
|
1890
|
+
* @returns {boolean} Current `playing` state after advancing.
|
|
1874
1891
|
*/
|
|
1875
1892
|
tick() {
|
|
1876
1893
|
if (!this.playing) return false;
|
|
1877
1894
|
const nSeg = this.segments;
|
|
1878
|
-
if (nSeg === 0) {
|
|
1879
|
-
this.playing = false; this._onDeactivate?.(); return false;
|
|
1880
|
-
}
|
|
1895
|
+
if (nSeg === 0) { this.playing = false; this._onDeactivate?.(); return false; }
|
|
1881
1896
|
if (this._rate === 0) return true;
|
|
1882
1897
|
|
|
1883
1898
|
const dur = Math.max(1, this.duration | 0);
|
|
@@ -1885,7 +1900,6 @@ class Track {
|
|
|
1885
1900
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1886
1901
|
const next = s + this._rate * this._dir;
|
|
1887
1902
|
|
|
1888
|
-
// ── loop:true, bounce:true — bounce forever ───────────────────────────
|
|
1889
1903
|
if (this.loop && this.bounce) {
|
|
1890
1904
|
let pos = next, flips = 0;
|
|
1891
1905
|
while (pos < 0 || pos > total) {
|
|
@@ -1897,7 +1911,6 @@ class Track {
|
|
|
1897
1911
|
return true;
|
|
1898
1912
|
}
|
|
1899
1913
|
|
|
1900
|
-
// ── loop:false, bounce:true — bounce once, stop at origin ────────────
|
|
1901
1914
|
if (!this.loop && this.bounce) {
|
|
1902
1915
|
if (next >= total) {
|
|
1903
1916
|
this._setCursorFromScalar(Math.min(total, 2 * total - next));
|
|
@@ -1918,13 +1931,11 @@ class Track {
|
|
|
1918
1931
|
return true;
|
|
1919
1932
|
}
|
|
1920
1933
|
|
|
1921
|
-
// ── loop:true, bounce:false — repeat forever ──────────────────────────
|
|
1922
1934
|
if (this.loop) {
|
|
1923
1935
|
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1924
1936
|
return true;
|
|
1925
1937
|
}
|
|
1926
1938
|
|
|
1927
|
-
// ── loop:false, bounce:false — play once, stop at boundary ───────────
|
|
1928
1939
|
if (next <= 0) {
|
|
1929
1940
|
this._setCursorFromScalar(0);
|
|
1930
1941
|
this.playing = false;
|
|
@@ -1969,7 +1980,7 @@ class Track {
|
|
|
1969
1980
|
*
|
|
1970
1981
|
* tanIn — incoming position tangent at this keyframe (Hermite mode).
|
|
1971
1982
|
* tanOut — outgoing position tangent at this keyframe (Hermite mode).
|
|
1972
|
-
* When only one is supplied, the other mirrors it.
|
|
1983
|
+
* When only one is supplied, the other mirrors it at sample time.
|
|
1973
1984
|
* When neither is supplied, centripetal Catmull-Rom tangents are auto-computed.
|
|
1974
1985
|
*/
|
|
1975
1986
|
class PoseTrack extends Track {
|
|
@@ -1977,8 +1988,8 @@ class PoseTrack extends Track {
|
|
|
1977
1988
|
super();
|
|
1978
1989
|
/**
|
|
1979
1990
|
* Position interpolation mode.
|
|
1980
|
-
* - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom
|
|
1981
|
-
* when none are stored (default)
|
|
1991
|
+
* - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom
|
|
1992
|
+
* tangents when none are stored (default)
|
|
1982
1993
|
* - 'linear' — lerp
|
|
1983
1994
|
* - 'step' — snap to k0; useful for discrete state changes
|
|
1984
1995
|
* @type {'hermite'|'linear'|'step'}
|
|
@@ -1988,26 +1999,20 @@ class PoseTrack extends Track {
|
|
|
1988
1999
|
* Rotation interpolation mode.
|
|
1989
2000
|
* - 'slerp' — constant angular velocity (default)
|
|
1990
2001
|
* - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
|
|
1991
|
-
* - 'step' — snap to k0 quaternion
|
|
2002
|
+
* - 'step' — snap to k0 quaternion
|
|
1992
2003
|
* @type {'slerp'|'nlerp'|'step'}
|
|
1993
2004
|
*/
|
|
1994
2005
|
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
2006
|
}
|
|
2000
2007
|
|
|
2001
2008
|
/**
|
|
2002
2009
|
* Append one or more keyframes. Adjacent duplicates are skipped by default.
|
|
2010
|
+
* Accepts any spec form understood by _parseSpec, or an array of them.
|
|
2003
2011
|
* @param {Object|Object[]} spec
|
|
2004
2012
|
* @param {{ deduplicate?: boolean }} [opts]
|
|
2005
2013
|
*/
|
|
2006
2014
|
add(spec, opts) {
|
|
2007
|
-
if (Array.isArray(spec)) {
|
|
2008
|
-
for (const s of spec) this.add(s, opts);
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2015
|
+
if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
|
|
2011
2016
|
const kf = _parseSpec(spec);
|
|
2012
2017
|
if (!kf) return;
|
|
2013
2018
|
const dedup = !opts || opts.deduplicate !== false;
|
|
@@ -2018,10 +2023,11 @@ class PoseTrack extends Track {
|
|
|
2018
2023
|
}
|
|
2019
2024
|
|
|
2020
2025
|
/**
|
|
2021
|
-
* Replace
|
|
2026
|
+
* Replace the keyframe at `index`, or append at the end if `index` equals
|
|
2027
|
+
* the current keyframe count.
|
|
2022
2028
|
* @param {number} index
|
|
2023
|
-
* @param {Object} spec
|
|
2024
|
-
* @returns {boolean}
|
|
2029
|
+
* @param {Object} spec Any spec form understood by _parseSpec.
|
|
2030
|
+
* @returns {boolean} true on success; false for invalid index or spec.
|
|
2025
2031
|
*/
|
|
2026
2032
|
set(index, spec) {
|
|
2027
2033
|
if (!_isNum(index)) return false;
|
|
@@ -2033,48 +2039,64 @@ class PoseTrack extends Track {
|
|
|
2033
2039
|
}
|
|
2034
2040
|
|
|
2035
2041
|
/**
|
|
2036
|
-
* Sample the position path
|
|
2042
|
+
* Sample the position path. Zero-alloc, no cursor side effects. Honours
|
|
2043
|
+
* posInterp and the stored-tangent → auto-CR fallback chain.
|
|
2037
2044
|
*
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2045
|
+
* Two signatures:
|
|
2046
|
+
* samplePos(out) cursor form — reads current seg/f
|
|
2047
|
+
* samplePos(out, seg, t) explicit (seg, t), continuous on the path
|
|
2041
2048
|
*
|
|
2042
2049
|
* @param {number[]} out 3-element result buffer.
|
|
2043
|
-
* @param {number} seg
|
|
2044
|
-
* @param {number} t
|
|
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.
|
|
2045
2052
|
* @returns {number[]} out
|
|
2046
2053
|
*/
|
|
2047
2054
|
samplePos(out, seg, t) {
|
|
2055
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2048
2056
|
return _samplePathCore(out, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
|
|
2049
2057
|
}
|
|
2050
2058
|
|
|
2051
2059
|
/**
|
|
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.
|
|
2060
|
+
* Write the TRS pose as a column-major model mat4. Zero-alloc, no cursor
|
|
2061
|
+
* side effects.
|
|
2057
2062
|
*
|
|
2058
|
-
*
|
|
2059
|
-
*
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2063
|
+
* Two signatures:
|
|
2064
|
+
* mat4Model(out) cursor form — reads current seg/f
|
|
2065
|
+
* mat4Model(out, seg, t) explicit (seg, t), continuous on the path
|
|
2066
|
+
*
|
|
2067
|
+
* Replaces the previous toMatrix() method.
|
|
2068
|
+
*
|
|
2069
|
+
* @param {Float32Array|number[]} out 16-element result buffer.
|
|
2070
|
+
* @param {number} [seg] Segment index in [0, segments−1]. Omit for cursor.
|
|
2071
|
+
* @param {number} [t] Local parameter in [0, 1]. Omit for cursor.
|
|
2072
|
+
* @returns {Float32Array|number[]} out
|
|
2062
2073
|
*/
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2074
|
+
mat4Model(out, seg, t) {
|
|
2075
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2076
|
+
this._sampleTRS(_trsScratch, seg, t);
|
|
2077
|
+
return transformToMat4(out, _trsScratch);
|
|
2066
2078
|
}
|
|
2067
2079
|
|
|
2068
2080
|
/**
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2071
|
-
*
|
|
2081
|
+
* Effective incoming / outgoing position tangents at keyframe `index`.
|
|
2082
|
+
* Stored tanIn/tanOut take precedence; each mirrors the other when only
|
|
2083
|
+
* one is stored; else centripetal Catmull-Rom tangents are auto-computed
|
|
2084
|
+
* from neighbours. Boundary keyframes mirror across the missing side.
|
|
2085
|
+
*
|
|
2086
|
+
* @param {number[]} outIn 3-element result — incoming tangent.
|
|
2087
|
+
* @param {number[]} outOut 3-element result — outgoing tangent.
|
|
2088
|
+
* @param {number} index Keyframe index.
|
|
2089
|
+
* @returns {PoseTrack} this
|
|
2072
2090
|
*/
|
|
2073
|
-
|
|
2074
|
-
|
|
2091
|
+
tangents(outIn, outOut, index) {
|
|
2092
|
+
_sampleTangentsCore(outIn, outOut, this.keyframes, 'pos', 'tanIn', 'tanOut', index);
|
|
2093
|
+
return this;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/** @private — write interpolated TRS at (seg, t). */
|
|
2097
|
+
_sampleTRS(out, seg, t) {
|
|
2075
2098
|
const n = this.keyframes.length;
|
|
2076
2099
|
if (n === 0) return out;
|
|
2077
|
-
|
|
2078
2100
|
if (n === 1) {
|
|
2079
2101
|
const k = this.keyframes[0];
|
|
2080
2102
|
out.pos[0]=k.pos[0]; out.pos[1]=k.pos[1]; out.pos[2]=k.pos[2];
|
|
@@ -2082,18 +2104,16 @@ class PoseTrack extends Track {
|
|
|
2082
2104
|
out.scl[0]=k.scl[0]; out.scl[1]=k.scl[1]; out.scl[2]=k.scl[2];
|
|
2083
2105
|
return out;
|
|
2084
2106
|
}
|
|
2085
|
-
|
|
2086
2107
|
const nSeg = n - 1;
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const
|
|
2108
|
+
seg = seg | 0;
|
|
2109
|
+
if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
|
|
2110
|
+
else if (seg < 0) { seg = 0; t = 0; }
|
|
2111
|
+
t = _clamp01(t);
|
|
2112
|
+
const k0 = this.keyframes[seg];
|
|
2113
|
+
const k1 = this.keyframes[seg + 1];
|
|
2092
2114
|
|
|
2093
|
-
// pos — shared sampler (respects posInterp + tangent fallback chain)
|
|
2094
2115
|
_samplePathCore(out.pos, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
|
|
2095
2116
|
|
|
2096
|
-
// rot — step, slerp, or nlerp
|
|
2097
2117
|
if (this.rotInterp === 'step') {
|
|
2098
2118
|
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
2119
|
} else if (this.rotInterp === 'nlerp') {
|
|
@@ -2102,20 +2122,22 @@ class PoseTrack extends Track {
|
|
|
2102
2122
|
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
2103
2123
|
}
|
|
2104
2124
|
|
|
2105
|
-
// scl — lerp
|
|
2106
2125
|
lerpVec3(out.scl, k0.scl, k1.scl, t);
|
|
2107
|
-
|
|
2108
2126
|
return out;
|
|
2109
2127
|
}
|
|
2110
2128
|
|
|
2111
2129
|
/**
|
|
2112
|
-
* Evaluate
|
|
2113
|
-
* @param {
|
|
2114
|
-
* @returns {
|
|
2130
|
+
* Evaluate interpolated TRS pose at current cursor.
|
|
2131
|
+
* @param {{ pos:number[], rot:number[], scl:number[] }} [out]
|
|
2132
|
+
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
2115
2133
|
*/
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2134
|
+
eval(out) {
|
|
2135
|
+
out = out || { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
|
|
2136
|
+
const n = this.keyframes.length;
|
|
2137
|
+
if (n === 0) return out;
|
|
2138
|
+
if (n === 1) return this._sampleTRS(out, 0, 0);
|
|
2139
|
+
const [seg, t] = this._cursorSegT();
|
|
2140
|
+
return this._sampleTRS(out, seg, t);
|
|
2119
2141
|
}
|
|
2120
2142
|
}
|
|
2121
2143
|
|
|
@@ -2128,27 +2150,31 @@ class PoseTrack extends Track {
|
|
|
2128
2150
|
*
|
|
2129
2151
|
* Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z],
|
|
2130
2152
|
* fov?:number, halfHeight?:number,
|
|
2153
|
+
* near:number, far:number,
|
|
2131
2154
|
* eyeTanIn?:[x,y,z], eyeTanOut?:[x,y,z],
|
|
2132
2155
|
* centerTanIn?:[x,y,z], centerTanOut?:[x,y,z] }
|
|
2133
2156
|
*
|
|
2134
2157
|
* fov — vertical fov (radians) for perspective cameras; null for ortho.
|
|
2135
2158
|
* halfHeight — world-unit half-height of ortho frustum; null for perspective.
|
|
2136
|
-
* Both are optional and nullable
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
* interpolation of the eye and center paths respectively.
|
|
2141
|
-
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
2159
|
+
* Both are optional and nullable because exactly one is meaningful per
|
|
2160
|
+
* keyframe (perspective xor ortho). eval() lerps each only when both
|
|
2161
|
+
* adjacent keyframes carry a non-null value for that field; mixed or
|
|
2162
|
+
* missing entries pass `null` through.
|
|
2142
2163
|
*
|
|
2143
|
-
*
|
|
2164
|
+
* near, far — clip plane distances (positive, world units). Always real
|
|
2165
|
+
* numbers. Defaults: near = 0.1, far = 1000 (three.js / Bevy convention).
|
|
2166
|
+
* Linearly interpolated between keyframes without null-passthrough.
|
|
2144
2167
|
*
|
|
2145
|
-
*
|
|
2168
|
+
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite
|
|
2169
|
+
* interpolation of the eye and center paths respectively. When absent,
|
|
2170
|
+
* centripetal Catmull-Rom tangents are auto-computed at sample time.
|
|
2146
2171
|
*
|
|
2147
|
-
*
|
|
2148
|
-
*
|
|
2172
|
+
* Missing fields default to: center → [0,0,0], up → [0,1,0],
|
|
2173
|
+
* near → 0.1, far → 1000.
|
|
2149
2174
|
*
|
|
2150
|
-
*
|
|
2151
|
-
* for full
|
|
2175
|
+
* For matrix-based capture of a camera-like pose use
|
|
2176
|
+
* PoseTrack.add({ mat4Model: mat4Eye }) for full TRS fidelity including roll,
|
|
2177
|
+
* or a lookat spec here for camera-style interpolation.
|
|
2152
2178
|
*/
|
|
2153
2179
|
class CameraTrack extends Track {
|
|
2154
2180
|
constructor() {
|
|
@@ -2166,15 +2192,13 @@ class CameraTrack extends Track {
|
|
|
2166
2192
|
}
|
|
2167
2193
|
|
|
2168
2194
|
/**
|
|
2169
|
-
* Append one or more camera keyframes. Adjacent duplicates are skipped by
|
|
2195
|
+
* Append one or more camera keyframes. Adjacent duplicates are skipped by
|
|
2196
|
+
* default.
|
|
2170
2197
|
* @param {Object|Object[]} spec
|
|
2171
2198
|
* @param {{ deduplicate?: boolean }} [opts]
|
|
2172
2199
|
*/
|
|
2173
2200
|
add(spec, opts) {
|
|
2174
|
-
if (Array.isArray(spec)) {
|
|
2175
|
-
for (const s of spec) this.add(s, opts);
|
|
2176
|
-
return;
|
|
2177
|
-
}
|
|
2201
|
+
if (Array.isArray(spec)) { for (const s of spec) this.add(s, opts); return; }
|
|
2178
2202
|
const kf = _parseCameraSpec(spec);
|
|
2179
2203
|
if (!kf) return;
|
|
2180
2204
|
const dedup = !opts || opts.deduplicate !== false;
|
|
@@ -2185,7 +2209,8 @@ class CameraTrack extends Track {
|
|
|
2185
2209
|
}
|
|
2186
2210
|
|
|
2187
2211
|
/**
|
|
2188
|
-
* Replace
|
|
2212
|
+
* Replace the camera keyframe at `index`, or append at the end if `index`
|
|
2213
|
+
* equals the current keyframe count.
|
|
2189
2214
|
* @param {number} index
|
|
2190
2215
|
* @param {Object} spec
|
|
2191
2216
|
* @returns {boolean}
|
|
@@ -2200,97 +2225,152 @@ class CameraTrack extends Track {
|
|
|
2200
2225
|
}
|
|
2201
2226
|
|
|
2202
2227
|
/**
|
|
2203
|
-
* Sample the eye path
|
|
2228
|
+
* Sample the eye path. Zero-alloc, no cursor side effects.
|
|
2229
|
+
*
|
|
2230
|
+
* Two signatures:
|
|
2231
|
+
* sampleEye(out) cursor form — reads current seg/f
|
|
2232
|
+
* sampleEye(out, seg, t) explicit (seg, t), continuous on the path
|
|
2233
|
+
*
|
|
2204
2234
|
* @param {number[]} out
|
|
2205
|
-
* @param {number} seg
|
|
2206
|
-
* @param {number} t
|
|
2235
|
+
* @param {number} [seg]
|
|
2236
|
+
* @param {number} [t]
|
|
2207
2237
|
* @returns {number[]} out
|
|
2208
2238
|
*/
|
|
2209
2239
|
sampleEye(out, seg, t) {
|
|
2240
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2210
2241
|
return _samplePathCore(out, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
|
|
2211
2242
|
}
|
|
2212
2243
|
|
|
2213
2244
|
/**
|
|
2214
|
-
* Sample the center path
|
|
2245
|
+
* Sample the center path. Zero-alloc, no cursor side effects.
|
|
2246
|
+
*
|
|
2247
|
+
* Two signatures:
|
|
2248
|
+
* sampleCenter(out) cursor form — reads current seg/f
|
|
2249
|
+
* sampleCenter(out, seg, t) explicit (seg, t)
|
|
2250
|
+
*
|
|
2215
2251
|
* @param {number[]} out
|
|
2216
|
-
* @param {number} seg
|
|
2217
|
-
* @param {number} t
|
|
2252
|
+
* @param {number} [seg]
|
|
2253
|
+
* @param {number} [t]
|
|
2218
2254
|
* @returns {number[]} out
|
|
2219
2255
|
*/
|
|
2220
2256
|
sampleCenter(out, seg, t) {
|
|
2257
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2221
2258
|
return _samplePathCore(out, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
|
|
2222
2259
|
}
|
|
2223
2260
|
|
|
2224
2261
|
/**
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2262
|
+
* Write the interpolated lookat eye matrix as a column-major mat4
|
|
2263
|
+
* (eye→world rigid frame). Zero-alloc, no cursor side effects.
|
|
2264
|
+
*
|
|
2265
|
+
* Two signatures:
|
|
2266
|
+
* mat4Eye(out) cursor form — reads current seg/f
|
|
2267
|
+
* mat4Eye(out, seg, t) explicit (seg, t), continuous on the path
|
|
2268
|
+
*
|
|
2269
|
+
* @param {Float32Array|number[]} out 16-element result buffer.
|
|
2270
|
+
* @param {number} [seg]
|
|
2271
|
+
* @param {number} [t]
|
|
2272
|
+
* @returns {Float32Array|number[]} out
|
|
2230
2273
|
*/
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2274
|
+
mat4Eye(out, seg, t) {
|
|
2275
|
+
if (arguments.length < 3) [seg, t] = this._cursorSegT();
|
|
2276
|
+
this._sampleEyePose(_eyeScratch, seg, t);
|
|
2277
|
+
const e = _eyeScratch;
|
|
2278
|
+
return mat4Eye(out,
|
|
2279
|
+
e.eye[0], e.eye[1], e.eye[2],
|
|
2280
|
+
e.center[0], e.center[1], e.center[2],
|
|
2281
|
+
e.up[0], e.up[1], e.up[2]);
|
|
2234
2282
|
}
|
|
2235
2283
|
|
|
2236
2284
|
/**
|
|
2237
|
-
* Effective in/out
|
|
2238
|
-
* @param {number[]} outIn
|
|
2239
|
-
* @param {number[]} outOut
|
|
2240
|
-
* @param {number} i
|
|
2241
|
-
* @returns {CameraTrack} this
|
|
2285
|
+
* Effective in/out eye tangents at keyframe `index`.
|
|
2242
2286
|
*/
|
|
2243
|
-
|
|
2244
|
-
_sampleTangentsCore(outIn, outOut, this.keyframes, '
|
|
2287
|
+
eyeTangents(outIn, outOut, index) {
|
|
2288
|
+
_sampleTangentsCore(outIn, outOut, this.keyframes, 'eye', 'eyeTanIn', 'eyeTanOut', index);
|
|
2245
2289
|
return this;
|
|
2246
2290
|
}
|
|
2247
2291
|
|
|
2248
2292
|
/**
|
|
2249
|
-
*
|
|
2250
|
-
*
|
|
2251
|
-
* @param {{ eye:number[], center:number[], up:number[] }} [out]
|
|
2252
|
-
* @returns {{ eye:number[], center:number[], up:number[] }} out
|
|
2293
|
+
* Effective in/out center tangents at keyframe `index`.
|
|
2253
2294
|
*/
|
|
2254
|
-
|
|
2255
|
-
|
|
2295
|
+
centerTangents(outIn, outOut, index) {
|
|
2296
|
+
_sampleTangentsCore(outIn, outOut, this.keyframes, 'center', 'centerTanIn', 'centerTanOut', index);
|
|
2297
|
+
return this;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/** @private — write interpolated { eye, center, up } at (seg, t). */
|
|
2301
|
+
_sampleEyePose(out, seg, t) {
|
|
2256
2302
|
const n = this.keyframes.length;
|
|
2257
2303
|
if (n === 0) return out;
|
|
2258
|
-
|
|
2259
2304
|
if (n === 1) {
|
|
2260
2305
|
const k = this.keyframes[0];
|
|
2261
2306
|
out.eye[0]=k.eye[0]; out.eye[1]=k.eye[1]; out.eye[2]=k.eye[2];
|
|
2262
2307
|
out.center[0]=k.center[0]; out.center[1]=k.center[1]; out.center[2]=k.center[2];
|
|
2263
2308
|
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
2309
|
return out;
|
|
2267
2310
|
}
|
|
2268
|
-
|
|
2269
2311
|
const nSeg = n - 1;
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
_samplePathCore(out.eye,
|
|
2278
|
-
|
|
2279
|
-
// center — shared sampler
|
|
2312
|
+
seg = seg | 0;
|
|
2313
|
+
if (seg >= nSeg) { seg = nSeg - 1; t = 1; }
|
|
2314
|
+
else if (seg < 0) { seg = 0; t = 0; }
|
|
2315
|
+
t = _clamp01(t);
|
|
2316
|
+
const k0 = this.keyframes[seg];
|
|
2317
|
+
const k1 = this.keyframes[seg + 1];
|
|
2318
|
+
|
|
2319
|
+
_samplePathCore(out.eye, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
|
|
2280
2320
|
_samplePathCore(out.center, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
|
|
2281
2321
|
|
|
2282
|
-
// up — nlerp on unit sphere
|
|
2283
2322
|
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;
|
|
2323
|
+
const ul = Math.sqrt(out.up[0]*out.up[0]+out.up[1]*out.up[1]+out.up[2]*out.up[2]) || 1;
|
|
2285
2324
|
out.up[0]/=ul; out.up[1]/=ul; out.up[2]/=ul;
|
|
2325
|
+
return out;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
/**
|
|
2329
|
+
* Evaluate interpolated camera pose at current cursor.
|
|
2330
|
+
*
|
|
2331
|
+
* `fov` / `halfHeight` are lerped only when both adjacent keyframes carry
|
|
2332
|
+
* a non-null value; mixed entries pass `null` through so the bridge can
|
|
2333
|
+
* leave the projection unchanged.
|
|
2334
|
+
*
|
|
2335
|
+
* `near` / `far` are always real numbers and are linearly interpolated
|
|
2336
|
+
* unconditionally.
|
|
2337
|
+
*
|
|
2338
|
+
* @param {{ eye:number[], center:number[], up:number[],
|
|
2339
|
+
* fov:number|null, halfHeight:number|null,
|
|
2340
|
+
* near:number, far:number }} [out]
|
|
2341
|
+
* @returns {{ eye:number[], center:number[], up:number[],
|
|
2342
|
+
* fov:number|null, halfHeight:number|null,
|
|
2343
|
+
* near:number, far:number }} out
|
|
2344
|
+
*/
|
|
2345
|
+
eval(out) {
|
|
2346
|
+
out = out || { eye:[0,0,0], center:[0,0,0], up:[0,1,0],
|
|
2347
|
+
fov:null, halfHeight:null,
|
|
2348
|
+
near:_DEFAULT_NEAR, far:_DEFAULT_FAR };
|
|
2349
|
+
const n = this.keyframes.length;
|
|
2350
|
+
if (n === 0) return out;
|
|
2351
|
+
if (n === 1) {
|
|
2352
|
+
const k = this.keyframes[0];
|
|
2353
|
+
this._sampleEyePose(out, 0, 0);
|
|
2354
|
+
out.fov = k.fov; out.halfHeight = k.halfHeight;
|
|
2355
|
+
out.near = k.near; out.far = k.far;
|
|
2356
|
+
return out;
|
|
2357
|
+
}
|
|
2358
|
+
const [seg, t] = this._cursorSegT();
|
|
2359
|
+
const k0 = this.keyframes[seg];
|
|
2360
|
+
const k1 = this.keyframes[seg + 1];
|
|
2361
|
+
|
|
2362
|
+
this._sampleEyePose(out, seg, t);
|
|
2286
2363
|
|
|
2287
|
-
// fov / halfHeight — lerp when both keyframes carry non-null values
|
|
2288
2364
|
out.fov = (k0.fov != null && k1.fov != null)
|
|
2289
2365
|
? k0.fov + t * (k1.fov - k0.fov) : (k0.fov ?? k1.fov ?? null);
|
|
2290
2366
|
out.halfHeight = (k0.halfHeight != null && k1.halfHeight != null)
|
|
2291
2367
|
? k0.halfHeight + t * (k1.halfHeight - k0.halfHeight)
|
|
2292
2368
|
: (k0.halfHeight ?? k1.halfHeight ?? null);
|
|
2293
2369
|
|
|
2370
|
+
// near / far carry real defaults on every keyframe — always lerp.
|
|
2371
|
+
out.near = k0.near + t * (k1.near - k0.near);
|
|
2372
|
+
out.far = k0.far + t * (k1.far - k0.far);
|
|
2373
|
+
|
|
2294
2374
|
return out;
|
|
2295
2375
|
}
|
|
2296
2376
|
}
|