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 +44 -14
- package/THIRD_PARTY_LICENSES.md +2 -1
- package/cli/index.mjs +84 -3
- package/dist/bundle-inputs.json +21 -0
- package/dist/greenlight-motion-script.mjs +16749 -0
- package/dist/greenlight-motion.js +957 -681
- package/dist/greenlight-motion.mjs +59078 -55093
- package/dist/script-inputs.json +35 -0
- package/package.json +2 -1
- package/schema/capabilities.json +1 -1
- package/skill/SKILL.md +29 -21
- package/skill/references/scene-script.md +104 -0
- package/skill/references/video-3d.md +75 -6
- package/skill/references/video-camera.md +1 -0
- package/skill/references/video-design.md +65 -0
- package/skill/references/video-effects.md +6 -6
- package/skill/references/video-keyframes.md +1 -1
- package/skill/references/video-particles.md +5 -5
- package/skill/references/video-projects.md +42 -5
- package/skill/references/video-text-presets.md +47 -7
- package/skill/references/video-text.md +6 -4
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.
|
|
21
|
-
npx greenlight-motion@0.1.
|
|
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
|
-
|
|
24
|
-
npx greenlight-motion@0.1.
|
|
25
|
-
npx greenlight-motion@0.1.
|
|
26
|
-
npx greenlight-motion@0.1.
|
|
27
|
-
npx greenlight-motion@0.1.
|
|
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/`.
|
|
40
|
+
are in `skill/references/`.
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
75
|
+
<script src="https://cdn.jsdelivr.net/npm/greenlight-motion@0.1.2/dist/greenlight-motion.js"></script>
|
|
51
76
|
<script>
|
|
52
|
-
GreenLightMotion.
|
|
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
|
package/THIRD_PARTY_LICENSES.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Third-party licences
|
|
2
2
|
|
|
3
|
-
The engine bundle (`dist/greenlight-motion.js`, `dist/greenlight-motion.mjs`)
|
|
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.
|
|
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,
|
|
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 {
|
package/dist/bundle-inputs.json
CHANGED
|
@@ -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",
|