@moood/render 0.1.3 → 0.1.5
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 +82 -9
- package/cli.js +294 -28
- package/compose.js +71 -0
- package/engine/directions.js +3 -3
- package/engine/engine.js +1600 -150
- package/engine/engine.json +1 -1
- package/engine/gl.js +1092 -216
- package/engine/kit.js +50 -6
- package/engine/motion.html +47 -0
- package/engine/motion.js +258 -0
- package/engine/render.html +11 -3
- package/engine/scenestate.js +127 -15
- package/engine/select.js +4 -3
- package/engine/soma.bin +0 -0
- package/engine/soma.md +4 -0
- package/engine/speech.js +23 -19
- package/index.js +135 -26
- package/package.json +4 -2
- package/scene.js +28 -12
- package/video.js +66 -0
package/engine/scenestate.js
CHANGED
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
// default — it starts and stops gently; linear, in, out, back, spring, anticipate…). A value set
|
|
11
11
|
// is heard once, where it is set to (on('change')); a value moving there is heard each frame as it
|
|
12
12
|
// moves (on('move')). What a value was a moment ago stays known for a while (was): what lags
|
|
13
|
-
// behind — a coat's tail, hair, a head that follows — reads it. A
|
|
14
|
-
//
|
|
13
|
+
// behind — a coat's tail, hair, a head that follows — reads it. A cut (scene.cut) is a new shot:
|
|
14
|
+
// its values set at once and what came before it forgotten — was() reaches back no further, and
|
|
15
|
+
// what has inertia (the camera, a module's springs: e.cut) starts afresh. The scene's start is a
|
|
16
|
+
// cut too. A script may tell that something happened (scene.tell): the game decides what it means.
|
|
17
|
+
// A scene may describe its values (spec.states: each key's range or values, whether it moves
|
|
18
|
+
// smoothly, what it means) — for the game and the agents that turn them; then a
|
|
19
|
+
// key it doesn't describe, or a value outside what it says, is said (window.__mooodWarnings).
|
|
15
20
|
const MooodState = (() => {
|
|
16
21
|
let stage = null, tell = null, save = null, t0 = performance.now();
|
|
17
22
|
let values = {}, cur = {}, tweens = new Map(), listeners = [], timers = [], raf = 0, pending = {}, flushTimer = 0;
|
|
@@ -21,7 +26,75 @@ const MooodState = (() => {
|
|
|
21
26
|
// the engine's own values (engine.js, gl.js: the camera a film moves, its fade): 0 until set
|
|
22
27
|
const ENGINE = { camera_x: 0, camera_h: 0, camera_z: 0, camera_look: 0, fade: 0 };
|
|
23
28
|
let past = new Map();
|
|
29
|
+
// the last cut (performance.now()): the scene's start or scene.cut; shots — how many scene.cuts
|
|
30
|
+
// there have been (the scene's start is not one: the viewer keeps where they looked from)
|
|
31
|
+
let cutAt = t0, shots = 0;
|
|
24
32
|
const report = (who, err) => (window.__mooodErrors = window.__mooodErrors || []).push(`script ${who}: ${err && err.message || err}`);
|
|
33
|
+
// the scene's description of its values (spec.states), when it has one; what is wrong with what is
|
|
34
|
+
// set, said once each
|
|
35
|
+
let states = null;
|
|
36
|
+
const said = new Set();
|
|
37
|
+
const say = msg => { if (said.has(msg)) return; said.add(msg); (window.__mooodWarnings = window.__mooodWarnings || []).push(msg); };
|
|
38
|
+
// the keys the engine itself reads for the scene, each with what it takes (kit.js: keys — the
|
|
39
|
+
// camera's and fade, an insert's, a place's, a redraw's, the light's looks, a light's on): every
|
|
40
|
+
// scene's, described or not
|
|
41
|
+
let own = MooodKit.keys(null);
|
|
42
|
+
const TYPES = ['number', 'boolean', 'string'];
|
|
43
|
+
// what is wrong with value v for the description d of key k, or null
|
|
44
|
+
function wrong(k, d, v) {
|
|
45
|
+
if (v === null) return null;
|
|
46
|
+
const show = JSON.stringify(v);
|
|
47
|
+
if (Array.isArray(d.values)) return d.values.some(x => JSON.stringify(x) === show) ? null : `state "${k}": ${show} is not one of ${d.values.map(x => JSON.stringify(x)).join(', ')}`;
|
|
48
|
+
const type = Array.isArray(d.range) ? 'number' : d.type;
|
|
49
|
+
if (type && typeof v !== type) return `state "${k}": ${show} — a ${type} expected`;
|
|
50
|
+
if (Array.isArray(d.range) && (v < d.range[0] || v > d.range[1])) return `state "${k}": ${show} is outside its range [${d.range.join(', ')}]`;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// spec.states as it is given: { key: {range: [min, max] | values: [...] | type, smooth, means} } (where
|
|
54
|
+
// a value starts is the scene's `state`)
|
|
55
|
+
function describe(spec) {
|
|
56
|
+
const st = spec?.states;
|
|
57
|
+
if (st == null) return null;
|
|
58
|
+
if (typeof st !== 'object' || Array.isArray(st)) { say('states: { key: {range | values | type, smooth, means} } expected'); return null; }
|
|
59
|
+
for (const [k, d] of Object.entries(st)) {
|
|
60
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) { say(`states.${k}: an object expected`); continue; }
|
|
61
|
+
if (d.range !== undefined && !(Array.isArray(d.range) && d.range.length === 2 && d.range.every(Number.isFinite) && d.range[0] < d.range[1])) say(`states.${k}: "range" is [min, max]`);
|
|
62
|
+
if (d.values !== undefined && !(Array.isArray(d.values) && d.values.length)) say(`states.${k}: "values" is the list of what it can be`);
|
|
63
|
+
if (d.type !== undefined && !TYPES.includes(d.type)) say(`states.${k}: "type" is one of ${TYPES.join(', ')}`);
|
|
64
|
+
if ('start' in d) say(`states.${k}: where a value starts is the scene's "state" ("state": {"${k}": …}), not its description`);
|
|
65
|
+
}
|
|
66
|
+
states = st;
|
|
67
|
+
for (const [k, v] of Object.entries(spec.state && typeof spec.state === 'object' ? spec.state : {})) check(k, v);
|
|
68
|
+
return st;
|
|
69
|
+
}
|
|
70
|
+
// a value set: said if the scene's states don't have its key, or it is outside what they say (an
|
|
71
|
+
// engine's key: what the engine takes, unless the states describe it)
|
|
72
|
+
function check(key, value) {
|
|
73
|
+
if (!states) return;
|
|
74
|
+
const d = states[key] && typeof states[key] === 'object' ? states[key] : own.get(key);
|
|
75
|
+
if (!d) { say(`state "${key}": not one of the scene's states`); return; }
|
|
76
|
+
const w = wrong(key, d, value);
|
|
77
|
+
if (w) say(w);
|
|
78
|
+
}
|
|
79
|
+
// A scene without states (above): the keys it knows are its starting state's (declared), the
|
|
80
|
+
// engine's (own), those its scripts use (set, play, get, on, scene.state.x) or a widget reads, and
|
|
81
|
+
// those its painting reads (the stage's: its elements' code, its modules — as they are painted). A
|
|
82
|
+
// key set from outside it (who: the game, a widget, a direction, a render's --set) that none of
|
|
83
|
+
// them knows is kept (strangers); unknown() says those that nothing has used by the time it is
|
|
84
|
+
// asked — a render asks once its frames are taken, so a key read late, or only by what is painted
|
|
85
|
+
// later, is not mistaken for a mistyped one. (A scene with states says a key they lack as it is
|
|
86
|
+
// set: check.)
|
|
87
|
+
let declared = new Set(), used = new Set(), strangers = new Map();
|
|
88
|
+
function outside(key, who = 'a widget', start = true) {
|
|
89
|
+
key = String(key);
|
|
90
|
+
if (states || (start && declared.has(key)) || own.has(key) || strangers.has(key)) return;
|
|
91
|
+
strangers.set(key, who);
|
|
92
|
+
}
|
|
93
|
+
function unknown() {
|
|
94
|
+
const read = stage?.read?.() || new Set();
|
|
95
|
+
return [...strangers].filter(([k]) => !used.has(k) && !read.has(k))
|
|
96
|
+
.map(([k, who]) => `${who} sets "${k}": not in the scene's "state", and nothing in it read it — a mistyped key?`);
|
|
97
|
+
}
|
|
25
98
|
|
|
26
99
|
// where a move is at `now` (a move along a path: where its path is, `way` 0…1 of it)
|
|
27
100
|
const along = (m, now) => {
|
|
@@ -30,9 +103,10 @@ const MooodState = (() => {
|
|
|
30
103
|
const u = m.curve((now - m.start) / (m.end - m.start));
|
|
31
104
|
return m.way ? m.way(u) : m.from + (m.to - m.from) * u;
|
|
32
105
|
};
|
|
33
|
-
// what a number was `ago` seconds ago (as it is now if it hasn't moved since)
|
|
106
|
+
// what a number was `ago` seconds ago (as it is now if it hasn't moved since) — no further back
|
|
107
|
+
// than the last cut: before it, it was as the cut found it
|
|
34
108
|
function was(key, ago = 0) {
|
|
35
|
-
const now = performance.now(), at = now - ago * 1000, moves = past.get(key);
|
|
109
|
+
const now = performance.now(), at = Math.max(cutAt, now - ago * 1000), moves = past.get(key);
|
|
36
110
|
if (!moves || !moves.length) return cur[key];
|
|
37
111
|
for (let i = moves.length - 1; i >= 0; i--) if (moves[i].start <= at) return along(moves[i], at);
|
|
38
112
|
return moves[0].from;
|
|
@@ -44,7 +118,7 @@ const MooodState = (() => {
|
|
|
44
118
|
past.set(key, moves);
|
|
45
119
|
}
|
|
46
120
|
|
|
47
|
-
function publish() { cur = { ...values }; for (const [k, tw] of tweens) cur[k] = along(tw, performance.now()); stage?.setState(cur, was, sample); }
|
|
121
|
+
function publish() { cur = { ...values }; for (const [k, tw] of tweens) cur[k] = along(tw, performance.now()); stage?.setState(cur, was, sample, cutAt, shots); }
|
|
48
122
|
// the moving values, where they are now (the stage asks as it paints a frame): each heard moving
|
|
49
123
|
// from where it was last seen
|
|
50
124
|
function sample() {
|
|
@@ -76,6 +150,7 @@ const MooodState = (() => {
|
|
|
76
150
|
// null removes it. kept: false — the game set it and keeps it itself
|
|
77
151
|
function set(key, value, over = 0, ease = 'smooth', { kept = true } = {}) {
|
|
78
152
|
key = String(key);
|
|
153
|
+
check(key, value === undefined ? null : value);
|
|
79
154
|
const old = key in cur ? cur[key] : key in values ? values[key] : ENGINE[key];
|
|
80
155
|
if (value === undefined) value = null;
|
|
81
156
|
const now = performance.now();
|
|
@@ -93,12 +168,21 @@ const MooodState = (() => {
|
|
|
93
168
|
if (kept) keep({ [key]: value });
|
|
94
169
|
if (JSON.stringify(old) !== JSON.stringify(value)) emit('change', key, value, old, key);
|
|
95
170
|
}
|
|
171
|
+
// a cut: a new shot — the values in `changes` set at once (the camera elsewhere, people already
|
|
172
|
+
// where they got to), the moves of what it names stopped, and the history before it forgotten
|
|
173
|
+
function cut(changes = {}) {
|
|
174
|
+
if (!changes || typeof changes !== 'object' || Array.isArray(changes)) throw new Error('cut: an object of values expected');
|
|
175
|
+
cutAt = performance.now(); shots++;
|
|
176
|
+
for (const [k, v] of Object.entries(changes)) set(k, v);
|
|
177
|
+
publish();
|
|
178
|
+
}
|
|
96
179
|
// several numbers moved together through points, as one movement: keys ['cart_a', 'cart_d'], points
|
|
97
180
|
// [[a, d], …] (from where they are now); over `over` seconds or at `speed` (their units a second,
|
|
98
181
|
// along the way), along `ease` — so it slows nowhere but where the curve says (a path cut into
|
|
99
182
|
// set()s stops at every corner). Returns its seconds.
|
|
100
183
|
function path(keys, points, { over = 0, speed = 0, ease = 'smooth' } = {}) {
|
|
101
184
|
keys = keys.map(String);
|
|
185
|
+
for (const [i, k] of keys.entries()) for (const p of points) check(k, Number(p[i]) || 0);
|
|
102
186
|
const from = keys.map(k => Number(k in cur ? cur[k] : k in values ? values[k] : ENGINE[k]) || 0);
|
|
103
187
|
const pts = [from, ...points.map(p => keys.map((_, i) => Number(p[i]) || 0))], to = pts[pts.length - 1];
|
|
104
188
|
const ends = [0];
|
|
@@ -134,34 +218,44 @@ const MooodState = (() => {
|
|
|
134
218
|
// the API a script gets: for the scene (id null) or for one element
|
|
135
219
|
function api(who, id, element, shared) {
|
|
136
220
|
const own = fn => (...a) => { try { fn(...a); } catch (err) { report(who, err); } };
|
|
221
|
+
const use = (...keys) => { for (const k of keys.flat()) used.add(String(k)); };
|
|
137
222
|
const after = (seconds, fn) => { const h = setTimeout(own(fn), Math.max(0, seconds) * 1000); timers.push(() => clearTimeout(h)); };
|
|
138
223
|
return {
|
|
139
224
|
id, element,
|
|
140
225
|
anim: MooodKit.anim, kit: shared.kit, lib: shared.lib,
|
|
141
|
-
|
|
226
|
+
// the ground's height there (metres; spec.relief — 0 without one)
|
|
227
|
+
ground: (across, distance) => (shared.ground ? shared.ground(across, distance) : 0),
|
|
228
|
+
// (scene.state.x: x is a key the scene uses)
|
|
229
|
+
get state() { return new Proxy(cur, { get: (o, k) => { if (typeof k === 'string') used.add(k); return o[k]; } }); },
|
|
142
230
|
get time() { return (performance.now() - t0) / 1000; }, // seconds since the scene opened
|
|
143
|
-
get: key => cur[key],
|
|
144
|
-
set: (key, value, over, ease) => set(key, value, over, ease),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
231
|
+
get: key => { use(key); return cur[key]; },
|
|
232
|
+
set: (key, value, over, ease) => { use(key); set(key, value, over, ease); },
|
|
233
|
+
cut: changes => { use(Object.keys(changes || {})); cut(changes); },
|
|
234
|
+
turn: (key, degrees, over, ease) => { use(key); turn(key, degrees, over, ease); },
|
|
235
|
+
path: (keys, points, opts) => { use(keys); return path(keys, points, opts); },
|
|
236
|
+
was: (key, ago) => { use(key); return was(key, ago); },
|
|
148
237
|
// a platformer's hero now: { across, elevation (metres), ground } — or null
|
|
149
238
|
hero: () => stage?.hero?.() ?? null,
|
|
150
239
|
// something happened in the scene (about one of its elements, if `about`): the game hears it
|
|
151
240
|
tell: (text, about = null) => tell?.(String(text), about),
|
|
241
|
+
// something wrong but not an error — a name it does not know — said once, in the scene's errors
|
|
242
|
+
warn: text => say(`${who}: ${text}`),
|
|
152
243
|
every(seconds, fn) { const h = setInterval(own(fn), Math.max(0.05, seconds) * 1000); timers.push(() => clearInterval(h)); },
|
|
153
244
|
after,
|
|
154
245
|
// a timeline: rows [at, key, value, over?, ease?] — or [at, fn] — where `at` is the second it
|
|
155
246
|
// starts (from now), or '+s': s seconds after the row before it ends ('+0': as it ends); a
|
|
156
247
|
// key in degrees turned the short way: [at, key, {turn: degrees}, over?, ease?]; numbers
|
|
157
|
-
// moved together through points (path, above): [at, [keys], {path: points}, over, ease?]
|
|
248
|
+
// moved together through points (path, above): [at, [keys], {path: points}, over, ease?];
|
|
249
|
+
// a cut (above): [at, 'cut', {values}].
|
|
158
250
|
// Returns when the last row ends (seconds from now).
|
|
159
251
|
play(rows) {
|
|
160
252
|
let end = 0, last = 0;
|
|
161
253
|
for (const row of rows) {
|
|
162
254
|
const [at, a, b, over = 0, ease] = row;
|
|
163
255
|
const start = typeof at === 'string' && at.startsWith('+') ? last + Number(at.slice(1)) : Number(at);
|
|
256
|
+
if (typeof a !== 'function') use(a === 'cut' ? Object.keys(b || {}) : a);
|
|
164
257
|
if (typeof a === 'function') after(start, a);
|
|
258
|
+
else if (a === 'cut') after(start, () => cut(b));
|
|
165
259
|
else if (Array.isArray(a) && b && typeof b === 'object' && Array.isArray(b.path)) after(start, () => path(a, b.path, { over, ease }));
|
|
166
260
|
else if (b && typeof b === 'object' && 'turn' in b) after(start, () => turn(a, b.turn, over, ease));
|
|
167
261
|
else after(start, () => set(a, b, over, ease));
|
|
@@ -175,6 +269,7 @@ const MooodState = (() => {
|
|
|
175
269
|
// `over`, path), till it stops; on('load', fn(state))
|
|
176
270
|
on(ev, a, b) {
|
|
177
271
|
const fn = typeof a === 'function' ? a : b, key = typeof a === 'function' ? null : String(a);
|
|
272
|
+
if (key != null) use(key);
|
|
178
273
|
listeners.push({ ev, key, fn, who });
|
|
179
274
|
},
|
|
180
275
|
};
|
|
@@ -191,9 +286,13 @@ const MooodState = (() => {
|
|
|
191
286
|
stage = opts.stage || stage;
|
|
192
287
|
tell = opts.tell || null;
|
|
193
288
|
save = opts.save || null;
|
|
289
|
+
own = MooodKit.keys(spec); said.clear();
|
|
290
|
+
states = describe(spec);
|
|
291
|
+
for (const [k, v] of Object.entries(state)) check(k, v);
|
|
194
292
|
values = { ...(spec?.state && typeof spec.state === 'object' ? spec.state : {}), ...state };
|
|
293
|
+
declared = new Set(Object.keys(values)); used = new Set(); strangers = new Map();
|
|
195
294
|
past = new Map();
|
|
196
|
-
t0 = performance.now();
|
|
295
|
+
t0 = cutAt = performance.now();
|
|
197
296
|
publish();
|
|
198
297
|
emit('load', null, cur);
|
|
199
298
|
const mine = ++run;
|
|
@@ -216,9 +315,22 @@ const MooodState = (() => {
|
|
|
216
315
|
cancelAnimationFrame(raf); raf = 0;
|
|
217
316
|
}
|
|
218
317
|
|
|
318
|
+
// set from outside the scene (above) — by `who` (default: a widget, which has MooodState as it is): a
|
|
319
|
+
// key it does not know is said
|
|
219
320
|
return {
|
|
220
|
-
start, stop,
|
|
221
|
-
|
|
321
|
+
start, stop, was,
|
|
322
|
+
set: (key, value, over, ease, opts = {}) => { outside(key, opts.who); set(key, value, over, ease, opts); },
|
|
323
|
+
cut: (changes, { who } = {}) => { for (const k of Object.keys(changes || {})) outside(k, who); cut(changes); },
|
|
324
|
+
turn: (key, degrees, over, ease) => { outside(key); turn(key, degrees, over, ease); },
|
|
325
|
+
path: (keys, points, opts) => { keys.forEach(k => outside(k)); return path(keys, points, opts); },
|
|
326
|
+
// keys put into the starting state from outside (a render's --state, over the spec's): kept as
|
|
327
|
+
// strangers (above) unless the scene knows them
|
|
328
|
+
check: (keys, who) => keys.forEach(k => outside(k, who, false)),
|
|
329
|
+
// the keys set from outside that nothing in the scene has used so far: what is said of each
|
|
330
|
+
unknown,
|
|
331
|
+
// the shown scene's description of its values (spec.states), or null
|
|
332
|
+
get states() { return states; },
|
|
333
|
+
get: key => { used.add(String(key)); return cur[key]; },
|
|
222
334
|
all: () => cur,
|
|
223
335
|
on: (ev, a, b) => api('widget', null, null, { kit: {}, lib: {} }).on(ev, a, b),
|
|
224
336
|
};
|
package/engine/select.js
CHANGED
|
@@ -78,12 +78,13 @@ const MooodSelect = (() => {
|
|
|
78
78
|
const sx = (clientX - r.left) / k - x, sy = (clientY - r.top) / k - y;
|
|
79
79
|
// nearest first, in the renderer's order: it paints far to near, and at equal distance
|
|
80
80
|
// layers in spec order, then objects — so the later one is on top
|
|
81
|
-
// (looking around, the elements standing about — no landscape layers, the sky behind them
|
|
81
|
+
// (looking around, the elements standing about — no landscape layers, the sky behind them; what
|
|
82
|
+
// is near the eye, while it is seen: not faded out as it is passed, engine.js nearFade)
|
|
82
83
|
const round = I.camSetup(spec).round;
|
|
83
84
|
const things = [
|
|
84
85
|
...(round ? [] : spec.layers || []).map((L, i) => ({ path: `layers[${i}]`, L, i, at: { m: I.view(cam, dz.Z[i], W, H, dz.yh), D: dz.Z[i] - cam.z } })).filter(t => LAND.has(t.L.kind)),
|
|
85
|
-
...(spec.objects || []).map((o, i) => ({ path: `objects[${i}]`, o: placed(dz, o), at: round && o.sky ? null : I.placeOf(o, cam, W, H, dz, round) })).filter(t => t.o.kind !== 'bird' && !t.o.plane),
|
|
86
|
-
].map((t, k) => ({ ...t, k })).filter(t => t.at && t.at.D
|
|
86
|
+
...(spec.objects || []).map((o, i) => ({ path: `objects[${i}]`, o: placed(dz, o), at: round && o.sky ? null : I.placeOf(o, cam, W, H, dz, round) })).filter(t => t.o.kind !== 'bird' && !t.o.plane && !t.o.insert && !t.o.unseen),
|
|
87
|
+
].map((t, k) => ({ ...t, k })).filter(t => t.at && I.nearFade(t.at.D) >= 0.3 && (!only || only(t.path))).sort((a, b) => a.at.D - b.at.D || b.k - a.k);
|
|
87
88
|
for (const t of things) {
|
|
88
89
|
const m = heroed(t.at.m, t.o, cam, SW, H), s = m[0], px = (sx - m[4]) / s, py = (sy - m[5]) / m[3];
|
|
89
90
|
if (t.o) {
|
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/engine/speech.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
// voice being made — a "…" glimmers over them (thinking()).
|
|
8
8
|
const MooodSpeech = (() => {
|
|
9
9
|
let stage = null, canvas = null, layer = null, voiceless = null, raf = 0, queue = Promise.resolve();
|
|
10
|
-
|
|
10
|
+
// who says it: an element ('dv'), over its point (e.at) or a point it names ('dv#mouth') — one
|
|
11
|
+
// speaker, one bubble, wherever it is put
|
|
12
|
+
const bubbles = new Map(); // element id → { el, text, until, pending, at: where it is put ('dv#mouth') }
|
|
11
13
|
const waiting = new Map(); // element id → how many things it is about to say
|
|
14
|
+
const speaker = ref => String(ref).split('#')[0];
|
|
12
15
|
let current = null; // the voice being heard right now
|
|
13
16
|
let said = 0; // lines said before the player last spoke up: those not begun yet are dropped
|
|
14
17
|
const NARRATOR = 'narrator';
|
|
@@ -47,7 +50,8 @@ const MooodSpeech = (() => {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
// plays a line's voice, the bubble glimmering; resolves when it has been heard
|
|
50
|
-
async function hear(
|
|
53
|
+
async function hear(ref, text, a, bubble) {
|
|
54
|
+
const id = speaker(ref);
|
|
51
55
|
bubble?.el.classList.add('speaking'); told(id, text, true);
|
|
52
56
|
current = a;
|
|
53
57
|
a.volume = MooodVolume.get('voice');
|
|
@@ -65,8 +69,8 @@ const MooodSpeech = (() => {
|
|
|
65
69
|
current.onended();
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
function bubbleFor(
|
|
69
|
-
const old = bubbles.get(id);
|
|
72
|
+
function bubbleFor(ref, text, voice, how, who) {
|
|
73
|
+
const id = speaker(ref), old = bubbles.get(id);
|
|
70
74
|
if (old) old.el.remove();
|
|
71
75
|
const el = document.createElement('div');
|
|
72
76
|
el.className = id === NARRATOR ? 'bubble narrator' : 'bubble';
|
|
@@ -76,11 +80,11 @@ const MooodSpeech = (() => {
|
|
|
76
80
|
if (voice) {
|
|
77
81
|
const again = document.createElement('button');
|
|
78
82
|
again.type = 'button'; again.textContent = '▶'; again.title = 'ещё раз';
|
|
79
|
-
again.onclick = () => replay(
|
|
83
|
+
again.onclick = () => replay(ref, text, voice, how, who);
|
|
80
84
|
el.append(again);
|
|
81
85
|
}
|
|
82
86
|
layer.append(el);
|
|
83
|
-
const b = { el, text, until: Infinity, pending: false };
|
|
87
|
+
const b = { el, text, until: Infinity, pending: false, at: ref };
|
|
84
88
|
bubbles.set(id, b);
|
|
85
89
|
if (!raf) raf = requestAnimationFrame(place);
|
|
86
90
|
return b;
|
|
@@ -90,8 +94,8 @@ const MooodSpeech = (() => {
|
|
|
90
94
|
raf = 0;
|
|
91
95
|
const now = performance.now();
|
|
92
96
|
for (const [id, b] of bubbles) {
|
|
93
|
-
if (now > b.until) { b.el.remove(); bubbles.delete(id); if (waiting.
|
|
94
|
-
const at = id === NARRATOR ? [innerWidth / 2, innerHeight * 0.86] : stage.screenOf(
|
|
97
|
+
if (now > b.until) { b.el.remove(); bubbles.delete(id); if (waiting.has(id)) pend(waiting.get(id).at); continue; }
|
|
98
|
+
const at = id === NARRATOR ? [innerWidth / 2, innerHeight * 0.86] : stage.screenOf(b.at, 0.25, true);
|
|
95
99
|
b.el.style.visibility = at ? '' : 'hidden';
|
|
96
100
|
if (at) b.el.style.transform = `translate(${Math.round(at[0])}px, ${Math.round(at[1])}px) translate(-50%, -100%)`;
|
|
97
101
|
b.el.classList.toggle('fading', now > b.until - 1200);
|
|
@@ -100,23 +104,23 @@ const MooodSpeech = (() => {
|
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
// someone is about to speak (on) — or no longer (off): a glimmering "…" over them meanwhile
|
|
103
|
-
function pend(
|
|
104
|
-
const b = bubbleFor(
|
|
107
|
+
function pend(ref) {
|
|
108
|
+
const b = bubbleFor(ref, '…');
|
|
105
109
|
b.pending = true;
|
|
106
110
|
b.el.classList.add('pending');
|
|
107
111
|
}
|
|
108
|
-
function thinking(
|
|
109
|
-
const n = (waiting.get(id) || 0) + (on ? 1 : -1);
|
|
110
|
-
if (n > 0) waiting.set(id, n); else waiting.delete(id);
|
|
112
|
+
function thinking(ref, on) {
|
|
113
|
+
const id = speaker(ref), n = (waiting.get(id)?.n || 0) + (on ? 1 : -1);
|
|
114
|
+
if (n > 0) waiting.set(id, { n, at: ref }); else waiting.delete(id);
|
|
111
115
|
const b = bubbles.get(id);
|
|
112
|
-
if (n > 0 && (!b || b.until !== Infinity)) pend(
|
|
116
|
+
if (n > 0 && (!b || b.until !== Infinity)) pend(ref);
|
|
113
117
|
if (n <= 0 && b?.pending) { b.el.remove(); bubbles.delete(id); }
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
// id: who says it (an element of the scene
|
|
117
|
-
// `who` ("Savely, the old keeper"). A line with
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
+
// id: who says it (an element of the scene; 'id#name' — over a point it names); voice/how: heard
|
|
121
|
+
// in that voice, that way, played as `who` ("Savely, the old keeper"). A line with a voice appears
|
|
122
|
+
// when it can be heard — its words and its voice together; its voice is made while the lines
|
|
123
|
+
// before it are said.
|
|
120
124
|
function say(id, text, { voice, how, who } = {}) {
|
|
121
125
|
text = String(text || '').trim();
|
|
122
126
|
if (!text) return Promise.resolve();
|
|
@@ -140,7 +144,7 @@ const MooodSpeech = (() => {
|
|
|
140
144
|
async function replay(id, text, voice, how, who) {
|
|
141
145
|
const a = await voiced(text, voice, how, who);
|
|
142
146
|
if (!a) return;
|
|
143
|
-
const b = bubbles.get(id);
|
|
147
|
+
const b = bubbles.get(speaker(id));
|
|
144
148
|
if (b) b.until = Infinity;
|
|
145
149
|
await hear(id, text, a, b);
|
|
146
150
|
if (b) b.until = performance.now() + 2500;
|
package/index.js
CHANGED
|
@@ -1,70 +1,179 @@
|
|
|
1
1
|
// A moood scene rendered here, with moood's own engine (engine/: the site's web/scene, its version in
|
|
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
|
-
// the same every time, with the same state.
|
|
5
|
-
//
|
|
4
|
+
// the same every time, with the same state. Only the frames taken are drawn: up to each moment the
|
|
5
|
+
// scene goes on unseen (its scripts, its state, the memory its modules keep — all as when it plays;
|
|
6
|
+
// the engine's stage.draw(false)), so a late moment costs little more than an early one.
|
|
7
|
+
// render({spec, modules, device, at, state, set, views, bare, debug, look, crop, chromium, date, engine, each})
|
|
8
|
+
// → [{at, view, png, errors, warnings, tells, look}]
|
|
6
9
|
// spec — the scene (readScene(folder) from a world's repository, or any spec);
|
|
7
10
|
// modules — the modules it lists, themselves: {name: {code, data}} (readModules(world, spec));
|
|
8
11
|
// device — the frame: 16:9 (1600×900), 4:3, 21:9 (the same height), tile (640×360), big (1920×1080);
|
|
9
12
|
// at — the moments, seconds from the scene's start (default [0]): a frame each;
|
|
10
13
|
// state — values over the spec's starting state ({"dawn": 1, "girl": "gone"});
|
|
14
|
+
// set — values set as the scene goes, as a game sets them: [[second, key, value, over?, ease?], …]
|
|
15
|
+
// (a film's beat at its second: [[4, "beat", "cover"]]); a cut: [[4, "cut", {values}]] (scene.cut);
|
|
16
|
+
// views — other cameras on each moment: [{camera_target: "keeper", camera_shot: "close"}, {camera_x: 2},
|
|
17
|
+
// …] — a frame each (`view` 1, 2 …) of the very moment, the camera there at once; the film (its
|
|
18
|
+
// state, its clock, its own camera) goes on untouched;
|
|
11
19
|
// bare — without the title and the captions over it;
|
|
20
|
+
// debug — what is where, drawn over the frame (the scene's ?debug): ids, boxes, points, notes;
|
|
21
|
+
// true, or the ids of some elements ('keeper,boy'): only theirs;
|
|
22
|
+
// look — what is where, as numbers, with each frame (`look`): {t, camera, frame, elements, state}
|
|
23
|
+
// (the engine's stage.inspect(): each element's box in the frame's pixels, whether it is seen,
|
|
24
|
+
// its points, its notes, what its last painting cost);
|
|
25
|
+
// crop — an element's id: each frame only around it (its box, a margin round it);
|
|
26
|
+
// jpeg — frames as JPEG (`jpg` instead of `png`), quality 90: smaller, for a video;
|
|
12
27
|
// 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
|
-
|
|
28
|
+
// date — what the clock says at the start (default 2026-01-01T12:00:00Z);
|
|
29
|
+
// engine — another engine to render with: a folder like web/scene (another checkout's, an older
|
|
30
|
+
// package's engine/) — to compare engines;
|
|
31
|
+
// each — called with each frame as it is taken (async ok); then frames are not kept.
|
|
32
|
+
// Each frame: its errors, and what the scene's scripts told so far (scene.tell): [{at, text, about}].
|
|
33
|
+
// renderMotion({motion, at, views, kit, cell, chromium}) → {png, errors}
|
|
34
|
+
// a person's motion made by a model (moood's make_motion: list_motions(key, body=true) — its `body` is
|
|
35
|
+
// the motion, its `clip` the people module's entry) as frames on its body (engine/motion.html): a row
|
|
36
|
+
// per view (front, side, back, or {yaw, pitch, dist}), a column per moment (at, seconds; default eight
|
|
37
|
+
// across it); kit — the module's clip, its skeleton drawn over the body; cell — [w, h] of a frame.
|
|
38
|
+
// browser(chromium) — the Chromium it renders in, launched (compose.js lays frames out in it too).
|
|
39
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
40
|
+
import { join, resolve } from 'node:path';
|
|
15
41
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
42
|
import { chromium as browserType } from 'playwright-core';
|
|
17
43
|
|
|
18
44
|
export { readModules, readScene } from './scene.js';
|
|
19
45
|
|
|
20
|
-
const
|
|
21
|
-
export const ENGINE = JSON.parse(readFileSync(
|
|
46
|
+
const OWN = fileURLToPath(new URL('./engine/', import.meta.url));
|
|
47
|
+
export const ENGINE = JSON.parse(readFileSync(join(OWN, 'engine.json'), 'utf8')).engine;
|
|
22
48
|
export const DEVICES = { '16:9': [1600, 900], '4:3': [1200, 900], '21:9': [2100, 900], tile: [640, 360], big: [1920, 1080] };
|
|
23
49
|
const STEP = 100; // ms: timers fire as they come due, not all at the end of one jump
|
|
50
|
+
const FRAME = 17; // ms: one frame drawn
|
|
51
|
+
// ms before a frame taken that the scene is painted as when drawn (unseen still): what keeps a memory
|
|
52
|
+
// of its own and depends on how big it is painted (a module's detail drawn only up close) comes to
|
|
53
|
+
// the frame exactly as when the film plays; before that, coarsely
|
|
54
|
+
const SETTLE = 2000;
|
|
55
|
+
const PATIENCE = 10 * 60_000; // ms: how long anything on the page is waited for
|
|
24
56
|
|
|
25
|
-
export
|
|
57
|
+
export const browser = chromium => browserType.launch({
|
|
58
|
+
executablePath: chromium || process.env.MOOOD_CHROMIUM || undefined,
|
|
59
|
+
// WebGL on SwiftShader; 2D canvases on the CPU's Skia (SwiftShader takes minutes for many blended fills)
|
|
60
|
+
args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--disable-accelerated-2d-canvas',
|
|
61
|
+
'--allow-file-access-from-files'],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export async function render({ spec, modules = {}, device = '16:9', at = [0], state = {}, set = [], views = [], bare = false, debug = false,
|
|
65
|
+
look = false, crop = null, jpeg = false, chromium, date, engine, each } = {}) {
|
|
26
66
|
const size = DEVICES[device];
|
|
27
67
|
if (!size) throw new Error(`device: one of ${Object.keys(DEVICES).join(', ')}`);
|
|
28
68
|
if (!spec || typeof spec !== 'object') throw new Error('spec: a scene expected');
|
|
69
|
+
const dir = engine ? resolve(engine) : OWN;
|
|
70
|
+
if (!existsSync(join(dir, 'render.html'))) throw new Error(`engine: ${dir} is not an engine (no render.html in it: a folder like moood/web/scene)`);
|
|
29
71
|
const shown = { ...spec, state: { ...(spec.state && typeof spec.state === 'object' ? spec.state : {}), ...state } };
|
|
30
|
-
const
|
|
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
|
-
});
|
|
72
|
+
const b = await browser(chromium);
|
|
36
73
|
try {
|
|
37
|
-
const page = await (await
|
|
74
|
+
const page = await (await b.newContext({ viewport: { width: size[0], height: size[1] } })).newPage();
|
|
75
|
+
// a machine under load draws slowly: what waits on the page waits long (Playwright's own: 30 s)
|
|
76
|
+
page.setDefaultTimeout(PATIENCE);
|
|
38
77
|
const errors = [];
|
|
39
78
|
page.on('pageerror', e => errors.push(String(e.message)));
|
|
40
|
-
// the clock
|
|
79
|
+
// the real clock, before the page's own is stopped: what painting costs is timed by it
|
|
80
|
+
await page.addInitScript(() => { const now = performance.now.bind(performance); window.__mooodClock = () => now(); });
|
|
81
|
+
// the clock stands still: only the frames' moments move it (install alone lets it run as time
|
|
82
|
+
// does — from a minute before, so that however slowly a loaded machine gets to stopping it, it
|
|
83
|
+
// is stopped ahead of where it is, not behind: "Cannot fast-forward to the past")
|
|
41
84
|
const start = new Date(date || '2026-01-01T12:00:00Z').getTime();
|
|
42
|
-
await page.clock.install({ time: start });
|
|
43
|
-
await page.clock.pauseAt(start
|
|
85
|
+
await page.clock.install({ time: start - 60_000 });
|
|
86
|
+
await page.clock.pauseAt(start);
|
|
44
87
|
await page.addInitScript(([s, m]) => {
|
|
45
88
|
window.__mooodSpec = s; window.__mooodModules = m;
|
|
46
89
|
// chance, the same every time (the engine's grain, rain, sparks): the same moment, the same frame
|
|
47
90
|
let seed = 1;
|
|
48
91
|
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
92
|
}, [shown, modules]);
|
|
50
|
-
|
|
93
|
+
const query = [bare && 'bare', debug && (debug === true ? 'debug' : `debug=${encodeURIComponent(debug)}`)].filter(Boolean).join('&');
|
|
94
|
+
await page.goto(`${pathToFileURL(join(dir, 'render.html')).href}${query ? `?${query}` : ''}`);
|
|
51
95
|
// what loads by itself (fonts, the scene's modules) has loaded before the clock moves at all
|
|
52
96
|
await page.evaluate(() => document.fonts.ready);
|
|
53
97
|
await page.waitForTimeout(1000);
|
|
54
|
-
|
|
98
|
+
// values put in its starting state that nothing in the scene uses (a mistyped key): said
|
|
99
|
+
const extra = Object.keys(state).filter(k => !(spec.state && typeof spec.state === 'object' && k in spec.state));
|
|
100
|
+
if (extra.length) await page.evaluate(k => typeof MooodState !== 'undefined' && MooodState.check?.(k, '--state'), extra);
|
|
101
|
+
// an engine that can go on unseen (older ones draw every frame up to the moment)
|
|
102
|
+
const [unseeing, inspecting, stills] = await page.evaluate(() => ['draw', 'inspect', 'still'].map(f => typeof window.__mooodStage?.[f] === 'function'));
|
|
103
|
+
if ((look || crop) && !inspecting) throw new Error('this engine does not tell what is where (look, crop): it is older than them');
|
|
104
|
+
const frames = [], sets = [...set].map(([s, ...rest]) => [Number(s) * 1000, ...rest]).sort((a, b) => a[0] - b[0]);
|
|
55
105
|
let now = 0;
|
|
106
|
+
// the values due by now, set (the clock stops at each one's second: it is set on time)
|
|
107
|
+
const due = async () => {
|
|
108
|
+
while (sets.length && sets[0][0] <= now) {
|
|
109
|
+
const [, key, value, over, ease] = sets.shift();
|
|
110
|
+
await page.evaluate(([k, v, o, e]) => (k === 'cut' ? MooodState.cut(v, { who: '--set' }) : MooodState.set(k, v, o, e, { who: '--set' })), [key, value, over, ease]);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const run = async ms => { await page.clock.runFor(ms); now += ms; await due(); };
|
|
114
|
+
// on to `ms`, the clock stopping at each value's second (what is due by then is set)
|
|
115
|
+
const until = async ms => { while (now < ms) await run(Math.min(STEP, ms - now, sets.length ? sets[0][0] - now : STEP)); };
|
|
116
|
+
const draw = how => unseeing && page.evaluate(v => window.__mooodStage.draw(v), how);
|
|
117
|
+
// (keys set from outside that nothing in the scene has read — a mistyped one — said with the last
|
|
118
|
+
// moment's frame: by then whatever reads a key late has read it)
|
|
119
|
+
const last = Math.max(...at.map(Number));
|
|
120
|
+
const shot = async (t, view) => {
|
|
121
|
+
const [said, warned, tells, seen] = await page.evaluate(([l, end]) => [window.__mooodErrors || [], [...(window.__mooodWarnings || []), ...(end ? MooodState.unknown?.() || [] : [])], window.__mooodTells || [],
|
|
122
|
+
l ? { ...window.__mooodStage.inspect(), state: MooodState.all() } : null], [look || !!crop, t === last]);
|
|
123
|
+
let clip;
|
|
124
|
+
if (crop) {
|
|
125
|
+
const el = seen.elements[crop];
|
|
126
|
+
if (!el) throw new Error(`crop: there is no element "${crop}" (its id): ${Object.keys(seen.elements).join(', ')}`);
|
|
127
|
+
if (el.seen && el.box) {
|
|
128
|
+
const [x, y, w, h] = el.box, m = 0.1 * Math.max(w, h), x0 = Math.max(0, x - m), y0 = Math.max(0, y - m);
|
|
129
|
+
clip = { x: x0, y: y0, width: Math.min(size[0], x + w + m) - x0, height: Math.min(size[1], y + h + m) - y0 };
|
|
130
|
+
if (!(clip.width >= 8 && clip.height >= 8)) clip = undefined;
|
|
131
|
+
}
|
|
132
|
+
if (!clip) errors.push(`crop: "${crop}" is not in the frame at ${t}s — the whole frame instead`);
|
|
133
|
+
}
|
|
134
|
+
const image = await page.screenshot(jpeg ? { type: 'jpeg', quality: 90, clip } : { type: 'png', clip });
|
|
135
|
+
const frame = { at: t, view, [jpeg ? 'jpg' : 'png']: image, errors: [...new Set([...errors, ...said])].slice(0, 100), warnings: [...new Set(warned)].slice(0, 100), tells, ...(look ? { look: seen } : {}) };
|
|
136
|
+
if (each) await each(frame); else frames.push(frame);
|
|
137
|
+
};
|
|
138
|
+
await due();
|
|
56
139
|
for (const t of [...at].map(Number).sort((a, b) => a - b)) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await
|
|
60
|
-
|
|
140
|
+
// unseen up to a frame before the moment (its last seconds painted as when drawn); that frame drawn
|
|
141
|
+
if (now < t * 1000 - FRAME) {
|
|
142
|
+
if (now < t * 1000 - SETTLE) { await draw(false); await until(t * 1000 - SETTLE); }
|
|
143
|
+
await draw('painted'); await until(t * 1000 - FRAME); await draw(true);
|
|
61
144
|
}
|
|
145
|
+
// (the scene's first frame is drawn at the first display refresh: a moment before it is that frame)
|
|
146
|
+
await until(Math.max(t * 1000, FRAME));
|
|
62
147
|
await page.clock.runFor(0);
|
|
63
|
-
|
|
64
|
-
|
|
148
|
+
await shot(t);
|
|
149
|
+
if (!views.length) continue;
|
|
150
|
+
if (!stills) throw new Error('views: this engine cannot draw a moment from another camera (stage.still): it is older than that');
|
|
151
|
+
for (const [i, view] of views.entries()) {
|
|
152
|
+
await page.evaluate(v => window.__mooodStage.still(v), view);
|
|
153
|
+
await shot(t, i + 1);
|
|
154
|
+
}
|
|
65
155
|
}
|
|
66
156
|
return frames;
|
|
67
157
|
} finally {
|
|
68
|
-
await
|
|
158
|
+
await b.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function renderMotion({ motion, at, views = ['front', 'side'], kit = null, cell = [320, 400], chromium } = {}) {
|
|
163
|
+
if (!motion || !Array.isArray(motion.names)) throw new Error('motion: the body of a made motion expected (list_motions(key, body=true).body)');
|
|
164
|
+
const length = (motion.n - 1) / motion.fps;
|
|
165
|
+
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));
|
|
166
|
+
const b = await browser(chromium);
|
|
167
|
+
try {
|
|
168
|
+
const page = await (await b.newContext({ viewport: { width: cell[0] * moments.length, height: cell[1] * views.length } })).newPage();
|
|
169
|
+
const errors = [];
|
|
170
|
+
page.on('pageerror', e => errors.push(String(e.message)));
|
|
171
|
+
await page.addInitScript(([m, s]) => { window.__mooodMotion = m; window.__mooodMotionShow = s; }, [motion, { at: moments, views, kit, cell }]);
|
|
172
|
+
await page.goto(pathToFileURL(join(OWN, 'motion.html')).href);
|
|
173
|
+
await page.waitForFunction(() => document.title === 'ready', null, { timeout: 120_000 });
|
|
174
|
+
errors.push(...await page.evaluate(() => window.__mooodRenderErrors || []));
|
|
175
|
+
return { png: await page.screenshot({ type: 'png' }), errors: [...new Set(errors)] };
|
|
176
|
+
} finally {
|
|
177
|
+
await b.close();
|
|
69
178
|
}
|
|
70
179
|
}
|
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
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "A moood scene rendered locally — the site's own engine, in a browser already installed — at any moment of it, with a state: frames, a sheet, a video, what is where as numbers, two versions compared; 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",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"index.js",
|
|
13
13
|
"cli.js",
|
|
14
14
|
"scene.js",
|
|
15
|
+
"compose.js",
|
|
16
|
+
"video.js",
|
|
15
17
|
"engine/",
|
|
16
18
|
"README.md"
|
|
17
19
|
],
|