@flighthq/motionpath 0.1.0
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/dist/createMotionPath.d.ts +3 -0
- package/dist/createMotionPath.d.ts.map +1 -0
- package/dist/createMotionPath.js +19 -0
- package/dist/createMotionPath.js.map +1 -0
- package/dist/getMotionPathHeading.d.ts +3 -0
- package/dist/getMotionPathHeading.d.ts.map +1 -0
- package/dist/getMotionPathHeading.js +12 -0
- package/dist/getMotionPathHeading.js.map +1 -0
- package/dist/getMotionPathPosition.d.ts +3 -0
- package/dist/getMotionPathPosition.d.ts.map +1 -0
- package/dist/getMotionPathPosition.js +9 -0
- package/dist/getMotionPathPosition.js.map +1 -0
- package/dist/getMotionPathProgress.d.ts +3 -0
- package/dist/getMotionPathProgress.d.ts.map +1 -0
- package/dist/getMotionPathProgress.js +6 -0
- package/dist/getMotionPathProgress.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/setMotionPathDistance.d.ts +3 -0
- package/dist/setMotionPathDistance.d.ts.map +1 -0
- package/dist/setMotionPathDistance.js +12 -0
- package/dist/setMotionPathDistance.js.map +1 -0
- package/dist/setMotionPathProgress.d.ts +3 -0
- package/dist/setMotionPathProgress.d.ts.map +1 -0
- package/dist/setMotionPathProgress.js +11 -0
- package/dist/setMotionPathProgress.js.map +1 -0
- package/dist/updateMotionPath.d.ts +3 -0
- package/dist/updateMotionPath.d.ts.map +1 -0
- package/dist/updateMotionPath.js +60 -0
- package/dist/updateMotionPath.js.map +1 -0
- package/package.json +39 -0
- package/src/createMotionPath.test.ts +34 -0
- package/src/getMotionPathHeading.test.ts +43 -0
- package/src/getMotionPathPosition.test.ts +41 -0
- package/src/getMotionPathProgress.test.ts +28 -0
- package/src/setMotionPathDistance.test.ts +34 -0
- package/src/setMotionPathProgress.test.ts +34 -0
- package/src/updateMotionPath.test.ts +105 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createMotionPath.d.ts","sourceRoot":"","sources":["../src/createMotionPath.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAS5E,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,KAAK,SAAI,EACT,QAAQ,GAAE,kBAA4B,EACtC,SAAS,CAAC,EAAE,MAAM,GACjB,UAAU,CASZ"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getPathLength } from '@flighthq/path';
|
|
2
|
+
// Allocate a `MotionPath` marker at the start of `path` (`distance` 0, `direction` forward).
|
|
3
|
+
// `speed` is the traversal rate in path units per second (a magnitude; default 0 = stationary),
|
|
4
|
+
// `loopMode` the end behavior (default `clamp`). The path's total arc length is measured once here
|
|
5
|
+
// with `getPathLength(path, tolerance)` and cached in `length`; per-frame updates reuse it rather
|
|
6
|
+
// than re-measuring. `tolerance` is the curve-flattening tolerance forwarded to the arc-length
|
|
7
|
+
// measurement. This is the only allocating function; `updateMotionPath` and the seek helpers write
|
|
8
|
+
// into an existing marker.
|
|
9
|
+
export function createMotionPath(path, speed = 0, loopMode = 'clamp', tolerance) {
|
|
10
|
+
return {
|
|
11
|
+
direction: 1,
|
|
12
|
+
distance: 0,
|
|
13
|
+
length: getPathLength(path, tolerance),
|
|
14
|
+
loopMode,
|
|
15
|
+
path,
|
|
16
|
+
speed,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=createMotionPath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createMotionPath.js","sourceRoot":"","sources":["../src/createMotionPath.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,6FAA6F;AAC7F,gGAAgG;AAChG,mGAAmG;AACnG,kGAAkG;AAClG,+FAA+F;AAC/F,mGAAmG;AACnG,2BAA2B;AAC3B,MAAM,UAAU,gBAAgB,CAC9B,IAAU,EACV,KAAK,GAAG,CAAC,EACT,WAA+B,OAAO,EACtC,SAAkB;IAElB,OAAO;QACL,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC;QACtC,QAAQ;QACR,IAAI;QACJ,KAAK;KACN,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMotionPathHeading.d.ts","sourceRoot":"","sources":["../src/getMotionPathHeading.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMlD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,CAGrE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createVector2 } from '@flighthq/geometry';
|
|
2
|
+
import { getPathTangentAtDistance } from '@flighthq/path';
|
|
3
|
+
// Return the marker's heading angle in radians — `atan2(tangentY, tangentX)` of the path tangent at
|
|
4
|
+
// the current `distance` — for orienting an object along the curve. Samples the tangent into a
|
|
5
|
+
// module-scoped scratch vector, so the query allocates nothing. For an empty/degenerate path the
|
|
6
|
+
// tangent falls back to `(1, 0)`, giving a heading of 0.
|
|
7
|
+
export function getMotionPathHeading(mp) {
|
|
8
|
+
getPathTangentAtDistance(mp.path, mp.distance, scratchTangent);
|
|
9
|
+
return Math.atan2(scratchTangent.y, scratchTangent.x);
|
|
10
|
+
}
|
|
11
|
+
const scratchTangent = createVector2();
|
|
12
|
+
//# sourceMappingURL=getMotionPathHeading.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMotionPathHeading.js","sourceRoot":"","sources":["../src/getMotionPathHeading.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAG1D,oGAAoG;AACpG,+FAA+F;AAC/F,iGAAiG;AACjG,yDAAyD;AACzD,MAAM,UAAU,oBAAoB,CAAC,EAAwB;IAC3D,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,cAAc,GAAG,aAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMotionPathPosition.d.ts","sourceRoot":"","sources":["../src/getMotionPathPosition.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAM/D,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,CAAC,UAAU,CAAC,EACxB,QAAQ,EAAE,WAAW,EACrB,UAAU,EAAE,WAAW,GACtB,OAAO,CAET"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getPathPositionAtDistance } from '@flighthq/path';
|
|
2
|
+
// Sample the marker's current world position and unit tangent, writing them into `pointOut` and
|
|
3
|
+
// `tangentOut`. Returns `true` on success, `false` for an empty/degenerate path (in which case the
|
|
4
|
+
// outputs are left unchanged). Delegates to `@flighthq/path`'s `getPathPositionAtDistance` at the
|
|
5
|
+
// marker's current `distance`.
|
|
6
|
+
export function getMotionPathPosition(mp, pointOut, tangentOut) {
|
|
7
|
+
return getPathPositionAtDistance(mp.path, mp.distance, pointOut, tangentOut);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=getMotionPathPosition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMotionPathPosition.js","sourceRoot":"","sources":["../src/getMotionPathPosition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,gGAAgG;AAChG,mGAAmG;AACnG,kGAAkG;AAClG,+BAA+B;AAC/B,MAAM,UAAU,qBAAqB,CACnC,EAAwB,EACxB,QAAqB,EACrB,UAAuB;IAEvB,OAAO,yBAAyB,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMotionPathProgress.d.ts","sourceRoot":"","sources":["../src/getMotionPathProgress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,CAEtE"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Return the marker's normalized progress along the path, `distance / length` in `[0, 1]`. A
|
|
2
|
+
// zero-length path reports 0.
|
|
3
|
+
export function getMotionPathProgress(mp) {
|
|
4
|
+
return mp.length > 0 ? mp.distance / mp.length : 0;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=getMotionPathProgress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMotionPathProgress.js","sourceRoot":"","sources":["../src/getMotionPathProgress.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,8BAA8B;AAC9B,MAAM,UAAU,qBAAqB,CAAC,EAAwB;IAC5D,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './createMotionPath';
|
|
2
|
+
export * from './getMotionPathHeading';
|
|
3
|
+
export * from './getMotionPathPosition';
|
|
4
|
+
export * from './getMotionPathProgress';
|
|
5
|
+
export * from './setMotionPathDistance';
|
|
6
|
+
export * from './setMotionPathProgress';
|
|
7
|
+
export * from './updateMotionPath';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './createMotionPath';
|
|
2
|
+
export * from './getMotionPathHeading';
|
|
3
|
+
export * from './getMotionPathPosition';
|
|
4
|
+
export * from './getMotionPathProgress';
|
|
5
|
+
export * from './setMotionPathDistance';
|
|
6
|
+
export * from './setMotionPathProgress';
|
|
7
|
+
export * from './updateMotionPath';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setMotionPathDistance.d.ts","sourceRoot":"","sources":["../src/setMotionPathDistance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAM5E"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Seek the marker to an absolute arc-length `distance`, clamping to the valid `[0, length]` range.
|
|
2
|
+
// Does not change `direction` or `speed`.
|
|
3
|
+
export function setMotionPathDistance(mp, distance) {
|
|
4
|
+
const length = mp.length;
|
|
5
|
+
let clamped = distance;
|
|
6
|
+
if (clamped < 0)
|
|
7
|
+
clamped = 0;
|
|
8
|
+
else if (clamped > length)
|
|
9
|
+
clamped = length;
|
|
10
|
+
mp.distance = clamped;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=setMotionPathDistance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setMotionPathDistance.js","sourceRoot":"","sources":["../src/setMotionPathDistance.ts"],"names":[],"mappings":"AAEA,mGAAmG;AACnG,0CAA0C;AAC1C,MAAM,UAAU,qBAAqB,CAAC,EAAc,EAAE,QAAgB;IACpE,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC;IACzB,IAAI,OAAO,GAAG,QAAQ,CAAC;IACvB,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC;SACxB,IAAI,OAAO,GAAG,MAAM;QAAE,OAAO,GAAG,MAAM,CAAC;IAC5C,EAAE,CAAC,QAAQ,GAAG,OAAO,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setMotionPathProgress.d.ts","sourceRoot":"","sources":["../src/setMotionPathProgress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAKrE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Seek the marker to a normalized progress `t` along the path, clamping `t` to `[0, 1]` and setting
|
|
2
|
+
// `distance` to `t * length`. Does not change `direction` or `speed`.
|
|
3
|
+
export function setMotionPathProgress(mp, t) {
|
|
4
|
+
let clamped = t;
|
|
5
|
+
if (clamped < 0)
|
|
6
|
+
clamped = 0;
|
|
7
|
+
else if (clamped > 1)
|
|
8
|
+
clamped = 1;
|
|
9
|
+
mp.distance = clamped * mp.length;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=setMotionPathProgress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setMotionPathProgress.js","sourceRoot":"","sources":["../src/setMotionPathProgress.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,sEAAsE;AACtE,MAAM,UAAU,qBAAqB,CAAC,EAAc,EAAE,CAAS;IAC7D,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC;SACxB,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC;IAClC,EAAE,CAAC,QAAQ,GAAG,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateMotionPath.d.ts","sourceRoot":"","sources":["../src/updateMotionPath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMlD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAQxE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Advance `mp` by `deltaTime` seconds: move the marker `speed * deltaTime` path units along its
|
|
2
|
+
// current `direction`, then resolve the end behavior (`applyMotionPathLoopMode`). `deltaTime <= 0`
|
|
3
|
+
// and a zero-length path are no-ops. `distance` (and, for `pingpong`, `direction`) are updated in
|
|
4
|
+
// place; all inputs are read into locals before any write, so `mp` aliasing itself is safe.
|
|
5
|
+
export function updateMotionPath(mp, deltaTime) {
|
|
6
|
+
if (deltaTime <= 0)
|
|
7
|
+
return;
|
|
8
|
+
const length = mp.length;
|
|
9
|
+
if (length <= 0)
|
|
10
|
+
return;
|
|
11
|
+
// `move` is a magnitude (path units this frame); the travel sign lives in `mp.direction`.
|
|
12
|
+
const move = mp.speed * deltaTime;
|
|
13
|
+
applyMotionPathLoopMode(mp, move, length);
|
|
14
|
+
}
|
|
15
|
+
// Resolve the end behavior after advancing `mp` by `move` units along `mp.direction`, writing the
|
|
16
|
+
// new `distance` (kept in `[0, length]`) and, for `pingpong`, the new `direction` back into `mp`.
|
|
17
|
+
//
|
|
18
|
+
// `clamp` stops at the ends. `loop` wraps the signed advance modulo `length` into `[0, length)`.
|
|
19
|
+
// `pingpong` reflects off both ends: the marker travels on a phase line of period `2 * length`
|
|
20
|
+
// whose triangle wave folds back into `[0, length]`, so the closed form below bounces correctly even
|
|
21
|
+
// when a single large `move` crosses the path several times. Because `distance` alone is ambiguous
|
|
22
|
+
// between the outbound and return legs, the current `direction` selects which phase-line branch the
|
|
23
|
+
// marker is on (`direction < 0` mirrors it onto the descending branch) before advancing.
|
|
24
|
+
function applyMotionPathLoopMode(mp, move, length) {
|
|
25
|
+
const loopMode = mp.loopMode;
|
|
26
|
+
const distance = mp.distance;
|
|
27
|
+
const direction = mp.direction;
|
|
28
|
+
if (loopMode === 'loop') {
|
|
29
|
+
let wrapped = (distance + direction * move) % length;
|
|
30
|
+
if (wrapped < 0)
|
|
31
|
+
wrapped += length;
|
|
32
|
+
mp.distance = wrapped;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (loopMode === 'pingpong') {
|
|
36
|
+
const period = 2 * length;
|
|
37
|
+
// Phase of the current marker: identity on the outbound (forward) leg, mirrored on the return.
|
|
38
|
+
const phase = direction < 0 ? period - distance : distance;
|
|
39
|
+
let advanced = (phase + move) % period;
|
|
40
|
+
if (advanced < 0)
|
|
41
|
+
advanced += period;
|
|
42
|
+
if (advanced <= length) {
|
|
43
|
+
mp.distance = advanced;
|
|
44
|
+
mp.direction = 1;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
mp.distance = period - advanced;
|
|
48
|
+
mp.direction = -1;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// clamp
|
|
53
|
+
let clamped = distance + direction * move;
|
|
54
|
+
if (clamped < 0)
|
|
55
|
+
clamped = 0;
|
|
56
|
+
else if (clamped > length)
|
|
57
|
+
clamped = length;
|
|
58
|
+
mp.distance = clamped;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=updateMotionPath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateMotionPath.js","sourceRoot":"","sources":["../src/updateMotionPath.ts"],"names":[],"mappings":"AAEA,gGAAgG;AAChG,mGAAmG;AACnG,kGAAkG;AAClG,4FAA4F;AAC5F,MAAM,UAAU,gBAAgB,CAAC,EAAc,EAAE,SAAiB;IAChE,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO;IAC3B,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC;IACzB,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO;IAExB,0FAA0F;IAC1F,MAAM,IAAI,GAAG,EAAE,CAAC,KAAK,GAAG,SAAS,CAAC;IAClC,uBAAuB,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC;AAED,kGAAkG;AAClG,kGAAkG;AAClG,EAAE;AACF,iGAAiG;AACjG,+FAA+F;AAC/F,qGAAqG;AACrG,mGAAmG;AACnG,oGAAoG;AACpG,yFAAyF;AACzF,SAAS,uBAAuB,CAAC,EAAc,EAAE,IAAY,EAAE,MAAc;IAC3E,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,CAAC;IAC7B,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,CAAC;IAC7B,MAAM,SAAS,GAAG,EAAE,CAAC,SAAS,CAAC;IAE/B,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,IAAI,OAAO,GAAG,CAAC,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC;QACrD,IAAI,OAAO,GAAG,CAAC;YAAE,OAAO,IAAI,MAAM,CAAC;QACnC,EAAE,CAAC,QAAQ,GAAG,OAAO,CAAC;QACtB,OAAO;IACT,CAAC;IAED,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC;QAC1B,+FAA+F;QAC/F,MAAM,KAAK,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC3D,IAAI,QAAQ,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC;QACvC,IAAI,QAAQ,GAAG,CAAC;YAAE,QAAQ,IAAI,MAAM,CAAC;QACrC,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;YACvB,EAAE,CAAC,QAAQ,GAAG,QAAQ,CAAC;YACvB,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;YAChC,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QACpB,CAAC;QACD,OAAO;IACT,CAAC;IAED,QAAQ;IACR,IAAI,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC;IAC1C,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC;SACxB,IAAI,OAAO,GAAG,MAAM;QAAE,OAAO,GAAG,MAAM,CAAC;IAC5C,EAAE,CAAC,QAAQ,GAAG,OAAO,CAAC;AACxB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/motionpath",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/geometry": "0.1.0",
|
|
31
|
+
"@flighthq/path": "0.1.0",
|
|
32
|
+
"@flighthq/types": "0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.3.0"
|
|
36
|
+
},
|
|
37
|
+
"description": "Path-following animation — advances a marker along a @flighthq/path by arc length at a controlled speed and reports its world position, tangent, and heading angle, with clamp/loop/pingpong end behaviors and distance/progress seek; plain-data MotionPath state stepped by small out-param functions over path's arc-length sampling",
|
|
38
|
+
"sideEffects": false
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path';
|
|
2
|
+
import type { Path } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createMotionPath } from './createMotionPath';
|
|
6
|
+
|
|
7
|
+
describe('createMotionPath', () => {
|
|
8
|
+
it('caches the path arc length measured from the path', () => {
|
|
9
|
+
const mp = createMotionPath(line());
|
|
10
|
+
expect(mp.length).toBeCloseTo(100, 6);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('starts at the path start, forward, with clamp defaults', () => {
|
|
14
|
+
const mp = createMotionPath(line());
|
|
15
|
+
expect(mp.distance).toBe(0);
|
|
16
|
+
expect(mp.direction).toBe(1);
|
|
17
|
+
expect(mp.speed).toBe(0);
|
|
18
|
+
expect(mp.loopMode).toBe('clamp');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('carries the provided speed and loop mode', () => {
|
|
22
|
+
const mp = createMotionPath(line(), 50, 'pingpong');
|
|
23
|
+
expect(mp.speed).toBe(50);
|
|
24
|
+
expect(mp.loopMode).toBe('pingpong');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A straight horizontal line from (0,0) to (100,0): arc length 100, tangent (1,0) everywhere.
|
|
29
|
+
function line(): Path {
|
|
30
|
+
const path = createPath();
|
|
31
|
+
appendPathMoveTo(path, 0, 0);
|
|
32
|
+
appendPathLineTo(path, 100, 0);
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path';
|
|
2
|
+
import type { Path } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createMotionPath } from './createMotionPath';
|
|
6
|
+
import { getMotionPathHeading } from './getMotionPathHeading';
|
|
7
|
+
import { setMotionPathDistance } from './setMotionPathDistance';
|
|
8
|
+
|
|
9
|
+
describe('getMotionPathHeading', () => {
|
|
10
|
+
it('reads 0 radians along a rightward line', () => {
|
|
11
|
+
const mp = createMotionPath(line());
|
|
12
|
+
setMotionPathDistance(mp, 50);
|
|
13
|
+
expect(getMotionPathHeading(mp)).toBeCloseTo(0, 6);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('differs before and after a corner', () => {
|
|
17
|
+
const mp = createMotionPath(corner());
|
|
18
|
+
setMotionPathDistance(mp, 50); // on the horizontal leg, heading 0
|
|
19
|
+
const before = getMotionPathHeading(mp);
|
|
20
|
+
setMotionPathDistance(mp, 150); // on the vertical leg, heading +pi/2
|
|
21
|
+
const after = getMotionPathHeading(mp);
|
|
22
|
+
expect(before).toBeCloseTo(0, 6);
|
|
23
|
+
expect(after).toBeCloseTo(Math.PI / 2, 6);
|
|
24
|
+
expect(after).not.toBeCloseTo(before, 3);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A straight horizontal line from (0,0) to (100,0): arc length 100.
|
|
29
|
+
function line(): Path {
|
|
30
|
+
const path = createPath();
|
|
31
|
+
appendPathMoveTo(path, 0, 0);
|
|
32
|
+
appendPathLineTo(path, 100, 0);
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// An L-shape: (0,0) -> (100,0) -> (100,100). Arc length 200, corner at distance 100.
|
|
37
|
+
function corner(): Path {
|
|
38
|
+
const path = createPath();
|
|
39
|
+
appendPathMoveTo(path, 0, 0);
|
|
40
|
+
appendPathLineTo(path, 100, 0);
|
|
41
|
+
appendPathLineTo(path, 100, 100);
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createVector2 } from '@flighthq/geometry';
|
|
2
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path';
|
|
3
|
+
import type { Path } from '@flighthq/types';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createMotionPath } from './createMotionPath';
|
|
7
|
+
import { getMotionPathPosition } from './getMotionPathPosition';
|
|
8
|
+
import { setMotionPathDistance } from './setMotionPathDistance';
|
|
9
|
+
|
|
10
|
+
describe('getMotionPathPosition', () => {
|
|
11
|
+
it('samples the point and tangent at the current distance', () => {
|
|
12
|
+
const mp = createMotionPath(line());
|
|
13
|
+
setMotionPathDistance(mp, 50);
|
|
14
|
+
const point = createVector2();
|
|
15
|
+
const tangent = createVector2();
|
|
16
|
+
const ok = getMotionPathPosition(mp, point, tangent);
|
|
17
|
+
expect(ok).toBe(true);
|
|
18
|
+
expect(point.x).toBeCloseTo(50, 6);
|
|
19
|
+
expect(point.y).toBeCloseTo(0, 6);
|
|
20
|
+
expect(tangent.x).toBeCloseTo(1, 6);
|
|
21
|
+
expect(tangent.y).toBeCloseTo(0, 6);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns false and leaves outputs unchanged for an empty path', () => {
|
|
25
|
+
const mp = createMotionPath(createPath());
|
|
26
|
+
const point = createVector2(7, 8);
|
|
27
|
+
const tangent = createVector2(9, 9);
|
|
28
|
+
const ok = getMotionPathPosition(mp, point, tangent);
|
|
29
|
+
expect(ok).toBe(false);
|
|
30
|
+
expect(point.x).toBe(7);
|
|
31
|
+
expect(point.y).toBe(8);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// A straight horizontal line from (0,0) to (100,0): arc length 100.
|
|
36
|
+
function line(): Path {
|
|
37
|
+
const path = createPath();
|
|
38
|
+
appendPathMoveTo(path, 0, 0);
|
|
39
|
+
appendPathLineTo(path, 100, 0);
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path';
|
|
2
|
+
import type { Path } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createMotionPath } from './createMotionPath';
|
|
6
|
+
import { getMotionPathProgress } from './getMotionPathProgress';
|
|
7
|
+
import { setMotionPathDistance } from './setMotionPathDistance';
|
|
8
|
+
|
|
9
|
+
describe('getMotionPathProgress', () => {
|
|
10
|
+
it('reports distance / length', () => {
|
|
11
|
+
const mp = createMotionPath(line());
|
|
12
|
+
setMotionPathDistance(mp, 25);
|
|
13
|
+
expect(getMotionPathProgress(mp)).toBeCloseTo(0.25, 6);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('reports 0 for a zero-length path', () => {
|
|
17
|
+
const mp = createMotionPath(createPath());
|
|
18
|
+
expect(getMotionPathProgress(mp)).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// A straight horizontal line from (0,0) to (100,0): arc length 100.
|
|
23
|
+
function line(): Path {
|
|
24
|
+
const path = createPath();
|
|
25
|
+
appendPathMoveTo(path, 0, 0);
|
|
26
|
+
appendPathLineTo(path, 100, 0);
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path';
|
|
2
|
+
import type { Path } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createMotionPath } from './createMotionPath';
|
|
6
|
+
import { setMotionPathDistance } from './setMotionPathDistance';
|
|
7
|
+
|
|
8
|
+
describe('setMotionPathDistance', () => {
|
|
9
|
+
it('seeks to an absolute arc-length distance', () => {
|
|
10
|
+
const mp = createMotionPath(line());
|
|
11
|
+
setMotionPathDistance(mp, 25);
|
|
12
|
+
expect(mp.distance).toBe(25);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('clamps distances above the length', () => {
|
|
16
|
+
const mp = createMotionPath(line());
|
|
17
|
+
setMotionPathDistance(mp, 150);
|
|
18
|
+
expect(mp.distance).toBe(100);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('clamps negative distances to 0', () => {
|
|
22
|
+
const mp = createMotionPath(line());
|
|
23
|
+
setMotionPathDistance(mp, -10);
|
|
24
|
+
expect(mp.distance).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A straight horizontal line from (0,0) to (100,0): arc length 100.
|
|
29
|
+
function line(): Path {
|
|
30
|
+
const path = createPath();
|
|
31
|
+
appendPathMoveTo(path, 0, 0);
|
|
32
|
+
appendPathLineTo(path, 100, 0);
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path';
|
|
2
|
+
import type { Path } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createMotionPath } from './createMotionPath';
|
|
6
|
+
import { setMotionPathProgress } from './setMotionPathProgress';
|
|
7
|
+
|
|
8
|
+
describe('setMotionPathProgress', () => {
|
|
9
|
+
it('seeks to a normalized progress along the path', () => {
|
|
10
|
+
const mp = createMotionPath(line());
|
|
11
|
+
setMotionPathProgress(mp, 0.25);
|
|
12
|
+
expect(mp.distance).toBeCloseTo(25, 6);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('clamps progress above 1 to the end', () => {
|
|
16
|
+
const mp = createMotionPath(line());
|
|
17
|
+
setMotionPathProgress(mp, 1.5);
|
|
18
|
+
expect(mp.distance).toBeCloseTo(100, 6);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('clamps negative progress to the start', () => {
|
|
22
|
+
const mp = createMotionPath(line());
|
|
23
|
+
setMotionPathProgress(mp, -0.5);
|
|
24
|
+
expect(mp.distance).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A straight horizontal line from (0,0) to (100,0): arc length 100.
|
|
29
|
+
function line(): Path {
|
|
30
|
+
const path = createPath();
|
|
31
|
+
appendPathMoveTo(path, 0, 0);
|
|
32
|
+
appendPathLineTo(path, 100, 0);
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path';
|
|
2
|
+
import type { Path } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createMotionPath } from './createMotionPath';
|
|
6
|
+
import { getMotionPathProgress } from './getMotionPathProgress';
|
|
7
|
+
import { updateMotionPath } from './updateMotionPath';
|
|
8
|
+
|
|
9
|
+
describe('updateMotionPath', () => {
|
|
10
|
+
it('advances distance by speed * deltaTime', () => {
|
|
11
|
+
const mp = createMotionPath(line(), 50);
|
|
12
|
+
updateMotionPath(mp, 1);
|
|
13
|
+
expect(mp.distance).toBeCloseTo(50, 6);
|
|
14
|
+
expect(getMotionPathProgress(mp)).toBeCloseTo(0.5, 6);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('clamp stops at the end and stays there', () => {
|
|
18
|
+
const mp = createMotionPath(line(), 50, 'clamp');
|
|
19
|
+
updateMotionPath(mp, 10); // 500 units of travel, past the 100-length end
|
|
20
|
+
expect(mp.distance).toBeCloseTo(100, 6);
|
|
21
|
+
expect(getMotionPathProgress(mp)).toBeCloseTo(1, 6);
|
|
22
|
+
updateMotionPath(mp, 5);
|
|
23
|
+
expect(mp.distance).toBeCloseTo(100, 6);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('loop wraps past the end modulo length', () => {
|
|
27
|
+
const mp = createMotionPath(line(), 30, 'loop');
|
|
28
|
+
updateMotionPath(mp, 4); // 120 -> 20
|
|
29
|
+
expect(mp.distance).toBeCloseTo(20, 6);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('loop wraps backward into [0, length)', () => {
|
|
33
|
+
const mp = createMotionPath(line(), 30, 'loop');
|
|
34
|
+
mp.direction = -1;
|
|
35
|
+
updateMotionPath(mp, 4); // 0 - 120 -> -120 mod 100 -> 80
|
|
36
|
+
expect(mp.distance).toBeCloseTo(80, 6);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('pingpong reflects at the end and flips direction to -1', () => {
|
|
40
|
+
const mp = createMotionPath(line(), 30, 'pingpong');
|
|
41
|
+
mp.distance = 90;
|
|
42
|
+
updateMotionPath(mp, 1); // 90 + 30 = 120 -> reflect to 80
|
|
43
|
+
expect(mp.distance).toBeCloseTo(80, 6);
|
|
44
|
+
expect(mp.direction).toBe(-1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('pingpong then travels back toward the start', () => {
|
|
48
|
+
const mp = createMotionPath(line(), 30, 'pingpong');
|
|
49
|
+
mp.distance = 90;
|
|
50
|
+
updateMotionPath(mp, 1); // -> 80, direction -1
|
|
51
|
+
updateMotionPath(mp, 1); // 80 - 30 = 50
|
|
52
|
+
expect(mp.distance).toBeCloseTo(50, 6);
|
|
53
|
+
expect(mp.direction).toBe(-1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('loop wraps a single large step across the path multiple times', () => {
|
|
57
|
+
const mp = createMotionPath(line(), 250, 'loop');
|
|
58
|
+
updateMotionPath(mp, 1); // 250 mod 100 -> 50
|
|
59
|
+
expect(mp.distance).toBeCloseTo(50, 6);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('pingpong resolves a single large step that crosses the path several times', () => {
|
|
63
|
+
const mp = createMotionPath(line(), 250, 'pingpong');
|
|
64
|
+
updateMotionPath(mp, 1); // 0->100 (flip), 100->0 (flip), 0->50 => 50, direction +1
|
|
65
|
+
expect(mp.distance).toBeCloseTo(50, 6);
|
|
66
|
+
expect(mp.direction).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('pingpong reflects again at the start and flips direction back to +1', () => {
|
|
70
|
+
const mp = createMotionPath(line(), 80, 'pingpong');
|
|
71
|
+
mp.distance = 50;
|
|
72
|
+
mp.direction = -1;
|
|
73
|
+
updateMotionPath(mp, 1); // 50 - 80 = -30 -> reflect to 30, direction +1
|
|
74
|
+
expect(mp.distance).toBeCloseTo(30, 6);
|
|
75
|
+
expect(mp.direction).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('deltaTime <= 0 is a no-op', () => {
|
|
79
|
+
const mp = createMotionPath(line(), 50);
|
|
80
|
+
updateMotionPath(mp, 0);
|
|
81
|
+
expect(mp.distance).toBe(0);
|
|
82
|
+
updateMotionPath(mp, -1);
|
|
83
|
+
expect(mp.distance).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('a zero-length path is a no-op', () => {
|
|
87
|
+
const mp = createMotionPath(empty(), 50);
|
|
88
|
+
expect(mp.length).toBe(0);
|
|
89
|
+
updateMotionPath(mp, 1);
|
|
90
|
+
expect(mp.distance).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// A straight horizontal line from (0,0) to (100,0): arc length 100.
|
|
95
|
+
function line(): Path {
|
|
96
|
+
const path = createPath();
|
|
97
|
+
appendPathMoveTo(path, 0, 0);
|
|
98
|
+
appendPathLineTo(path, 100, 0);
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// An empty path with no commands: arc length 0.
|
|
103
|
+
function empty(): Path {
|
|
104
|
+
return createPath();
|
|
105
|
+
}
|