paper-motion 0.2.0 → 0.2.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 +2 -2
- package/bin/pamo.mjs +4 -3
- package/lib/customize.mjs +1 -1
- package/lib/live-board.js +1 -1
- package/lib/registry.mjs +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Installed globally (`npm i -g paper-motion`), the same commands run as `paper-mo
|
|
|
31
31
|
|
|
32
32
|
### Resources from paper-motion.app
|
|
33
33
|
|
|
34
|
-
The registry at [paper-motion.app](https://paper-motion.app) holds named moves, backgrounds, palettes, font pairs and recipes. Agents pick them for a brand, propose options as a board link, and install what the user chooses as code the user owns.
|
|
34
|
+
The registry at [paper-motion.app](https://paper-motion.vercel.app) holds named moves, backgrounds, palettes, font pairs and recipes. Agents pick them for a brand, propose options as a board link, and install what the user chooses as code the user owns.
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
npx paper-motion search --type background --mode dark --near "#0a0f1f,#eef1f8,#3d6bff" # find resources by mood, mode, color fit
|
|
@@ -41,7 +41,7 @@ npx paper-motion board --url --title "Backgrounds" --look cobalt,geist \
|
|
|
41
41
|
npx paper-motion list background # every resource of a type
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
`https://paper-motion.app/llms.txt` explains the flow to an agent.
|
|
44
|
+
`https://paper-motion.vercel.app/llms.txt` explains the flow to an agent.
|
|
45
45
|
|
|
46
46
|
## Inside a scene
|
|
47
47
|
|
package/bin/pamo.mjs
CHANGED
|
@@ -579,14 +579,15 @@ async function main() {
|
|
|
579
579
|
|
|
580
580
|
case 'add': {
|
|
581
581
|
const ids = args._.slice(1);
|
|
582
|
-
if (!ids.length || args.help) { console.error('Usage: add <id> [<id>...] [--set key=value ...] [--force] [--name piece] [--registry url|dir]\n --set writes your values into the installed copy (with several ids: --set id:key=value). --registry picks where resources come from (default: the site).'); process.exit(args.help ? 0 : 1); }
|
|
582
|
+
if (!ids.length || args.help) { console.error('Usage: add <id> [<id>...] [--set key=value ...] [--force] [--name piece] [--brand "Name"] [--registry url|dir]\n --set writes your values into the installed copy (with several ids: --set id:key=value). --force replaces values already in the project. --brand names the brand in brand/tokens.json; palettes, fonts and recipes also write brand/motion.json and brand/sound.json defaults when missing. --registry picks where resources come from (default: the site).'); process.exit(args.help ? 0 : 1); }
|
|
583
583
|
const { add } = await import('../lib/registry.mjs');
|
|
584
584
|
const set = process.argv.flatMap((a, i, all) => (a === '--set' ? [all[i + 1]] : a.startsWith('--set=') ? [a.slice(6)] : [])).filter(Boolean);
|
|
585
|
-
const report = await add(ids.filter((x) => !set.includes(x)), { registry: args.registry, force: !!args.force, name: typeof args.name === 'string' ? args.name : undefined, set });
|
|
585
|
+
const report = await add(ids.filter((x) => !set.includes(x)), { registry: args.registry, force: !!args.force, name: typeof args.name === 'string' ? args.name : undefined, brand: typeof args.brand === 'string' ? args.brand : undefined, set });
|
|
586
586
|
for (const r of report) {
|
|
587
587
|
console.log(`✓ ${r.id} (${r.type})${r.wrote.length ? ` → ${[...new Set(r.wrote)].join(', ')}` : ''}`);
|
|
588
588
|
for (const f of r.skipped) console.log(` kept your ${f} (it differs; --force to replace)`);
|
|
589
589
|
for (const c of r.conflicts) console.log(` kept your value at ${c} (--force to replace)`);
|
|
590
|
+
for (const n of r.notes.filter((x) => x.startsWith('the brand'))) console.log(` ${n}`);
|
|
590
591
|
}
|
|
591
592
|
const last = report.filter((r) => ids.includes(r.id) && r.usage);
|
|
592
593
|
if (last.length === 1) console.log(`\nUsage:\n${last[0].usage.split('\n').map((l) => ` ${l}`).join('\n')}`);
|
|
@@ -637,7 +638,7 @@ async function main() {
|
|
|
637
638
|
} else await withServer(demo, (url) => renderVideo({ url: url(`?render&demo=${it.id}`), out: mp4, from: 0, to: 5, sfx: false, crf: 16, motionBlur: Number(args['motion-blur'] ?? 4) }));
|
|
638
639
|
const vf = 'scale=1280:-2';
|
|
639
640
|
const a = spawnSync('ffmpeg', ['-v', 'error', '-y', '-i', mp4, '-vf', vf, '-c:v', 'libvpx-vp9', '-crf', '38', '-b:v', '0', '-row-mt', '1', '-an', path.join(out, `${it.id}.webm`)]);
|
|
640
|
-
const b = spawnSync('ffmpeg', ['-v', 'error', '-y', '-ss', String(POSTER[it.type] ?? 1.5), '-i', mp4, '-frames:v', '1', '-vf', vf, '-q:v', '4', path.join(out, `${it.id}.jpg`)]);
|
|
641
|
+
const b = spawnSync('ffmpeg', ['-v', 'error', '-y', '-ss', String(it.preview?.at ?? POSTER[it.type] ?? 1.5), '-i', mp4, '-frames:v', '1', '-vf', vf, '-q:v', '4', path.join(out, `${it.id}.jpg`)]);
|
|
641
642
|
if (a.status || b.status) throw new Error(`ffmpeg failed for ${it.id}: ${a.stderr}${b.stderr}`);
|
|
642
643
|
}
|
|
643
644
|
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
package/lib/customize.mjs
CHANGED
|
@@ -120,7 +120,7 @@ export function apply(item, values) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** The code tabs, the change count and the agent prompt for a resource at these values. */
|
|
123
|
-
export function code(item, values, { showAll = false, site = 'https://paper-motion.app' } = {}) {
|
|
123
|
+
export function code(item, values, { showAll = false, site = 'https://paper-motion.vercel.app' } = {}) {
|
|
124
124
|
const v = { ...defaults(item), ...values }, diff = changed(item, v), out = apply(item, v);
|
|
125
125
|
const args = setArgs(item, v);
|
|
126
126
|
const agent = `Use the paper-motion resource "${item.id}" (${site}/r/${item.id}.json) in my video. Install it with: npx paper-motion add ${item.id}${args.length ? ` ${args.join(' ')}` : ''}`;
|
package/lib/live-board.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A board: 2–4 labelled options the agent proposes, carried in a paper-motion.app link so the user can see them
|
|
2
2
|
// live in their own look, tune one and choose. Nothing reaches a server: the payload travels in the URL fragment.
|
|
3
|
-
// https://paper-motion.app/b#<base64url(deflate-raw(JSON))>
|
|
3
|
+
// https://paper-motion.vercel.app/b#<base64url(deflate-raw(JSON))>
|
|
4
4
|
// This module runs in Node (`pamo board --url`) and in the browser (the /b page), so it has no Node imports: it
|
|
5
5
|
// picks zlib or CompressionStream at call time. Plain ESM; the site copies it next to customize.mjs.
|
|
6
6
|
|
package/lib/registry.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { apply as applyValues, parseSet } from './customize.mjs';
|
|
|
9
9
|
|
|
10
10
|
const ENGINE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
11
|
const REPO = path.resolve(ENGINE, '..', '..');
|
|
12
|
-
|
|
12
|
+
// until the paper-motion.app domain is connected, the live site is the vercel URL
|
|
13
|
+
export const SITE = (process.env.PAPER_MOTION_SITE ?? 'https://paper-motion.vercel.app').replace(/\/$/, '');
|
|
13
14
|
const TYPES = ['component', 'transition', 'text', 'camera', 'background', 'palette', 'font-pair', 'recipe'];
|
|
14
15
|
const TYPE_TITLES = { component: 'Component transitions', transition: 'Transitions', text: 'Text effects', camera: 'Camera and energy', background: 'Backgrounds', palette: 'Palettes', 'font-pair': 'Font pairs', recipe: 'Recipes' };
|
|
15
16
|
|
|
@@ -121,7 +122,10 @@ export function llmsTxt(items, site, { full = false } = {}) {
|
|
|
121
122
|
4. **Install the choices.** \`npx paper-motion add <id> [--set key=value …]\` in the project root, with exactly the
|
|
122
123
|
\`--set\` arguments from the reply. Moves go to \`moves/\`, backgrounds to \`brand/shader-set/set.json\`, palettes and
|
|
123
124
|
fonts to \`brand/tokens.json\`, recipes to \`pieces/<id>/\`. Their choices also accumulate in a kit at ${site}/kit,
|
|
124
|
-
whose prompt carries the whole \`pamo add\` command.
|
|
125
|
+
whose prompt carries the whole \`pamo add\` command. \`add\` never overwrites a value already in the project: when
|
|
126
|
+
the user re-chooses or re-tunes something you installed before, pass \`--force\`. A recipe also reads
|
|
127
|
+
\`brand/motion.json\` (pace) and \`brand/sound.json\` (cues; without it the piece is silent) and the brand's name
|
|
128
|
+
from \`tokens.json\` \`brand\`: write those from the interview before rendering.
|
|
125
129
|
5. **Make the video.** Build \`scene.html\` (a pure function of time), check stills with \`pamo still\`, render with
|
|
126
130
|
\`pamo render scene.html --motion-blur 5\`. The test shot and the final piece are real renders, not boards.
|
|
127
131
|
|
|
@@ -208,7 +212,7 @@ export function build({ dir = resolveRegistry().dir, out, site = SITE } = {}) {
|
|
|
208
212
|
if (problems.length) { const e = new Error(`registry: ${problems.length} problem(s)\n ${problems.join('\n ')}`); e.problems = problems; throw e; }
|
|
209
213
|
fs.mkdirSync(path.join(out, 'r'), { recursive: true });
|
|
210
214
|
// publish only the preview files that were rendered: a background plays live and may have none
|
|
211
|
-
for (const it of items) if (it.preview) { const have = Object.fromEntries(Object.entries(it.preview).filter(([, p]) => fs.existsSync(path.join(out, p)))); it.preview = Object.
|
|
215
|
+
for (const it of items) if (it.preview) { const have = Object.fromEntries(Object.entries(it.preview).filter(([, p]) => typeof p !== 'string' || fs.existsSync(path.join(out, p)))); it.preview = Object.values(have).some((p) => typeof p === 'string') ? have : undefined; }
|
|
212
216
|
const index = {
|
|
213
217
|
name: 'paper-motion', site, schema: `${site}/schema.json`, count: items.length, types: TYPES.filter((t) => items.some((i) => i.type === t)),
|
|
214
218
|
items: items.map(({ id, type, title, summary, tags, version, preview, related, when, mood, energy, role, colors, mode, motion, voice, duration, formats, params }) => ({ id, type, title, summary, tags, version, url: `${site}/r/${id}.json`, ...Object.fromEntries(Object.entries({ when, mood, energy, role, colors, mode, motion, voice, duration, formats, preview, related, params: params?.filter((p) => !p.hidden).map((p) => p.name) }).filter(([, v]) => v !== undefined)) })),
|
|
@@ -267,8 +271,22 @@ function mergeInto(base, frag, force, at = '', conflicts = []) {
|
|
|
267
271
|
* Installs resources into the project at `cwd`, resolving `requires.items` first. Returns a report per resource.
|
|
268
272
|
* Never overwrites a changed file or a different brand value unless `force`.
|
|
269
273
|
*/
|
|
270
|
-
|
|
274
|
+
// what a recipe reads beside the tokens and the shader set; written once, when the project has none
|
|
275
|
+
const MOTION_DEFAULT = { pace: 'medium', camera: 'still', text: 'rise', color: 'constant', ending: 'wordmark', timing: null, moves: [] };
|
|
276
|
+
const SOUND_DEFAULT = { palette: 'soft', lowpass: 2400, cues: { transition: { type: 'sweep', dur: 0.7, gain: 0.07 }, land: { type: 'thud', gain: 0.09 }, text: { type: 'tick', freq: 900, gain: 0.04 }, bed: null }, keys: { bank: null, style: 'creamy', gain: 0.12 }, music: null, loudness: -14 };
|
|
277
|
+
|
|
278
|
+
export async function add(ids, { cwd = process.cwd(), registry, force = false, name, brand, set = [] } = {}) {
|
|
271
279
|
const reg = resolveRegistry(registry), done = new Map(), report = [];
|
|
280
|
+
const scaffold = (r) => {
|
|
281
|
+
// a brand folder a recipe can run on: its name, a pace and sound cues (silent pieces are the usual surprise)
|
|
282
|
+
const tokensFile = path.join(cwd, 'brand/tokens.json'), tokens = readJson(tokensFile, {});
|
|
283
|
+
if (brand && (force || !tokens.brand)) { tokens.brand = brand; writeJson(tokensFile, tokens); r.wrote.push('brand/tokens.json brand'); }
|
|
284
|
+
for (const [file, def] of [['brand/motion.json', MOTION_DEFAULT], ['brand/sound.json', SOUND_DEFAULT]]) {
|
|
285
|
+
const f = path.join(cwd, file);
|
|
286
|
+
if (!fs.existsSync(f)) { writeJson(f, def); r.wrote.push(`${file} (defaults)`); }
|
|
287
|
+
}
|
|
288
|
+
if (!tokens.brand && !brand) r.notes.push('the brand has no name yet: pamo add --brand "Name" (recipes show it as the wordmark)');
|
|
289
|
+
};
|
|
272
290
|
const lockFile = path.join(cwd, 'paper-motion.json'), lock = readJson(lockFile, { registry: reg.url ?? 'local', items: {} });
|
|
273
291
|
async function one(id) {
|
|
274
292
|
if (done.has(id)) return;
|
|
@@ -309,6 +327,7 @@ export async function add(ids, { cwd = process.cwd(), registry, force = false, n
|
|
|
309
327
|
r.conflicts.push(...mergeInto(tokens, it.install.entry ?? {}, force).map((k) => `brand/tokens.json ${k}`));
|
|
310
328
|
writeJson(f, tokens); r.wrote.push('brand/tokens.json');
|
|
311
329
|
}
|
|
330
|
+
if (kind === 'tokens' || kind === 'scene') scaffold(r);
|
|
312
331
|
lock.items[it.id] = it.version;
|
|
313
332
|
report.push(r);
|
|
314
333
|
}
|