greenlight-motion 0.2.2 → 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 +9 -9
- package/cli/index.mjs +9 -1
- package/dist/bundle-inputs.json +1 -0
- package/dist/greenlight-motion-script.mjs +25 -9
- package/dist/greenlight-motion.js +497 -496
- package/dist/greenlight-motion.mjs +17707 -17658
- package/package.json +1 -1
- package/skill/SKILL.md +17 -14
- package/skill/references/scene-script.md +6 -4
- package/skill/references/video-backgrounds.md +1 -1
- package/skill/references/video-camera.md +3 -1
- package/skill/references/video-particles.md +5 -5
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.
|
|
21
|
-
npx greenlight-motion@0.2.
|
|
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.
|
|
25
|
-
npx greenlight-motion@0.2.
|
|
26
|
-
npx greenlight-motion@0.2.
|
|
27
|
-
npx greenlight-motion@0.2.
|
|
28
|
-
npx greenlight-motion@0.2.
|
|
29
|
-
npx greenlight-motion@0.2.
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/dist/bundle-inputs.json
CHANGED
|
@@ -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 <
|
|
21063
|
-
problems.push(`${cw}: keyframes.${name} needs
|
|
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)
|
|
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 <
|
|
21404
|
-
problems.push(`${ew}: keyframes.${name} needs
|
|
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
|
|
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).
|