greenlight-motion 0.1.0 → 0.1.2

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,14 +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.1.0 doctor # Node 18+, Chrome or Edge, WebGPU, H.264: what's missing and how to fix it
21
- npx greenlight-motion@0.1.0 init my-video # a working starter: index.html, project.json, assets/
20
+ npx greenlight-motion@0.1.2 doctor # Node 18+, Chrome or Edge, WebGPU, H.264: what's missing and how to fix it
21
+ npx greenlight-motion@0.1.2 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
- npx greenlight-motion@0.1.0 check my-video # what to fix: missing files, template mistakes, blank frames, broken plugins
24
- npx greenlight-motion@0.1.0 stills my-video # frames + a labelled contact sheet, to look at
25
- npx greenlight-motion@0.1.0 render my-video --fps 60 --motion-blur
26
- npx greenlight-motion@0.1.0 pack my-video # everything in ONE html file
27
- npx greenlight-motion@0.1.0 pack my-video --glvideo # ONE .glvideo: double-clicked, it opens in GreenLight Dash
23
+ # or: write my-video/script.json (a scene script, skill/references/scene-script.md), then
24
+ npx greenlight-motion@0.1.2 script my-video # script.json → project.json with the Video Editor's own presets
25
+ npx greenlight-motion@0.1.2 check my-video # what to fix: missing files, template mistakes, blank frames, broken plugins
26
+ npx greenlight-motion@0.1.2 stills my-video # frames + a labelled contact sheet, to look at
27
+ npx greenlight-motion@0.1.2 render my-video --fps 60 --motion-blur
28
+ npx greenlight-motion@0.1.2 pack my-video # everything in ONE html file
29
+ npx greenlight-motion@0.1.2 pack my-video --glvideo # ONE .glvideo: double-clicked, it opens in GreenLight Dash
28
30
  ```
29
31
 
30
32
  Also: `trace` (every animated layer's values frame by frame, plus the layer's moments), `set key=value`
@@ -35,11 +37,34 @@ catalog).
35
37
 
36
38
  Read **[skill/SKILL.md](skill/SKILL.md)**. It covers the workflow, the project format, templates,
37
39
  sound cues, plugins and how to make motion look made rather than generated. The format references
38
- are in `skill/references/`. To install the skill for Claude Code:
40
+ are in `skill/references/`.
39
41
 
40
- ```bash
41
- mkdir -p ~/.claude/skills && cp -R node_modules/greenlight-motion/skill ~/.claude/skills/greenlight-motion
42
- ```
42
+ Setup for each agent is in the
43
+ [repository README](https://github.com/wpsoul/greenlight-motion-engine#ways-to-use-it):
44
+
45
+ - **Claude or ChatGPT:** add the connector `https://mcp.greenlightdash.pro/motion`. The video plays in the chat,
46
+ and the model reviews the frames your browser renders.
47
+ - **Claude Code:**
48
+
49
+ ```bash
50
+ claude plugin marketplace add wpsoul/greenlight-motion-engine
51
+ claude plugin install greenlight-motion@greenlight-motion-engine
52
+ ```
53
+
54
+ - **Codex:**
55
+
56
+ ```bash
57
+ codex plugin marketplace add wpsoul/greenlight-motion-engine
58
+ codex plugin add greenlight-motion@greenlight-motion-engine
59
+ ```
60
+
61
+ - **Only the skill:**
62
+
63
+ ```bash
64
+ mkdir -p ~/.claude/skills && cp -R node_modules/greenlight-motion/skill ~/.claude/skills/greenlight-motion
65
+ ```
66
+
67
+ For Codex, copy it to `~/.agents/skills/greenlight-motion` instead.
43
68
 
44
69
  ## In a web page or a Claude artifact
45
70
 
@@ -47,16 +72,21 @@ The engine is one classic script, so it runs from a double-clicked file, a CDN o
47
72
 
48
73
  ```html
49
74
  <div id="app"></div>
50
- <script src="https://cdn.jsdelivr.net/npm/greenlight-motion@0.1.0/dist/greenlight-motion.js"></script>
75
+ <script src="https://cdn.jsdelivr.net/npm/greenlight-motion@0.1.2/dist/greenlight-motion.js"></script>
51
76
  <script>
52
- GreenLightMotion.mountPage(document.getElementById('app'), {
77
+ GreenLightMotion.mountStudio(document.getElementById('app'), {
53
78
  project: { /* project.json */ },
54
79
  assets: { 'asset:photo.jpg': 'data:image/jpeg;base64,…' }, // ref → data: / blob: / https: URL or Blob
55
- theme: 'auto', // 'dark' (default) | 'light' | 'auto'
56
80
  });
57
81
  </script>
58
82
  ```
59
83
 
84
+ `mountStudio` is the page every GreenLight surface shows (a project folder's page, a chat's GreenLight Motion page, a
85
+ shared link, a review card on a board): the preview with Edit · Notes · Highlight, the inspector, the layers and Render.
86
+ Its review (notes with drawings, highlights, changes) is text to copy for your agent; a host can send it elsewhere
87
+ (`config.save`). `GreenLightMotion.mountPage(el, { project, assets, theme })` is the plain page under it, without the
88
+ review interface.
89
+
60
90
  Without the page UI (in the browser, through a bundler or the ESM build; Node runs only the CLI):
61
91
 
62
92
  ```js
@@ -1,6 +1,7 @@
1
1
  # Third-party licences
2
2
 
3
- The engine bundle (`dist/greenlight-motion.js`, `dist/greenlight-motion.mjs`) includes code from these packages.
3
+ The engine bundle (`dist/greenlight-motion.js`, `dist/greenlight-motion.mjs`) and the scene script (`dist/greenlight-motion-script.mjs`)
4
+ include code from these packages.
4
5
  Generated by `scripts/prepare.mjs` from the build. Files on the GreenLight CDN are listed in
5
6
  [legal/THIRD_PARTY_ASSETS.md](legal/THIRD_PARTY_ASSETS.md); artwork credits are in [NOTICE](NOTICE).
6
7
 
package/cli/index.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  //
4
4
  // npx greenlight-motion doctor what this computer has, and how to fix what's missing
5
5
  // npx greenlight-motion init <dir> [--from p.json] a project folder: index.html, project.json, assets/
6
+ // npx greenlight-motion script <dir> [--from s.json] [--force] <dir>/script.json (a scene script) → project.json
6
7
  // npx greenlight-motion build <dir> project.json + assets/ → a page the user double-clicks
7
8
  // npx greenlight-motion check <dir> [--json] problems to fix (missing files, template, errors, blank frames)
8
9
  // npx greenlight-motion stills <dir> [--at 0.5,2] [--count 6] [--out d] PNG frames + a labelled contact sheet
@@ -20,6 +21,7 @@
20
21
  import { createRequire } from 'node:module';
21
22
  import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, copyFileSync, rmSync } from 'node:fs';
22
23
  import { spawn, spawnSync } from 'node:child_process';
24
+ import { createHash } from 'node:crypto';
23
25
  import path from 'node:path';
24
26
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
27
  import { applyEdits } from './apply.mjs';
@@ -112,7 +114,7 @@ const indexHtml = (title) => `<!doctype html>
112
114
  <script src="greenlight-motion.js"></script>
113
115
  <script src="project.js"></script>
114
116
  <script src="assets.js"></script>
115
- <script>GreenLightMotion.mountPage(document.getElementById('app'), { project: window.GL_PROJECT, assets: window.GL_ASSETS });</script>
117
+ <script>GreenLightMotion.mountStudio(document.getElementById('app'), { project: window.GL_PROJECT, assets: window.GL_ASSETS });</script>
116
118
  `;
117
119
  // JSON inside a <script>: no "</script", no "<!--" (both escapes stay valid JSON and JS, so the app can read a page back)
118
120
  const scriptSafe = (s) => s.replace(/<\/(script)/gi, '<\\/$1').replace(/<!--/g, '<\\u0021--');
@@ -483,6 +485,85 @@ async function apply() {
483
485
  }
484
486
  }
485
487
 
488
+ // ── the scene script: <dir>/script.json → <dir>/project.json, with the converter the GreenLight Motion connector's
489
+ // build_video uses (dist/greenlight-motion-script.mjs — frontend/src/motionEngine/sceneScript.js; the format:
490
+ // skill/references/scene-script.md). The script is the source: project.json is rewritten on every run, so a project
491
+ // edited by hand since then is kept unless --force.
492
+ async function script() {
493
+ const dir = rest[0];
494
+ if (!dir) fail('script needs a folder', 'npx greenlight-motion script my-video (it reads my-video/script.json)');
495
+ const S = await import(pathToFileURL(path.join(PKG, 'dist', 'greenlight-motion-script.mjs')).href);
496
+ const from = flags.from ? path.resolve(String(flags.from)) : path.join(dir, 'script.json');
497
+ if (!existsSync(from)) fail(`no scene script at ${from}`, 'write one (skill/references/scene-script.md) or pass --from <file.json>');
498
+ let scene;
499
+ try { scene = JSON.parse(readFileSync(from, 'utf8')); } catch (e) { fail(`${from} is not valid JSON: ${e.message}`); }
500
+ const projectFile = path.join(dir, 'project.json');
501
+ const stamp = path.join(dir, '.script-built'); // the sha256 of the project.json the last run wrote
502
+ if (existsSync(projectFile) && !flags.force) {
503
+ const now = createHash('sha256').update(readFileSync(projectFile)).digest('hex');
504
+ const built = existsSync(stamp) ? readFileSync(stamp, 'utf8').trim() : '';
505
+ if (now !== built) fail(`${projectFile} ${built ? 'was edited since script wrote it' : 'was not written by script'}`, 'pass --force to replace it (its own edits are lost), or go on editing project.json with apply');
506
+ }
507
+ mkdirSync(path.join(dir, 'assets'), { recursive: true });
508
+ if (path.resolve(from) !== path.resolve(dir, 'script.json')) writeFileSync(path.join(dir, 'script.json'), JSON.stringify(scene, null, 2) + '\n');
509
+ // every src: a file in assets/ (asset:<name>), a file on this computer (copied in) or an https link (downloaded in,
510
+ // so the folder plays offline and the engine may draw it) — its kind and size read from its own bytes
511
+ const files = new Map(); const problems = [];
512
+ for (const src of S.scriptSources(scene)) {
513
+ try { files.set(src, await scriptFile(S, dir, src)); } catch (e) { problems.push(`src "${String(src).slice(0, 80)}": ${e.message}`); }
514
+ }
515
+ const caps = JSON.parse(readFileSync(path.join(PKG, 'schema', 'capabilities.json'), 'utf8'));
516
+ const out = S.compileScript(scene, caps, files);
517
+ out.problems.unshift(...problems);
518
+ for (const w of out.warnings) console.log(`warning: ${w}`);
519
+ if (out.problems.length) {
520
+ console.error(`greenlight-motion: the script can't be converted yet — fix these and run script again:\n${out.problems.map((p) => ` - ${p}`).join('\n')}`);
521
+ process.exit(1);
522
+ }
523
+ const text = JSON.stringify(out.project, null, 2) + '\n';
524
+ writeFileSync(projectFile, text);
525
+ writeFileSync(stamp, createHash('sha256').update(text).digest('hex') + '\n');
526
+ build(dir);
527
+ console.log(`\n${out.duration} s, ${out.summary.length} scene${out.summary.length === 1 ? '' : 's'}:\n${out.summary.join('\n')}
528
+ \nnext: npx greenlight-motion check ${dir} → stills ${dir} (look at the contact sheet, fix script.json, run script again) → render ${dir}`);
529
+ }
530
+ async function scriptFile(S, dir, src) {
531
+ const take = (bytes, name) => {
532
+ const type = S.sniff(bytes);
533
+ if (!type) throw new Error('not a picture, video or sound the engine plays (PNG, JPEG, GIF, WebP, SVG, MP4, MOV, WebM, MP3, M4A, WAV, OGG)');
534
+ const kind = S.KIND[type];
535
+ if (bytes.length > S.LIMITS[kind]) throw new Error(`the ${kind} is ${Math.round(bytes.length / 1048576)} MB (up to ${S.LIMITS[kind] / 1048576} MB)`);
536
+ return { url: `asset:${name}`, kind, type, name, ...S.probe(bytes, type) };
537
+ };
538
+ const into = (name, bytes) => { // a file brought in: under its own name, or a numbered one if a different file has it
539
+ const base = path.basename(name).replace(/[^\w.-]+/g, '-') || 'file';
540
+ let n = base;
541
+ for (let i = 2; existsSync(path.join(dir, 'assets', n)) && !readFileSync(path.join(dir, 'assets', n)).equals(bytes); i += 1) n = base.replace(/(\.\w+)?$/, `-${i}$1`);
542
+ writeFileSync(path.join(dir, 'assets', n), bytes);
543
+ return n;
544
+ };
545
+ if (/^asset:/i.test(src)) {
546
+ const name = src.replace(/^asset:/i, '');
547
+ const file = path.join(dir, 'assets', name);
548
+ if (!existsSync(file)) throw new Error(`put the file at ${file}`);
549
+ return take(readFileSync(file), name);
550
+ }
551
+ if (/^https?:\/\//i.test(src)) {
552
+ const r = await fetch(src, { redirect: 'follow', signal: AbortSignal.timeout(60000) });
553
+ if (!r.ok) throw new Error(`the link answered ${r.status}`);
554
+ const bytes = Buffer.from(await r.arrayBuffer());
555
+ let name = 'file';
556
+ try { name = decodeURIComponent(new URL(r.url || src).pathname.split('/').pop() || 'file'); } catch { /* the default */ }
557
+ const type = S.sniff(bytes);
558
+ if (type && !new RegExp(`\\.${type === 'jpg' ? 'jpe?g' : type}$`, 'i').test(name)) name = `${name.replace(/\.\w*$/, '') || 'file'}.${type}`;
559
+ return take(bytes, into(name, bytes));
560
+ }
561
+ const local = path.resolve(src);
562
+ if (!existsSync(local)) throw new Error(`no such file — put it in ${path.join(dir, 'assets')}/ and write "asset:<file name>"`);
563
+ const bytes = readFileSync(local);
564
+ return take(bytes, into(path.basename(local), bytes));
565
+ }
566
+
486
567
  function schema() {
487
568
  const topic = rest[0] || 'format';
488
569
  const caps = () => JSON.parse(readFileSync(path.join(PKG, 'schema', 'capabilities.json'), 'utf8'));
@@ -589,9 +670,9 @@ async function review() {
589
670
  await new Promise(() => {}); // serves until stopped
590
671
  }
591
672
 
592
- const COMMANDS = { doctor, init, build: () => build(rest[0] || '.'), check, stills, render, pack, set, apply, schema, trace, review };
673
+ const COMMANDS = { doctor, init, script, build: () => build(rest[0] || '.'), check, stills, render, pack, set, apply, schema, trace, review };
593
674
  if (!cmd || cmd === 'help' || flags.help || !COMMANDS[cmd]) {
594
- console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').split('\n').slice(1, 19).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'));
675
+ console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').split('\n').slice(1, 20).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'));
595
676
  process.exit(cmd && !COMMANDS[cmd] && cmd !== 'help' ? 1 : 0);
596
677
  }
597
678
  try {
@@ -15,6 +15,7 @@
15
15
  "frontend/src/components/mockupGenerator/backgroundSelector.css",
16
16
  "frontend/src/components/mockupGenerator/screenTexture.js",
17
17
  "frontend/src/components/model3dFallback.jsx",
18
+ "frontend/src/components/sketch/sketchEngine.js",
18
19
  "frontend/src/components/videoEditor/DirectionPad.jsx",
19
20
  "frontend/src/components/videoEditor/Fields.jsx",
20
21
  "frontend/src/components/videoEditor/KeyframePresetModal.jsx",
@@ -182,6 +183,7 @@
182
183
  "frontend/src/components/videoEditor/model3dPrimitives.js",
183
184
  "frontend/src/components/videoEditor/model3dShadowPlate.js",
184
185
  "frontend/src/components/videoEditor/model3dTonePass.js",
186
+ "frontend/src/components/videoEditor/modelDof.js",
185
187
  "frontend/src/components/videoEditor/motionBlur.js",
186
188
  "frontend/src/components/videoEditor/motionBlurLayer.js",
187
189
  "frontend/src/components/videoEditor/motionBlurLevels.js",
@@ -249,6 +251,7 @@
249
251
  "frontend/src/components/videoEditor/rigContactShadows.js",
250
252
  "frontend/src/components/videoEditor/rigDeform.js",
251
253
  "frontend/src/components/videoEditor/rigDeformNodes.js",
254
+ "frontend/src/components/videoEditor/rigShadowFit.js",
252
255
  "frontend/src/components/videoEditor/rigSoftShadows.js",
253
256
  "frontend/src/components/videoEditor/rotoRegion.js",
254
257
  "frontend/src/components/videoEditor/runningGlow.js",
@@ -295,12 +298,15 @@
295
298
  "frontend/src/components/videoEditor/vectorMedia.js",
296
299
  "frontend/src/components/videoEditor/videoEditorStore.js",
297
300
  "frontend/src/components/videoEditor/videoExporter.js",
301
+ "frontend/src/components/videoEditor/videoPlate.js",
298
302
  "frontend/src/components/videoEditor/videoRenderer.js",
299
303
  "frontend/src/components/videoEditor/videoSizePresets.js",
300
304
  "frontend/src/components/videoEditor/videoStabilization.js",
301
305
  "frontend/src/components/videoEditor/voiceSync.js",
302
306
  "frontend/src/components/videoEditor/warpNodes.js",
303
307
  "frontend/src/components/videoEditor/webcodecsClipDecoder.js",
308
+ "frontend/src/components/videoEditor/webmClipDecoder.js",
309
+ "frontend/src/components/videoEditor/webmDemux.js",
304
310
  "frontend/src/components/videoEditor/webmFrameCache.js",
305
311
  "frontend/src/hooks/appPrefs.js",
306
312
  "frontend/src/hooks/useHoverTip.jsx",
@@ -319,6 +325,21 @@
319
325
  "frontend/src/motionEngine/soundCues.js",
320
326
  "frontend/src/motionEngine/soundLibrary.js",
321
327
  "frontend/src/motionEngine/stubs/motionTemplatesRegistry.js",
328
+ "frontend/src/motionEngine/studio/color.css",
329
+ "frontend/src/motionEngine/studio/color.js",
330
+ "frontend/src/motionEngine/studio/focus.css",
331
+ "frontend/src/motionEngine/studio/gizmo.js",
332
+ "frontend/src/motionEngine/studio/highlight.css",
333
+ "frontend/src/motionEngine/studio/highlight.js",
334
+ "frontend/src/motionEngine/studio/index.js",
335
+ "frontend/src/motionEngine/studio/inspector.js",
336
+ "frontend/src/motionEngine/studio/lab.css",
337
+ "frontend/src/motionEngine/studio/lab.js",
338
+ "frontend/src/motionEngine/studio/navigator.css",
339
+ "frontend/src/motionEngine/studio/notes.css",
340
+ "frontend/src/motionEngine/studio/notes.js",
341
+ "frontend/src/motionEngine/studio/review.css",
342
+ "frontend/src/motionEngine/studio/review.js",
322
343
  "frontend/src/motionEngine/template.js",
323
344
  "frontend/src/motionEngine/trace.js",
324
345
  "frontend/src/motionEngine/useReview.js",