@moood/render 0.0.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
@@ -1,29 +1,29 @@
1
- # @moood/render
2
-
3
- A scene of [moood](https://moood.me) rendered on your machine — with moood's own engine, the version
4
- the site runs (`moood-render --version`), in a Chromium you have already (nothing is downloaded, no
5
- network): a PNG and the errors the scene's code threw.
6
-
7
- npm i -g @moood/render
8
- moood-render <world>/scenes/<folder> --at 0,2.5 --device 16:9 --out frame.png
9
-
10
- - **A scene** is a folder of a world's repository (`scene.json` and its code beside it — see
11
- https://github.com/moood-me/docs), or a spec as a `.json` file. The modules it lists are read from
12
- the world's `modules/` (`--world <the world's repository>`; by default two folders up).
13
- - **`--at`** — moments, in seconds from the scene's start: a frame each (`frame-2.5s.png`). Time is a
14
- virtual clock stepped from the start, a tenth of a second at a time; chance (`Math.random`) is the
15
- same every run. The same moment and state give the same picture — pixel-close, not bit-equal (the
16
- GPU emulation's rounding): compare frames with a small tolerance.
17
- - **`--state '{"dawn": 1}'`** — values over the scene's starting state.
18
- - **`--device`** — `16:9` (1600×900), `4:3`, `21:9` (the same height), `tile` (640×360), `big` (1920×1080).
19
- - **`--bare`** — without the title and captions over it.
20
- - **The browser**: `--chromium <path>`, or `MOOOD_CHROMIUM`, or Playwright's own (`PLAYWRIGHT_BROWSERS_PATH`).
21
-
22
- From code:
23
-
24
- import { render, readScene, readModules } from '@moood/render';
25
- const spec = readScene('world/scenes/kukhnya');
26
- const [frame] = await render({ spec, modules: readModules('world', spec), at: [3] });
27
- // frame.png (a Buffer), frame.errors
28
-
29
- Exits 1 if the scene's code threw.
1
+ # @moood/render
2
+
3
+ A scene of [moood](https://moood.me) rendered on your machine — with moood's own engine, the version
4
+ the site runs (`moood-render --version`), in a Chromium you have already (nothing is downloaded, no
5
+ network): a PNG and the errors the scene's code threw.
6
+
7
+ npm i -g @moood/render
8
+ moood-render <world>/scenes/<folder> --at 0,2.5 --device 16:9 --out frame.png
9
+
10
+ - **A scene** is a folder of a world's repository (`scene.json` and its code beside it — see
11
+ https://github.com/moood-me/docs), or a spec as a `.json` file. The modules it lists are read from
12
+ the world's `modules/` (`--world <the world's repository>`; by default two folders up).
13
+ - **`--at`** — moments, in seconds from the scene's start: a frame each (`frame-2.5s.png`). Time is a
14
+ virtual clock stepped from the start, a tenth of a second at a time; chance (`Math.random`) is the
15
+ same every run. The same moment and state give the same picture — pixel-close, not bit-equal (the
16
+ GPU emulation's rounding): compare frames with a small tolerance.
17
+ - **`--state '{"dawn": 1}'`** — values over the scene's starting state.
18
+ - **`--device`** — `16:9` (1600×900), `4:3`, `21:9` (the same height), `tile` (640×360), `big` (1920×1080).
19
+ - **`--bare`** — without the title and captions over it.
20
+ - **The browser**: `--chromium <path>`, or `MOOOD_CHROMIUM`, or Playwright's own (`PLAYWRIGHT_BROWSERS_PATH`).
21
+
22
+ From code:
23
+
24
+ import { render, readScene, readModules } from '@moood/render';
25
+ const spec = readScene('world/scenes/kukhnya');
26
+ const [frame] = await render({ spec, modules: readModules('world', spec), at: [3] });
27
+ // frame.png (a Buffer), frame.errors
28
+
29
+ Exits 1 if the scene's code threw.
package/cli.js CHANGED
@@ -1,34 +1,34 @@
1
- #!/usr/bin/env node
2
- // moood-render <scene folder | spec.json> [--world <its world's repository>] [--device 16:9] [--at 0,1.5,3]
3
- // [--state '{"dawn":1}'] [--bare] [--out frame.png] [--chromium <path>]
4
- // A frame for each moment (--out frame.png → frame.png, or frame-1.5s.png … for several); the
5
- // errors the scene's code threw, on stderr. The world defaults to the folder two levels up
6
- // (<world>/scenes/<folder>): where its modules are.
7
- import { statSync, writeFileSync, readFileSync } from 'node:fs';
8
- import { dirname, resolve } from 'node:path';
9
- import { parseArgs } from 'node:util';
10
- import { ENGINE, readModules, readScene, render } from './index.js';
11
-
12
- const { values: o, positionals: [what] } = parseArgs({
13
- allowPositionals: true,
14
- options: { world: { type: 'string' }, device: { type: 'string', default: '16:9' }, at: { type: 'string', default: '0' },
15
- state: { type: 'string' }, bare: { type: 'boolean' }, out: { type: 'string', default: 'frame.png' },
16
- chromium: { type: 'string' }, version: { type: 'boolean' } },
17
- });
18
- if (o.version) { console.log(`moood engine ${ENGINE}`); process.exit(0); }
19
- if (!what) { console.error('moood-render <scene folder | spec.json> [--world …] [--device 16:9] [--at 0,1.5] [--state {…}] [--bare] [--out frame.png]'); process.exit(2); }
20
-
21
- const folder = statSync(what).isDirectory();
22
- const spec = folder ? readScene(what) : JSON.parse(readFileSync(what, 'utf8'));
23
- const world = o.world || (folder ? resolve(what, '..', '..') : dirname(resolve(what)));
24
- const at = o.at.split(',').map(Number);
25
- const frames = await render({ spec, modules: readModules(world, spec), device: o.device, at, state: o.state ? JSON.parse(o.state) : {},
26
- bare: o.bare, chromium: o.chromium });
27
- let failed = false;
28
- for (const f of frames) {
29
- const out = frames.length > 1 ? o.out.replace(/(\.png)?$/, `-${f.at}s.png`) : o.out;
30
- writeFileSync(out, f.png);
31
- console.log(out);
32
- for (const e of f.errors) { console.error(`${f.at}s: ${e}`); failed = true; }
33
- }
34
- process.exit(failed ? 1 : 0);
1
+ #!/usr/bin/env node
2
+ // moood-render <scene folder | spec.json> [--world <its world's repository>] [--device 16:9] [--at 0,1.5,3]
3
+ // [--state '{"dawn":1}'] [--bare] [--out frame.png] [--chromium <path>]
4
+ // A frame for each moment (--out frame.png → frame.png, or frame-1.5s.png … for several); the
5
+ // errors the scene's code threw, on stderr. The world defaults to the folder two levels up
6
+ // (<world>/scenes/<folder>): where its modules are.
7
+ import { statSync, writeFileSync, readFileSync } from 'node:fs';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { parseArgs } from 'node:util';
10
+ import { ENGINE, readModules, readScene, render } from './index.js';
11
+
12
+ const { values: o, positionals: [what] } = parseArgs({
13
+ allowPositionals: true,
14
+ options: { world: { type: 'string' }, device: { type: 'string', default: '16:9' }, at: { type: 'string', default: '0' },
15
+ state: { type: 'string' }, bare: { type: 'boolean' }, out: { type: 'string', default: 'frame.png' },
16
+ chromium: { type: 'string' }, version: { type: 'boolean' } },
17
+ });
18
+ if (o.version) { console.log(`moood engine ${ENGINE}`); process.exit(0); }
19
+ if (!what) { console.error('moood-render <scene folder | spec.json> [--world …] [--device 16:9] [--at 0,1.5] [--state {…}] [--bare] [--out frame.png]'); process.exit(2); }
20
+
21
+ const folder = statSync(what).isDirectory();
22
+ const spec = folder ? readScene(what) : JSON.parse(readFileSync(what, 'utf8'));
23
+ const world = o.world || (folder ? resolve(what, '..', '..') : dirname(resolve(what)));
24
+ const at = o.at.split(',').map(Number);
25
+ const frames = await render({ spec, modules: readModules(world, spec), device: o.device, at, state: o.state ? JSON.parse(o.state) : {},
26
+ bare: o.bare, chromium: o.chromium });
27
+ let failed = false;
28
+ for (const f of frames) {
29
+ const out = frames.length > 1 ? o.out.replace(/(\.png)?$/, `-${f.at}s.png`) : o.out;
30
+ writeFileSync(out, f.png);
31
+ console.log(out);
32
+ for (const e of f.errors) { console.error(`${f.at}s: ${e}`); failed = true; }
33
+ }
34
+ process.exit(failed ? 1 : 0);
package/engine/actions.js CHANGED
@@ -1,69 +1,69 @@
1
- // What can be done with the things in a scene: whatever plays on it (a game) offers some of the
2
- // scene's elements, each with what can be done with it. The pointer over one of them is a hand; the
3
- // player picks it — it glows (select.js), its actions open beside it — and picks an action; the
4
- // others don't answer a click.
5
- // const actions = MooodActions.create({ canvas, stage, onAct(element, action) })
6
- // actions.setSpec(spec) — the scene shown (what was offered is gone with the scene before)
7
- // actions.offer({ <element id>: [{ id, label } | label, …], … }) — what is offered now (in place
8
- // of what was); {} — nothing
9
- // actions.clear() — nothing picked
10
- const MooodActions = (() => {
11
- function create({ canvas, stage, onAct }) {
12
- let spec = null, offers = {}, open = null; // open: the element whose actions are shown
13
- const menu = document.createElement('div');
14
- menu.id = 'actions';
15
- menu.hidden = true;
16
- document.body.append(menu);
17
- const idAt = path => {
18
- const m = /^objects\[(\d+)\]$/.exec(path || '');
19
- return m ? spec?.objects?.[Number(m[1])]?.id ?? null : null;
20
- };
21
- const listed = id => (offers[id] || []).map(a => (typeof a === 'string' ? { id: a, label: a } : a));
22
- const selection = MooodSelect.create({ canvas, stage, withCard: false, only: path => listed(idAt(path)).length > 0, onChange: show });
23
- let hovered = 0;
24
- canvas.addEventListener('pointermove', e => {
25
- if (e.pointerType !== 'mouse' || performance.now() - hovered < 120) return;
26
- hovered = performance.now();
27
- canvas.style.cursor = Object.keys(offers).length && selection.at(e.clientX, e.clientY) ? 'pointer' : '';
28
- });
29
-
30
- function show() {
31
- const o = selection.object(), acts = o ? listed(o.id) : [];
32
- open = acts.length ? o : null;
33
- menu.hidden = !open;
34
- if (!open) return;
35
- const title = document.createElement('b');
36
- title.textContent = o.name || '';
37
- menu.replaceChildren(title, ...acts.map(a => {
38
- const b = document.createElement('button');
39
- b.type = 'button';
40
- b.textContent = a.label;
41
- b.onclick = () => { const el = open; selection.clear(); onAct?.(el, a); };
42
- return b;
43
- }));
44
- place();
45
- }
46
- // beside the thing, wherever it is now (the camera moves): right of it, else left, within the screen
47
- function place() {
48
- if (!open) return;
49
- requestAnimationFrame(place);
50
- const b = selection.box();
51
- if (!b) return;
52
- const w = menu.offsetWidth, h = menu.offsetHeight, m = 12;
53
- const x = b.x + b.w + m + w < innerWidth ? b.x + b.w + m : Math.max(m, b.x - m - w);
54
- const y = Math.min(innerHeight - h - m, Math.max(m, b.y + b.h / 2 - h / 2));
55
- menu.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
56
- }
57
-
58
- return {
59
- setSpec(next) { spec = next; offers = {}; selection.setSpec(next); },
60
- offer(next) {
61
- offers = next || {};
62
- const o = selection.object();
63
- if (o && !listed(o.id).length) selection.clear(); else show();
64
- },
65
- clear: () => selection.clear(),
66
- };
67
- }
68
- return { create };
69
- })();
1
+ // What can be done with the things in a scene: whatever plays on it (a game) offers some of the
2
+ // scene's elements, each with what can be done with it. The pointer over one of them is a hand; the
3
+ // player picks it — it glows (select.js), its actions open beside it — and picks an action; the
4
+ // others don't answer a click.
5
+ // const actions = MooodActions.create({ canvas, stage, onAct(element, action) })
6
+ // actions.setSpec(spec) — the scene shown (what was offered is gone with the scene before)
7
+ // actions.offer({ <element id>: [{ id, label } | label, …], … }) — what is offered now (in place
8
+ // of what was); {} — nothing
9
+ // actions.clear() — nothing picked
10
+ const MooodActions = (() => {
11
+ function create({ canvas, stage, onAct }) {
12
+ let spec = null, offers = {}, open = null; // open: the element whose actions are shown
13
+ const menu = document.createElement('div');
14
+ menu.id = 'actions';
15
+ menu.hidden = true;
16
+ document.body.append(menu);
17
+ const idAt = path => {
18
+ const m = /^objects\[(\d+)\]$/.exec(path || '');
19
+ return m ? spec?.objects?.[Number(m[1])]?.id ?? null : null;
20
+ };
21
+ const listed = id => (offers[id] || []).map(a => (typeof a === 'string' ? { id: a, label: a } : a));
22
+ const selection = MooodSelect.create({ canvas, stage, withCard: false, only: path => listed(idAt(path)).length > 0, onChange: show });
23
+ let hovered = 0;
24
+ canvas.addEventListener('pointermove', e => {
25
+ if (e.pointerType !== 'mouse' || performance.now() - hovered < 120) return;
26
+ hovered = performance.now();
27
+ canvas.style.cursor = Object.keys(offers).length && selection.at(e.clientX, e.clientY) ? 'pointer' : '';
28
+ });
29
+
30
+ function show() {
31
+ const o = selection.object(), acts = o ? listed(o.id) : [];
32
+ open = acts.length ? o : null;
33
+ menu.hidden = !open;
34
+ if (!open) return;
35
+ const title = document.createElement('b');
36
+ title.textContent = o.name || '';
37
+ menu.replaceChildren(title, ...acts.map(a => {
38
+ const b = document.createElement('button');
39
+ b.type = 'button';
40
+ b.textContent = a.label;
41
+ b.onclick = () => { const el = open; selection.clear(); onAct?.(el, a); };
42
+ return b;
43
+ }));
44
+ place();
45
+ }
46
+ // beside the thing, wherever it is now (the camera moves): right of it, else left, within the screen
47
+ function place() {
48
+ if (!open) return;
49
+ requestAnimationFrame(place);
50
+ const b = selection.box();
51
+ if (!b) return;
52
+ const w = menu.offsetWidth, h = menu.offsetHeight, m = 12;
53
+ const x = b.x + b.w + m + w < innerWidth ? b.x + b.w + m : Math.max(m, b.x - m - w);
54
+ const y = Math.min(innerHeight - h - m, Math.max(m, b.y + b.h / 2 - h / 2));
55
+ menu.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
56
+ }
57
+
58
+ return {
59
+ setSpec(next) { spec = next; offers = {}; selection.setSpec(next); },
60
+ offer(next) {
61
+ offers = next || {};
62
+ const o = selection.object();
63
+ if (o && !listed(o.id).length) selection.clear(); else show();
64
+ },
65
+ clear: () => selection.clear(),
66
+ };
67
+ }
68
+ return { create };
69
+ })();
@@ -1,20 +1,20 @@
1
- // Timed captions — the director's lines that appear over the scene one after another
2
- // (spec.captions: [{ text, at: seconds, for?: seconds }]). Without `for` a line stays.
3
- const MooodCaptions = (() => {
4
- let box = null, timers = [];
5
- function show(list) {
6
- for (const t of timers) clearTimeout(t);
7
- timers = [];
8
- if (!box) { box = document.createElement('div'); box.id = 'captions'; document.body.append(box); }
9
- box.textContent = '';
10
- for (const c of Array.isArray(list) ? list : []) {
11
- if (!c || typeof c.text !== 'string') continue;
12
- const p = document.createElement('p');
13
- p.textContent = c.text;
14
- box.append(p);
15
- timers.push(setTimeout(() => p.classList.add('on'), Math.max(0, c.at || 0) * 1000));
16
- if (c.for > 0) timers.push(setTimeout(() => p.classList.remove('on'), (Math.max(0, c.at || 0) + c.for) * 1000));
17
- }
18
- }
19
- return { show };
20
- })();
1
+ // Timed captions — the director's lines that appear over the scene one after another
2
+ // (spec.captions: [{ text, at: seconds, for?: seconds }]). Without `for` a line stays.
3
+ const MooodCaptions = (() => {
4
+ let box = null, timers = [];
5
+ function show(list) {
6
+ for (const t of timers) clearTimeout(t);
7
+ timers = [];
8
+ if (!box) { box = document.createElement('div'); box.id = 'captions'; document.body.append(box); }
9
+ box.textContent = '';
10
+ for (const c of Array.isArray(list) ? list : []) {
11
+ if (!c || typeof c.text !== 'string') continue;
12
+ const p = document.createElement('p');
13
+ p.textContent = c.text;
14
+ box.append(p);
15
+ timers.push(setTimeout(() => p.classList.add('on'), Math.max(0, c.at || 0) * 1000));
16
+ if (c.for > 0) timers.push(setTimeout(() => p.classList.remove('on'), (Math.max(0, c.at || 0) + c.for) * 1000));
17
+ }
18
+ }
19
+ return { show };
20
+ })();
@@ -1,44 +1,44 @@
1
- // The ways a scene can change before the eyes (spec.directions): each a state value from 0 (the
2
- // scene as it is) to 1 (fully changed), painted by the scene itself. For now a panel to try them:
3
- // a slider per direction, and ▶ / ◀ to watch it happen there and back. A direction can't move
4
- // while one it excludes (or that excludes it) has moved off 0.
5
- const MooodDirections = (() => {
6
- const SECONDS = 6; // ▶ / ◀: how long the change takes
7
-
8
- function show(spec, panel) {
9
- const all = Array.isArray(spec?.directions) ? spec.directions.filter(d => d && d.key) : [];
10
- panel.replaceChildren();
11
- panel.hidden = !all.length;
12
- if (!all.length) return;
13
- const excluded = d => all.filter(o => o.key !== d.key && ((d.excludes || []).includes(o.key) || (o.excludes || []).includes(d.key)));
14
- const blocked = d => excluded(d).some(o => (MooodState.get(o.key) || 0) > 0.001);
15
- const rows = all.map(d => {
16
- const row = document.createElement('div'), name = document.createElement('span');
17
- const slider = document.createElement('input'), back = document.createElement('button'), go = document.createElement('button');
18
- row.className = 'direction';
19
- name.textContent = d.name || d.key;
20
- Object.assign(slider, { type: 'range', min: 0, max: 1, step: 0.01 });
21
- back.type = go.type = 'button';
22
- back.textContent = '◀'; go.textContent = '▶';
23
- back.title = 'обратно'; go.title = 'превратить';
24
- slider.oninput = () => MooodState.set(d.key, Number(slider.value));
25
- go.onclick = () => MooodState.set(d.key, 1, SECONDS * (1 - (MooodState.get(d.key) || 0)));
26
- back.onclick = () => MooodState.set(d.key, 0, SECONDS * (MooodState.get(d.key) || 0));
27
- row.append(name, back, slider, go);
28
- panel.append(row);
29
- return { d, row, slider, back, go };
30
- });
31
- const sync = () => {
32
- for (const r of rows) {
33
- const off = blocked(r.d);
34
- r.slider.value = MooodState.get(r.d.key) || 0;
35
- r.slider.disabled = r.go.disabled = off;
36
- r.row.classList.toggle('blocked', off);
37
- }
38
- requestAnimationFrame(sync);
39
- };
40
- sync();
41
- }
42
-
43
- return { show };
44
- })();
1
+ // The ways a scene can change before the eyes (spec.directions): each a state value from 0 (the
2
+ // scene as it is) to 1 (fully changed), painted by the scene itself. For now a panel to try them:
3
+ // a slider per direction, and ▶ / ◀ to watch it happen there and back. A direction can't move
4
+ // while one it excludes (or that excludes it) has moved off 0.
5
+ const MooodDirections = (() => {
6
+ const SECONDS = 6; // ▶ / ◀: how long the change takes
7
+
8
+ function show(spec, panel) {
9
+ const all = Array.isArray(spec?.directions) ? spec.directions.filter(d => d && d.key) : [];
10
+ panel.replaceChildren();
11
+ panel.hidden = !all.length;
12
+ if (!all.length) return;
13
+ const excluded = d => all.filter(o => o.key !== d.key && ((d.excludes || []).includes(o.key) || (o.excludes || []).includes(d.key)));
14
+ const blocked = d => excluded(d).some(o => (MooodState.get(o.key) || 0) > 0.001);
15
+ const rows = all.map(d => {
16
+ const row = document.createElement('div'), name = document.createElement('span');
17
+ const slider = document.createElement('input'), back = document.createElement('button'), go = document.createElement('button');
18
+ row.className = 'direction';
19
+ name.textContent = d.name || d.key;
20
+ Object.assign(slider, { type: 'range', min: 0, max: 1, step: 0.01 });
21
+ back.type = go.type = 'button';
22
+ back.textContent = '◀'; go.textContent = '▶';
23
+ back.title = 'обратно'; go.title = 'превратить';
24
+ slider.oninput = () => MooodState.set(d.key, Number(slider.value));
25
+ go.onclick = () => MooodState.set(d.key, 1, SECONDS * (1 - (MooodState.get(d.key) || 0)));
26
+ back.onclick = () => MooodState.set(d.key, 0, SECONDS * (MooodState.get(d.key) || 0));
27
+ row.append(name, back, slider, go);
28
+ panel.append(row);
29
+ return { d, row, slider, back, go };
30
+ });
31
+ const sync = () => {
32
+ for (const r of rows) {
33
+ const off = blocked(r.d);
34
+ r.slider.value = MooodState.get(r.d.key) || 0;
35
+ r.slider.disabled = r.go.disabled = off;
36
+ r.row.classList.toggle('blocked', off);
37
+ }
38
+ requestAnimationFrame(sync);
39
+ };
40
+ sync();
41
+ }
42
+
43
+ return { show };
44
+ })();