@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,644 @@
1
+ import type {
2
+ AnimatableProp,
3
+ AnimationTrack,
4
+ EaseRef,
5
+ Keyframe,
6
+ } from "@framefields/compositions/program";
7
+
8
+ export interface LayerBase {
9
+ x: number;
10
+ y: number;
11
+ scale: number;
12
+ rotation: number;
13
+ opacity: number;
14
+ width?: number;
15
+ height?: number;
16
+ durationFrames: number;
17
+ }
18
+
19
+ export interface PresetChip {
20
+ groupId: string;
21
+ presetType: string;
22
+ kind: "entrance" | "exit" | "emphasis";
23
+ label: string;
24
+ durationFrames: number;
25
+ ease?: EaseRef;
26
+ }
27
+
28
+ function generateId() {
29
+ return Math.random().toString(36).substring(2, 11);
30
+ }
31
+
32
+ export function getPresetDetails(presetType: string): {
33
+ kind: "entrance" | "exit" | "emphasis";
34
+ label: string;
35
+ } {
36
+ switch (presetType) {
37
+ case "fade-in":
38
+ return { kind: "entrance", label: "Fade In" };
39
+ case "fade-out":
40
+ return { kind: "exit", label: "Fade Out" };
41
+ case "typewriter":
42
+ return { kind: "entrance", label: "Typewriter" };
43
+ case "word-reveal":
44
+ return { kind: "entrance", label: "Word Reveal" };
45
+ case "line-reveal":
46
+ return { kind: "entrance", label: "Line Reveal" };
47
+ case "karaoke":
48
+ return { kind: "entrance", label: "Karaoke" };
49
+ case "slide-in-left":
50
+ return { kind: "entrance", label: "Slide In Left" };
51
+ case "slide-in-right":
52
+ return { kind: "entrance", label: "Slide In Right" };
53
+ case "slide-in-top":
54
+ return { kind: "entrance", label: "Slide In Top" };
55
+ case "slide-in-bottom":
56
+ return { kind: "entrance", label: "Slide In Bottom" };
57
+ case "slide-out-left":
58
+ return { kind: "exit", label: "Slide Out Left" };
59
+ case "slide-out-right":
60
+ return { kind: "exit", label: "Slide Out Right" };
61
+ case "slide-out-top":
62
+ return { kind: "exit", label: "Slide Out Top" };
63
+ case "slide-out-bottom":
64
+ return { kind: "exit", label: "Slide Out Bottom" };
65
+ case "zoom-in":
66
+ return { kind: "entrance", label: "Zoom In" };
67
+ case "zoom-out":
68
+ return { kind: "exit", label: "Zoom Out" };
69
+ case "bounce-in":
70
+ return { kind: "entrance", label: "Bounce In" };
71
+ case "rotate-cw":
72
+ return { kind: "emphasis", label: "Rotate Clockwise" };
73
+ case "rotate-ccw":
74
+ return { kind: "emphasis", label: "Rotate Counter-Clockwise" };
75
+ case "pulse":
76
+ return { kind: "emphasis", label: "Pulse" };
77
+ case "shake":
78
+ return { kind: "emphasis", label: "Shake" };
79
+ case "wobble":
80
+ return { kind: "emphasis", label: "Wobble" };
81
+ case "bounce":
82
+ return { kind: "emphasis", label: "Bounce" };
83
+ case "tracking-expand":
84
+ return { kind: "entrance", label: "Tracking Expand" };
85
+ case "blur-reveal":
86
+ return { kind: "entrance", label: "Blur Reveal" };
87
+ case "kinetic-wave":
88
+ return { kind: "emphasis", label: "Kinetic Wave" };
89
+ case "slice-wipe":
90
+ return { kind: "entrance", label: "Slice Wipe" };
91
+ default:
92
+ return { kind: "emphasis", label: presetType };
93
+ }
94
+ }
95
+
96
+ export function chipsForLayer(
97
+ layer:
98
+ | {
99
+ animation?: { tracks?: AnimationTrack[] };
100
+ }
101
+ | null
102
+ | undefined,
103
+ ): PresetChip[] {
104
+ const groups: Record<
105
+ string,
106
+ { minFrame: number; maxFrame: number; ease?: EaseRef; presetType?: string }
107
+ > = {};
108
+ const tracks = layer?.animation?.tracks ?? [];
109
+
110
+ for (const track of tracks) {
111
+ for (const kf of track.keyframes) {
112
+ if (kf.presetGroupId) {
113
+ if (!groups[kf.presetGroupId]) {
114
+ groups[kf.presetGroupId] = {
115
+ minFrame: kf.frame,
116
+ maxFrame: kf.frame,
117
+ ease: kf.ease,
118
+ presetType: kf.presetType,
119
+ };
120
+ } else {
121
+ const g = groups[kf.presetGroupId];
122
+ if (kf.frame < g.minFrame) g.minFrame = kf.frame;
123
+ if (kf.frame > g.maxFrame) {
124
+ g.maxFrame = kf.frame;
125
+ if (kf.ease) g.ease = kf.ease;
126
+ }
127
+ if (kf.presetType && !g.presetType) {
128
+ g.presetType = kf.presetType;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ const chips: PresetChip[] = [];
136
+ for (const [groupId, data] of Object.entries(groups)) {
137
+ const presetType =
138
+ data.presetType ||
139
+ (groupId.includes("_")
140
+ ? groupId.slice(0, groupId.lastIndexOf("_"))
141
+ : groupId) ||
142
+ "";
143
+ const { kind, label } = getPresetDetails(presetType);
144
+ chips.push({
145
+ groupId,
146
+ presetType,
147
+ kind,
148
+ label,
149
+ durationFrames: data.maxFrame - data.minFrame,
150
+ ease: data.ease,
151
+ });
152
+ }
153
+
154
+ return chips;
155
+ }
156
+
157
+ export function materializePreset(
158
+ layerBase: LayerBase,
159
+ presetType: string,
160
+ options: {
161
+ width: number;
162
+ height: number;
163
+ fps: number;
164
+ presetGroupId?: string;
165
+ durationFrames?: number;
166
+ },
167
+ ): AnimationTrack[] {
168
+ const groupId = options.presetGroupId ?? `${presetType}_${generateId()}`;
169
+ const width = options.width;
170
+ const height = options.height;
171
+ const layerDuration = layerBase.durationFrames;
172
+ const defaultDuration = options.durationFrames ?? options.fps; // Default 1s or custom duration
173
+
174
+ let prop: AnimatableProp;
175
+ let keyframes: Keyframe[] = [];
176
+ let repeat: number | undefined;
177
+ let yoyo: boolean | undefined;
178
+
179
+ switch (presetType) {
180
+ case "fade-in": {
181
+ prop = "opacity";
182
+ keyframes = [
183
+ { id: generateId(), frame: 0, value: 0, presetGroupId: groupId },
184
+ {
185
+ id: generateId(),
186
+ frame: defaultDuration,
187
+ value: layerBase.opacity,
188
+ ease: { name: "none", dir: "out" },
189
+ presetGroupId: groupId,
190
+ },
191
+ ];
192
+ break;
193
+ }
194
+ case "fade-out": {
195
+ prop = "opacity";
196
+ keyframes = [
197
+ {
198
+ id: generateId(),
199
+ frame: Math.max(0, layerDuration - defaultDuration),
200
+ value: layerBase.opacity,
201
+ presetGroupId: groupId,
202
+ },
203
+ {
204
+ id: generateId(),
205
+ frame: layerDuration,
206
+ value: 0,
207
+ ease: { name: "none", dir: "out" },
208
+ presetGroupId: groupId,
209
+ },
210
+ ];
211
+ break;
212
+ }
213
+ case "slide-in-left": {
214
+ prop = "x";
215
+ keyframes = [
216
+ {
217
+ id: generateId(),
218
+ frame: 0,
219
+ value: layerBase.x - width,
220
+ presetGroupId: groupId,
221
+ },
222
+ {
223
+ id: generateId(),
224
+ frame: defaultDuration,
225
+ value: layerBase.x,
226
+ ease: { name: "none", dir: "out" },
227
+ presetGroupId: groupId,
228
+ },
229
+ ];
230
+ break;
231
+ }
232
+ case "slide-in-right": {
233
+ prop = "x";
234
+ keyframes = [
235
+ {
236
+ id: generateId(),
237
+ frame: 0,
238
+ value: layerBase.x + width,
239
+ presetGroupId: groupId,
240
+ },
241
+ {
242
+ id: generateId(),
243
+ frame: defaultDuration,
244
+ value: layerBase.x,
245
+ ease: { name: "none", dir: "out" },
246
+ presetGroupId: groupId,
247
+ },
248
+ ];
249
+ break;
250
+ }
251
+ case "slide-in-top": {
252
+ prop = "y";
253
+ keyframes = [
254
+ {
255
+ id: generateId(),
256
+ frame: 0,
257
+ value: layerBase.y - height,
258
+ presetGroupId: groupId,
259
+ },
260
+ {
261
+ id: generateId(),
262
+ frame: defaultDuration,
263
+ value: layerBase.y,
264
+ ease: { name: "none", dir: "out" },
265
+ presetGroupId: groupId,
266
+ },
267
+ ];
268
+ break;
269
+ }
270
+ case "slide-in-bottom": {
271
+ prop = "y";
272
+ keyframes = [
273
+ {
274
+ id: generateId(),
275
+ frame: 0,
276
+ value: layerBase.y + height,
277
+ presetGroupId: groupId,
278
+ },
279
+ {
280
+ id: generateId(),
281
+ frame: defaultDuration,
282
+ value: layerBase.y,
283
+ ease: { name: "none", dir: "out" },
284
+ presetGroupId: groupId,
285
+ },
286
+ ];
287
+ break;
288
+ }
289
+ case "slide-out-left": {
290
+ prop = "x";
291
+ keyframes = [
292
+ {
293
+ id: generateId(),
294
+ frame: Math.max(0, layerDuration - defaultDuration),
295
+ value: layerBase.x,
296
+ presetGroupId: groupId,
297
+ },
298
+ {
299
+ id: generateId(),
300
+ frame: layerDuration,
301
+ value: layerBase.x - width,
302
+ ease: { name: "none", dir: "out" },
303
+ presetGroupId: groupId,
304
+ },
305
+ ];
306
+ break;
307
+ }
308
+ case "slide-out-right": {
309
+ prop = "x";
310
+ keyframes = [
311
+ {
312
+ id: generateId(),
313
+ frame: Math.max(0, layerDuration - defaultDuration),
314
+ value: layerBase.x,
315
+ presetGroupId: groupId,
316
+ },
317
+ {
318
+ id: generateId(),
319
+ frame: layerDuration,
320
+ value: layerBase.x + width,
321
+ ease: { name: "none", dir: "out" },
322
+ presetGroupId: groupId,
323
+ },
324
+ ];
325
+ break;
326
+ }
327
+ case "slide-out-top": {
328
+ prop = "y";
329
+ keyframes = [
330
+ {
331
+ id: generateId(),
332
+ frame: Math.max(0, layerDuration - defaultDuration),
333
+ value: layerBase.y,
334
+ presetGroupId: groupId,
335
+ },
336
+ {
337
+ id: generateId(),
338
+ frame: layerDuration,
339
+ value: layerBase.y - height,
340
+ ease: { name: "none", dir: "out" },
341
+ presetGroupId: groupId,
342
+ },
343
+ ];
344
+ break;
345
+ }
346
+ case "slide-out-bottom": {
347
+ prop = "y";
348
+ keyframes = [
349
+ {
350
+ id: generateId(),
351
+ frame: Math.max(0, layerDuration - defaultDuration),
352
+ value: layerBase.y,
353
+ presetGroupId: groupId,
354
+ },
355
+ {
356
+ id: generateId(),
357
+ frame: layerDuration,
358
+ value: layerBase.y + height,
359
+ ease: { name: "none", dir: "out" },
360
+ presetGroupId: groupId,
361
+ },
362
+ ];
363
+ break;
364
+ }
365
+ case "zoom-in": {
366
+ prop = "scale";
367
+ keyframes = [
368
+ { id: generateId(), frame: 0, value: 0, presetGroupId: groupId },
369
+ {
370
+ id: generateId(),
371
+ frame: defaultDuration,
372
+ value: layerBase.scale,
373
+ ease: { name: "none", dir: "out" },
374
+ presetGroupId: groupId,
375
+ },
376
+ ];
377
+ break;
378
+ }
379
+ case "zoom-out": {
380
+ prop = "scale";
381
+ keyframes = [
382
+ {
383
+ id: generateId(),
384
+ frame: Math.max(0, layerDuration - defaultDuration),
385
+ value: layerBase.scale,
386
+ presetGroupId: groupId,
387
+ },
388
+ {
389
+ id: generateId(),
390
+ frame: layerDuration,
391
+ value: 0,
392
+ ease: { name: "none", dir: "out" },
393
+ presetGroupId: groupId,
394
+ },
395
+ ];
396
+ break;
397
+ }
398
+ case "bounce-in": {
399
+ prop = "scale";
400
+ keyframes = [
401
+ { id: generateId(), frame: 0, value: 0, presetGroupId: groupId },
402
+ {
403
+ id: generateId(),
404
+ frame: defaultDuration,
405
+ value: layerBase.scale,
406
+ ease: { name: "back", dir: "out", params: [1.7] },
407
+ presetGroupId: groupId,
408
+ },
409
+ ];
410
+ break;
411
+ }
412
+ case "rotate-cw": {
413
+ prop = "rotation";
414
+ keyframes = [
415
+ {
416
+ id: generateId(),
417
+ frame: 0,
418
+ value: layerBase.rotation,
419
+ presetGroupId: groupId,
420
+ },
421
+ {
422
+ id: generateId(),
423
+ frame: defaultDuration,
424
+ value: layerBase.rotation + 360,
425
+ ease: { name: "none", dir: "out" },
426
+ presetGroupId: groupId,
427
+ },
428
+ ];
429
+ repeat = 1;
430
+ break;
431
+ }
432
+ case "rotate-ccw": {
433
+ prop = "rotation";
434
+ keyframes = [
435
+ {
436
+ id: generateId(),
437
+ frame: 0,
438
+ value: layerBase.rotation,
439
+ presetGroupId: groupId,
440
+ },
441
+ {
442
+ id: generateId(),
443
+ frame: defaultDuration,
444
+ value: layerBase.rotation - 360,
445
+ ease: { name: "none", dir: "out" },
446
+ presetGroupId: groupId,
447
+ },
448
+ ];
449
+ repeat = 1;
450
+ break;
451
+ }
452
+ case "pulse": {
453
+ prop = "scale";
454
+ const halfDur = Math.max(1, Math.round(defaultDuration / 2));
455
+ keyframes = [
456
+ {
457
+ id: generateId(),
458
+ frame: 0,
459
+ value: layerBase.scale,
460
+ presetGroupId: groupId,
461
+ },
462
+ {
463
+ id: generateId(),
464
+ frame: halfDur,
465
+ value: layerBase.scale * 1.1,
466
+ ease: { name: "none", dir: "out" },
467
+ presetGroupId: groupId,
468
+ },
469
+ ];
470
+ repeat = -1;
471
+ yoyo = true;
472
+ break;
473
+ }
474
+ case "shake": {
475
+ prop = "x";
476
+ const segmentDur = Math.max(1, Math.round(defaultDuration / 10));
477
+ keyframes = [
478
+ {
479
+ id: generateId(),
480
+ frame: 0,
481
+ value: layerBase.x,
482
+ presetGroupId: groupId,
483
+ },
484
+ {
485
+ id: generateId(),
486
+ frame: segmentDur,
487
+ value: layerBase.x + 20,
488
+ ease: { name: "sine", dir: "inOut" },
489
+ presetGroupId: groupId,
490
+ },
491
+ ];
492
+ repeat = 9;
493
+ yoyo = true;
494
+ break;
495
+ }
496
+ case "wobble": {
497
+ prop = "rotation";
498
+ const segmentDur = Math.max(1, Math.round(defaultDuration / 6));
499
+ keyframes = [
500
+ {
501
+ id: generateId(),
502
+ frame: 0,
503
+ value: layerBase.rotation,
504
+ presetGroupId: groupId,
505
+ },
506
+ {
507
+ id: generateId(),
508
+ frame: segmentDur,
509
+ value: layerBase.rotation + 15,
510
+ ease: { name: "sine", dir: "inOut" },
511
+ presetGroupId: groupId,
512
+ },
513
+ ];
514
+ repeat = 9;
515
+ yoyo = true;
516
+ break;
517
+ }
518
+ case "bounce": {
519
+ prop = "y";
520
+ keyframes = [
521
+ {
522
+ id: generateId(),
523
+ frame: 0,
524
+ value: layerBase.y - 100,
525
+ presetGroupId: groupId,
526
+ },
527
+ {
528
+ id: generateId(),
529
+ frame: defaultDuration,
530
+ value: layerBase.y,
531
+ ease: { name: "bounce", dir: "out" },
532
+ presetGroupId: groupId,
533
+ },
534
+ ];
535
+ break;
536
+ }
537
+ case "typewriter":
538
+ case "word-reveal":
539
+ case "line-reveal":
540
+ case "karaoke": {
541
+ prop = "text";
542
+ keyframes = [
543
+ {
544
+ id: generateId(),
545
+ frame: 0,
546
+ value: 0,
547
+ presetGroupId: groupId,
548
+ presetType,
549
+ },
550
+ {
551
+ id: generateId(),
552
+ frame: defaultDuration,
553
+ value: 1,
554
+ ease: { name: "none", dir: "out" },
555
+ presetGroupId: groupId,
556
+ presetType,
557
+ },
558
+ ];
559
+ break;
560
+ }
561
+ case "tracking-expand": {
562
+ prop = "letterSpacing";
563
+ keyframes = [
564
+ { id: generateId(), frame: 0, value: 0, presetGroupId: groupId },
565
+ {
566
+ id: generateId(),
567
+ frame: defaultDuration,
568
+ value: 24,
569
+ ease: { name: "sine", dir: "out" },
570
+ presetGroupId: groupId,
571
+ },
572
+ ];
573
+ break;
574
+ }
575
+ case "blur-reveal": {
576
+ prop = "opacity";
577
+ keyframes = [
578
+ { id: generateId(), frame: 0, value: 0, presetGroupId: groupId },
579
+ {
580
+ id: generateId(),
581
+ frame: defaultDuration,
582
+ value: layerBase.opacity,
583
+ ease: { name: "power1", dir: "out" },
584
+ presetGroupId: groupId,
585
+ },
586
+ ];
587
+ break;
588
+ }
589
+ case "kinetic-wave": {
590
+ prop = "y";
591
+ const halfDur = Math.max(1, Math.round(defaultDuration / 2));
592
+ keyframes = [
593
+ {
594
+ id: generateId(),
595
+ frame: 0,
596
+ value: layerBase.y,
597
+ presetGroupId: groupId,
598
+ },
599
+ {
600
+ id: generateId(),
601
+ frame: halfDur,
602
+ value: layerBase.y - 24,
603
+ ease: { name: "sine", dir: "inOut" },
604
+ presetGroupId: groupId,
605
+ },
606
+ ];
607
+ repeat = 3;
608
+ yoyo = true;
609
+ break;
610
+ }
611
+ case "slice-wipe": {
612
+ prop = "x";
613
+ const offset = layerBase.width ?? 300;
614
+ keyframes = [
615
+ {
616
+ id: generateId(),
617
+ frame: 0,
618
+ value: layerBase.x - offset,
619
+ presetGroupId: groupId,
620
+ },
621
+ {
622
+ id: generateId(),
623
+ frame: defaultDuration,
624
+ value: layerBase.x,
625
+ ease: { name: "expo", dir: "out" },
626
+ presetGroupId: groupId,
627
+ },
628
+ ];
629
+ break;
630
+ }
631
+ default:
632
+ throw new Error(`Unsupported preset type: ${presetType}`);
633
+ }
634
+
635
+ return [
636
+ {
637
+ id: generateId(),
638
+ prop,
639
+ keyframes: keyframes.map((kf) => ({ ...kf, presetType })),
640
+ ...(repeat !== undefined && { repeat }),
641
+ ...(yoyo !== undefined && { yoyo }),
642
+ },
643
+ ];
644
+ }