@moood/render 0.1.3 → 0.1.4
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 +19 -1
- package/cli.js +24 -8
- package/engine/engine.json +1 -1
- package/engine/motion.html +47 -0
- package/engine/motion.js +258 -0
- package/engine/render.html +6 -2
- package/engine/soma.bin +0 -0
- package/engine/soma.md +4 -0
- package/index.js +47 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@ network): a PNG and the errors the scene's code threw.
|
|
|
15
15
|
same every run. The same moment and state give the same picture — pixel-close, not bit-equal (the
|
|
16
16
|
GPU emulation's rounding): compare frames with a small tolerance.
|
|
17
17
|
- **`--state '{"dawn": 1}'`** — values over the scene's starting state.
|
|
18
|
+
- **`--set '[[4, "beat", "cover"], [6, "dawn", 1, 3]]'`** — values set as the scene goes, as a game sets
|
|
19
|
+
them: `[second, key, value, over?, ease?]` (the clock stops at each one's second). A film's beat at
|
|
20
|
+
its second; a branch begun mid-beat.
|
|
21
|
+
- **What the scene told** (`scene.tell` — a film's `beat:<name>` as a beat ends) is printed with its
|
|
22
|
+
second: `told at 4.50s: beat:cover`.
|
|
18
23
|
- **`--device`** — `16:9` (1600×900), `4:3`, `21:9` (the same height), `tile` (640×360), `big` (1920×1080).
|
|
19
24
|
- **`--bare`** — without the title and captions over it.
|
|
20
25
|
- **The browser**: `--chromium <path>`, or `MOOOD_CHROMIUM`, or Playwright's own (`PLAYWRIGHT_BROWSERS_PATH`).
|
|
@@ -24,6 +29,19 @@ From code:
|
|
|
24
29
|
import { render, readScene, readModules } from '@moood/render';
|
|
25
30
|
const spec = readScene('world/scenes/kukhnya');
|
|
26
31
|
const [frame] = await render({ spec, modules: readModules('world', spec), at: [3] });
|
|
27
|
-
// frame.png (a Buffer), frame.errors
|
|
32
|
+
// frame.png (a Buffer), frame.errors, frame.tells ([{at, text, about}] so far)
|
|
28
33
|
|
|
29
34
|
Exits 1 if the scene's code threw.
|
|
35
|
+
|
|
36
|
+
## A person's motion
|
|
37
|
+
|
|
38
|
+
A motion made by moood's `make_motion` (NVIDIA Kimodo), as frames on its body — the same picture as the
|
|
39
|
+
MCP's `look_at_motion`. Save `list_motions(key, body=true)` as a file, then:
|
|
40
|
+
|
|
41
|
+
moood-render motion walk.json --at 0,1,2,3 --views front,side --out walk.png
|
|
42
|
+
|
|
43
|
+
- **`--at`** — moments in seconds (by default eight across the motion); **`--views`** — rows: `front`, `side`
|
|
44
|
+
(from the person's left), `back` — each from where the person faces then; **`--kit`** — over the body, the
|
|
45
|
+
skeleton a people module gets from it (the file's `clip`).
|
|
46
|
+
|
|
47
|
+
From code: `renderMotion({motion, at, views, kit})` → `{png, errors}`.
|
package/cli.js
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
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>]
|
|
3
|
+
// [--state '{"dawn":1}'] [--set '[[4,"beat","cover"]]'] [--bare] [--out frame.png] [--chromium <path>]
|
|
4
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
|
|
5
|
+
// errors the scene's code threw, on stderr; what its scripts told (scene.tell), on stdout. The world defaults to the folder two levels up
|
|
6
6
|
// (<world>/scenes/<folder>): where its modules are.
|
|
7
|
+
// moood-render motion <motion.json> [--at 0,1.5,3] [--views front,side] [--kit] [--out motion.png]
|
|
8
|
+
// A person's motion made by a model (list_motions(key, body=true) saved as a file) as frames on its body.
|
|
7
9
|
import { statSync, writeFileSync, readFileSync } from 'node:fs';
|
|
8
10
|
import { dirname, resolve } from 'node:path';
|
|
9
11
|
import { parseArgs } from 'node:util';
|
|
10
|
-
import { ENGINE, readModules, readScene, render } from './index.js';
|
|
12
|
+
import { ENGINE, readModules, readScene, render, renderMotion } from './index.js';
|
|
11
13
|
|
|
12
|
-
const { values: o, positionals: [what] } = parseArgs({
|
|
14
|
+
const { values: o, positionals: [what, file] } = parseArgs({
|
|
13
15
|
allowPositionals: true,
|
|
14
16
|
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
|
+
state: { type: 'string' }, set: { type: 'string' }, bare: { type: 'boolean' }, out: { type: 'string', default: 'frame.png' },
|
|
18
|
+
chromium: { type: 'string' }, version: { type: 'boolean' }, views: { type: 'string', default: 'front,side' },
|
|
19
|
+
kit: { type: 'boolean' } },
|
|
17
20
|
});
|
|
18
21
|
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); }
|
|
22
|
+
if (!what) { console.error('moood-render <scene folder | spec.json> [--world …] [--device 16:9] [--at 0,1.5] [--state {…}] [--set [[s,k,v],…]] [--bare] [--out frame.png]'); process.exit(2); }
|
|
23
|
+
|
|
24
|
+
if (what === 'motion') {
|
|
25
|
+
if (!file) { console.error('moood-render motion <motion.json> [--at 0,1.5] [--views front,side] [--kit] [--out motion.png]'); process.exit(2); }
|
|
26
|
+
const made = JSON.parse(readFileSync(file, 'utf8'));
|
|
27
|
+
const { png, errors } = await renderMotion({ motion: made.body || made, kit: o.kit ? made.clip : null, chromium: o.chromium,
|
|
28
|
+
at: process.argv.includes('--at') ? o.at.split(',').map(Number) : null, views: o.views.split(',') });
|
|
29
|
+
const out = o.out === 'frame.png' ? 'motion.png' : o.out;
|
|
30
|
+
writeFileSync(out, png);
|
|
31
|
+
console.log(out);
|
|
32
|
+
for (const e of errors) console.error(e);
|
|
33
|
+
process.exit(errors.length ? 1 : 0);
|
|
34
|
+
}
|
|
20
35
|
|
|
21
36
|
const folder = statSync(what).isDirectory();
|
|
22
37
|
const spec = folder ? readScene(what) : JSON.parse(readFileSync(what, 'utf8'));
|
|
23
38
|
const world = o.world || (folder ? resolve(what, '..', '..') : dirname(resolve(what)));
|
|
24
39
|
const at = o.at.split(',').map(Number);
|
|
25
40
|
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 });
|
|
41
|
+
set: o.set ? JSON.parse(o.set) : [], bare: o.bare, chromium: o.chromium });
|
|
27
42
|
let failed = false;
|
|
28
43
|
for (const f of frames) {
|
|
29
44
|
const out = frames.length > 1 ? o.out.replace(/(\.png)?$/, `-${f.at}s.png`) : o.out;
|
|
@@ -31,4 +46,5 @@ for (const f of frames) {
|
|
|
31
46
|
console.log(out);
|
|
32
47
|
for (const e of f.errors) { console.error(`${f.at}s: ${e}`); failed = true; }
|
|
33
48
|
}
|
|
49
|
+
for (const t of frames.at(-1)?.tells || []) console.log(`told at ${t.at.toFixed(2)}s: ${t.text}${t.about ? ` (${t.about})` : ''}`);
|
|
34
50
|
process.exit(failed ? 1 : 0);
|
package/engine/engine.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"engine":"
|
|
1
|
+
{"engine":"ff145eec9cf9"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ru">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>moood · motion</title>
|
|
6
|
+
<style>
|
|
7
|
+
html, body { margin: 0; height: 100%; overflow: hidden; background: #151a22; font: 13px system-ui, sans-serif; }
|
|
8
|
+
canvas { position: fixed; inset: 0; width: 100%; height: 100%; }
|
|
9
|
+
.label { position: fixed; padding: 3px 7px; color: #d6dae1; background: rgba(14, 17, 22, 0.6); border-bottom-right-radius: 6px; }
|
|
10
|
+
.label b { color: #e8c98a; font-weight: 600; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<!-- A made motion as frames on its body, for the render (services/render, @moood/render): what an
|
|
15
|
+
assistant looks at. Handed in: window.__mooodMotion — the motion ({n, fps, names, q, p}: the motion
|
|
16
|
+
service's body of it); window.__mooodMotionShow — {at: [seconds…], views: ["front" | "side" |
|
|
17
|
+
"back" | {yaw, pitch, dist}…], kit: its module clip or null (the module's skeleton over the body),
|
|
18
|
+
cell: [w, h]}: a row per view, a column per moment, each frame labelled. The page is as large as
|
|
19
|
+
the sheet; the title turns "ready" when it is drawn, errors in window.__mooodRenderErrors. -->
|
|
20
|
+
<canvas id="sheet"></canvas>
|
|
21
|
+
<script src="motion.js"></script>
|
|
22
|
+
<script>
|
|
23
|
+
window.__mooodRenderErrors = [];
|
|
24
|
+
(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const motion = window.__mooodMotion, show = window.__mooodMotionShow;
|
|
27
|
+
const [w, h] = show.cell, canvas = document.getElementById('sheet');
|
|
28
|
+
const v = MooodMotion.view(canvas, await MooodMotion.body(), motion, show.kit);
|
|
29
|
+
const cells = [];
|
|
30
|
+
show.views.forEach((view, r) => show.at.forEach((s, c) => {
|
|
31
|
+
const t = Math.max(0, Math.min(motion.n - 1, s * motion.fps));
|
|
32
|
+
cells.push([c * w, r * h, w, h, t, typeof view === 'string' ? { facing: view } : view]);
|
|
33
|
+
const label = document.createElement('div');
|
|
34
|
+
label.className = 'label';
|
|
35
|
+
label.style.left = `${c * w}px`; label.style.top = `${r * h}px`;
|
|
36
|
+
label.innerHTML = `<b>${s.toFixed(2)} s</b> · ${typeof view === 'string' ? view : 'view'}`;
|
|
37
|
+
document.body.append(label);
|
|
38
|
+
}));
|
|
39
|
+
v.draw(0, {}, { kit: !!show.kit, cells });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
window.__mooodRenderErrors.push(String(e && e.stack || e));
|
|
42
|
+
}
|
|
43
|
+
document.title = 'ready';
|
|
44
|
+
})();
|
|
45
|
+
</script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
package/engine/motion.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// A person's motion made by a model (NVIDIA Kimodo: the motion service), drawn on a body — what the
|
|
2
|
+
// sandbox plays (the site) and what the render shows an assistant (motion.html: frames of it in a
|
|
3
|
+
// sheet). The body is Kimodo's own (soma.bin beside this file — its SOMA skin, Apache-2.0: soma.md):
|
|
4
|
+
// 18k vertices on 77 joints, 8 weights each, moved by every joint's rotation and place as Kimodo made
|
|
5
|
+
// them ({n, fps, names, q, p}: the motion service's /api/motions/<key>/body), skinned here, drawn
|
|
6
|
+
// with WebGL; the floor in metres, the pelvis's path on it. Over it, if asked, the skeleton a people
|
|
7
|
+
// module gets from the same motion (its data entry, {n, fps, v, p, c, r}: bone directions, the pelvis,
|
|
8
|
+
// foot contacts, the path over the floor — rebuilt the way dev/mocap/feat.py's rebuild does, on a body of usual proportions).
|
|
9
|
+
// Kimodo's axes: y up, metres; at frame 0 the pelvis over the origin, the person facing +z, their left +x.
|
|
10
|
+
// MooodMotion.body() → the body (fetched once)
|
|
11
|
+
// MooodMotion.view(canvas, body, motion, clip?) → {n, fps, draw(t, camera, {kit, cells})}
|
|
12
|
+
// t — the frame (fractional: between two, mixed); camera — {yaw, pitch, dist} around the pelvis
|
|
13
|
+
// at t (yaw 0: from +z), or {facing: 'front' | 'side' | 'back'} — from where the person faces at t;
|
|
14
|
+
// kit — the module's skeleton over the body; cells — [[x, y, w, h, t, camera]…] for several
|
|
15
|
+
// frames on one canvas (a sheet), instead of t and camera.
|
|
16
|
+
(function () {
|
|
17
|
+
const HERE = document.currentScript.src;
|
|
18
|
+
let soma = null;
|
|
19
|
+
|
|
20
|
+
const bytes = b64 => Uint8Array.from(atob(b64), ch => ch.charCodeAt(0));
|
|
21
|
+
|
|
22
|
+
// (a request, not fetch: the local render reads the engine from files, and fetch doesn't)
|
|
23
|
+
const read = url => new Promise((resolve, reject) => {
|
|
24
|
+
const x = new XMLHttpRequest();
|
|
25
|
+
x.open('GET', url); x.responseType = 'arraybuffer';
|
|
26
|
+
x.onload = () => (x.response && (x.status === 200 || x.status === 0) ? resolve(x.response) : reject(new Error(`${url}: ${x.status}`)));
|
|
27
|
+
x.onerror = () => reject(new Error(`${url}: not read`));
|
|
28
|
+
x.send();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function body() {
|
|
32
|
+
return (soma ??= read(new URL('soma.bin', HERE)).then(buf => {
|
|
33
|
+
const [V, F, J] = new Uint32Array(buf, 0, 3);
|
|
34
|
+
let at = 12;
|
|
35
|
+
const take = (Kind, n, size) => { const a = new Kind(buf.slice(at, at + n * size)); at += n * size; return a; };
|
|
36
|
+
return { V, F, J, bind: take(Float32Array, V * 3, 4), faces: take(Uint16Array, F * 3, 2), joint: take(Uint8Array, V * 8, 1),
|
|
37
|
+
weight: take(Uint8Array, V * 8, 1), inverse: take(Float32Array, J * 12, 4) };
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── the module's skeleton (the clip, as feat.py has it) ────────────────
|
|
42
|
+
|
|
43
|
+
const VEC = ['pF', 'pU', 'cF', 'cU', 'hF', 'hU', 'spine', 'neck', 'rUp', 'rFo', 'rHa', 'lUp', 'lFo', 'lHa', 'rTh', 'rSh', 'rFt', 'lTh', 'lSh', 'lFt'];
|
|
44
|
+
const D = { leg: 0.9, thigh: 0.43, shin: 0.43, spine: 0.5, neck: 0.1, head: 0.24, sw: 0.36, hw: 0.18, upper: 0.31, fore: 0.26, foot: 0.15 };
|
|
45
|
+
const BONES = [['pelvis', 'spine'], ['spine', 'chest'], ['chest', 'neck'], ['neck', 'headtop'], ['rhip', 'lhip'],
|
|
46
|
+
...['r', 'l'].flatMap(s => [['chest', `${s}shoulder`], [`${s}shoulder`, `${s}elbow`], [`${s}elbow`, `${s}wrist`], [`${s}wrist`, `${s}hand`],
|
|
47
|
+
[`${s}hip`, `${s}knee`], [`${s}knee`, `${s}ankle`], [`${s}ankle`, `${s}ball`], [`${s}ball`, `${s}toe`]])];
|
|
48
|
+
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
49
|
+
const mul = (v, k) => [v[0] * k, v[1] * k, v[2] * k];
|
|
50
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
51
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
52
|
+
const unit = v => mul(v, 1 / (Math.hypot(...v) || 1));
|
|
53
|
+
// a frame (its columns: right, up, forward) from forward and up; applied to v; one frame in another
|
|
54
|
+
const frame = (F, U) => { F = unit(F); U = unit(add(U, mul(F, -dot(U, F)))); return [cross(U, F), U, F]; };
|
|
55
|
+
const apply = (M, v) => add(add(mul(M[0], v[0]), mul(M[1], v[1])), mul(M[2], v[2]));
|
|
56
|
+
const within = (A, B) => B.map(c => apply(A, c));
|
|
57
|
+
|
|
58
|
+
// the clip's frames as the module's joints, the body going along its path (r) — in Kimodo's axes (the kit's x is
|
|
59
|
+
// the person's right: mirrored back)
|
|
60
|
+
function skeleton(clip) {
|
|
61
|
+
const v = new Int8Array(bytes(clip.v).buffer), p = new Int16Array(bytes(clip.p).buffer), r = clip.r ? new Int16Array(bytes(clip.r).buffer) : null;
|
|
62
|
+
return Array.from({ length: clip.n }, (_, i) => {
|
|
63
|
+
const V = Object.fromEntries(VEC.map((k, j) => [k, [v[i * 60 + j * 3] / 127, v[i * 60 + j * 3 + 1] / 127, v[i * 60 + j * 3 + 2] / 127]]));
|
|
64
|
+
const P = frame(V.pF, V.pU), C = within(P, frame(V.cF, V.cU)), H = within(C, frame(V.hF, V.hU));
|
|
65
|
+
const hip = [(p[i * 3] / 8000 + (r ? r[i * 2] / 100 : 0)) * D.leg, p[i * 3 + 1] / 8000 * D.leg, (p[i * 3 + 2] / 8000 + (r ? r[i * 2 + 1] / 100 : 0)) * D.leg];
|
|
66
|
+
const J = { pelvis: hip, rhip: add(hip, apply(P, [D.hw / 2, 0, 0])), lhip: add(hip, apply(P, [-D.hw / 2, 0, 0])) };
|
|
67
|
+
J.spine = add(hip, mul(apply(P, V.spine), D.spine / 2));
|
|
68
|
+
J.chest = add(hip, mul(apply(P, V.spine), D.spine));
|
|
69
|
+
J.neck = add(J.chest, mul(apply(C, V.neck), D.neck));
|
|
70
|
+
J.headtop = add(J.neck, apply(H, [0, D.head, 0]));
|
|
71
|
+
for (const [s, sg] of [['r', 1], ['l', -1]]) {
|
|
72
|
+
J[`${s}shoulder`] = add(J.chest, apply(C, [sg * D.sw / 2, -0.03, 0]));
|
|
73
|
+
J[`${s}elbow`] = add(J[`${s}shoulder`], mul(apply(C, V[`${s}Up`]), D.upper));
|
|
74
|
+
J[`${s}wrist`] = add(J[`${s}elbow`], mul(apply(C, V[`${s}Fo`]), D.fore));
|
|
75
|
+
J[`${s}hand`] = add(J[`${s}wrist`], mul(apply(C, V[`${s}Ha`]), 0.08));
|
|
76
|
+
J[`${s}knee`] = add(J[`${s}hip`], mul(apply(P, V[`${s}Th`]), D.thigh));
|
|
77
|
+
J[`${s}ankle`] = add(J[`${s}knee`], mul(apply(P, V[`${s}Sh`]), D.shin));
|
|
78
|
+
J[`${s}ball`] = add(J[`${s}ankle`], mul(apply(P, V[`${s}Ft`]), D.foot));
|
|
79
|
+
J[`${s}toe`] = add(J[`${s}ball`], apply(P, [0, 0, 0.05]));
|
|
80
|
+
}
|
|
81
|
+
for (const k in J) J[k] = [-J[k][0], J[k][1], J[k][2]];
|
|
82
|
+
return J;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── WebGL ─────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const MESH_VS = `#version 300 es
|
|
89
|
+
in vec3 at; in vec3 normal; uniform mat4 view; out vec3 n;
|
|
90
|
+
void main() { n = normal; gl_Position = view * vec4(at, 1.0); }`;
|
|
91
|
+
const MESH_FS = `#version 300 es
|
|
92
|
+
precision mediump float; in vec3 n; out vec4 color;
|
|
93
|
+
void main() {
|
|
94
|
+
vec3 m = normalize(n);
|
|
95
|
+
float light = 0.32 + 0.55 * max(dot(m, normalize(vec3(0.4, 0.8, 0.6))), 0.0) + 0.18 * max(dot(m, normalize(vec3(-0.6, 0.2, -0.7))), 0.0);
|
|
96
|
+
color = vec4(vec3(0.84, 0.80, 0.74) * light, 1.0);
|
|
97
|
+
}`;
|
|
98
|
+
const LINE_VS = `#version 300 es
|
|
99
|
+
in vec3 at; uniform mat4 view; void main() { gl_Position = view * vec4(at, 1.0); }`;
|
|
100
|
+
const LINE_FS = `#version 300 es
|
|
101
|
+
precision mediump float; uniform vec4 tint; out vec4 color; void main() { color = tint; }`;
|
|
102
|
+
|
|
103
|
+
function program(gl, vs, fs) {
|
|
104
|
+
const p = gl.createProgram();
|
|
105
|
+
for (const [kind, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
|
|
106
|
+
const s = gl.createShader(kind);
|
|
107
|
+
gl.shaderSource(s, src); gl.compileShader(s);
|
|
108
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
|
|
109
|
+
gl.attachShader(p, s);
|
|
110
|
+
}
|
|
111
|
+
gl.linkProgram(p);
|
|
112
|
+
return p;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// perspective × look-at, column-major
|
|
116
|
+
function viewOf(eye, target, aspect) {
|
|
117
|
+
const f = unit([eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]]);
|
|
118
|
+
const r = unit(cross([0, 1, 0], f)), u = cross(f, r);
|
|
119
|
+
const V = [r[0], u[0], f[0], 0, r[1], u[1], f[1], 0, r[2], u[2], f[2], 0, -dot(r, eye), -dot(u, eye), -dot(f, eye), 1];
|
|
120
|
+
const t = 1 / Math.tan(0.4), n = 0.05, far = 100;
|
|
121
|
+
const P = [t / aspect, 0, 0, 0, 0, t, 0, 0, 0, 0, (far + n) / (n - far), -1, 0, 0, 2 * far * n / (n - far), 0];
|
|
122
|
+
const M = new Float32Array(16);
|
|
123
|
+
for (let c = 0; c < 4; c++) for (let r2 = 0; r2 < 4; r2++) {
|
|
124
|
+
let s = 0;
|
|
125
|
+
for (let k = 0; k < 4; k++) s += P[k * 4 + r2] * V[c * 4 + k];
|
|
126
|
+
M[c * 4 + r2] = s;
|
|
127
|
+
}
|
|
128
|
+
return M;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function view(canvas, B, motion, clip) {
|
|
132
|
+
const gl = canvas.getContext('webgl2', { antialias: true, preserveDrawingBuffer: true });
|
|
133
|
+
const q = new Int16Array(bytes(motion.q).buffer), pos = new Int16Array(bytes(motion.p).buffer), J = motion.names.length, n = motion.n;
|
|
134
|
+
const kit = clip ? skeleton(clip) : null;
|
|
135
|
+
const mesh = program(gl, MESH_VS, MESH_FS), lines = program(gl, LINE_VS, LINE_FS);
|
|
136
|
+
const skinned = new Float32Array(B.V * 3), normals = new Float32Array(B.V * 3), M = new Float32Array(J * 12);
|
|
137
|
+
const vao = gl.createVertexArray();
|
|
138
|
+
gl.bindVertexArray(vao);
|
|
139
|
+
const atBuf = gl.createBuffer(), nBuf = gl.createBuffer(), iBuf = gl.createBuffer();
|
|
140
|
+
for (const [buf, name] of [[atBuf, 'at'], [nBuf, 'normal']]) {
|
|
141
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, skinned.byteLength, gl.DYNAMIC_DRAW);
|
|
142
|
+
const loc = gl.getAttribLocation(mesh, name); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 3, gl.FLOAT, false, 0, 0);
|
|
143
|
+
}
|
|
144
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuf); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, B.faces, gl.STATIC_DRAW);
|
|
145
|
+
gl.bindVertexArray(null);
|
|
146
|
+
const lineBuf = gl.createBuffer(), lineAt = gl.getAttribLocation(lines, 'at');
|
|
147
|
+
const drawLines = (points, tint, V, mode = gl.LINES) => {
|
|
148
|
+
gl.useProgram(lines);
|
|
149
|
+
gl.uniformMatrix4fv(gl.getUniformLocation(lines, 'view'), false, V);
|
|
150
|
+
gl.uniform4fv(gl.getUniformLocation(lines, 'tint'), tint);
|
|
151
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, lineBuf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STREAM_DRAW);
|
|
152
|
+
gl.enableVertexAttribArray(lineAt); gl.vertexAttribPointer(lineAt, 3, gl.FLOAT, false, 0, 0);
|
|
153
|
+
gl.drawArrays(mode, 0, points.length / 3);
|
|
154
|
+
};
|
|
155
|
+
const joint = (f, j) => [0, 1, 2].map(k => pos[(f * J + j) * 3 + k] / 1000);
|
|
156
|
+
|
|
157
|
+
// the joints' matrices at frame t (between two frames: positions mixed, rotations mixed and renormalised),
|
|
158
|
+
// each times its inverse bind
|
|
159
|
+
const pose = t => {
|
|
160
|
+
const a = Math.floor(t) % n, b = Math.min(n - 1, a + 1), u = t - Math.floor(t);
|
|
161
|
+
for (let j = 0; j < J; j++) {
|
|
162
|
+
const ia = (a * J + j) * 4, ib = (b * J + j) * 4;
|
|
163
|
+
const sign = q[ia] * q[ib] + q[ia + 1] * q[ib + 1] + q[ia + 2] * q[ib + 2] + q[ia + 3] * q[ib + 3] < 0 ? -1 : 1;
|
|
164
|
+
let x = q[ia] * (1 - u) + sign * q[ib] * u, y = q[ia + 1] * (1 - u) + sign * q[ib + 1] * u,
|
|
165
|
+
z = q[ia + 2] * (1 - u) + sign * q[ib + 2] * u, w = q[ia + 3] * (1 - u) + sign * q[ib + 3] * u;
|
|
166
|
+
const l = Math.hypot(x, y, z, w); x /= l; y /= l; z /= l; w /= l;
|
|
167
|
+
const R = [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w),
|
|
168
|
+
2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w),
|
|
169
|
+
2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)];
|
|
170
|
+
const pa = (a * J + j) * 3, pb = (b * J + j) * 3;
|
|
171
|
+
const T = [0, 1, 2].map(k => (pos[pa + k] * (1 - u) + pos[pb + k] * u) / 1000);
|
|
172
|
+
const I = B.inverse.subarray(j * 12, j * 12 + 12);
|
|
173
|
+
for (let r = 0; r < 3; r++) {
|
|
174
|
+
for (let c = 0; c < 4; c++) {
|
|
175
|
+
M[j * 12 + r * 4 + c] = R[r * 3] * I[c] + R[r * 3 + 1] * I[4 + c] + R[r * 3 + 2] * I[8 + c] + (c === 3 ? T[r] : 0);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const skin = () => {
|
|
181
|
+
const { bind, joint: jn, weight } = B;
|
|
182
|
+
for (let i = 0; i < B.V; i++) {
|
|
183
|
+
const x = bind[i * 3], y = bind[i * 3 + 1], z = bind[i * 3 + 2];
|
|
184
|
+
let sx = 0, sy = 0, sz = 0;
|
|
185
|
+
for (let k = 0; k < 8; k++) {
|
|
186
|
+
const w = weight[i * 8 + k];
|
|
187
|
+
if (!w) continue;
|
|
188
|
+
const o = jn[i * 8 + k] * 12, f = w / 255;
|
|
189
|
+
sx += f * (M[o] * x + M[o + 1] * y + M[o + 2] * z + M[o + 3]);
|
|
190
|
+
sy += f * (M[o + 4] * x + M[o + 5] * y + M[o + 6] * z + M[o + 7]);
|
|
191
|
+
sz += f * (M[o + 8] * x + M[o + 9] * y + M[o + 10] * z + M[o + 11]);
|
|
192
|
+
}
|
|
193
|
+
skinned[i * 3] = sx; skinned[i * 3 + 1] = sy; skinned[i * 3 + 2] = sz;
|
|
194
|
+
}
|
|
195
|
+
normals.fill(0);
|
|
196
|
+
const fc = B.faces;
|
|
197
|
+
for (let f = 0; f < B.F * 3; f += 3) {
|
|
198
|
+
const a = fc[f] * 3, b = fc[f + 1] * 3, c = fc[f + 2] * 3;
|
|
199
|
+
const ux = skinned[b] - skinned[a], uy = skinned[b + 1] - skinned[a + 1], uz = skinned[b + 2] - skinned[a + 2];
|
|
200
|
+
const vx = skinned[c] - skinned[a], vy = skinned[c + 1] - skinned[a + 1], vz = skinned[c + 2] - skinned[a + 2];
|
|
201
|
+
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
202
|
+
for (const v of [a, b, c]) { normals[v] += nx; normals[v + 1] += ny; normals[v + 2] += nz; }
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// the pelvis's path, and the floor around all of it
|
|
207
|
+
const hips = Array.from({ length: n }, (_, i) => joint(i, 0));
|
|
208
|
+
const mid = [hips.reduce((s, h) => s + h[0], 0) / n, hips.reduce((s, h) => s + h[2], 0) / n];
|
|
209
|
+
const reach = Math.max(...hips.map(h => Math.hypot(h[0] - mid[0], h[2] - mid[1])));
|
|
210
|
+
const R = Math.ceil(reach) + 5, x0 = Math.round(mid[0]), z0 = Math.round(mid[1]), grid = [];
|
|
211
|
+
for (let d = -R; d <= R; d++) grid.push(x0 + d, 0, z0 - R, x0 + d, 0, z0 + R, x0 - R, 0, z0 + d, x0 + R, 0, z0 + d);
|
|
212
|
+
const path = hips.flatMap(h => [h[0], 0.002, h[2]]);
|
|
213
|
+
// which way the person faces at frame f, as the camera's yaw from the front: the line from the right
|
|
214
|
+
// hip to the left one (+x while facing +z), turned a quarter
|
|
215
|
+
const L = motion.names.indexOf('LeftLeg'), Rl = motion.names.indexOf('RightLeg');
|
|
216
|
+
const facing = f => { const l = joint(f, L), r = joint(f, Rl); return Math.atan2(-(l[2] - r[2]), l[0] - r[0]); };
|
|
217
|
+
|
|
218
|
+
const cell = (x, y, w, h, t, camera, kitOn) => {
|
|
219
|
+
gl.viewport(x, y, w, h); gl.scissor(x, y, w, h);
|
|
220
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.enable(gl.DEPTH_TEST);
|
|
221
|
+
const f = Math.min(n - 1, Math.floor(t)), at = [hips[f][0], 0.85, hips[f][2]];
|
|
222
|
+
let { yaw = 0.6, pitch = 0.22, dist = 3.6 } = camera;
|
|
223
|
+
if (camera.facing) yaw = facing(f) + { front: 0, side: Math.PI / 2, back: Math.PI }[camera.facing];
|
|
224
|
+
const eye = [at[0] + dist * Math.cos(pitch) * Math.sin(yaw), at[1] + dist * Math.sin(pitch), at[2] + dist * Math.cos(pitch) * Math.cos(yaw)];
|
|
225
|
+
const V = viewOf(eye, at, w / h);
|
|
226
|
+
drawLines(grid, [0.2, 0.24, 0.3, 1], V);
|
|
227
|
+
drawLines(path, [0.5, 0.53, 0.58, 1], V, gl.LINE_STRIP);
|
|
228
|
+
pose(t); skin();
|
|
229
|
+
gl.useProgram(mesh);
|
|
230
|
+
gl.uniformMatrix4fv(gl.getUniformLocation(mesh, 'view'), false, V);
|
|
231
|
+
gl.bindVertexArray(vao);
|
|
232
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, atBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, skinned);
|
|
233
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, nBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, normals);
|
|
234
|
+
gl.drawElements(gl.TRIANGLES, B.F * 3, gl.UNSIGNED_SHORT, 0);
|
|
235
|
+
gl.bindVertexArray(null);
|
|
236
|
+
if (kitOn && kit) {
|
|
237
|
+
// (its clip may be a frame or two shorter or longer: the same moment)
|
|
238
|
+
const S = kit[Math.min(kit.length - 1, Math.round(t * kit.length / n))];
|
|
239
|
+
gl.disable(gl.DEPTH_TEST);
|
|
240
|
+
drawLines(BONES.flatMap(([a, b]) => [...S[a], ...S[b]]), [0.91, 0.79, 0.54, 1], V);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
n, fps: motion.fps,
|
|
245
|
+
draw(t, camera = {}, { kit: kitOn = false, cells = null } = {}) {
|
|
246
|
+
const k = devicePixelRatio || 1, w = canvas.clientWidth, h = canvas.clientHeight;
|
|
247
|
+
if (canvas.width !== Math.round(w * k) || canvas.height !== Math.round(h * k)) { canvas.width = Math.round(w * k); canvas.height = Math.round(h * k); }
|
|
248
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
249
|
+
gl.clearColor(0.082, 0.102, 0.133, 1);
|
|
250
|
+
if (!cells) return cell(0, 0, canvas.width, canvas.height, t, camera, kitOn);
|
|
251
|
+
// cells in CSS pixels from the top left; WebGL's from the bottom left
|
|
252
|
+
for (const [x, y, cw, ch, ct, cc] of cells) cell(Math.round(x * k), Math.round(canvas.height - (y + ch) * k), Math.round(cw * k), Math.round(ch * k), ct, cc, kitOn);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
window.MooodMotion = { body, view };
|
|
258
|
+
})();
|
package/engine/render.html
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
screenshots for the artist's look at its own work, the pictures of scenes and assistants (MCP).
|
|
13
13
|
The service hands the
|
|
14
14
|
spec in as window.__mooodSpec; errors the scene's code threw end up in
|
|
15
|
-
window.__mooodRenderErrors
|
|
15
|
+
window.__mooodRenderErrors, what its scripts told (scene.tell) in window.__mooodTells ({at:
|
|
16
|
+
seconds since it started, text, about}); the title turns "ready" when it is time to take the
|
|
17
|
+
picture. -->
|
|
16
18
|
<canvas id="scene"></canvas>
|
|
17
19
|
<header class="titles"><h1 id="title"></h1><p id="caption"></p></header>
|
|
18
20
|
<script src="kit.js"></script>
|
|
@@ -34,8 +36,10 @@
|
|
|
34
36
|
}
|
|
35
37
|
const modules = { texts: window.__mooodModules || {} }; // the modules the scene lists, given (kit.js)
|
|
36
38
|
const canvas = document.getElementById('scene'), stage = Moood.mount(canvas);
|
|
39
|
+
// the starting state, scripts running (their errors are reported) — before the scene is shown, so its first frame is as they set it
|
|
40
|
+
const t0 = performance.now(), told = window.__mooodTells = [];
|
|
41
|
+
MooodState.start(spec, {}, { stage, modules, tell: (text, about) => told.push({ at: (performance.now() - t0) / 1000, text, about }) });
|
|
37
42
|
stage.show(spec, modules);
|
|
38
|
-
MooodState.start(spec, {}, { stage, modules }); // the starting state, scripts running (their errors are reported)
|
|
39
43
|
if (!bare) MooodWidgets.create({ canvas, stage }).show({ ...spec, widgets: (spec.widgets || []).map(w => ({ ...w, at: 0, for: 0 })) });
|
|
40
44
|
// show every caption at once, so the reviewer reads them all
|
|
41
45
|
if (!bare) MooodCaptions.show((spec.captions || []).map(c => ({ ...c, at: 0, for: 0 })));
|
package/engine/soma.bin
ADDED
|
Binary file
|
package/engine/soma.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
`soma.bin` — the SOMA body: NVIDIA Kimodo's skin of its 77-joint skeleton
|
|
2
|
+
(https://github.com/nv-tlabs/kimodo, `kimodo/assets/skeletons/somaskel77/skin_standard.npz`; Copyright (c)
|
|
3
|
+
2026 NVIDIA CORPORATION & AFFILIATES, Apache License 2.0). Made by `dev/mocap/soma_body.py`; `motion.js`
|
|
4
|
+
draws the motions Kimodo made on it (the sandbox, the render).
|
package/index.js
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
// engine/engine.json) in a Chromium already installed (playwright-core: nothing is downloaded) — no
|
|
3
3
|
// network, no site. Time is a virtual clock, stepped from the scene's start: a frame at a moment is
|
|
4
4
|
// the same every time, with the same state.
|
|
5
|
-
// render({spec, modules, device, at, state, bare, chromium, date}) → [{at, png, errors}]
|
|
5
|
+
// render({spec, modules, device, at, state, set, bare, chromium, date}) → [{at, png, errors, tells}]
|
|
6
6
|
// spec — the scene (readScene(folder) from a world's repository, or any spec);
|
|
7
7
|
// modules — the modules it lists, themselves: {name: {code, data}} (readModules(world, spec));
|
|
8
8
|
// device — the frame: 16:9 (1600×900), 4:3, 21:9 (the same height), tile (640×360), big (1920×1080);
|
|
9
9
|
// at — the moments, seconds from the scene's start (default [0]): a frame each;
|
|
10
10
|
// state — values over the spec's starting state ({"dawn": 1, "girl": "gone"});
|
|
11
|
+
// set — values set as the scene goes, as a game sets them: [[second, key, value, over?, ease?], …]
|
|
12
|
+
// (a film's beat at its second: [[4, "beat", "cover"]]);
|
|
11
13
|
// bare — without the title and the captions over it;
|
|
12
14
|
// chromium — the browser's executable (else MOOOD_CHROMIUM, else Playwright's own);
|
|
13
15
|
// date — what the clock says at the start (default 2026-01-01T12:00:00Z).
|
|
16
|
+
// Each frame: its errors, and what the scene's scripts told so far (scene.tell): [{at, text, about}].
|
|
17
|
+
// renderMotion({motion, at, views, kit, cell, chromium}) → {png, errors}
|
|
18
|
+
// a person's motion made by a model (moood's make_motion: list_motions(key, body=true) — its `body` is
|
|
19
|
+
// the motion, its `clip` the people module's entry) as frames on its body (engine/motion.html): a row
|
|
20
|
+
// per view (front, side, back, or {yaw, pitch, dist}), a column per moment (at, seconds; default eight
|
|
21
|
+
// across it); kit — the module's clip, its skeleton drawn over the body; cell — [w, h] of a frame.
|
|
14
22
|
import { readFileSync } from 'node:fs';
|
|
15
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
24
|
import { chromium as browserType } from 'playwright-core';
|
|
@@ -18,20 +26,21 @@ import { chromium as browserType } from 'playwright-core';
|
|
|
18
26
|
export { readModules, readScene } from './scene.js';
|
|
19
27
|
|
|
20
28
|
const PAGE = pathToFileURL(fileURLToPath(new URL('./engine/render.html', import.meta.url))).href;
|
|
29
|
+
const MOTION = pathToFileURL(fileURLToPath(new URL('./engine/motion.html', import.meta.url))).href;
|
|
21
30
|
export const ENGINE = JSON.parse(readFileSync(new URL('./engine/engine.json', import.meta.url), 'utf8')).engine;
|
|
22
31
|
export const DEVICES = { '16:9': [1600, 900], '4:3': [1200, 900], '21:9': [2100, 900], tile: [640, 360], big: [1920, 1080] };
|
|
23
32
|
const STEP = 100; // ms: timers fire as they come due, not all at the end of one jump
|
|
33
|
+
// WebGL on SwiftShader; 2D canvases on the CPU's Skia (SwiftShader takes minutes for many blended fills)
|
|
34
|
+
const ARGS = ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--disable-accelerated-2d-canvas',
|
|
35
|
+
'--allow-file-access-from-files'];
|
|
24
36
|
|
|
25
|
-
export async function render({ spec, modules = {}, device = '16:9', at = [0], state = {}, bare = false, chromium, date } = {}) {
|
|
37
|
+
export async function render({ spec, modules = {}, device = '16:9', at = [0], state = {}, set = [], bare = false, chromium, date } = {}) {
|
|
26
38
|
const size = DEVICES[device];
|
|
27
39
|
if (!size) throw new Error(`device: one of ${Object.keys(DEVICES).join(', ')}`);
|
|
28
40
|
if (!spec || typeof spec !== 'object') throw new Error('spec: a scene expected');
|
|
29
41
|
const shown = { ...spec, state: { ...(spec.state && typeof spec.state === 'object' ? spec.state : {}), ...state } };
|
|
30
42
|
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'],
|
|
43
|
+
executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined, args: ARGS,
|
|
35
44
|
});
|
|
36
45
|
try {
|
|
37
46
|
const page = await (await browser.newContext({ viewport: { width: size[0], height: size[1] } })).newPage();
|
|
@@ -51,20 +60,48 @@ export async function render({ spec, modules = {}, device = '16:9', at = [0], st
|
|
|
51
60
|
// what loads by itself (fonts, the scene's modules) has loaded before the clock moves at all
|
|
52
61
|
await page.evaluate(() => document.fonts.ready);
|
|
53
62
|
await page.waitForTimeout(1000);
|
|
54
|
-
const frames = [];
|
|
63
|
+
const frames = [], sets = [...set].map(([s, ...rest]) => [Number(s) * 1000, ...rest]).sort((a, b) => a[0] - b[0]);
|
|
55
64
|
let now = 0;
|
|
65
|
+
// the values due by now, set (the clock stops at each one's second: it is set on time)
|
|
66
|
+
const due = async () => {
|
|
67
|
+
while (sets.length && sets[0][0] <= now) {
|
|
68
|
+
const [, key, value, over, ease] = sets.shift();
|
|
69
|
+
await page.evaluate(([k, v, o, e]) => MooodState.set(k, v, o, e), [key, value, over, ease]);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
await due();
|
|
56
73
|
for (const t of [...at].map(Number).sort((a, b) => a - b)) {
|
|
57
74
|
while (now < t * 1000) {
|
|
58
|
-
const step = Math.min(STEP, t * 1000 - now);
|
|
75
|
+
const step = Math.min(STEP, t * 1000 - now, sets.length ? sets[0][0] - now : STEP); // (what is due by now is set)
|
|
59
76
|
await page.clock.runFor(step);
|
|
60
77
|
now += step;
|
|
78
|
+
await due();
|
|
61
79
|
}
|
|
62
80
|
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) });
|
|
81
|
+
const [said, tells] = await page.evaluate(() => [window.__mooodErrors || [], window.__mooodTells || []]);
|
|
82
|
+
frames.push({ at: t, png: await page.screenshot({ type: 'png' }), errors: [...new Set([...errors, ...said])].slice(0, 100), tells });
|
|
65
83
|
}
|
|
66
84
|
return frames;
|
|
67
85
|
} finally {
|
|
68
86
|
await browser.close();
|
|
69
87
|
}
|
|
70
88
|
}
|
|
89
|
+
|
|
90
|
+
export async function renderMotion({ motion, at, views = ['front', 'side'], kit = null, cell = [320, 400], chromium } = {}) {
|
|
91
|
+
if (!motion || !Array.isArray(motion.names)) throw new Error('motion: the body of a made motion expected (list_motions(key, body=true).body)');
|
|
92
|
+
const length = (motion.n - 1) / motion.fps;
|
|
93
|
+
const moments = at && at.length ? at.map(s => Math.min(Math.max(0, Number(s)), length)) : Array.from({ length: 8 }, (_, i) => +(length * i / 7).toFixed(2));
|
|
94
|
+
const browser = await browserType.launch({ executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined, args: ARGS });
|
|
95
|
+
try {
|
|
96
|
+
const page = await (await browser.newContext({ viewport: { width: cell[0] * moments.length, height: cell[1] * views.length } })).newPage();
|
|
97
|
+
const errors = [];
|
|
98
|
+
page.on('pageerror', e => errors.push(String(e.message)));
|
|
99
|
+
await page.addInitScript(([m, s]) => { window.__mooodMotion = m; window.__mooodMotionShow = s; }, [motion, { at: moments, views, kit, cell }]);
|
|
100
|
+
await page.goto(MOTION);
|
|
101
|
+
await page.waitForFunction(() => document.title === 'ready', null, { timeout: 120_000 });
|
|
102
|
+
errors.push(...await page.evaluate(() => window.__mooodRenderErrors || []));
|
|
103
|
+
return { png: await page.screenshot({ type: 'png' }), errors: [...new Set(errors)] };
|
|
104
|
+
} finally {
|
|
105
|
+
await browser.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moood/render",
|
|
3
|
-
"version": "0.1.
|
|
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.",
|
|
3
|
+
"version": "0.1.4",
|
|
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; and a person's motion made by a model, as frames on its body.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|