@nakednous/tree 0.0.2 → 0.0.3
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 +217 -57
- package/dist/index.js +550 -356
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -881,58 +881,62 @@ function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
|
|
|
881
881
|
}
|
|
882
882
|
|
|
883
883
|
/**
|
|
884
|
-
* @file Pure quaternion/spline math +
|
|
884
|
+
* @file Pure quaternion/spline math + track state machines.
|
|
885
885
|
* @module tree/track
|
|
886
886
|
* @license GPL-3.0-only
|
|
887
887
|
*
|
|
888
888
|
* Zero dependencies. No p5, DOM, WebGL, or WebGPU usage.
|
|
889
|
-
* All quaternion operations use flat [x,y,z,w] arrays (w-last = glTF layout).
|
|
890
|
-
* PoseTrack is a pure state machine that stores {pos,rot,scl} keyframes
|
|
891
|
-
* and advances a cursor via tick().
|
|
892
889
|
*
|
|
893
890
|
* ── Exports ──────────────────────────────────────────────────────────────────
|
|
894
891
|
* Quaternion helpers
|
|
895
|
-
* qSet qCopy qDot qNormalize qNegate qMul qSlerp
|
|
892
|
+
* qSet qCopy qDot qNormalize qNegate qMul qSlerp qNlerp
|
|
896
893
|
* qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
897
894
|
* quatToAxisAngle
|
|
898
895
|
* Spline / vector helpers
|
|
899
896
|
* catmullRomVec3 lerpVec3
|
|
900
897
|
* Transform / mat4 helpers
|
|
901
898
|
* transformToMat4 mat4ToTransform
|
|
902
|
-
*
|
|
903
|
-
* PoseTrack
|
|
899
|
+
* Tracks
|
|
900
|
+
* PoseTrack — { pos, rot, scl } TRS keyframes
|
|
901
|
+
* CameraTrack — { eye, center, up } lookat keyframes
|
|
902
|
+
*
|
|
903
|
+
* ── Class hierarchy ───────────────────────────────────────────────────────────
|
|
904
|
+
* Track (unexported, never instantiated directly)
|
|
905
|
+
* └── PoseTrack (exported)
|
|
906
|
+
* └── CameraTrack (exported)
|
|
907
|
+
*
|
|
908
|
+
* Track holds all transport machinery: cursor, play/stop/seek/tick,
|
|
909
|
+
* hooks, rate semantics. Subclasses add only keyframe storage and
|
|
910
|
+
* add() / eval() for their respective data shape.
|
|
904
911
|
*
|
|
905
912
|
* ── Hook architecture ─────────────────────────────────────────────────────────
|
|
906
913
|
* _onActivate / _onDeactivate — lib-space (underscore, set by host layer)
|
|
907
|
-
* Fire
|
|
908
|
-
* Used by the addon to register/unregister from the draw-loop tick set.
|
|
914
|
+
* Fire on playing transitions: false→true / true→false.
|
|
909
915
|
*
|
|
910
|
-
* onPlay / onEnd / onStop — user-space (public
|
|
911
|
-
* onPlay : fires in play()
|
|
912
|
-
* onEnd : fires in tick()
|
|
913
|
-
* onStop : fires in stop()
|
|
914
|
-
*
|
|
915
|
-
* onEnd and onStop are mutually exclusive per deactivation event.
|
|
916
|
-
* To react to any deactivation, chain both.
|
|
916
|
+
* onPlay / onEnd / onStop — user-space (public)
|
|
917
|
+
* onPlay : fires in play() on false→true transition.
|
|
918
|
+
* onEnd : fires in tick() at natural boundary (once mode only).
|
|
919
|
+
* onStop : fires in stop() / reset() — explicit deactivation.
|
|
920
|
+
* onEnd and onStop are mutually exclusive per event.
|
|
917
921
|
*
|
|
918
922
|
* Firing order:
|
|
919
923
|
* play() → onPlay → _onActivate
|
|
920
|
-
* tick() → onEnd → _onDeactivate
|
|
924
|
+
* tick() → onEnd → _onDeactivate
|
|
921
925
|
* stop() → onStop → _onDeactivate
|
|
922
926
|
* reset() → onStop → _onDeactivate
|
|
923
927
|
*
|
|
924
928
|
* ── Playback semantics (rate) ─────────────────────────────────────────────────
|
|
925
|
-
* rate > 0 forward
|
|
926
|
-
* rate < 0 backward
|
|
927
|
-
* rate === 0 frozen: tick()
|
|
929
|
+
* rate > 0 forward
|
|
930
|
+
* rate < 0 backward
|
|
931
|
+
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
928
932
|
*
|
|
929
|
-
* play()
|
|
930
|
-
* stop()
|
|
931
|
-
* Assigning rate never
|
|
933
|
+
* play() is the sole setter of playing = true.
|
|
934
|
+
* stop() is the sole setter of playing = false.
|
|
935
|
+
* Assigning rate never starts or stops playback.
|
|
932
936
|
*
|
|
933
937
|
* ── One-keyframe behaviour ────────────────────────────────────────────────────
|
|
934
938
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
935
|
-
* setting playing = true and without
|
|
939
|
+
* setting playing = true and without firing hooks.
|
|
936
940
|
*/
|
|
937
941
|
|
|
938
942
|
|
|
@@ -953,68 +957,62 @@ const qCopy = (out, a) => {
|
|
|
953
957
|
/** Dot product of two quaternions. */
|
|
954
958
|
const qDot = (a, b) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
955
959
|
|
|
956
|
-
/** Normalise in-place. @returns {number[]} out */
|
|
960
|
+
/** Normalise quaternion in-place. @returns {number[]} out */
|
|
957
961
|
const qNormalize = (out) => {
|
|
958
|
-
const
|
|
959
|
-
out[0]
|
|
960
|
-
return out;
|
|
962
|
+
const l = Math.sqrt(out[0]*out[0]+out[1]*out[1]+out[2]*out[2]+out[3]*out[3]) || 1;
|
|
963
|
+
out[0]/=l; out[1]/=l; out[2]/=l; out[3]/=l; return out;
|
|
961
964
|
};
|
|
962
965
|
|
|
963
|
-
/** Negate
|
|
964
|
-
const qNegate = (out) => {
|
|
965
|
-
out[0]
|
|
966
|
-
return out;
|
|
966
|
+
/** Negate quaternion (same rotation, different hemisphere). @returns {number[]} out */
|
|
967
|
+
const qNegate = (out, a) => {
|
|
968
|
+
out[0]=-a[0]; out[1]=-a[1]; out[2]=-a[2]; out[3]=-a[3]; return out;
|
|
967
969
|
};
|
|
968
970
|
|
|
969
|
-
/** out = a * b
|
|
971
|
+
/** Hamilton product out = a * b. @returns {number[]} out */
|
|
970
972
|
const qMul = (out, a, b) => {
|
|
971
|
-
const ax
|
|
972
|
-
|
|
973
|
-
out[
|
|
974
|
-
out[
|
|
975
|
-
out[
|
|
976
|
-
out[3] = aw*bw - ax*bx - ay*by - az*bz;
|
|
973
|
+
const ax=a[0],ay=a[1],az=a[2],aw=a[3], bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
974
|
+
out[0]=aw*bx+ax*bw+ay*bz-az*by;
|
|
975
|
+
out[1]=aw*by-ax*bz+ay*bw+az*bx;
|
|
976
|
+
out[2]=aw*bz+ax*by-ay*bx+az*bw;
|
|
977
|
+
out[3]=aw*bw-ax*bx-ay*by-az*bz;
|
|
977
978
|
return out;
|
|
978
979
|
};
|
|
979
980
|
|
|
981
|
+
/** Spherical linear interpolation. @returns {number[]} out */
|
|
982
|
+
const qSlerp = (out, a, b, t) => {
|
|
983
|
+
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
984
|
+
let d = a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw;
|
|
985
|
+
if (d < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; d=-d; }
|
|
986
|
+
let f0, f1;
|
|
987
|
+
if (1-d > 1e-10) {
|
|
988
|
+
const th=Math.acos(d), st=Math.sin(th);
|
|
989
|
+
f0=Math.sin((1-t)*th)/st; f1=Math.sin(t*th)/st;
|
|
990
|
+
} else {
|
|
991
|
+
f0=1-t; f1=t;
|
|
992
|
+
}
|
|
993
|
+
out[0]=a[0]*f0+bx*f1; out[1]=a[1]*f0+by*f1;
|
|
994
|
+
out[2]=a[2]*f0+bz*f1; out[3]=a[3]*f0+bw*f1;
|
|
995
|
+
return qNormalize(out);
|
|
996
|
+
};
|
|
997
|
+
|
|
980
998
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
* @param {number[]} out 4-element result array.
|
|
985
|
-
* @param {number[]} a Start quaternion [x,y,z,w].
|
|
986
|
-
* @param {number[]} b End quaternion [x,y,z,w].
|
|
987
|
-
* @param {number} t Blend [0, 1].
|
|
999
|
+
* Normalised linear interpolation (nlerp).
|
|
1000
|
+
* Cheaper than slerp; slightly non-constant angular velocity.
|
|
1001
|
+
* Handles antipodal quats by flipping b when dot < 0.
|
|
988
1002
|
* @returns {number[]} out
|
|
989
1003
|
*/
|
|
990
|
-
const
|
|
991
|
-
let
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
out[1] = a[1] + t*(by - a[1]);
|
|
997
|
-
out[2] = a[2] + t*(bz - a[2]);
|
|
998
|
-
out[3] = a[3] + t*(bw - a[3]);
|
|
999
|
-
return qNormalize(out);
|
|
1000
|
-
}
|
|
1001
|
-
const theta = Math.acos(d), sinT = Math.sin(theta);
|
|
1002
|
-
const s0 = Math.sin((1 - t) * theta) / sinT;
|
|
1003
|
-
const s1 = Math.sin(t * theta) / sinT;
|
|
1004
|
-
out[0] = s0*a[0] + s1*bx;
|
|
1005
|
-
out[1] = s0*a[1] + s1*by;
|
|
1006
|
-
out[2] = s0*a[2] + s1*bz;
|
|
1007
|
-
out[3] = s0*a[3] + s1*bw;
|
|
1008
|
-
return out;
|
|
1004
|
+
const qNlerp = (out, a, b, t) => {
|
|
1005
|
+
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
1006
|
+
if (a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; }
|
|
1007
|
+
out[0]=a[0]+t*(bx-a[0]); out[1]=a[1]+t*(by-a[1]);
|
|
1008
|
+
out[2]=a[2]+t*(bz-a[2]); out[3]=a[3]+t*(bw-a[3]);
|
|
1009
|
+
return qNormalize(out);
|
|
1009
1010
|
};
|
|
1010
1011
|
|
|
1011
1012
|
/**
|
|
1012
|
-
* Build a quaternion from
|
|
1013
|
-
* The axis need not be normalised.
|
|
1013
|
+
* Build a quaternion from axis-angle.
|
|
1014
1014
|
* @param {number[]} out
|
|
1015
|
-
* @param {number} ax Axis
|
|
1016
|
-
* @param {number} ay Axis y.
|
|
1017
|
-
* @param {number} az Axis z.
|
|
1015
|
+
* @param {number} ax @param {number} ay @param {number} az Axis (need not be unit).
|
|
1018
1016
|
* @param {number} angle Radians.
|
|
1019
1017
|
* @returns {number[]} out
|
|
1020
1018
|
*/
|
|
@@ -1022,93 +1020,76 @@ const qFromAxisAngle = (out, ax, ay, az, angle) => {
|
|
|
1022
1020
|
const half = angle * 0.5;
|
|
1023
1021
|
const s = Math.sin(half);
|
|
1024
1022
|
const len = Math.sqrt(ax*ax + ay*ay + az*az) || 1;
|
|
1025
|
-
out[0] = s * ax / len;
|
|
1026
|
-
out[1] = s * ay / len;
|
|
1027
|
-
out[2] = s * az / len;
|
|
1023
|
+
out[0] = s * ax / len; out[1] = s * ay / len; out[2] = s * az / len;
|
|
1028
1024
|
out[3] = Math.cos(half);
|
|
1029
1025
|
return out;
|
|
1030
1026
|
};
|
|
1031
1027
|
|
|
1032
1028
|
/**
|
|
1033
|
-
* Build a quaternion from a look direction (
|
|
1034
|
-
* and an optional up vector (defaults to +Y).
|
|
1029
|
+
* Build a quaternion from a look direction (−Z forward) and optional up (default +Y).
|
|
1035
1030
|
* @param {number[]} out
|
|
1036
1031
|
* @param {number[]} dir Forward direction [x,y,z].
|
|
1037
1032
|
* @param {number[]} [up] Up vector [x,y,z].
|
|
1038
1033
|
* @returns {number[]} out
|
|
1039
1034
|
*/
|
|
1040
1035
|
const qFromLookDir = (out, dir, up) => {
|
|
1041
|
-
let fx
|
|
1042
|
-
const
|
|
1043
|
-
fx
|
|
1044
|
-
let ux
|
|
1045
|
-
let rx
|
|
1046
|
-
const
|
|
1047
|
-
rx
|
|
1048
|
-
ux
|
|
1049
|
-
return qFromRotMat3x3(out, rx,
|
|
1036
|
+
let fx=dir[0],fy=dir[1],fz=dir[2];
|
|
1037
|
+
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1038
|
+
fx/=fl; fy/=fl; fz/=fl;
|
|
1039
|
+
let ux=up?up[0]:0, uy=up?up[1]:1, uz=up?up[2]:0;
|
|
1040
|
+
let rx=uy*fz-uz*fy, ry=uz*fx-ux*fz, rz=ux*fy-uy*fx;
|
|
1041
|
+
const rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1;
|
|
1042
|
+
rx/=rl; ry/=rl; rz/=rl;
|
|
1043
|
+
ux=fy*rz-fz*ry; uy=fz*rx-fx*rz; uz=fx*ry-fy*rx;
|
|
1044
|
+
return qFromRotMat3x3(out, rx,ry,rz, ux,uy,uz, -fx,-fy,-fz);
|
|
1050
1045
|
};
|
|
1051
1046
|
|
|
1052
1047
|
/**
|
|
1053
|
-
* Build a quaternion from a
|
|
1048
|
+
* Build a quaternion from a 3×3 rotation matrix (9 row-major scalars).
|
|
1054
1049
|
* @returns {number[]} out (normalised)
|
|
1055
1050
|
*/
|
|
1056
|
-
const qFromRotMat3x3 = (out, m00,
|
|
1057
|
-
const tr = m00
|
|
1051
|
+
const qFromRotMat3x3 = (out, m00,m01,m02, m10,m11,m12, m20,m21,m22) => {
|
|
1052
|
+
const tr = m00+m11+m22;
|
|
1058
1053
|
if (tr > 0) {
|
|
1059
|
-
const s
|
|
1060
|
-
out[3]
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
out[
|
|
1064
|
-
} else if (
|
|
1065
|
-
const s
|
|
1066
|
-
out[3] =
|
|
1067
|
-
out[0] = 0.25 * s;
|
|
1068
|
-
out[1] = (m01 + m10) / s;
|
|
1069
|
-
out[2] = (m02 + m20) / s;
|
|
1070
|
-
} else if (m11 > m22) {
|
|
1071
|
-
const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
|
1072
|
-
out[3] = (m02 - m20) / s;
|
|
1073
|
-
out[0] = (m01 + m10) / s;
|
|
1074
|
-
out[1] = 0.25 * s;
|
|
1075
|
-
out[2] = (m12 + m21) / s;
|
|
1054
|
+
const s=0.5/Math.sqrt(tr+1);
|
|
1055
|
+
out[3]=0.25/s; out[0]=(m21-m12)*s; out[1]=(m02-m20)*s; out[2]=(m10-m01)*s;
|
|
1056
|
+
} else if (m00>m11 && m00>m22) {
|
|
1057
|
+
const s=2*Math.sqrt(1+m00-m11-m22);
|
|
1058
|
+
out[3]=(m21-m12)/s; out[0]=0.25*s; out[1]=(m01+m10)/s; out[2]=(m02+m20)/s;
|
|
1059
|
+
} else if (m11>m22) {
|
|
1060
|
+
const s=2*Math.sqrt(1+m11-m00-m22);
|
|
1061
|
+
out[3]=(m02-m20)/s; out[0]=(m01+m10)/s; out[1]=0.25*s; out[2]=(m12+m21)/s;
|
|
1076
1062
|
} else {
|
|
1077
|
-
const s
|
|
1078
|
-
out[3]
|
|
1079
|
-
out[0] = (m02 + m20) / s;
|
|
1080
|
-
out[1] = (m12 + m21) / s;
|
|
1081
|
-
out[2] = 0.25 * s;
|
|
1063
|
+
const s=2*Math.sqrt(1+m22-m00-m11);
|
|
1064
|
+
out[3]=(m10-m01)/s; out[0]=(m02+m20)/s; out[1]=(m12+m21)/s; out[2]=0.25*s;
|
|
1082
1065
|
}
|
|
1083
1066
|
return qNormalize(out);
|
|
1084
1067
|
};
|
|
1085
1068
|
|
|
1086
1069
|
/**
|
|
1087
|
-
* Extract a unit quaternion from the upper-left
|
|
1070
|
+
* Extract a unit quaternion from the upper-left 3×3 of a column-major mat4.
|
|
1088
1071
|
* @param {number[]} out
|
|
1089
1072
|
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1090
1073
|
* @returns {number[]} out
|
|
1091
1074
|
*/
|
|
1092
1075
|
const qFromMat4 = (out, m) =>
|
|
1093
|
-
qFromRotMat3x3(out, m[0],
|
|
1076
|
+
qFromRotMat3x3(out, m[0],m[4],m[8], m[1],m[5],m[9], m[2],m[6],m[10]);
|
|
1094
1077
|
|
|
1095
1078
|
/**
|
|
1096
|
-
* Write a quaternion into a column-major mat4
|
|
1097
|
-
*
|
|
1079
|
+
* Write a quaternion into the rotation block of a column-major mat4.
|
|
1080
|
+
* Translation and perspective rows/cols are set to identity values.
|
|
1098
1081
|
* @param {Float32Array|number[]} out 16-element array.
|
|
1099
1082
|
* @param {number[]} q [x,y,z,w].
|
|
1100
1083
|
* @returns {Float32Array|number[]} out
|
|
1101
1084
|
*/
|
|
1102
1085
|
const qToMat4 = (out, q) => {
|
|
1103
|
-
const x
|
|
1104
|
-
const x2
|
|
1105
|
-
const xx
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
out[
|
|
1109
|
-
out[
|
|
1110
|
-
out[8] = xz+wy; out[9] = yz-wx; out[10] = 1-(xx+yy); out[11] = 0;
|
|
1111
|
-
out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1;
|
|
1086
|
+
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
1087
|
+
const x2=x+x,y2=y+y,z2=z+z;
|
|
1088
|
+
const xx=x*x2,xy=x*y2,xz=x*z2,yy=y*y2,yz=y*z2,zz=z*z2,wx=w*x2,wy=w*y2,wz=w*z2;
|
|
1089
|
+
out[0]=1-(yy+zz); out[1]=xy+wz; out[2]=xz-wy; out[3]=0;
|
|
1090
|
+
out[4]=xy-wz; out[5]=1-(xx+zz); out[6]=yz+wx; out[7]=0;
|
|
1091
|
+
out[8]=xz+wy; out[9]=yz-wx; out[10]=1-(xx+yy); out[11]=0;
|
|
1092
|
+
out[12]=0; out[13]=0; out[14]=0; out[15]=1;
|
|
1112
1093
|
return out;
|
|
1113
1094
|
};
|
|
1114
1095
|
|
|
@@ -1120,11 +1101,11 @@ const qToMat4 = (out, q) => {
|
|
|
1120
1101
|
*/
|
|
1121
1102
|
const quatToAxisAngle = (q, out) => {
|
|
1122
1103
|
out = out || {};
|
|
1123
|
-
const x
|
|
1124
|
-
const sinHalf = Math.sqrt(x*x
|
|
1125
|
-
if (sinHalf < 1e-8) { out.axis
|
|
1126
|
-
out.angle = 2
|
|
1127
|
-
out.axis = [x
|
|
1104
|
+
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
1105
|
+
const sinHalf = Math.sqrt(x*x+y*y+z*z);
|
|
1106
|
+
if (sinHalf < 1e-8) { out.axis=[0,1,0]; out.angle=0; return out; }
|
|
1107
|
+
out.angle = 2*Math.atan2(sinHalf, w);
|
|
1108
|
+
out.axis = [x/sinHalf, y/sinHalf, z/sinHalf];
|
|
1128
1109
|
return out;
|
|
1129
1110
|
};
|
|
1130
1111
|
|
|
@@ -1133,35 +1114,34 @@ const quatToAxisAngle = (q, out) => {
|
|
|
1133
1114
|
// =========================================================================
|
|
1134
1115
|
|
|
1135
1116
|
function _dist3(a, b) {
|
|
1136
|
-
const dx
|
|
1137
|
-
return Math.sqrt(dx*dx
|
|
1117
|
+
const dx=a[0]-b[0], dy=a[1]-b[1], dz=a[2]-b[2];
|
|
1118
|
+
return Math.sqrt(dx*dx+dy*dy+dz*dz);
|
|
1138
1119
|
}
|
|
1139
1120
|
|
|
1140
1121
|
/**
|
|
1141
|
-
* Centripetal Catmull-Rom interpolation (alpha
|
|
1142
|
-
* out = interp(p0, p1, p2, p3, t) where t
|
|
1143
|
-
* Boundary:
|
|
1122
|
+
* Centripetal Catmull-Rom interpolation (alpha=0.5, Barry-Goldman).
|
|
1123
|
+
* out = interp(p0, p1, p2, p3, t) where t∈[0,1] maps p1→p2.
|
|
1124
|
+
* Boundary: p0===p1 or p2===p3 clamps the end tangent.
|
|
1144
1125
|
* @param {number[]} out 3-element result.
|
|
1145
|
-
* @param {number[]} p0
|
|
1146
|
-
* @param {number[]} p1
|
|
1147
|
-
* @param {number[]} p2
|
|
1148
|
-
* @param {number[]} p3
|
|
1149
|
-
* @param {number} t
|
|
1126
|
+
* @param {number[]} p0 Control point before p1.
|
|
1127
|
+
* @param {number[]} p1 Segment start.
|
|
1128
|
+
* @param {number[]} p2 Segment end.
|
|
1129
|
+
* @param {number[]} p3 Control point after p2.
|
|
1130
|
+
* @param {number} t Blend [0, 1].
|
|
1150
1131
|
* @returns {number[]} out
|
|
1151
1132
|
*/
|
|
1152
1133
|
const catmullRomVec3 = (out, p0, p1, p2, p3, t) => {
|
|
1153
1134
|
const alpha = 0.5;
|
|
1154
|
-
const dt0
|
|
1155
|
-
const dt1
|
|
1156
|
-
const dt2
|
|
1135
|
+
const dt0 = Math.pow(_dist3(p0,p1), alpha) || 1;
|
|
1136
|
+
const dt1 = Math.pow(_dist3(p1,p2), alpha) || 1;
|
|
1137
|
+
const dt2 = Math.pow(_dist3(p2,p3), alpha) || 1;
|
|
1157
1138
|
for (let i = 0; i < 3; i++) {
|
|
1158
1139
|
const t1_0 = (p1[i]-p0[i])/dt0 - (p2[i]-p0[i])/(dt0+dt1) + (p2[i]-p1[i])/dt1;
|
|
1159
1140
|
const t2_0 = (p2[i]-p1[i])/dt1 - (p3[i]-p1[i])/(dt1+dt2) + (p3[i]-p2[i])/dt2;
|
|
1160
|
-
const m1
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
out[i] = a*t*t*t + b*t*t + m1*t + p1[i];
|
|
1141
|
+
const m1=t1_0*dt1, m2=t2_0*dt1;
|
|
1142
|
+
const a= 2*p1[i]-2*p2[i]+m1+m2;
|
|
1143
|
+
const b=-3*p1[i]+3*p2[i]-2*m1-m2;
|
|
1144
|
+
out[i] = a*t*t*t + b*t*t + m1*t + p1[i];
|
|
1165
1145
|
}
|
|
1166
1146
|
return out;
|
|
1167
1147
|
};
|
|
@@ -1175,9 +1155,9 @@ const catmullRomVec3 = (out, p0, p1, p2, p3, t) => {
|
|
|
1175
1155
|
* @returns {number[]} out
|
|
1176
1156
|
*/
|
|
1177
1157
|
const lerpVec3 = (out, a, b, t) => {
|
|
1178
|
-
out[0]
|
|
1179
|
-
out[1]
|
|
1180
|
-
out[2]
|
|
1158
|
+
out[0]=a[0]+t*(b[0]-a[0]);
|
|
1159
|
+
out[1]=a[1]+t*(b[1]-a[1]);
|
|
1160
|
+
out[2]=a[2]+t*(b[2]-a[2]);
|
|
1181
1161
|
return out;
|
|
1182
1162
|
};
|
|
1183
1163
|
|
|
@@ -1187,45 +1167,42 @@ const lerpVec3 = (out, a, b, t) => {
|
|
|
1187
1167
|
|
|
1188
1168
|
/**
|
|
1189
1169
|
* Write a TRS transform into a column-major mat4.
|
|
1190
|
-
* Rotation is encoded as a quaternion; scale is baked into rotation columns.
|
|
1191
1170
|
* @param {Float32Array|number[]} out 16-element column-major mat4.
|
|
1192
1171
|
* @param {{ pos:number[], rot:number[], scl:number[] }} xform
|
|
1193
1172
|
* @returns {Float32Array|number[]} out
|
|
1194
1173
|
*/
|
|
1195
1174
|
const transformToMat4 = (out, xform) => {
|
|
1196
1175
|
qToMat4(out, xform.rot);
|
|
1197
|
-
const sx
|
|
1198
|
-
out[0]
|
|
1199
|
-
out[4]
|
|
1200
|
-
out[8]
|
|
1201
|
-
out[12] = xform.pos[
|
|
1202
|
-
out[13] = xform.pos[1];
|
|
1203
|
-
out[14] = xform.pos[2];
|
|
1176
|
+
const sx=xform.scl[0], sy=xform.scl[1], sz=xform.scl[2];
|
|
1177
|
+
out[0]*=sx; out[1]*=sx; out[2]*=sx;
|
|
1178
|
+
out[4]*=sy; out[5]*=sy; out[6]*=sy;
|
|
1179
|
+
out[8]*=sz; out[9]*=sz; out[10]*=sz;
|
|
1180
|
+
out[12]=xform.pos[0]; out[13]=xform.pos[1]; out[14]=xform.pos[2];
|
|
1204
1181
|
return out;
|
|
1205
1182
|
};
|
|
1206
1183
|
|
|
1207
1184
|
/**
|
|
1208
1185
|
* Decompose a column-major mat4 into a TRS transform.
|
|
1209
|
-
* Assumes no shear. Scale
|
|
1186
|
+
* Assumes no shear. Scale extracted from column lengths.
|
|
1210
1187
|
* @param {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1211
1188
|
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1212
1189
|
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1213
1190
|
*/
|
|
1214
1191
|
const mat4ToTransform = (out, m) => {
|
|
1215
|
-
out.pos[0]
|
|
1216
|
-
const sx
|
|
1217
|
-
const sy
|
|
1218
|
-
const sz
|
|
1219
|
-
out.scl[0]
|
|
1192
|
+
out.pos[0]=m[12]; out.pos[1]=m[13]; out.pos[2]=m[14];
|
|
1193
|
+
const sx=Math.sqrt(m[0]*m[0]+m[1]*m[1]+m[2]*m[2]);
|
|
1194
|
+
const sy=Math.sqrt(m[4]*m[4]+m[5]*m[5]+m[6]*m[6]);
|
|
1195
|
+
const sz=Math.sqrt(m[8]*m[8]+m[9]*m[9]+m[10]*m[10]);
|
|
1196
|
+
out.scl[0]=sx; out.scl[1]=sy; out.scl[2]=sz;
|
|
1220
1197
|
qFromRotMat3x3(out.rot,
|
|
1221
|
-
m[0]/sx,
|
|
1222
|
-
m[1]/sx,
|
|
1223
|
-
m[2]/sx,
|
|
1198
|
+
m[0]/sx,m[4]/sy,m[8]/sz,
|
|
1199
|
+
m[1]/sx,m[5]/sy,m[9]/sz,
|
|
1200
|
+
m[2]/sx,m[6]/sy,m[10]/sz);
|
|
1224
1201
|
return out;
|
|
1225
1202
|
};
|
|
1226
1203
|
|
|
1227
1204
|
// =========================================================================
|
|
1228
|
-
//
|
|
1205
|
+
// S4a Spec parser — PoseTrack
|
|
1229
1206
|
// =========================================================================
|
|
1230
1207
|
|
|
1231
1208
|
const _isNum = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
@@ -1234,198 +1211,175 @@ const _clampS = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
|
|
|
1234
1211
|
|
|
1235
1212
|
function _parseVec3(v) {
|
|
1236
1213
|
if (!v) return null;
|
|
1237
|
-
if (Array.isArray(v) && v.length >= 3 && v.every(n => typeof n === 'number')) return [v[0],
|
|
1238
|
-
if (typeof v === 'object' && 'x' in v) return [v.x
|
|
1214
|
+
if (Array.isArray(v) && v.length >= 3 && v.every(n => typeof n === 'number')) return [v[0],v[1],v[2]];
|
|
1215
|
+
if (typeof v === 'object' && 'x' in v) return [v.x||0, v.y||0, v.z||0];
|
|
1239
1216
|
return null;
|
|
1240
1217
|
}
|
|
1241
1218
|
|
|
1242
1219
|
function _parseQuat(v) {
|
|
1243
1220
|
if (!v) return null;
|
|
1244
|
-
if (Array.isArray(v) && v.length === 4 && v.every(n => typeof n === 'number')) return [v[0],
|
|
1221
|
+
if (Array.isArray(v) && v.length === 4 && v.every(n => typeof n === 'number')) return [v[0],v[1],v[2],v[3]];
|
|
1245
1222
|
if (v.axis && typeof v.angle === 'number') {
|
|
1246
|
-
const a = Array.isArray(v.axis) ? v.axis : [v.axis.x
|
|
1247
|
-
return qFromAxisAngle([0,
|
|
1223
|
+
const a = Array.isArray(v.axis) ? v.axis : [v.axis.x||0, v.axis.y||0, v.axis.z||0];
|
|
1224
|
+
return qFromAxisAngle([0,0,0,1], a[0],a[1],a[2], v.angle);
|
|
1248
1225
|
}
|
|
1249
1226
|
if (v.dir) {
|
|
1250
|
-
const d = Array.isArray(v.dir) ? v.dir : [v.dir.x
|
|
1251
|
-
const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x
|
|
1252
|
-
return qFromLookDir([0,
|
|
1227
|
+
const d = Array.isArray(v.dir) ? v.dir : [v.dir.x||0, v.dir.y||0, v.dir.z||0];
|
|
1228
|
+
const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x||0, v.up.y||0, v.up.z||0]) : null;
|
|
1229
|
+
return qFromLookDir([0,0,0,1], d, u);
|
|
1230
|
+
}
|
|
1231
|
+
// { view } — column-major mat4 or {mat4} wrapper
|
|
1232
|
+
if (v.view != null) {
|
|
1233
|
+
const m = (ArrayBuffer.isView(v.view) || Array.isArray(v.view)) ? v.view : (v.view.mat4 ?? null);
|
|
1234
|
+
if (m && m.length === 16) return qFromMat4([0,0,0,1], m);
|
|
1235
|
+
}
|
|
1236
|
+
// { eye, center, up? } — lookat shorthand matching CameraTrack input
|
|
1237
|
+
if (v.eye && v.center) {
|
|
1238
|
+
const eye = _parseVec3(v.eye), ctr = _parseVec3(v.center);
|
|
1239
|
+
if (eye && ctr) {
|
|
1240
|
+
const up = (v.up ? _parseVec3(v.up) : null) || [0,1,0];
|
|
1241
|
+
let fx=ctr[0]-eye[0], fy=ctr[1]-eye[1], fz=ctr[2]-eye[2];
|
|
1242
|
+
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1; fx/=fl; fy/=fl; fz/=fl;
|
|
1243
|
+
let rx=fy*up[2]-fz*up[1], ry=fz*up[0]-fx*up[2], rz=fx*up[1]-fy*up[0];
|
|
1244
|
+
const rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1; rx/=rl; ry/=rl; rz/=rl;
|
|
1245
|
+
const ux=ry*fz-rz*fy, uy=rz*fx-rx*fz, uz=rx*fy-ry*fx;
|
|
1246
|
+
return qFromRotMat3x3([0,0,0,1], rx,ux,-fx, ry,uy,-fy, rz,uz,-fz);
|
|
1247
|
+
}
|
|
1253
1248
|
}
|
|
1254
1249
|
return null;
|
|
1255
1250
|
}
|
|
1256
1251
|
|
|
1257
1252
|
function _parseSpec(spec) {
|
|
1258
1253
|
if (!spec || typeof spec !== 'object') return null;
|
|
1259
|
-
const pos = _parseVec3(spec.pos) || [0,
|
|
1260
|
-
const rot = _parseQuat(spec.rot) || [0,
|
|
1261
|
-
const scl = _parseVec3(spec.scl) || [1,
|
|
1254
|
+
const pos = _parseVec3(spec.pos) || [0,0,0];
|
|
1255
|
+
const rot = _parseQuat(spec.rot) || [0,0,0,1];
|
|
1256
|
+
const scl = _parseVec3(spec.scl) || [1,1,1];
|
|
1262
1257
|
return { pos, rot, scl };
|
|
1263
1258
|
}
|
|
1264
1259
|
|
|
1265
1260
|
function _sameTransform(a, b) {
|
|
1266
|
-
for (let i
|
|
1267
|
-
for (let i
|
|
1261
|
+
for (let i=0;i<3;i++) if (a.pos[i]!==b.pos[i]||a.scl[i]!==b.scl[i]) return false;
|
|
1262
|
+
for (let i=0;i<4;i++) if (a.rot[i]!==b.rot[i]) return false;
|
|
1268
1263
|
return true;
|
|
1269
1264
|
}
|
|
1270
1265
|
|
|
1271
1266
|
// =========================================================================
|
|
1272
|
-
//
|
|
1267
|
+
// S4b Spec parser — CameraTrack
|
|
1273
1268
|
// =========================================================================
|
|
1274
1269
|
|
|
1275
1270
|
/**
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
* Keyframes are TRS pose objects: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z] }.
|
|
1279
|
-
* The track maintains a scalar cursor (seg, f) that advances each tick().
|
|
1280
|
-
*
|
|
1281
|
-
* Position uses centripetal Catmull-Rom spline by default (posInterp = 'catmullrom');
|
|
1282
|
-
* set posInterp = 'linear' to switch to lerp. Rotation uses SLERP. Scale uses LERP.
|
|
1271
|
+
* Parse a camera keyframe spec into internal { eye, center, up } form.
|
|
1283
1272
|
*
|
|
1284
|
-
*
|
|
1285
|
-
* rate > 0 forward
|
|
1286
|
-
* rate < 0 backward
|
|
1287
|
-
* rate === 0 frozen: tick() is a no-op; playing is NOT changed
|
|
1273
|
+
* Accepted forms:
|
|
1288
1274
|
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1275
|
+
* { eye, center, up? }
|
|
1276
|
+
* Explicit lookat. up defaults to [0,1,0] and is normalised on storage.
|
|
1291
1277
|
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1278
|
+
* { view: mat4 }
|
|
1279
|
+
* Column-major view matrix (Float32Array(16), plain Array, or {mat4} wrapper).
|
|
1280
|
+
* eye is extracted from the matrix translation block.
|
|
1281
|
+
* center = eye + forward * 1 (unit distance — sufficient for interpolation).
|
|
1282
|
+
* up defaults to [0,1,0]. The matrix's up_ortho (col1) is intentionally
|
|
1283
|
+
* NOT extracted; using up_ortho as the hint causes orbitControl drift.
|
|
1284
|
+
* If you need to preserve roll from a rolled camera, pass the live
|
|
1285
|
+
* camera's up hint via capturePose() instead of using { view }.
|
|
1295
1286
|
*
|
|
1296
|
-
* @
|
|
1297
|
-
*
|
|
1298
|
-
* track.add({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] })
|
|
1299
|
-
* track.add({ pos:[0,100,0], rot:[0,0,0,1], scl:[1,1,1] })
|
|
1300
|
-
* track.play({ loop: true, onStop: t => console.log('stopped at', t.time()) })
|
|
1287
|
+
* @param {Object} spec
|
|
1288
|
+
* @returns {{ eye:number[], center:number[], up:number[] }|null}
|
|
1301
1289
|
*/
|
|
1302
|
-
|
|
1290
|
+
function _parseCameraSpec(spec) {
|
|
1291
|
+
if (!spec || typeof spec !== 'object') return null;
|
|
1292
|
+
|
|
1293
|
+
// { view } form
|
|
1294
|
+
if (spec.view != null) {
|
|
1295
|
+
const m = (ArrayBuffer.isView(spec.view) || Array.isArray(spec.view))
|
|
1296
|
+
? spec.view
|
|
1297
|
+
: (spec.view.mat4 ?? null);
|
|
1298
|
+
if (!m || m.length < 16) return null;
|
|
1299
|
+
// Extract eye position: eye = -R^T * t
|
|
1300
|
+
// Column-major view mat: col0=right, col1=up_ortho, col2=-fwd, col3=translation
|
|
1301
|
+
const ex = -(m[0]*m[12] + m[4]*m[13] + m[8]*m[14]);
|
|
1302
|
+
const ey = -(m[1]*m[12] + m[5]*m[13] + m[9]*m[14]);
|
|
1303
|
+
const ez = -(m[2]*m[12] + m[6]*m[13] + m[10]*m[14]);
|
|
1304
|
+
// forward = -col2 = [-m[8], -m[9], -m[10]]
|
|
1305
|
+
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1306
|
+
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1307
|
+
// center = eye + normalized_fwd * 1
|
|
1308
|
+
const cx=ex+fx/fl, cy=ey+fy/fl, cz=ez+fz/fl;
|
|
1309
|
+
return { eye:[ex,ey,ez], center:[cx,cy,cz], up:[0,1,0] };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// { eye, center, up? } form
|
|
1313
|
+
const eye = _parseVec3(spec.eye);
|
|
1314
|
+
const center = _parseVec3(spec.center);
|
|
1315
|
+
if (!eye || !center) return null;
|
|
1316
|
+
|
|
1317
|
+
const upRaw = spec.up ? _parseVec3(spec.up) : null;
|
|
1318
|
+
const up = upRaw || [0,1,0];
|
|
1319
|
+
const ul = Math.sqrt(up[0]*up[0]+up[1]*up[1]+up[2]*up[2]) || 1;
|
|
1320
|
+
return { eye, center, up: [up[0]/ul, up[1]/ul, up[2]/ul] };
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function _sameCameraKeyframe(a, b) {
|
|
1324
|
+
for (let i=0;i<3;i++) {
|
|
1325
|
+
if (a.eye[i]!==b.eye[i]) return false;
|
|
1326
|
+
if (a.center[i]!==b.center[i]) return false;
|
|
1327
|
+
if (a.up[i]!==b.up[i]) return false;
|
|
1328
|
+
}
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// =========================================================================
|
|
1333
|
+
// S5 Track — unexported base class (transport machinery only)
|
|
1334
|
+
// =========================================================================
|
|
1335
|
+
|
|
1336
|
+
class Track {
|
|
1303
1337
|
constructor() {
|
|
1304
|
-
/** @type {Array
|
|
1338
|
+
/** @type {Array} Keyframe array — shape depends on subclass. */
|
|
1305
1339
|
this.keyframes = [];
|
|
1306
1340
|
/** Whether playback is active. @type {boolean} */
|
|
1307
1341
|
this.playing = false;
|
|
1308
|
-
/** Loop
|
|
1342
|
+
/** Loop at boundaries. @type {boolean} */
|
|
1309
1343
|
this.loop = false;
|
|
1310
|
-
/** Ping-pong bounce
|
|
1344
|
+
/** Ping-pong bounce (takes precedence over loop). @type {boolean} */
|
|
1311
1345
|
this.pingPong = false;
|
|
1312
|
-
/** Frames per segment (
|
|
1346
|
+
/** Frames per segment (≥1). @type {number} */
|
|
1313
1347
|
this.duration = 30;
|
|
1314
1348
|
/** Current segment index. @type {number} */
|
|
1315
1349
|
this.seg = 0;
|
|
1316
|
-
/** Frame offset within
|
|
1350
|
+
/** Frame offset within segment (can be fractional). @type {number} */
|
|
1317
1351
|
this.f = 0;
|
|
1318
|
-
/**
|
|
1319
|
-
* Position interpolation mode.
|
|
1320
|
-
* @type {'catmullrom'|'linear'}
|
|
1321
|
-
*/
|
|
1322
|
-
this.posInterp = 'catmullrom';
|
|
1323
1352
|
|
|
1324
|
-
//
|
|
1325
|
-
this._pos = [0, 0, 0];
|
|
1326
|
-
this._rot = [0, 0, 0, 1];
|
|
1327
|
-
this._scl = [1, 1, 1];
|
|
1328
|
-
|
|
1329
|
-
// Internal rate — assigning never touches playing
|
|
1353
|
+
// Internal rate — never directly starts/stops playback
|
|
1330
1354
|
this._rate = 1;
|
|
1331
1355
|
|
|
1332
|
-
// User-space hooks
|
|
1333
|
-
/**
|
|
1334
|
-
this.
|
|
1335
|
-
/**
|
|
1336
|
-
this.onEnd = null;
|
|
1337
|
-
/** Fires on explicit stop() or reset(). Mutually exclusive with onEnd per event. @type {Function|null} */
|
|
1338
|
-
this.onStop = null;
|
|
1339
|
-
|
|
1340
|
-
// Lib-space hooks (set by host layer — e.g. p5 bridge)
|
|
1341
|
-
/** @type {Function|null} */
|
|
1342
|
-
this._onActivate = null;
|
|
1343
|
-
/** @type {Function|null} */
|
|
1344
|
-
this._onDeactivate = null;
|
|
1345
|
-
}
|
|
1356
|
+
// User-space hooks
|
|
1357
|
+
/** @type {Function|null} */ this.onPlay = null;
|
|
1358
|
+
/** @type {Function|null} */ this.onEnd = null;
|
|
1359
|
+
/** @type {Function|null} */ this.onStop = null;
|
|
1346
1360
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1361
|
+
// Lib-space hooks (set by host layer, e.g. p5 bridge)
|
|
1362
|
+
/** @type {Function|null} */ this._onActivate = null;
|
|
1363
|
+
/** @type {Function|null} */ this._onDeactivate = null;
|
|
1364
|
+
}
|
|
1350
1365
|
|
|
1351
|
-
/** Playback rate.
|
|
1366
|
+
/** Playback rate. Assigning never starts/stops playback. @type {number} */
|
|
1352
1367
|
get rate() { return this._rate; }
|
|
1353
|
-
set rate(v) {
|
|
1354
|
-
this._rate = (typeof v === 'number' && Number.isFinite(v)) ? v : 1;
|
|
1355
|
-
// Intentionally does NOT start or stop playback.
|
|
1356
|
-
}
|
|
1368
|
+
set rate(v) { this._rate = (_isNum(v)) ? v : 1; }
|
|
1357
1369
|
|
|
1358
|
-
/** Number of interpolatable segments (keyframes.length
|
|
1370
|
+
/** Number of interpolatable segments (keyframes.length − 1, min 0). @type {number} */
|
|
1359
1371
|
get segments() { return Math.max(0, this.keyframes.length - 1); }
|
|
1360
1372
|
|
|
1361
|
-
// ── Keyframe management ──────────────────────────────────────────────────
|
|
1362
|
-
|
|
1363
|
-
/**
|
|
1364
|
-
* Append a keyframe. Adjacent duplicates are skipped by default.
|
|
1365
|
-
* @param {{ pos?, rot?, scl? }} spec pos/rot/scl arrays, {x,y,z}, axis-angle, look-dir.
|
|
1366
|
-
* @param {{ deduplicate?: boolean }} [opts]
|
|
1367
|
-
*/
|
|
1368
|
-
add(spec, opts) {
|
|
1369
|
-
const kf = _parseSpec(spec);
|
|
1370
|
-
if (!kf) return;
|
|
1371
|
-
const dedup = !opts || opts.deduplicate !== false;
|
|
1372
|
-
if (dedup && this.keyframes.length > 0) {
|
|
1373
|
-
if (_sameTransform(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1374
|
-
}
|
|
1375
|
-
this.keyframes.push(kf);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
/**
|
|
1379
|
-
* Replace (or append at end) the keyframe at index.
|
|
1380
|
-
* @param {number} index Existing index or keyframes.length to append.
|
|
1381
|
-
* @param {{ pos?, rot?, scl? }} spec
|
|
1382
|
-
* @returns {boolean}
|
|
1383
|
-
*/
|
|
1384
|
-
set(index, spec) {
|
|
1385
|
-
if (!_isNum(index)) return false;
|
|
1386
|
-
const i = index | 0;
|
|
1387
|
-
const kf = _parseSpec(spec);
|
|
1388
|
-
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1389
|
-
if (i === this.keyframes.length) { this.keyframes.push(kf); }
|
|
1390
|
-
else { this.keyframes[i] = kf; }
|
|
1391
|
-
return true;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
/**
|
|
1395
|
-
* Remove the keyframe at index. Adjusts cursor if needed.
|
|
1396
|
-
* @param {number} index
|
|
1397
|
-
* @returns {boolean}
|
|
1398
|
-
*/
|
|
1399
|
-
remove(index) {
|
|
1400
|
-
if (!_isNum(index)) return false;
|
|
1401
|
-
const i = index | 0;
|
|
1402
|
-
if (i < 0 || i >= this.keyframes.length) return false;
|
|
1403
|
-
this.keyframes.splice(i, 1);
|
|
1404
|
-
const nSeg = this.segments;
|
|
1405
|
-
if (nSeg === 0) { this.seg = 0; this.f = 0; }
|
|
1406
|
-
else if (this.seg >= nSeg) { this.seg = nSeg - 1; }
|
|
1407
|
-
return true;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// ── Transport ────────────────────────────────────────────────────────────
|
|
1411
|
-
|
|
1412
1373
|
/**
|
|
1413
1374
|
* Start or update playback.
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
* Zero keyframes: no-op.
|
|
1418
|
-
* One keyframe: snaps cursor (seg=0, f=0); no playing=true, no hooks.
|
|
1419
|
-
* Already playing: updates params in place; hooks are not re-fired.
|
|
1420
|
-
* rate=0 is valid: track will be playing but frozen until rate changes.
|
|
1421
|
-
*
|
|
1422
|
-
* @param {number|Object} [rateOrOpts]
|
|
1423
|
-
* @returns {PoseTrack} this
|
|
1375
|
+
* @param {number|Object} [rateOrOpts] Numeric rate or options object:
|
|
1376
|
+
* { rate, duration, loop, pingPong, onPlay, onEnd, onStop }
|
|
1377
|
+
* @returns {Track} this
|
|
1424
1378
|
*/
|
|
1425
1379
|
play(rateOrOpts) {
|
|
1426
1380
|
if (this.keyframes.length === 0) return this;
|
|
1427
1381
|
|
|
1428
|
-
// One keyframe: snap
|
|
1382
|
+
// One keyframe: snap cursor, no animation
|
|
1429
1383
|
if (this.keyframes.length === 1) {
|
|
1430
1384
|
this.seg = 0; this.f = 0;
|
|
1431
1385
|
return this;
|
|
@@ -1444,9 +1398,7 @@ class PoseTrack {
|
|
|
1444
1398
|
if (_isNum(o.rate)) this._rate = o.rate;
|
|
1445
1399
|
}
|
|
1446
1400
|
|
|
1447
|
-
|
|
1448
|
-
const nSeg = this.segments;
|
|
1449
|
-
const dur = Math.max(1, this.duration | 0);
|
|
1401
|
+
const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
|
|
1450
1402
|
if (this.seg < 0) this.seg = 0;
|
|
1451
1403
|
if (this.seg >= nSeg) this.seg = nSeg - 1;
|
|
1452
1404
|
if (this.f < 0) this.f = 0;
|
|
@@ -1454,7 +1406,6 @@ class PoseTrack {
|
|
|
1454
1406
|
|
|
1455
1407
|
const wasPlaying = this.playing;
|
|
1456
1408
|
this.playing = true;
|
|
1457
|
-
|
|
1458
1409
|
if (!wasPlaying) {
|
|
1459
1410
|
if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
|
|
1460
1411
|
this._onActivate?.();
|
|
@@ -1463,11 +1414,9 @@ class PoseTrack {
|
|
|
1463
1414
|
}
|
|
1464
1415
|
|
|
1465
1416
|
/**
|
|
1466
|
-
* Stop playback.
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
1469
|
-
* @param {boolean} [rewind=false] Seek to playback origin after stopping.
|
|
1470
|
-
* @returns {PoseTrack} this
|
|
1417
|
+
* Stop playback.
|
|
1418
|
+
* @param {boolean} [rewind=false] Seek to origin after stopping.
|
|
1419
|
+
* @returns {Track} this
|
|
1471
1420
|
*/
|
|
1472
1421
|
stop(rewind) {
|
|
1473
1422
|
const wasPlaying = this.playing;
|
|
@@ -1482,8 +1431,7 @@ class PoseTrack {
|
|
|
1482
1431
|
|
|
1483
1432
|
/**
|
|
1484
1433
|
* Clear all keyframes and stop.
|
|
1485
|
-
*
|
|
1486
|
-
* @returns {PoseTrack} this
|
|
1434
|
+
* @returns {Track} this
|
|
1487
1435
|
*/
|
|
1488
1436
|
reset() {
|
|
1489
1437
|
const wasPlaying = this.playing;
|
|
@@ -1497,13 +1445,27 @@ class PoseTrack {
|
|
|
1497
1445
|
return this;
|
|
1498
1446
|
}
|
|
1499
1447
|
|
|
1448
|
+
/**
|
|
1449
|
+
* Remove the keyframe at index. Adjusts cursor if needed.
|
|
1450
|
+
* @param {number} index
|
|
1451
|
+
* @returns {boolean}
|
|
1452
|
+
*/
|
|
1453
|
+
remove(index) {
|
|
1454
|
+
if (!_isNum(index)) return false;
|
|
1455
|
+
const i = index | 0;
|
|
1456
|
+
if (i < 0 || i >= this.keyframes.length) return false;
|
|
1457
|
+
this.keyframes.splice(i, 1);
|
|
1458
|
+
const nSeg = this.segments;
|
|
1459
|
+
if (nSeg === 0) { this.seg = 0; this.f = 0; }
|
|
1460
|
+
else if (this.seg >= nSeg) { this.seg = nSeg - 1; }
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1500
1464
|
/**
|
|
1501
1465
|
* Seek to a normalised position [0,1] across the full path.
|
|
1502
|
-
* Can optionally target a specific segment (t is then local to that segment).
|
|
1503
|
-
* Does not change the playing flag.
|
|
1504
1466
|
* @param {number} t Normalised time [0, 1].
|
|
1505
1467
|
* @param {number} [segIndex] Optional segment override.
|
|
1506
|
-
* @returns {
|
|
1468
|
+
* @returns {Track} this
|
|
1507
1469
|
*/
|
|
1508
1470
|
seek(t, segIndex) {
|
|
1509
1471
|
const nSeg = this.segments;
|
|
@@ -1519,8 +1481,7 @@ class PoseTrack {
|
|
|
1519
1481
|
}
|
|
1520
1482
|
|
|
1521
1483
|
/**
|
|
1522
|
-
* Normalised playback
|
|
1523
|
-
* Returns 0 when fewer than 2 keyframes exist.
|
|
1484
|
+
* Normalised playback position [0,1].
|
|
1524
1485
|
* @returns {number}
|
|
1525
1486
|
*/
|
|
1526
1487
|
time() {
|
|
@@ -1531,10 +1492,8 @@ class PoseTrack {
|
|
|
1531
1492
|
}
|
|
1532
1493
|
|
|
1533
1494
|
/**
|
|
1534
|
-
* Snapshot of
|
|
1535
|
-
* @returns {
|
|
1536
|
-
* time:number, playing:boolean, loop:boolean, pingPong:boolean,
|
|
1537
|
-
* rate:number, duration:number }}
|
|
1495
|
+
* Snapshot of transport state.
|
|
1496
|
+
* @returns {Object}
|
|
1538
1497
|
*/
|
|
1539
1498
|
info() {
|
|
1540
1499
|
return {
|
|
@@ -1552,12 +1511,8 @@ class PoseTrack {
|
|
|
1552
1511
|
}
|
|
1553
1512
|
|
|
1554
1513
|
/**
|
|
1555
|
-
* Advance
|
|
1556
|
-
*
|
|
1557
|
-
* rate === 0: frozen — returns this.playing without moving (no-op).
|
|
1558
|
-
* Returns false and fires onEnd → _onDeactivate when a once-mode boundary is hit.
|
|
1559
|
-
* Returns true while playing and continuing.
|
|
1560
|
-
*
|
|
1514
|
+
* Advance cursor by rate frames.
|
|
1515
|
+
* Returns true while playing, false when stopping.
|
|
1561
1516
|
* @returns {boolean}
|
|
1562
1517
|
*/
|
|
1563
1518
|
tick() {
|
|
@@ -1566,8 +1521,6 @@ class PoseTrack {
|
|
|
1566
1521
|
if (nSeg === 0) {
|
|
1567
1522
|
this.playing = false; this._onDeactivate?.(); return false;
|
|
1568
1523
|
}
|
|
1569
|
-
|
|
1570
|
-
// Frozen: position does not advance, playing stays true
|
|
1571
1524
|
if (this._rate === 0) return true;
|
|
1572
1525
|
|
|
1573
1526
|
const dur = Math.max(1, this.duration | 0);
|
|
@@ -1575,25 +1528,22 @@ class PoseTrack {
|
|
|
1575
1528
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1576
1529
|
const next = s + this._rate;
|
|
1577
1530
|
|
|
1578
|
-
// ── pingPong ──
|
|
1579
1531
|
if (this.pingPong) {
|
|
1580
1532
|
let pos = next, flips = 0;
|
|
1581
1533
|
while (pos < 0 || pos > total) {
|
|
1582
|
-
if (pos < 0)
|
|
1583
|
-
else
|
|
1534
|
+
if (pos < 0) { pos = -pos; flips++; }
|
|
1535
|
+
else { pos = 2 * total - pos; flips++; }
|
|
1584
1536
|
}
|
|
1585
1537
|
if (flips & 1) this._rate = -this._rate;
|
|
1586
1538
|
this._setCursorFromScalar(pos);
|
|
1587
1539
|
return true;
|
|
1588
1540
|
}
|
|
1589
1541
|
|
|
1590
|
-
// ── loop ──
|
|
1591
1542
|
if (this.loop) {
|
|
1592
1543
|
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1593
1544
|
return true;
|
|
1594
1545
|
}
|
|
1595
1546
|
|
|
1596
|
-
// ── once — boundary check ──
|
|
1597
1547
|
if (next <= 0) {
|
|
1598
1548
|
this._setCursorFromScalar(0);
|
|
1599
1549
|
this.playing = false;
|
|
@@ -1613,15 +1563,112 @@ class PoseTrack {
|
|
|
1613
1563
|
return true;
|
|
1614
1564
|
}
|
|
1615
1565
|
|
|
1566
|
+
/** @private */
|
|
1567
|
+
_setCursorFromScalar(s) {
|
|
1568
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1569
|
+
const nSeg = this.segments;
|
|
1570
|
+
this.seg = Math.floor(s / dur);
|
|
1571
|
+
this.f = s - this.seg * dur;
|
|
1572
|
+
if (this.seg >= nSeg) { this.seg = nSeg - 1; this.f = dur; }
|
|
1573
|
+
if (this.seg < 0) { this.seg = 0; this.f = 0; }
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// =========================================================================
|
|
1578
|
+
// S6 PoseTrack
|
|
1579
|
+
// =========================================================================
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Renderer-agnostic TRS keyframe track.
|
|
1583
|
+
*
|
|
1584
|
+
* Keyframe shape: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z] }
|
|
1585
|
+
*
|
|
1586
|
+
* add() accepts individual specs or a bulk array of specs:
|
|
1587
|
+
* { pos, rot, scl } direct TRS
|
|
1588
|
+
* { pos, rot: [x,y,z,w] } explicit quaternion
|
|
1589
|
+
* { pos, rot: { axis, angle } } axis-angle
|
|
1590
|
+
* { pos, rot: { dir, up? } } look direction (object orientation)
|
|
1591
|
+
* { pos, rot: { view: mat4 } } from view matrix rotation block
|
|
1592
|
+
* { pos, rot: { eye, center, up? } } lookat shorthand
|
|
1593
|
+
* [ spec, spec, ... ] bulk
|
|
1594
|
+
*
|
|
1595
|
+
* eval() writes { pos, rot, scl }:
|
|
1596
|
+
* pos — Catmull-Rom (posInterp='catmullrom') or lerp
|
|
1597
|
+
* rot — slerp (rotInterp='slerp') or nlerp
|
|
1598
|
+
* scl — lerp
|
|
1599
|
+
*
|
|
1600
|
+
* @example
|
|
1601
|
+
* const track = new PoseTrack()
|
|
1602
|
+
* track.add({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] })
|
|
1603
|
+
* track.add({ pos:[100,0,0], rot:[0,0,0,1], scl:[1,1,1] })
|
|
1604
|
+
* track.play({ loop: true })
|
|
1605
|
+
* // per frame:
|
|
1606
|
+
* track.tick()
|
|
1607
|
+
* const out = { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }
|
|
1608
|
+
* track.eval(out)
|
|
1609
|
+
*/
|
|
1610
|
+
class PoseTrack extends Track {
|
|
1611
|
+
constructor() {
|
|
1612
|
+
super();
|
|
1613
|
+
/**
|
|
1614
|
+
* Position interpolation mode.
|
|
1615
|
+
* @type {'catmullrom'|'linear'}
|
|
1616
|
+
*/
|
|
1617
|
+
this.posInterp = 'catmullrom';
|
|
1618
|
+
/**
|
|
1619
|
+
* Rotation interpolation mode.
|
|
1620
|
+
* - 'slerp' — constant angular velocity (default)
|
|
1621
|
+
* - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
|
|
1622
|
+
* @type {'slerp'|'nlerp'}
|
|
1623
|
+
*/
|
|
1624
|
+
this.rotInterp = 'slerp';
|
|
1625
|
+
// Scratch for toMatrix() — avoids hot-path allocations
|
|
1626
|
+
this._pos = [0,0,0];
|
|
1627
|
+
this._rot = [0,0,0,1];
|
|
1628
|
+
this._scl = [1,1,1];
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Append one or more keyframes. Adjacent duplicates are skipped by default.
|
|
1633
|
+
* @param {Object|Object[]} spec
|
|
1634
|
+
* @param {{ deduplicate?: boolean }} [opts]
|
|
1635
|
+
*/
|
|
1636
|
+
add(spec, opts) {
|
|
1637
|
+
if (Array.isArray(spec)) {
|
|
1638
|
+
for (const s of spec) this.add(s, opts);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const kf = _parseSpec(spec);
|
|
1642
|
+
if (!kf) return;
|
|
1643
|
+
const dedup = !opts || opts.deduplicate !== false;
|
|
1644
|
+
if (dedup && this.keyframes.length > 0) {
|
|
1645
|
+
if (_sameTransform(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1646
|
+
}
|
|
1647
|
+
this.keyframes.push(kf);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Replace (or append at end) the keyframe at index.
|
|
1652
|
+
* @param {number} index
|
|
1653
|
+
* @param {Object} spec
|
|
1654
|
+
* @returns {boolean}
|
|
1655
|
+
*/
|
|
1656
|
+
set(index, spec) {
|
|
1657
|
+
if (!_isNum(index)) return false;
|
|
1658
|
+
const i = index | 0, kf = _parseSpec(spec);
|
|
1659
|
+
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1660
|
+
if (i === this.keyframes.length) this.keyframes.push(kf);
|
|
1661
|
+
else this.keyframes[i] = kf;
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1616
1665
|
/**
|
|
1617
|
-
* Evaluate
|
|
1618
|
-
* If out is omitted a new object is allocated (avoid in hot paths).
|
|
1619
|
-
* Uses centripetal Catmull-Rom for position (posInterp === 'catmullrom') or lerp.
|
|
1666
|
+
* Evaluate interpolated TRS pose at current cursor.
|
|
1620
1667
|
* @param {{ pos:number[], rot:number[], scl:number[] }} [out]
|
|
1621
1668
|
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1622
1669
|
*/
|
|
1623
1670
|
eval(out) {
|
|
1624
|
-
out = out || { pos:
|
|
1671
|
+
out = out || { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
|
|
1625
1672
|
const n = this.keyframes.length;
|
|
1626
1673
|
if (n === 0) return out;
|
|
1627
1674
|
|
|
@@ -1640,27 +1687,30 @@ class PoseTrack {
|
|
|
1640
1687
|
const k0 = this.keyframes[seg];
|
|
1641
1688
|
const k1 = this.keyframes[seg + 1];
|
|
1642
1689
|
|
|
1643
|
-
//
|
|
1690
|
+
// pos — Catmull-Rom or lerp
|
|
1644
1691
|
if (this.posInterp === 'catmullrom') {
|
|
1645
|
-
const p0 = seg > 0
|
|
1692
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1646
1693
|
const p3 = seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1647
1694
|
catmullRomVec3(out.pos, p0, k0.pos, k1.pos, p3, t);
|
|
1648
1695
|
} else {
|
|
1649
1696
|
lerpVec3(out.pos, k0.pos, k1.pos, t);
|
|
1650
1697
|
}
|
|
1651
1698
|
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1699
|
+
// rot — slerp or nlerp
|
|
1700
|
+
if (this.rotInterp === 'nlerp') {
|
|
1701
|
+
qNlerp(out.rot, k0.rot, k1.rot, t);
|
|
1702
|
+
} else {
|
|
1703
|
+
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
1704
|
+
}
|
|
1654
1705
|
|
|
1655
|
-
//
|
|
1706
|
+
// scl — lerp
|
|
1656
1707
|
lerpVec3(out.scl, k0.scl, k1.scl, t);
|
|
1657
1708
|
|
|
1658
1709
|
return out;
|
|
1659
1710
|
}
|
|
1660
1711
|
|
|
1661
1712
|
/**
|
|
1662
|
-
* Evaluate
|
|
1663
|
-
* Reuses internal scratch arrays — no allocation per call.
|
|
1713
|
+
* Evaluate into an existing column-major mat4.
|
|
1664
1714
|
* @param {Float32Array|number[]} outMat4 16-element array.
|
|
1665
1715
|
* @returns {Float32Array|number[]} outMat4
|
|
1666
1716
|
*/
|
|
@@ -1668,17 +1718,161 @@ class PoseTrack {
|
|
|
1668
1718
|
const xf = this.eval({ pos: this._pos, rot: this._rot, scl: this._scl });
|
|
1669
1719
|
return transformToMat4(outMat4, xf);
|
|
1670
1720
|
}
|
|
1721
|
+
}
|
|
1671
1722
|
|
|
1672
|
-
|
|
1723
|
+
// =========================================================================
|
|
1724
|
+
// S7 CameraTrack
|
|
1725
|
+
// =========================================================================
|
|
1673
1726
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1727
|
+
/**
|
|
1728
|
+
* Lookat camera keyframe track.
|
|
1729
|
+
*
|
|
1730
|
+
* Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z] }
|
|
1731
|
+
*
|
|
1732
|
+
* Each field is independently interpolated — eye and center along their
|
|
1733
|
+
* own paths, up nlerped on the unit sphere. This correctly handles cameras
|
|
1734
|
+
* that always look at a fixed target (center stays at origin throughout)
|
|
1735
|
+
* as well as free-fly paths where center moves independently.
|
|
1736
|
+
*
|
|
1737
|
+
* add() accepts individual specs or a bulk array of specs:
|
|
1738
|
+
* { eye, center, up? } explicit lookat; up defaults to [0,1,0]
|
|
1739
|
+
* { view: mat4 } view matrix; eye extracted, center = eye+fwd*1,
|
|
1740
|
+
* up = [0,1,0] (safe default — see note below)
|
|
1741
|
+
* [ spec, spec, ... ] bulk
|
|
1742
|
+
*
|
|
1743
|
+
* Note on up for { view: mat4 }:
|
|
1744
|
+
* The view matrix's col1 (up_ortho) is intentionally not used as up.
|
|
1745
|
+
* For upright cameras up_ortho differs from the hint [0,1,0], and
|
|
1746
|
+
* passing it to cam.camera() would shift orbitControl's orbit reference.
|
|
1747
|
+
* Use capturePose() when you need to preserve the real up hint.
|
|
1748
|
+
*
|
|
1749
|
+
* eval() writes { eye, center, up }:
|
|
1750
|
+
* eye — Catmull-Rom (eyeInterp='catmullrom') or lerp
|
|
1751
|
+
* center — Catmull-Rom (centerInterp='catmullrom') or lerp
|
|
1752
|
+
* up — nlerp (normalize-after-lerp on unit sphere)
|
|
1753
|
+
*
|
|
1754
|
+
* @example
|
|
1755
|
+
* const track = new CameraTrack()
|
|
1756
|
+
* track.add({ eye:[0,0,500], center:[0,0,0] })
|
|
1757
|
+
* track.add({ eye:[300,-150,0], center:[0,0,0] })
|
|
1758
|
+
* track.play({ loop: true })
|
|
1759
|
+
* // per frame:
|
|
1760
|
+
* track.tick()
|
|
1761
|
+
* const out = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] }
|
|
1762
|
+
* track.eval(out)
|
|
1763
|
+
* cam.camera(out.eye[0],out.eye[1],out.eye[2],
|
|
1764
|
+
* out.center[0],out.center[1],out.center[2],
|
|
1765
|
+
* out.up[0],out.up[1],out.up[2])
|
|
1766
|
+
*/
|
|
1767
|
+
class CameraTrack extends Track {
|
|
1768
|
+
constructor() {
|
|
1769
|
+
super();
|
|
1770
|
+
/**
|
|
1771
|
+
* Eye position interpolation mode.
|
|
1772
|
+
* @type {'catmullrom'|'linear'}
|
|
1773
|
+
*/
|
|
1774
|
+
this.eyeInterp = 'catmullrom';
|
|
1775
|
+
/**
|
|
1776
|
+
* Center (lookat target) interpolation mode.
|
|
1777
|
+
* 'linear' suits fixed or predictably moving targets.
|
|
1778
|
+
* 'catmullrom' gives smoother paths when center is also flying freely.
|
|
1779
|
+
* @type {'catmullrom'|'linear'}
|
|
1780
|
+
*/
|
|
1781
|
+
this.centerInterp = 'linear';
|
|
1782
|
+
// Scratch for toCamera() — avoids hot-path allocations
|
|
1783
|
+
this._eye = [0,0,0];
|
|
1784
|
+
this._center = [0,0,0];
|
|
1785
|
+
this._up = [0,1,0];
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Append one or more camera keyframes. Adjacent duplicates are skipped by default.
|
|
1790
|
+
*
|
|
1791
|
+
* @param {Object|Object[]} spec
|
|
1792
|
+
* { eye, center, up? } or { view: mat4 } or an array of either.
|
|
1793
|
+
* @param {{ deduplicate?: boolean }} [opts]
|
|
1794
|
+
*/
|
|
1795
|
+
add(spec, opts) {
|
|
1796
|
+
if (Array.isArray(spec)) {
|
|
1797
|
+
for (const s of spec) this.add(s, opts);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const kf = _parseCameraSpec(spec);
|
|
1801
|
+
if (!kf) return;
|
|
1802
|
+
const dedup = !opts || opts.deduplicate !== false;
|
|
1803
|
+
if (dedup && this.keyframes.length > 0) {
|
|
1804
|
+
if (_sameCameraKeyframe(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1805
|
+
}
|
|
1806
|
+
this.keyframes.push(kf);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Replace (or append at end) the keyframe at index.
|
|
1811
|
+
* @param {number} index
|
|
1812
|
+
* @param {Object} spec
|
|
1813
|
+
* @returns {boolean}
|
|
1814
|
+
*/
|
|
1815
|
+
set(index, spec) {
|
|
1816
|
+
if (!_isNum(index)) return false;
|
|
1817
|
+
const i = index | 0, kf = _parseCameraSpec(spec);
|
|
1818
|
+
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1819
|
+
if (i === this.keyframes.length) this.keyframes.push(kf);
|
|
1820
|
+
else this.keyframes[i] = kf;
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Evaluate interpolated camera pose at current cursor.
|
|
1826
|
+
*
|
|
1827
|
+
* @param {{ eye:number[], center:number[], up:number[] }} [out]
|
|
1828
|
+
* @returns {{ eye:number[], center:number[], up:number[] }} out
|
|
1829
|
+
*/
|
|
1830
|
+
eval(out) {
|
|
1831
|
+
out = out || { eye:[0,0,0], center:[0,0,0], up:[0,1,0] };
|
|
1832
|
+
const n = this.keyframes.length;
|
|
1833
|
+
if (n === 0) return out;
|
|
1834
|
+
|
|
1835
|
+
if (n === 1) {
|
|
1836
|
+
const k = this.keyframes[0];
|
|
1837
|
+
out.eye[0]=k.eye[0]; out.eye[1]=k.eye[1]; out.eye[2]=k.eye[2];
|
|
1838
|
+
out.center[0]=k.center[0]; out.center[1]=k.center[1]; out.center[2]=k.center[2];
|
|
1839
|
+
out.up[0]=k.up[0]; out.up[1]=k.up[1]; out.up[2]=k.up[2];
|
|
1840
|
+
return out;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const nSeg = n - 1;
|
|
1676
1844
|
const dur = Math.max(1, this.duration | 0);
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1845
|
+
const seg = _clampS(this.seg, 0, nSeg - 1);
|
|
1846
|
+
const t = _clamp01(this.f / dur);
|
|
1847
|
+
const k0 = this.keyframes[seg];
|
|
1848
|
+
const k1 = this.keyframes[seg + 1];
|
|
1849
|
+
|
|
1850
|
+
// eye — Catmull-Rom or lerp
|
|
1851
|
+
if (this.eyeInterp === 'catmullrom') {
|
|
1852
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].eye : k0.eye;
|
|
1853
|
+
const p3 = seg + 2 < n ? this.keyframes[seg + 2].eye : k1.eye;
|
|
1854
|
+
catmullRomVec3(out.eye, p0, k0.eye, k1.eye, p3, t);
|
|
1855
|
+
} else {
|
|
1856
|
+
lerpVec3(out.eye, k0.eye, k1.eye, t);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// center — Catmull-Rom or lerp (independent lookat target)
|
|
1860
|
+
if (this.centerInterp === 'catmullrom') {
|
|
1861
|
+
const c0 = seg > 0 ? this.keyframes[seg - 1].center : k0.center;
|
|
1862
|
+
const c3 = seg + 2 < n ? this.keyframes[seg + 2].center : k1.center;
|
|
1863
|
+
catmullRomVec3(out.center, c0, k0.center, k1.center, c3, t);
|
|
1864
|
+
} else {
|
|
1865
|
+
lerpVec3(out.center, k0.center, k1.center, t);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// up — nlerp (normalize after lerp; correct for typical near-upright cameras)
|
|
1869
|
+
const ux = k0.up[0] + t*(k1.up[0]-k0.up[0]);
|
|
1870
|
+
const uy = k0.up[1] + t*(k1.up[1]-k0.up[1]);
|
|
1871
|
+
const uz = k0.up[2] + t*(k1.up[2]-k0.up[2]);
|
|
1872
|
+
const ul = Math.sqrt(ux*ux+uy*uy+uz*uz) || 1;
|
|
1873
|
+
out.up[0]=ux/ul; out.up[1]=uy/ul; out.up[2]=uz/ul;
|
|
1874
|
+
|
|
1875
|
+
return out;
|
|
1682
1876
|
}
|
|
1683
1877
|
}
|
|
1684
1878
|
|
|
@@ -1839,5 +2033,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
1839
2033
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
1840
2034
|
}
|
|
1841
2035
|
|
|
1842
|
-
export { EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, boxVisibility, catmullRomVec3, distanceToPlane, frustumPlanes, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3FromMat4T, mat3MulVec3, mat3NormalFromMat4, mat4Identity, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4PMV, mat4PV, mat4ToTransform, mat4Transpose, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNormalize, qSet, qSlerp, qToMat4, quatToAxisAngle, sphereVisibility, transformToMat4 };
|
|
2036
|
+
export { CameraTrack, EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, boxVisibility, catmullRomVec3, distanceToPlane, frustumPlanes, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3FromMat4T, mat3MulVec3, mat3NormalFromMat4, mat4Identity, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4PMV, mat4PV, mat4ToTransform, mat4Transpose, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNlerp, qNormalize, qSet, qSlerp, qToMat4, quatToAxisAngle, sphereVisibility, transformToMat4 };
|
|
1843
2037
|
//# sourceMappingURL=index.js.map
|