@moood/render 0.1.4 → 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/engine/kit.js CHANGED
@@ -9,7 +9,11 @@
9
9
  // uses (spec.modules) and gets them as kit.<name>, as they are at the commit it is shown at.
10
10
  // the scene's lib — code its own elements and scripts share: spec.lib, the body of
11
11
  // function (anim, kit) that returns what it offers.
12
- // Painting code sees them as e.anim, e.kit, e.lib; scripts as scene.anim, scene.kit, scene.lib.
12
+ // the scene's land — spec.relief, the body of function (across, distance, anim, lib) that returns
13
+ // the ground's height there (metres; across to the right of the start's line of sight, distance
14
+ // ahead of it): what stands, walks, casts a shadow and looks on it, all on the one ground.
15
+ // Painting code sees them as e.anim, e.kit, e.lib, e.ground; scripts as scene.anim, scene.kit,
16
+ // scene.lib, scene.ground.
13
17
  // Where a spec's modules come from (the page says, with the spec):
14
18
  // {world, refs} — the world's id and the modules by their content's sha ({name: {code, data}},
15
19
  // as the scene's payload has them: the commit it shows); a module the refs don't name (a draft
@@ -120,9 +124,29 @@ const MooodKit = (() => {
120
124
  return built(refs);
121
125
  });
122
126
  }
123
- // what a spec's code shares, once its modules are here: { kit, lib } — or a promise of it (they
124
- // are still on their way); `source`: where its modules come from (above). The same modules are
125
- // the same kit, and a lib on them is made once.
127
+ // the ground's height (metres) at (across, distance) as `relief` says — said once if it throws or
128
+ // returns no number (it is flat there then); null without a relief
129
+ const grounds = new Map();
130
+ function groundOf(relief, lib, key) {
131
+ if (!relief) return null;
132
+ if (!grounds.has(key)) {
133
+ let fn = null, said = false;
134
+ try { fn = new Function('across', 'distance', 'anim', 'lib', relief); } catch (err) { report('relief', err); }
135
+ const say = msg => { if (!said) { said = true; report('relief', msg); } };
136
+ grounds.set(key, fn && ((across, distance) => {
137
+ try {
138
+ const h = fn(across, distance, anim, lib);
139
+ if (Number.isFinite(h)) return h;
140
+ say(`at across ${across}, distance ${distance} it returned ${h}, not a number of metres`);
141
+ } catch (err) { say(err.message || err); }
142
+ return 0;
143
+ }));
144
+ }
145
+ return grounds.get(key);
146
+ }
147
+ // what a spec's code shares, once its modules are here: { kit, lib, ground } — or a promise of it
148
+ // (they are still on their way); `source`: where its modules come from (above). The same modules
149
+ // are the same kit, and a lib on them is made once.
126
150
  const kits = new Map();
127
151
  function shared(spec, source) {
128
152
  const done = ({ mods, key }) => {
@@ -133,10 +157,30 @@ const MooodKit = (() => {
133
157
  try { value = new Function('anim', 'kit', lib)(anim, kit) || {}; } catch (err) { report('lib', err); }
134
158
  libs.set(`${key}\n\n${lib}`, value);
135
159
  }
136
- return { kit, lib: lib ? libs.get(`${key}\n\n${lib}`) : {} };
160
+ const made = lib ? libs.get(`${key}\n\n${lib}`) : {}, relief = typeof spec?.relief === 'string' && spec.relief.trim() ? spec.relief : null;
161
+ return { kit, lib: made, ground: groundOf(relief, made, `${key}\n\n${lib}\n\n${relief}`) };
137
162
  };
138
163
  const m = modulesOf(spec, source);
139
164
  return m instanceof Promise ? m.then(done) : done(m);
140
165
  }
141
- return { anim, shared };
166
+ // The state keys the engine itself reads for a spec — whatever the scene's own code does: the film's
167
+ // camera and fade (every scene's), an insert's key, a place's, a redraw's, the light's looks, a
168
+ // light's `on` and place — each with what it takes, as spec.states describes a key ({range} or
169
+ // {type}; {} — anything). A key set from outside that neither these nor the scene knows, or one of
170
+ // these set to what the engine can't take, is said (scenestate.js).
171
+ const NUM = { type: 'number' }, UNIT = { range: [0, 1] };
172
+ const CAMERA = { camera_target: {}, camera_shot: {}, camera_frame: {}, camera_glide: NUM, camera_x: NUM, camera_z: NUM, camera_look: NUM, camera_h: NUM, fade: UNIT, fade_color: { type: 'string' } };
173
+ function keys(spec) {
174
+ const out = new Map(Object.entries(CAMERA)), obj = v => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
175
+ const add = (d, ...ks) => { for (const k of ks) if (typeof k === 'string') out.set(k, d); };
176
+ for (const o of Array.isArray(spec?.objects) ? spec.objects : []) {
177
+ if (!o || typeof o !== 'object') continue;
178
+ add(UNIT, o.insert); add(NUM, ...Object.values(obj(o.place)));
179
+ add({}, ...(Array.isArray(o.redraw) ? o.redraw : Object.keys(obj(o.redraw))));
180
+ }
181
+ for (const l of Array.isArray(spec?.lights) ? spec.lights : []) if (l && typeof l === 'object') { add(UNIT, l.on); add(NUM, ...Object.values(obj(l.place))); }
182
+ add(UNIT, ...Object.keys(obj(obj(spec?.light).looks)));
183
+ return out;
184
+ }
185
+ return { anim, shared, keys };
142
186
  })();
@@ -12,9 +12,12 @@
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, what its scripts told (scene.tell) in window.__mooodTells ({at:
15
+ window.__mooodRenderErrors, what it does wrong without failing (a name it does not know, a
16
+ mistyped key) in window.__mooodRenderWarnings, what its scripts told (scene.tell) in window.__mooodTells ({at:
16
17
  seconds since it started, text, about}); the title turns "ready" when it is time to take the
17
- picture. -->
18
+ picture. The stage is window.__mooodStage: a render steps the film unseen to the moments it
19
+ takes (draw(false)) and asks what is where (inspect()); it is checked as it is drawn (a painting cut
20
+ off by its box is said). ?debug — what is where, over the frame. -->
18
21
  <canvas id="scene"></canvas>
19
22
  <header class="titles"><h1 id="title"></h1><p id="caption"></p></header>
20
23
  <script src="kit.js"></script>
@@ -35,7 +38,7 @@
35
38
  document.getElementById('caption').textContent = spec.caption || '';
36
39
  }
37
40
  const modules = { texts: window.__mooodModules || {} }; // the modules the scene lists, given (kit.js)
38
- const canvas = document.getElementById('scene'), stage = Moood.mount(canvas);
41
+ const canvas = document.getElementById('scene'), stage = window.__mooodStage = Moood.mount(canvas, { check: true });
39
42
  // the starting state, scripts running (their errors are reported) — before the scene is shown, so its first frame is as they set it
40
43
  const t0 = performance.now(), told = window.__mooodTells = [];
41
44
  MooodState.start(spec, {}, { stage, modules, tell: (text, about) => told.push({ at: (performance.now() - t0) / 1000, text, about }) });
@@ -47,6 +50,7 @@
47
50
  await new Promise(r => setTimeout(r, 1500));
48
51
  errors.push(...(window.__mooodErrors || []));
49
52
  window.__mooodRenderErrors = errors;
53
+ window.__mooodRenderWarnings = [...(window.__mooodWarnings || []), ...MooodState.unknown()];
50
54
  document.title = 'ready';
51
55
  })();
52
56
  </script>
@@ -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 script may tell that something
14
- // happened (scene.tell): the game decides what it means.
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
- get state() { return cur; },
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
- turn: (key, degrees, over, ease) => turn(key, degrees, over, ease),
146
- path: (keys, points, opts) => path(keys, points, opts),
147
- was: (key, ago) => was(key, ago),
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, set, turn, path, was,
221
- get: key => cur[key],
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 > 0.48 && (!only || only(t.path))).sort((a, b) => a.at.D - b.at.D || b.k - a.k);
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/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
- const bubbles = new Map(); // element id → { el, text, until, pending }
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(id, text, a, bubble) {
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(id, text, voice, how, who) {
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(id, text, voice, how, who);
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.get(id)) pend(id); continue; }
94
- const at = id === NARRATOR ? [innerWidth / 2, innerHeight * 0.86] : stage.screenOf(id, 0.25, true);
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(id) {
104
- const b = bubbleFor(id, '…');
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(id, on) {
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(id);
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); voice/how: heard in that voice, that way, played as
117
- // `who` ("Savely, the old keeper"). A line with
118
- // a voice appears when it can be heard — its words and its voice together; its voice is made
119
- // while the lines before it are said.
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;