greenlight-motion 0.1.0 → 0.1.1
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 +37 -12
- package/THIRD_PARTY_LICENSES.md +2 -1
- package/cli/index.mjs +83 -2
- package/dist/bundle-inputs.json +5 -0
- package/dist/greenlight-motion-script.mjs +16754 -0
- package/dist/greenlight-motion.js +895 -677
- package/dist/greenlight-motion.mjs +40001 -38558
- package/dist/script-inputs.json +34 -0
- package/package.json +2 -1
- package/schema/capabilities.json +1 -1
- package/skill/SKILL.md +26 -11
- package/skill/references/scene-script.md +111 -0
- package/skill/references/video-3d.md +75 -6
- package/skill/references/video-camera.md +1 -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 +39 -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.1 doctor # Node 18+, Chrome or Edge, WebGPU, H.264: what's missing and how to fix it
|
|
21
|
+
npx greenlight-motion@0.1.1 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.1 script my-video # script.json → project.json with the Video Editor's own presets
|
|
25
|
+
npx greenlight-motion@0.1.1 check my-video # what to fix: missing files, template mistakes, blank frames, broken plugins
|
|
26
|
+
npx greenlight-motion@0.1.1 stills my-video # frames + a labelled contact sheet, to look at
|
|
27
|
+
npx greenlight-motion@0.1.1 render my-video --fps 60 --motion-blur
|
|
28
|
+
npx greenlight-motion@0.1.1 pack my-video # everything in ONE html file
|
|
29
|
+
npx greenlight-motion@0.1.1 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,7 +72,7 @@ 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.1/dist/greenlight-motion.js"></script>
|
|
51
76
|
<script>
|
|
52
77
|
GreenLightMotion.mountPage(document.getElementById('app'), {
|
|
53
78
|
project: { /* project.json */ },
|
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';
|
|
@@ -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
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
"frontend/src/components/videoEditor/model3dPrimitives.js",
|
|
183
183
|
"frontend/src/components/videoEditor/model3dShadowPlate.js",
|
|
184
184
|
"frontend/src/components/videoEditor/model3dTonePass.js",
|
|
185
|
+
"frontend/src/components/videoEditor/modelDof.js",
|
|
185
186
|
"frontend/src/components/videoEditor/motionBlur.js",
|
|
186
187
|
"frontend/src/components/videoEditor/motionBlurLayer.js",
|
|
187
188
|
"frontend/src/components/videoEditor/motionBlurLevels.js",
|
|
@@ -249,6 +250,7 @@
|
|
|
249
250
|
"frontend/src/components/videoEditor/rigContactShadows.js",
|
|
250
251
|
"frontend/src/components/videoEditor/rigDeform.js",
|
|
251
252
|
"frontend/src/components/videoEditor/rigDeformNodes.js",
|
|
253
|
+
"frontend/src/components/videoEditor/rigShadowFit.js",
|
|
252
254
|
"frontend/src/components/videoEditor/rigSoftShadows.js",
|
|
253
255
|
"frontend/src/components/videoEditor/rotoRegion.js",
|
|
254
256
|
"frontend/src/components/videoEditor/runningGlow.js",
|
|
@@ -295,12 +297,15 @@
|
|
|
295
297
|
"frontend/src/components/videoEditor/vectorMedia.js",
|
|
296
298
|
"frontend/src/components/videoEditor/videoEditorStore.js",
|
|
297
299
|
"frontend/src/components/videoEditor/videoExporter.js",
|
|
300
|
+
"frontend/src/components/videoEditor/videoPlate.js",
|
|
298
301
|
"frontend/src/components/videoEditor/videoRenderer.js",
|
|
299
302
|
"frontend/src/components/videoEditor/videoSizePresets.js",
|
|
300
303
|
"frontend/src/components/videoEditor/videoStabilization.js",
|
|
301
304
|
"frontend/src/components/videoEditor/voiceSync.js",
|
|
302
305
|
"frontend/src/components/videoEditor/warpNodes.js",
|
|
303
306
|
"frontend/src/components/videoEditor/webcodecsClipDecoder.js",
|
|
307
|
+
"frontend/src/components/videoEditor/webmClipDecoder.js",
|
|
308
|
+
"frontend/src/components/videoEditor/webmDemux.js",
|
|
304
309
|
"frontend/src/components/videoEditor/webmFrameCache.js",
|
|
305
310
|
"frontend/src/hooks/appPrefs.js",
|
|
306
311
|
"frontend/src/hooks/useHoverTip.jsx",
|