greenlight-motion 0.2.3 → 0.2.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/README.md CHANGED
@@ -17,16 +17,16 @@ The agent writes the video as data: a `project.json` and its files. The engine t
17
17
  ## Quick start
18
18
 
19
19
  ```bash
20
- npx greenlight-motion@0.2.3 doctor # Node 18+, Chrome or Edge, WebGPU, H.264: what's missing and how to fix it
21
- npx greenlight-motion@0.2.3 init my-video # a working starter: index.html, project.json, assets/
20
+ npx greenlight-motion@0.2.4 doctor # Node 18+, Chrome or Edge, WebGPU, H.264: what's missing and how to fix it
21
+ npx greenlight-motion@0.2.4 init my-video # a working starter: index.html, project.json, assets/
22
22
  # edit my-video/project.json, put pictures / video / sound in my-video/assets/
23
23
  # or: write my-video/script.json (a scene script, skill/references/scene-script.md), then
24
- npx greenlight-motion@0.2.3 script my-video # script.json → project.json with the Video Editor's own presets
25
- npx greenlight-motion@0.2.3 check my-video # what to fix: missing files, template mistakes, blank frames, broken plugins
26
- npx greenlight-motion@0.2.3 stills my-video # frames + a labelled contact sheet, to look at
27
- npx greenlight-motion@0.2.3 render my-video --fps 60 --motion-blur
28
- npx greenlight-motion@0.2.3 pack my-video # everything in ONE html file
29
- npx greenlight-motion@0.2.3 pack my-video --glvideo # ONE .glvideo: double-clicked, it opens in GreenLight Dash
24
+ npx greenlight-motion@0.2.4 script my-video # script.json → project.json with the Video Editor's own presets
25
+ npx greenlight-motion@0.2.4 check my-video # what to fix: missing files, template mistakes, blank frames, broken plugins
26
+ npx greenlight-motion@0.2.4 stills my-video # frames + a labelled contact sheet, to look at
27
+ npx greenlight-motion@0.2.4 render my-video --fps 60 --motion-blur
28
+ npx greenlight-motion@0.2.4 pack my-video # everything in ONE html file
29
+ npx greenlight-motion@0.2.4 pack my-video --glvideo # ONE .glvideo: double-clicked, it opens in GreenLight Dash
30
30
  ```
31
31
 
32
32
  Also: `trace` (every animated layer's values frame by frame, plus the layer's moments), `set key=value`
@@ -72,7 +72,7 @@ The engine is one classic script, so it runs from a double-clicked file, a CDN o
72
72
 
73
73
  ```html
74
74
  <div id="app"></div>
75
- <script src="https://cdn.jsdelivr.net/npm/greenlight-motion@0.2.3/dist/greenlight-motion.js"></script>
75
+ <script src="https://cdn.jsdelivr.net/npm/greenlight-motion@0.2.4/dist/greenlight-motion.js"></script>
76
76
  <script>
77
77
  GreenLightMotion.mountStudio(document.getElementById('app'), {
78
78
  project: { /* project.json */ },
package/cli/index.mjs CHANGED
@@ -344,12 +344,20 @@ async function check() {
344
344
  else if (p.ok == null) problems.push({ code: 'plugin', ref, detail: 'it did not load', fix: "check the package's exports against the custom-effects / advanced-transitions starter and that its file name is its id" });
345
345
  }
346
346
  for (const e of errors) problems.push({ code: 'page-error', detail: e, fix: 'a value in project.json the engine cannot use; compare with the references in the skill' });
347
- return { ok: problems.length === 0, duration, problems };
347
+ // notes: worth a look, not wrong by themselves — text the engine wraps onto more lines than it was written with
348
+ const notes = [];
349
+ for (const w of await page.evaluate((proj) => window.GreenLightMotion.textWraps?.(proj) || [], project)) {
350
+ notes.push({ code: 'text-wraps', ref: w.id, rows: w.rows, written: w.written,
351
+ detail: `"${w.text}" (${w.fontPx} px) wraps onto ${w.rows} lines in its ${w.widthPx} px box, written as ${w.written}`,
352
+ fix: `if the break is wanted, write it into the text (\\n); if not, widen the box (textWidthPx, or the script's width) or lower the size` });
353
+ }
354
+ return { ok: problems.length === 0, duration, problems, notes };
348
355
  });
349
356
  if (flags.json) console.log(JSON.stringify(result, null, 2));
350
357
  else {
351
358
  console.log(result.ok ? `ok — ${result.duration.toFixed(2)} s, nothing to fix` : `${result.problems.length} problem(s):`);
352
359
  for (const p of result.problems) console.log(`- ${p.code}: ${p.ref || p.detail || (p.times || []).join(', ')}\n → ${p.fix}`);
360
+ for (const n of result.notes || []) console.log(`note — ${n.code} ${n.ref}: ${n.detail}\n → ${n.fix}`);
353
361
  }
354
362
  process.exitCode = result.ok ? 0 : 1;
355
363
  }
@@ -356,6 +356,7 @@
356
356
  "frontend/src/motionEngine/studio/review.js",
357
357
  "frontend/src/motionEngine/studio/voice.js",
358
358
  "frontend/src/motionEngine/template.js",
359
+ "frontend/src/motionEngine/textWraps.js",
359
360
  "frontend/src/motionEngine/trace.js",
360
361
  "frontend/src/motionEngine/useReview.js",
361
362
  "frontend/src/platform/compact.js",
@@ -20844,6 +20844,12 @@ var contrast = (a, b2) => {
20844
20844
  return la == null || lb == null ? 21 : (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
20845
20845
  };
20846
20846
  var EDGE_FILLED = /* @__PURE__ */ new Set(["Advanced Movement"]);
20847
+ var backdropLuma = (bg) => {
20848
+ if (!bg) return null;
20849
+ const hexes = bg.type === "gradient" ? String(bg.gradientCss || "").match(/#[0-9a-f]{6}\b|#[0-9a-f]{3}\b/gi) || [] : bg.type === "solid" ? [bg.solidColor] : [];
20850
+ const ls = hexes.map(luminance).filter((v) => v != null);
20851
+ return ls.length ? ls.reduce((a, b2) => a + b2, 0) / ls.length : null;
20852
+ };
20847
20853
  var GOOGLE_FAMILIES = new Set(googleFontCatalog_default.split("\n").map((l) => l.split("|")[0].trim().toLowerCase()).filter(Boolean));
20848
20854
  var KF_IN = new Map(kfpVariants(KEYFRAME_PRESETS, "in").map((p) => [p.id, p]));
20849
20855
  var KF_OUT = new Map(kfpVariants(KEYFRAME_PRESETS, "out").map((p) => [p.id, p]));
@@ -21059,8 +21065,8 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
21059
21065
  problems.push(`${cw}: keyframes.${name} \u2014 ${mode === "rig" ? "a rig" : "zooms"} drive${mode === "rig" ? "s" : ""} ${CAM_DRIVEN[mode].join(", ")} themselves; key ${Object.keys(CAM_TRACKS).filter((t) => !CAM_DRIVEN[mode].includes(t)).join(", ")}`);
21060
21066
  continue;
21061
21067
  }
21062
- if (!Array.isArray(keys) || keys.length < 2) {
21063
- problems.push(`${cw}: keyframes.${name} needs two or more [seconds, value] keys`);
21068
+ if (!Array.isArray(keys) || keys.length < 1) {
21069
+ problems.push(`${cw}: keyframes.${name} needs one or more [seconds, value] keys (one key holds that value)`);
21064
21070
  continue;
21065
21071
  }
21066
21072
  const list = [];
@@ -21086,6 +21092,7 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
21086
21092
  list.push({ position: r3(clamp5(Number(t) / len * 100, 0, 100)), value: String(r3(value)), easing });
21087
21093
  }
21088
21094
  if (list.length === keys.length) {
21095
+ if (list.length === 1) list.splice(0, 1, { ...list[0], position: 0, easing: "Linear" }, { ...list[0], position: 100, easing: "Linear" });
21089
21096
  if (pk[track] && track !== "camPathT") warnings.push(`${cw}: keyframes.${name} replace the ${track} keys of move "${c.move}"`);
21090
21097
  pk[track] = list.sort((a, b2) => a.position - b2.position);
21091
21098
  if (name === "focusZ") item.camDofEnabled = 1;
@@ -21342,7 +21349,12 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
21342
21349
  const kf = {};
21343
21350
  const drawn = el?.draw !== false ? r3(Math.min(100, 0.9 / (end - start) * 100)) : 0;
21344
21351
  if (drawn) kf.lineTrimEnd = [{ position: 0, value: "0", easing: "AE Out" }, { position: drawn, value: String(full), easing: "Linear" }];
21345
- if (el?.drain) kf.lineTrimStart = [{ position: drawn, value: "0", easing: "Linear" }, { position: 100, value: String(full), easing: "Linear" }];
21352
+ if (el?.drain) {
21353
+ const len2 = Math.max(1e-3, end - start);
21354
+ const [d0, d1] = Array.isArray(el.drain) && el.drain.length === 2 && el.drain.every((v) => Number.isFinite(Number(v))) ? el.drain.map((v) => clamp5(Number(v) / len2 * 100, 0, 100)) : [drawn, 100];
21355
+ if (d1 <= d0) problems.push(`${ew}: drain is [start, end] seconds into the element, end after start`);
21356
+ else kf.lineTrimStart = [{ position: r3(d0), value: "0", easing: "Linear" }, { position: r3(d1), value: String(full), easing: "Linear" }];
21357
+ }
21346
21358
  if (Object.keys(kf).length) item.propertyKeyframes = kf;
21347
21359
  if (!el?.color || el.color === "accent") accentTargets.push({ itemId: id, path: "shapeFillColor" });
21348
21360
  } else if (type === "line") {
@@ -21400,8 +21412,8 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
21400
21412
  problems.push(`${ew}: keyframes "${name}" \u2014 one of ${Object.keys(KF_TRACKS).join(", ")}`);
21401
21413
  continue;
21402
21414
  }
21403
- if (!Array.isArray(keys) || keys.length < 2) {
21404
- problems.push(`${ew}: keyframes.${name} needs two or more [seconds, value] keys`);
21415
+ if (!Array.isArray(keys) || keys.length < 1) {
21416
+ problems.push(`${ew}: keyframes.${name} needs one or more [seconds, value] keys (one key holds that value)`);
21405
21417
  continue;
21406
21418
  }
21407
21419
  const list = [];
@@ -21418,6 +21430,7 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
21418
21430
  list.push({ position: r3(clamp5(Number(t) / len * 100, 0, 100)), value: String(name === "opacity" ? clamp5(Number(v), 0, 1) : Number(v)), easing });
21419
21431
  }
21420
21432
  if (list.length === keys.length) {
21433
+ if (list.length === 1) list.splice(0, 1, { ...list[0], position: 0, easing: "Linear" }, { ...list[0], position: 100, easing: "Linear" });
21421
21434
  if (item.propertyKeyframes?.[track]) warnings.push(`${ew}: keyframes.${name} replace the ${track} keys of its entrance / exit`);
21422
21435
  item.propertyKeyframes = { ...item.propertyKeyframes || {}, [track]: list.sort((a, b2) => a.position - b2.position) };
21423
21436
  }
@@ -21465,6 +21478,7 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
21465
21478
  if (own.includes(k)) own.splice(own.indexOf(k), 1);
21466
21479
  }
21467
21480
  }
21481
+ if (tr === "Circular Wipe" && opts.beam === void 0 && (backdropLuma(sceneBg || projectBg) ?? 0) > 0.4) opts.beam = false;
21468
21482
  for (const k of own) accentTargets.push({ itemId: sceneId, path: `transitionOptions.${k}` });
21469
21483
  if (Object.keys(opts).length) sceneItem.transitionOptions = opts;
21470
21484
  sceneItem.transitionDuration = r3(clamp5(num7(sc?.transitionDuration, 0.6), 0.2, Math.min(prev, D) / 2));
@@ -21590,25 +21604,27 @@ matte, masks, particles, 3D) goes on from \`project.json\`: SKILL.md, "Beyond th
21590
21604
  a CSS gradient; \`width\` (px, wraps), \`align\`, \`uppercase\`, \`letterSpacing\`, \`lineHeight\`. A headline gets \`glide-text\` unless you set \`textAnimation\`.
21591
21605
  The text box is CENTRED on \`at\`: without \`width\` it is as wide as its words but stays inside the frame (off-centre text wraps
21592
21606
  sooner); for a left-aligned column give \`width\` and \`"align": "left"\` and put \`at\` at the column's centre.
21593
- - **rect \xB7 pill \xB7 circle**: \`w\`, \`h\`, \`color\` (or a CSS gradient), \`radius\`, \`border\` {width, color}, \`shadow\`: true. They enter with \`pop-overshoot\` unless you set \`enter\`.
21607
+ - **rect \xB7 pill \xB7 circle**: \`w\`, \`h\`, \`color\` (or a CSS gradient), \`radius\`, \`border\` {width, color}, \`shadow\`: true. They enter with \`pop-overshoot\` unless you set \`enter\` (\`"none"\`: no entrance, it is simply there).
21594
21608
  - **line**: from \`at\` to \`to\`, \`width\`, drawn on (\`draw\`: false to skip).
21595
21609
  - **ring**: a circle drawn as a stroke from the top, clockwise (a timer, a status or focus ring): \`w\` (diameter), \`width\` (stroke),
21596
21610
  \`color\`, \`sweep\` (degrees, 360 = full), drawn on (\`draw\`: false to skip), \`drain\`: true empties it again by the element's
21597
- end (a countdown), \`ghost\` (0\u2013100: a faint full track under it).
21611
+ end (a countdown) or \`[start, end]\` (s into the element) empties it then, \`ghost\` (0\u2013100: a faint full track under it).
21598
21612
  - **image \xB7 video**: \`src\` (${host === "chat" ? "a link or the user's file \u2014 below" : "a file in the folder's assets/ as `asset:<name>`, or an https link"}) or, for an image, \`svg\` (SVG markup you write); \`w\` or \`h\`
21599
21613
  (px; the other follows the file's shape) or \`fit\` cover | contain
21600
21614
  (default: a video covers the frame, a picture sits at 70 % of contain), \`rotate\` (deg). A video also takes
21601
21615
  \`startAt\` (s into the file) and \`volume\` (0\u2013100, default muted). No entrance unless you set \`enter\`.
21602
21616
  - **Keyframes on any element** \u2014 your own motion: \`"keyframes": { "x": [[0, -400], [0.8, 0, "Smooth Glide"]], "scale": [[0, 0.9], [2, 1.05]] }\`
21603
21617
  \u2014 tracks x, y (px offsets from \`at\`), scale, rotate (deg), opacity (0\u20131); each key [seconds into the element, value, easing of the
21604
- segment after it \u2014 Smooth Glide, AE Smooth, Expo Out, Back Out, Elastic, Linear \u2026]. They replace an entrance's keys on the same track.
21618
+ segment after it \u2014 Smooth Glide, AE Smooth, Expo Out, Back Out, Elastic, Linear \u2026]. They replace an entrance's keys on the same track;
21619
+ one key holds that value for the whole element.
21605
21620
  - **Every element**: \`start\` / \`end\` (s within its scene; default: staggered 0.12 s, to the scene's end), \`enter\` / \`exit\` (keyframe presets, \`direction\` up | down | left | right
21606
21621
  for directional ones), \`effects\`: [{ "id", "params"? }], \`sound\` (played at its start), \`id\`, \`label\` (its name in the page's options), \`opacity\` (0\u20131),
21607
21622
  \`z\` (px of depth, + toward the viewer, default 0: a real camera's dolly, orbit and depth of field separate layers by it), \`locked\`: true
21608
21623
  (the scene's camera doesn't move it: a logo, a caption). Keyframes also take a \`z\` track.
21609
21624
  - **Scene**: \`duration\` (s), \`transition\` into it (default Cross Fade, 0.6 s; \`transitionDuration\`), \`transitionOptions\` (the
21610
21625
  transition's own settings, e.g. Circular Wipe's \`point\` { x, y } (% of the frame from its top-left: 50, 50 is the centre) and \`feather\`; ${host === "chat" ? 'every option: get_motion_reference "video-transitions"' : "every option: `npx greenlight-motion schema transitions`"}.
21611
- A transition's vivid colours, such as Circular Wipe's beam, take the accent unless you set them), \`camera\` (below; default push-in),
21626
+ A transition's vivid colours, such as Circular Wipe's beam, take the accent unless you set them; over a light backdrop Circular
21627
+ Wipe's glowing beam is off, since it flares there, unless you set \`beam\`), \`camera\` (below; default push-in),
21612
21628
  \`background\` (overrides style), \`sounds\`: [{ "sfx", "at" }]. Script-level \`sound\`: auto (default: a whoosh on each transition and
21613
21629
  on the first headline, from the GreenLight sound library${host === "chat" ? "" : ", which the CLI downloads into assets/sounds/ when it builds"}) | none (nothing from the library: when
21614
21630
  nothing may be downloaded, use \`music\` with a file you made).