reframe-video 0.6.3 → 0.6.4
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 +10 -0
- package/dist/index.js +120 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/montage.d.ts +56 -0
- package/guides/edsl-guide.md +25 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -952,6 +952,15 @@ var init_effects = __esm({
|
|
|
952
952
|
}
|
|
953
953
|
});
|
|
954
954
|
|
|
955
|
+
// ../core/src/montage.ts
|
|
956
|
+
var init_montage = __esm({
|
|
957
|
+
"../core/src/montage.ts"() {
|
|
958
|
+
"use strict";
|
|
959
|
+
init_dsl();
|
|
960
|
+
init_gradient();
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
955
964
|
// ../core/src/presets.ts
|
|
956
965
|
function makeRng(seed) {
|
|
957
966
|
let a = seed >>> 0 || 2654435769;
|
|
@@ -1410,6 +1419,7 @@ var init_src = __esm({
|
|
|
1410
1419
|
init_camera();
|
|
1411
1420
|
init_gradient();
|
|
1412
1421
|
init_effects();
|
|
1422
|
+
init_montage();
|
|
1413
1423
|
init_presets();
|
|
1414
1424
|
init_devicePreset();
|
|
1415
1425
|
init_cursor();
|
package/dist/index.js
CHANGED
|
@@ -1012,6 +1012,121 @@ function dropShadow(color, blur = 24, x = 0, y = 12) {
|
|
|
1012
1012
|
return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
|
|
1013
1013
|
}
|
|
1014
1014
|
|
|
1015
|
+
// ../core/src/montage.ts
|
|
1016
|
+
function makeRng(seed) {
|
|
1017
|
+
let a = seed >>> 0 || 2654435769;
|
|
1018
|
+
return () => {
|
|
1019
|
+
a = a + 1831565813 | 0;
|
|
1020
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
1021
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1022
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
var norm = (img) => typeof img === "string" ? { src: img } : img;
|
|
1026
|
+
function photoMontage(images, opts = {}) {
|
|
1027
|
+
const id = opts.id ?? "shot";
|
|
1028
|
+
const W = opts.size?.width ?? 1920;
|
|
1029
|
+
const H = opts.size?.height ?? 1080;
|
|
1030
|
+
const hold = Math.max(0.5, opts.hold ?? 3.2);
|
|
1031
|
+
const zoom = Math.max(1.001, opts.zoom ?? 1.18);
|
|
1032
|
+
const grade = opts.grade !== false;
|
|
1033
|
+
const rand2 = makeRng((opts.seed ?? 0) + 1);
|
|
1034
|
+
const slides = images.map(norm);
|
|
1035
|
+
const cx = W / 2;
|
|
1036
|
+
const cy = H / 2;
|
|
1037
|
+
const nodes = [];
|
|
1038
|
+
const shots = [];
|
|
1039
|
+
slides.forEach((slide, i) => {
|
|
1040
|
+
const nid = `${id}-${i}`;
|
|
1041
|
+
const slideHold = Math.max(0.5, slide.hold ?? hold);
|
|
1042
|
+
const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
|
|
1043
|
+
const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
|
|
1044
|
+
const angle = rand2() * Math.PI * 2;
|
|
1045
|
+
const panFrac = 0.4 + rand2() * 0.35;
|
|
1046
|
+
const dx = Math.cos(angle);
|
|
1047
|
+
const dy = Math.sin(angle);
|
|
1048
|
+
let kA, kB;
|
|
1049
|
+
let xA, xB, yA, yB;
|
|
1050
|
+
if (kind === "pan") {
|
|
1051
|
+
kA = kB = zoom;
|
|
1052
|
+
const sx = dx * (zoom - 1) * (W / 2) * panFrac;
|
|
1053
|
+
const sy = dy * (zoom - 1) * (H / 2) * panFrac;
|
|
1054
|
+
xA = cx - sx;
|
|
1055
|
+
xB = cx + sx;
|
|
1056
|
+
yA = cy - sy;
|
|
1057
|
+
yB = cy + sy;
|
|
1058
|
+
} else {
|
|
1059
|
+
kA = kind === "in" ? 1 : zoom;
|
|
1060
|
+
kB = kind === "in" ? zoom : 1;
|
|
1061
|
+
xA = cx + dx * (kA - 1) * (W / 2) * panFrac;
|
|
1062
|
+
xB = cx + dx * (kB - 1) * (W / 2) * panFrac;
|
|
1063
|
+
yA = cy + dy * (kA - 1) * (H / 2) * panFrac;
|
|
1064
|
+
yB = cy + dy * (kB - 1) * (H / 2) * panFrac;
|
|
1065
|
+
}
|
|
1066
|
+
nodes.push(
|
|
1067
|
+
image({
|
|
1068
|
+
id: nid,
|
|
1069
|
+
src: slide.src,
|
|
1070
|
+
x: xA,
|
|
1071
|
+
y: yA,
|
|
1072
|
+
width: W,
|
|
1073
|
+
height: H,
|
|
1074
|
+
anchor: "center",
|
|
1075
|
+
scale: kA,
|
|
1076
|
+
opacity: i === 0 ? 1 : 0
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
const ken = tween(
|
|
1080
|
+
nid,
|
|
1081
|
+
{ scale: kB, x: xB, y: yB },
|
|
1082
|
+
{ duration: slideHold, ease: "easeInOutQuad", label: `shot-${i}` }
|
|
1083
|
+
);
|
|
1084
|
+
const shot = i === 0 ? par(ken) : par(
|
|
1085
|
+
ken,
|
|
1086
|
+
tween(`${id}-${i - 1}`, { opacity: 0 }, { duration: transition, ease: "linear", label: `cross-${i}` }),
|
|
1087
|
+
tween(nid, { opacity: 1 }, { duration: transition, ease: "linear" })
|
|
1088
|
+
);
|
|
1089
|
+
shots.push(shot);
|
|
1090
|
+
});
|
|
1091
|
+
if (grade) {
|
|
1092
|
+
nodes.push(
|
|
1093
|
+
rect({
|
|
1094
|
+
id: `${id}-vignette`,
|
|
1095
|
+
x: 0,
|
|
1096
|
+
y: 0,
|
|
1097
|
+
width: W,
|
|
1098
|
+
height: H,
|
|
1099
|
+
fill: radialGradient(
|
|
1100
|
+
[
|
|
1101
|
+
{ offset: 0.55, color: "#FFFFFF" },
|
|
1102
|
+
{ offset: 1, color: "#6E6E6E" }
|
|
1103
|
+
],
|
|
1104
|
+
{ cx: 0.5, cy: 0.5, r: 0.72 }
|
|
1105
|
+
),
|
|
1106
|
+
blend: "multiply"
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
nodes.push(
|
|
1110
|
+
rect({
|
|
1111
|
+
id: `${id}-scrim`,
|
|
1112
|
+
x: 0,
|
|
1113
|
+
y: 0,
|
|
1114
|
+
width: W,
|
|
1115
|
+
height: H,
|
|
1116
|
+
fill: linearGradient(
|
|
1117
|
+
[
|
|
1118
|
+
{ offset: 0, color: "#00000000" },
|
|
1119
|
+
{ offset: 0.62, color: "#00000000" },
|
|
1120
|
+
{ offset: 1, color: "#000000B0" }
|
|
1121
|
+
],
|
|
1122
|
+
{ angle: 90 }
|
|
1123
|
+
)
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
return { nodes, timeline: beat("montage", { nodes: nodes.map((n3) => n3.id) }, [seq(...shots)]) };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1015
1130
|
// ../core/src/presets.ts
|
|
1016
1131
|
var PRESET_NAMES = [
|
|
1017
1132
|
"draw-bloom",
|
|
@@ -1021,7 +1136,7 @@ var PRESET_NAMES = [
|
|
|
1021
1136
|
"reveal-orbit",
|
|
1022
1137
|
"spin-forge"
|
|
1023
1138
|
];
|
|
1024
|
-
function
|
|
1139
|
+
function makeRng2(seed) {
|
|
1025
1140
|
let a = seed >>> 0 || 2654435769;
|
|
1026
1141
|
return () => {
|
|
1027
1142
|
a = a + 1831565813 | 0;
|
|
@@ -1033,7 +1148,7 @@ function makeRng(seed) {
|
|
|
1033
1148
|
var clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
1034
1149
|
var SET = 1 / 120;
|
|
1035
1150
|
function ctx(o) {
|
|
1036
|
-
const rand2 =
|
|
1151
|
+
const rand2 = makeRng2((o.seed ?? 0) + 1);
|
|
1037
1152
|
return {
|
|
1038
1153
|
e: clamp01(o.energy ?? 0.5),
|
|
1039
1154
|
sp: Math.max(0.25, o.speed ?? 1),
|
|
@@ -1588,7 +1703,7 @@ var CHARACTER_PRESET_NAMES = ["walk", "run", "jump", "dance", "wave", "cheer"];
|
|
|
1588
1703
|
var THIGH = 76;
|
|
1589
1704
|
var SHIN = 72;
|
|
1590
1705
|
var clamp012 = (x) => Math.max(0, Math.min(1, x));
|
|
1591
|
-
function
|
|
1706
|
+
function makeRng3(seed) {
|
|
1592
1707
|
let a = seed >>> 0 || 2654435769;
|
|
1593
1708
|
return () => {
|
|
1594
1709
|
a = a + 1831565813 | 0;
|
|
@@ -1599,7 +1714,7 @@ function makeRng2(seed) {
|
|
|
1599
1714
|
}
|
|
1600
1715
|
var dur2 = (base, sp) => base / sp;
|
|
1601
1716
|
function ctx2(o) {
|
|
1602
|
-
const rand2 =
|
|
1717
|
+
const rand2 = makeRng3((o.seed ?? 0) + 1);
|
|
1603
1718
|
return {
|
|
1604
1719
|
g: o.target,
|
|
1605
1720
|
label: o.label,
|
|
@@ -3273,6 +3388,7 @@ export {
|
|
|
3273
3388
|
path,
|
|
3274
3389
|
pathPoint,
|
|
3275
3390
|
pathTangentAngle,
|
|
3391
|
+
photoMontage,
|
|
3276
3392
|
poseTo,
|
|
3277
3393
|
radialGradient,
|
|
3278
3394
|
rect,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
|
8
8
|
export { cameraTo, 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
|
+
export { photoMontage, type MontageImage, type MontageOpts, type MontageResult, type KenBurns } from "./montage.js";
|
|
11
12
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
12
13
|
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
13
14
|
export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo montage — a SEEDED GENERATOR that turns a list of images into a polished
|
|
3
|
+
* slideshow: layered image nodes + a retimable `beat` that crossfades between
|
|
4
|
+
* slides and pans/zooms each (Ken Burns), with an optional cinematic grade
|
|
5
|
+
* (vignette + bottom scrim) built from gradients + blend modes. The photo analog
|
|
6
|
+
* of `motionPreset` / `splitText`.
|
|
7
|
+
*
|
|
8
|
+
* Pure and deterministic: the per-slide Ken Burns direction / framing is chosen by
|
|
9
|
+
* a seeded PRNG (mulberry32) — same (images, opts) → identical IR; a different
|
|
10
|
+
* `seed` re-frames within the same family. No Math.random / Date.
|
|
11
|
+
*
|
|
12
|
+
* Constraint it works around: the `image` node draws STRETCHED to width×height
|
|
13
|
+
* (no object-fit). So images must already be the frame's aspect ratio; each layer
|
|
14
|
+
* is sized to the frame and the Ken Burns keeps `scale >= 1` with the pan bounded
|
|
15
|
+
* to the scale's slack, so an edge is never revealed.
|
|
16
|
+
*/
|
|
17
|
+
import type { NodeIR, TimelineIR } from "./ir.js";
|
|
18
|
+
export type KenBurns = "in" | "out" | "pan";
|
|
19
|
+
/** One slide: a bare src, or a src with per-slide overrides. */
|
|
20
|
+
export type MontageImage = string | {
|
|
21
|
+
src: string;
|
|
22
|
+
hold?: number;
|
|
23
|
+
ken?: KenBurns;
|
|
24
|
+
};
|
|
25
|
+
export interface MontageOpts {
|
|
26
|
+
/** Node-id prefix → stable regen addresses `${id}-${i}`. Default "shot". */
|
|
27
|
+
id?: string;
|
|
28
|
+
/** Frame size; must match the scene size. Default 1920×1080. */
|
|
29
|
+
size?: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
};
|
|
33
|
+
/** Seconds each slide is held (incl. its incoming crossfade). Default 3.2. */
|
|
34
|
+
hold?: number;
|
|
35
|
+
/** Crossfade seconds between slides. Default 0.6. */
|
|
36
|
+
transition?: number;
|
|
37
|
+
/** Max Ken Burns zoom (>1). Default 1.18. */
|
|
38
|
+
zoom?: number;
|
|
39
|
+
/** Emit the vignette + bottom-scrim grade overlays. Default true. */
|
|
40
|
+
grade?: boolean;
|
|
41
|
+
/** Deterministic framing. Same seed → identical IR. Default 0. */
|
|
42
|
+
seed?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface MontageResult {
|
|
45
|
+
/** Image layers (+ grade overlays) — place these in `scene({ nodes })`. */
|
|
46
|
+
nodes: NodeIR[];
|
|
47
|
+
/** The montage beat — place in `scene({ timeline })` (compose with `seq`). */
|
|
48
|
+
timeline: TimelineIR;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a montage from a list of frame-aspect images.
|
|
52
|
+
*
|
|
53
|
+
* const m = photoMontage(["a.jpg", "b.jpg", "c.jpg"], { seed: 7 });
|
|
54
|
+
* scene({ size, nodes: [...m.nodes, ...titles], timeline: seq(m.timeline) });
|
|
55
|
+
*/
|
|
56
|
+
export declare function photoMontage(images: MontageImage[], opts?: MontageOpts): MontageResult;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -289,6 +289,31 @@ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 }
|
|
|
289
289
|
Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
|
|
290
290
|
`textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
|
|
291
291
|
|
|
292
|
+
## Photo montage (`photoMontage`)
|
|
293
|
+
|
|
294
|
+
Turn a list of images into a polished slideshow — crossfades + seeded Ken Burns
|
|
295
|
+
(pan/zoom) + an optional cinematic grade (vignette + bottom scrim via gradients +
|
|
296
|
+
blend) — without hand-wiring each move. The photo analog of `motionPreset`.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
const m = photoMontage(["a.jpg", "b.jpg", "c.jpg"], {
|
|
300
|
+
id: "shot", size: { width: 1920, height: 1080 },
|
|
301
|
+
hold: 3.4, transition: 0.7, zoom: 1.16, seed: 7,
|
|
302
|
+
});
|
|
303
|
+
scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTrack) });
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- Returns `{ nodes, timeline }` (like `splitText` owns its glyph nodes). `nodes` are
|
|
307
|
+
the stacked image layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
308
|
+
`timeline` is a retimable `beat("montage", …)`. Stable addresses: `${id}-${i}`,
|
|
309
|
+
labels `shot-${i}` / `cross-${i}`.
|
|
310
|
+
- **Images must be the frame's aspect ratio** — the `image` node draws stretched
|
|
311
|
+
(no object-fit), so cover-crop your photos to `size` first. The Ken Burns keeps
|
|
312
|
+
`scale ≥ 1` with the pan bounded to its slack, so an edge is never revealed.
|
|
313
|
+
- Per-slide overrides: `{ src, hold?, ken? }` where `ken` is `"in" | "out" | "pan"`.
|
|
314
|
+
- Seeded + pure (same `(images, opts)` → identical IR). Note: image-node sources do
|
|
315
|
+
not render in `reframe player` / artifacts — montage ships as mp4.
|
|
316
|
+
|
|
292
317
|
## Cursor (UI demos)
|
|
293
318
|
|
|
294
319
|
A vector mouse pointer that glides across the scene and clicks things — for app
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|