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 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 makeRng(seed) {
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 = makeRng((o.seed ?? 0) + 1);
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 makeRng2(seed) {
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 = makeRng2((o.seed ?? 0) + 1);
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,
@@ -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;
@@ -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",
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",