reframe-video 0.6.25 → 0.6.27
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/dist/bin.js +388 -50
- package/dist/cli.js +390 -49
- package/dist/compile-api.js +30 -1
- package/dist/compile.js +30 -1
- package/dist/diff.js +28 -1
- package/dist/frame.js +28 -1
- package/dist/index.js +72 -6
- package/dist/labels.js +28 -1
- package/dist/types/audio.d.ts +2 -2
- package/dist/types/camera.d.ts +27 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/ir.d.ts +21 -4
- package/guides/edsl-guide.md +38 -6
- package/package.json +1 -1
package/dist/diff.js
CHANGED
|
@@ -13,6 +13,30 @@ import { dirname, resolve } from "node:path";
|
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
15
|
// ../core/src/ir.ts
|
|
16
|
+
var SFX_NAMES = [
|
|
17
|
+
"whoosh",
|
|
18
|
+
"swish",
|
|
19
|
+
"rise",
|
|
20
|
+
"riser",
|
|
21
|
+
"warp",
|
|
22
|
+
"tick",
|
|
23
|
+
"click",
|
|
24
|
+
"blip",
|
|
25
|
+
"pop",
|
|
26
|
+
"select",
|
|
27
|
+
"thud",
|
|
28
|
+
"boom",
|
|
29
|
+
"knock",
|
|
30
|
+
"chime",
|
|
31
|
+
"ding",
|
|
32
|
+
"coin",
|
|
33
|
+
"sparkle",
|
|
34
|
+
"shimmer",
|
|
35
|
+
"success",
|
|
36
|
+
"zap",
|
|
37
|
+
"error"
|
|
38
|
+
];
|
|
39
|
+
var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
|
|
16
40
|
var DEFAULT_TO_DURATION = 0.5;
|
|
17
41
|
var DEFAULT_TWEEN_DURATION = 0.5;
|
|
18
42
|
var DEFAULT_MOTIONPATH_DURATION = 1;
|
|
@@ -662,7 +686,6 @@ function validateScene(ir) {
|
|
|
662
686
|
}
|
|
663
687
|
}
|
|
664
688
|
}
|
|
665
|
-
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
666
689
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
667
690
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
668
691
|
problems.push(
|
|
@@ -698,6 +721,10 @@ function validateScene(ir) {
|
|
|
698
721
|
if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
|
|
699
722
|
problems.push('audio.bgm: use either "file" or "synth", not both');
|
|
700
723
|
}
|
|
724
|
+
const bgmSynth = ir.audio?.bgm?.synth;
|
|
725
|
+
if (bgmSynth !== void 0 && !BGM_SYNTHS.includes(bgmSynth)) {
|
|
726
|
+
problems.push(`audio.bgm.synth: unknown synth "${bgmSynth}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
|
|
727
|
+
}
|
|
701
728
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
702
729
|
}
|
|
703
730
|
var TRANSITIONS = ["cut", "crossfade"];
|
package/dist/frame.js
CHANGED
|
@@ -40,6 +40,30 @@ import { existsSync } from "node:fs";
|
|
|
40
40
|
import { extname, isAbsolute, resolve } from "node:path";
|
|
41
41
|
|
|
42
42
|
// ../core/src/ir.ts
|
|
43
|
+
var SFX_NAMES = [
|
|
44
|
+
"whoosh",
|
|
45
|
+
"swish",
|
|
46
|
+
"rise",
|
|
47
|
+
"riser",
|
|
48
|
+
"warp",
|
|
49
|
+
"tick",
|
|
50
|
+
"click",
|
|
51
|
+
"blip",
|
|
52
|
+
"pop",
|
|
53
|
+
"select",
|
|
54
|
+
"thud",
|
|
55
|
+
"boom",
|
|
56
|
+
"knock",
|
|
57
|
+
"chime",
|
|
58
|
+
"ding",
|
|
59
|
+
"coin",
|
|
60
|
+
"sparkle",
|
|
61
|
+
"shimmer",
|
|
62
|
+
"success",
|
|
63
|
+
"zap",
|
|
64
|
+
"error"
|
|
65
|
+
];
|
|
66
|
+
var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
|
|
43
67
|
var DEFAULT_TO_DURATION = 0.5;
|
|
44
68
|
var DEFAULT_TWEEN_DURATION = 0.5;
|
|
45
69
|
var DEFAULT_MOTIONPATH_DURATION = 1;
|
|
@@ -689,7 +713,6 @@ function validateScene(ir) {
|
|
|
689
713
|
}
|
|
690
714
|
}
|
|
691
715
|
}
|
|
692
|
-
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
693
716
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
694
717
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
695
718
|
problems.push(
|
|
@@ -725,6 +748,10 @@ function validateScene(ir) {
|
|
|
725
748
|
if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
|
|
726
749
|
problems.push('audio.bgm: use either "file" or "synth", not both');
|
|
727
750
|
}
|
|
751
|
+
const bgmSynth = ir.audio?.bgm?.synth;
|
|
752
|
+
if (bgmSynth !== void 0 && !BGM_SYNTHS.includes(bgmSynth)) {
|
|
753
|
+
problems.push(`audio.bgm.synth: unknown synth "${bgmSynth}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
|
|
754
|
+
}
|
|
728
755
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
729
756
|
}
|
|
730
757
|
var TRANSITIONS = ["cut", "crossfade"];
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
// ../core/src/ir.ts
|
|
2
|
+
var SFX_NAMES = [
|
|
3
|
+
"whoosh",
|
|
4
|
+
"swish",
|
|
5
|
+
"rise",
|
|
6
|
+
"riser",
|
|
7
|
+
"warp",
|
|
8
|
+
"tick",
|
|
9
|
+
"click",
|
|
10
|
+
"blip",
|
|
11
|
+
"pop",
|
|
12
|
+
"select",
|
|
13
|
+
"thud",
|
|
14
|
+
"boom",
|
|
15
|
+
"knock",
|
|
16
|
+
"chime",
|
|
17
|
+
"ding",
|
|
18
|
+
"coin",
|
|
19
|
+
"sparkle",
|
|
20
|
+
"shimmer",
|
|
21
|
+
"success",
|
|
22
|
+
"zap",
|
|
23
|
+
"error"
|
|
24
|
+
];
|
|
25
|
+
var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
|
|
2
26
|
var DEFAULT_CROSSFADE = 0.5;
|
|
3
27
|
var DEFAULT_TO_DURATION = 0.5;
|
|
4
28
|
var DEFAULT_TWEEN_DURATION = 0.5;
|
|
@@ -785,7 +809,6 @@ function validateScene(ir) {
|
|
|
785
809
|
}
|
|
786
810
|
}
|
|
787
811
|
}
|
|
788
|
-
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
789
812
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
790
813
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
791
814
|
problems.push(
|
|
@@ -821,6 +844,10 @@ function validateScene(ir) {
|
|
|
821
844
|
if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
|
|
822
845
|
problems.push('audio.bgm: use either "file" or "synth", not both');
|
|
823
846
|
}
|
|
847
|
+
const bgmSynth = ir.audio?.bgm?.synth;
|
|
848
|
+
if (bgmSynth !== void 0 && !BGM_SYNTHS.includes(bgmSynth)) {
|
|
849
|
+
problems.push(`audio.bgm.synth: unknown synth "${bgmSynth}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
|
|
850
|
+
}
|
|
824
851
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
825
852
|
}
|
|
826
853
|
var TRANSITIONS = ["cut", "crossfade"];
|
|
@@ -1208,6 +1235,14 @@ function cameraMatrix(cam, size) {
|
|
|
1208
1235
|
const d = Math.cos(r) * zoom;
|
|
1209
1236
|
return [a, b, c, d, W / 2 - a * x - c * y, H / 2 - b * x - d * y];
|
|
1210
1237
|
}
|
|
1238
|
+
function cameraFit(box, opts = {}) {
|
|
1239
|
+
const W = opts.size?.width ?? 1920;
|
|
1240
|
+
const H = opts.size?.height ?? 1080;
|
|
1241
|
+
const m = opts.margin ?? 80;
|
|
1242
|
+
const fit = Math.min(W / (box.width + 2 * m), H / (box.height + 2 * m));
|
|
1243
|
+
const zoom = Math.min(fit, opts.maxZoom ?? 2.4);
|
|
1244
|
+
return { x: box.x + box.width / 2, y: box.y + box.height / 2, zoom };
|
|
1245
|
+
}
|
|
1211
1246
|
function cameraTo(props, opts = {}) {
|
|
1212
1247
|
return tween(CAMERA_ID, props, opts);
|
|
1213
1248
|
}
|
|
@@ -2881,12 +2916,32 @@ function motionOp(name, target, opts = {}) {
|
|
|
2881
2916
|
|
|
2882
2917
|
// ../core/src/audio.ts
|
|
2883
2918
|
var SFX_DURATION = {
|
|
2919
|
+
// transition
|
|
2884
2920
|
whoosh: 0.35,
|
|
2885
|
-
|
|
2886
|
-
tick: 0.03,
|
|
2921
|
+
swish: 0.32,
|
|
2887
2922
|
rise: 0.5,
|
|
2923
|
+
riser: 0.85,
|
|
2924
|
+
warp: 0.5,
|
|
2925
|
+
// ui
|
|
2926
|
+
tick: 0.03,
|
|
2927
|
+
click: 0.05,
|
|
2928
|
+
blip: 0.1,
|
|
2929
|
+
pop: 0.12,
|
|
2930
|
+
select: 0.18,
|
|
2931
|
+
// impact
|
|
2932
|
+
thud: 0.25,
|
|
2933
|
+
boom: 0.6,
|
|
2934
|
+
knock: 0.14,
|
|
2935
|
+
// positive
|
|
2936
|
+
chime: 0.7,
|
|
2937
|
+
ding: 0.5,
|
|
2938
|
+
coin: 0.3,
|
|
2939
|
+
sparkle: 0.6,
|
|
2888
2940
|
shimmer: 0.9,
|
|
2889
|
-
|
|
2941
|
+
success: 0.6,
|
|
2942
|
+
// alert
|
|
2943
|
+
zap: 0.22,
|
|
2944
|
+
error: 0.4
|
|
2890
2945
|
};
|
|
2891
2946
|
var FILE_CUE_DURATION = 0.4;
|
|
2892
2947
|
function collectClipAudio(ir, duration, warnings) {
|
|
@@ -2946,7 +3001,11 @@ function resolveAudioPlan(compiled) {
|
|
|
2946
3001
|
fadeIn: cue.fadeIn ?? 0,
|
|
2947
3002
|
fadeOut: cue.fadeOut ?? 0,
|
|
2948
3003
|
pan: cue.pan ?? 0,
|
|
2949
|
-
source: cue.sfx ?
|
|
3004
|
+
source: cue.sfx ? (
|
|
3005
|
+
// auto-vary: default the seed to the cue's order so repeated sfx differ
|
|
3006
|
+
// (pitch/texture); an explicit params.seed always wins.
|
|
3007
|
+
{ kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
|
|
3008
|
+
) : { kind: "file", path: cue.file }
|
|
2950
3009
|
});
|
|
2951
3010
|
}
|
|
2952
3011
|
cues.sort((a, b) => a.t - b.t);
|
|
@@ -3026,7 +3085,11 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
3026
3085
|
fadeIn: cue.fadeIn ?? 0,
|
|
3027
3086
|
fadeOut: cue.fadeOut ?? 0,
|
|
3028
3087
|
pan: cue.pan ?? 0,
|
|
3029
|
-
source: cue.sfx ?
|
|
3088
|
+
source: cue.sfx ? (
|
|
3089
|
+
// auto-vary: default the seed to the cue's order so repeated sfx differ
|
|
3090
|
+
// (pitch/texture); an explicit params.seed always wins.
|
|
3091
|
+
{ kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
|
|
3092
|
+
) : { kind: "file", path: cue.file }
|
|
3030
3093
|
});
|
|
3031
3094
|
}
|
|
3032
3095
|
if (!audio?.bgm && cues.length === 0 && clipAudio.length === 0) return null;
|
|
@@ -3585,6 +3648,7 @@ function sketchToTimeline(sketch, nodeIds) {
|
|
|
3585
3648
|
return par(...steps);
|
|
3586
3649
|
}
|
|
3587
3650
|
export {
|
|
3651
|
+
BGM_SYNTHS,
|
|
3588
3652
|
CAMERA_ID,
|
|
3589
3653
|
CAMERA_PROPS2 as CAMERA_PROPS,
|
|
3590
3654
|
CHARACTER_PRESET_NAMES,
|
|
@@ -3600,8 +3664,10 @@ export {
|
|
|
3600
3664
|
PRESET_NAMES,
|
|
3601
3665
|
PROPS_BY_TYPE,
|
|
3602
3666
|
SFX_DURATION,
|
|
3667
|
+
SFX_NAMES,
|
|
3603
3668
|
SceneValidationError,
|
|
3604
3669
|
beat,
|
|
3670
|
+
cameraFit,
|
|
3605
3671
|
cameraMatrix,
|
|
3606
3672
|
cameraTo,
|
|
3607
3673
|
characterPreset,
|
package/dist/labels.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
// ../core/src/ir.ts
|
|
4
|
+
var SFX_NAMES = [
|
|
5
|
+
"whoosh",
|
|
6
|
+
"swish",
|
|
7
|
+
"rise",
|
|
8
|
+
"riser",
|
|
9
|
+
"warp",
|
|
10
|
+
"tick",
|
|
11
|
+
"click",
|
|
12
|
+
"blip",
|
|
13
|
+
"pop",
|
|
14
|
+
"select",
|
|
15
|
+
"thud",
|
|
16
|
+
"boom",
|
|
17
|
+
"knock",
|
|
18
|
+
"chime",
|
|
19
|
+
"ding",
|
|
20
|
+
"coin",
|
|
21
|
+
"sparkle",
|
|
22
|
+
"shimmer",
|
|
23
|
+
"success",
|
|
24
|
+
"zap",
|
|
25
|
+
"error"
|
|
26
|
+
];
|
|
27
|
+
var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
|
|
4
28
|
var DEFAULT_TO_DURATION = 0.5;
|
|
5
29
|
var DEFAULT_TWEEN_DURATION = 0.5;
|
|
6
30
|
var DEFAULT_MOTIONPATH_DURATION = 1;
|
|
@@ -650,7 +674,6 @@ function validateScene(ir) {
|
|
|
650
674
|
}
|
|
651
675
|
}
|
|
652
676
|
}
|
|
653
|
-
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
654
677
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
655
678
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
656
679
|
problems.push(
|
|
@@ -686,6 +709,10 @@ function validateScene(ir) {
|
|
|
686
709
|
if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
|
|
687
710
|
problems.push('audio.bgm: use either "file" or "synth", not both');
|
|
688
711
|
}
|
|
712
|
+
const bgmSynth = ir.audio?.bgm?.synth;
|
|
713
|
+
if (bgmSynth !== void 0 && !BGM_SYNTHS.includes(bgmSynth)) {
|
|
714
|
+
problems.push(`audio.bgm.synth: unknown synth "${bgmSynth}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
|
|
715
|
+
}
|
|
689
716
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
690
717
|
}
|
|
691
718
|
|
package/dist/types/audio.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { CompiledScene } from "./compile.js";
|
|
10
10
|
import type { CompiledComposition } from "./composeComposition.js";
|
|
11
|
-
import type { SfxName } from "./ir.js";
|
|
11
|
+
import type { BgmSynth, SfxName } from "./ir.js";
|
|
12
12
|
/** Nominal cue lengths (s) for duck-window math; file cues use a default. */
|
|
13
13
|
export declare const SFX_DURATION: Record<SfxName, number>;
|
|
14
14
|
export interface ResolvedCue {
|
|
@@ -55,7 +55,7 @@ export interface AudioPlan {
|
|
|
55
55
|
path: string;
|
|
56
56
|
} | {
|
|
57
57
|
kind: "synth";
|
|
58
|
-
name:
|
|
58
|
+
name: BgmSynth;
|
|
59
59
|
};
|
|
60
60
|
gain: number;
|
|
61
61
|
fadeIn: number;
|
package/dist/types/camera.d.ts
CHANGED
|
@@ -24,6 +24,33 @@ export declare const CAMERA_PROPS: readonly ["x", "y", "zoom", "rotation", "pers
|
|
|
24
24
|
* collapse to the identity.
|
|
25
25
|
*/
|
|
26
26
|
export declare function cameraMatrix(cam: CameraIR, size: Size): Mat2D;
|
|
27
|
+
/**
|
|
28
|
+
* Frame a scene-space bounding box in the viewport — returns `{ x, y, zoom }` to
|
|
29
|
+
* spread into `cameraTo`, GUARANTEED not to clip the box. The visible scene rect
|
|
30
|
+
* is `W/zoom × H/zoom` centred on `(x, y)`; fitting `box` (+ `margin` padding on
|
|
31
|
+
* the tight axis) means `zoom = min(W/(box.w+2m), H/(box.h+2m))`, capped by
|
|
32
|
+
* `maxZoom` so a tiny target doesn't zoom absurdly close.
|
|
33
|
+
*
|
|
34
|
+
* cameraTo(cameraFit({ x: 200, y: 760, width: 740, height: 360 }, { margin: 90 }),
|
|
35
|
+
* { duration: 1, ease: "easeInOutCubic" })
|
|
36
|
+
*
|
|
37
|
+
* `box` is a top-left rect in scene coords (a centre-anchored panel at (px,py) of
|
|
38
|
+
* size (pw,ph) is `{ x: px-pw/2, y: py-ph/2, width: pw, height: ph }`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function cameraFit(box: {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
}, opts?: {
|
|
46
|
+
size?: Size;
|
|
47
|
+
margin?: number;
|
|
48
|
+
maxZoom?: number;
|
|
49
|
+
}): {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
zoom: number;
|
|
53
|
+
};
|
|
27
54
|
/** Keyframe the camera: a `tween` on the reserved "camera" target. */
|
|
28
55
|
export declare function cameraTo(props: CameraIR, opts?: {
|
|
29
56
|
duration?: number;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { compileComposition, type CompiledComposition, type ScenePlacement, } fr
|
|
|
5
5
|
export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport, } from "./compose.js";
|
|
6
6
|
export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan, type MotionDriver } from "./compile.js";
|
|
7
7
|
export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
8
|
-
export { cameraTo, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
|
|
8
|
+
export { cameraTo, cameraFit, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
|
|
9
9
|
export { linearGradient, radialGradient, conicGradient, isGradient } from "./gradient.js";
|
|
10
10
|
export { glow, dropShadow } from "./effects.js";
|
|
11
11
|
export { row, column, grid, type RowOpts, type GridOpts } from "./layout.js";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -412,7 +412,20 @@ export interface BehaviorIR {
|
|
|
412
412
|
};
|
|
413
413
|
};
|
|
414
414
|
}
|
|
415
|
-
|
|
415
|
+
/**
|
|
416
|
+
* The procedural sfx palette — the single source of truth (the type, validation,
|
|
417
|
+
* and the render-cli synth recipes all key off this). Grouped by use:
|
|
418
|
+
* transition: whoosh swish rise riser warp · ui: tick click blip pop select
|
|
419
|
+
* impact: thud boom knock · positive: chime ding coin sparkle shimmer success
|
|
420
|
+
* alert: zap error
|
|
421
|
+
* Every cue's pitch/texture varies with its `seed` (auto-seeded by cue order), so
|
|
422
|
+
* repeated cues sound different; `params.pitch` is an explicit multiplier.
|
|
423
|
+
*/
|
|
424
|
+
export declare const SFX_NAMES: readonly ["whoosh", "swish", "rise", "riser", "warp", "tick", "click", "blip", "pop", "select", "thud", "boom", "knock", "chime", "ding", "coin", "sparkle", "shimmer", "success", "zap", "error"];
|
|
425
|
+
export type SfxName = (typeof SFX_NAMES)[number];
|
|
426
|
+
/** Synthesized background-music beds (license-free). */
|
|
427
|
+
export declare const BGM_SYNTHS: readonly ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
|
|
428
|
+
export type BgmSynth = (typeof BGM_SYNTHS)[number];
|
|
416
429
|
export interface AudioCueIR {
|
|
417
430
|
/** Anchor: a timeline label (the step's start) or absolute seconds. */
|
|
418
431
|
at: string | number;
|
|
@@ -430,14 +443,18 @@ export interface AudioCueIR {
|
|
|
430
443
|
fadeOut?: number;
|
|
431
444
|
/** Stereo balance: -1 full left, 0 centre (default), +1 full right. */
|
|
432
445
|
pan?: number;
|
|
433
|
-
/**
|
|
446
|
+
/**
|
|
447
|
+
* Synth parameter overrides — numbers only. `seed` varies pitch/texture
|
|
448
|
+
* (defaults to the cue's order so repeats differ); `pitch` is an explicit
|
|
449
|
+
* frequency multiplier (1 = unchanged, 2 = an octave up); `gainDb` trims level.
|
|
450
|
+
*/
|
|
434
451
|
params?: Record<string, number>;
|
|
435
452
|
}
|
|
436
453
|
export interface AudioIR {
|
|
437
454
|
bgm?: {
|
|
438
455
|
file?: string;
|
|
439
|
-
/** License-free synthesized bed. */
|
|
440
|
-
synth?:
|
|
456
|
+
/** License-free synthesized bed (see {@link BGM_SYNTHS}). */
|
|
457
|
+
synth?: BgmSynth;
|
|
441
458
|
gain?: number;
|
|
442
459
|
fadeIn?: number;
|
|
443
460
|
fadeOut?: number;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -90,6 +90,14 @@ row(3, { center: 960, gap: 60, itemWidth: 440 }).map((x, i) =>
|
|
|
90
90
|
|
|
91
91
|
`column` is `row` for the y axis.
|
|
92
92
|
|
|
93
|
+
**Charts/widgets in a panel — derive geometry from the box, and `clip` it.** Don't
|
|
94
|
+
hand-pick a pixels-per-unit scale (bars routinely overflow the panel that way).
|
|
95
|
+
Define the panel rect ONCE, then size from it — bar height `(v/max) · innerH`, x via
|
|
96
|
+
`row(...)` across the panel width — so a tall value can't exceed the box. As a safety
|
|
97
|
+
net, wrap the chart in a clipped group so nothing can ever punch out the panel:
|
|
98
|
+
`group({ clip: { kind: "rect", x, y, width, height, radius } }, [ ...bars ])`. See
|
|
99
|
+
`examples/scenes/annual-report.ts` (and `cameraFit` above to frame the panel).
|
|
100
|
+
|
|
93
101
|
## States: declare looks, not motion
|
|
94
102
|
|
|
95
103
|
Base props on nodes describe the **finished design**. A state is a sparse
|
|
@@ -178,6 +186,14 @@ scene({
|
|
|
178
186
|
`tween` on the `"camera"` target, so `motionPath("camera", pts, …)` (pan along
|
|
179
187
|
a curve) and `oscillate/wiggle("camera", "rotation"|"x"|…)` (handheld drift)
|
|
180
188
|
also work.
|
|
189
|
+
- **Frame a region without clipping — use `cameraFit`, not a guessed `zoom`.** The
|
|
190
|
+
visible scene rect is `W/zoom × H/zoom` centred on `(camera.x, camera.y)`, so a
|
|
191
|
+
hand-picked `zoom` that's too big crops the target. `cameraFit(box, { margin })`
|
|
192
|
+
returns `{ x, y, zoom }` that frames a scene-space bbox (top-left `{x,y,width,
|
|
193
|
+
height}`) with padding, guaranteed in-bounds: `cameraTo(cameraFit({ x, y, width,
|
|
194
|
+
height }, { margin: 80 }), { duration: 1, ease: "easeInOutCubic" })`. A centre-
|
|
195
|
+
anchored panel at `(px,py)` size `(pw,ph)` is `{ x: px-pw/2, y: py-ph/2, width:
|
|
196
|
+
pw, height: ph }`. `maxZoom` (default 2.4) caps absurd close-ups.
|
|
181
197
|
- **Pin HUD/titles to the screen** with `fixed: true` on a TOP-LEVEL node — the
|
|
182
198
|
camera won't move it (for overlays, watermarks, captions).
|
|
183
199
|
- Defaults are the identity, so a scene without a camera is unchanged. Don't name
|
|
@@ -542,12 +558,28 @@ audio: {
|
|
|
542
558
|
}
|
|
543
559
|
```
|
|
544
560
|
|
|
545
|
-
Procedural sfx
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
561
|
+
**Procedural sfx palette** (deterministic; exactly one of `sfx`/`file` per cue):
|
|
562
|
+
|
|
563
|
+
| group | names |
|
|
564
|
+
| --- | --- |
|
|
565
|
+
| transition | `whoosh` `swish` `rise` `riser` `warp` |
|
|
566
|
+
| ui | `tick` `click` `blip` `pop` `select` |
|
|
567
|
+
| impact | `thud` `boom` `knock` |
|
|
568
|
+
| positive | `chime` `ding` `coin` `sparkle` `shimmer` `success` |
|
|
569
|
+
| alert | `zap` `error` |
|
|
570
|
+
|
|
571
|
+
**Variation — repeats don't sound the same.** Each cue's `seed` shifts the sound's
|
|
572
|
+
PITCH (a musical step) and texture, and it **defaults to the cue's order**, so a run of
|
|
573
|
+
the same sfx becomes a little phrase instead of a stuck note — no setup needed. Override
|
|
574
|
+
explicitly with `params`: `{ sfx: "blip", params: { seed: 4 } }` (pick the variant) or
|
|
575
|
+
`{ sfx: "tick", params: { pitch: 1.5 } }` (an explicit frequency multiplier; `2` = octave
|
|
576
|
+
up). `params.gainDb` trims a single hit.
|
|
577
|
+
|
|
578
|
+
**bgm beds** (`bgm.synth`): `ambient-pad` `lofi` `pulse` `tension` `uplift` — or
|
|
579
|
+
`bgm.file` for your own. **Mixing**: any cue takes `fadeIn`/`fadeOut` (seconds) and `pan`
|
|
580
|
+
(-1 left … 0 centre … +1 right). A `video` clip's audio takes `fadeIn` and `pan` too
|
|
581
|
+
(clip fade-out isn't supported yet). The bed auto-ducks under cues (`bgm.duck`). See
|
|
582
|
+
`examples/scenes/sfx-showcase.ts` to audition the whole palette.
|
|
551
583
|
|
|
552
584
|
## Rules
|
|
553
585
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.27",
|
|
4
4
|
"description": "Declarative motion graphics that AI can write and humans can tweak — human edits survive AI regeneration. Deterministic mp4 renders from a plain-data scene format.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"motion-graphics",
|