@framefields/node-compositor 0.0.0-stage → 2.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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +22 -0
  2. package/.turbo/turbo-test.log +21 -0
  3. package/CHANGELOG.md +34 -0
  4. package/LICENCE +202 -0
  5. package/SKILL.md +370 -0
  6. package/dist/index-B0ir9Z7o.d.mts +139 -0
  7. package/dist/index-B0ir9Z7o.d.mts.map +1 -0
  8. package/dist/index.d.mts +230 -0
  9. package/dist/index.d.mts.map +1 -0
  10. package/dist/index.mjs +674 -0
  11. package/dist/index.mjs.map +1 -0
  12. package/dist/renderer.d.mts +2 -0
  13. package/dist/renderer.mjs +3 -0
  14. package/dist/renderers-DoYYCzgA.mjs +3326 -0
  15. package/dist/renderers-DoYYCzgA.mjs.map +1 -0
  16. package/package.json +51 -3
  17. package/src/index.ts +2 -0
  18. package/src/renderers/audio-processor.ts +123 -0
  19. package/src/renderers/container-bg.test.ts +22 -0
  20. package/src/renderers/container-bg.ts +15 -0
  21. package/src/renderers/index.ts +9 -0
  22. package/src/renderers/transform.test.ts +120 -0
  23. package/src/renderers/transform.ts +101 -0
  24. package/src/renderers/transform3d.test.ts +290 -0
  25. package/src/renderers/transform3d.ts +428 -0
  26. package/src/renderers/webgpu-renderer.ts +4038 -0
  27. package/src/shared/audio-reactive.test.ts +224 -0
  28. package/src/shared/bezier-solver.test.ts +92 -0
  29. package/src/shared/bezier-solver.ts +257 -0
  30. package/src/shared/compiler.test.ts +1309 -0
  31. package/src/shared/compiler.ts +996 -0
  32. package/src/shared/config.ts +44 -0
  33. package/src/shared/index.ts +3 -0
  34. package/src/shared/kinetic-typography.test.ts +138 -0
  35. package/src/shared/presets.test.ts +217 -0
  36. package/src/shared/presets.ts +644 -0
  37. package/src/shared/processor.ts +105 -0
  38. package/tsconfig.json +13 -0
  39. package/tsdown.config.ts +13 -0
  40. package/README.md +0 -3
@@ -0,0 +1,224 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { compileTimeline } from "./compiler.js";
3
+ import type { CompositorNodeConfig } from "./config.js";
4
+ import { processCompositor } from "./processor.js";
5
+
6
+ describe("Audio-Reactive Motion Graphics Engine Pipeline", () => {
7
+ it("connects signal streams to Compositor and drives beat-synchronized motion", () => {
8
+ // Mock dynamic signal streams (beat spikes at frame 12 and 24, continuous bass ramp)
9
+ const beatSamples = new Array(48).fill(0.0);
10
+ beatSamples[12] = 1.0; // Beat hit 1
11
+ beatSamples[24] = 1.0; // Beat hit 2
12
+
13
+ const bassSamples = new Array(48).fill(0.0).map((_, i) => i / 48);
14
+
15
+ const signalsByHandle = new Map<string, number[]>([
16
+ ["audio_beat_handle", beatSamples],
17
+ ["audio_bass_handle", bassSamples],
18
+ ]);
19
+
20
+ const compositorConfig: CompositorNodeConfig = {
21
+ width: 1920,
22
+ height: 1080,
23
+ volume: 1,
24
+ fps: 24,
25
+ mode: "Video",
26
+ layout: [
27
+ {
28
+ id: "bouncing-logo-star",
29
+ kind: "shape",
30
+ shapeType: "star",
31
+ starPoints: 5,
32
+ starInnerRadiusRatio: 0.5,
33
+ width: 300,
34
+ height: 300,
35
+ x: 810,
36
+ y: 390,
37
+ position: "absolute",
38
+ strokeWidth: 4,
39
+ strokeColor: "#38bdf8",
40
+ animation: {
41
+ tracks: [
42
+ {
43
+ id: "t-beat-scale",
44
+ prop: "scale",
45
+ source: {
46
+ type: "signal",
47
+ inputHandleId: "audio_beat_handle",
48
+ multiplier: 0.6,
49
+ offset: 1.0,
50
+ smoothingWindowFrames: 0,
51
+ },
52
+ keyframes: [],
53
+ },
54
+ {
55
+ id: "t-bass-trim",
56
+ prop: "trimEnd",
57
+ source: {
58
+ type: "signal",
59
+ inputHandleId: "audio_bass_handle",
60
+ multiplier: 0.8,
61
+ offset: 0.2,
62
+ smoothingWindowFrames: 0,
63
+ },
64
+ keyframes: [],
65
+ },
66
+ {
67
+ id: "t-procedural-wiggle",
68
+ prop: "rotation",
69
+ source: {
70
+ type: "wiggle",
71
+ frequency: 2.0,
72
+ amplitude: 15.0,
73
+ octaves: 2,
74
+ seed: 777,
75
+ },
76
+ keyframes: [],
77
+ },
78
+ ],
79
+ },
80
+ },
81
+ {
82
+ id: "staggered-bars-row",
83
+ kind: "flex",
84
+ dir: "row",
85
+ gap: 20,
86
+ staggerFrames: 4,
87
+ staggerDirection: "forward",
88
+ children: [
89
+ {
90
+ id: "bar-1",
91
+ kind: "shape",
92
+ width: 40,
93
+ height: 200,
94
+ startFrame: 0,
95
+ durationFrames: 24,
96
+ animation: {
97
+ tracks: [
98
+ {
99
+ id: "t-bar1-fade",
100
+ prop: "opacity",
101
+ keyframes: [
102
+ { id: "k1", frame: 0, value: 0 },
103
+ { id: "k2", frame: 12, value: 1 },
104
+ ],
105
+ },
106
+ ],
107
+ },
108
+ },
109
+ {
110
+ id: "bar-2",
111
+ kind: "shape",
112
+ width: 40,
113
+ height: 200,
114
+ startFrame: 0,
115
+ durationFrames: 24,
116
+ animation: {
117
+ tracks: [
118
+ {
119
+ id: "t-bar2-fade",
120
+ prop: "opacity",
121
+ keyframes: [
122
+ { id: "k3", frame: 0, value: 0 },
123
+ { id: "k4", frame: 12, value: 1 },
124
+ ],
125
+ },
126
+ ],
127
+ },
128
+ },
129
+ {
130
+ id: "bar-3",
131
+ kind: "shape",
132
+ width: 40,
133
+ height: 200,
134
+ startFrame: 0,
135
+ durationFrames: 24,
136
+ animation: {
137
+ tracks: [
138
+ {
139
+ id: "t-bar3-fade",
140
+ prop: "opacity",
141
+ keyframes: [
142
+ { id: "k5", frame: 0, value: 0 },
143
+ { id: "k6", frame: 12, value: 1 },
144
+ ],
145
+ },
146
+ ],
147
+ },
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ };
153
+
154
+ // 1. Process compositor into VirtualMediaData tree
155
+ const vm = processCompositor(
156
+ compositorConfig,
157
+ new Map(),
158
+ true,
159
+ signalsByHandle,
160
+ );
161
+
162
+ // Verify signals attached to operation
163
+ const operation = vm.operation as Record<string, unknown>;
164
+ expect(operation.signals).toBeDefined();
165
+ const signals = operation.signals as Record<string, unknown>;
166
+ expect(signals.audio_beat_handle).toBe(beatSamples);
167
+
168
+ // 2. Compile into timeline
169
+ const { tl, targetsById } = compileTimeline(
170
+ "audio-reactive-session",
171
+ vm,
172
+ { fps: 24, durationSec: 2 },
173
+ );
174
+
175
+ const logoStub = targetsById["bouncing-logo-star"];
176
+ expect(logoStub).toBeDefined();
177
+
178
+ // Check baseline at frame 0 (t = 0 sec):
179
+ // Beat signal = 0.0 -> scale = 0.0 * 0.6 + 1.0 = 1.0
180
+ tl.seek(0);
181
+ expect(logoStub.scale).toBeCloseTo(1.0, 3);
182
+ // Bass signal = 0.0 -> trimEnd = 0.0 * 0.8 + 0.2 = 0.2
183
+ expect(logoStub.trimEnd).toBeCloseTo(0.2, 3);
184
+ // Rotation wiggles within amplitude [-15, 15]
185
+ expect(Math.abs(logoStub.rotation as number)).toBeLessThanOrEqual(15);
186
+
187
+ // Check beat hit at frame 12 (t = 12/24 = 0.5 sec):
188
+ // Beat signal = 1.0 -> scale = 1.0 * 0.6 + 1.0 = 1.6!
189
+ tl.seek(12 / 24);
190
+ expect(logoStub.scale).toBeCloseTo(1.6, 3);
191
+
192
+ // Check post-beat settling at frame 14:
193
+ // Beat signal = 0.0 -> scale back to 1.0
194
+ tl.seek(14 / 24);
195
+ expect(logoStub.scale).toBeCloseTo(1.0, 3);
196
+
197
+ // Check second beat hit at frame 24 (t = 1.0 sec):
198
+ tl.seek(24 / 24);
199
+ expect(logoStub.scale).toBeCloseTo(1.6, 3);
200
+
201
+ // Check container staggering:
202
+ // bar-1: startFrame = 0
203
+ // bar-2: startFrame = 4 (stagger offset 1 * 4)
204
+ // bar-3: startFrame = 8 (stagger offset 2 * 4)
205
+ const bar1 = targetsById["bar-1"];
206
+ const bar2 = targetsById["bar-2"];
207
+ const bar3 = targetsById["bar-3"];
208
+
209
+ // At frame 0:
210
+ tl.seek(0);
211
+ expect(bar1.opacity).toBe(0);
212
+
213
+ // At frame 4 (bar-1 is halfway faded 0.33, bar-2 is just starting):
214
+ tl.seek(4 / 24);
215
+ expect(bar1.opacity).toBeGreaterThan(0);
216
+ expect(bar2.opacity).toBe(0);
217
+ expect(bar3.opacity).toBe(0);
218
+
219
+ // At frame 8 (bar-3 is just starting):
220
+ tl.seek(8 / 24);
221
+ expect(bar2.opacity).toBeGreaterThan(0);
222
+ expect(bar3.opacity).toBe(0);
223
+ });
224
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildSpatialSplineSegments,
4
+ catmullRomToBezier,
5
+ sampleBezierCurve,
6
+ sampleCubicBezierSegment,
7
+ solveBezierDerivative,
8
+ solveCubicBezier,
9
+ } from "./bezier-solver.js";
10
+
11
+ describe("bezier-solver", () => {
12
+ it("evaluates linear cubic bezier curve correctly", () => {
13
+ expect(solveCubicBezier(0, 0, 1, 1, 0)).toBe(0);
14
+ expect(solveCubicBezier(0, 0, 1, 1, 0.5)).toBeCloseTo(0.5, 3);
15
+ expect(solveCubicBezier(0, 0, 1, 1, 1)).toBe(1);
16
+ });
17
+
18
+ it("evaluates standard ease-in and ease-out curves correctly", () => {
19
+ // Ease-in: slow start, steep finish (e.g. 0.42, 0, 1, 1)
20
+ const easeInMid = solveCubicBezier(0.42, 0, 1, 1, 0.5);
21
+ expect(easeInMid).toBeLessThan(0.5);
22
+
23
+ // Ease-out: fast start, slow finish (e.g. 0, 0, 0.58, 1)
24
+ const easeOutMid = solveCubicBezier(0, 0, 0.58, 1, 0.5);
25
+ expect(easeOutMid).toBeGreaterThan(0.5);
26
+ });
27
+
28
+ it("calculates derivative (speed) accurately", () => {
29
+ // For linear, speed should be constant ~1.0
30
+ const linearSpeed = solveBezierDerivative(0, 0, 1, 1, 0.5);
31
+ expect(linearSpeed).toBeCloseTo(1.0, 2);
32
+
33
+ // Ease-out has higher initial speed and lower final speed
34
+ const easeOutStartSpeed = solveBezierDerivative(0.1, 0.8, 0.2, 1.0, 0.1);
35
+ const easeOutEndSpeed = solveBezierDerivative(0.1, 0.8, 0.2, 1.0, 0.9);
36
+ expect(easeOutStartSpeed).toBeGreaterThan(easeOutEndSpeed);
37
+ });
38
+
39
+ it("samples bezier curve points across 0..1 range", () => {
40
+ const samples = sampleBezierCurve(0.25, 0.1, 0.25, 1.0, 10);
41
+ expect(samples).toHaveLength(11);
42
+ expect(samples[0].t).toBe(0);
43
+ expect(samples[0].value).toBe(0);
44
+ expect(samples[10].t).toBe(1);
45
+ expect(samples[10].value).toBe(1);
46
+ });
47
+
48
+ it("converts Catmull-Rom to Cubic Bezier control points", () => {
49
+ const p0 = { x: 0, y: 0 };
50
+ const p1 = { x: 100, y: 0 };
51
+ const p2 = { x: 200, y: 100 };
52
+ const p3 = { x: 300, y: 100 };
53
+
54
+ const { c1, c2 } = catmullRomToBezier(p0, p1, p2, p3);
55
+ expect(c1.x).toBeCloseTo(100 + 200 / 6, 2);
56
+ expect(c1.y).toBeCloseTo(100 / 6, 2);
57
+ expect(c2.x).toBeCloseTo(200 - 200 / 6, 2);
58
+ });
59
+
60
+ it("builds spatial spline segments with custom or Catmull-Rom tangents", () => {
61
+ const points = [
62
+ { frame: 0, x: 0, y: 0, spatialTangentOut: { x: 50, y: -20 } },
63
+ { frame: 30, x: 200, y: 100, spatialTangentIn: { x: -30, y: 10 } },
64
+ { frame: 60, x: 400, y: 100 },
65
+ ];
66
+
67
+ const segments = buildSpatialSplineSegments(points);
68
+ expect(segments).toHaveLength(2);
69
+
70
+ // Segment 0 uses authored tangents
71
+ expect(segments[0].c1).toEqual({ x: 50, y: -20 });
72
+ expect(segments[0].c2).toEqual({ x: 170, y: 110 });
73
+
74
+ // Segment 1 uses Catmull-Rom tangent computation
75
+ expect(segments[1].p0).toEqual({ x: 200, y: 100 });
76
+ expect(segments[1].p1).toEqual({ x: 400, y: 100 });
77
+ });
78
+
79
+ it("samples cubic bezier segment points smoothly", () => {
80
+ const p0 = { x: 0, y: 0 };
81
+ const c1 = { x: 50, y: 100 };
82
+ const c2 = { x: 150, y: 100 };
83
+ const p1 = { x: 200, y: 0 };
84
+
85
+ const sampled = sampleCubicBezierSegment(p0, c1, c2, p1, 10);
86
+ expect(sampled).toHaveLength(11);
87
+ expect(sampled[0]).toEqual(p0);
88
+ expect(sampled[10]).toEqual(p1);
89
+ // Midpoint should have positive y due to control points
90
+ expect(sampled[5].y).toBeGreaterThan(50);
91
+ });
92
+ });
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Cubic Bezier and Speed Curve Solver for Motion Graphics
3
+ * Provides analytical/Newton-Raphson solvers for unit easing curves and 2D spatial trajectory splines.
4
+ */
5
+
6
+ export interface Point2D {
7
+ x: number;
8
+ y: number;
9
+ }
10
+
11
+ export interface SpatialKeyframePoint {
12
+ frame: number;
13
+ x: number;
14
+ y: number;
15
+ spatialTangentIn?: Point2D;
16
+ spatialTangentOut?: Point2D;
17
+ }
18
+
19
+ export interface SplineSegment {
20
+ p0: Point2D;
21
+ c1: Point2D;
22
+ c2: Point2D;
23
+ p1: Point2D;
24
+ startFrame: number;
25
+ endFrame: number;
26
+ }
27
+
28
+ /**
29
+ * Solves a cubic bezier curve defined by control points (x1, y1) and (x2, y2)
30
+ * for a given progress parameter t in [0, 1].
31
+ */
32
+ export function solveCubicBezier(
33
+ x1: number,
34
+ y1: number,
35
+ x2: number,
36
+ y2: number,
37
+ t: number,
38
+ ): number {
39
+ if (t <= 0) return 0;
40
+ if (t >= 1) return 1;
41
+
42
+ // Check for linear edge case
43
+ if (x1 === y1 && x2 === y2) return t;
44
+
45
+ // Find u such that x(u) = t
46
+ // x(u) = 3*(1-u)^2 * u * x1 + 3*(1-u) * u^2 * x2 + u^3
47
+ // dx/du = 3*(1-u)^2 * x1 + 6*(1-u)*u * (x2 - x1) + 3*u^2 * (1 - x2)
48
+ let u = t; // initial guess
49
+ for (let i = 0; i < 8; i++) {
50
+ const oneMinusU = 1 - u;
51
+ const oneMinusU2 = oneMinusU * oneMinusU;
52
+ const u2 = u * u;
53
+
54
+ const currentX =
55
+ 3 * oneMinusU2 * u * x1 + 3 * oneMinusU * u2 * x2 + u2 * u;
56
+ const diff = currentX - t;
57
+ if (Math.abs(diff) < 1e-6) break;
58
+
59
+ const dx =
60
+ 3 * oneMinusU2 * x1 +
61
+ 6 * oneMinusU * u * (x2 - x1) +
62
+ 3 * u2 * (1 - x2);
63
+
64
+ if (Math.abs(dx) < 1e-6) break;
65
+ u -= diff / dx;
66
+ u = Math.max(0, Math.min(1, u));
67
+ }
68
+
69
+ // Bisection fallback if Newton didn't converge within bounds
70
+ const oneMinusU = 1 - u;
71
+ const checkX =
72
+ 3 * oneMinusU * oneMinusU * u * x1 +
73
+ 3 * oneMinusU * u * u * x2 +
74
+ u * u * u;
75
+
76
+ if (Math.abs(checkX - t) > 1e-3) {
77
+ let low = 0;
78
+ let high = 1;
79
+ u = t;
80
+ for (let i = 0; i < 16; i++) {
81
+ const mid = (low + high) * 0.5;
82
+ const oMid = 1 - mid;
83
+ const xMid =
84
+ 3 * oMid * oMid * mid * x1 +
85
+ 3 * oMid * mid * mid * x2 +
86
+ mid * mid * mid;
87
+ if (Math.abs(xMid - t) < 1e-5) {
88
+ u = mid;
89
+ break;
90
+ }
91
+ if (xMid < t) low = mid;
92
+ else high = mid;
93
+ u = mid;
94
+ }
95
+ }
96
+
97
+ // Evaluate y(u)
98
+ const oU = 1 - u;
99
+ return (
100
+ 3 * oU * oU * u * y1 +
101
+ 3 * oU * u * u * y2 +
102
+ u * u * u
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Calculates the instantaneous speed (derivative |dy/dt|) at parameter t in [0, 1].
108
+ */
109
+ export function solveBezierDerivative(
110
+ x1: number,
111
+ y1: number,
112
+ x2: number,
113
+ y2: number,
114
+ t: number,
115
+ ): number {
116
+ const clampedT = Math.max(0, Math.min(1, t));
117
+ const eps = 1e-4;
118
+ const t0 = Math.max(0, clampedT - eps);
119
+ const t1 = Math.min(1, clampedT + eps);
120
+ const dt = t1 - t0;
121
+ if (dt <= 0) return 0;
122
+
123
+ const y0 = solveCubicBezier(x1, y1, x2, y2, t0);
124
+ const y1Val = solveCubicBezier(x1, y1, x2, y2, t1);
125
+ return Math.abs((y1Val - y0) / dt);
126
+ }
127
+
128
+ /**
129
+ * Generates an array of samples along the cubic bezier curve for visualization.
130
+ */
131
+ export function sampleBezierCurve(
132
+ x1: number,
133
+ y1: number,
134
+ x2: number,
135
+ y2: number,
136
+ steps = 60,
137
+ ): Array<{ t: number; value: number; speed: number }> {
138
+ const samples: Array<{ t: number; value: number; speed: number }> = [];
139
+ for (let i = 0; i <= steps; i++) {
140
+ const t = i / steps;
141
+ const value = solveCubicBezier(x1, y1, x2, y2, t);
142
+ const speed = solveBezierDerivative(x1, y1, x2, y2, t);
143
+ samples.push({ t, value, speed });
144
+ }
145
+ return samples;
146
+ }
147
+
148
+ /**
149
+ * Converts Catmull-Rom four-point configuration to cubic Bezier control points.
150
+ */
151
+ export function catmullRomToBezier(
152
+ p0: Point2D,
153
+ p1: Point2D,
154
+ p2: Point2D,
155
+ p3: Point2D,
156
+ ): { c1: Point2D; c2: Point2D } {
157
+ return {
158
+ c1: {
159
+ x: p1.x + (p2.x - p0.x) / 6,
160
+ y: p1.y + (p2.y - p0.y) / 6,
161
+ },
162
+ c2: {
163
+ x: p2.x - (p3.x - p1.x) / 6,
164
+ y: p2.y - (p3.y - p1.y) / 6,
165
+ },
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Builds smooth Bezier spline segments between an ordered array of spatial keyframes.
171
+ * Respects custom spatial tangents if authored; otherwise uses smooth Catmull-Rom.
172
+ */
173
+ export function buildSpatialSplineSegments(
174
+ points: SpatialKeyframePoint[],
175
+ ): SplineSegment[] {
176
+ if (points.length < 2) return [];
177
+
178
+ const segments: SplineSegment[] = [];
179
+
180
+ for (let i = 0; i < points.length - 1; i++) {
181
+ const curr = points[i];
182
+ const next = points[i + 1];
183
+
184
+ let c1: Point2D;
185
+ let c2: Point2D;
186
+
187
+ if (curr.spatialTangentOut) {
188
+ c1 = {
189
+ x: curr.x + curr.spatialTangentOut.x,
190
+ y: curr.y + curr.spatialTangentOut.y,
191
+ };
192
+ } else {
193
+ // Natural Catmull-Rom tangent
194
+ const prev = i > 0 ? points[i - 1] : curr;
195
+ const pNextNext = i + 2 < points.length ? points[i + 2] : next;
196
+ const cr = catmullRomToBezier(prev, curr, next, pNextNext);
197
+ c1 = cr.c1;
198
+ }
199
+
200
+ if (next.spatialTangentIn) {
201
+ c2 = {
202
+ x: next.x + next.spatialTangentIn.x,
203
+ y: next.y + next.spatialTangentIn.y,
204
+ };
205
+ } else {
206
+ const prev = i > 0 ? points[i - 1] : curr;
207
+ const pNextNext = i + 2 < points.length ? points[i + 2] : next;
208
+ const cr = catmullRomToBezier(prev, curr, next, pNextNext);
209
+ c2 = cr.c2;
210
+ }
211
+
212
+ segments.push({
213
+ p0: { x: curr.x, y: curr.y },
214
+ c1,
215
+ c2,
216
+ p1: { x: next.x, y: next.y },
217
+ startFrame: curr.frame,
218
+ endFrame: next.frame,
219
+ });
220
+ }
221
+
222
+ return segments;
223
+ }
224
+
225
+ /**
226
+ * Samples points along a cubic bezier segment between p0 and p1 with control points c1, c2.
227
+ */
228
+ export function sampleCubicBezierSegment(
229
+ p0: Point2D,
230
+ c1: Point2D,
231
+ c2: Point2D,
232
+ p1: Point2D,
233
+ steps = 20,
234
+ ): Point2D[] {
235
+ const pts: Point2D[] = [];
236
+ for (let i = 0; i <= steps; i++) {
237
+ const u = i / steps;
238
+ const oU = 1 - u;
239
+ const oU2 = oU * oU;
240
+ const oU3 = oU2 * oU;
241
+ const u2 = u * u;
242
+ const u3 = u2 * u;
243
+
244
+ const x =
245
+ oU3 * p0.x +
246
+ 3 * oU2 * u * c1.x +
247
+ 3 * oU * u2 * c2.x +
248
+ u3 * p1.x;
249
+ const y =
250
+ oU3 * p0.y +
251
+ 3 * oU2 * u * c1.y +
252
+ 3 * oU * u2 * c2.y +
253
+ u3 * p1.y;
254
+ pts.push({ x, y });
255
+ }
256
+ return pts;
257
+ }