@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/index.js CHANGED
@@ -1,70 +1,70 @@
1
- // A moood scene rendered here, with moood's own engine (engine/: the site's web/scene, its version in
2
- // engine/engine.json) in a Chromium already installed (playwright-core: nothing is downloaded) — no
3
- // network, no site. Time is a virtual clock, stepped from the scene's start: a frame at a moment is
4
- // the same every time, with the same state.
5
- // render({spec, modules, device, at, state, bare, chromium, date}) → [{at, png, errors}]
6
- // spec — the scene (readScene(folder) from a world's repository, or any spec);
7
- // modules — the modules it lists, themselves: {name: {code, data}} (readModules(world, spec));
8
- // device — the frame: 16:9 (1600×900), 4:3, 21:9 (the same height), tile (640×360), big (1920×1080);
9
- // at — the moments, seconds from the scene's start (default [0]): a frame each;
10
- // state — values over the spec's starting state ({"dawn": 1, "girl": "gone"});
11
- // bare — without the title and the captions over it;
12
- // chromium — the browser's executable (else MOOOD_CHROMIUM, else Playwright's own);
13
- // date — what the clock says at the start (default 2026-01-01T12:00:00Z).
14
- import { readFileSync } from 'node:fs';
15
- import { fileURLToPath, pathToFileURL } from 'node:url';
16
- import { chromium as browserType } from 'playwright-core';
17
-
18
- export { readModules, readScene } from './scene.js';
19
-
20
- const PAGE = pathToFileURL(fileURLToPath(new URL('./engine/render.html', import.meta.url))).href;
21
- export const ENGINE = JSON.parse(readFileSync(new URL('./engine/engine.json', import.meta.url), 'utf8')).engine;
22
- export const DEVICES = { '16:9': [1600, 900], '4:3': [1200, 900], '21:9': [2100, 900], tile: [640, 360], big: [1920, 1080] };
23
- const STEP = 100; // ms: timers fire as they come due, not all at the end of one jump
24
-
25
- export async function render({ spec, modules = {}, device = '16:9', at = [0], state = {}, bare = false, chromium, date } = {}) {
26
- const size = DEVICES[device];
27
- if (!size) throw new Error(`device: one of ${Object.keys(DEVICES).join(', ')}`);
28
- if (!spec || typeof spec !== 'object') throw new Error('spec: a scene expected');
29
- const shown = { ...spec, state: { ...(spec.state && typeof spec.state === 'object' ? spec.state : {}), ...state } };
30
- const browser = await browserType.launch({
31
- executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined,
32
- // WebGL on SwiftShader; 2D canvases on the CPU's Skia (SwiftShader takes minutes for many blended fills)
33
- args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--disable-accelerated-2d-canvas',
34
- '--allow-file-access-from-files'],
35
- });
36
- try {
37
- const page = await (await browser.newContext({ viewport: { width: size[0], height: size[1] } })).newPage();
38
- const errors = [];
39
- page.on('pageerror', e => errors.push(String(e.message)));
40
- // the clock stands still: only the frames' moments move it (install alone lets it run as time does)
41
- const start = new Date(date || '2026-01-01T12:00:00Z').getTime();
42
- await page.clock.install({ time: start });
43
- await page.clock.pauseAt(start + 1);
44
- await page.addInitScript(([s, m]) => {
45
- window.__mooodSpec = s; window.__mooodModules = m;
46
- // chance, the same every time (the engine's grain, rain, sparks): the same moment, the same frame
47
- let seed = 1;
48
- Math.random = () => { seed = (seed + 0x6d2b79f5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
49
- }, [shown, modules]);
50
- await page.goto(bare ? `${PAGE}?bare` : PAGE);
51
- // what loads by itself (fonts, the scene's modules) has loaded before the clock moves at all
52
- await page.evaluate(() => document.fonts.ready);
53
- await page.waitForTimeout(1000);
54
- const frames = [];
55
- let now = 0;
56
- for (const t of [...at].map(Number).sort((a, b) => a - b)) {
57
- while (now < t * 1000) {
58
- const step = Math.min(STEP, t * 1000 - now);
59
- await page.clock.runFor(step);
60
- now += step;
61
- }
62
- await page.clock.runFor(0);
63
- const said = await page.evaluate(() => window.__mooodErrors || []);
64
- frames.push({ at: t, png: await page.screenshot({ type: 'png' }), errors: [...new Set([...errors, ...said])].slice(0, 100) });
65
- }
66
- return frames;
67
- } finally {
68
- await browser.close();
69
- }
70
- }
1
+ // A moood scene rendered here, with moood's own engine (engine/: the site's web/scene, its version in
2
+ // engine/engine.json) in a Chromium already installed (playwright-core: nothing is downloaded) — no
3
+ // network, no site. Time is a virtual clock, stepped from the scene's start: a frame at a moment is
4
+ // the same every time, with the same state.
5
+ // render({spec, modules, device, at, state, bare, chromium, date}) → [{at, png, errors}]
6
+ // spec — the scene (readScene(folder) from a world's repository, or any spec);
7
+ // modules — the modules it lists, themselves: {name: {code, data}} (readModules(world, spec));
8
+ // device — the frame: 16:9 (1600×900), 4:3, 21:9 (the same height), tile (640×360), big (1920×1080);
9
+ // at — the moments, seconds from the scene's start (default [0]): a frame each;
10
+ // state — values over the spec's starting state ({"dawn": 1, "girl": "gone"});
11
+ // bare — without the title and the captions over it;
12
+ // chromium — the browser's executable (else MOOOD_CHROMIUM, else Playwright's own);
13
+ // date — what the clock says at the start (default 2026-01-01T12:00:00Z).
14
+ import { readFileSync } from 'node:fs';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import { chromium as browserType } from 'playwright-core';
17
+
18
+ export { readModules, readScene } from './scene.js';
19
+
20
+ const PAGE = pathToFileURL(fileURLToPath(new URL('./engine/render.html', import.meta.url))).href;
21
+ export const ENGINE = JSON.parse(readFileSync(new URL('./engine/engine.json', import.meta.url), 'utf8')).engine;
22
+ export const DEVICES = { '16:9': [1600, 900], '4:3': [1200, 900], '21:9': [2100, 900], tile: [640, 360], big: [1920, 1080] };
23
+ const STEP = 100; // ms: timers fire as they come due, not all at the end of one jump
24
+
25
+ export async function render({ spec, modules = {}, device = '16:9', at = [0], state = {}, bare = false, chromium, date } = {}) {
26
+ const size = DEVICES[device];
27
+ if (!size) throw new Error(`device: one of ${Object.keys(DEVICES).join(', ')}`);
28
+ if (!spec || typeof spec !== 'object') throw new Error('spec: a scene expected');
29
+ const shown = { ...spec, state: { ...(spec.state && typeof spec.state === 'object' ? spec.state : {}), ...state } };
30
+ const browser = await browserType.launch({
31
+ executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined,
32
+ // WebGL on SwiftShader; 2D canvases on the CPU's Skia (SwiftShader takes minutes for many blended fills)
33
+ args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--disable-accelerated-2d-canvas',
34
+ '--allow-file-access-from-files'],
35
+ });
36
+ try {
37
+ const page = await (await browser.newContext({ viewport: { width: size[0], height: size[1] } })).newPage();
38
+ const errors = [];
39
+ page.on('pageerror', e => errors.push(String(e.message)));
40
+ // the clock stands still: only the frames' moments move it (install alone lets it run as time does)
41
+ const start = new Date(date || '2026-01-01T12:00:00Z').getTime();
42
+ await page.clock.install({ time: start });
43
+ await page.clock.pauseAt(start + 1);
44
+ await page.addInitScript(([s, m]) => {
45
+ window.__mooodSpec = s; window.__mooodModules = m;
46
+ // chance, the same every time (the engine's grain, rain, sparks): the same moment, the same frame
47
+ let seed = 1;
48
+ Math.random = () => { seed = (seed + 0x6d2b79f5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
49
+ }, [shown, modules]);
50
+ await page.goto(bare ? `${PAGE}?bare` : PAGE);
51
+ // what loads by itself (fonts, the scene's modules) has loaded before the clock moves at all
52
+ await page.evaluate(() => document.fonts.ready);
53
+ await page.waitForTimeout(1000);
54
+ const frames = [];
55
+ let now = 0;
56
+ for (const t of [...at].map(Number).sort((a, b) => a - b)) {
57
+ while (now < t * 1000) {
58
+ const step = Math.min(STEP, t * 1000 - now);
59
+ await page.clock.runFor(step);
60
+ now += step;
61
+ }
62
+ await page.clock.runFor(0);
63
+ const said = await page.evaluate(() => window.__mooodErrors || []);
64
+ frames.push({ at: t, png: await page.screenshot({ type: 'png' }), errors: [...new Set([...errors, ...said])].slice(0, 100) });
65
+ }
66
+ return frames;
67
+ } finally {
68
+ await browser.close();
69
+ }
70
+ }
package/package.json CHANGED
@@ -1,13 +1,27 @@
1
- {
2
- "name": "@moood/render",
3
- "version": "0.0.0",
4
- "description": "A moood scene rendered locally — the site's own engine, in a browser already installed — at a moment, with a state: a PNG and the errors its code threw.",
5
- "license": "UNLICENSED",
6
- "type": "module",
7
- "main": "index.js",
8
- "bin": { "moood-render": "cli.js" },
9
- "files": ["index.js", "cli.js", "scene.js", "engine/", "README.md"],
10
- "engines": { "node": ">=20" },
11
- "dependencies": { "playwright-core": "^1.56.0" },
12
- "scripts": { "build": "node build.js" }
13
- }
1
+ {
2
+ "name": "@moood/render",
3
+ "version": "0.1.2",
4
+ "description": "A moood scene rendered locally — the site's own engine, in a browser already installed — at a moment, with a state: a PNG and the errors its code threw.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "bin": {
9
+ "moood-render": "cli.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "cli.js",
14
+ "scene.js",
15
+ "engine/",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "dependencies": {
22
+ "playwright-core": "^1.56.0"
23
+ },
24
+ "scripts": {
25
+ "build": "node build.js"
26
+ }
27
+ }
package/scene.js CHANGED
@@ -1,43 +1,43 @@
1
- // A scene as it lies in its world's repository, put together as moood does it (the scenes service's
2
- // layout.py): scenes/<folder>/scene.json and its code beside it — elements/<id>.js (objects[].code),
3
- // elements/<id>.script.js (objects[].script), script.js, lib.js, notes.md, widgets/<id>.html — and
4
- // the modules it lists (modules/<name>.js, .data.json, in the world's repository).
5
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
6
- import { join, relative, sep } from 'node:path';
7
-
8
- const TOP = { script: 'script.js', lib: 'lib.js', notes: 'notes.md' };
9
- const IN = { objects: { code: 'elements/{id}.js', script: 'elements/{id}.script.js' }, widgets: { html: 'widgets/{id}.html' } };
10
-
11
- const walk = dir => readdirSync(dir).flatMap(f => (statSync(join(dir, f)).isDirectory() ? walk(join(dir, f)) : [join(dir, f)]));
12
-
13
- // the spec a scene's folder makes (an error says what is wrong with it)
14
- export function readScene(folder) {
15
- const files = Object.fromEntries(walk(folder).map(p => [relative(folder, p).split(sep).join('/'), readFileSync(p, 'utf8')]));
16
- if (!('scene.json' in files)) throw new Error(`${folder}: no scene.json`);
17
- const spec = JSON.parse(files['scene.json']);
18
- const used = new Set(['scene.json']);
19
- for (const [key, name] of Object.entries(TOP)) if (name in files) { spec[key] = files[name].replace(/\n+$/, ''); used.add(name); }
20
- for (const [key, fields] of Object.entries(IN)) {
21
- for (const item of spec[key] || []) {
22
- if (!item || typeof item !== 'object' || !item.id) continue;
23
- for (const [field, path] of Object.entries(fields)) {
24
- const name = path.replace('{id}', item.id);
25
- if (name in files) { item[field] = files[name].replace(/\n+$/, ''); used.add(name); }
26
- }
27
- }
28
- }
29
- const extra = Object.keys(files).filter(f => !used.has(f));
30
- if (extra.length) throw new Error(`files no element or widget of scene.json names: ${extra.join(', ')}`);
31
- return spec;
32
- }
33
-
34
- // the modules a scene lists, from its world's repository: {name: {code, data}}
35
- export function readModules(world, spec) {
36
- const out = {};
37
- for (const name of [...new Set((spec.modules || []).filter(n => typeof n === 'string'))].sort()) {
38
- const code = join(world, 'modules', `${name}.js`), data = join(world, 'modules', `${name}.data.json`);
39
- if (!existsSync(code)) throw new Error(`the scene lists modules/${name}.js, and there is none`);
40
- out[name] = { code: readFileSync(code, 'utf8'), data: existsSync(data) ? JSON.parse(readFileSync(data, 'utf8')) : null };
41
- }
42
- return out;
43
- }
1
+ // A scene as it lies in its world's repository, put together as moood does it (the scenes service's
2
+ // layout.py): scenes/<folder>/scene.json and its code beside it — elements/<id>.js (objects[].code),
3
+ // elements/<id>.script.js (objects[].script), script.js, lib.js, notes.md, widgets/<id>.html — and
4
+ // the modules it lists (modules/<name>.js, .data.json, in the world's repository).
5
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
6
+ import { join, relative, sep } from 'node:path';
7
+
8
+ const TOP = { script: 'script.js', lib: 'lib.js', notes: 'notes.md' };
9
+ const IN = { objects: { code: 'elements/{id}.js', script: 'elements/{id}.script.js' }, widgets: { html: 'widgets/{id}.html' } };
10
+
11
+ const walk = dir => readdirSync(dir).flatMap(f => (statSync(join(dir, f)).isDirectory() ? walk(join(dir, f)) : [join(dir, f)]));
12
+
13
+ // the spec a scene's folder makes (an error says what is wrong with it)
14
+ export function readScene(folder) {
15
+ const files = Object.fromEntries(walk(folder).map(p => [relative(folder, p).split(sep).join('/'), readFileSync(p, 'utf8')]));
16
+ if (!('scene.json' in files)) throw new Error(`${folder}: no scene.json`);
17
+ const spec = JSON.parse(files['scene.json']);
18
+ const used = new Set(['scene.json']);
19
+ for (const [key, name] of Object.entries(TOP)) if (name in files) { spec[key] = files[name].replace(/\n+$/, ''); used.add(name); }
20
+ for (const [key, fields] of Object.entries(IN)) {
21
+ for (const item of spec[key] || []) {
22
+ if (!item || typeof item !== 'object' || !item.id) continue;
23
+ for (const [field, path] of Object.entries(fields)) {
24
+ const name = path.replace('{id}', item.id);
25
+ if (name in files) { item[field] = files[name].replace(/\n+$/, ''); used.add(name); }
26
+ }
27
+ }
28
+ }
29
+ const extra = Object.keys(files).filter(f => !used.has(f));
30
+ if (extra.length) throw new Error(`files no element or widget of scene.json names: ${extra.join(', ')}`);
31
+ return spec;
32
+ }
33
+
34
+ // the modules a scene lists, from its world's repository: {name: {code, data}}
35
+ export function readModules(world, spec) {
36
+ const out = {};
37
+ for (const name of [...new Set((spec.modules || []).filter(n => typeof n === 'string'))].sort()) {
38
+ const code = join(world, 'modules', `${name}.js`), data = join(world, 'modules', `${name}.data.json`);
39
+ if (!existsSync(code)) throw new Error(`the scene lists modules/${name}.js, and there is none`);
40
+ out[name] = { code: readFileSync(code, 'utf8'), data: existsSync(data) ? JSON.parse(readFileSync(data, 'utf8')) : null };
41
+ }
42
+ return out;
43
+ }