@nakednous/tree 0.0.1 → 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 +566 -348
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -881,55 +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
|
|
911
|
-
* onPlay : fires in play()
|
|
912
|
-
* onEnd : fires in tick()
|
|
913
|
-
*
|
|
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.
|
|
914
921
|
*
|
|
915
922
|
* Firing order:
|
|
916
|
-
* play() →
|
|
917
|
-
* tick() →
|
|
918
|
-
* stop() →
|
|
919
|
-
* reset() →
|
|
923
|
+
* play() → onPlay → _onActivate
|
|
924
|
+
* tick() → onEnd → _onDeactivate
|
|
925
|
+
* stop() → onStop → _onDeactivate
|
|
926
|
+
* reset() → onStop → _onDeactivate
|
|
920
927
|
*
|
|
921
928
|
* ── Playback semantics (rate) ─────────────────────────────────────────────────
|
|
922
|
-
* rate > 0 forward
|
|
923
|
-
* rate < 0 backward
|
|
924
|
-
* rate === 0 frozen: tick()
|
|
929
|
+
* rate > 0 forward
|
|
930
|
+
* rate < 0 backward
|
|
931
|
+
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
925
932
|
*
|
|
926
|
-
* play()
|
|
927
|
-
* stop()
|
|
928
|
-
* 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.
|
|
929
936
|
*
|
|
930
937
|
* ── One-keyframe behaviour ────────────────────────────────────────────────────
|
|
931
938
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
932
|
-
* setting playing = true and without
|
|
939
|
+
* setting playing = true and without firing hooks.
|
|
933
940
|
*/
|
|
934
941
|
|
|
935
942
|
|
|
@@ -950,174 +957,155 @@ const qCopy = (out, a) => {
|
|
|
950
957
|
/** Dot product of two quaternions. */
|
|
951
958
|
const qDot = (a, b) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
952
959
|
|
|
953
|
-
/** Normalise in-place. @returns {number[]} out */
|
|
960
|
+
/** Normalise quaternion in-place. @returns {number[]} out */
|
|
954
961
|
const qNormalize = (out) => {
|
|
955
|
-
const
|
|
956
|
-
out[0]
|
|
957
|
-
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;
|
|
958
964
|
};
|
|
959
965
|
|
|
960
|
-
/** Negate
|
|
961
|
-
const qNegate = (out) => {
|
|
962
|
-
out[0]
|
|
963
|
-
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;
|
|
964
969
|
};
|
|
965
970
|
|
|
966
|
-
/** out = a * b
|
|
971
|
+
/** Hamilton product out = a * b. @returns {number[]} out */
|
|
967
972
|
const qMul = (out, a, b) => {
|
|
968
|
-
const ax
|
|
969
|
-
|
|
970
|
-
out[
|
|
971
|
-
out[
|
|
972
|
-
out[
|
|
973
|
-
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;
|
|
974
978
|
return out;
|
|
975
979
|
};
|
|
976
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
|
+
|
|
977
998
|
/**
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
* @param {number[]} out 4-element result array.
|
|
982
|
-
* @param {number[]} a Start quaternion [x,y,z,w].
|
|
983
|
-
* @param {number[]} b End quaternion [x,y,z,w].
|
|
984
|
-
* @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.
|
|
985
1002
|
* @returns {number[]} out
|
|
986
1003
|
*/
|
|
987
|
-
const
|
|
988
|
-
let
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
out[1] = a[1] + t*(by - a[1]);
|
|
994
|
-
out[2] = a[2] + t*(bz - a[2]);
|
|
995
|
-
out[3] = a[3] + t*(bw - a[3]);
|
|
996
|
-
return qNormalize(out);
|
|
997
|
-
}
|
|
998
|
-
const theta = Math.acos(d), sinT = Math.sin(theta);
|
|
999
|
-
const s0 = Math.sin((1 - t) * theta) / sinT;
|
|
1000
|
-
const s1 = Math.sin(t * theta) / sinT;
|
|
1001
|
-
out[0] = s0*a[0] + s1*bx;
|
|
1002
|
-
out[1] = s0*a[1] + s1*by;
|
|
1003
|
-
out[2] = s0*a[2] + s1*bz;
|
|
1004
|
-
out[3] = s0*a[3] + s1*bw;
|
|
1005
|
-
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);
|
|
1006
1010
|
};
|
|
1007
1011
|
|
|
1008
1012
|
/**
|
|
1009
|
-
* Build a quaternion from
|
|
1010
|
-
*
|
|
1013
|
+
* Build a quaternion from axis-angle.
|
|
1014
|
+
* @param {number[]} out
|
|
1015
|
+
* @param {number} ax @param {number} ay @param {number} az Axis (need not be unit).
|
|
1016
|
+
* @param {number} angle Radians.
|
|
1011
1017
|
* @returns {number[]} out
|
|
1012
1018
|
*/
|
|
1013
1019
|
const qFromAxisAngle = (out, ax, ay, az, angle) => {
|
|
1014
1020
|
const half = angle * 0.5;
|
|
1015
1021
|
const s = Math.sin(half);
|
|
1016
1022
|
const len = Math.sqrt(ax*ax + ay*ay + az*az) || 1;
|
|
1017
|
-
out[0] = s * ax / len;
|
|
1018
|
-
out[1] = s * ay / len;
|
|
1019
|
-
out[2] = s * az / len;
|
|
1023
|
+
out[0] = s * ax / len; out[1] = s * ay / len; out[2] = s * az / len;
|
|
1020
1024
|
out[3] = Math.cos(half);
|
|
1021
1025
|
return out;
|
|
1022
1026
|
};
|
|
1023
1027
|
|
|
1024
1028
|
/**
|
|
1025
|
-
* Build a quaternion from a look direction (
|
|
1026
|
-
* and an optional up vector (defaults to +Y).
|
|
1029
|
+
* Build a quaternion from a look direction (−Z forward) and optional up (default +Y).
|
|
1027
1030
|
* @param {number[]} out
|
|
1028
1031
|
* @param {number[]} dir Forward direction [x,y,z].
|
|
1029
1032
|
* @param {number[]} [up] Up vector [x,y,z].
|
|
1030
1033
|
* @returns {number[]} out
|
|
1031
1034
|
*/
|
|
1032
1035
|
const qFromLookDir = (out, dir, up) => {
|
|
1033
|
-
let fx
|
|
1034
|
-
const
|
|
1035
|
-
fx
|
|
1036
|
-
let ux
|
|
1037
|
-
let rx
|
|
1038
|
-
const
|
|
1039
|
-
rx
|
|
1040
|
-
ux
|
|
1041
|
-
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);
|
|
1042
1045
|
};
|
|
1043
1046
|
|
|
1044
1047
|
/**
|
|
1045
|
-
* Build a quaternion from a
|
|
1048
|
+
* Build a quaternion from a 3×3 rotation matrix (9 row-major scalars).
|
|
1046
1049
|
* @returns {number[]} out (normalised)
|
|
1047
1050
|
*/
|
|
1048
|
-
const qFromRotMat3x3 = (out, m00,
|
|
1049
|
-
const tr = m00
|
|
1051
|
+
const qFromRotMat3x3 = (out, m00,m01,m02, m10,m11,m12, m20,m21,m22) => {
|
|
1052
|
+
const tr = m00+m11+m22;
|
|
1050
1053
|
if (tr > 0) {
|
|
1051
|
-
const s
|
|
1052
|
-
out[3]
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
out[
|
|
1056
|
-
} else if (
|
|
1057
|
-
const s
|
|
1058
|
-
out[3] =
|
|
1059
|
-
out[0] = 0.25 * s;
|
|
1060
|
-
out[1] = (m01 + m10) / s;
|
|
1061
|
-
out[2] = (m02 + m20) / s;
|
|
1062
|
-
} else if (m11 > m22) {
|
|
1063
|
-
const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
|
1064
|
-
out[3] = (m02 - m20) / s;
|
|
1065
|
-
out[0] = (m01 + m10) / s;
|
|
1066
|
-
out[1] = 0.25 * s;
|
|
1067
|
-
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;
|
|
1068
1062
|
} else {
|
|
1069
|
-
const s
|
|
1070
|
-
out[3]
|
|
1071
|
-
out[0] = (m02 + m20) / s;
|
|
1072
|
-
out[1] = (m12 + m21) / s;
|
|
1073
|
-
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;
|
|
1074
1065
|
}
|
|
1075
1066
|
return qNormalize(out);
|
|
1076
1067
|
};
|
|
1077
1068
|
|
|
1078
1069
|
/**
|
|
1079
|
-
* Extract a unit quaternion from the upper-left
|
|
1070
|
+
* Extract a unit quaternion from the upper-left 3×3 of a column-major mat4.
|
|
1080
1071
|
* @param {number[]} out
|
|
1081
1072
|
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1082
1073
|
* @returns {number[]} out
|
|
1083
1074
|
*/
|
|
1084
1075
|
const qFromMat4 = (out, m) =>
|
|
1085
|
-
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]);
|
|
1086
1077
|
|
|
1087
1078
|
/**
|
|
1088
|
-
* Write a quaternion into a column-major mat4
|
|
1089
|
-
*
|
|
1079
|
+
* Write a quaternion into the rotation block of a column-major mat4.
|
|
1080
|
+
* Translation and perspective rows/cols are set to identity values.
|
|
1090
1081
|
* @param {Float32Array|number[]} out 16-element array.
|
|
1091
1082
|
* @param {number[]} q [x,y,z,w].
|
|
1092
1083
|
* @returns {Float32Array|number[]} out
|
|
1093
1084
|
*/
|
|
1094
1085
|
const qToMat4 = (out, q) => {
|
|
1095
|
-
const x
|
|
1096
|
-
const x2
|
|
1097
|
-
const xx
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
out[
|
|
1101
|
-
out[
|
|
1102
|
-
out[8] = xz+wy; out[9] = yz-wx; out[10] = 1-(xx+yy); out[11] = 0;
|
|
1103
|
-
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;
|
|
1104
1093
|
return out;
|
|
1105
1094
|
};
|
|
1106
1095
|
|
|
1107
1096
|
/**
|
|
1108
1097
|
* Decompose a unit quaternion into { axis:[x,y,z], angle } (radians).
|
|
1109
|
-
* out is optional; a new object is returned if omitted.
|
|
1110
1098
|
* @param {number[]} q [x,y,z,w].
|
|
1111
1099
|
* @param {Object} [out]
|
|
1112
1100
|
* @returns {{ axis: number[], angle: number }}
|
|
1113
1101
|
*/
|
|
1114
1102
|
const quatToAxisAngle = (q, out) => {
|
|
1115
1103
|
out = out || {};
|
|
1116
|
-
const x
|
|
1117
|
-
const sinHalf = Math.sqrt(x*x
|
|
1118
|
-
if (sinHalf < 1e-8) { out.axis
|
|
1119
|
-
out.angle = 2
|
|
1120
|
-
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];
|
|
1121
1109
|
return out;
|
|
1122
1110
|
};
|
|
1123
1111
|
|
|
@@ -1126,36 +1114,34 @@ const quatToAxisAngle = (q, out) => {
|
|
|
1126
1114
|
// =========================================================================
|
|
1127
1115
|
|
|
1128
1116
|
function _dist3(a, b) {
|
|
1129
|
-
const dx
|
|
1130
|
-
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);
|
|
1131
1119
|
}
|
|
1132
1120
|
|
|
1133
1121
|
/**
|
|
1134
|
-
* Centripetal Catmull-Rom interpolation (alpha
|
|
1135
|
-
* out = interp(p0, p1, p2, p3, t) where t
|
|
1136
|
-
*
|
|
1137
|
-
* zero-tension clamped end tangents.
|
|
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.
|
|
1138
1125
|
* @param {number[]} out 3-element result.
|
|
1139
|
-
* @param {number[]} p0
|
|
1140
|
-
* @param {number[]} p1
|
|
1141
|
-
* @param {number[]} p2
|
|
1142
|
-
* @param {number[]} p3
|
|
1143
|
-
* @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].
|
|
1144
1131
|
* @returns {number[]} out
|
|
1145
1132
|
*/
|
|
1146
1133
|
const catmullRomVec3 = (out, p0, p1, p2, p3, t) => {
|
|
1147
1134
|
const alpha = 0.5;
|
|
1148
|
-
const dt0
|
|
1149
|
-
const dt1
|
|
1150
|
-
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;
|
|
1151
1138
|
for (let i = 0; i < 3; i++) {
|
|
1152
1139
|
const t1_0 = (p1[i]-p0[i])/dt0 - (p2[i]-p0[i])/(dt0+dt1) + (p2[i]-p1[i])/dt1;
|
|
1153
1140
|
const t2_0 = (p2[i]-p1[i])/dt1 - (p3[i]-p1[i])/(dt1+dt2) + (p3[i]-p2[i])/dt2;
|
|
1154
|
-
const m1
|
|
1155
|
-
const
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
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];
|
|
1159
1145
|
}
|
|
1160
1146
|
return out;
|
|
1161
1147
|
};
|
|
@@ -1169,9 +1155,9 @@ const catmullRomVec3 = (out, p0, p1, p2, p3, t) => {
|
|
|
1169
1155
|
* @returns {number[]} out
|
|
1170
1156
|
*/
|
|
1171
1157
|
const lerpVec3 = (out, a, b, t) => {
|
|
1172
|
-
out[0]
|
|
1173
|
-
out[1]
|
|
1174
|
-
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]);
|
|
1175
1161
|
return out;
|
|
1176
1162
|
};
|
|
1177
1163
|
|
|
@@ -1181,45 +1167,42 @@ const lerpVec3 = (out, a, b, t) => {
|
|
|
1181
1167
|
|
|
1182
1168
|
/**
|
|
1183
1169
|
* Write a TRS transform into a column-major mat4.
|
|
1184
|
-
* Rotation is encoded as a quaternion; scale is baked into rotation columns.
|
|
1185
1170
|
* @param {Float32Array|number[]} out 16-element column-major mat4.
|
|
1186
1171
|
* @param {{ pos:number[], rot:number[], scl:number[] }} xform
|
|
1187
1172
|
* @returns {Float32Array|number[]} out
|
|
1188
1173
|
*/
|
|
1189
1174
|
const transformToMat4 = (out, xform) => {
|
|
1190
1175
|
qToMat4(out, xform.rot);
|
|
1191
|
-
const sx
|
|
1192
|
-
out[0]
|
|
1193
|
-
out[4]
|
|
1194
|
-
out[8]
|
|
1195
|
-
out[12] = xform.pos[
|
|
1196
|
-
out[13] = xform.pos[1];
|
|
1197
|
-
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];
|
|
1198
1181
|
return out;
|
|
1199
1182
|
};
|
|
1200
1183
|
|
|
1201
1184
|
/**
|
|
1202
1185
|
* Decompose a column-major mat4 into a TRS transform.
|
|
1203
|
-
* Assumes no shear. Scale
|
|
1186
|
+
* Assumes no shear. Scale extracted from column lengths.
|
|
1204
1187
|
* @param {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1205
1188
|
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1206
1189
|
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1207
1190
|
*/
|
|
1208
1191
|
const mat4ToTransform = (out, m) => {
|
|
1209
|
-
out.pos[0]
|
|
1210
|
-
const sx
|
|
1211
|
-
const sy
|
|
1212
|
-
const sz
|
|
1213
|
-
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;
|
|
1214
1197
|
qFromRotMat3x3(out.rot,
|
|
1215
|
-
m[0]/sx,
|
|
1216
|
-
m[1]/sx,
|
|
1217
|
-
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);
|
|
1218
1201
|
return out;
|
|
1219
1202
|
};
|
|
1220
1203
|
|
|
1221
1204
|
// =========================================================================
|
|
1222
|
-
//
|
|
1205
|
+
// S4a Spec parser — PoseTrack
|
|
1223
1206
|
// =========================================================================
|
|
1224
1207
|
|
|
1225
1208
|
const _isNum = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
@@ -1228,189 +1211,175 @@ const _clampS = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
|
|
|
1228
1211
|
|
|
1229
1212
|
function _parseVec3(v) {
|
|
1230
1213
|
if (!v) return null;
|
|
1231
|
-
if (Array.isArray(v) && v.length >= 3 && v.every(n => typeof n === 'number')) return [v[0],
|
|
1232
|
-
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];
|
|
1233
1216
|
return null;
|
|
1234
1217
|
}
|
|
1235
1218
|
|
|
1236
1219
|
function _parseQuat(v) {
|
|
1237
1220
|
if (!v) return null;
|
|
1238
|
-
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]];
|
|
1239
1222
|
if (v.axis && typeof v.angle === 'number') {
|
|
1240
|
-
const a = Array.isArray(v.axis) ? v.axis : [v.axis.x
|
|
1241
|
-
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);
|
|
1242
1225
|
}
|
|
1243
1226
|
if (v.dir) {
|
|
1244
|
-
const d = Array.isArray(v.dir) ? v.dir : [v.dir.x
|
|
1245
|
-
const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x
|
|
1246
|
-
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
|
+
}
|
|
1247
1248
|
}
|
|
1248
1249
|
return null;
|
|
1249
1250
|
}
|
|
1250
1251
|
|
|
1251
1252
|
function _parseSpec(spec) {
|
|
1252
1253
|
if (!spec || typeof spec !== 'object') return null;
|
|
1253
|
-
const pos = _parseVec3(spec.pos) || [0,
|
|
1254
|
-
const rot = _parseQuat(spec.rot) || [0,
|
|
1255
|
-
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];
|
|
1256
1257
|
return { pos, rot, scl };
|
|
1257
1258
|
}
|
|
1258
1259
|
|
|
1259
1260
|
function _sameTransform(a, b) {
|
|
1260
|
-
for (let i
|
|
1261
|
-
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;
|
|
1262
1263
|
return true;
|
|
1263
1264
|
}
|
|
1264
1265
|
|
|
1265
1266
|
// =========================================================================
|
|
1266
|
-
//
|
|
1267
|
+
// S4b Spec parser — CameraTrack
|
|
1267
1268
|
// =========================================================================
|
|
1268
1269
|
|
|
1269
1270
|
/**
|
|
1270
|
-
*
|
|
1271
|
+
* Parse a camera keyframe spec into internal { eye, center, up } form.
|
|
1271
1272
|
*
|
|
1272
|
-
*
|
|
1273
|
-
* The track maintains a scalar cursor (seg, f) that advances each tick().
|
|
1273
|
+
* Accepted forms:
|
|
1274
1274
|
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1275
|
+
* { eye, center, up? }
|
|
1276
|
+
* Explicit lookat. up defaults to [0,1,0] and is normalised on storage.
|
|
1277
1277
|
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
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 }.
|
|
1282
1286
|
*
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
* One-keyframe behaviour:
|
|
1287
|
-
* play() with exactly one keyframe snaps eval() to that keyframe
|
|
1288
|
-
* without setting playing = true and without firing hooks.
|
|
1287
|
+
* @param {Object} spec
|
|
1288
|
+
* @returns {{ eye:number[], center:number[], up:number[] }|null}
|
|
1289
1289
|
*/
|
|
1290
|
-
|
|
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 {
|
|
1291
1337
|
constructor() {
|
|
1292
|
-
/** @type {Array
|
|
1338
|
+
/** @type {Array} Keyframe array — shape depends on subclass. */
|
|
1293
1339
|
this.keyframes = [];
|
|
1294
1340
|
/** Whether playback is active. @type {boolean} */
|
|
1295
1341
|
this.playing = false;
|
|
1296
|
-
/** Loop
|
|
1342
|
+
/** Loop at boundaries. @type {boolean} */
|
|
1297
1343
|
this.loop = false;
|
|
1298
|
-
/** Ping-pong bounce
|
|
1344
|
+
/** Ping-pong bounce (takes precedence over loop). @type {boolean} */
|
|
1299
1345
|
this.pingPong = false;
|
|
1300
|
-
/** Frames per segment (
|
|
1346
|
+
/** Frames per segment (≥1). @type {number} */
|
|
1301
1347
|
this.duration = 30;
|
|
1302
1348
|
/** Current segment index. @type {number} */
|
|
1303
1349
|
this.seg = 0;
|
|
1304
|
-
/** Frame offset within
|
|
1350
|
+
/** Frame offset within segment (can be fractional). @type {number} */
|
|
1305
1351
|
this.f = 0;
|
|
1306
|
-
/**
|
|
1307
|
-
* Position interpolation mode.
|
|
1308
|
-
* @type {'catmullrom'|'linear'}
|
|
1309
|
-
*/
|
|
1310
|
-
this.posInterp = 'catmullrom';
|
|
1311
|
-
|
|
1312
|
-
// Scratch arrays reused by eval() / toMatrix() — avoids hot-path allocations
|
|
1313
|
-
this._pos = [0, 0, 0];
|
|
1314
|
-
this._rot = [0, 0, 0, 1];
|
|
1315
|
-
this._scl = [1, 1, 1];
|
|
1316
1352
|
|
|
1317
|
-
// Internal rate —
|
|
1353
|
+
// Internal rate — never directly starts/stops playback
|
|
1318
1354
|
this._rate = 1;
|
|
1319
1355
|
|
|
1320
1356
|
// User-space hooks
|
|
1321
|
-
/**
|
|
1322
|
-
this.
|
|
1323
|
-
/**
|
|
1324
|
-
this.onEnd = null;
|
|
1325
|
-
|
|
1326
|
-
// Lib-space hooks (set by host layer — e.g. p5 bridge)
|
|
1327
|
-
/** @type {Function|null} */
|
|
1328
|
-
this._onActivate = null;
|
|
1329
|
-
/** @type {Function|null} */
|
|
1330
|
-
this._onDeactivate = null;
|
|
1331
|
-
}
|
|
1357
|
+
/** @type {Function|null} */ this.onPlay = null;
|
|
1358
|
+
/** @type {Function|null} */ this.onEnd = null;
|
|
1359
|
+
/** @type {Function|null} */ this.onStop = null;
|
|
1332
1360
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
+
}
|
|
1336
1365
|
|
|
1337
|
-
/** Playback rate.
|
|
1366
|
+
/** Playback rate. Assigning never starts/stops playback. @type {number} */
|
|
1338
1367
|
get rate() { return this._rate; }
|
|
1339
|
-
set rate(v) {
|
|
1340
|
-
this._rate = (typeof v === 'number' && Number.isFinite(v)) ? v : 1;
|
|
1341
|
-
// Intentionally does NOT start or stop playback.
|
|
1342
|
-
}
|
|
1368
|
+
set rate(v) { this._rate = (_isNum(v)) ? v : 1; }
|
|
1343
1369
|
|
|
1344
|
-
/** Number of interpolatable segments (keyframes.length
|
|
1370
|
+
/** Number of interpolatable segments (keyframes.length − 1, min 0). @type {number} */
|
|
1345
1371
|
get segments() { return Math.max(0, this.keyframes.length - 1); }
|
|
1346
1372
|
|
|
1347
|
-
// ── Keyframe management ──────────────────────────────────────────────────
|
|
1348
|
-
|
|
1349
|
-
/**
|
|
1350
|
-
* Append a keyframe. Adjacent duplicates are skipped by default.
|
|
1351
|
-
* @param {{ pos?, rot?, scl? }} spec pos/rot/scl arrays, {x,y,z}, axis-angle, look-dir.
|
|
1352
|
-
* @param {{ deduplicate?: boolean }} [opts]
|
|
1353
|
-
*/
|
|
1354
|
-
add(spec, opts) {
|
|
1355
|
-
const kf = _parseSpec(spec);
|
|
1356
|
-
if (!kf) return;
|
|
1357
|
-
const dedup = !opts || opts.deduplicate !== false;
|
|
1358
|
-
if (dedup && this.keyframes.length > 0) {
|
|
1359
|
-
if (_sameTransform(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1360
|
-
}
|
|
1361
|
-
this.keyframes.push(kf);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
/**
|
|
1365
|
-
* Replace (or append at end) the keyframe at index.
|
|
1366
|
-
* @param {number} index Existing index or keyframes.length to append.
|
|
1367
|
-
* @param {{ pos?, rot?, scl? }} spec
|
|
1368
|
-
* @returns {boolean}
|
|
1369
|
-
*/
|
|
1370
|
-
set(index, spec) {
|
|
1371
|
-
if (!_isNum(index)) return false;
|
|
1372
|
-
const i = index | 0;
|
|
1373
|
-
const kf = _parseSpec(spec);
|
|
1374
|
-
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1375
|
-
if (i === this.keyframes.length) { this.keyframes.push(kf); }
|
|
1376
|
-
else { this.keyframes[i] = kf; }
|
|
1377
|
-
return true;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
/**
|
|
1381
|
-
* Remove the keyframe at index. Adjusts cursor if needed.
|
|
1382
|
-
* @param {number} index
|
|
1383
|
-
* @returns {boolean}
|
|
1384
|
-
*/
|
|
1385
|
-
remove(index) {
|
|
1386
|
-
if (!_isNum(index)) return false;
|
|
1387
|
-
const i = index | 0;
|
|
1388
|
-
if (i < 0 || i >= this.keyframes.length) return false;
|
|
1389
|
-
this.keyframes.splice(i, 1);
|
|
1390
|
-
const nSeg = this.segments;
|
|
1391
|
-
if (nSeg === 0) { this.seg = 0; this.f = 0; }
|
|
1392
|
-
else if (this.seg >= nSeg) { this.seg = nSeg - 1; }
|
|
1393
|
-
return true;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// ── Transport ────────────────────────────────────────────────────────────
|
|
1397
|
-
|
|
1398
1373
|
/**
|
|
1399
1374
|
* Start or update playback.
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1403
|
-
* One keyframe: snaps cursor (seg=0, f=0); no playing=true, no hooks.
|
|
1404
|
-
* Already playing: updates params in place; hooks are not re-fired.
|
|
1405
|
-
* rate=0 is valid: track will be playing but frozen until rate changes.
|
|
1406
|
-
*
|
|
1407
|
-
* @param {number|Object} [rateOrOpts]
|
|
1408
|
-
* @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
|
|
1409
1378
|
*/
|
|
1410
1379
|
play(rateOrOpts) {
|
|
1411
1380
|
if (this.keyframes.length === 0) return this;
|
|
1412
1381
|
|
|
1413
|
-
// One keyframe: snap
|
|
1382
|
+
// One keyframe: snap cursor, no animation
|
|
1414
1383
|
if (this.keyframes.length === 1) {
|
|
1415
1384
|
this.seg = 0; this.f = 0;
|
|
1416
1385
|
return this;
|
|
@@ -1425,12 +1394,11 @@ class PoseTrack {
|
|
|
1425
1394
|
if ('pingPong' in o) this.pingPong = !!o.pingPong;
|
|
1426
1395
|
if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
|
|
1427
1396
|
if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
|
|
1397
|
+
if (typeof o.onStop === 'function') this.onStop = o.onStop;
|
|
1428
1398
|
if (_isNum(o.rate)) this._rate = o.rate;
|
|
1429
1399
|
}
|
|
1430
1400
|
|
|
1431
|
-
|
|
1432
|
-
const nSeg = this.segments;
|
|
1433
|
-
const dur = Math.max(1, this.duration | 0);
|
|
1401
|
+
const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
|
|
1434
1402
|
if (this.seg < 0) this.seg = 0;
|
|
1435
1403
|
if (this.seg >= nSeg) this.seg = nSeg - 1;
|
|
1436
1404
|
if (this.f < 0) this.f = 0;
|
|
@@ -1438,7 +1406,6 @@ class PoseTrack {
|
|
|
1438
1406
|
|
|
1439
1407
|
const wasPlaying = this.playing;
|
|
1440
1408
|
this.playing = true;
|
|
1441
|
-
|
|
1442
1409
|
if (!wasPlaying) {
|
|
1443
1410
|
if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
|
|
1444
1411
|
this._onActivate?.();
|
|
@@ -1447,39 +1414,58 @@ class PoseTrack {
|
|
|
1447
1414
|
}
|
|
1448
1415
|
|
|
1449
1416
|
/**
|
|
1450
|
-
* Stop playback.
|
|
1451
|
-
* @param {boolean} [
|
|
1452
|
-
* @returns {
|
|
1417
|
+
* Stop playback.
|
|
1418
|
+
* @param {boolean} [rewind=false] Seek to origin after stopping.
|
|
1419
|
+
* @returns {Track} this
|
|
1453
1420
|
*/
|
|
1454
|
-
stop(
|
|
1421
|
+
stop(rewind) {
|
|
1455
1422
|
const wasPlaying = this.playing;
|
|
1456
1423
|
this.playing = false;
|
|
1457
|
-
if (wasPlaying)
|
|
1458
|
-
|
|
1459
|
-
|
|
1424
|
+
if (wasPlaying) {
|
|
1425
|
+
if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
|
|
1426
|
+
this._onDeactivate?.();
|
|
1427
|
+
if (rewind && this.keyframes.length > 1) this.seek(this._rate < 0 ? 1 : 0);
|
|
1428
|
+
}
|
|
1460
1429
|
return this;
|
|
1461
1430
|
}
|
|
1462
1431
|
|
|
1463
1432
|
/**
|
|
1464
|
-
* Clear all keyframes and stop.
|
|
1465
|
-
* @returns {
|
|
1433
|
+
* Clear all keyframes and stop.
|
|
1434
|
+
* @returns {Track} this
|
|
1466
1435
|
*/
|
|
1467
1436
|
reset() {
|
|
1468
1437
|
const wasPlaying = this.playing;
|
|
1469
1438
|
this.playing = false;
|
|
1470
|
-
if (wasPlaying)
|
|
1439
|
+
if (wasPlaying) {
|
|
1440
|
+
if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
|
|
1441
|
+
this._onDeactivate?.();
|
|
1442
|
+
}
|
|
1471
1443
|
this.keyframes.length = 0;
|
|
1472
1444
|
this.seg = 0; this.f = 0;
|
|
1473
1445
|
return this;
|
|
1474
1446
|
}
|
|
1475
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
|
+
|
|
1476
1464
|
/**
|
|
1477
1465
|
* Seek to a normalised position [0,1] across the full path.
|
|
1478
|
-
* Can optionally target a specific segment (t is then local to that segment).
|
|
1479
|
-
* Does not change the playing flag.
|
|
1480
1466
|
* @param {number} t Normalised time [0, 1].
|
|
1481
1467
|
* @param {number} [segIndex] Optional segment override.
|
|
1482
|
-
* @returns {
|
|
1468
|
+
* @returns {Track} this
|
|
1483
1469
|
*/
|
|
1484
1470
|
seek(t, segIndex) {
|
|
1485
1471
|
const nSeg = this.segments;
|
|
@@ -1495,8 +1481,7 @@ class PoseTrack {
|
|
|
1495
1481
|
}
|
|
1496
1482
|
|
|
1497
1483
|
/**
|
|
1498
|
-
* Normalised playback
|
|
1499
|
-
* Returns 0 when fewer than 2 keyframes exist.
|
|
1484
|
+
* Normalised playback position [0,1].
|
|
1500
1485
|
* @returns {number}
|
|
1501
1486
|
*/
|
|
1502
1487
|
time() {
|
|
@@ -1507,10 +1492,8 @@ class PoseTrack {
|
|
|
1507
1492
|
}
|
|
1508
1493
|
|
|
1509
1494
|
/**
|
|
1510
|
-
* Snapshot of
|
|
1511
|
-
* @returns {
|
|
1512
|
-
* time:number, playing:boolean, loop:boolean, pingPong:boolean,
|
|
1513
|
-
* rate:number, duration:number }}
|
|
1495
|
+
* Snapshot of transport state.
|
|
1496
|
+
* @returns {Object}
|
|
1514
1497
|
*/
|
|
1515
1498
|
info() {
|
|
1516
1499
|
return {
|
|
@@ -1528,12 +1511,8 @@ class PoseTrack {
|
|
|
1528
1511
|
}
|
|
1529
1512
|
|
|
1530
1513
|
/**
|
|
1531
|
-
* Advance
|
|
1532
|
-
*
|
|
1533
|
-
* rate === 0: frozen — returns this.playing without moving (no-op).
|
|
1534
|
-
* Returns false and fires onEnd/_onDeactivate when a once-mode boundary is hit.
|
|
1535
|
-
* Returns true while playing and continuing.
|
|
1536
|
-
*
|
|
1514
|
+
* Advance cursor by rate frames.
|
|
1515
|
+
* Returns true while playing, false when stopping.
|
|
1537
1516
|
* @returns {boolean}
|
|
1538
1517
|
*/
|
|
1539
1518
|
tick() {
|
|
@@ -1542,8 +1521,6 @@ class PoseTrack {
|
|
|
1542
1521
|
if (nSeg === 0) {
|
|
1543
1522
|
this.playing = false; this._onDeactivate?.(); return false;
|
|
1544
1523
|
}
|
|
1545
|
-
|
|
1546
|
-
// Frozen: position does not advance, playing stays true
|
|
1547
1524
|
if (this._rate === 0) return true;
|
|
1548
1525
|
|
|
1549
1526
|
const dur = Math.max(1, this.duration | 0);
|
|
@@ -1551,25 +1528,22 @@ class PoseTrack {
|
|
|
1551
1528
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1552
1529
|
const next = s + this._rate;
|
|
1553
1530
|
|
|
1554
|
-
// ── pingPong ──
|
|
1555
1531
|
if (this.pingPong) {
|
|
1556
1532
|
let pos = next, flips = 0;
|
|
1557
1533
|
while (pos < 0 || pos > total) {
|
|
1558
|
-
if (pos < 0)
|
|
1559
|
-
else
|
|
1534
|
+
if (pos < 0) { pos = -pos; flips++; }
|
|
1535
|
+
else { pos = 2 * total - pos; flips++; }
|
|
1560
1536
|
}
|
|
1561
1537
|
if (flips & 1) this._rate = -this._rate;
|
|
1562
1538
|
this._setCursorFromScalar(pos);
|
|
1563
1539
|
return true;
|
|
1564
1540
|
}
|
|
1565
1541
|
|
|
1566
|
-
// ── loop ──
|
|
1567
1542
|
if (this.loop) {
|
|
1568
1543
|
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1569
1544
|
return true;
|
|
1570
1545
|
}
|
|
1571
1546
|
|
|
1572
|
-
// ── once — boundary check ──
|
|
1573
1547
|
if (next <= 0) {
|
|
1574
1548
|
this._setCursorFromScalar(0);
|
|
1575
1549
|
this.playing = false;
|
|
@@ -1589,15 +1563,112 @@ class PoseTrack {
|
|
|
1589
1563
|
return true;
|
|
1590
1564
|
}
|
|
1591
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
|
+
|
|
1592
1665
|
/**
|
|
1593
|
-
* Evaluate
|
|
1594
|
-
* If out is omitted a new object is allocated (avoid in hot paths).
|
|
1595
|
-
* Uses centripetal Catmull-Rom for position (posInterp === 'catmullrom') or lerp.
|
|
1666
|
+
* Evaluate interpolated TRS pose at current cursor.
|
|
1596
1667
|
* @param {{ pos:number[], rot:number[], scl:number[] }} [out]
|
|
1597
1668
|
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1598
1669
|
*/
|
|
1599
1670
|
eval(out) {
|
|
1600
|
-
out = out || { pos:
|
|
1671
|
+
out = out || { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
|
|
1601
1672
|
const n = this.keyframes.length;
|
|
1602
1673
|
if (n === 0) return out;
|
|
1603
1674
|
|
|
@@ -1616,27 +1687,30 @@ class PoseTrack {
|
|
|
1616
1687
|
const k0 = this.keyframes[seg];
|
|
1617
1688
|
const k1 = this.keyframes[seg + 1];
|
|
1618
1689
|
|
|
1619
|
-
//
|
|
1690
|
+
// pos — Catmull-Rom or lerp
|
|
1620
1691
|
if (this.posInterp === 'catmullrom') {
|
|
1621
|
-
const p0 = seg > 0
|
|
1692
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1622
1693
|
const p3 = seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1623
1694
|
catmullRomVec3(out.pos, p0, k0.pos, k1.pos, p3, t);
|
|
1624
1695
|
} else {
|
|
1625
1696
|
lerpVec3(out.pos, k0.pos, k1.pos, t);
|
|
1626
1697
|
}
|
|
1627
1698
|
|
|
1628
|
-
//
|
|
1629
|
-
|
|
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
|
+
}
|
|
1630
1705
|
|
|
1631
|
-
//
|
|
1706
|
+
// scl — lerp
|
|
1632
1707
|
lerpVec3(out.scl, k0.scl, k1.scl, t);
|
|
1633
1708
|
|
|
1634
1709
|
return out;
|
|
1635
1710
|
}
|
|
1636
1711
|
|
|
1637
1712
|
/**
|
|
1638
|
-
* Evaluate
|
|
1639
|
-
* Reuses internal scratch arrays — no allocation per call.
|
|
1713
|
+
* Evaluate into an existing column-major mat4.
|
|
1640
1714
|
* @param {Float32Array|number[]} outMat4 16-element array.
|
|
1641
1715
|
* @returns {Float32Array|number[]} outMat4
|
|
1642
1716
|
*/
|
|
@@ -1644,17 +1718,161 @@ class PoseTrack {
|
|
|
1644
1718
|
const xf = this.eval({ pos: this._pos, rot: this._rot, scl: this._scl });
|
|
1645
1719
|
return transformToMat4(outMat4, xf);
|
|
1646
1720
|
}
|
|
1721
|
+
}
|
|
1647
1722
|
|
|
1648
|
-
|
|
1723
|
+
// =========================================================================
|
|
1724
|
+
// S7 CameraTrack
|
|
1725
|
+
// =========================================================================
|
|
1649
1726
|
|
|
1650
|
-
|
|
1651
|
-
|
|
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;
|
|
1652
1844
|
const dur = Math.max(1, this.duration | 0);
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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;
|
|
1658
1876
|
}
|
|
1659
1877
|
}
|
|
1660
1878
|
|
|
@@ -1815,5 +2033,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
1815
2033
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
1816
2034
|
}
|
|
1817
2035
|
|
|
1818
|
-
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 };
|
|
1819
2037
|
//# sourceMappingURL=index.js.map
|