@moood/render 0.1.2 → 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 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.js CHANGED
@@ -352,8 +352,9 @@ const Moood = (() => {
352
352
  const STATE = { values: {}, ver: 0, was: key => STATE.values[key], sample: null };
353
353
  // what the shown scene's code shares (kit.js): the world's modules it lists and the scene's lib
354
354
  let SHARED = { kit: {}, lib: {} };
355
- // Where an element is now, as its painting code says (e.at): fractions of the stage as seen
356
- // from the start — speech bubbles and widgets follow it (stage.screenOf).
355
+ // Where an element is now, as its painting code says (e.at): the point it marks, in metres from
356
+ // its anchor as painted — speech bubbles and widgets follow it (stage.screenOf) wherever the
357
+ // element is put (its `place`)
357
358
  const FOCUS = new Map();
358
359
  // A platformer's hero as it moves now (camera.mode platform): its painting code reads e.hero —
359
360
  // { vx, vy (m/s), ground } — to move its legs; the engine moves and turns the painting itself.
@@ -376,22 +377,19 @@ const Moood = (() => {
376
377
  compiled.set(code, fn);
377
378
  }
378
379
  if (!fn) return;
379
- // sky and ground elements are painted in stage heights from the stage's top-left corner, as
380
- // seen from the start (x 0…W/H, y 0…1, with margins); others in metres from their anchor
381
- const flat = o.sky || o.ground;
382
- const ppm = flat ? H : o.size * H / (o.height || 2), x = flat ? 0 : o.x * W, y = flat ? 0 : o.y * H;
380
+ const [x, y, ppm] = anchorOf(o, W, H);
383
381
  ctx.save();
384
382
  ctx.translate(x, y);
385
383
  ctx.scale(ppm, ppm);
386
384
  ctx.fillStyle = ctx.strokeStyle = o.color || '#000000';
387
385
  ctx.lineCap = ctx.lineJoin = 'round';
388
386
  const period = o.period || 2;
389
- // a point the code names, as painted — through whatever it did to the canvas since (translate,
390
- // scale, rotate): [x, y, how much it is scaled]
387
+ // a point the code names, in its units from the anchor as painted — through whatever it did to
388
+ // the canvas since (translate, scale, rotate): [x, y, how much it is scaled]
391
389
  const base = ctx.getTransform(), unbase = base.inverse();
392
390
  const painted = (lx, ly) => {
393
391
  const now = ctx.getTransform(), q = unbase.transformPoint(now.transformPoint({ x: lx, y: ly }));
394
- return [x + q.x * ppm, y + q.y * ppm, Math.hypot(now.a, now.b) / Math.hypot(base.a, base.b)];
392
+ return [q.x, q.y, Math.hypot(now.a, now.b) / Math.hypot(base.a, base.b)];
395
393
  };
396
394
  const read = { time: false, state: false };
397
395
  READS.set(o, read);
@@ -404,11 +402,11 @@ const Moood = (() => {
404
402
  light(lx, ly, r, color = '#ffd08a', strength = 0.6) {
405
403
  try {
406
404
  const [px, py, k] = painted(lx, ly);
407
- lightsOut.push({ x: px, y: py, r: r * k * ppm, col: rgb(color), a: strength, flicker: o.flicker ?? 0.3 });
405
+ lightsOut.push({ x: x + px * ppm, y: y + py * ppm, r: r * k * ppm, col: rgb(color), a: strength, flicker: o.flicker ?? 0.3 });
408
406
  } catch (err) { reportError(o, err); }
409
407
  },
410
408
  // where this element is now (its head, say): what speaks or hovers over it follows this point
411
- at(lx, ly) { if (o.id) { const [px, py] = painted(lx, ly); FOCUS.set(o.id, { x: px / W, y: py / H }); } },
409
+ at(lx, ly) { if (o.id) { const [px, py] = painted(lx, ly); FOCUS.set(o.id, { x: px, y: py }); } },
412
410
  anim: MooodKit.anim, kit: SHARED.kit, lib: SHARED.lib,
413
411
  was: (key, ago = 0) => { read.state = true; return STATE.was(key, ago); },
414
412
  speed: key => { read.state = true; const a = STATE.was(key, 0), b = STATE.was(key, 0.05); return typeof a === 'number' && typeof b === 'number' ? (a - b) / 0.05 : 0; },
@@ -417,6 +415,12 @@ const Moood = (() => {
417
415
  try { fn(ctx, e); } catch (err) { reportError(o, err); }
418
416
  ctx.restore();
419
417
  }
418
+ // Where a custom element's painting has its origin on the W × H stage, and its scale: [x, y, px a
419
+ // unit]. Sky and ground elements are painted in stage heights from the stage's top-left corner, as
420
+ // seen from the start (x 0…W/H, y 0…1, with margins); others in metres from their anchor.
421
+ function anchorOf(o, W, H) {
422
+ return o.sky || o.ground ? [0, 0, H] : [o.x * W, o.y * H, o.size * H / (o.height || 2)];
423
+ }
420
424
  // The box an object's painting fits in (pixels of the W × H stage): built-in kinds a fixed frame
421
425
  // around the base, custom ones their `box` in metres (default: a little wider than tall).
422
426
  function objectBox(o, W, H) {
@@ -545,6 +549,15 @@ const Moood = (() => {
545
549
  const land = (spec.layers || []).filter(L => LAND.has(L.kind));
546
550
  return land.length ? Math.min(...land.map(L => L.y)) : 0.6;
547
551
  }
552
+ // An element that moves through the world has `place`: its across, distance, elevation or x are the
553
+ // names of numbers in the state (metres) — it is where they say now, the rest as the spec has it.
554
+ const PLACE = ['across', 'distance', 'elevation', 'x'];
555
+ function placed(o) {
556
+ if (!o.place) return o;
557
+ const n = { ...o };
558
+ for (const f of PLACE) { const k = o.place[f]; if (typeof k === 'string' && typeof STATE.values[k] === 'number') n[f] = STATE.values[k]; }
559
+ return n;
560
+ }
548
561
  function depths(spec) {
549
562
  const layers = spec.layers || [];
550
563
  // An object stands where its base is — unless it is attached to a bigger one (a window on
@@ -589,21 +602,27 @@ const Moood = (() => {
589
602
  const paintZ = o => (o.plane ? planeZ(o) : custom(o) ? sightZ(o) : own(host(o) || o));
590
603
  const objectX = o => (bearing(o) ? 0.5 : typeof o.across === 'number' && !o.sky ? 0.5 + o.across / EYE / STAGE / paintZ(o) : o.x ?? 0.5);
591
604
  const objectY = o => (o.sky || o.ground ? 0 : custom(o) ? yh + (1 - (o.elevation || 0) / EYE) / sightZ(o) : o.kind === 'bird' ? o.y : o.y + shift(o));
605
+ const objectSize = o => (o.sky || o.ground ? 1 : custom(o) ? (o.height || 2) / EYE / sightZ(o) : o.size);
606
+ // where it stands in the world (eye heights: X to the right, Z ahead, Y its anchor's height) and
607
+ // the distance Zp it is painted for
608
+ const objectAt = o => {
609
+ const Zp = paintZ(o), Y = 1 - (objectY(o) - yh) * Zp;
610
+ if (!bearing(o)) return { X: (objectX(o) - 0.5) * STAGE * Zp, Y, Z: Zp, Zp };
611
+ const b = o.bearing * Math.PI / 180, d = o.distance / EYE;
612
+ return { X: d * Math.sin(b), Y, Z: d * Math.cos(b), Zp };
613
+ };
614
+ // ground planes are drawn right after the sky, under everything standing on them
615
+ // a horizontal plane (gl.js) is ordered by its far edge, before what stands on it
616
+ const objectZ = o => (o.sky ? Infinity : o.ground ? Z_FAR * 4 : o.plane ? clamp(planeZ(o) + (o.depth || 4) / 2 / EYE, Z_NEAR, Z_FAR) : custom(o) ? customZ(o) : o.kind === 'bird' ? 30 : own(host(o) || o));
592
617
  return {
593
- yh, Z, objectX,
594
- // where it stands in the world (eye heights: X to the right, Z ahead, Y its anchor's
595
- // height) and the distance Zp it is painted for
596
- objectAt: o => {
597
- const Zp = paintZ(o), Y = 1 - (objectY(o) - yh) * Zp;
598
- if (!bearing(o)) return { X: (objectX(o) - 0.5) * STAGE * Zp, Y, Z: Zp, Zp };
599
- const b = o.bearing * Math.PI / 180, d = o.distance / EYE;
600
- return { X: d * Math.sin(b), Y, Z: d * Math.cos(b), Zp };
618
+ yh, Z, objectX, objectAt, objectZ,
619
+ // an element where it is now (its `place`), the one truth the renderers, the picking and
620
+ // whatever follows it go by: { o: as painted on the stage (x, y, size: fractions of it),
621
+ // Z: the depth it is drawn at, at: where it stands in the world }
622
+ now: o => {
623
+ const n = placed(o);
624
+ return { o: { ...o, x: objectX(n), y: objectY(n), size: objectSize(n), _yh: yh }, Z: objectZ(n), at: objectAt(n) };
601
625
  },
602
- // ground planes are drawn right after the sky, under everything standing on them
603
- // a horizontal plane (gl.js) is ordered by its far edge, before what stands on it
604
- objectZ: o => (o.sky ? Infinity : o.ground ? Z_FAR * 4 : o.plane ? clamp(planeZ(o) + (o.depth || 4) / 2 / EYE, Z_NEAR, Z_FAR) : custom(o) ? customZ(o) : o.kind === 'bird' ? 30 : own(host(o) || o)),
605
- objectY,
606
- objectSize: o => (o.sky || o.ground ? 1 : custom(o) ? (o.height || 2) / EYE / sightZ(o) : o.size),
607
626
  };
608
627
  }
609
628
  // Screen transform [a b c d e f] for a plane at distance Z (Infinity = the sky): maps the
@@ -635,10 +654,11 @@ const Moood = (() => {
635
654
  return { m: [s, 0, 0, s, q[0] - s * ax, q[1] - s * ay], D: q[2] };
636
655
  }
637
656
  // Where an element's painting (as depths() places it on the SW-wide stage) goes on the frame from
638
- // camera cam: { m: the transform, D: its distance along the gaze }, or null behind the eye
657
+ // camera cam, where it is now: { m: the transform, D: its distance along the gaze }, or null behind the eye
639
658
  function placeOf(o, cam, W, H, dz, round) {
640
- if (!round) { const Z = dz.objectZ(o); return { m: view(cam, Z, W, H, dz.yh), D: Z - cam.z }; }
641
- return billboard(cam, dz.objectAt(o), dz.objectX(o) * STAGE * H, dz.objectY(o) * H, W, H, dz.yh);
659
+ const n = dz.now(o);
660
+ if (!round) return { m: view(cam, n.Z, W, H, dz.yh), D: n.Z - cam.z };
661
+ return billboard(cam, n.at, n.o.x * STAGE * H, n.o.y * H, W, H, dz.yh);
642
662
  }
643
663
  // The hero of a platformer h = { dx, dy (metres from where it is painted), flip } as a transform
644
664
  // of its painting: its anchor painted at x = ax, `ppm` painted px per metre
@@ -779,9 +799,12 @@ const Moood = (() => {
779
799
  if (L.kind === 'water') return { L, Z };
780
800
  return { L, Z, ...staticLayer(L, SW, H, seed + i * 101) };
781
801
  }).sort((a, b) => b.Z - a.Z);
782
- // looking around, each also stands somewhere in the world (p, depths().objectAt)
783
- const round = camSetup(spec).round;
784
- const objects = (spec.objects || []).map(o => ({ o: { ...o, x: dz.objectX(o), y: dz.objectY(o), size: dz.objectSize(o), _yh: dz.yh }, Z: dz.objectZ(o), p: round && !o.sky && !o.ground ? dz.objectAt(o) : null })).sort((a, b) => (b.Z - a.Z) || 0);
802
+ // looking around, each also stands somewhere in the world (p, depths().objectAt); what moves
803
+ // through the world (`place`) is put where the state has it as each frame starts (move)
804
+ const round = camSetup(spec).round, farFirst = (a, b) => (b.Z - a.Z) || 0;
805
+ const at = o => { const n = dz.now(o); return { o: n.o, Z: n.Z, p: round && !o.sky && !o.ground ? n.at : null }; };
806
+ const objects = (spec.objects || []).map(o => ({ from: o, ...at(o) })).sort(farFirst), moving = objects.filter(ob => ob.from.place);
807
+ const move = () => { if (!moving.length) return; for (const ob of moving) Object.assign(ob, at(ob.from)); objects.sort(farFirst); };
785
808
  const sky = !spec.sky ? '#000000' : (() => {
786
809
  const c = document.createElement('canvas').getContext('2d');
787
810
  const g = c.createLinearGradient(0, 0, 0, H);
@@ -795,7 +818,7 @@ const Moood = (() => {
795
818
  v.addColorStop(0, 'rgba(0,0,0,0)');
796
819
  v.addColorStop(1, `rgba(0,0,0,${0.75 * clamp(spec.light?.vignette ?? 0.5, 0, 1)})`);
797
820
  vg.fillStyle = v; vg.fillRect(0, 0, W, H);
798
- return { W, H, SW, u, x0, span, yh: dz.yh, round, lim: limits(spec, W, H), sky, stars, layers, particles, objects, vignette, scratch: layer(W, H) };
821
+ return { W, H, SW, u, x0, span, yh: dz.yh, round, lim: limits(spec, W, H), sky, stars, layers, particles, objects, move, vignette, scratch: layer(W, H) };
799
822
  }
800
823
 
801
824
  // Looking around, with Canvas 2D (the fallback): the sky turned as a flat band (not bent into a
@@ -852,6 +875,7 @@ const Moood = (() => {
852
875
  // S.out: where the frame lies in the canvas (pixels); the scene goes on around it as painted
853
876
  function frame(ctx, S, spec, cam, t, dt) {
854
877
  const { W, H, SW, u } = S, out = S.out;
878
+ S.move();
855
879
  ctx.setTransform(1, 0, 0, 1, 0, 0);
856
880
  ctx.fillStyle = '#000000';
857
881
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -1048,7 +1072,7 @@ const Moood = (() => {
1048
1072
  const pick = (clientX, clientY) => {
1049
1073
  const [cx, cy] = local(clientX, clientY), Hc = box.h, Wc = box.w, yh = S?.yh ?? 0.6, dz = spec ? depths(spec) : null;
1050
1074
  const at = D => ({ Z: target.z + D, x: target.x + (cx - Wc / 2) / Hc * D });
1051
- const near = (spec?.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane).map(o => ({ o: { ...o, x: dz.objectX(o), y: dz.objectY(o), size: dz.objectSize(o), _yh: yh }, Z: dz.objectZ(o) }))
1075
+ const near = (spec?.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane).map(o => dz.now(o))
1052
1076
  .filter(({ Z }) => fadeNear(Z, target) > 0).sort((a, b) => a.Z - b.Z);
1053
1077
  for (const { o, Z } of near) {
1054
1078
  const m = view(target, Z, Wc, Hc, yh), b = objectBox(o, STAGE * Hc, Hc);
@@ -1114,32 +1138,31 @@ const Moood = (() => {
1114
1138
  glide.vy += ((target.y - y0) / dt - glide.vy) * k;
1115
1139
  lastMove = now;
1116
1140
  };
1117
- // where in the canvas (CSS px) an element is from camera c — the point its code marks (e.at),
1118
- // else its anchor (its top, with over) — lifted by `lift` metres: [x, y, its distance]; null
1119
- // when out of sight
1141
+ // where in the canvas (CSS px) an element is from camera c — where its `place` has it now: the
1142
+ // point its code marks (e.at), else its anchor (its top, with over) — lifted by `lift` metres:
1143
+ // [x, y, its distance]; null when out of sight
1120
1144
  function seenFrom(c, id, lift = 0, over = false) {
1121
1145
  const o = (spec?.objects || []).find(o => o.id === id);
1122
1146
  if (!o || o.sky) return null;
1123
1147
  const W = box.w, H = box.h, dz = depths(spec), yh = dz.yh;
1124
- if (round()) {
1125
- // looking around: its anchor (or the point its code marks) seen from where you stand
1126
- const p = dz.objectAt(o);
1127
- if (o.plane || !FOCUS.has(id)) {
1128
- const q = project(c, p.X, p.Y + (lift + (over && !o.plane ? o.height ?? EYE : 0)) / EYE, p.Z, W, H, yh);
1148
+ if (o.plane) {
1149
+ const p = dz.objectAt(o), Z = p.Zp, up = lift / EYE;
1150
+ if (round()) {
1151
+ const q = project(c, p.X, p.Y + up, p.Z, W, H, yh);
1129
1152
  return q && q[2] > 0.3 ? [box.x + q[0], box.y + q[1], q[2]] : null;
1130
1153
  }
1131
- const b = billboard(c, p, dz.objectX(o) * STAGE * H, dz.objectY(o) * H, W, H, yh), f = FOCUS.get(id);
1132
- if (!b || b.D < 0.3) return null;
1133
- return [box.x + b.m[0] * f.x * STAGE * H + b.m[4], box.y + b.m[3] * (f.y * H - (lift / EYE) * H / p.Zp) + b.m[5], b.D];
1154
+ if (fadeNear(Z, c) < 0.3) return null;
1155
+ const m = view(c, Z, W, H, yh), px = dz.objectX(o) * STAGE * H, py = (yh + (1 - (o.elevation || 0) / EYE) / Z - up / Z) * H;
1156
+ return [box.x + m[0] * px + m[4], box.y + m[3] * py + m[5], Z - c.z];
1134
1157
  }
1135
- const Z = o.plane ? (o.distance > 0 ? o.distance : 10) / EYE : dz.objectZ(o);
1136
- if (Z - c.z < 0.3 || fadeNear(Z, c) < 0.3) return null;
1137
- const f = FOCUS.get(id) || { x: dz.objectX(o), y: o.plane ? yh + (1 - (o.elevation || 0) / EYE) / Z : dz.objectY(o) };
1158
+ const at = placeOf(o, c, W, H, dz, round());
1159
+ // behind the eye, or faded out as it is passed (fadeNear)
1160
+ if (!at || at.D < 0.48) return null;
1161
+ const n = dz.now(o), [ax, ay, ppm] = anchorOf(n.o, STAGE * H, H), f = FOCUS.get(id) || { x: 0, y: 0 };
1138
1162
  if (over && !FOCUS.has(id)) lift += o.height ?? EYE;
1139
- const m = view(c, Z, W, H, yh);
1140
- let px = f.x * STAGE * H, py = f.y * H - (lift / EYE) * H / Z;
1141
- if (c.hero && id === c.hero.id) { const hm = heroMatrix(c.hero, dz.objectX(o) * STAGE * H, dz.objectSize(o) * H / (o.height || 2)); px = hm[0] * px + hm[4]; py += hm[5]; }
1142
- return [box.x + m[0] * px + m[4], box.y + m[3] * py + m[5], Z - c.z];
1163
+ let px = ax + f.x * ppm, py = ay + f.y * ppm - lift / EYE * H / n.at.Zp;
1164
+ if (c.hero && id === c.hero.id) { const hm = heroMatrix(c.hero, ax, ppm); px = hm[0] * px + hm[4]; py += hm[5]; }
1165
+ return [box.x + at.m[0] * px + at.m[4], box.y + at.m[3] * py + at.m[5], at.D];
1143
1166
  }
1144
1167
  // the camera keeps the elements it follows in the visible part of the screen: together when
1145
1168
  // they fit, else the first of them
@@ -1 +1 @@
1
- {"engine":"717ecc3e74f1"}
1
+ {"engine":"ff145eec9cf9"}
package/engine/gl.js CHANGED
@@ -453,7 +453,7 @@ const MooodGL = (() => {
453
453
  const objects = (spec.objects || []).map(o0 => {
454
454
  if (o0.plane) return planeOf(o0, dz, tex);
455
455
  // looking around, the sky is the full circle, 2π stage heights wide (engine.js)
456
- const o = { ...o0, x: dz.objectX(o0), y: dz.objectY(o0), size: dz.objectSize(o0), _yh: dz.yh, _sw: ROUND && o0.sky ? I.TAU * H : SW }, Z = dz.objectZ(o0);
456
+ const n = dz.now(o0), o = { ...n.o, _sw: ROUND && o0.sky ? I.TAU * H : SW }, Z = n.Z;
457
457
  const x = o.x * SW, y = o.y * H, s = Math.max(2, o.size * H);
458
458
  if (o.kind === 'bird') {
459
459
  const canvas = paint(s * 1.2 * SS, s * 0.6 * SS, (g) => {
@@ -472,7 +472,7 @@ const MooodGL = (() => {
472
472
  const live = (o.kind === 'custom' || !o.kind) ? liveOf(o, o.frames, tex) : null;
473
473
  return {
474
474
  o, Z, x, y, s, bx, by, bw, bh, tex: frames[0], frames, period: o.period || 2, detail: null,
475
- p: ROUND && !o.sky ? dz.objectAt(o0) : null,
475
+ p: ROUND && !o.sky ? n.at : null,
476
476
  live, ground: !!o.ground, from: o0.place ? o0 : null,
477
477
  reflect: o.reflect ?? (o.kind === 'boat' || o.kind === 'bridge'),
478
478
  lights: lights.map(l => ({ x: l.x, y: l.y, r: l.r, c: col(l.col), a: l.a, dot: l.dot, fire: o.kind === 'campfire', flicker: l.flicker })),
@@ -873,22 +873,18 @@ const MooodGL = (() => {
873
873
  }
874
874
  }
875
875
 
876
- // An element that moves through the world (its `place`: {across, distance, elevation, x} — the
877
- // names of numbers in the state, metres): put where the state says now — its size, its depth,
878
- // its haze and what it is drawn in front of or behind follow.
876
+ // An element that moves through the world (its `place`): put where the state has it now
877
+ // (depths().now) — its size, its depth, its haze and what it is drawn in front of or behind follow.
879
878
  function placeMoving(S) {
880
- const v = I.STATE.values;
881
879
  let moved = false;
882
880
  for (const ob of S.objects) {
883
881
  if (!ob.from) continue;
884
- const o1 = { ...ob.from };
885
- for (const [f, k] of Object.entries(ob.from.place)) if (typeof k === 'string' && typeof v[k] === 'number') o1[f] = v[k];
886
- const key = `${o1.x}|${o1.across}|${o1.distance}|${o1.elevation}`;
882
+ const n = S.dz.now(ob.from), key = `${n.o.x}|${n.o.y}|${n.o.size}`;
887
883
  if (key === ob.placed) continue;
888
884
  ob.placed = key; moved = true;
889
- const o = { ...ob.o, x: S.dz.objectX(o1), y: S.dz.objectY(o1), size: S.dz.objectSize(o1) };
890
- Object.assign(ob, { o, Z: S.dz.objectZ(o1), x: o.x * SW, y: o.y * H, s: Math.max(2, o.size * H) }, I.objectBox(o, SW, H));
891
- if (ROUND && !o.sky) ob.p = S.dz.objectAt(o1);
885
+ const o = { ...n.o, _sw: ob.o._sw };
886
+ Object.assign(ob, { o, Z: n.Z, x: o.x * SW, y: o.y * H, s: Math.max(2, o.size * H) }, I.objectBox(o, SW, H));
887
+ if (ROUND && !o.sky) ob.p = n.at;
892
888
  }
893
889
  if (!moved) return;
894
890
  for (const item of S.order) if (item.ob) item.Z = item.ob.Z;
@@ -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>
@@ -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
+ })();
@@ -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; the title turns "ready" when it is time to take the picture. -->
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 })));
@@ -7,10 +7,11 @@
7
7
  // widgets — MooodState is global: get / set / on.
8
8
  // The painting code reads the current values as e.state and is repainted when they change. A
9
9
  // number may be set to move there over some seconds, along an easing curve (kit.js: smooth by
10
- // default — it starts and stops gently; linear, in, out, back, spring, anticipate…). What a value
11
- // was a moment ago stays known for a while (was): what lags behind — a coat's tail, hair, a
12
- // head that follows — reads it. A script may tell that something happened (scene.tell): the
13
- // game decides what it means.
10
+ // default — it starts and stops gently; linear, in, out, back, spring, anticipate…). A value set
11
+ // is heard once, where it is set to (on('change')); a value moving there is heard each frame as it
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 script may tell that something
14
+ // happened (scene.tell): the game decides what it means.
14
15
  const MooodState = (() => {
15
16
  let stage = null, tell = null, save = null, t0 = performance.now();
16
17
  let values = {}, cur = {}, tweens = new Map(), listeners = [], timers = [], raf = 0, pending = {}, flushTimer = 0;
@@ -44,12 +45,14 @@ const MooodState = (() => {
44
45
  }
45
46
 
46
47
  function publish() { cur = { ...values }; for (const [k, tw] of tweens) cur[k] = along(tw, performance.now()); stage?.setState(cur, was, sample); }
47
- // the moving values, where they are now (the stage asks as it paints a frame)
48
+ // the moving values, where they are now (the stage asks as it paints a frame): each heard moving
49
+ // from where it was last seen
48
50
  function sample() {
49
51
  if (!tweens.size) return;
50
- const now = performance.now();
52
+ const now = performance.now(), before = cur, moving = [...tweens.keys()];
51
53
  for (const [k, tw] of tweens) if (now >= tw.end) tweens.delete(k);
52
54
  publish();
55
+ for (const k of moving) if (cur[k] !== before[k]) emit('move', k, cur[k], before[k], k);
53
56
  }
54
57
  function tick() {
55
58
  raf = 0;
@@ -167,7 +170,9 @@ const MooodState = (() => {
167
170
  }
168
171
  return end;
169
172
  },
170
- // on('change', fn(value, old, key)) · on('change', key, fn) · on('load', fn(state))
173
+ // on('change', fn(value, old, key)) · on('change', key, fn) — a value set (with `over`: where it
174
+ // goes, once); on('move', [key,] fn(value, last, key)) — a value moving, each frame (set with
175
+ // `over`, path), till it stops; on('load', fn(state))
171
176
  on(ev, a, b) {
172
177
  const fn = typeof a === 'function' ? a : b, key = typeof a === 'function' ? null : String(a);
173
178
  listeners.push({ ev, key, fn, who });
package/engine/select.js CHANGED
@@ -51,12 +51,12 @@ const MooodSelect = (() => {
51
51
  // so a plane's view() maps painted coordinates straight to where they are in the frame;
52
52
  // the frame lies at (x, y) in the canvas.
53
53
  const size = () => { const f = stage.frame(); return { W: f.w, H: f.h, SW: I.STAGE * f.h, x: f.x, y: f.y }; };
54
- // objects stand where the engine puts them (it may move them to their real distance)
55
- const placed = (dz, o) => ({ ...o, x: dz.objectX(o), y: dz.objectY(o), size: dz.objectSize(o), _yh: dz.yh });
54
+ // objects stand where the engine puts them now (its depths().now: their real distance, their `place`)
55
+ const placed = (dz, o) => dz.now(o).o;
56
56
  // a platformer's hero is drawn moved and turned (engine.js heroMatrix): so is its outline
57
57
  function heroed(m, o, cam, SW, H) {
58
58
  if (!o || !cam.hero || o.id !== cam.hero.id) return m;
59
- const dz = I.depths(spec), h = I.heroMatrix(cam.hero, dz.objectX(o) * SW, dz.objectSize(o) * H / (o.height || 2));
59
+ const p = placed(I.depths(spec), o), h = I.heroMatrix(cam.hero, p.x * SW, p.size * H / (o.height || 2));
60
60
  return [m[0] * h[0], 0, 0, m[3], m[0] * h[4] + m[4], m[3] * h[5] + m[5]];
61
61
  }
62
62
  const layerSeed = i => I.hash(JSON.stringify(spec.layers || []) + (spec.title || '')) + i * 101;
@@ -125,7 +125,7 @@ const MooodSelect = (() => {
125
125
  const c = I.layer(b.bw * k, b.bh * k), g = c.getContext('2d');
126
126
  g.setTransform(k, 0, 0, k, -b.bx * k, -b.by * k);
127
127
  I.drawObject(g, o, SW, H, 0, []);
128
- return { c, x: b.bx, y: b.by, k, Z: dz.objectZ(it), obj: it };
128
+ return { c, x: b.bx, y: b.by, k, Z: dz.now(it).Z, obj: it };
129
129
  }
130
130
  if (sel.startsWith('layers') && LAND.has(it.kind)) {
131
131
  if (it.kind === 'water') {
@@ -239,7 +239,7 @@ const MooodSelect = (() => {
239
239
  if (!coverage.has(path)) {
240
240
  const { W, H, SW } = size(), dz = I.depths(spec), it = item(spec, path), n = 64, c = I.layer(n, n * H / W), g = c.getContext('2d');
241
241
  // from the start — looking around, turned toward it
242
- const round = I.camSetup(spec).round, p = round && !it.sky ? dz.objectAt(it) : null;
242
+ const round = I.camSetup(spec).round, p = round && !it.sky ? dz.now(it).at : null;
243
243
  const at = I.placeOf(it, { x: 0, y: 0, z: 0, h: 1, yaw: p ? Math.atan2(p.X, p.Z) : 0 }, W, H, dz, round && !it.sky), k = n / W;
244
244
  const m = at ? at.m : [0, 0, 0, 0, 0, 0];
245
245
  g.setTransform(m[0] * k, 0, 0, m[3] * k, m[4] * k, m[5] * k);
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.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.",
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",