@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.
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +21 -0
- package/CHANGELOG.md +34 -0
- package/LICENCE +202 -0
- package/SKILL.md +370 -0
- package/dist/index-B0ir9Z7o.d.mts +139 -0
- package/dist/index-B0ir9Z7o.d.mts.map +1 -0
- package/dist/index.d.mts +230 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +674 -0
- package/dist/index.mjs.map +1 -0
- package/dist/renderer.d.mts +2 -0
- package/dist/renderer.mjs +3 -0
- package/dist/renderers-DoYYCzgA.mjs +3326 -0
- package/dist/renderers-DoYYCzgA.mjs.map +1 -0
- package/package.json +51 -3
- package/src/index.ts +2 -0
- package/src/renderers/audio-processor.ts +123 -0
- package/src/renderers/container-bg.test.ts +22 -0
- package/src/renderers/container-bg.ts +15 -0
- package/src/renderers/index.ts +9 -0
- package/src/renderers/transform.test.ts +120 -0
- package/src/renderers/transform.ts +101 -0
- package/src/renderers/transform3d.test.ts +290 -0
- package/src/renderers/transform3d.ts +428 -0
- package/src/renderers/webgpu-renderer.ts +4038 -0
- package/src/shared/audio-reactive.test.ts +224 -0
- package/src/shared/bezier-solver.test.ts +92 -0
- package/src/shared/bezier-solver.ts +257 -0
- package/src/shared/compiler.test.ts +1309 -0
- package/src/shared/compiler.ts +996 -0
- package/src/shared/config.ts +44 -0
- package/src/shared/index.ts +3 -0
- package/src/shared/kinetic-typography.test.ts +138 -0
- package/src/shared/presets.test.ts +217 -0
- package/src/shared/presets.ts +644 -0
- package/src/shared/processor.ts +105 -0
- package/tsconfig.json +13 -0
- package/tsdown.config.ts +13 -0
- 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,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
|
+
|