@framefields/node-compositor 0.0.0-stage → 2.0.2

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 +32 -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,44 @@
1
+ import {
2
+ type CompositorProgramConfig,
3
+ CompositorProgramSchema,
4
+ } from "@framefields/compositions/program";
5
+ import { MediaMetadataSchema } from "@framefields/core";
6
+ import { ImageResultSchema, VideoResultSchema } from "@framefields/node-sdk";
7
+ import { z } from "zod";
8
+
9
+ /**
10
+ * v2 config — THE document (spec: SKILL.md).
11
+ * No `layers`, no `layerUpdates`, no aliases. The node config IS the
12
+ * composition program: strict zod, validated by the E-code pre-parse scan
13
+ * in `@framefields/compositions/program/validate`.
14
+ */
15
+ export const CompositorNodeConfigSchema = CompositorProgramSchema;
16
+ export type CompositorNodeConfig = CompositorProgramConfig;
17
+
18
+ /** Data types accepted by the variable composer inputs. */
19
+ export const VariableInputDataTypes = [
20
+ "Text",
21
+ "Image",
22
+ "Video",
23
+ "Audio",
24
+ "Caption",
25
+ "SVG",
26
+ "GIF",
27
+ "Lottie",
28
+ "Signal",
29
+ ] as const;
30
+ export type VariableInputDataType = (typeof VariableInputDataTypes)[number];
31
+
32
+ /** The Compositor op framing (used in the render tree, per processCompositor). */
33
+ export const CompositorOperationSchema = CompositorNodeConfigSchema.extend({
34
+ op: z.literal("Compositor"),
35
+ dataType: z.enum(["Video", "Image"]).optional(),
36
+ metadata: MediaMetadataSchema.optional(),
37
+ }).strict();
38
+
39
+ export type CompositorOperation = z.infer<typeof CompositorOperationSchema>;
40
+
41
+ export const CompositorResultSchema = z.union([
42
+ VideoResultSchema,
43
+ ImageResultSchema,
44
+ ]);
@@ -0,0 +1,3 @@
1
+ export * from "./compiler.js";
2
+ export * from "./config.js";
3
+ export * from "./presets.js";
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { compileTimeline } from "./compiler.js";
3
+
4
+ describe("Kinetic Typography Engine Integration (Section 5)", () => {
5
+ it("compiles keyframe tracks for offset, rangeStart, and rangeEnd", () => {
6
+ const virtualMedia = {
7
+ operation: {
8
+ op: "Compositor",
9
+ width: 1920,
10
+ height: 1080,
11
+ },
12
+ children: [
13
+ {
14
+ operation: {
15
+ op: "CompositorLayer",
16
+ id: "text-kinetic-1",
17
+ kind: "text",
18
+ text: "KINETIC MOTION GRAPHICS",
19
+ fontSize: 64,
20
+ startFrame: 0,
21
+ durationFrames: 60,
22
+ animators: [
23
+ {
24
+ id: "wave-rise",
25
+ unit: "character" as const,
26
+ rangeStart: 0,
27
+ rangeEnd: 1,
28
+ offset: -1,
29
+ easing: "power2.out",
30
+ transform: {
31
+ y: 40,
32
+ scale: 0.8,
33
+ rotation: 10,
34
+ rotationX: 90,
35
+ opacity: 0,
36
+ blur: 12,
37
+ },
38
+ },
39
+ ],
40
+ animation: {
41
+ tracks: [
42
+ {
43
+ id: "track-offset",
44
+ prop: "offset" as const,
45
+ keyframes: [
46
+ { id: "kf-0", frame: 0, value: -1 },
47
+ {
48
+ id: "kf-1",
49
+ frame: 48,
50
+ value: 1,
51
+ ease: { name: "power2" as const, dir: "out" as const },
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ },
59
+ ],
60
+ };
61
+
62
+ const { tl, targetsById } = compileTimeline("test-kinetic", virtualMedia, {
63
+ fps: 24,
64
+ durationSec: 3,
65
+ });
66
+
67
+ const stub = targetsById["text-kinetic-1"];
68
+ expect(stub).toBeDefined();
69
+
70
+ // Seek to frame 0
71
+ tl.seek(0);
72
+ expect(stub.offset).toBeCloseTo(-1);
73
+
74
+ // Seek to frame 48 (2.0s at 24fps)
75
+ tl.seek(2.0);
76
+ expect(stub.offset).toBeCloseTo(1);
77
+
78
+ // Seek to midpoint frame 24 (1.0s at 24fps)
79
+ tl.seek(1.0);
80
+ expect(stub.offset).toBeGreaterThan(-1);
81
+ expect(stub.offset).toBeLessThan(1);
82
+ });
83
+
84
+ it("supports word and line units on kinetic text animators", () => {
85
+ const virtualMedia = {
86
+ operation: {
87
+ op: "Compositor",
88
+ width: 1920,
89
+ height: 1080,
90
+ },
91
+ children: [
92
+ {
93
+ operation: {
94
+ op: "CompositorLayer",
95
+ id: "text-words",
96
+ kind: "text",
97
+ text: "HELLO WORLD FROM GATEWAI",
98
+ fontSize: 48,
99
+ startFrame: 0,
100
+ durationFrames: 30,
101
+ animators: [
102
+ {
103
+ id: "word-pop",
104
+ unit: "word" as const,
105
+ rangeStart: 0.2,
106
+ rangeEnd: 0.8,
107
+ offset: 0,
108
+ transform: {
109
+ scale: 1.3,
110
+ y: -15,
111
+ },
112
+ },
113
+ {
114
+ id: "line-slide",
115
+ unit: "line" as const,
116
+ rangeStart: 0,
117
+ rangeEnd: 1,
118
+ offset: 0,
119
+ transform: {
120
+ x: 20,
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ },
126
+ ],
127
+ };
128
+
129
+ const { targetsById } = compileTimeline("test-words", virtualMedia, {
130
+ fps: 24,
131
+ durationSec: 2,
132
+ });
133
+
134
+ const stub = targetsById["text-words"];
135
+ expect(stub).toBeDefined();
136
+ expect(stub.scale).toBe(1);
137
+ });
138
+ });
@@ -0,0 +1,217 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ chipsForLayer,
4
+ getPresetDetails,
5
+ materializePreset,
6
+ } from "./presets.js";
7
+
8
+ describe("Compositor Presets", () => {
9
+ it("should return correct details for standard preset types", () => {
10
+ expect(getPresetDetails("fade-in")).toEqual({
11
+ kind: "entrance",
12
+ label: "Fade In",
13
+ });
14
+ expect(getPresetDetails("fade-out")).toEqual({
15
+ kind: "exit",
16
+ label: "Fade Out",
17
+ });
18
+ expect(getPresetDetails("pulse")).toEqual({
19
+ kind: "emphasis",
20
+ label: "Pulse",
21
+ });
22
+ });
23
+
24
+ it("should materialize fade-in preset correctly", () => {
25
+ const base = {
26
+ x: 100,
27
+ y: 200,
28
+ scale: 1.5,
29
+ rotation: 45,
30
+ opacity: 0.8,
31
+ durationFrames: 100,
32
+ };
33
+
34
+ const tracks = materializePreset(base, "fade-in", {
35
+ width: 1920,
36
+ height: 1080,
37
+ fps: 24,
38
+ presetGroupId: "fade-in-group",
39
+ });
40
+ expect(tracks).toHaveLength(1);
41
+ expect(tracks[0].prop).toBe("opacity");
42
+ expect(tracks[0].keyframes).toHaveLength(2);
43
+ expect(tracks[0].keyframes[0].frame).toBe(0);
44
+ expect(tracks[0].keyframes[0].value).toBe(0);
45
+ expect(tracks[0].keyframes[0].presetGroupId).toBe("fade-in-group");
46
+ expect(tracks[0].keyframes[1].frame).toBe(24); // default duration matches fps (24)
47
+ expect(tracks[0].keyframes[1].value).toBe(0.8); // layerBase.opacity
48
+ expect(tracks[0].keyframes[1].ease).toEqual({ name: "none", dir: "out" });
49
+ });
50
+
51
+ it("should materialize slide-in-left preset correctly", () => {
52
+ const base = {
53
+ x: 100,
54
+ y: 200,
55
+ scale: 1.5,
56
+ rotation: 45,
57
+ opacity: 0.8,
58
+ durationFrames: 100,
59
+ };
60
+
61
+ const tracks = materializePreset(base, "slide-in-left", {
62
+ width: 1920,
63
+ height: 1080,
64
+ fps: 24,
65
+ presetGroupId: "slide-left-group",
66
+ });
67
+ expect(tracks).toHaveLength(1);
68
+ expect(tracks[0].prop).toBe("x");
69
+ expect(tracks[0].keyframes).toHaveLength(2);
70
+ expect(tracks[0].keyframes[0].value).toBe(100 - 1920); // x - width
71
+ expect(tracks[0].keyframes[1].value).toBe(100); // base.x
72
+ });
73
+
74
+ it("should derive chips from layer keyframes correctly", () => {
75
+ const layer = {
76
+ animation: {
77
+ tracks: [
78
+ {
79
+ id: "track-1",
80
+ prop: "opacity" as const,
81
+ keyframes: [
82
+ { id: "kf-1", frame: 0, value: 0, presetGroupId: "fade-in_123" },
83
+ {
84
+ id: "kf-2",
85
+ frame: 24,
86
+ value: 1,
87
+ ease: { name: "none" as const, dir: "out" as const },
88
+ presetGroupId: "fade-in_123",
89
+ },
90
+ ],
91
+ },
92
+ {
93
+ id: "track-2",
94
+ prop: "scale" as const,
95
+ keyframes: [
96
+ { id: "kf-3", frame: 48, value: 1, presetGroupId: "pulse_456" },
97
+ {
98
+ id: "kf-4",
99
+ frame: 60,
100
+ value: 1.2,
101
+ ease: { name: "sine" as const, dir: "inOut" as const },
102
+ presetGroupId: "pulse_456",
103
+ },
104
+ ],
105
+ },
106
+ ],
107
+ },
108
+ };
109
+
110
+ const chips = chipsForLayer(layer);
111
+ expect(chips).toHaveLength(2);
112
+
113
+ const fadeInChip = chips.find((c) => c.presetType === "fade-in");
114
+ expect(fadeInChip).toBeDefined();
115
+ expect(fadeInChip?.kind).toBe("entrance");
116
+ expect(fadeInChip?.durationFrames).toBe(24);
117
+ expect(fadeInChip?.ease).toEqual({ name: "none", dir: "out" });
118
+
119
+ const pulseChip = chips.find((c) => c.presetType === "pulse");
120
+ expect(pulseChip).toBeDefined();
121
+ expect(pulseChip?.kind).toBe("emphasis");
122
+ expect(pulseChip?.durationFrames).toBe(12);
123
+ });
124
+
125
+ it("should materialize text animation presets (typewriter, word-reveal, line-reveal, karaoke)", () => {
126
+ const base = {
127
+ x: 0,
128
+ y: 0,
129
+ scale: 1,
130
+ rotation: 0,
131
+ opacity: 1,
132
+ durationFrames: 60,
133
+ };
134
+
135
+ for (const preset of [
136
+ "typewriter",
137
+ "word-reveal",
138
+ "line-reveal",
139
+ "karaoke",
140
+ ]) {
141
+ const tracks = materializePreset(base, preset, {
142
+ width: 1920,
143
+ height: 1080,
144
+ fps: 30,
145
+ presetGroupId: `${preset}_grp`,
146
+ });
147
+ expect(tracks).toHaveLength(1);
148
+ expect(tracks[0].prop).toBe("text");
149
+ expect(tracks[0].keyframes).toHaveLength(2);
150
+ expect(tracks[0].keyframes[0].value).toBe(0);
151
+ expect(tracks[0].keyframes[0].frame).toBe(0);
152
+ expect(tracks[0].keyframes[1].value).toBe(1);
153
+ expect(tracks[0].keyframes[1].frame).toBe(30);
154
+ expect(tracks[0].keyframes[1].presetType).toBe(preset);
155
+
156
+ const details = getPresetDetails(preset);
157
+ expect(details.kind).toBe("entrance");
158
+ }
159
+ });
160
+
161
+ it("should materialize modern motion presets (tracking-expand, blur-reveal, kinetic-wave, slice-wipe)", () => {
162
+ const base = {
163
+ x: 100,
164
+ y: 200,
165
+ scale: 1,
166
+ rotation: 0,
167
+ opacity: 1,
168
+ width: 400,
169
+ height: 300,
170
+ durationFrames: 60,
171
+ };
172
+
173
+ // Tracking expand
174
+ const teTracks = materializePreset(base, "tracking-expand", {
175
+ width: 1920,
176
+ height: 1080,
177
+ fps: 30,
178
+ });
179
+ expect(teTracks).toHaveLength(1);
180
+ expect(teTracks[0].prop).toBe("letterSpacing");
181
+ expect(teTracks[0].keyframes[1].value).toBe(24);
182
+
183
+ // Blur reveal
184
+ const brTracks = materializePreset(base, "blur-reveal", {
185
+ width: 1920,
186
+ height: 1080,
187
+ fps: 30,
188
+ });
189
+ expect(brTracks).toHaveLength(1);
190
+ expect(brTracks[0].prop).toBe("opacity");
191
+ expect(brTracks[0].keyframes[0].value).toBe(0);
192
+ expect(brTracks[0].keyframes[1].value).toBe(1);
193
+
194
+ // Kinetic wave
195
+ const kwTracks = materializePreset(base, "kinetic-wave", {
196
+ width: 1920,
197
+ height: 1080,
198
+ fps: 30,
199
+ });
200
+ expect(kwTracks).toHaveLength(1);
201
+ expect(kwTracks[0].prop).toBe("y");
202
+ expect(kwTracks[0].repeat).toBe(3);
203
+ expect(kwTracks[0].yoyo).toBe(true);
204
+
205
+ // Slice wipe
206
+ const swTracks = materializePreset(base, "slice-wipe", {
207
+ width: 1920,
208
+ height: 1080,
209
+ fps: 30,
210
+ });
211
+ expect(swTracks).toHaveLength(1);
212
+ expect(swTracks[0].prop).toBe("x");
213
+ expect(swTracks[0].keyframes[0].value).toBe(100 - 400);
214
+ expect(swTracks[0].keyframes[1].value).toBe(100);
215
+ });
216
+ });
217
+