@flighthq/velocity 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.
@@ -0,0 +1,4 @@
1
+ export { contributeTransformVelocity } from './transformVelocity';
2
+ export { addVelocity, beginVelocityFrame, clampVelocity, contributeVelocity, copyVelocity, createVelocityField, dampVelocity, ensureVelocitySample, getVelocity, hasVelocity, isVelocityZero, lengthOfVelocity, lerpVelocity, normalizeVelocity, scaleVelocity, subtractVelocity, suppressVelocity, zeroVelocity, } from './velocityField';
3
+ export { getVelocitySampleAt } from './velocitySample';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { contributeTransformVelocity } from './transformVelocity';
2
+ export { addVelocity, beginVelocityFrame, clampVelocity, contributeVelocity, copyVelocity, createVelocityField, dampVelocity, ensureVelocitySample, getVelocity, hasVelocity, isVelocityZero, lengthOfVelocity, lerpVelocity, normalizeVelocity, scaleVelocity, subtractVelocity, suppressVelocity, zeroVelocity, } from './velocityField';
3
+ export { getVelocitySampleAt } from './velocitySample';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Transform2DNode, VelocityField } from '@flighthq/types';
2
+ export declare function contributeTransformVelocity<Traits extends object>(field: VelocityField, root: Readonly<Transform2DNode<Traits>>): void;
3
+ //# sourceMappingURL=transformVelocity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformVelocity.d.ts","sourceRoot":"","sources":["../src/transformVelocity.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAatE,wBAAgB,2BAA2B,CAAC,MAAM,SAAS,MAAM,EAC/D,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,GACtC,IAAI,CAEN"}
@@ -0,0 +1,44 @@
1
+ import { copyMatrix, createMatrix } from '@flighthq/geometry';
2
+ import { ensureNodeWorldTransformMatrix, getNodeChildAt, getNodeChildCount, getNodeWorldTransformMatrix, } from '@flighthq/node';
3
+ import { ensureVelocitySample } from './velocityField';
4
+ // The default velocity contributor. Walks `root`'s subtree top-down, deriving each node's screen-space
5
+ // velocity from the delta of its world transform since the previous frame, then commits the current world
6
+ // transform as the new previous. This is the "any transform is velocity" baseline — a tween, physics
7
+ // step, camera move, or manual edit all change world transforms and so produce velocity here for free.
8
+ //
9
+ // Nodes an explicit contributor already set this frame keep that velocity (their explicitFrameId matches),
10
+ // but still have their previous transform updated, so order relative to explicit contributors does not
11
+ // matter. Run once per frame after beginVelocityFrame. Velocity is in node units; a producer scales by
12
+ // the render pixel ratio.
13
+ export function contributeTransformVelocity(field, root) {
14
+ visitTransformVelocity(field, root);
15
+ }
16
+ function visitTransformVelocity(field, node) {
17
+ const mutableNode = node;
18
+ ensureNodeWorldTransformMatrix(mutableNode);
19
+ const world = getNodeWorldTransformMatrix(mutableNode);
20
+ const sample = ensureVelocitySample(field, node);
21
+ if (sample.explicitFrameId !== field.frameId) {
22
+ if (sample.previousWorldTransform !== null) {
23
+ sample.velocity.x = world.tx - sample.previousWorldTransform.tx;
24
+ sample.velocity.y = world.ty - sample.previousWorldTransform.ty;
25
+ }
26
+ else {
27
+ sample.velocity.x = 0;
28
+ sample.velocity.y = 0;
29
+ }
30
+ sample.lastFrameId = field.frameId;
31
+ }
32
+ if (sample.previousWorldTransform === null)
33
+ sample.previousWorldTransform = createMatrix();
34
+ copyMatrix(sample.previousWorldTransform, world);
35
+ // Children of a transform node in a homogeneous display/sprite graph are themselves transform nodes;
36
+ // the hierarchy type does not carry that, so the trait is asserted.
37
+ const count = getNodeChildCount(mutableNode);
38
+ for (let i = 0; i < count; i++) {
39
+ const child = getNodeChildAt(mutableNode, i);
40
+ if (child !== null)
41
+ visitTransformVelocity(field, child);
42
+ }
43
+ }
44
+ //# sourceMappingURL=transformVelocity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformVelocity.js","sourceRoot":"","sources":["../src/transformVelocity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EACL,8BAA8B,EAC9B,cAAc,EACd,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,uGAAuG;AACvG,0GAA0G;AAC1G,qGAAqG;AACrG,uGAAuG;AACvG,EAAE;AACF,2GAA2G;AAC3G,uGAAuG;AACvG,uGAAuG;AACvG,0BAA0B;AAC1B,MAAM,UAAU,2BAA2B,CACzC,KAAoB,EACpB,IAAuC;IAEvC,sBAAsB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,sBAAsB,CAC7B,KAAoB,EACpB,IAAuC;IAEvC,MAAM,WAAW,GAAG,IAA+B,CAAC;IACpD,8BAA8B,CAAC,WAAW,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,oBAAoB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAEjD,IAAI,MAAM,CAAC,eAAe,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;QAC7C,IAAI,MAAM,CAAC,sBAAsB,KAAK,IAAI,EAAE,CAAC;YAC3C,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,sBAAsB,CAAC,EAAE,CAAC;YAChE,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,sBAAsB,CAAC,EAAE,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC;YACtB,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,MAAM,CAAC,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC;IACrC,CAAC;IAED,IAAI,MAAM,CAAC,sBAAsB,KAAK,IAAI;QAAE,MAAM,CAAC,sBAAsB,GAAG,YAAY,EAAE,CAAC;IAC3F,UAAU,CAAC,MAAM,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAEjD,qGAAqG;IACrG,oEAAoE;IACpE,MAAM,KAAK,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAC7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,cAAc,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QAC7C,IAAI,KAAK,KAAK,IAAI;YAAE,sBAAsB,CAAC,KAAK,EAAE,KAAqD,CAAC,CAAC;IAC3G,CAAC;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { Velocity2D, VelocityField, VelocitySample } from '@flighthq/types';
2
+ export declare function addVelocity(out: Velocity2D, a: Readonly<Velocity2D>, b: Readonly<Velocity2D>): Velocity2D;
3
+ export declare function beginVelocityFrame(field: VelocityField): void;
4
+ export declare function clampVelocity(out: Velocity2D, velocity: Readonly<Velocity2D>, maxLength: number): Velocity2D;
5
+ export declare function contributeVelocity(field: VelocityField, source: object, x: number, y: number): void;
6
+ export declare function copyVelocity(out: Velocity2D, source: Readonly<Velocity2D>): Velocity2D;
7
+ export declare function createVelocityField(): VelocityField;
8
+ export declare function dampVelocity(out: Velocity2D, current: Readonly<Velocity2D>, previous: Readonly<Velocity2D>, factor: number): Velocity2D;
9
+ export declare function ensureVelocitySample(field: VelocityField, source: object): VelocitySample;
10
+ export declare function getVelocity(field: VelocityField, source: object, out: Velocity2D): Velocity2D;
11
+ export declare function hasVelocity(field: VelocityField, source: object): boolean;
12
+ export declare function isVelocityZero(velocity: Readonly<Velocity2D>, epsilon?: number): boolean;
13
+ export declare function lengthOfVelocity(velocity: Readonly<Velocity2D>): number;
14
+ export declare function lerpVelocity(out: Velocity2D, a: Readonly<Velocity2D>, b: Readonly<Velocity2D>, t: number): Velocity2D;
15
+ export declare function normalizeVelocity(out: Velocity2D, source: Readonly<Velocity2D>): Velocity2D;
16
+ export declare function scaleVelocity(out: Velocity2D, velocity: Readonly<Velocity2D>, scale: number): Velocity2D;
17
+ export declare function subtractVelocity(out: Velocity2D, a: Readonly<Velocity2D>, b: Readonly<Velocity2D>): Velocity2D;
18
+ export declare function suppressVelocity(field: VelocityField, source: object): void;
19
+ export declare function zeroVelocity(out: Velocity2D): Velocity2D;
20
+ //# sourceMappingURL=velocityField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"velocityField.d.ts","sourceRoot":"","sources":["../src/velocityField.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AASjF,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,CAMzG;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAE7D;AAID,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,CAc5G;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAMnG;AAGD,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,CAMtF;AAED,wBAAgB,mBAAmB,IAAI,aAAa,CAEnD;AAKD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC7B,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC9B,MAAM,EAAE,MAAM,GACb,UAAU,CAQZ;AAGD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAOzF;AAID,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,UAAU,CAU7F;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKzE;AAID,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAGxF;AAGD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,CAEvE;AAGD,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAMrH;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,CAa3F;AAID,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAMxG;AAGD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,CAM9G;AAGD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAE3E;AAGD,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAIxD"}
@@ -0,0 +1,153 @@
1
+ // The VelocityField is the generic seam: any system (physics, tween, camera, manual edit) contributes a
2
+ // source object's screen-space velocity for the current frame, and any consumer reads it. The accessors
3
+ // are source-agnostic — they key on any object, not only nodes (a batch, a custom entity, etc.). The
4
+ // node-graph-specific baseline is contributeTransformVelocity. Explicit contributions win over the
5
+ // baseline regardless of call order, via per-sample explicitFrameId.
6
+ // Adds velocity contributions: `out = a + b`.
7
+ export function addVelocity(out, a, b) {
8
+ const ax = a.x;
9
+ const ay = a.y;
10
+ out.x = ax + b.x;
11
+ out.y = ay + b.y;
12
+ return out;
13
+ }
14
+ export function beginVelocityFrame(field) {
15
+ field.frameId++;
16
+ }
17
+ // Clamps velocity to `maxLength` in-place style: `out` may equal `velocity` (alias-safe).
18
+ // The most common motion-blur safety helper — prevents runaway blur lengths on fast objects.
19
+ export function clampVelocity(out, velocity, maxLength) {
20
+ const vx = velocity.x;
21
+ const vy = velocity.y;
22
+ const lenSq = vx * vx + vy * vy;
23
+ const maxSq = maxLength * maxLength;
24
+ if (lenSq > maxSq && lenSq > 0) {
25
+ const scale = maxLength / Math.sqrt(lenSq);
26
+ out.x = vx * scale;
27
+ out.y = vy * scale;
28
+ }
29
+ else {
30
+ out.x = vx;
31
+ out.y = vy;
32
+ }
33
+ return out;
34
+ }
35
+ export function contributeVelocity(field, source, x, y) {
36
+ const sample = ensureVelocitySample(field, source);
37
+ sample.velocity.x = x;
38
+ sample.velocity.y = y;
39
+ sample.lastFrameId = field.frameId;
40
+ sample.explicitFrameId = field.frameId;
41
+ }
42
+ // Copies one velocity value into another (alias-safe: out may equal source).
43
+ export function copyVelocity(out, source) {
44
+ const sx = source.x;
45
+ const sy = source.y;
46
+ out.x = sx;
47
+ out.y = sy;
48
+ return out;
49
+ }
50
+ export function createVelocityField() {
51
+ return { samples: new WeakMap(), frameId: 0 };
52
+ }
53
+ // Exponential moving average (EMA) of velocity across frames. Useful for jitter-free motion buffers.
54
+ // `factor` in (0, 1]: 1 = no smoothing (current wins), 0.1 = heavy smoothing (previous dominates).
55
+ // Alias-safe: `out` may equal `current` or `previous`.
56
+ export function dampVelocity(out, current, previous, factor) {
57
+ const cx = current.x;
58
+ const cy = current.y;
59
+ const px = previous.x;
60
+ const py = previous.y;
61
+ out.x = cx * factor + px * (1 - factor);
62
+ out.y = cy * factor + py * (1 - factor);
63
+ return out;
64
+ }
65
+ // Get-or-create a source's per-frame sample. Shared with the transform-delta contributor in this package.
66
+ export function ensureVelocitySample(field, source) {
67
+ let sample = field.samples.get(source);
68
+ if (sample === undefined) {
69
+ sample = { previousWorldTransform: null, velocity: { x: 0, y: 0 }, lastFrameId: -1, explicitFrameId: -1 };
70
+ field.samples.set(source, sample);
71
+ }
72
+ return sample;
73
+ }
74
+ // Writes the source's current-frame velocity into `out`. Returns zero velocity for sources with no
75
+ // sample or whose sample is stale (not touched this frame).
76
+ export function getVelocity(field, source, out) {
77
+ const sample = field.samples.get(source);
78
+ if (sample === undefined || sample.lastFrameId !== field.frameId) {
79
+ out.x = 0;
80
+ out.y = 0;
81
+ return out;
82
+ }
83
+ out.x = sample.velocity.x;
84
+ out.y = sample.velocity.y;
85
+ return out;
86
+ }
87
+ export function hasVelocity(field, source) {
88
+ const sample = field.samples.get(source);
89
+ return (sample !== undefined && sample.lastFrameId === field.frameId && (sample.velocity.x !== 0 || sample.velocity.y !== 0));
90
+ }
91
+ // Returns true if the velocity vector's magnitude is within `epsilon` of zero. Useful for skipping
92
+ // velocity-buffer writes on effectively-still objects.
93
+ export function isVelocityZero(velocity, epsilon) {
94
+ const e = epsilon ?? 0;
95
+ return Math.abs(velocity.x) <= e && Math.abs(velocity.y) <= e;
96
+ }
97
+ // Returns the magnitude (length) of the velocity vector.
98
+ export function lengthOfVelocity(velocity) {
99
+ return Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
100
+ }
101
+ // Linearly interpolates between two velocity values. Alias-safe: `out` may equal `a` or `b`.
102
+ export function lerpVelocity(out, a, b, t) {
103
+ const ax = a.x;
104
+ const ay = a.y;
105
+ out.x = ax + (b.x - ax) * t;
106
+ out.y = ay + (b.y - ay) * t;
107
+ return out;
108
+ }
109
+ // Normalizes the velocity vector to unit length. Returns the zero vector when length is zero.
110
+ // Alias-safe: `out` may equal `source`.
111
+ export function normalizeVelocity(out, source) {
112
+ const sx = source.x;
113
+ const sy = source.y;
114
+ const len = Math.sqrt(sx * sx + sy * sy);
115
+ if (len > 0) {
116
+ const inv = 1 / len;
117
+ out.x = sx * inv;
118
+ out.y = sy * inv;
119
+ }
120
+ else {
121
+ out.x = 0;
122
+ out.y = 0;
123
+ }
124
+ return out;
125
+ }
126
+ // Scales a velocity by a scalar factor. Used for pixel-ratio conversion and unit normalization.
127
+ // Alias-safe: `out` may equal `velocity`.
128
+ export function scaleVelocity(out, velocity, scale) {
129
+ const vx = velocity.x;
130
+ const vy = velocity.y;
131
+ out.x = vx * scale;
132
+ out.y = vy * scale;
133
+ return out;
134
+ }
135
+ // Subtracts `b` from `a`: `out = a - b`. Alias-safe.
136
+ export function subtractVelocity(out, a, b) {
137
+ const ax = a.x;
138
+ const ay = a.y;
139
+ out.x = ax - b.x;
140
+ out.y = ay - b.y;
141
+ return out;
142
+ }
143
+ // Zeroes a source's velocity this frame — a teleport/cut that must not smear.
144
+ export function suppressVelocity(field, source) {
145
+ contributeVelocity(field, source, 0, 0);
146
+ }
147
+ // Sets both components to zero.
148
+ export function zeroVelocity(out) {
149
+ out.x = 0;
150
+ out.y = 0;
151
+ return out;
152
+ }
153
+ //# sourceMappingURL=velocityField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"velocityField.js","sourceRoot":"","sources":["../src/velocityField.ts"],"names":[],"mappings":"AAEA,wGAAwG;AACxG,wGAAwG;AACxG,qGAAqG;AACrG,mGAAmG;AACnG,qEAAqE;AAErE,8CAA8C;AAC9C,MAAM,UAAU,WAAW,CAAC,GAAe,EAAE,CAAuB,EAAE,CAAuB;IAC3F,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACjB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,KAAK,CAAC,OAAO,EAAE,CAAC;AAClB,CAAC;AAED,0FAA0F;AAC1F,6FAA6F;AAC7F,MAAM,UAAU,aAAa,CAAC,GAAe,EAAE,QAA8B,EAAE,SAAiB;IAC9F,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;IACtB,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;IACtB,MAAM,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;IACpC,IAAI,KAAK,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACnB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QACX,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACb,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAoB,EAAE,MAAc,EAAE,CAAS,EAAE,CAAS;IAC3F,MAAM,MAAM,GAAG,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACnD,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC;IACtB,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC;IACtB,MAAM,CAAC,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC;IACnC,MAAM,CAAC,eAAe,GAAG,KAAK,CAAC,OAAO,CAAC;AACzC,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,MAA4B;IACxE,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC;IACpB,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC;IACpB,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACX,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACX,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,EAAE,OAAO,EAAE,IAAI,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAChD,CAAC;AAED,qGAAqG;AACrG,mGAAmG;AACnG,uDAAuD;AACvD,MAAM,UAAU,YAAY,CAC1B,GAAe,EACf,OAA6B,EAC7B,QAA8B,EAC9B,MAAc;IAEd,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;IACtB,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;IACtB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;IACxC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;IACxC,OAAO,GAAG,CAAC;AACb,CAAC;AAED,0GAA0G;AAC1G,MAAM,UAAU,oBAAoB,CAAC,KAAoB,EAAE,MAAc;IACvE,IAAI,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,EAAE,sBAAsB,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,EAAE,CAAC;QAC1G,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,mGAAmG;AACnG,4DAA4D;AAC5D,MAAM,UAAU,WAAW,CAAC,KAAoB,EAAE,MAAc,EAAE,GAAe;IAC/E,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;QACjE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACV,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACV,OAAO,GAAG,CAAC;IACb,CAAC;IACD,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1B,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1B,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAoB,EAAE,MAAc;IAC9D,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO,CACL,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,KAAK,KAAK,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,CACrH,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,uDAAuD;AACvD,MAAM,UAAU,cAAc,CAAC,QAA8B,EAAE,OAAgB;IAC7E,MAAM,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAChE,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,gBAAgB,CAAC,QAA8B;IAC7D,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,CAAuB,EAAE,CAAuB,EAAE,CAAS;IACvG,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IAC5B,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,GAAG,CAAC;AACb,CAAC;AAED,8FAA8F;AAC9F,wCAAwC;AACxC,MAAM,UAAU,iBAAiB,CAAC,GAAe,EAAE,MAA4B;IAC7E,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC;IACpB,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC;IACpB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QACpB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC;QACjB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC;IACnB,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACV,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,gGAAgG;AAChG,0CAA0C;AAC1C,MAAM,UAAU,aAAa,CAAC,GAAe,EAAE,QAA8B,EAAE,KAAa;IAC1F,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;IACtB,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;IACtB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IACnB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IACnB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,gBAAgB,CAAC,GAAe,EAAE,CAAuB,EAAE,CAAuB;IAChG,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACjB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,gBAAgB,CAAC,KAAoB,EAAE,MAAc;IACnE,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,YAAY,CAAC,GAAe;IAC1C,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IACV,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Matrix, Velocity2D, VelocitySample } from '@flighthq/types';
2
+ export declare function getVelocitySampleAt(sample: Readonly<VelocitySample>, currentWorldTransform: Readonly<Matrix>, pointX: number, pointY: number, out: Velocity2D): Velocity2D;
3
+ //# sourceMappingURL=velocitySample.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"velocitySample.d.ts","sourceRoot":"","sources":["../src/velocitySample.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAQ1E,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,EAChC,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,EACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,UAAU,GACd,UAAU,CAoBZ"}
@@ -0,0 +1,26 @@
1
+ // Returns the per-pixel screen-space velocity at local-space point (`pointX`, `pointY`) by computing
2
+ // `current·p − previous·p` using the sample's stored previousWorldTransform and the node's current world
3
+ // transform. Writes the result into `out`. Returns zero when the sample has no previous transform.
4
+ //
5
+ // Use when the full affine delta (rotation + scale) at a specific point is needed, such as when writing
6
+ // a per-pixel velocity buffer from a screen-space fragment shader.
7
+ export function getVelocitySampleAt(sample, currentWorldTransform, pointX, pointY, out) {
8
+ if (sample.previousWorldTransform === null) {
9
+ out.x = 0;
10
+ out.y = 0;
11
+ return out;
12
+ }
13
+ // Read both transforms into locals before writing out (alias safety, though out is Velocity2D here).
14
+ const cx = currentWorldTransform.a * pointX + currentWorldTransform.c * pointY + currentWorldTransform.tx;
15
+ const cy = currentWorldTransform.b * pointX + currentWorldTransform.d * pointY + currentWorldTransform.ty;
16
+ const px = sample.previousWorldTransform.a * pointX +
17
+ sample.previousWorldTransform.c * pointY +
18
+ sample.previousWorldTransform.tx;
19
+ const py = sample.previousWorldTransform.b * pointX +
20
+ sample.previousWorldTransform.d * pointY +
21
+ sample.previousWorldTransform.ty;
22
+ out.x = cx - px;
23
+ out.y = cy - py;
24
+ return out;
25
+ }
26
+ //# sourceMappingURL=velocitySample.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"velocitySample.js","sourceRoot":"","sources":["../src/velocitySample.ts"],"names":[],"mappings":"AAEA,qGAAqG;AACrG,yGAAyG;AACzG,mGAAmG;AACnG,EAAE;AACF,wGAAwG;AACxG,mEAAmE;AACnE,MAAM,UAAU,mBAAmB,CACjC,MAAgC,EAChC,qBAAuC,EACvC,MAAc,EACd,MAAc,EACd,GAAe;IAEf,IAAI,MAAM,CAAC,sBAAsB,KAAK,IAAI,EAAE,CAAC;QAC3C,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACV,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACV,OAAO,GAAG,CAAC;IACb,CAAC;IACD,qGAAqG;IACrG,MAAM,EAAE,GAAG,qBAAqB,CAAC,CAAC,GAAG,MAAM,GAAG,qBAAqB,CAAC,CAAC,GAAG,MAAM,GAAG,qBAAqB,CAAC,EAAE,CAAC;IAC1G,MAAM,EAAE,GAAG,qBAAqB,CAAC,CAAC,GAAG,MAAM,GAAG,qBAAqB,CAAC,CAAC,GAAG,MAAM,GAAG,qBAAqB,CAAC,EAAE,CAAC;IAC1G,MAAM,EAAE,GACN,MAAM,CAAC,sBAAsB,CAAC,CAAC,GAAG,MAAM;QACxC,MAAM,CAAC,sBAAsB,CAAC,CAAC,GAAG,MAAM;QACxC,MAAM,CAAC,sBAAsB,CAAC,EAAE,CAAC;IACnC,MAAM,EAAE,GACN,MAAM,CAAC,sBAAsB,CAAC,CAAC,GAAG,MAAM;QACxC,MAAM,CAAC,sBAAsB,CAAC,CAAC,GAAG,MAAM;QACxC,MAAM,CAAC,sBAAsB,CAAC,EAAE,CAAC;IACnC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC;IAChB,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@flighthq/velocity",
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/node": "0.1.0",
32
+ "@flighthq/types": "0.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@flighthq/displayobject": "*",
36
+ "typescript": "^5.3.0"
37
+ },
38
+ "description": "Generic per-node velocity field and contributors (transform-delta baseline + explicit overrides)",
39
+ "sideEffects": false
40
+ }
@@ -0,0 +1,58 @@
1
+ import { createDisplayObject } from '@flighthq/displayobject';
2
+ import { invalidateNodeLocalTransform } from '@flighthq/node';
3
+
4
+ import { contributeTransformVelocity } from './transformVelocity';
5
+ import { beginVelocityFrame, contributeVelocity, createVelocityField, getVelocity } from './velocityField';
6
+
7
+ describe('contributeTransformVelocity', () => {
8
+ it('reports zero velocity on the first frame', () => {
9
+ const field = createVelocityField();
10
+ const obj = createDisplayObject();
11
+ contributeTransformVelocity(field, obj);
12
+ expect(getVelocity(field, obj, { x: 0, y: 0 })).toEqual({ x: 0, y: 0 });
13
+ });
14
+
15
+ it('derives velocity from the world-transform delta between frames', () => {
16
+ const field = createVelocityField();
17
+ const obj = createDisplayObject();
18
+ contributeTransformVelocity(field, obj);
19
+
20
+ obj.x = 10;
21
+ obj.y = -5;
22
+ invalidateNodeLocalTransform(obj);
23
+ beginVelocityFrame(field);
24
+ contributeTransformVelocity(field, obj);
25
+
26
+ expect(getVelocity(field, obj, { x: 0, y: 0 })).toEqual({ x: 10, y: -5 });
27
+ });
28
+
29
+ it('lets an explicit contribution override the derived delta regardless of call order', () => {
30
+ const field = createVelocityField();
31
+ const obj = createDisplayObject();
32
+ contributeTransformVelocity(field, obj);
33
+
34
+ obj.x = 100;
35
+ invalidateNodeLocalTransform(obj);
36
+ beginVelocityFrame(field);
37
+ contributeVelocity(field, obj, 2, 2); // explicit set before the baseline runs
38
+ contributeTransformVelocity(field, obj);
39
+
40
+ expect(getVelocity(field, obj, { x: 0, y: 0 })).toEqual({ x: 2, y: 2 });
41
+ });
42
+
43
+ it('still updates previousWorldTransform when an explicit override is in effect', () => {
44
+ const field = createVelocityField();
45
+ const obj = createDisplayObject();
46
+ contributeTransformVelocity(field, obj);
47
+ obj.x = 10;
48
+ invalidateNodeLocalTransform(obj);
49
+ beginVelocityFrame(field);
50
+ contributeVelocity(field, obj, 99, 99);
51
+ contributeTransformVelocity(field, obj);
52
+ // On the third frame with no explicit override, previousWorldTransform should reflect frame 2 position.
53
+ beginVelocityFrame(field);
54
+ contributeTransformVelocity(field, obj);
55
+ // No movement between frame 2 and frame 3 — velocity should be zero.
56
+ expect(getVelocity(field, obj, { x: 0, y: 0 })).toEqual({ x: 0, y: 0 });
57
+ });
58
+ });
@@ -0,0 +1,295 @@
1
+ import {
2
+ addVelocity,
3
+ beginVelocityFrame,
4
+ clampVelocity,
5
+ contributeVelocity,
6
+ copyVelocity,
7
+ createVelocityField,
8
+ dampVelocity,
9
+ ensureVelocitySample,
10
+ getVelocity,
11
+ hasVelocity,
12
+ isVelocityZero,
13
+ lengthOfVelocity,
14
+ lerpVelocity,
15
+ normalizeVelocity,
16
+ scaleVelocity,
17
+ subtractVelocity,
18
+ suppressVelocity,
19
+ zeroVelocity,
20
+ } from './velocityField';
21
+
22
+ describe('addVelocity', () => {
23
+ it('adds two velocity vectors', () => {
24
+ const out = { x: 0, y: 0 };
25
+ addVelocity(out, { x: 1, y: 2 }, { x: 3, y: 4 });
26
+ expect(out).toEqual({ x: 4, y: 6 });
27
+ });
28
+
29
+ it('is alias-safe when out equals a', () => {
30
+ const a = { x: 5, y: 3 };
31
+ addVelocity(a, a, { x: 1, y: 1 });
32
+ expect(a).toEqual({ x: 6, y: 4 });
33
+ });
34
+ });
35
+
36
+ describe('beginVelocityFrame', () => {
37
+ it('advances the frame id', () => {
38
+ const field = createVelocityField();
39
+ const before = field.frameId;
40
+ beginVelocityFrame(field);
41
+ expect(field.frameId).toBe(before + 1);
42
+ });
43
+ });
44
+
45
+ describe('clampVelocity', () => {
46
+ it('returns velocity unchanged when length is within maxLength', () => {
47
+ const out = { x: 0, y: 0 };
48
+ clampVelocity(out, { x: 3, y: 0 }, 5);
49
+ expect(out).toEqual({ x: 3, y: 0 });
50
+ });
51
+
52
+ it('clamps to maxLength when length exceeds maxLength', () => {
53
+ const out = { x: 0, y: 0 };
54
+ clampVelocity(out, { x: 10, y: 0 }, 5);
55
+ expect(out.x).toBeCloseTo(5);
56
+ expect(out.y).toBeCloseTo(0);
57
+ });
58
+
59
+ it('clamps a diagonal velocity to maxLength', () => {
60
+ const out = { x: 0, y: 0 };
61
+ clampVelocity(out, { x: 3, y: 4 }, 2.5);
62
+ expect(lengthOfVelocity(out)).toBeCloseTo(2.5);
63
+ });
64
+
65
+ it('is alias-safe when out equals velocity', () => {
66
+ const v = { x: 10, y: 0 };
67
+ clampVelocity(v, v, 5);
68
+ expect(v.x).toBeCloseTo(5);
69
+ expect(v.y).toBeCloseTo(0);
70
+ });
71
+
72
+ it('handles zero-length velocity without division error', () => {
73
+ const out = { x: 0, y: 0 };
74
+ clampVelocity(out, { x: 0, y: 0 }, 5);
75
+ expect(out).toEqual({ x: 0, y: 0 });
76
+ });
77
+ });
78
+
79
+ describe('contributeVelocity', () => {
80
+ it('records an explicit velocity readable this frame', () => {
81
+ const field = createVelocityField();
82
+ const source = {};
83
+ contributeVelocity(field, source, 3, -4);
84
+ expect(getVelocity(field, source, { x: 0, y: 0 })).toEqual({ x: 3, y: -4 });
85
+ });
86
+
87
+ it('accepts any object source, not only nodes', () => {
88
+ const field = createVelocityField();
89
+ const batchLike = { instances: 12 };
90
+ contributeVelocity(field, batchLike, 1, 2);
91
+ expect(getVelocity(field, batchLike, { x: 0, y: 0 })).toEqual({ x: 1, y: 2 });
92
+ });
93
+ });
94
+
95
+ describe('copyVelocity', () => {
96
+ it('copies x and y', () => {
97
+ const out = { x: 0, y: 0 };
98
+ copyVelocity(out, { x: 5, y: -3 });
99
+ expect(out).toEqual({ x: 5, y: -3 });
100
+ });
101
+
102
+ it('is alias-safe when out equals source', () => {
103
+ const v = { x: 7, y: 2 };
104
+ copyVelocity(v, v);
105
+ expect(v).toEqual({ x: 7, y: 2 });
106
+ });
107
+ });
108
+
109
+ describe('createVelocityField', () => {
110
+ it('starts at frame zero and returns zero for unknown sources', () => {
111
+ const field = createVelocityField();
112
+ expect(field.frameId).toBe(0);
113
+ expect(getVelocity(field, {}, { x: 0, y: 0 })).toEqual({ x: 0, y: 0 });
114
+ });
115
+ });
116
+
117
+ describe('dampVelocity', () => {
118
+ it('returns current at factor=1 (no smoothing)', () => {
119
+ const out = { x: 0, y: 0 };
120
+ dampVelocity(out, { x: 10, y: 5 }, { x: 0, y: 0 }, 1);
121
+ expect(out).toEqual({ x: 10, y: 5 });
122
+ });
123
+
124
+ it('returns previous at factor=0 (full smoothing)', () => {
125
+ const out = { x: 0, y: 0 };
126
+ dampVelocity(out, { x: 10, y: 5 }, { x: 2, y: 3 }, 0);
127
+ expect(out).toEqual({ x: 2, y: 3 });
128
+ });
129
+
130
+ it('blends at factor=0.5', () => {
131
+ const out = { x: 0, y: 0 };
132
+ dampVelocity(out, { x: 10, y: 0 }, { x: 0, y: 0 }, 0.5);
133
+ expect(out).toEqual({ x: 5, y: 0 });
134
+ });
135
+
136
+ it('is alias-safe when out equals current', () => {
137
+ const current = { x: 10, y: 0 };
138
+ dampVelocity(current, current, { x: 0, y: 0 }, 0.5);
139
+ expect(current).toEqual({ x: 5, y: 0 });
140
+ });
141
+ });
142
+
143
+ describe('ensureVelocitySample', () => {
144
+ it('returns the same sample on repeat calls', () => {
145
+ const field = createVelocityField();
146
+ const source = {};
147
+ expect(ensureVelocitySample(field, source)).toBe(ensureVelocitySample(field, source));
148
+ });
149
+ });
150
+
151
+ describe('getVelocity', () => {
152
+ it('returns zero once the sample is stale after the frame advances', () => {
153
+ const field = createVelocityField();
154
+ const source = {};
155
+ contributeVelocity(field, source, 5, 5);
156
+ beginVelocityFrame(field);
157
+ expect(getVelocity(field, source, { x: 0, y: 0 })).toEqual({ x: 0, y: 0 });
158
+ });
159
+ });
160
+
161
+ describe('hasVelocity', () => {
162
+ it('is true for nonzero velocity this frame and false for suppressed', () => {
163
+ const field = createVelocityField();
164
+ const moving = {};
165
+ const still = {};
166
+ contributeVelocity(field, moving, 1, 0);
167
+ suppressVelocity(field, still);
168
+ expect(hasVelocity(field, moving)).toBe(true);
169
+ expect(hasVelocity(field, still)).toBe(false);
170
+ });
171
+ });
172
+
173
+ describe('isVelocityZero', () => {
174
+ it('returns true for exact zero', () => {
175
+ expect(isVelocityZero({ x: 0, y: 0 })).toBe(true);
176
+ });
177
+
178
+ it('returns false for nonzero', () => {
179
+ expect(isVelocityZero({ x: 0.001, y: 0 })).toBe(false);
180
+ });
181
+
182
+ it('respects epsilon', () => {
183
+ expect(isVelocityZero({ x: 0.001, y: 0 }, 0.01)).toBe(true);
184
+ expect(isVelocityZero({ x: 0.1, y: 0 }, 0.01)).toBe(false);
185
+ });
186
+ });
187
+
188
+ describe('lengthOfVelocity', () => {
189
+ it('returns the magnitude of the velocity vector', () => {
190
+ expect(lengthOfVelocity({ x: 3, y: 4 })).toBeCloseTo(5);
191
+ });
192
+
193
+ it('returns 0 for zero vector', () => {
194
+ expect(lengthOfVelocity({ x: 0, y: 0 })).toBe(0);
195
+ });
196
+ });
197
+
198
+ describe('lerpVelocity', () => {
199
+ it('returns a at t=0', () => {
200
+ const out = { x: 0, y: 0 };
201
+ lerpVelocity(out, { x: 1, y: 2 }, { x: 9, y: 8 }, 0);
202
+ expect(out).toEqual({ x: 1, y: 2 });
203
+ });
204
+
205
+ it('returns b at t=1', () => {
206
+ const out = { x: 0, y: 0 };
207
+ lerpVelocity(out, { x: 1, y: 2 }, { x: 9, y: 8 }, 1);
208
+ expect(out).toEqual({ x: 9, y: 8 });
209
+ });
210
+
211
+ it('interpolates at t=0.5', () => {
212
+ const out = { x: 0, y: 0 };
213
+ lerpVelocity(out, { x: 0, y: 0 }, { x: 10, y: 4 }, 0.5);
214
+ expect(out).toEqual({ x: 5, y: 2 });
215
+ });
216
+
217
+ it('is alias-safe when out equals a', () => {
218
+ const a = { x: 0, y: 0 };
219
+ lerpVelocity(a, a, { x: 10, y: 0 }, 0.5);
220
+ expect(a).toEqual({ x: 5, y: 0 });
221
+ });
222
+ });
223
+
224
+ describe('normalizeVelocity', () => {
225
+ it('returns unit vector for nonzero input', () => {
226
+ const out = { x: 0, y: 0 };
227
+ normalizeVelocity(out, { x: 3, y: 4 });
228
+ expect(lengthOfVelocity(out)).toBeCloseTo(1);
229
+ expect(out.x).toBeCloseTo(0.6);
230
+ expect(out.y).toBeCloseTo(0.8);
231
+ });
232
+
233
+ it('returns zero vector for zero input', () => {
234
+ const out = { x: 0, y: 0 };
235
+ normalizeVelocity(out, { x: 0, y: 0 });
236
+ expect(out).toEqual({ x: 0, y: 0 });
237
+ });
238
+
239
+ it('is alias-safe when out equals source', () => {
240
+ const v = { x: 5, y: 0 };
241
+ normalizeVelocity(v, v);
242
+ expect(v).toEqual({ x: 1, y: 0 });
243
+ });
244
+ });
245
+
246
+ describe('scaleVelocity', () => {
247
+ it('scales by a factor', () => {
248
+ const out = { x: 0, y: 0 };
249
+ scaleVelocity(out, { x: 3, y: -2 }, 2);
250
+ expect(out).toEqual({ x: 6, y: -4 });
251
+ });
252
+
253
+ it('is alias-safe when out equals velocity', () => {
254
+ const v = { x: 4, y: 2 };
255
+ scaleVelocity(v, v, 0.5);
256
+ expect(v).toEqual({ x: 2, y: 1 });
257
+ });
258
+ });
259
+
260
+ describe('subtractVelocity', () => {
261
+ it('subtracts b from a', () => {
262
+ const out = { x: 0, y: 0 };
263
+ subtractVelocity(out, { x: 5, y: 3 }, { x: 2, y: 1 });
264
+ expect(out).toEqual({ x: 3, y: 2 });
265
+ });
266
+
267
+ it('is alias-safe when out equals a', () => {
268
+ const a = { x: 5, y: 3 };
269
+ subtractVelocity(a, a, { x: 2, y: 1 });
270
+ expect(a).toEqual({ x: 3, y: 2 });
271
+ });
272
+ });
273
+
274
+ describe('suppressVelocity', () => {
275
+ it('zeroes a previously set velocity', () => {
276
+ const field = createVelocityField();
277
+ const source = {};
278
+ contributeVelocity(field, source, 9, 9);
279
+ suppressVelocity(field, source);
280
+ expect(getVelocity(field, source, { x: 0, y: 0 })).toEqual({ x: 0, y: 0 });
281
+ });
282
+ });
283
+
284
+ describe('zeroVelocity', () => {
285
+ it('sets both components to zero', () => {
286
+ const v = { x: 5, y: -3 };
287
+ zeroVelocity(v);
288
+ expect(v).toEqual({ x: 0, y: 0 });
289
+ });
290
+
291
+ it('returns the out reference', () => {
292
+ const v = { x: 1, y: 1 };
293
+ expect(zeroVelocity(v)).toBe(v);
294
+ });
295
+ });
@@ -0,0 +1,61 @@
1
+ import { createMatrix } from '@flighthq/geometry';
2
+
3
+ import { createVelocityField, ensureVelocitySample } from './velocityField';
4
+ import { getVelocitySampleAt } from './velocitySample';
5
+
6
+ describe('getVelocitySampleAt', () => {
7
+ it('returns zero when the sample has no previousWorldTransform', () => {
8
+ const field = createVelocityField();
9
+ const source = {};
10
+ const sample = ensureVelocitySample(field, source);
11
+ const identity = createMatrix();
12
+ const out = { x: 99, y: 99 };
13
+ getVelocitySampleAt(sample, identity, 0, 0, out);
14
+ expect(out).toEqual({ x: 0, y: 0 });
15
+ });
16
+
17
+ it('returns translation delta at the origin when transform is translation-only', () => {
18
+ const field = createVelocityField();
19
+ const source = {};
20
+ const sample = ensureVelocitySample(field, source);
21
+ sample.previousWorldTransform = createMatrix(1, 0, 0, 1, 3, 4);
22
+ const current = createMatrix(1, 0, 0, 1, 8, 9);
23
+ const out = { x: 0, y: 0 };
24
+ getVelocitySampleAt(sample, current, 0, 0, out);
25
+ expect(out).toEqual({ x: 5, y: 5 });
26
+ });
27
+
28
+ it('computes affine reprojection for a non-origin point', () => {
29
+ const field = createVelocityField();
30
+ const source = {};
31
+ const sample = ensureVelocitySample(field, source);
32
+ sample.previousWorldTransform = createMatrix();
33
+ const current = createMatrix(1, 0, 0, 1, 2, 3);
34
+ const out = { x: 0, y: 0 };
35
+ getVelocitySampleAt(sample, current, 1, 0, out);
36
+ expect(out).toEqual({ x: 2, y: 3 });
37
+ });
38
+
39
+ it('computes correct velocity for a rotating transform at a non-origin point', () => {
40
+ const field = createVelocityField();
41
+ const source = {};
42
+ const sample = ensureVelocitySample(field, source);
43
+ sample.previousWorldTransform = createMatrix();
44
+ const rotated90 = createMatrix(0, 1, -1, 0, 0, 0);
45
+ const out = { x: 0, y: 0 };
46
+ getVelocitySampleAt(sample, rotated90, 1, 0, out);
47
+ expect(out.x).toBeCloseTo(-1);
48
+ expect(out.y).toBeCloseTo(1);
49
+ });
50
+
51
+ it('is alias-safe when out references a different object (basic alias check)', () => {
52
+ const field = createVelocityField();
53
+ const source = {};
54
+ const sample = ensureVelocitySample(field, source);
55
+ sample.previousWorldTransform = createMatrix(1, 0, 0, 1, 1, 2);
56
+ const current = createMatrix(1, 0, 0, 1, 4, 6);
57
+ const out = { x: 0, y: 0 };
58
+ getVelocitySampleAt(sample, current, 0, 0, out);
59
+ expect(out).toEqual({ x: 3, y: 4 });
60
+ });
61
+ });