@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/README.md +65 -10
- package/cli.js +287 -37
- 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/render.html +7 -3
- package/engine/scenestate.js +127 -15
- package/engine/select.js +4 -3
- package/engine/speech.js +23 -19
- package/index.js +105 -33
- package/package.json +4 -2
- package/scene.js +28 -12
- package/video.js +66 -0
package/engine/engine.js
CHANGED
|
@@ -210,7 +210,8 @@ const Moood = (() => {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
// W × H: the stage, in pixels
|
|
213
|
-
function drawObject(ctx, o, W, H, t, lightsOut) {
|
|
213
|
+
function drawObject(ctx, o, W, H, t, lightsOut, partsOut) {
|
|
214
|
+
const t0 = clock();
|
|
214
215
|
const x = o.x * W, y = o.y * H, s = o.size * H, col = o.color;
|
|
215
216
|
const light = o.light ? rgb(o.light) : null, fl = 0.9 + 0.1 * Math.sin(t * 7 + x) * Math.sin(t * 3.1 + y);
|
|
216
217
|
ctx.fillStyle = ctx.strokeStyle = col;
|
|
@@ -333,43 +334,101 @@ const Moood = (() => {
|
|
|
333
334
|
for (let i = -4; i <= 4; i++) { const u = i / 4.5, yy = y - s * 0.45 * (1 - u * u) * 1.0; ctx.beginPath(); ctx.moveTo(x + u * s * 1.4, yy); ctx.lineTo(x + u * s * 1.4, yy - s * 0.22); ctx.stroke(); }
|
|
334
335
|
break;
|
|
335
336
|
}
|
|
336
|
-
case 'custom': case undefined: paintCustom(ctx, o, W, H, t, lightsOut); break;
|
|
337
|
+
case 'custom': case undefined: paintCustom(ctx, o, W, H, t, lightsOut, partsOut); break;
|
|
337
338
|
default: throw new Error(`unknown object kind ${o.kind}`);
|
|
338
339
|
}
|
|
340
|
+
PERF.add(o, clock() - t0);
|
|
339
341
|
}
|
|
340
342
|
|
|
341
343
|
// Elements the world's agent paints itself. `o.code` is the body of a function (g, e):
|
|
342
344
|
// g is a 2D canvas context in metres — origin at the element's anchor (where it stands),
|
|
343
345
|
// x to the right, y DOWN (a 6 m house spans y −6…0). e.t = seconds by the real clock (e.phase =
|
|
344
346
|
// 0…1 of the loop for animated elements), e.id = the element's id, e.rnd() = seeded random, e.px = one screen pixel in metres,
|
|
345
|
-
// e.light(x, y, radius, color, strength) = a glowing light the engine animates
|
|
346
|
-
//
|
|
347
|
+
// e.light(x, y, radius, color, strength, name) = a glowing light the engine animates — and a source
|
|
348
|
+
// of light the scene knows (below: lights), by the element's id, or 'id#name'; e.lights(x, y) = the
|
|
349
|
+
// light falling on a point of the painting, from where and how strong (lightsAt); e.ambient = the
|
|
350
|
+
// scene's ambient light now. What scenes share (kit.js): e.anim — the animator's toolkit; e.kit — the world's modules; e.lib — the scene's
|
|
347
351
|
// lib. e.was(key, ago) — what a value of the state was `ago` seconds ago (what lags behind reads
|
|
348
|
-
// it); e.speed(key) — how fast it is changing now (per second).
|
|
352
|
+
// it); e.speed(key) — how fast it is changing now (per second); e.cut — when the last cut was
|
|
353
|
+
// (scenestate.js: scene.cut, or the scene's start), by e.t's clock: what keeps its own inertia (a
|
|
354
|
+
// module's springs, cloth, hair) starts afresh after it. e.debug(value, x?, y?) — a note shown
|
|
355
|
+
// only to whoever looks closely (?debug, a render's --debug and --json); e.warn(text) — something
|
|
356
|
+
// wrong but not an error (a pose, a name it does not know), said once in the scene's errors.
|
|
349
357
|
// Errors are collected (window.__mooodErrors) so the agent reviewing its work sees them.
|
|
350
358
|
// The scene's state (scenestate.js): values the scene's scripts, agents and widgets set, which
|
|
351
359
|
// the painting code reads as e.state; ver counts changes, so the renderer knows to repaint.
|
|
352
|
-
const STATE = { values: {}, ver: 0, was: key => STATE.values[key], sample: null };
|
|
353
|
-
//
|
|
354
|
-
|
|
360
|
+
const STATE = { values: {}, ver: 0, was: key => STATE.values[key], sample: null, cut: 0, shots: 0, read: new Set() };
|
|
361
|
+
// the state as the painting code reads it (e.state; a module's beforeFrame too): each key it reads is
|
|
362
|
+
// kept (STATE.read, since the scene was shown) — a key set from outside that nothing in the scene
|
|
363
|
+
// reads is said (scenestate.js) — and, by whoever reads it (`keys`: an element's painting, or
|
|
364
|
+
// BEFORE: the modules' beforeFrame), with its value then: what it is repainted for (moved). Listing
|
|
365
|
+
// the keys (Object.keys, for…in) reads them all.
|
|
366
|
+
// (a key kept once per painting: a read after the first costs a lookup)
|
|
367
|
+
const keep = (keys, v, k) => { if (typeof k === 'string' && !keys.has(k)) { keys.set(k, v[k]); STATE.read.add(k); } };
|
|
368
|
+
const stateRead = keys => new Proxy(STATE.values, {
|
|
369
|
+
get: (v, k) => { keep(keys, v, k); return v[k]; },
|
|
370
|
+
has: (v, k) => { keep(keys, v, k); return k in v; },
|
|
371
|
+
ownKeys: v => { keys.set(ALL, STATE.ver); return Reflect.ownKeys(v); },
|
|
372
|
+
});
|
|
373
|
+
const ALL = Symbol('all');
|
|
374
|
+
// the keys the modules' beforeFrame read (since the scene was shown): what an element painting
|
|
375
|
+
// through them (e.kit, e.lib) depends on as well
|
|
376
|
+
const BEFORE = new Map();
|
|
377
|
+
// whether the state an element's painting read (READS: read.keys — through the modules too: read.kit,
|
|
378
|
+
// how many keys their beforeFrame had read then) has changed since it was painted
|
|
379
|
+
function moved(read) {
|
|
380
|
+
if (!read || read.kit < BEFORE.size) return true;
|
|
381
|
+
for (const [k, was] of read.keys) if (k === ALL ? was !== STATE.ver : !Object.is(STATE.values[k], was)) return true;
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
// what the shown scene's code shares (kit.js): the world's modules it lists, the scene's lib and
|
|
385
|
+
// its land (ground: (across, distance) → metres, or null — flat)
|
|
386
|
+
let SHARED = { kit: {}, lib: {}, ground: null };
|
|
387
|
+
const groundAt = (across, distance) => (SHARED.ground ? SHARED.ground(across, distance) : 0);
|
|
355
388
|
// Where an element is now, as its painting code says (e.at): the point it marks, in metres from
|
|
356
389
|
// its anchor as painted — speech bubbles and widgets follow it (stage.screenOf) wherever the
|
|
357
|
-
// element is put (its `place`)
|
|
358
|
-
|
|
390
|
+
// element is put (its `place`). FOCUS: its own point (the head, say) — it stays till marked
|
|
391
|
+
// again; NAMED: the points it names (e.at(x, y, 'window')) — as its last painting marked them, so
|
|
392
|
+
// one it no longer marks is gone. A point in one of its parts (e.part) carries the part's offset.
|
|
393
|
+
const FOCUS = new Map(), NAMED = new Map();
|
|
359
394
|
// A platformer's hero as it moves now (camera.mode platform): its painting code reads e.hero —
|
|
360
395
|
// { vx, vy (m/s), ground } — to move its legs; the engine moves and turns the painting itself.
|
|
361
396
|
const HERO = { id: null, vx: 0, vy: 0, ground: true };
|
|
362
|
-
// What an element's code read as it was last painted (element → {time, state
|
|
363
|
-
//
|
|
364
|
-
//
|
|
397
|
+
// What an element's code read as it was last painted (element → {time, state, keys, kit, lights},
|
|
398
|
+
// and how many parts it painted): one that reads the time is repainted every frame, one that reads
|
|
399
|
+
// only the state (or the light falling on it) when a key it read changes (moved), one that reads
|
|
400
|
+
// neither stays as painted (gl.js) — whether it reads them itself or through the kit
|
|
365
401
|
const READS = new WeakMap();
|
|
402
|
+
// What looking closely at a scene shows (?debug, a render's --debug and --json), by element (its
|
|
403
|
+
// id, else its name): NOTES — what its code wrote as it was last painted (e.debug: a value, and the
|
|
404
|
+
// point it is about), PARTS — the parts it painted (e.part: dx, dz, box); what its painting cost
|
|
405
|
+
// and what it read are PERF's (below).
|
|
406
|
+
const NOTES = new Map(), PARTS = new Map(), keyOf = o => o.id ?? o.name;
|
|
407
|
+
// the real clock, for what costs are timed by: a render stops the page's own (window.__mooodClock:
|
|
408
|
+
// the clock before it did)
|
|
409
|
+
const clock = window.__mooodClock || (() => performance.now());
|
|
366
410
|
const compiled = new Map();
|
|
411
|
+
// what the scene does wrong without failing (a name it does not know, a value it can't take — e.warn's
|
|
412
|
+
// too), said once (window.__mooodWarnings); what its code throws is an error (window.__mooodErrors)
|
|
413
|
+
const warned = new Set();
|
|
414
|
+
// the scene checked as it is drawn — in a render (mount's check) and with ?debug: what costs a
|
|
415
|
+
// little each frame to find (gl.js: a painting cut off by its box) is looked for only then
|
|
416
|
+
const CHECK = { on: false };
|
|
417
|
+
function warn(msg) {
|
|
418
|
+
if (warned.has(msg)) return;
|
|
419
|
+
warned.add(msg);
|
|
420
|
+
(window.__mooodWarnings = window.__mooodWarnings || []).push(msg);
|
|
421
|
+
console.warn('moood:', msg);
|
|
422
|
+
}
|
|
367
423
|
function reportError(o, err) {
|
|
368
|
-
const msg = `${o.name || o.kind}: ${err && err.message || err}`;
|
|
424
|
+
const msg = `${o.name || o.id || o.kind}: ${err && err.message || err}`;
|
|
369
425
|
(window.__mooodErrors = window.__mooodErrors || []).push(msg);
|
|
370
426
|
console.warn('moood custom element:', msg);
|
|
371
427
|
}
|
|
372
|
-
|
|
428
|
+
// partsOut: where the parts it paints (e.part) go, each to be painted in a canvas of its own at its
|
|
429
|
+
// own depth — { dx, dz, box, paint(g, ax, ay, ppm, lights) }; without it (a probe, the picking)
|
|
430
|
+
// they are painted in place, as seen from the start
|
|
431
|
+
function paintCustom(ctx, o, W, H, t, lightsOut, partsOut) {
|
|
373
432
|
const code = String(o.code || '');
|
|
374
433
|
let fn = compiled.get(code);
|
|
375
434
|
if (fn === undefined) {
|
|
@@ -384,48 +443,128 @@ const Moood = (() => {
|
|
|
384
443
|
ctx.fillStyle = ctx.strokeStyle = o.color || '#000000';
|
|
385
444
|
ctx.lineCap = ctx.lineJoin = 'round';
|
|
386
445
|
const period = o.period || 2;
|
|
446
|
+
// what is painting now: the element itself, or one of its parts — its canvas, its anchor (x, y,
|
|
447
|
+
// ppm on the stage) and the transform there, where its lights go, the part's offset
|
|
448
|
+
const own = (g, ax, ay, k, lights, part) => { const base = g.getTransform(); return { g, x: ax, y: ay, ppm: k, base, unbase: base.inverse(), lights, part }; };
|
|
449
|
+
let cur = own(ctx, x, y, ppm, lightsOut, null);
|
|
387
450
|
// a point the code names, in its units from the anchor as painted — through whatever it did to
|
|
388
451
|
// the canvas since (translate, scale, rotate): [x, y, how much it is scaled]
|
|
389
|
-
const base = ctx.getTransform(), unbase = base.inverse();
|
|
390
452
|
const painted = (lx, ly) => {
|
|
391
|
-
const now =
|
|
392
|
-
return [q.x, q.y, Math.hypot(now.a, now.b) / Math.hypot(base.a, base.b)];
|
|
453
|
+
const now = cur.g.getTransform(), q = cur.unbase.transformPoint(now.transformPoint({ x: lx, y: ly }));
|
|
454
|
+
return [q.x, q.y, Math.hypot(now.a, now.b) / Math.hypot(cur.base.a, cur.base.b)];
|
|
393
455
|
};
|
|
394
|
-
const read = { time: false, state: false };
|
|
456
|
+
const read = { time: false, state: false, keys: new Map(), kit: Infinity, lights: false }, key = keyOf(o);
|
|
457
|
+
let state = null;
|
|
458
|
+
// a key read through e.was, e.speed, e.spot (its place), e.ambient (the light's looks)
|
|
459
|
+
const reads = (...ks) => { read.state = true; for (const k of ks) { STATE.read.add(k); if (!read.keys.has(k)) read.keys.set(k, STATE.values[k]); } };
|
|
460
|
+
// (an insert is painted through the iris its key opens: gl.js)
|
|
461
|
+
if (typeof o.insert === 'string') reads(o.insert);
|
|
462
|
+
// what the modules' beforeFrame read, through the kit or the lib
|
|
463
|
+
const throughKit = () => { if (read.kit !== Infinity || !BEFORE.size) return; read.state = true; read.kit = BEFORE.size; for (const k of BEFORE.keys()) if (!read.keys.has(k)) read.keys.set(k, k === ALL ? STATE.ver : STATE.values[k]); };
|
|
395
464
|
READS.set(o, read);
|
|
465
|
+
NOTES.delete(key);
|
|
466
|
+
const parts = [], named = new Map();
|
|
467
|
+
if (o.id) NAMED.set(o.id, named);
|
|
396
468
|
const e = {
|
|
397
469
|
get t() { read.time = true; return t; },
|
|
398
470
|
get phase() { read.time = true; return ((t / period) % 1 + 1) % 1; },
|
|
399
|
-
get state() { read.state = true; return
|
|
471
|
+
get state() { read.state = true; return state ||= stateRead(read.keys); },
|
|
400
472
|
id: o.id ?? null, rnd: seeded(hash(code) ^ hash(o.x + ':' + o.y)),
|
|
401
|
-
px
|
|
402
|
-
light(lx, ly, r, color = '#ffd08a', strength = 0.6) {
|
|
473
|
+
get px() { return 1 / Math.max(1e-6, cur.base.a); }, W, H, aspect: W / H,
|
|
474
|
+
light(lx, ly, r, color = '#ffd08a', strength = 0.6, name) {
|
|
403
475
|
try {
|
|
404
476
|
const [px, py, k] = painted(lx, ly);
|
|
405
|
-
|
|
477
|
+
cur.lights.push({ x: cur.x + px * cur.ppm, y: cur.y + py * cur.ppm, r: r * k * cur.ppm, col: rgb(color), a: strength, flicker: o.flicker ?? 0.3, id: o.id == null ? null : name == null ? String(o.id) : `${o.id}#${name}` });
|
|
406
478
|
} catch (err) { reportError(o, err); }
|
|
407
479
|
},
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
480
|
+
// the light falling on a point of the painting: [{ id, color, strength, x, y, z, dir, distance }]
|
|
481
|
+
lights(lx, ly) {
|
|
482
|
+
read.lights = true;
|
|
483
|
+
const [px, py] = painted(lx, ly);
|
|
484
|
+
return lightsAt(worldOf(o, px, py, cur.part));
|
|
485
|
+
},
|
|
486
|
+
get ambient() { reads(...Object.keys(LOOK.spec?.light?.looks || {})); return LOOK.now?.ambient ?? 1; },
|
|
487
|
+
// where this element is now (its head, say): what speaks or hovers over it follows this point;
|
|
488
|
+
// with a name, one of its points the game and the camera may aim at ('window', 'head', 'horn')
|
|
489
|
+
at(lx, ly, name) {
|
|
490
|
+
if (!o.id) return;
|
|
491
|
+
const [px, py] = painted(lx, ly), p = { x: px, y: py, part: cur.part };
|
|
492
|
+
if (name == null) FOCUS.set(o.id, p); else named.set(String(name), p);
|
|
493
|
+
},
|
|
494
|
+
// a part of it standing dx metres to the right and dz farther than its anchor (on the ground
|
|
495
|
+
// there): painted by paint(g, e) in metres from its own foot, at its own depth — what stands
|
|
496
|
+
// between is drawn between (a fence along a road, a crowd, figures one behind another)
|
|
497
|
+
part(dx, dz, paint, box) {
|
|
498
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dz) || typeof paint !== 'function') { reportError(o, new Error('e.part(dx, dz, paint, box?): metres to the right, metres farther, a function painting it')); return; }
|
|
499
|
+
if (box !== undefined && !(Array.isArray(box) && box.length === 4 && box.every(Number.isFinite))) { reportError(o, new Error('e.part: box is [left, top, right, bottom], metres around its foot')); box = undefined; }
|
|
500
|
+
parts.push({ dx, dz, box, paint });
|
|
501
|
+
},
|
|
502
|
+
// the ground's height (metres) at (across, distance) — spec.relief, 0 without one
|
|
503
|
+
ground: groundAt,
|
|
504
|
+
// where it stands now (metres): across, distance, its elevation and the ground's under it
|
|
505
|
+
get spot() { if (o.place) reads(...Object.values(o.place).filter(k => typeof k === 'string')); return o._at ? { ...o._at } : null; },
|
|
506
|
+
// a note for whoever looks closely (?debug): text or a value (a module's poses and their
|
|
507
|
+
// weights), about the point (lx, ly) if given — shown by the element; nothing otherwise
|
|
508
|
+
debug(value, lx, ly) {
|
|
509
|
+
if (key == null) return;
|
|
510
|
+
let at = null;
|
|
511
|
+
try { if (typeof lx === 'number' && typeof ly === 'number') { const [px, py] = painted(lx, ly); at = { x: px, y: py }; } } catch (err) { reportError(o, err); }
|
|
512
|
+
(NOTES.get(key) || NOTES.set(key, []).get(key)).push({ value, at });
|
|
513
|
+
},
|
|
514
|
+
// something wrong but not an error — a name it does not know (a pose, a figure) — said once
|
|
515
|
+
warn(text) { warn(`${o.name || o.id || o.kind}: ${text}`); },
|
|
516
|
+
anim: MooodKit.anim,
|
|
517
|
+
get kit() { throughKit(); return SHARED.kit; },
|
|
518
|
+
get lib() { throughKit(); return SHARED.lib; },
|
|
519
|
+
was: (key, ago = 0) => { reads(String(key)); return STATE.was(key, ago); },
|
|
520
|
+
speed: key => { reads(String(key)); const a = STATE.was(key, 0), b = STATE.was(key, 0.05); return typeof a === 'number' && typeof b === 'number' ? (a - b) / 0.05 : 0; },
|
|
521
|
+
cut: STATE.cut,
|
|
413
522
|
};
|
|
414
523
|
if (o.id && o.id === HERO.id) { read.time = true; e.hero = { vx: HERO.vx, vy: HERO.vy, ground: HERO.ground }; }
|
|
415
524
|
try { fn(ctx, e); } catch (err) { reportError(o, err); }
|
|
416
525
|
ctx.restore();
|
|
526
|
+
// its parts: each painted where the renderer puts it — or here, in place as seen from the start
|
|
527
|
+
// (u, v: metres in its own plane, from its anchor)
|
|
528
|
+
const run = part => (g, ax, ay, k, lights) => {
|
|
529
|
+
g.save(); g.translate(ax, ay); g.scale(k, k);
|
|
530
|
+
g.fillStyle = g.strokeStyle = o.color || '#000000';
|
|
531
|
+
g.lineCap = g.lineJoin = 'round';
|
|
532
|
+
const was = cur;
|
|
533
|
+
cur = own(g, ax, ay, k, lights, { dx: part.dx, dz: part.dz });
|
|
534
|
+
try { part.paint(g, e); } catch (err) { reportError(o, err); }
|
|
535
|
+
cur = was;
|
|
536
|
+
g.restore();
|
|
537
|
+
};
|
|
538
|
+
read.parts = parts.length;
|
|
539
|
+
if (key != null) { if (parts.length) PARTS.set(key, parts.map(q => ({ dx: q.dx, dz: q.dz, box: q.box }))); else PARTS.delete(key); }
|
|
540
|
+
for (const part of parts) {
|
|
541
|
+
if (partsOut) { partsOut.push({ dx: part.dx, dz: part.dz, box: part.box, paint: run(part) }); continue; }
|
|
542
|
+
const q = inPlace(o, part.dx, part.dz);
|
|
543
|
+
if (q) run(part)(ctx, x + q.u * ppm, y + q.v * ppm, q.s * ppm, lightsOut);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// A part (dx, dz) of element o painted in o's own plane as seen from the start — its foot u, v
|
|
547
|
+
// metres from o's anchor, its scale s — or null (o is not where it could have parts)
|
|
548
|
+
function inPlace(o, dx, dz) {
|
|
549
|
+
const at = o._at;
|
|
550
|
+
if (!at || !(at.distance > 0) || !(at.distance + dz > 0.05)) return null;
|
|
551
|
+
const s = at.distance / (at.distance + dz), feet = at.elevation - at.ground + groundAt(at.across + dx, at.distance + dz);
|
|
552
|
+
return { u: (at.across + dx) * s - at.across, v: (EYE - feet) * s - (EYE - at.elevation), s };
|
|
417
553
|
}
|
|
418
554
|
// 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
|
|
420
|
-
//
|
|
555
|
+
// unit]. Sky, ground and insert elements are painted in stage heights from the stage's top-left
|
|
556
|
+
// corner (x 0…W/H, y 0…1, with margins; the sky and the ground as seen from the start); others in
|
|
557
|
+
// metres from their anchor.
|
|
421
558
|
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)];
|
|
559
|
+
return o.sky || o.ground || o.insert ? [0, 0, H] : [o.x * W, o.y * H, o.size * H / (o.height || 2)];
|
|
423
560
|
}
|
|
424
561
|
// The box an object's painting fits in (pixels of the W × H stage): built-in kinds a fixed frame
|
|
425
562
|
// around the base, custom ones their `box` in metres (default: a little wider than tall).
|
|
426
563
|
function objectBox(o, W, H) {
|
|
427
564
|
// the sky: as wide as the widest frame
|
|
428
565
|
if (o.sky) return { bx: (W - WIDEST * H) / 2, by: -0.35 * H, bw: WIDEST * H, bh: 1.6 * H };
|
|
566
|
+
// an insert: the frame itself, as wide as the widest
|
|
567
|
+
if (o.insert) return { bx: (W - WIDEST * H) / 2, by: 0, bw: WIDEST * H, bh: H };
|
|
429
568
|
// ground: from the horizon down, wider than the screen (for stepping aside and looking down)
|
|
430
569
|
if (o.ground) return { bx: -EXT * W, by: o._yh * H, bw: (1 + 2 * EXT) * W, bh: (1.7 - o._yh) * H };
|
|
431
570
|
const s = Math.max(2, o.size * H), x = o.x * W, y = o.y * H;
|
|
@@ -456,6 +595,329 @@ const Moood = (() => {
|
|
|
456
595
|
ctx.restore();
|
|
457
596
|
}
|
|
458
597
|
|
|
598
|
+
// ── the scene's light ──────────────────────────────────────────────────
|
|
599
|
+
// spec.light as the state has it now: the grade of the whole picture — tint, tintStrength,
|
|
600
|
+
// vignette, grain, haze, hazeColor, brightness (×), saturation (×) — and, for a lit scene, ambient
|
|
601
|
+
// (0…1: how much of its painted colour a thing keeps where no light falls on it) and ambientColor.
|
|
602
|
+
// `looks`: { key: fields } — a state value `key` (0…1) mixes the light toward that look, in the
|
|
603
|
+
// order they are listed (night over the day, dawn over the night): a script moves the key with
|
|
604
|
+
// `over`, the light changes softly. A scene is lit — its light lies on what is painted, by how far
|
|
605
|
+
// each thing is from each source (gl.js) — as much as it or a look of it gives `ambient` now: `lit`
|
|
606
|
+
// 1 when the light itself has it, else the weight of the most of the looks that do (0: not lit —
|
|
607
|
+
// its sources come in as their look does, from nothing, so it never jumps). hazeGlow (0 unset): the
|
|
608
|
+
// haze lit by the point sources (airOf).
|
|
609
|
+
// haze 0…1 → how far (eye heights) the air hides a thing to 1/e; unset: clear air, 220
|
|
610
|
+
const hazeReach = h => 600 * Math.pow(25 / 600, clamp(h, 0, 1));
|
|
611
|
+
const LOOK_NUM = { tintStrength: 0, vignette: 0.5, grain: 0.5, haze: Math.log(220 / 600) / Math.log(25 / 600), brightness: 1, saturation: 1, ambient: 1, hazeGlow: 0 };
|
|
612
|
+
const LOOK_COL = ['tint', 'hazeColor', 'ambientColor'];
|
|
613
|
+
const LOOK = { spec: null, ver: -1, now: null };
|
|
614
|
+
// "#rrggbb" → [r, g, b] 0…255, or null (said)
|
|
615
|
+
function colourOf(hex, where) {
|
|
616
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(typeof hex === 'string' ? hex : '');
|
|
617
|
+
if (!m) { warn(`${where}: ${JSON.stringify(hex)} is not a colour ("#rrggbb")`); return null; }
|
|
618
|
+
const n = parseInt(m[1], 16);
|
|
619
|
+
return [n >> 16, (n >> 8) & 255, n & 255];
|
|
620
|
+
}
|
|
621
|
+
function lookOf(spec) {
|
|
622
|
+
if (LOOK.spec === spec && LOOK.ver === STATE.ver) return LOOK.now;
|
|
623
|
+
const L = spec?.light && typeof spec.light === 'object' ? spec.light : {}, v = {};
|
|
624
|
+
for (const [k, d] of Object.entries(LOOK_NUM)) v[k] = typeof L[k] === 'number' ? L[k] : d;
|
|
625
|
+
v.tint = L.tint ? colourOf(L.tint, 'light.tint') : null;
|
|
626
|
+
v.hazeColor = colourOf(L.hazeColor || spec?.sky?.bottom || '#808080', 'light.hazeColor') || [128, 128, 128];
|
|
627
|
+
v.ambientColor = (L.ambientColor && colourOf(L.ambientColor, 'light.ambientColor')) || [255, 255, 255];
|
|
628
|
+
let lit = typeof L.ambient === 'number' ? 1 : 0;
|
|
629
|
+
const looks = L.looks && typeof L.looks === 'object' ? L.looks : {};
|
|
630
|
+
for (const [key, look] of Object.entries(looks)) {
|
|
631
|
+
if (!look || typeof look !== 'object') { warn(`light.looks.${key}: an object of light fields`); continue; }
|
|
632
|
+
for (const k of Object.keys(look)) if (!(k in LOOK_NUM) && !LOOK_COL.includes(k)) warn(`light.looks.${key}: "${k}" is not a field of the light (${[...Object.keys(LOOK_NUM), ...LOOK_COL].join(', ')})`);
|
|
633
|
+
const w = clamp(Number(STATE.values[key]) || 0, 0, 1);
|
|
634
|
+
if (w <= 0) continue;
|
|
635
|
+
if (typeof look.ambient === 'number') lit = Math.max(lit, w);
|
|
636
|
+
for (const k of Object.keys(LOOK_NUM)) if (typeof look[k] === 'number') v[k] += (look[k] - v[k]) * w;
|
|
637
|
+
for (const k of LOOK_COL) {
|
|
638
|
+
const c = look[k] && colourOf(look[k], `light.looks.${key}.${k}`);
|
|
639
|
+
if (c) v[k] = v[k] ? v[k].map((a, i) => a + (c[i] - a) * w) : c;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
v.lit = lit;
|
|
643
|
+
v.hazeReach = hazeReach(v.haze);
|
|
644
|
+
Object.assign(LOOK, { spec, ver: STATE.ver, now: v });
|
|
645
|
+
return v;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ── where light comes from ─────────────────────────────────────────────
|
|
649
|
+
// The scene's sources of light, as data its painting reads (e.lights) and the renderer lays on
|
|
650
|
+
// what is painted (a lit scene) and casts shadows from (`shadow`):
|
|
651
|
+
// spec.lights: { id, across | x, distance, elevation (metres), place (as an element's: a lamp
|
|
652
|
+
// carried), color, strength (1), reach (metres it lights, 8), on (a state key 0…1: it shines
|
|
653
|
+
// that much), flicker (0…1), glow (metres: its halo, which the renderer draws) } — a point: a
|
|
654
|
+
// lamp, a forge, a fire; { id, of: an element's light (its id, or 'id#name' — below), color,
|
|
655
|
+
// strength, reach, on, flicker } — the light an element paints, where it paints it, shining as
|
|
656
|
+
// this says (a lantern in a hand: its glow small, its reach long); or { id, from: [bearing,
|
|
657
|
+
// height] (degrees: where it shines from — 0 the far side, 90 the right, 180 behind the viewer;
|
|
658
|
+
// how high above the horizon), color, strength, on } — the moon, the sun, the dawn: far off, the
|
|
659
|
+
// same everywhere;
|
|
660
|
+
// every e.light() an element paints (its id the element's, or 'id#name' for one it names),
|
|
661
|
+
// reaching twice its radius — as it was last painted.
|
|
662
|
+
// World units, eye heights: X to the right, Y up, Z ahead. `ver` changes when any of them does:
|
|
663
|
+
// what reads them is repainted then.
|
|
664
|
+
const SOURCES = { list: [], ver: 0 };
|
|
665
|
+
const hexOf = c => '#' + c.map(v => Math.round(clamp(v, 0, 255)).toString(16).padStart(2, '0')).join('');
|
|
666
|
+
// the spec's sources now (dz: depths()); from: the elements' lights — as [{ id, at, reach, rgb,
|
|
667
|
+
// strength }] — they too become the sources (but those the spec takes over: `of`). A source that
|
|
668
|
+
// flickers has `flicker` (0…1) and `seed`; one with a halo `glow` (eye heights)
|
|
669
|
+
function sourcesNow(spec, dz, from) {
|
|
670
|
+
const list = [], taken = new Set();
|
|
671
|
+
(Array.isArray(spec.lights) ? spec.lights : []).forEach((l, i) => {
|
|
672
|
+
if (!l || typeof l !== 'object') { warn(`lights[${i}]: an object`); return; }
|
|
673
|
+
const id = l.id != null ? String(l.id) : `lights[${i}]`;
|
|
674
|
+
const on = typeof l.on === 'string' ? clamp(Number(STATE.values[l.on]) || 0, 0, 1) : 1;
|
|
675
|
+
const flicker = clamp(Number(l.flicker) || 0, 0, 1), seed = hash(id) % 1000;
|
|
676
|
+
if (l.of != null) {
|
|
677
|
+
const el = from.find(s => s.id === String(l.of));
|
|
678
|
+
if (!el) {
|
|
679
|
+
// (an element painting no such light now: it is out)
|
|
680
|
+
const owner = String(l.of).split('#')[0];
|
|
681
|
+
if (!(spec.objects || []).some(o => o.id != null && String(o.id) === owner)) warn(`lights ${id}: "of" names an element's light ("lamp", "lamp#wick") — there is no element ${JSON.stringify(owner)}`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
taken.add(el);
|
|
685
|
+
const rgb = l.color ? colourOf(l.color, `lights ${id}: color`) || el.rgb : el.rgb;
|
|
686
|
+
list.push({ ...el, id, rgb, strength: (typeof l.strength === 'number' ? l.strength : el.strength) * on, reach: l.reach > 0 ? l.reach / EYE : el.reach, ...(flicker ? { flicker, seed, sprite: undefined } : {}) });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const rgb = colourOf(l.color || '#ffffff', `lights ${id}: color`) || [255, 255, 255];
|
|
690
|
+
const strength = (typeof l.strength === 'number' ? l.strength : 1) * on;
|
|
691
|
+
if (Array.isArray(l.from)) {
|
|
692
|
+
const [b, h] = l.from.map(v => (Number(v) || 0) * Math.PI / 180);
|
|
693
|
+
list.push({ id, dir: [Math.sin(b) * Math.cos(h), Math.sin(h), Math.cos(b) * Math.cos(h)], rgb, strength });
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (!(l.distance > 0) && typeof l.place?.distance !== 'string') { warn(`lights ${id}: a point light has a "distance" (metres); one far off has "from": [bearing, height]`); return; }
|
|
697
|
+
const at = dz.now({ ...l, kind: undefined, sky: false, plane: false, ground: false, height: 1 }).at;
|
|
698
|
+
list.push({ id, at: [at.X, at.Y, at.Z], reach: (l.reach > 0 ? l.reach : 8) / EYE, rgb, strength, flicker, seed, halo: l.glow > 0 ? l.glow / EYE : 0 });
|
|
699
|
+
});
|
|
700
|
+
list.push(...from.filter(s => !taken.has(s)));
|
|
701
|
+
// (the same as a frame ago, to a thousandth, compared as numbers: nothing made each frame)
|
|
702
|
+
const near = (a, b) => Math.abs(a - b) < 5e-4, was = SOURCES.list;
|
|
703
|
+
const same = list.length === was.length && list.every((s, i) => {
|
|
704
|
+
const w = was[i], p = s.at || s.dir, q = w.at || w.dir;
|
|
705
|
+
return s.id === w.id && near(s.strength, w.strength) && s.rgb.every((v, j) => v === w.rgb[j]) && p.length === q.length && p.every((v, j) => near(v, q[j]));
|
|
706
|
+
});
|
|
707
|
+
if (!same) SOURCES.ver++;
|
|
708
|
+
SOURCES.list = list;
|
|
709
|
+
return list;
|
|
710
|
+
}
|
|
711
|
+
// how strong a source that flickers (sourcesNow: flicker, seed) shines at t, 0…1
|
|
712
|
+
const flickerNow = (s, t) => (s.flicker ? 1 - s.flicker * 0.5 * (0.5 + 0.5 * Math.sin(t * 9 + s.seed) * Math.sin(t * 5.3 + s.seed * 0.7)) : 1);
|
|
713
|
+
// The light in the air, drawn among what stands, each at its depth (far: a source → its depth, as
|
|
714
|
+
// the renderer orders what it draws): the halos of the scene's own lights (`glow`), and — the
|
|
715
|
+
// light's hazeGlow — the haze round every point source (a lamp, a fire, a window's e.light) glowing
|
|
716
|
+
// its colour as far as it reaches, a little nearer than the source (over what it lights). Farthest
|
|
717
|
+
// first: [{ h: the source, fog, Z }]; with no point sources, or no hazeGlow, only the halos.
|
|
718
|
+
function airOf(sources, look, far) {
|
|
719
|
+
const out = [];
|
|
720
|
+
for (const h of sources) {
|
|
721
|
+
if (!h.at || h.strength <= 0.002) continue;
|
|
722
|
+
if (h.halo) out.push({ h, fog: false, Z: far(h) });
|
|
723
|
+
if (look.hazeGlow > 0) out.push({ h, fog: true, Z: far(h) - h.reach / 2 });
|
|
724
|
+
}
|
|
725
|
+
return out.sort((a, b) => b.Z - a.Z);
|
|
726
|
+
}
|
|
727
|
+
// how strongly the haze round point source s glows seen D eye heights away (0…1, 0: not at all): as
|
|
728
|
+
// much as there is air within its reach (its haze — thick fog glows more), scattered toward the eye
|
|
729
|
+
// (×3), through the air between, as the light shines now — × hazeGlow
|
|
730
|
+
const airGlow = (look, s, D, t) => Math.min(1, look.hazeGlow * 3 * s.reach / look.hazeReach * Math.exp(-Math.max(0, D) / look.hazeReach) * Math.min(1, s.strength) * flickerNow(s, t));
|
|
731
|
+
// how much of a point light reaches x of its reach away
|
|
732
|
+
const falloff = x => (x >= 1 ? 0 : (1 - x * x) * (1 - x * x));
|
|
733
|
+
// A point of an element's painting (metres, from its anchor as painted — or from the foot of its
|
|
734
|
+
// part { dx, dz }, e.part) in the world (eye heights), or null for what has no place in it (the sky,
|
|
735
|
+
// the ground's painting, an insert)
|
|
736
|
+
function worldOf(o, px, py, part) {
|
|
737
|
+
if (o._plane) { const P = o._plane, u = px / EYE, v = py / EYE; return [P.Xc + u * P.cos + v * P.sin, P.Y, P.Zc - v * P.cos + u * P.sin]; }
|
|
738
|
+
const at = o._at;
|
|
739
|
+
if (!at || o.sky || o.ground || o.insert) return null;
|
|
740
|
+
if (!part) return [(at.across + px) / EYE, (at.elevation - py) / EYE, at.distance / EYE];
|
|
741
|
+
const across = at.across + part.dx, distance = at.distance + part.dz;
|
|
742
|
+
return [(across + px) / EYE, (at.elevation - at.ground + groundAt(across, distance) - py) / EYE, distance / EYE];
|
|
743
|
+
}
|
|
744
|
+
// The light falling on world point P (null: only what comes from far off), strongest first:
|
|
745
|
+
// { id, color, strength (as much as reaches it), x, y (toward the light on the painting: x to the
|
|
746
|
+
// right, y down — a unit vector with z), z (toward the viewer: + it lights the side we see, − it
|
|
747
|
+
// is behind: an edge of light), dir (degrees on the screen toward the light: 0 right, 90 down,
|
|
748
|
+
// −90 up, 180 left), distance (metres; null from far off) }
|
|
749
|
+
function lightsAt(P) {
|
|
750
|
+
const out = [];
|
|
751
|
+
for (const s of SOURCES.list) {
|
|
752
|
+
let v = s.dir, k = 1, d = null;
|
|
753
|
+
if (!v) {
|
|
754
|
+
if (!P) continue;
|
|
755
|
+
v = [s.at[0] - P[0], s.at[1] - P[1], s.at[2] - P[2]];
|
|
756
|
+
d = Math.hypot(v[0], v[1], v[2]);
|
|
757
|
+
k = falloff(d / s.reach);
|
|
758
|
+
v = d > 1e-6 ? v.map(c => c / d) : [0, 1, 0];
|
|
759
|
+
}
|
|
760
|
+
const strength = s.strength * k;
|
|
761
|
+
if (strength < 0.002) continue;
|
|
762
|
+
out.push({ id: s.id, color: hexOf(s.rgb), strength, x: v[0], y: -v[1], z: -v[2], dir: Math.atan2(-v[1], v[0]) * 180 / Math.PI, distance: d === null ? null : d * EYE });
|
|
763
|
+
}
|
|
764
|
+
return out.sort((a, b) => b.strength - a.strength);
|
|
765
|
+
}
|
|
766
|
+
// `shadow` on an element standing in the scene: it casts one from a light of the scene onto what
|
|
767
|
+
// `onto` names — 'ground' (the ground under everything, height 0), a plane (a floor, a table top) or
|
|
768
|
+
// an upright element (a wall) — each point of it onto the first of them its ray meets: its own
|
|
769
|
+
// painting, black, softened by the size of the light (`soft`, metres), as dark as `strength` where
|
|
770
|
+
// the light is full, fading as the light does. true = { light: the strongest at it, onto:
|
|
771
|
+
// ['ground'], strength: 0.5, soft: 0.15 }. The scene's shadows: [{ id, onto: ['ground' | id],
|
|
772
|
+
// light: id | null, strength, soft (eye heights) }] (what is wrong with one, said)
|
|
773
|
+
function shadowsOf(spec) {
|
|
774
|
+
const objs = spec.objects || [], byId = new Map(objs.filter(o => o.id != null).map(o => [String(o.id), o])), out = [];
|
|
775
|
+
for (const o of objs) {
|
|
776
|
+
const sh = o.shadow;
|
|
777
|
+
if (!sh) continue;
|
|
778
|
+
const who = o.name || o.id, c = sh === true ? {} : typeof sh === 'object' && !Array.isArray(sh) ? sh : null;
|
|
779
|
+
if (!c || o.id == null || o.sky || o.ground || o.plane || o.insert || o.kind === 'bird') { warn(`${who}: "shadow" is true or { light, onto, strength, soft } — on something standing in the scene, with an id`); continue; }
|
|
780
|
+
if (o.unseen) { warn(`${who}: it is unseen — what is not seen casts no shadow`); continue; }
|
|
781
|
+
const onto = (c.onto == null ? ['ground'] : Array.isArray(c.onto) ? c.onto : [c.onto]).map(id => {
|
|
782
|
+
const r = byId.get(String(id));
|
|
783
|
+
if (id === 'ground' || (r && r !== o && !r.sky && !r.ground && !r.insert)) return String(id);
|
|
784
|
+
warn(`${who}: its shadow falls on ${JSON.stringify(id)} — 'ground', a plane or an upright element of the scene`);
|
|
785
|
+
return null;
|
|
786
|
+
}).filter(Boolean);
|
|
787
|
+
const light = c.light != null ? String(c.light) : null, owner = light && light.split('#')[0];
|
|
788
|
+
if (light && !(spec.lights || []).some(l => l && String(l.id) === light) && !byId.has(owner)) warn(`${who}: its shadow's light "${light}" is not a light of the scene (a light's id, or an element's — "lamp", "lamp#wick")`);
|
|
789
|
+
out.push({ id: String(o.id), onto, light, strength: clamp(Number(c.strength ?? 0.5) || 0, 0, 1), soft: Math.max(0, Number(c.soft ?? 0.15) || 0) / EYE });
|
|
790
|
+
}
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
// the light a shadow is cast from: the one it names (null while it does not shine: an element not
|
|
794
|
+
// painting it now), else the strongest at A (where the element stands, eye heights) — a source
|
|
795
|
+
// (sourcesNow), or null
|
|
796
|
+
function shadowLight(sources, sh, A) {
|
|
797
|
+
if (sh.light) return sources.find(s => s.id === sh.light) || null;
|
|
798
|
+
let best = null, most = 0.01;
|
|
799
|
+
for (const s of sources) {
|
|
800
|
+
const k = s.dir ? s.strength : s.strength * falloff(Math.hypot(s.at[0] - A.X, s.at[1] - A.Y, s.at[2] - A.Z) / s.reach);
|
|
801
|
+
if (k > most) { most = k; best = s; }
|
|
802
|
+
}
|
|
803
|
+
return best;
|
|
804
|
+
}
|
|
805
|
+
// A lit scene's light where no source reaches: the ambient, and what comes from far off (as much as
|
|
806
|
+
// the scene is lit: look.lit) — from the viewer's side it lights what we see, from beyond only a
|
|
807
|
+
// little: [r, g, b], 0…1 a channel
|
|
808
|
+
function ambientOf(look, sources) {
|
|
809
|
+
const amb = look.ambientColor.map(v => v / 255 * look.ambient);
|
|
810
|
+
for (const s of sources) if (s.dir) { const k = (1 - 0.7 * clamp(s.dir[2] / 0.3, 0, 1)) * look.lit; for (let i = 0; i < 3; i++) amb[i] += s.rgb[i] / 255 * s.strength * k; }
|
|
811
|
+
return amb;
|
|
812
|
+
}
|
|
813
|
+
// how dark a shadow of `src` is at world point P: in a lit scene the share of the light there that
|
|
814
|
+
// the source gives (amb: the ambient's level), else as much of full light as reaches — mixed by how
|
|
815
|
+
// lit the scene is (lit 0…1, lookOf) — times strength
|
|
816
|
+
function shadowAt(sh, src, P, amb, lit) {
|
|
817
|
+
const I = src.strength * (src.at ? falloff(Math.hypot(P[0] - src.at[0], P[1] - src.at[1], P[2] - src.at[2]) / src.reach) : 1);
|
|
818
|
+
return sh.strength * ((1 - lit) * Math.min(1, I) + lit * I / (amb + I || 1));
|
|
819
|
+
}
|
|
820
|
+
// where a ray of light meets a plane { q: a point of it, n: its normal }: from a point L through C
|
|
821
|
+
// (t ≥ 1: at C or beyond it), or from far off along d (toward the light) back through C (t > 0) —
|
|
822
|
+
// [point, t], or null
|
|
823
|
+
const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
824
|
+
function rayHit(pl, C, L, d) {
|
|
825
|
+
if (d) {
|
|
826
|
+
const nd = dot3(pl.n, d);
|
|
827
|
+
if (Math.abs(nd) < 1e-6) return null;
|
|
828
|
+
const t = (dot3(pl.n, C) - dot3(pl.n, pl.q)) / nd;
|
|
829
|
+
return t > 1e-4 ? [[C[0] - d[0] * t, C[1] - d[1] * t, C[2] - d[2] * t], t] : null;
|
|
830
|
+
}
|
|
831
|
+
const v = [C[0] - L[0], C[1] - L[1], C[2] - L[2]], nv = dot3(pl.n, v);
|
|
832
|
+
if (Math.abs(nv) < 1e-6) return null;
|
|
833
|
+
const t = (dot3(pl.n, pl.q) - dot3(pl.n, L)) / nv;
|
|
834
|
+
return t > 0.9999 ? [[L[0] + v[0] * t, L[1] + v[1] * t, L[2] + v[2] * t], t] : null;
|
|
835
|
+
}
|
|
836
|
+
// `redraw`: the state keys a painting depends on — [keys], or { key: step } (repainted once the
|
|
837
|
+
// value has moved a step) — repainted when they change, whatever else it reads. What they are now,
|
|
838
|
+
// as a string to compare; null without it
|
|
839
|
+
function redrawOf(o) {
|
|
840
|
+
const r = o.redraw;
|
|
841
|
+
if (r == null) return null;
|
|
842
|
+
const steps = Array.isArray(r) ? Object.fromEntries(r.map(k => [k, 0])) : r && typeof r === 'object' ? r : null;
|
|
843
|
+
if (!steps || Object.entries(steps).some(([k, v]) => typeof v !== 'number' || v < 0)) { warn(`${o.name || o.id}: "redraw" is [state keys] or { key: step }`); return null; }
|
|
844
|
+
return JSON.stringify(Object.entries(steps).map(([k, step]) => {
|
|
845
|
+
const v = STATE.values[k];
|
|
846
|
+
return typeof v === 'number' && step > 0 ? Math.round(v / step) : v ?? null;
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// What the scene's code shares (a world's module, the scene's lib) may give beforeFrame({ t, dt,
|
|
851
|
+
// state, cut }): called once a frame before anything is painted, whether its figures are painted or
|
|
852
|
+
// not — to keep its own time (the springs and steps of a figure out of sight go on), and to work
|
|
853
|
+
// out this frame's points of its figures before any element reads them.
|
|
854
|
+
// (those that have one, found once for what is shared: a scene with none does nothing a frame)
|
|
855
|
+
let befores = null, beforesOf = null, beforeState = null, beforeValues = null;
|
|
856
|
+
function beforeFrame(t, dt) {
|
|
857
|
+
if (beforesOf !== SHARED) {
|
|
858
|
+
beforesOf = SHARED;
|
|
859
|
+
befores = [...Object.entries(SHARED.kit), ['lib', SHARED.lib]].filter(([, m]) => typeof m?.beforeFrame === 'function');
|
|
860
|
+
}
|
|
861
|
+
if (!befores.length) return;
|
|
862
|
+
// (the state's values are a new object when they change: read through one made for them)
|
|
863
|
+
if (beforeValues !== STATE.values) { beforeValues = STATE.values; beforeState = stateRead(BEFORE); }
|
|
864
|
+
for (const [name, m] of befores) {
|
|
865
|
+
try { m.beforeFrame({ t, dt, state: beforeState, cut: STATE.cut }); } catch (err) { warn(`${name === 'lib' ? 'lib' : `module ${name}`}: beforeFrame: ${err?.message || err}`); }
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ── what each element costs ────────────────────────────────────────────
|
|
870
|
+
// While the performance panel is open (key P): the time each element's painting takes a frame (ms,
|
|
871
|
+
// summed by its name; in WebGL its upload too). And always its last painting's, with what it read
|
|
872
|
+
// (`last`, by its id, else its name — one with neither isn't told apart): what ?debug and a
|
|
873
|
+
// render's --json show.
|
|
874
|
+
const PERF = {
|
|
875
|
+
on: false, ms: new Map(), last: new Map(),
|
|
876
|
+
// how many times each live element (one painted again as what it reads moves) was repainted since
|
|
877
|
+
// the scene was shown, by its id (else its name)
|
|
878
|
+
repaints: new Map(),
|
|
879
|
+
repainted(o) { const k = keyOf(o); if (k != null) PERF.repaints.set(k, (PERF.repaints.get(k) || 0) + 1); },
|
|
880
|
+
add(o, ms, upload = false) {
|
|
881
|
+
if (!upload && keyOf(o) != null) PERF.last.set(keyOf(o), { ms, read: READS.get(o) });
|
|
882
|
+
if (PERF.on) { const k = o.name || o.id || o.kind || '?'; PERF.ms.set(k, (PERF.ms.get(k) || 0) + ms); }
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// ── inserts ────────────────────────────────────────────────────────────
|
|
887
|
+
// An insert (`insert`: the state key that opens it) is a shot of its own over the scene — the six
|
|
888
|
+
// fingers seen close, a letter, a face: painted in the frame's own coordinates (stage heights, as
|
|
889
|
+
// the sky is), fixed to the frame whatever the camera does, and seen through an iris that its key
|
|
890
|
+
// opens, 0 (closed: the scene) … 1 (open). `iris`: [x, y, rx, ry] — the window fully open (stage
|
|
891
|
+
// heights: its centre and radii, an oval); without it the iris opens from the middle of the frame
|
|
892
|
+
// to all of it. The scene goes on under it; closing it is the way back.
|
|
893
|
+
const IRIS_ALL = Math.hypot(WIDEST / 2, 0.5) + 0.05;
|
|
894
|
+
function opening(o) {
|
|
895
|
+
if (typeof o.insert === 'string') return clamp(Number(STATE.values[o.insert]) || 0, 0, 1);
|
|
896
|
+
warn(`${o.name || o.id}: "insert" is the state key that opens it`);
|
|
897
|
+
return 0;
|
|
898
|
+
}
|
|
899
|
+
// the insert's glows painted in (they don't shine past the iris), then all cut to the iris opened u:
|
|
900
|
+
// on g, a canvas holding the insert alone, transformed to the W × H stage
|
|
901
|
+
function iris(g, o, W, H, u, lights) {
|
|
902
|
+
g.save();
|
|
903
|
+
g.globalCompositeOperation = 'lighter';
|
|
904
|
+
for (const l of lights) { glow(g, l.x, l.y, l.r, l.col, l.a * 0.6); if (l.dot) glow(g, l.x, l.y, l.dot * 3, '255,245,220', 0.9); }
|
|
905
|
+
lights.length = 0;
|
|
906
|
+
let win = o.iris;
|
|
907
|
+
if (win !== undefined && !(Array.isArray(win) && win.length === 4 && win.every(v => typeof v === 'number') && win[2] > 0 && win[3] > 0)) {
|
|
908
|
+
warn(`${o.name || o.id}: "iris" is [x, y, rx, ry] (stage heights, radii above 0)`);
|
|
909
|
+
win = undefined;
|
|
910
|
+
}
|
|
911
|
+
const [cx, cy, rx, ry] = win || [W / H / 2, 0.5, IRIS_ALL, IRIS_ALL], r = Math.min(rx, ry) * u;
|
|
912
|
+
// a soft edge, about 2% of the frame's height
|
|
913
|
+
const soft = clamp(0.02 / r, 0.002, 0.5), reach = 2 * WIDEST / r + 2, gr = g.createRadialGradient(0, 0, 0, 0, 0, 1);
|
|
914
|
+
gr.addColorStop(1 - soft, '#000'); gr.addColorStop(1, 'rgba(0,0,0,0)');
|
|
915
|
+
g.globalCompositeOperation = 'destination-in';
|
|
916
|
+
g.translate(cx * H, cy * H); g.scale(rx * u * H, ry * u * H);
|
|
917
|
+
g.fillStyle = gr; g.fillRect(-reach, -reach, 2 * reach, 2 * reach);
|
|
918
|
+
g.restore();
|
|
919
|
+
}
|
|
920
|
+
|
|
459
921
|
// ── particles ──────────────────────────────────────────────────────────
|
|
460
922
|
const PARTICLES = { snow: 380, rain: 520, fireflies: 45, dust: 140, petals: 90, embers: 90, leaves: 60, bubbles: 60 };
|
|
461
923
|
function makeParticles(p, W, H, rnd) {
|
|
@@ -601,7 +1063,16 @@ const Moood = (() => {
|
|
|
601
1063
|
const bearing = o => typeof o.bearing === 'number' && o.distance > 0 && !o.sky;
|
|
602
1064
|
const paintZ = o => (o.plane ? planeZ(o) : custom(o) ? sightZ(o) : own(host(o) || o));
|
|
603
1065
|
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);
|
|
604
|
-
|
|
1066
|
+
// where it is over the land (metres: across, ahead) — for an element placed by its distance
|
|
1067
|
+
const spotOf = o => {
|
|
1068
|
+
if (bearing(o)) { const b = o.bearing * Math.PI / 180; return [o.distance * Math.sin(b), o.distance * Math.cos(b)]; }
|
|
1069
|
+
return [(objectX(o) - 0.5) * STAGE * sightZ(o) * EYE, o.distance];
|
|
1070
|
+
};
|
|
1071
|
+
// with a relief (spec.relief), what stands where its distance says stands on the ground there:
|
|
1072
|
+
// its elevation is above it
|
|
1073
|
+
const onLand = o => !!SHARED.ground && custom(o) && o.distance > 0 && !o.sky && !o.ground && !o.insert && !o.plane;
|
|
1074
|
+
const landUnder = o => (onLand(o) ? groundAt(...spotOf(o)) : 0);
|
|
1075
|
+
const objectY = o => (o.sky || o.ground ? 0 : custom(o) ? yh + (1 - ((o.elevation || 0) + landUnder(o)) / EYE) / sightZ(o) : o.kind === 'bird' ? o.y : o.y + shift(o));
|
|
605
1076
|
const objectSize = o => (o.sky || o.ground ? 1 : custom(o) ? (o.height || 2) / EYE / sightZ(o) : o.size);
|
|
606
1077
|
// where it stands in the world (eye heights: X to the right, Z ahead, Y its anchor's height) and
|
|
607
1078
|
// the distance Zp it is painted for
|
|
@@ -614,17 +1085,219 @@ const Moood = (() => {
|
|
|
614
1085
|
// ground planes are drawn right after the sky, under everything standing on them
|
|
615
1086
|
// a horizontal plane (gl.js) is ordered by its far edge, before what stands on it
|
|
616
1087
|
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));
|
|
1088
|
+
// an element where it is now (its `place`), the one truth the renderers, the picking and
|
|
1089
|
+
// whatever follows it go by: { o: as painted on the stage (x, y, size: fractions of it; _at:
|
|
1090
|
+
// where it is in the world, metres — across, distance, its anchor's height and the ground's
|
|
1091
|
+
// under it), Z: the depth it is drawn at, at: where it stands in the world }
|
|
1092
|
+
const now = o => {
|
|
1093
|
+
const n = placed(o), at = objectAt(n), ground = landUnder(n);
|
|
1094
|
+
const _at = o.sky || o.ground || o.insert ? null : { across: at.X * EYE, distance: at.Z * EYE, elevation: at.Y * EYE, ground };
|
|
1095
|
+
return { o: { ...o, x: objectX(n), y: objectY(n), size: objectSize(n), _yh: yh, _at }, Z: objectZ(n), at };
|
|
1096
|
+
};
|
|
1097
|
+
return {
|
|
1098
|
+
yh, Z, objectX, objectAt, objectZ, now,
|
|
1099
|
+
// part (dx, dz) of element o (e.part) as an element of its own: standing dx metres to the
|
|
1100
|
+
// right and dz farther than o's anchor, at o's elevation over the ground there, its box `box`
|
|
1101
|
+
// (o's without one) — where it is now, like now()
|
|
1102
|
+
part: (o, dx, dz, box) => {
|
|
1103
|
+
const n = placed(o), at = objectAt(n), elevation = at.Y * EYE - landUnder(n);
|
|
1104
|
+
const { x, y, bearing, place, ...rest } = o;
|
|
1105
|
+
return now({ ...rest, across: at.X * EYE + dx, distance: Math.max(0.05, at.Z * EYE + dz), elevation, box: box || o.box });
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
// ── in front, behind, cut by another ───────────────────────────────────
|
|
1110
|
+
// What is drawn in front of what is their depth — at equal depth the later in the spec's objects —
|
|
1111
|
+
// unless an element says: `behind: [ids]` — drawn behind those, `inFront: [ids]` — in front of
|
|
1112
|
+
// them, whatever the depths. The rules of a spec: [the one behind, the one in front] by id.
|
|
1113
|
+
// v: an id or a list of them, given as o's `key` — those naming other elements of the scene (ids:
|
|
1114
|
+
// what stands in it — an insert is over all of it, not among them)
|
|
1115
|
+
const idList = (v, o, key, ids) => {
|
|
1116
|
+
if (v === undefined) return [];
|
|
1117
|
+
const list = typeof v === 'string' ? [v] : Array.isArray(v) ? v : null;
|
|
1118
|
+
if (!list) { warn(`${o.name || o.id}: "${key}" is a list of element ids`); return []; }
|
|
1119
|
+
return list.filter(id => {
|
|
1120
|
+
if (ids.has(id) && id !== o.id) return true;
|
|
1121
|
+
warn(`${o.name || o.id}: "${key}" names "${id}" — not another element standing in the scene (an insert is over all of it)`);
|
|
1122
|
+
return false;
|
|
1123
|
+
});
|
|
1124
|
+
};
|
|
1125
|
+
function orderRules(spec) {
|
|
1126
|
+
const objs = spec.objects || [], ids = new Set(objs.filter(o => !o.insert).map(o => o.id).filter(id => id != null)), rules = [];
|
|
1127
|
+
for (const o of objs) {
|
|
1128
|
+
if (o.insert && (o.behind || o.inFront || o.clip)) warn(`${o.name || o.id}: an insert is over the whole scene — "behind", "inFront" and "clip" are for what stands in it`);
|
|
1129
|
+
if (o.id == null || o.insert) continue;
|
|
1130
|
+
for (const id of idList(o.behind, o, 'behind', ids)) rules.push([o.id, id]);
|
|
1131
|
+
for (const id of idList(o.inFront, o, 'inFront', ids)) rules.push([id, o.id]);
|
|
1132
|
+
}
|
|
1133
|
+
return rules;
|
|
1134
|
+
}
|
|
1135
|
+
// list (far to near) with the rules kept: what is to be behind moved to just before the first
|
|
1136
|
+
// drawing of what it is behind (an element drawn in parts: all of them). idOf(item) → its element's id
|
|
1137
|
+
function arrange(list, rules, idOf) {
|
|
1138
|
+
for (let pass = 0; pass <= rules.length; pass++) {
|
|
1139
|
+
let moved = false;
|
|
1140
|
+
for (const [far, near] of rules) {
|
|
1141
|
+
const first = list.findIndex(it => idOf(it) === near), late = [];
|
|
1142
|
+
if (first < 0) continue;
|
|
1143
|
+
for (let i = list.length - 1; i > first; i--) if (idOf(list[i]) === far) late.unshift(...list.splice(i, 1));
|
|
1144
|
+
if (late.length) { list.splice(first, 0, ...late); moved = true; }
|
|
1145
|
+
}
|
|
1146
|
+
if (!moved) return list;
|
|
1147
|
+
}
|
|
1148
|
+
warn('behind / inFront: elements put each in front of the other, round in a circle');
|
|
1149
|
+
return list;
|
|
1150
|
+
}
|
|
1151
|
+
// `clip`: an element seen only within what other elements paint — {inside: ids} (a figure in a
|
|
1152
|
+
// doorway, a face in a window) — or only out of it — {outside: ids} (under a table top, below
|
|
1153
|
+
// water) — as they are in the frame now. An element with `unseen: true` is not shown itself: a
|
|
1154
|
+
// shape others are cut by. → { inside: true | false, ids } or null
|
|
1155
|
+
function clipOf(o, ids) {
|
|
1156
|
+
const c = o.clip;
|
|
1157
|
+
if (c == null) return null;
|
|
1158
|
+
const side = c && typeof c === 'object' && !Array.isArray(c) ? ['inside', 'outside'].find(k => k in c) : null;
|
|
1159
|
+
if (!side) { warn(`${o.name || o.id}: "clip" is {inside: ids} or {outside: ids}`); return null; }
|
|
1160
|
+
const list = idList(c[side], o, `clip.${side}`, ids);
|
|
1161
|
+
return list.length ? { inside: side === 'inside', ids: list } : null;
|
|
1162
|
+
}
|
|
1163
|
+
// ── flat pieces of the world ───────────────────────────────────────────
|
|
1164
|
+
// A plane (`plane`: a floor, a deck, a table top; the land, draped over the relief) where it lies:
|
|
1165
|
+
// { Xc, Y, Zc: its centre (eye heights), w, d: across and away, cos, sin: its turn, drape, mirror,
|
|
1166
|
+
// pool } — mirror: what it reflects (mirrorOf), pool: how much the scene's lights lie on it unlit (lit)
|
|
1167
|
+
function planeOf(o, dz) {
|
|
1168
|
+
const at = dz.objectAt(o), rot = (o.rotation || 0) * Math.PI / 180;
|
|
1169
|
+
return { Xc: at.X, Y: (o.elevation || 0) / EYE, Zc: at.Z, w: Math.max(0.05, o.width || 4) / EYE, d: Math.max(0.05, o.depth || 4) / EYE, cos: Math.cos(rot), sin: Math.sin(rot), drape: !!o.drape, mirror: mirrorOf(o), pool: poolOf(o) };
|
|
1170
|
+
}
|
|
1171
|
+
// A plane that reflects (`reflect`: a polished floor, still water): all that is above it is seen in
|
|
1172
|
+
// it as in a mirror — { strength: how much (0.35), fade: metres above the plane over which what it
|
|
1173
|
+
// mirrors fades out (4; 0: never), blur: how much the mirrored spreads per metre above it (0.05) };
|
|
1174
|
+
// true, or a number: the strength. → { strength, fade (eye heights), blur } or null (what is wrong, said)
|
|
1175
|
+
function mirrorOf(o) {
|
|
1176
|
+
const r = o.reflect;
|
|
1177
|
+
if (!r) return null;
|
|
1178
|
+
const c = r === true ? {} : typeof r === 'number' ? { strength: r } : typeof r === 'object' && !Array.isArray(r) ? r : null;
|
|
1179
|
+
if (!c || [c.strength, c.fade, c.blur].some(v => v != null && !(typeof v === 'number' && v >= 0))) { warn(`${o.name || o.id}: "reflect" on a plane is true, a strength 0…1 or { "strength", "fade" (metres), "blur" }`); return null; }
|
|
1180
|
+
return { strength: clamp(c.strength ?? 0.35, 0, 1), fade: (c.fade ?? 4) / EYE, blur: c.blur ?? 0.05 };
|
|
1181
|
+
}
|
|
1182
|
+
// `lit` on a plane: the scene's point lights lie on it where the scene is not lit (lookOf) — a pool
|
|
1183
|
+
// under a lamp, a fire's glow on the floor; true, or how strongly (1). → 0…
|
|
1184
|
+
function poolOf(o) {
|
|
1185
|
+
if (o.lit == null || o.lit === false) return 0;
|
|
1186
|
+
if (o.lit === true) return 1;
|
|
1187
|
+
if (typeof o.lit === 'number' && o.lit >= 0) return o.lit;
|
|
1188
|
+
warn(`${o.name || o.id}: "lit" on a plane is true or a strength (1)`);
|
|
1189
|
+
return 0;
|
|
1190
|
+
}
|
|
1191
|
+
// is world point A ({ X, Y, Z }, eye heights) above a reflecting plane of `planes` (planeOf's)? — what
|
|
1192
|
+
// stands on it is mirrored in it (its own `reflect` copy is not drawn)
|
|
1193
|
+
const overMirror = (planes, A) => !!A && planes.some(P => {
|
|
1194
|
+
if (!P.mirror || A.Y < P.Y - 0.01) return false;
|
|
1195
|
+
const dx = A.X - P.Xc, dz = A.Z - P.Zc;
|
|
1196
|
+
return Math.abs(dx * P.cos + dz * P.sin) <= P.w / 2 && Math.abs(dx * P.sin - dz * P.cos) <= P.d / 2;
|
|
1197
|
+
});
|
|
1198
|
+
// the point (f, g) of a plane's painting (both 0…1: across, and from its far edge toward the viewer)
|
|
1199
|
+
// in the world, eye heights — a draped one over the land there
|
|
1200
|
+
const onPlane = P => (f, g) => {
|
|
1201
|
+
const u = -P.w / 2 + P.w * f, v = -P.d / 2 + P.d * g, X = P.Xc + u * P.cos + v * P.sin, Z = P.Zc - v * P.cos + u * P.sin;
|
|
1202
|
+
return [X, P.Y + (P.drape ? groundAt(X * EYE, Z * EYE) / EYE : 0), Z];
|
|
1203
|
+
};
|
|
1204
|
+
// how finely each renderer lays a plane (cells across × away); the land hides what is behind it by
|
|
1205
|
+
// the same cells as it is drawn (landLine)
|
|
1206
|
+
const PLANE_CELLS = { webgl: [24, 48], '2d': [12, 24] };
|
|
1207
|
+
// A flat piece of the world seen truly in perspective from cam: world(f, g) → [X, Y, Z] (eye heights)
|
|
1208
|
+
// for the point (f, g) of its painting, both 0…1, cut into C × R cells; a cell close to the eye
|
|
1209
|
+
// (nearer than it is big) is split finer, and one partly behind the eye is cut at the near plane,
|
|
1210
|
+
// not dropped — a floor reaches right to your feet. each(pts) for each cell within view [x0, y0,
|
|
1211
|
+
// x1, y1] (the frame's px): its corners [x, y, f, g, depth along the gaze] — 3 to 5, convex.
|
|
1212
|
+
const NEAR = 0.06;
|
|
1213
|
+
function cellsOf(cam, world, C, R, W, H, yh, view, each) {
|
|
1214
|
+
const c = Math.cos(cam.yaw || 0), s = Math.sin(cam.yaw || 0), h = cam.h ?? 1;
|
|
1215
|
+
// a point of it seen from the eye: e — [right, down, along the gaze] (eye heights), f, g
|
|
1216
|
+
const eye = (f, g) => { const P = world(f, g), rx = P[0] - cam.x, rz = P[2] - cam.z; return { e: [rx * c - rz * s, h - P[1], rx * s + rz * c], f, g }; };
|
|
1217
|
+
// the grid's corners, each seen once
|
|
1218
|
+
const grid = [];
|
|
1219
|
+
for (let i = 0; i <= R; i++) for (let j = 0; j <= C; j++) grid.push(eye(j / C, i / R));
|
|
1220
|
+
const cell = (a, b, d, q, depth) => { // its corners: far left, far right, near right, near left
|
|
1221
|
+
const zs = [a.e[2], b.e[2], d.e[2], q.e[2]];
|
|
1222
|
+
let zmin = zs[0], zmax = zs[0];
|
|
1223
|
+
for (const z of zs) { if (z < zmin) zmin = z; if (z > zmax) zmax = z; }
|
|
1224
|
+
if (zmax < NEAR) return;
|
|
1225
|
+
if (depth < 4 && zmin < Math.hypot(a.e[0] - d.e[0], a.e[1] - d.e[1], a.e[2] - d.e[2])) {
|
|
1226
|
+
const fm = (a.f + b.f) / 2, gm = (a.g + q.g) / 2, top = eye(fm, a.g), right = eye(b.f, gm), bottom = eye(fm, q.g), left = eye(a.f, gm), mid = eye(fm, gm);
|
|
1227
|
+
cell(a, top, mid, left, depth + 1); cell(top, b, right, mid, depth + 1); cell(left, mid, bottom, q, depth + 1); cell(mid, right, d, bottom, depth + 1);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
let poly = [a, b, d, q];
|
|
1231
|
+
if (zmin < NEAR) {
|
|
1232
|
+
const cut = [];
|
|
1233
|
+
for (let i = 0; i < 4; i++) {
|
|
1234
|
+
const u = poly[i], v = poly[(i + 1) % 4], uin = u.e[2] >= NEAR, vin = v.e[2] >= NEAR;
|
|
1235
|
+
if (uin) cut.push(u);
|
|
1236
|
+
if (uin !== vin) {
|
|
1237
|
+
const k = (NEAR - u.e[2]) / (v.e[2] - u.e[2]);
|
|
1238
|
+
cut.push({ e: u.e.map((x, j) => x + (v.e[j] - x) * k), f: u.f + (v.f - u.f) * k, g: u.g + (v.g - u.g) * k });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
poly = cut;
|
|
1242
|
+
if (poly.length < 3) return;
|
|
1243
|
+
}
|
|
1244
|
+
const pt = [];
|
|
1245
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
1246
|
+
for (const k of poly) {
|
|
1247
|
+
const x = W / 2 + H * k.e[0] / k.e[2], y = yh * H + H * k.e[1] / k.e[2] - cam.y * H;
|
|
1248
|
+
pt.push([x, y, k.f, k.g, k.e[2]]);
|
|
1249
|
+
if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y;
|
|
1250
|
+
}
|
|
1251
|
+
if (x1 < view[0] || x0 > view[2] || y1 < view[1] || y0 > view[3]) return;
|
|
1252
|
+
each(pt);
|
|
1253
|
+
};
|
|
1254
|
+
for (let i = 0; i < R; i++) for (let j = 0; j < C; j++) { const n = i * (C + 1) + j; cell(grid[n], grid[n + 1], grid[n + C + 2], grid[n + C + 1], 0); }
|
|
1255
|
+
}
|
|
1256
|
+
// The land (spec.relief, drawn as the planes draped over it) hides what stands behind its rises.
|
|
1257
|
+
// Over the columns of view [x0, y0, x1, y1] (the frame's px: what is seen; one every LAND_PX), for
|
|
1258
|
+
// LAND_STEPS depths: how high on the frame the land nearer than each reaches — the land as it is
|
|
1259
|
+
// drawn this frame: `cells`, those of its draped planes (cellsOf; what lies below the view hides
|
|
1260
|
+
// nothing seen, so they may leave it out) — so an element D away (along the gaze) is seen only
|
|
1261
|
+
// above the line at the depth just in front of it. Null without them. (The line half a pixel low:
|
|
1262
|
+
// what is cut and the land's edge meet with no pixel of sky between.)
|
|
1263
|
+
const LAND_PX = 4, LAND_STEPS = 80, LAND_NEAR = 0.1, LAND_GAP = 0.1, LAND_NONE = 1e5;
|
|
1264
|
+
function landLine([x0, , x1], cells) {
|
|
1265
|
+
if (!cells.length) return null;
|
|
1266
|
+
const k = Math.pow(Z_FAR / LAND_NEAR, 1 / (LAND_STEPS - 1)), zs = Array.from({ length: LAND_STEPS }, (_, i) => LAND_NEAR * Math.pow(k, i));
|
|
1267
|
+
const cols = Math.max(2, Math.ceil((x1 - x0) / LAND_PX)), dx = (x1 - x0) / cols, top = new Float32Array((cols + 1) * LAND_STEPS).fill(LAND_NONE);
|
|
1268
|
+
// each cell as drawn: where its edges cross each column, at the depth there — a point of the land
|
|
1269
|
+
// there is in front of every depth from the first at or past it (its step: the log of its depth,
|
|
1270
|
+
// eased along the edge)
|
|
1271
|
+
const step = z => Math.log(Math.max(z, LAND_NEAR) / LAND_NEAR) / Math.log(k);
|
|
1272
|
+
for (const pts of cells) {
|
|
1273
|
+
for (let i = 0; i < pts.length; i++) {
|
|
1274
|
+
let l = pts[i], r = pts[(i + 1) % pts.length];
|
|
1275
|
+
if (l[0] === r[0]) continue;
|
|
1276
|
+
if (l[0] > r[0]) [l, r] = [r, l];
|
|
1277
|
+
const sl = step(l[4]), sr = step(r[4]), w = r[0] - l[0];
|
|
1278
|
+
for (let j = Math.max(0, Math.ceil((l[0] - x0) / dx)), last = Math.min(cols, Math.floor((r[0] - x0) / dx)); j <= last; j++) {
|
|
1279
|
+
const u = (x0 + j * dx - l[0]) / w, y = l[1] + (r[1] - l[1]) * u, n = j * LAND_STEPS + Math.min(LAND_STEPS - 1, Math.max(0, Math.ceil(sl + (sr - sl) * u - 1e-9)));
|
|
1280
|
+
if (y < top[n]) top[n] = y;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
for (let j = 0; j <= cols; j++) for (let i = 1, n = j * LAND_STEPS + 1; i < LAND_STEPS; i++, n++) if (top[n - 1] < top[n]) top[n] = top[n - 1];
|
|
617
1285
|
return {
|
|
618
|
-
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1286
|
+
// the line in front of depth D over the columns from xa to xb: [[x, y], …], or null if nowhere
|
|
1287
|
+
// there it comes above `bottom` (y: the lowest the element reaches on the frame)
|
|
1288
|
+
at(D, xa, xb, bottom) {
|
|
1289
|
+
let n = zs.findIndex(z => z > D - LAND_GAP);
|
|
1290
|
+
n = n < 0 ? LAND_STEPS - 1 : n - 1;
|
|
1291
|
+
if (n < 0 || xb <= x0 || xa >= x1) return null;
|
|
1292
|
+
const j0 = Math.max(0, Math.floor((xa - x0) / dx)), j1 = Math.min(cols, Math.ceil((xb - x0) / dx));
|
|
1293
|
+
const pts = [];
|
|
1294
|
+
let hides = false;
|
|
1295
|
+
for (let j = j0; j <= j1; j++) { const y = top[j * LAND_STEPS + n] + 0.5; pts.push([x0 + j * dx, y]); if (y < bottom) hides = true; }
|
|
1296
|
+
return hides ? pts : null;
|
|
625
1297
|
},
|
|
626
1298
|
};
|
|
627
1299
|
}
|
|
1300
|
+
|
|
628
1301
|
// Screen transform [a b c d e f] for a plane at distance Z (Infinity = the sky): maps the
|
|
629
1302
|
// scene as painted from the starting spot (on the stage) to the frame (W × H) from where the
|
|
630
1303
|
// camera is now.
|
|
@@ -655,14 +1328,17 @@ const Moood = (() => {
|
|
|
655
1328
|
}
|
|
656
1329
|
// Where an element's painting (as depths() places it on the SW-wide stage) goes on the frame from
|
|
657
1330
|
// camera cam, where it is now: { m: the transform, D: its distance along the gaze }, or null behind the eye
|
|
658
|
-
function placeOf(o, cam, W, H, dz, round) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1331
|
+
function placeOf(o, cam, W, H, dz, round) { return seenOf(cam, dz.now(o), W, H, dz.yh, round); }
|
|
1332
|
+
// where something standing — n: { o, at, Z }, a now() of it (depths: the element, or a part of it) —
|
|
1333
|
+
// is seen from cam: { m, D }, or null (behind the eye, looking around)
|
|
1334
|
+
function seenOf(cam, n, W, H, yh, round) {
|
|
1335
|
+
return round ? billboard(cam, n.at, n.o.x * STAGE * H, n.o.y * H, W, H, yh) : { m: view(cam, n.Z, W, H, yh), D: n.Z - cam.z };
|
|
662
1336
|
}
|
|
663
1337
|
// The hero of a platformer h = { dx, dy (metres from where it is painted), flip } as a transform
|
|
664
1338
|
// of its painting: its anchor painted at x = ax, `ppm` painted px per metre
|
|
665
1339
|
const heroMatrix = (h, ax, ppm) => [h.flip ? -1 : 1, 0, 0, 1, (h.flip ? 2 * ax : 0) + h.dx * ppm, -h.dy * ppm];
|
|
1340
|
+
// (2D) affine m after n: [a b c d e f] each
|
|
1341
|
+
const mul = (m, n) => [m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1], m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3], m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5]];
|
|
666
1342
|
// The full circle of the sky looking around: painted 2π stage heights wide, straight ahead in
|
|
667
1343
|
// its middle; seen through the frame as a cylinder.
|
|
668
1344
|
const TAU = 2 * Math.PI;
|
|
@@ -707,7 +1383,7 @@ const Moood = (() => {
|
|
|
707
1383
|
function limits(spec, W, H) {
|
|
708
1384
|
const { Z, objectZ, objectX } = depths(spec), aspect = W / H;
|
|
709
1385
|
const land = (spec.layers || []).map((L, i) => (LAND.has(L.kind) ? Z[i] : null)).filter(z => z !== null);
|
|
710
|
-
const things = (spec.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane).map(o => {
|
|
1386
|
+
const things = (spec.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane && !o.insert).map(o => {
|
|
711
1387
|
const z = objectZ(o), b = Array.isArray(o.box) && o.box.length === 4 ? (o.box[0] + o.box[2]) / 2 / EYE : 0;
|
|
712
1388
|
return { x: (objectX(o) - 0.5) * STAGE * z + b, z };
|
|
713
1389
|
});
|
|
@@ -721,7 +1397,7 @@ const Moood = (() => {
|
|
|
721
1397
|
if (setup.mode === 'roam') {
|
|
722
1398
|
if (setup.area) { const [l, r, b, f] = setup.area; room = [-l, r, -b, f]; }
|
|
723
1399
|
else {
|
|
724
|
-
const dz = depths(spec), at = (spec.objects || []).filter(o => !o.sky && !o.ground && o.kind !== 'bird').map(o => dz.objectAt(o)).filter(p => Math.hypot(p.X, p.Z) <= ROOM);
|
|
1400
|
+
const dz = depths(spec), at = (spec.objects || []).filter(o => !o.sky && !o.ground && !o.insert && o.kind !== 'bird').map(o => dz.objectAt(o)).filter(p => Math.hypot(p.X, p.Z) <= ROOM);
|
|
725
1401
|
const xs = [0, ...at.map(p => p.X)], zs = [0, ...at.map(p => p.Z)], k = 1 / EYE;
|
|
726
1402
|
room = [Math.min(0, Math.min(...xs) + k), Math.max(0, Math.max(...xs) - k), Math.min(0, Math.min(...zs) + k), Math.max(0, Math.max(...zs) - k)];
|
|
727
1403
|
}
|
|
@@ -772,8 +1448,13 @@ const Moood = (() => {
|
|
|
772
1448
|
const hw = Math.max(0, cam.z - zb) * lim.aspect / 2;
|
|
773
1449
|
cam.x = clamp(cam.x, Math.max(lo, -hw), Math.min(hi, hw));
|
|
774
1450
|
}
|
|
775
|
-
//
|
|
776
|
-
|
|
1451
|
+
// Near things: an element D away along the gaze (eye heights) fades over the last steps before
|
|
1452
|
+
// the eye — from PASS_TO (60 cm) to PASS_FROM (20 cm), as you walk through a curtain of leaves: a
|
|
1453
|
+
// painted card is not to pass the eye magnified many times over. Nearer than that, and behind the
|
|
1454
|
+
// eye, it is not drawn. One beside the way (a post, a figure a metre aside) slides out of the frame
|
|
1455
|
+
// whole before it comes so near.
|
|
1456
|
+
const PASS_FROM = 0.12, PASS_TO = 0.35;
|
|
1457
|
+
const nearFade = D => clamp((D - PASS_FROM) / (PASS_TO - PASS_FROM), 0, 1);
|
|
777
1458
|
|
|
778
1459
|
function place(ctx, S, cam, Z) { ctx.transform(...view(cam, Z, S.W, S.H, S.yh)); }
|
|
779
1460
|
|
|
@@ -803,7 +1484,8 @@ const Moood = (() => {
|
|
|
803
1484
|
// through the world (`place`) is put where the state has it as each frame starts (move)
|
|
804
1485
|
const round = camSetup(spec).round, farFirst = (a, b) => (b.Z - a.Z) || 0;
|
|
805
1486
|
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);
|
|
1487
|
+
const objects = (spec.objects || []).filter(o => !o.insert).map(o => ({ from: o, ...at(o) })).sort(farFirst), moving = objects.filter(ob => ob.from.place);
|
|
1488
|
+
const inserts = (spec.objects || []).filter(o => o.insert).map(o => ({ ...o, _yh: dz.yh }));
|
|
807
1489
|
const move = () => { if (!moving.length) return; for (const ob of moving) Object.assign(ob, at(ob.from)); objects.sort(farFirst); };
|
|
808
1490
|
const sky = !spec.sky ? '#000000' : (() => {
|
|
809
1491
|
const c = document.createElement('canvas').getContext('2d');
|
|
@@ -812,20 +1494,151 @@ const Moood = (() => {
|
|
|
812
1494
|
return g;
|
|
813
1495
|
})();
|
|
814
1496
|
const particles = (spec.particles || []).map(p => ({ p, list: makeParticles(p, W, H, rnd) }));
|
|
815
|
-
// vignette depends only on the
|
|
1497
|
+
// vignette depends only on the frame: painted once, full, laid as strong as the light says
|
|
816
1498
|
const vignette = layer(W, H), vg = vignette.getContext('2d');
|
|
817
1499
|
const v = vg.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.35, W / 2, H / 2, Math.hypot(W, H) * 0.6);
|
|
818
1500
|
v.addColorStop(0, 'rgba(0,0,0,0)');
|
|
819
|
-
v.addColorStop(1,
|
|
1501
|
+
v.addColorStop(1, 'rgba(0,0,0,0.75)');
|
|
820
1502
|
vg.fillStyle = v; vg.fillRect(0, 0, W, H);
|
|
821
|
-
|
|
1503
|
+
const ids = new Set(objects.map(ob => ob.from.id).filter(id => id != null)), clippers = new Set();
|
|
1504
|
+
for (const ob of objects) { ob.clip = clipOf(ob.from, ids); for (const id of ob.clip?.ids || []) clippers.add(id); }
|
|
1505
|
+
// planes where they lie (planeOf); the land: those draped over the relief
|
|
1506
|
+
for (const ob of objects) if (ob.o.plane) ob.plane = planeOf(ob.from, dz);
|
|
1507
|
+
// the shadows each element casts (by its id)
|
|
1508
|
+
const casts = new Map();
|
|
1509
|
+
for (const sh of shadowsOf(spec)) casts.set(sh.id, [...(casts.get(sh.id) || []), sh]);
|
|
1510
|
+
return { W, H, SW, u, x0, span, yh: dz.yh, dz, round, lim: limits(spec, W, H), sky, stars, layers, particles, objects, inserts, move, vignette, scratch: layer(W, H), rules: orderRules(spec), casts, clippers, lands: objects.filter(ob => ob.plane?.drape), mirrors: objects.filter(ob => ob.plane?.mirror && !ob.from.unseen).map(ob => ob.plane), painted: [] };
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// ── light and shadows with Canvas 2D ───────────────────────────────────
|
|
1514
|
+
// (the fallback: lit and shadowed looking ahead only; the light laid by the screen, not by depth)
|
|
1515
|
+
// canvas-sized layers of a 2D stage, made once: [canvas, its context]
|
|
1516
|
+
function layer2d(S, name) {
|
|
1517
|
+
const o = S.out;
|
|
1518
|
+
if (!S[name]) S[name] = layer(o.cw, o.ch);
|
|
1519
|
+
return [S[name], S[name].getContext('2d')];
|
|
1520
|
+
}
|
|
1521
|
+
// the elements' lights as sources (sourcesNow), where they were last painted (S.painted: { o, lights }
|
|
1522
|
+
// — each element, and each part of one, as standing() drew it)
|
|
1523
|
+
function sources2d(S, spec) {
|
|
1524
|
+
const from = [];
|
|
1525
|
+
for (const { o, lights } of S.painted) {
|
|
1526
|
+
const [ax, ay, k] = anchorOf(o, S.SW, S.H);
|
|
1527
|
+
for (const l of lights) {
|
|
1528
|
+
const at = worldOf(o, (l.x - ax) / k, (l.y - ay) / k);
|
|
1529
|
+
if (at) from.push({ id: l.id ?? String(o.id ?? o.kind), at, reach: 2 * l.r / k / EYE, rgb: l.col.split(',').map(Number), strength: l.a });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return sourcesNow(spec, S.dz, from);
|
|
1533
|
+
}
|
|
1534
|
+
// (2D) A lit scene's light: what is lit (the land, what stands, particles) is drawn on a layer of its
|
|
1535
|
+
// own (L.g), and the light laid on it — each time something unlit comes (L.flush: a halo, what
|
|
1536
|
+
// glows; and at the end), so that what is unlit is over what is behind it and under what is in
|
|
1537
|
+
// front of it, as in WebGL. L.dirty: drawn on since. Not lit: L.g null, ctx drawn on.
|
|
1538
|
+
function lit2d(ctx, S, look, cam) {
|
|
1539
|
+
const L = { g: null, dirty: false, look, flush() {} };
|
|
1540
|
+
if (!S.lit) return L;
|
|
1541
|
+
const { out } = S, [lc, g] = layer2d(S, 'litLayer');
|
|
1542
|
+
g.setTransform(1, 0, 0, 1, 0, 0); g.clearRect(0, 0, out.cw, out.ch); g.translate(out.x, out.y);
|
|
1543
|
+
let map = null;
|
|
1544
|
+
L.g = g;
|
|
1545
|
+
L.flush = () => {
|
|
1546
|
+
if (!L.dirty) return;
|
|
1547
|
+
L.dirty = false;
|
|
1548
|
+
map ||= lightMap(S, look, cam);
|
|
1549
|
+
// the light only where something is painted, multiplied in; then put down, the layer cleared
|
|
1550
|
+
const [laid, lg] = layer2d(S, 'lightLaid');
|
|
1551
|
+
lg.globalCompositeOperation = 'copy'; lg.drawImage(map, 0, 0);
|
|
1552
|
+
lg.globalCompositeOperation = 'destination-in'; lg.drawImage(lc, 0, 0);
|
|
1553
|
+
g.save(); g.setTransform(1, 0, 0, 1, 0, 0);
|
|
1554
|
+
g.globalCompositeOperation = 'multiply'; g.drawImage(laid, 0, 0);
|
|
1555
|
+
ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.drawImage(lc, 0, 0); ctx.restore();
|
|
1556
|
+
g.globalCompositeOperation = 'source-over'; g.clearRect(0, 0, out.cw, out.ch); g.restore();
|
|
1557
|
+
};
|
|
1558
|
+
return L;
|
|
1559
|
+
}
|
|
1560
|
+
// the light as it falls this frame: the ambient, the point sources as soft discs where they are seen
|
|
1561
|
+
function lightMap(S, look, cam) {
|
|
1562
|
+
const { W, H, out } = S, [map, mg] = layer2d(S, 'lightMap'), amb = ambientOf(look, S.sources).map(v => v * 255);
|
|
1563
|
+
// (its sources as much as the scene is lit)
|
|
1564
|
+
mg.setTransform(1, 0, 0, 1, 0, 0); mg.globalCompositeOperation = 'source-over';
|
|
1565
|
+
mg.fillStyle = `rgb(${amb.map(v => Math.round(clamp(v, 0, 255))).join(',')})`; mg.fillRect(0, 0, map.width, map.height);
|
|
1566
|
+
mg.globalCompositeOperation = 'lighter';
|
|
1567
|
+
for (const s of S.sources) {
|
|
1568
|
+
const p = !s.dir && project(cam, ...s.at, W, H, S.yh);
|
|
1569
|
+
if (!p || s.strength < 0.002) continue;
|
|
1570
|
+
const r = s.reach * H / p[2], x = p[0] + out.x, y = p[1] + out.y, g = mg.createRadialGradient(x, y, 0, x, y, r);
|
|
1571
|
+
// (1 − x²)²: as the light falls off (in its own colour: past full it can't brighten here)
|
|
1572
|
+
for (const u of [0, 0.25, 0.5, 0.75, 1]) g.addColorStop(u, `rgba(${s.rgb.join(',')},${Math.min(1, s.strength * look.lit * falloff(u))})`);
|
|
1573
|
+
mg.fillStyle = g; mg.fillRect(x - r, y - r, 2 * r, 2 * r);
|
|
1574
|
+
}
|
|
1575
|
+
mg.globalCompositeOperation = 'source-over';
|
|
1576
|
+
return map;
|
|
1577
|
+
}
|
|
1578
|
+
// the affine map taking three points a to three points b ([x, y] each): [a b c d e f]
|
|
1579
|
+
function affine3(a, b) {
|
|
1580
|
+
const [[x0, y0], [x1, y1], [x2, y2]] = a, det = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0);
|
|
1581
|
+
if (Math.abs(det) < 1e-9) return null;
|
|
1582
|
+
const solve = (v0, v1, v2) => { const p = ((v1 - v0) * (y2 - y0) - (v2 - v0) * (y1 - y0)) / det, q = ((x1 - x0) * (v2 - v0) - (x2 - x0) * (v1 - v0)) / det; return [p, q, v0 - p * x0 - q * y0]; };
|
|
1583
|
+
const [A, C, E] = solve(b[0][0], b[1][0], b[2][0]), [B, D, F] = solve(b[0][1], b[1][1], b[2][1]);
|
|
1584
|
+
return [A, B, C, D, E, F];
|
|
1585
|
+
}
|
|
1586
|
+
// An element's shadows (shadowsOf), with Canvas 2D — or a part's, its element's: its painting (the
|
|
1587
|
+
// item's own canvas, standing()) laid where the light throws three points of its box — an affine
|
|
1588
|
+
// likeness of the true shadow — black and blurred, onto g before it (on an upright element: within
|
|
1589
|
+
// its box; on a plane: within it as it lies). g draws in the canvas's pixels; items are in the frame's
|
|
1590
|
+
function shadows2d(g, S, it, list, cam) {
|
|
1591
|
+
const { W, H, SW, out } = S, o = it.o, m = it.m, b = objectBox(o, SW, H), [ax, ay, k] = anchorOf(o, SW, H);
|
|
1592
|
+
const world = (px, py) => worldOf(o, (px - ax) / k, (py - ay) / k), A0 = world(ax, ay), A = { X: A0[0], Y: A0[1], Z: A0[2] };
|
|
1593
|
+
// (its foot at the anchor: what stands on the ground touches its shadow there)
|
|
1594
|
+
const pts = [[b.bx, ay], [b.bx + b.bw, ay], [b.bx + b.bw / 2, b.by]];
|
|
1595
|
+
for (const sh of S.casts.get(String(it.id)) || []) {
|
|
1596
|
+
const src = shadowLight(S.sources, sh, A);
|
|
1597
|
+
if (!src) continue;
|
|
1598
|
+
const on = sh.onto.filter(id => id !== 'ground').map(id => list.find(x => !x.part && String(x.id) === id)).filter(Boolean), walls = on.filter(w => !w.plane);
|
|
1599
|
+
for (const id of sh.onto) {
|
|
1600
|
+
const R = id === 'ground' ? null : on.find(w => String(w.id) === id);
|
|
1601
|
+
if (id !== 'ground' && !R) continue;
|
|
1602
|
+
const pl = R?.plane ? { q: [R.plane.Xc, R.plane.Y, R.plane.Zc], n: [0, 1, 0] } : R ? { q: worldOf(R.o, 0, 0), n: [0, 0, 1] } : { q: [0, groundAt(o._at.across, o._at.distance) / EYE, 0], n: [0, 1, 0] };
|
|
1603
|
+
const hits = pts.map(([px, py]) => rayHit(pl, world(px, py), src.at, src.dir));
|
|
1604
|
+
const seen = hits.map(h => h && project(cam, ...h[0], W, H, S.yh));
|
|
1605
|
+
if (seen.some(q => !q)) continue;
|
|
1606
|
+
const T = affine3(pts.map(([px, py]) => [m[0] * px + m[4] - it.sx, m[3] * py + m[5] - it.sy]), seen.map(q => [q[0] + out.x, q[1] + out.y]));
|
|
1607
|
+
if (!T) continue;
|
|
1608
|
+
// as dark as at its foot
|
|
1609
|
+
const [sc, sg] = layer2d(S, 'shadowLayer'), top = hits[2], a = shadowAt(sh, src, hits[0][0], S.ambient, S.lit);
|
|
1610
|
+
if (a < 0.005) continue;
|
|
1611
|
+
sg.setTransform(1, 0, 0, 1, 0, 0); sg.globalCompositeOperation = 'source-over'; sg.clearRect(0, 0, sc.width, sc.height);
|
|
1612
|
+
sg.setTransform(...T); sg.drawImage(it.canvas, 0, 0);
|
|
1613
|
+
sg.setTransform(1, 0, 0, 1, 0, 0); sg.globalCompositeOperation = 'source-in'; sg.fillStyle = '#000'; sg.fillRect(0, 0, sc.width, sc.height);
|
|
1614
|
+
g.save(); g.setTransform(1, 0, 0, 1, 0, 0);
|
|
1615
|
+
// on a plane: within it; on a wall: within its box; on the ground: below the walls' feet it
|
|
1616
|
+
// also falls on
|
|
1617
|
+
if (R?.plane) {
|
|
1618
|
+
g.beginPath();
|
|
1619
|
+
cellsOf(cam, onPlane(R.plane), 1, 1, W, H, S.yh, [-out.x, -out.y, out.cw - out.x, out.ch - out.y], pts => { g.moveTo(pts[0][0] + out.x, pts[0][1] + out.y); for (const q of pts) g.lineTo(q[0] + out.x, q[1] + out.y); g.closePath(); });
|
|
1620
|
+
g.clip();
|
|
1621
|
+
} else if (R) {
|
|
1622
|
+
const rm = R.m, rb = R.box;
|
|
1623
|
+
g.beginPath(); g.rect(rm[0] * rb.bx + rm[4] + out.x, rm[3] * rb.by + rm[5] + out.y, rm[0] * rb.bw, rm[3] * rb.bh); g.clip();
|
|
1624
|
+
} else if (walls.length) {
|
|
1625
|
+
const feet = Math.max(...walls.map(w => w.m[3] * w.o.y * H + w.m[5]));
|
|
1626
|
+
g.beginPath(); g.rect(0, feet + out.y, out.cw, out.ch); g.clip();
|
|
1627
|
+
}
|
|
1628
|
+
// a penumbra as wide as the light seen from the caster's top, over the way to the shadow
|
|
1629
|
+
const blur = sh.soft * (src.dir ? top[1] : top[1] - 1) * H / seen[2][2] / 2;
|
|
1630
|
+
g.globalAlpha = a; if (blur > 0.5) g.filter = `blur(${blur.toFixed(1)}px)`;
|
|
1631
|
+
g.drawImage(sc, 0, 0);
|
|
1632
|
+
g.restore();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
822
1635
|
}
|
|
823
1636
|
|
|
824
1637
|
// Looking around, with Canvas 2D (the fallback): the sky turned as a flat band (not bent into a
|
|
825
|
-
// cylinder)
|
|
826
|
-
//
|
|
827
|
-
function
|
|
828
|
-
const { W, H
|
|
1638
|
+
// cylinder); then what stands about (standing()). Cards turned their own way (`facing`) are drawn
|
|
1639
|
+
// by the WebGL renderer only.
|
|
1640
|
+
function skyAround(ctx, S, cam, t) {
|
|
1641
|
+
const { W, H } = S, full = TAU * H, off = W / 2 - full / 2 - cam.yaw * H;
|
|
829
1642
|
for (const { o } of S.objects) {
|
|
830
1643
|
if (!o.sky) continue;
|
|
831
1644
|
for (let x0 = ((off % full) + full) % full - full; x0 < W; x0 += full) {
|
|
@@ -835,30 +1648,279 @@ const Moood = (() => {
|
|
|
835
1648
|
ctx.restore();
|
|
836
1649
|
}
|
|
837
1650
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// What stands in the scene, with Canvas 2D: far to near (as the elements say: behind, inFront),
|
|
1654
|
+
// each cut by what it is to be seen inside or out of (clip), and by the land in front of it. An
|
|
1655
|
+
// element painting parts (e.part) is painted first, it and each part in a canvas of its own, and
|
|
1656
|
+
// those drawn among the rest at their own depths (as is one that casts a shadow or cuts another:
|
|
1657
|
+
// its canvas is what is cast, and the mask). Planes lie among them, each at its farthest. Everything
|
|
1658
|
+
// here is in the frame's pixels. A lit scene: what is lit is drawn on L's layer (lit2d), what glows
|
|
1659
|
+
// and the halos straight on ctx, between. One that casts a shadow (and each of its parts) has it
|
|
1660
|
+
// drawn just before it; the halos of the scene's own lights at their depth among the rest.
|
|
1661
|
+
function standing(ctx, S, cam, t, L) {
|
|
1662
|
+
const { W, H, SW, dz, out } = S, items = [], painted = S.painted = [], lit = L.g;
|
|
1663
|
+
// o as seen from cam: an item to draw ({ id, o, m, D, Z: what orders it, box, fade, clip, unseen }), or null
|
|
1664
|
+
const itemOf = (ob, o, p, Z) => {
|
|
1665
|
+
const at = seenOf(cam, { o, at: p, Z }, W, H, S.yh, S.round);
|
|
1666
|
+
if (!at) return null;
|
|
1667
|
+
const box = objectBox(o, SW, H), fade = nearFade(at.D);
|
|
1668
|
+
// (a platformer's hero: moved and turned from where it is painted)
|
|
1669
|
+
const m = cam.hero && o.id === cam.hero.id ? mul(at.m, heroMatrix(cam.hero, o.x * SW, o.size * H / (o.height || 2))) : at.m;
|
|
1670
|
+
return fade > 0 ? { id: ob.from.id, ob, o, m, D: at.D, Z: S.round ? at.D : Z, box, fade, clip: ob.clip, unseen: !!ob.from.unseen } : null;
|
|
1671
|
+
};
|
|
1672
|
+
for (const ob of S.objects) {
|
|
1673
|
+
const o = ob.o;
|
|
1674
|
+
if (o.plane) {
|
|
1675
|
+
const P = ob.plane;
|
|
1676
|
+
if (!ob.from.unseen) items.push({ id: ob.from.id, ob, o, plane: P, Z: S.round ? Math.hypot(P.Xc - cam.x, P.Zc - cam.z) + Math.max(P.w, P.d) / 2 : ob.Z });
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (S.round && (o.sky || o.ground || typeof o.facing === 'number')) continue;
|
|
1680
|
+
const it = itemOf(ob, o, ob.p, ob.Z), id = String(ob.from.id);
|
|
1681
|
+
// (out of sight — behind the eye, passed — it goes on all the same, as a frame unseen paints it)
|
|
1682
|
+
if (!S.casts.has(id) && !S.clippers.has(id) && (ob.parted === false || (o.kind && o.kind !== 'custom'))) { if (it) items.push(it); else unseen2d(S, o, t); continue; }
|
|
1683
|
+
// painted now, it and its parts, each where it is seen (painted all the same when it is out of
|
|
1684
|
+
// sight: its parts may not be)
|
|
1685
|
+
const recs = [], whole = prepaint(S, ob, 0, it || { o, m: [1, 0, 0, 1, 0, 0] }, t, painted, recs);
|
|
1686
|
+
ob.parted = recs.length > 0;
|
|
1687
|
+
// (its canvases kept only while it needs them: one painting no parts, casting no shadow, cutting
|
|
1688
|
+
// nothing is drawn straight from the next frame on — this frame's item holds its canvas till drawn)
|
|
1689
|
+
if (!ob.parted && !S.casts.has(id) && !S.clippers.has(id)) ob.canvases = null;
|
|
1690
|
+
else ob.canvases.length = recs.length + 1;
|
|
1691
|
+
if (it && whole) items.push(whole);
|
|
1692
|
+
recs.forEach((r, i) => {
|
|
1693
|
+
const n = dz.part(ob.from, r.dx, r.dz, r.box), pit = itemOf(ob, n.o, n.at, n.Z), done = pit && prepaint(S, ob, i + 1, pit, t, painted, null, r);
|
|
1694
|
+
if (done) items.push(done);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
items.sort((a, b) => (b.Z - a.Z) || 0);
|
|
1698
|
+
arrange(items, S.rules, it => it.id);
|
|
1699
|
+
// the land hiding what stands behind its rises: for the camera as it is (kept while it stands)
|
|
1700
|
+
const key = SHARED.ground && `${cam.x}|${cam.y}|${cam.z}|${cam.h}|${cam.yaw}`;
|
|
1701
|
+
// (the land's cells as they are drawn this frame: plane2d)
|
|
1702
|
+
const seen = [-out.x, -out.y, out.cw - out.x, out.ch - out.y];
|
|
1703
|
+
for (const ob of S.lands) { ob.cells = []; cellsOf(cam, onPlane(ob.plane), ...PLANE_CELLS['2d'], W, H, S.yh, [seen[0], -Infinity, seen[2], seen[3]], pts => ob.cells.push(pts)); }
|
|
1704
|
+
if (key !== S.landKey) { S.landKey = key; S.land = key ? landLine(seen, S.lands.flatMap(ob => ob.cells)) : null; }
|
|
1705
|
+
const land = S.land;
|
|
1706
|
+
const far = h => (S.round ? Math.hypot(h.at[0] - cam.x, h.at[2] - cam.z) : h.at[2]);
|
|
1707
|
+
const look = L.look, halos = airOf(S.sources, look, far);
|
|
1708
|
+
const halo = ({ h, fog }) => {
|
|
1709
|
+
const p = project(cam, ...h.at, W, H, S.yh);
|
|
1710
|
+
if (!p) return;
|
|
1711
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
|
1712
|
+
if (fog) glow(ctx, p[0], p[1], h.reach * H / p[2], h.rgb.join(','), airGlow(look, h, p[2], t));
|
|
1713
|
+
else {
|
|
1714
|
+
const r = h.halo * H / p[2], a = Math.min(1, h.strength) * flickerNow(h, t);
|
|
1715
|
+
glow(ctx, p[0], p[1], r, h.rgb.join(','), 0.6 * a); glow(ctx, p[0], p[1], r / 4, '255,245,220', 0.8 * a);
|
|
1716
|
+
}
|
|
850
1717
|
ctx.restore();
|
|
1718
|
+
};
|
|
1719
|
+
const masks = new Map();
|
|
1720
|
+
// (in a lit scene what is unlit — a halo, what glows — is painted over what the light has been
|
|
1721
|
+
// laid on so far, under what is in front of it: L.flush)
|
|
1722
|
+
// the reflecting planes' mirrors, made before any is drawn: what stands nearer is in them too
|
|
1723
|
+
const mirrors = new Map(items.filter(it => it.plane?.mirror).map(it => [it, mirror2d(S, it, items, cam, t)]));
|
|
1724
|
+
for (const it of items) {
|
|
1725
|
+
if (halos.length && halos[0].Z >= it.Z) { L.flush(); while (halos.length && halos[0].Z >= it.Z) halo(halos.shift()); }
|
|
1726
|
+
if (it.unseen) continue;
|
|
1727
|
+
if (lit && it.o.glows) L.flush();
|
|
1728
|
+
const g = lit && !it.o.sky && !it.o.glows ? lit : ctx;
|
|
1729
|
+
if (g === lit) L.dirty = true;
|
|
1730
|
+
if (it.plane) {
|
|
1731
|
+
plane2d(g, it, S, cam, t, painted);
|
|
1732
|
+
if (mirrors.has(it)) onPlane2d(g, S, it.plane, cam, () => { g.globalAlpha = it.plane.mirror.strength; g.filter = `blur(${(it.plane.mirror.blur * 0.1 * H).toFixed(1)}px)`; g.drawImage(mirrors.get(it), 0, 0); });
|
|
1733
|
+
if (it.plane.pool && S.lit < 1) onPlane2d(g, S, it.plane, cam, () => pool2d(g, S, it.plane, cam, t));
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
const cut = land && !it.o.sky && !it.o.ground ? landCut(land, it) : null;
|
|
1737
|
+
if (cut === false) continue;
|
|
1738
|
+
if (it.canvas && S.casts.has(String(it.id))) shadows2d(g, S, it, items, cam);
|
|
1739
|
+
// on water: a mirrored, fainter copy about the water surface under it, fading away from it (as
|
|
1740
|
+
// gl.js draws it) — not over a reflecting plane: that mirrors it
|
|
1741
|
+
const copy = !S.round && !it.part && (it.o.reflect ?? (it.o.kind === 'boat' || it.o.kind === 'bridge')), A = copy && worldOf(it.o, 0, 0);
|
|
1742
|
+
if (copy && !(A && overMirror(S.mirrors, { X: A[0], Y: A[1], Z: A[2] }))) {
|
|
1743
|
+
const m = it.m, gy = m[3] * (isFinite(it.Z) ? Math.max(it.o.y * H, (S.yh + 1 / it.Z) * H) : S.yh * H) + m[5];
|
|
1744
|
+
g.save(); g.setTransform(1, 0, 0, 1, 0, 0); g.globalAlpha = 0.3 * it.fade;
|
|
1745
|
+
g.drawImage(flipped(S, gy, gy - (m[3] * it.box.by + m[5]), 0.27, gc => drawItem(gc, it, S, cam, t)), 0, 0);
|
|
1746
|
+
g.restore();
|
|
1747
|
+
}
|
|
1748
|
+
g.save();
|
|
1749
|
+
if (cut) {
|
|
1750
|
+
g.beginPath(); g.moveTo(cut[0][0], -10 * H);
|
|
1751
|
+
for (const [x, y] of cut) g.lineTo(x, y);
|
|
1752
|
+
g.lineTo(cut.at(-1)[0], -10 * H); g.clip();
|
|
1753
|
+
}
|
|
1754
|
+
if (it.clip) {
|
|
1755
|
+
// in a layer of its own, cut by what the others named paint now (their mask made once a
|
|
1756
|
+
// frame for all it cuts — a plane among them laid as it lies)
|
|
1757
|
+
const ids = it.clip.ids.join('\n');
|
|
1758
|
+
let mask = masks.get(ids);
|
|
1759
|
+
if (!mask) {
|
|
1760
|
+
mask = (S.masks ||= [])[masks.size] ||= layer(W, H);
|
|
1761
|
+
const mg = mask.getContext('2d');
|
|
1762
|
+
mg.clearRect(0, 0, W, H);
|
|
1763
|
+
for (const m of items) if (it.clip.ids.includes(m.id)) { if (m.plane) plane2d(mg, m, S, cam, t, []); else drawItem(mg, m, S, cam, t); }
|
|
1764
|
+
masks.set(ids, mask);
|
|
1765
|
+
}
|
|
1766
|
+
const lay = S.lay ||= layer(W, H), lg = lay.getContext('2d');
|
|
1767
|
+
lg.clearRect(0, 0, W, H);
|
|
1768
|
+
drawItem(lg, it, S, cam, t);
|
|
1769
|
+
lg.globalCompositeOperation = it.clip.inside ? 'destination-in' : 'destination-out';
|
|
1770
|
+
lg.drawImage(mask, 0, 0);
|
|
1771
|
+
lg.globalCompositeOperation = 'source-over';
|
|
1772
|
+
g.drawImage(lay, 0, 0);
|
|
1773
|
+
} else drawItem(g, it, S, cam, t, painted);
|
|
1774
|
+
g.restore();
|
|
1775
|
+
// its glows (e.light, a house's windows) over it, unlit
|
|
1776
|
+
if (lit && it.lights?.length) { L.flush(); ctx.save(); ctx.transform(...it.m); ctx.globalAlpha = it.fade; drawLights(ctx, it.lights); ctx.restore(); }
|
|
1777
|
+
// (one that began painting parts: from the next frame on, they stand at their own depths)
|
|
1778
|
+
if (!it.canvas && READS.get(it.o)?.parts) it.ob.parted = true;
|
|
1779
|
+
}
|
|
1780
|
+
if (halos.length) { L.flush(); halos.forEach(halo); }
|
|
1781
|
+
}
|
|
1782
|
+
// (2D) drawn by draw(g) — in the frame's pixels — mirrored about the line y = line, only below it,
|
|
1783
|
+
// fading over `fade` px below it to `to` (0 by default; fade 0: none): a canvas the size of the canvas
|
|
1784
|
+
function flipped(S, line, fade, to, draw) {
|
|
1785
|
+
const out = S.out, [c, g] = layer2d(S, 'flipped'), y = out.y + line;
|
|
1786
|
+
g.setTransform(1, 0, 0, 1, 0, 0); g.globalCompositeOperation = 'source-over'; g.clearRect(0, 0, c.width, c.height);
|
|
1787
|
+
g.setTransform(1, 0, 0, -1, out.x, out.y + 2 * line);
|
|
1788
|
+
draw(g);
|
|
1789
|
+
g.setTransform(1, 0, 0, 1, 0, 0); g.globalCompositeOperation = 'destination-in';
|
|
1790
|
+
if (fade > 0) { const gr = g.createLinearGradient(0, y, 0, y + fade); gr.addColorStop(0, '#000'); gr.addColorStop(1, `rgba(0,0,0,${to})`); g.fillStyle = gr; } else g.fillStyle = '#000';
|
|
1791
|
+
g.fillRect(0, y, c.width, Math.max(0, c.height - y));
|
|
1792
|
+
g.globalCompositeOperation = 'source-over';
|
|
1793
|
+
return c;
|
|
1794
|
+
}
|
|
1795
|
+
// (2D) a reflecting plane's mirror (planeOf: mirror): what stands above it — each item, and each
|
|
1796
|
+
// halo of the scene's lights — flipped about where the plane's level is seen at its depth, fading out
|
|
1797
|
+
// over the mirror's `fade` above it; a canvas the size of the canvas. (Planes, and what is cut by
|
|
1798
|
+
// others, are not mirrored; its blur is one for all of it.)
|
|
1799
|
+
function mirror2d(S, pl, items, cam, t) {
|
|
1800
|
+
const { H, out } = S, P = pl.plane, M = P.mirror, [c, g] = layer2d(S, `mirror:${pl.id}`);
|
|
1801
|
+
g.setTransform(1, 0, 0, 1, 0, 0); g.clearRect(0, 0, c.width, c.height);
|
|
1802
|
+
const A = (S.yh - cam.y) * H, B = H * ((cam.h ?? 1) - P.Y);
|
|
1803
|
+
const add = (D, draw) => {
|
|
1804
|
+
if (!isFinite(D) && M.fade > 0) return;
|
|
1805
|
+
g.drawImage(flipped(S, isFinite(D) ? A + B / D : A, M.fade * H / D, 0, draw), 0, 0);
|
|
1806
|
+
};
|
|
1807
|
+
for (const it of items) if (!it.plane && !it.unseen && !it.clip && !it.o.ground) add(it.D, gc => drawItem(gc, it, S, cam, t));
|
|
1808
|
+
for (const h of S.sources) {
|
|
1809
|
+
const p = h.halo && h.strength > 0.002 && project(cam, ...h.at, S.W, H, S.yh);
|
|
1810
|
+
if (p) add(p[2], gc => { gc.globalCompositeOperation = 'lighter'; glow(gc, p[0], p[1], h.halo * H / p[2], h.rgb.join(','), 0.6 * Math.min(1, h.strength) * flickerNow(h, t)); });
|
|
1811
|
+
}
|
|
1812
|
+
return c;
|
|
1813
|
+
}
|
|
1814
|
+
// (2D) draw(g) over a plane P only where it lies — g in the canvas's pixels
|
|
1815
|
+
function onPlane2d(g, S, P, cam, draw) {
|
|
1816
|
+
const { W, H, out } = S;
|
|
1817
|
+
g.save(); g.setTransform(1, 0, 0, 1, 0, 0); g.beginPath();
|
|
1818
|
+
cellsOf(cam, onPlane(P), 1, 1, W, H, S.yh, [-out.x, -out.y, out.cw - out.x, out.ch - out.y], pts => { g.moveTo(pts[0][0] + out.x, pts[0][1] + out.y); for (const q of pts) g.lineTo(q[0] + out.x, q[1] + out.y); g.closePath(); });
|
|
1819
|
+
g.clip();
|
|
1820
|
+
draw();
|
|
1821
|
+
g.restore();
|
|
1822
|
+
}
|
|
1823
|
+
// (2D) a `lit` plane P: the scene's point lights lie on it as soft pools, as much as the scene is not
|
|
1824
|
+
// lit — each a disc of the plane under the light as wide as its reach there, laid by three of its
|
|
1825
|
+
// points (an affine likeness)
|
|
1826
|
+
function pool2d(g, S, P, cam, t) {
|
|
1827
|
+
const { W, H, out } = S, k = P.pool * (1 - S.lit);
|
|
1828
|
+
g.globalCompositeOperation = 'lighter';
|
|
1829
|
+
for (const s of S.sources) {
|
|
1830
|
+
const h = s.dir ? -1 : s.at[1] - P.Y;
|
|
1831
|
+
if (h < -0.03 || h >= s.reach || s.strength < 0.002) continue;
|
|
1832
|
+
const r = Math.sqrt(s.reach * s.reach - h * h), C = [s.at[0], P.Y, s.at[2]];
|
|
1833
|
+
const q = [C, [C[0] + r, C[1], C[2]], [C[0], C[1], C[2] + r]].map(p => project(cam, ...p, W, H, S.yh));
|
|
1834
|
+
const T = q.every(Boolean) && affine3([[0, 0], [1, 0], [0, 1]], q.map(p => [p[0] + out.x, p[1] + out.y]));
|
|
1835
|
+
if (!T) continue;
|
|
1836
|
+
const a = k * s.strength * flickerNow(s, t) * falloff(Math.max(0, h) / s.reach), gr = g.createRadialGradient(0, 0, 0, 0, 0, 1);
|
|
1837
|
+
for (const u of [0, 0.25, 0.5, 0.75, 1]) gr.addColorStop(u, `rgba(${s.rgb.join(',')},${Math.min(1, 0.35 * a * falloff(u))})`);
|
|
1838
|
+
g.setTransform(...T); g.fillStyle = gr; g.fillRect(-1, -1, 2, 2);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
// (2D) a plane lying in the scene (it.plane: planeOf): its painting (from above, in metres — the
|
|
1842
|
+
// sky's units, as gl.js paints it) kept in a canvas of its own, repainted as gl.js repaints a live
|
|
1843
|
+
// element (what it read moves; `redraw`), laid in perspective through the renderer's cells
|
|
1844
|
+
// (cellsOf), each an affine triangle or two of it. painted: where its lights go (sources2d)
|
|
1845
|
+
const PLANE_2D = 1024;
|
|
1846
|
+
function plane2d(g, it, S, cam, t, painted) {
|
|
1847
|
+
const { W, H, SW, out } = S, ob = it.ob, P = it.plane, w = P.w * EYE * H, d = P.d * EYE * H;
|
|
1848
|
+
const o = ob.flat ||= { ...ob.from, sky: true, plane: true, _plane: P }, snap = redrawOf(o);
|
|
1849
|
+
const read = ob.texRead || {}, due = !ob.tex || (o.redraw != null ? ob.texSnap !== snap : read.time || (read.state && ob.texVer !== STATE.ver && moved(read)) || (read.lights && ob.texSrc !== SOURCES.ver));
|
|
1850
|
+
if (due) {
|
|
1851
|
+
const k = PLANE_2D / Math.max(w, d), c = ob.tex ||= layer(w * k, d * k), pg = c.getContext('2d'), lights = [];
|
|
1852
|
+
pg.setTransform(1, 0, 0, 1, 0, 0); pg.clearRect(0, 0, c.width, c.height);
|
|
1853
|
+
pg.setTransform(k, 0, 0, k, w / 2 * k, d / 2 * k);
|
|
1854
|
+
drawObject(pg, o, SW, H, t, lights); drawLights(pg, lights);
|
|
1855
|
+
Object.assign(ob, { texLights: lights, texVer: STATE.ver, texSrc: SOURCES.ver, texSnap: snap, texRead: READS.get(o) });
|
|
1856
|
+
PERF.repainted(o);
|
|
851
1857
|
}
|
|
1858
|
+
painted.push({ o, lights: ob.texLights });
|
|
1859
|
+
const c = ob.tex, tris = pts => {
|
|
1860
|
+
for (let i = 1; i + 1 < pts.length; i++) {
|
|
1861
|
+
const tri = [pts[0], pts[i], pts[i + 1]], T = affine3(tri.map(q => [q[2] * c.width, q[3] * c.height]), tri.map(q => [q[0], q[1]]));
|
|
1862
|
+
if (!T) continue;
|
|
1863
|
+
// (a little wider than the triangle: neighbours meet without a seam)
|
|
1864
|
+
const cx = (tri[0][0] + tri[1][0] + tri[2][0]) / 3, cy = (tri[0][1] + tri[1][1] + tri[2][1]) / 3;
|
|
1865
|
+
g.save(); g.beginPath();
|
|
1866
|
+
for (const [x, y] of tri) { const l = Math.hypot(x - cx, y - cy) || 1; g.lineTo(x + (x - cx) / l * 0.7, y + (y - cy) / l * 0.7); }
|
|
1867
|
+
g.clip(); g.transform(...T); g.drawImage(c, 0, 0); g.restore();
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
// (the land's cells made already this frame: standing)
|
|
1871
|
+
if (ob.cells) ob.cells.forEach(tris); else cellsOf(cam, onPlane(P), ...PLANE_CELLS['2d'], W, H, S.yh, [-out.x, -out.y, out.cw - out.x, out.ch - out.y], tris);
|
|
1872
|
+
}
|
|
1873
|
+
// (2D) an element's painting — or, with rec, one of its parts' — made in a canvas of its own as it
|
|
1874
|
+
// is seen (it.m), over the part of the canvas it covers: the item, now drawn as that canvas, or
|
|
1875
|
+
// null when it covers none of it. painted: where its lights go (sources2d); recs: where its parts
|
|
1876
|
+
// go (e.part)
|
|
1877
|
+
function prepaint(S, ob, i, it, t, painted, recs, rec) {
|
|
1878
|
+
const { SW, H, out } = S, box = objectBox(it.o, SW, H), m = it.m;
|
|
1879
|
+
// (m mirrors a platformer's hero turned left)
|
|
1880
|
+
const xs = [m[0] * box.bx + m[4], m[0] * (box.bx + box.bw) + m[4]], ys = [m[3] * box.by + m[5], m[3] * (box.by + box.bh) + m[5]];
|
|
1881
|
+
const xa = Math.max(-out.x, Math.min(...xs)), xb = Math.min(out.cw - out.x, Math.max(...xs));
|
|
1882
|
+
const ya = Math.max(-out.y, Math.min(...ys)), yb = Math.min(out.ch - out.y, Math.max(...ys));
|
|
1883
|
+
const seen = xb - xa >= 1 && yb - ya >= 1, w = seen ? Math.ceil(xb - xa) : 1, h = seen ? Math.ceil(yb - ya) : 1;
|
|
1884
|
+
const c = (ob.canvases ||= [])[i] ||= layer(1, 1), g = c.getContext('2d'), lights = [];
|
|
1885
|
+
if (c.width !== w || c.height !== h) { c.width = w; c.height = h; } else { g.setTransform(1, 0, 0, 1, 0, 0); g.clearRect(0, 0, w, h); }
|
|
1886
|
+
g.setTransform(1, 0, 0, 1, -xa, -ya); g.transform(...m);
|
|
1887
|
+
if (rec) { const [ax, ay, k] = anchorOf(it.o, SW, H); rec.paint(g, ax, ay, k, lights); } else drawObject(g, it.o, SW, H, t, lights, recs);
|
|
1888
|
+
// (in a lit scene its glows are unlit: drawn apart, over it — standing)
|
|
1889
|
+
if (!(S.lit > 0)) drawLights(g, lights);
|
|
1890
|
+
painted.push({ o: it.o, lights });
|
|
1891
|
+
return seen ? { ...it, canvas: c, sx: xa, sy: ya, part: !!rec, lights: S.lit > 0 ? lights : null } : null;
|
|
1892
|
+
}
|
|
1893
|
+
// (2D) one item drawn on g (the frame's pixels); painted: where its lights go
|
|
1894
|
+
function drawItem(g, it, S, cam, t, painted) {
|
|
1895
|
+
g.save();
|
|
1896
|
+
g.globalAlpha = it.fade;
|
|
1897
|
+
if (it.canvas) { g.drawImage(it.canvas, it.sx, it.sy); g.restore(); return; }
|
|
1898
|
+
const lights = [];
|
|
1899
|
+
g.transform(...it.m);
|
|
1900
|
+
drawObject(g, it.o, S.SW, S.H, t, lights);
|
|
1901
|
+
// (in a lit scene its glows are unlit: drawn apart, over it — standing)
|
|
1902
|
+
if (S.lit > 0) it.lights = lights; else drawLights(g, lights);
|
|
1903
|
+
painted?.push({ o: it.o, lights });
|
|
1904
|
+
g.restore();
|
|
1905
|
+
}
|
|
1906
|
+
// what of an item the land in front of it leaves seen: the line it is seen above ([[x, y], …]),
|
|
1907
|
+
// null — all of it, false — none of it
|
|
1908
|
+
function landCut(land, it) {
|
|
1909
|
+
const { m, box: b } = it, xa = m[0] * b.bx + m[4], xb = xa + m[0] * b.bw, x0 = Math.min(xa, xb), x1 = Math.max(xa, xb), y0 = m[3] * b.by + m[5], y1 = y0 + m[3] * b.bh;
|
|
1910
|
+
const line = land.at(it.D, x0, x1, y1);
|
|
1911
|
+
if (!line) return null;
|
|
1912
|
+
return line.every(([, y]) => y <= y0) ? false : line;
|
|
852
1913
|
}
|
|
853
1914
|
|
|
854
|
-
|
|
1915
|
+
// (src: the canvas the water mirrors — what is put down so far)
|
|
1916
|
+
function drawWater(ctx, S, L, wy, t, src) {
|
|
855
1917
|
const { W, H, u } = S, depth = H - wy, step = Math.max(3, Math.round(u * 3));
|
|
856
1918
|
if (depth <= 0) return;
|
|
857
1919
|
const k = Math.min(1.2, wy / depth), speed = 1 + Math.abs(L.drift ?? 0) * 2;
|
|
858
1920
|
// copy only the band above the horizon that the reflection actually shows
|
|
859
1921
|
const band = Math.max(0, Math.floor(wy - depth * k - step)), sc = S.scratch.getContext('2d');
|
|
860
1922
|
sc.clearRect(0, band, W, wy - band);
|
|
861
|
-
sc.drawImage(
|
|
1923
|
+
sc.drawImage(src, S.out.x, S.out.y + band, W, wy - band, 0, band, W, wy - band);
|
|
862
1924
|
for (let y = wy; y < H; y += step) {
|
|
863
1925
|
const d = (y - wy) / depth;
|
|
864
1926
|
const dx = (Math.sin(30 * Math.log(0.03 + d) - t * 1.4 * speed) * 0.6 + Math.sin(d * 53 + t * 0.8) * 0.4) * (1 + d * 16) * u;
|
|
@@ -900,53 +1962,79 @@ const Moood = (() => {
|
|
|
900
1962
|
}
|
|
901
1963
|
ctx.restore();
|
|
902
1964
|
|
|
1965
|
+
const look = lookOf(spec);
|
|
1966
|
+
S.sources = sources2d(S, spec);
|
|
1967
|
+
// lit as much as the light is now (lookOf); its ambient level: how much a shadow takes away is the
|
|
1968
|
+
// source's share of the light
|
|
1969
|
+
S.lit = look.lit;
|
|
1970
|
+
S.ambient = ambientOf(look, S.sources).reduce((a, b) => a + b) / 3;
|
|
1971
|
+
const lit = lit2d(ctx, S, look, cam);
|
|
903
1972
|
for (const l of S.round ? [] : S.layers) {
|
|
904
|
-
const L = l.L;
|
|
1973
|
+
const L = l.L, g = lit.g || ctx;
|
|
905
1974
|
if (l.Z - cam.z < 0.05) continue;
|
|
906
|
-
|
|
907
|
-
|
|
1975
|
+
lit.dirty = !!lit.g;
|
|
1976
|
+
// (the water mirrors what is put down already: in a lit scene, lit)
|
|
1977
|
+
if (L.kind === 'water') { lit.flush(); lit.dirty = !!lit.g; const m = view(cam, l.Z, W, H, S.yh); drawWater(g, S, L, Math.round(m[5] + m[3] * L.y * H), t, ctx.canvas); continue; }
|
|
1978
|
+
g.save(); place(g, S, cam, l.Z);
|
|
908
1979
|
if (l.canvas) {
|
|
909
|
-
|
|
910
|
-
|
|
1980
|
+
g.drawImage(l.canvas, l.x0, 0);
|
|
1981
|
+
g.fillStyle = L.color; g.fillRect(l.x0, l.canvas.height - 1, l.canvas.width, 50 * H); // the body goes on below
|
|
911
1982
|
if (l.windows?.length) {
|
|
912
1983
|
const lc = rgb('#ffd08a');
|
|
913
1984
|
for (const w of l.windows) {
|
|
914
|
-
if (Math.sin(t * 0.07 + w.p) > -0.85) {
|
|
1985
|
+
if (Math.sin(t * 0.07 + w.p) > -0.85) { g.fillStyle = `rgba(${lc},${0.55 + 0.25 * Math.sin(w.p)})`; g.fillRect(w.x, w.y, w.w, w.h); }
|
|
915
1986
|
}
|
|
916
1987
|
}
|
|
917
1988
|
} else if (L.kind === 'clouds') {
|
|
918
|
-
for (const c of l.clouds)
|
|
1989
|
+
for (const c of l.clouds) g.drawImage(c.c, wrapX(c.x + t * c.sp * (L.drift ?? 0.3) * 3, S, c.c.width), c.y - c.c.height / 2);
|
|
919
1990
|
} else if (L.kind === 'fog') {
|
|
920
1991
|
const col = rgb(L.color);
|
|
921
1992
|
for (const b of l.blobs) {
|
|
922
|
-
|
|
923
|
-
|
|
1993
|
+
g.save(); g.translate(wrapX(b.x + t * b.sp * (L.drift ?? 0.3) * 2, S, 2 * b.rx) + b.rx, L.y * H + b.dy);
|
|
1994
|
+
g.scale(1, b.ry / b.rx); glow(g, 0, 0, b.rx, col, b.a * 2.2); g.restore();
|
|
924
1995
|
}
|
|
925
1996
|
}
|
|
926
|
-
|
|
1997
|
+
g.restore();
|
|
927
1998
|
}
|
|
928
1999
|
|
|
929
|
-
if (S.round)
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
2000
|
+
if (S.round) skyAround(ctx, S, cam, t);
|
|
2001
|
+
standing(ctx, S, cam, t, lit);
|
|
2002
|
+
|
|
2003
|
+
for (const { p, list } of S.particles) { stepParticles(lit.g || ctx, p, list, W, H, t, dt, u); lit.dirty = !!lit.g; }
|
|
2004
|
+
lit.flush();
|
|
2005
|
+
// inserts over it all, each in a layer of its own (the iris cuts it alone)
|
|
2006
|
+
for (const o of S.inserts) {
|
|
2007
|
+
const open = opening(o);
|
|
2008
|
+
if (open < 0.002) continue;
|
|
2009
|
+
const g = S.scratch.getContext('2d'), lights = [];
|
|
2010
|
+
g.setTransform(1, 0, 0, 1, 0, 0); g.clearRect(0, 0, W, H);
|
|
2011
|
+
g.setTransform(1, 0, 0, 1, (W - SW) / 2, 0);
|
|
2012
|
+
drawObject(g, o, SW, H, t, lights);
|
|
2013
|
+
iris(g, o, SW, H, open, lights);
|
|
2014
|
+
ctx.drawImage(S.scratch, 0, 0);
|
|
2015
|
+
}
|
|
2016
|
+
// the grade: tint, saturation, brightness, vignette, grain (the light now: lookOf)
|
|
2017
|
+
if (look.tint && look.tintStrength) {
|
|
2018
|
+
ctx.save(); ctx.globalAlpha = clamp(look.tintStrength, 0, 0.5) * 0.45;
|
|
2019
|
+
ctx.fillStyle = `rgb(${look.tint.map(Math.round).join(',')})`; ctx.fillRect(0, 0, W, H); ctx.restore();
|
|
2020
|
+
}
|
|
2021
|
+
if (look.saturation < 1) {
|
|
2022
|
+
ctx.save(); ctx.globalCompositeOperation = 'saturation'; ctx.globalAlpha = clamp(1 - look.saturation, 0, 1);
|
|
2023
|
+
ctx.fillStyle = '#808080'; ctx.fillRect(0, 0, W, H); ctx.restore();
|
|
2024
|
+
}
|
|
2025
|
+
// (the colour times b, as WebGL: darker multiplied, brighter added to itself)
|
|
2026
|
+
if (look.brightness < 1) {
|
|
2027
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply';
|
|
2028
|
+
ctx.fillStyle = `rgb(${Array(3).fill(Math.round(Math.max(0, look.brightness) * 255)).join(',')})`;
|
|
2029
|
+
ctx.fillRect(0, 0, W, H); ctx.restore();
|
|
2030
|
+
} else if (look.brightness > 1) {
|
|
2031
|
+
ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalCompositeOperation = 'lighter';
|
|
2032
|
+
ctx.globalAlpha = Math.min(1, look.brightness - 1); ctx.drawImage(ctx.canvas, 0, 0); // (up to twice as bright)
|
|
938
2033
|
ctx.restore();
|
|
939
2034
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if (lt.tint && lt.tintStrength) {
|
|
944
|
-
ctx.save(); ctx.globalAlpha = clamp(lt.tintStrength, 0, 0.5) * 0.45;
|
|
945
|
-
ctx.fillStyle = lt.tint; ctx.fillRect(0, 0, W, H); ctx.restore();
|
|
946
|
-
}
|
|
947
|
-
ctx.drawImage(S.vignette, 0, 0);
|
|
948
|
-
if ((lt.grain ?? 0.5) > 0) {
|
|
949
|
-
ctx.save(); ctx.globalAlpha = clamp(lt.grain ?? 0.5, 0, 1);
|
|
2035
|
+
ctx.save(); ctx.globalAlpha = clamp(look.vignette, 0, 1); ctx.drawImage(S.vignette, 0, 0); ctx.restore();
|
|
2036
|
+
if (look.grain > 0) {
|
|
2037
|
+
ctx.save(); ctx.globalAlpha = clamp(look.grain, 0, 1);
|
|
950
2038
|
ctx.translate(Math.random() * 160, Math.random() * 160); ctx.fillStyle = S.grain; ctx.fillRect(-160, -160, W + 320, H + 320); ctx.restore();
|
|
951
2039
|
}
|
|
952
2040
|
// out of the frame the scene dims toward the canvas's edges (with Canvas 2D, what nothing
|
|
@@ -960,6 +2048,28 @@ const Moood = (() => {
|
|
|
960
2048
|
}
|
|
961
2049
|
}
|
|
962
2050
|
|
|
2051
|
+
// A frame unseen (stage.draw(false)): the scene goes on as if it were drawn — what moves through
|
|
2052
|
+
// the world is put where it is now, the painting code of what has a memory of its own (a module's
|
|
2053
|
+
// springs and steps, the point it marks) runs as for a frame, particles move — and nothing is
|
|
2054
|
+
// drawn. Custom elements are painted at a fraction of their size, to a canvas nobody sees.
|
|
2055
|
+
// (A drawn frame paints so what is out of sight: unseen2d.)
|
|
2056
|
+
const UNSEEN = 0.05;
|
|
2057
|
+
function unseen2d(S, o, t) {
|
|
2058
|
+
if (o.kind && o.kind !== 'custom') return;
|
|
2059
|
+
const g = S.unseen.getContext('2d');
|
|
2060
|
+
g.setTransform(UNSEEN, 0, 0, UNSEEN, 0, 0);
|
|
2061
|
+
paintCustom(g, o, S.SW, S.H, t, []);
|
|
2062
|
+
}
|
|
2063
|
+
function tick(S, t, dt) {
|
|
2064
|
+
const { W, H, u } = S;
|
|
2065
|
+
S.move();
|
|
2066
|
+
for (const o of [...S.objects.map(ob => ob.o), ...S.inserts.filter(o => opening(o) >= 0.002)]) unseen2d(S, o, t);
|
|
2067
|
+
for (const { p, list } of S.particles) {
|
|
2068
|
+
const wind = (p.wind ?? 0) * 60 * u;
|
|
2069
|
+
for (const q of list) moveParticle(p.kind, q, W, H, t, dt, u, wind);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
963
2073
|
// Mounts the engine on a canvas: returns { show(spec), resetCamera() }.
|
|
964
2074
|
// Camera: drag = step sideways / turn the head up-down, wheel / pinch = walk toward the cursor,
|
|
965
2075
|
// keys: A D ← → = sideways, W S ↑ ↓ = forward-back, 0 = back to the start.
|
|
@@ -971,12 +2081,16 @@ const Moood = (() => {
|
|
|
971
2081
|
const grain = ctx.createPattern(grainTile, 'repeat');
|
|
972
2082
|
return {
|
|
973
2083
|
name: '2d', fpsIdle: 20, pixelBudget: 1.3e6, resMax: 1.5,
|
|
974
|
-
build(spec, W, H, prev, out) { const S = build(spec, W, H); S.grain = grain; S.out = out; return S; },
|
|
2084
|
+
build(spec, W, H, prev, out) { const S = build(spec, W, H); S.grain = grain; S.out = out; S.unseen = layer(8, 8); return S; },
|
|
975
2085
|
frame(S, spec, cam, t, dt) { frame(ctx, S, spec, cam, t, dt); },
|
|
2086
|
+
// painted as when drawn: Canvas 2D paints every element each frame — that is drawing it
|
|
2087
|
+
tick(S, spec, cam, t, dt, goal, painted) { if (painted) frame(ctx, S, spec, cam, t, dt); else tick(S, t, dt); },
|
|
976
2088
|
};
|
|
977
2089
|
}
|
|
978
2090
|
|
|
979
|
-
|
|
2091
|
+
// opts.check: the scene checked as it is drawn (CHECK — ?debug does it too)
|
|
2092
|
+
function mount(canvas, opts = {}) {
|
|
2093
|
+
CHECK.on = !!opts.check || /[?&]debug\b/.test(location.search);
|
|
980
2094
|
// a canvas gets one context type for life: WebGL2 when available, else 2D
|
|
981
2095
|
// (?2d in the URL forces the 2D renderer, for comparison)
|
|
982
2096
|
const R = (typeof MooodGL !== 'undefined' && !/[?&]2d\b/.test(location.search) && MooodGL.create(canvas)) || canvas2DRenderer(canvas);
|
|
@@ -1055,6 +2169,9 @@ const Moood = (() => {
|
|
|
1055
2169
|
// the pointer moves, and kept till the gesture ends; it stays under the cursor all the way —
|
|
1056
2170
|
// the gaze lifting or lowering with it — and the walk stops where it about fills the frame
|
|
1057
2171
|
// (something high up: where it still shows with the gaze lifted as far as it goes).
|
|
2172
|
+
// What the viewer points at is seen from the camera as shown — the film's camera on top of the
|
|
2173
|
+
// viewer's own (off: how far the film moves it); the viewer's moves change their own.
|
|
2174
|
+
const off = { x: 0, y: 0, z: 0, h: 0, yaw: 0 }, seenCam = () => ({ x: target.x + off.x, y: target.y + off.y, z: target.z + off.z, h: target.h + off.h, yaw: target.yaw + off.yaw });
|
|
1058
2175
|
let aim = null, aimAt = 0;
|
|
1059
2176
|
// is the element truly painted at the cursor: nearly opaque paint on a tenth of the 9×9
|
|
1060
2177
|
// pixels around it (the thin spokes of a wheel count; a veil, a glow, empty air don't)?
|
|
@@ -1070,35 +2187,35 @@ const Moood = (() => {
|
|
|
1070
2187
|
return n >= 8;
|
|
1071
2188
|
};
|
|
1072
2189
|
const pick = (clientX, clientY) => {
|
|
1073
|
-
const [cx, cy] = local(clientX, clientY), Hc = box.h, Wc = box.w, yh = S?.yh ?? 0.6, dz = spec ? depths(spec) : null;
|
|
1074
|
-
const at = D => ({ Z:
|
|
1075
|
-
const near = (spec?.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane).map(o => dz.now(o))
|
|
1076
|
-
.filter(({ Z }) =>
|
|
2190
|
+
const [cx, cy] = local(clientX, clientY), Hc = box.h, Wc = box.w, yh = S?.yh ?? 0.6, dz = spec ? depths(spec) : null, v = seenCam();
|
|
2191
|
+
const at = D => ({ Z: v.z + D, x: v.x + (cx - Wc / 2) / Hc * D });
|
|
2192
|
+
const near = (spec?.objects || []).filter(o => o.kind !== 'bird' && !o.sky && !o.ground && !o.plane && !o.insert && !o.unseen).map(o => dz.now(o))
|
|
2193
|
+
.filter(({ Z }) => nearFade(Z - v.z) > 0).sort((a, b) => a.Z - b.Z);
|
|
1077
2194
|
for (const { o, Z } of near) {
|
|
1078
|
-
const m = view(
|
|
2195
|
+
const m = view(v, Z, Wc, Hc, yh), b = objectBox(o, STAGE * Hc, Hc);
|
|
1079
2196
|
const x0 = m[0] * b.bx + m[4], y0 = m[3] * b.by + m[5];
|
|
1080
2197
|
if (cx < x0 || cx > x0 + m[0] * b.bw || cy < y0 || cy > y0 + m[3] * b.bh) continue;
|
|
1081
2198
|
if (!seenAt(o, m, cx, cy, Hc)) continue;
|
|
1082
2199
|
// the point under the cursor, Y eye heights up; stop where the thing about fills the
|
|
1083
2200
|
// frame, or — high up — where the point still shows with the gaze lifted as far as it goes
|
|
1084
|
-
const D = Z -
|
|
1085
|
-
const sight = Y >
|
|
2201
|
+
const D = Z - v.z, Y = v.h - (cy / Hc - yh + v.y) * D, up = (lim.setup?.look ?? 0) + LOOK_UP;
|
|
2202
|
+
const sight = Y > v.h ? (Y - v.h) / Math.max(0.05, yh - cy / Hc - up) : 0;
|
|
1086
2203
|
return { ...at(D), Y, cy: cy / Hc, stop: Math.max(1.1, 1.15 * o.size * Z, sight) };
|
|
1087
2204
|
}
|
|
1088
|
-
const below = cy - (yh -
|
|
1089
|
-
return { ...at(below > 0.02 * Hc ? clamp(Hc *
|
|
2205
|
+
const below = cy - (yh - v.y) * Hc;
|
|
2206
|
+
return { ...at(below > 0.02 * Hc ? clamp(Hc * v.h / below, 0.8, 100) : 6), stop: 1.2 };
|
|
1090
2207
|
};
|
|
1091
2208
|
const walkToward = (factor, clientX, clientY) => {
|
|
1092
2209
|
const now = performance.now();
|
|
1093
2210
|
if (!aim || now - aimAt > 400 || Math.hypot(clientX - aim.px, clientY - aim.py) > 6) aim = { ...pick(clientX, clientY), px: clientX, py: clientY };
|
|
1094
2211
|
aimAt = now;
|
|
1095
|
-
const d0 = aim.Z -
|
|
2212
|
+
const v = seenCam(), d0 = aim.Z - v.z;
|
|
1096
2213
|
if (d0 <= 0.05) return;
|
|
1097
2214
|
// each notch covers a share of the way left (the approach slows by itself), up to the stop
|
|
1098
2215
|
const d = factor > 1 ? Math.max(Math.min(d0, aim.stop), d0 / factor) : d0 / factor;
|
|
1099
|
-
target.z = aim.Z - d;
|
|
1100
|
-
target.x = aim.x - (aim.x -
|
|
1101
|
-
if (aim.Y !== undefined) target.y = (S?.yh ?? 0.6) - aim.cy + (
|
|
2216
|
+
target.z = aim.Z - d - off.z;
|
|
2217
|
+
target.x = aim.x - (aim.x - v.x) * d / d0 - off.x;
|
|
2218
|
+
if (aim.Y !== undefined) target.y = (S?.yh ?? 0.6) - aim.cy + (v.h - aim.Y) / Math.max(0.05, d) - off.y;
|
|
1102
2219
|
// what you walked up to may lie past where walking goes: you may stand there
|
|
1103
2220
|
lim.reach = target.z > lim.zMax ? Math.max(lim.reach ?? 0, target.z) : undefined;
|
|
1104
2221
|
limitCam(target, lim);
|
|
@@ -1113,8 +2230,8 @@ const Moood = (() => {
|
|
|
1113
2230
|
const grabAt = (clientX, clientY) => {
|
|
1114
2231
|
// looking around, a drag turns the head: what you took hold of follows the finger
|
|
1115
2232
|
if (round()) { const [cx, cy] = local(clientX, clientY); grab = { round: true, cx, cy, yaw: target.yaw, look: target.y }; return; }
|
|
1116
|
-
const [cx, cy] = local(clientX, clientY), D = clamp(pick(clientX, clientY).Z -
|
|
1117
|
-
grab = { x:
|
|
2233
|
+
const [cx, cy] = local(clientX, clientY), v = seenCam(), D = clamp(pick(clientX, clientY).Z - v.z, 0.4, 60);
|
|
2234
|
+
grab = { x: v.x + (cx - box.w / 2) / box.h * D, D, cy, look: target.y };
|
|
1118
2235
|
};
|
|
1119
2236
|
const dragTo = (clientX, clientY) => {
|
|
1120
2237
|
const [cx, cy] = local(clientX, clientY), x0 = target.x, y0 = target.y, now = performance.now();
|
|
@@ -1129,7 +2246,7 @@ const Moood = (() => {
|
|
|
1129
2246
|
lastMove = now;
|
|
1130
2247
|
return;
|
|
1131
2248
|
}
|
|
1132
|
-
target.x = grab.x - (cx - box.w / 2) / box.h * grab.D;
|
|
2249
|
+
target.x = grab.x - (cx - box.w / 2) / box.h * grab.D - off.x;
|
|
1133
2250
|
target.y = grab.look - (cy - grab.cy) / box.h;
|
|
1134
2251
|
limitCam(target, lim);
|
|
1135
2252
|
const dt = Math.max(8, now - lastMove) / 1000, k = Math.min(1, dt * 20);
|
|
@@ -1138,37 +2255,51 @@ const Moood = (() => {
|
|
|
1138
2255
|
glide.vy += ((target.y - y0) / dt - glide.vy) * k;
|
|
1139
2256
|
lastMove = now;
|
|
1140
2257
|
};
|
|
1141
|
-
//
|
|
1142
|
-
// point its
|
|
2258
|
+
// A point of an element, by `ref`: its id — the point its code marks (e.at), else its anchor — or
|
|
2259
|
+
// 'id#name', a point its painting names (e.at(x, y, name)). → { o: the element, n: it where it is
|
|
2260
|
+
// now (depths().now — or the part the point is in, depths().part), f: the point in metres from
|
|
2261
|
+
// n's anchor as painted, or null (its anchor), named }, or null: no such element, or its painting
|
|
2262
|
+
// marks no such point now
|
|
2263
|
+
function pointOf(ref, dz) {
|
|
2264
|
+
const [id, name] = String(ref).split('#'), o = byId.get(id);
|
|
2265
|
+
if (!o) return null;
|
|
2266
|
+
const f = name !== undefined ? NAMED.get(id)?.get(name) : FOCUS.get(id);
|
|
2267
|
+
if (name !== undefined && !f) return null;
|
|
2268
|
+
const n = f?.part ? dz.part(o, f.part.dx, f.part.dz) : dz.now(o);
|
|
2269
|
+
return { o, n, f: f || null, named: name !== undefined };
|
|
2270
|
+
}
|
|
2271
|
+
// where in the canvas (CSS px) an element's point (pointOf) is from camera c — where its `place`
|
|
2272
|
+
// has it now — its anchor's top (with over) when its code marks none — lifted by `lift` metres:
|
|
1143
2273
|
// [x, y, its distance]; null when out of sight
|
|
1144
|
-
function seenFrom(c,
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
2274
|
+
function seenFrom(c, ref, lift = 0, over = false) {
|
|
2275
|
+
if (!spec) return null;
|
|
2276
|
+
const dz = depths(spec), P = pointOf(ref, dz);
|
|
2277
|
+
if (!P || P.o.sky || P.o.insert) return null;
|
|
2278
|
+
const { o, n, f } = P, W = box.w, H = box.h, yh = dz.yh;
|
|
1148
2279
|
if (o.plane) {
|
|
1149
|
-
const p = dz.objectAt(o), Z = p.Zp, up = lift / EYE;
|
|
2280
|
+
const p = dz.objectAt(o), Z = p.Zp, up = lift / EYE + (o.drape ? groundAt(p.X * EYE, p.Z * EYE) / EYE : 0);
|
|
1150
2281
|
if (round()) {
|
|
1151
2282
|
const q = project(c, p.X, p.Y + up, p.Z, W, H, yh);
|
|
1152
2283
|
return q && q[2] > 0.3 ? [box.x + q[0], box.y + q[1], q[2]] : null;
|
|
1153
2284
|
}
|
|
1154
|
-
if (
|
|
2285
|
+
if (Z - c.z <= 0.05) return null; // (behind the eye)
|
|
1155
2286
|
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
2287
|
return [box.x + m[0] * px + m[4], box.y + m[3] * py + m[5], Z - c.z];
|
|
1157
2288
|
}
|
|
1158
|
-
const at =
|
|
1159
|
-
// behind the eye, or faded out as it is passed (
|
|
1160
|
-
if (!at || at.D < 0.
|
|
1161
|
-
const
|
|
1162
|
-
if (over && !
|
|
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]; }
|
|
2289
|
+
const at = seenOf(c, n, W, H, yh, round());
|
|
2290
|
+
// behind the eye, or faded out as it is passed (nearFade)
|
|
2291
|
+
if (!at || nearFade(at.D) < 0.3) return null;
|
|
2292
|
+
const [ax, ay, ppm] = anchorOf(n.o, STAGE * H, H);
|
|
2293
|
+
if (over && !f) lift += o.height ?? EYE;
|
|
2294
|
+
let px = ax + (f ? f.x : 0) * ppm, py = ay + (f ? f.y : 0) * ppm - lift / EYE * H / n.at.Zp;
|
|
2295
|
+
if (c.hero && o.id === c.hero.id && !f?.part) { const hm = heroMatrix(c.hero, ax, ppm); px = hm[0] * px + hm[4]; py += hm[5]; }
|
|
1165
2296
|
return [box.x + at.m[0] * px + at.m[4], box.y + at.m[3] * py + at.m[5], at.D];
|
|
1166
2297
|
}
|
|
1167
2298
|
// the camera keeps the elements it follows in the visible part of the screen: together when
|
|
1168
2299
|
// they fit, else the first of them
|
|
1169
2300
|
function keepInView() {
|
|
1170
2301
|
const ids = lim.setup?.follow;
|
|
1171
|
-
if (!ids?.length || pointers.size || keys.size || glide) return;
|
|
2302
|
+
if (!ids?.length || pointers.size || keys.size || glide || op.pose) return;
|
|
1172
2303
|
const [vl, vr] = lim.view, L = box.x + (vl + 0.1) * box.w, R = box.x + (vr - 0.1) * box.w;
|
|
1173
2304
|
const pts = ids.map(id => seenFrom(target, id)).filter(Boolean);
|
|
1174
2305
|
if (!pts.length) return;
|
|
@@ -1177,6 +2308,136 @@ const Moood = (() => {
|
|
|
1177
2308
|
const aim = hi - lo <= R - L ? (lo + hi) / 2 : pts[0][0];
|
|
1178
2309
|
target.x += (aim - (L + R) / 2) * pts[0][2] / box.h;
|
|
1179
2310
|
}
|
|
2311
|
+
// What is where in the frame now, seen from camera c — for whoever looks closely (?debug, a
|
|
2312
|
+
// render's --json). By element (its id, else its name, else its kind and place in the spec):
|
|
2313
|
+
// { kind, seen, box: [x, y, w, h] — where its painting may reach, in the canvas's CSS px; at — the
|
|
2314
|
+
// point its code marks (e.at), anchor — where it stands, [x, y]; distance (m); fade — 1, less in
|
|
2315
|
+
// the last steps before the eye (nearFade); points — those its code names (e.at(x, y, name)): {name: [x, y]};
|
|
2316
|
+
// parts — those it paints (e.part): [{dx, dz, box, seen, distance}]; notes — what its code wrote
|
|
2317
|
+
// (e.debug): {value, at?}; paint — its last painting: {ms, repaint: 'frame' (it reads the time),
|
|
2318
|
+
// 'state' (the state or the light on it) or 'once'} }
|
|
2319
|
+
function layout(c) {
|
|
2320
|
+
const out = {};
|
|
2321
|
+
if (!spec || !dzOf) return out;
|
|
2322
|
+
const W = box.w, H = box.h, SW = STAGE * H, yh = dzOf.yh;
|
|
2323
|
+
const r1 = v => Math.round(v * 10) / 10, pt = (x, y) => [r1(box.x + x), r1(box.y + y)];
|
|
2324
|
+
const rect = (x0, y0, x1, y1) => [r1(box.x + Math.min(x0, x1)), r1(box.y + Math.min(y0, y1)), r1(Math.abs(x1 - x0)), r1(Math.abs(y1 - y0))];
|
|
2325
|
+
const inFrame = ([x, y, w, h]) => x < box.x + W && x + w > box.x && y < box.y + H && y + h > box.y;
|
|
2326
|
+
(spec.objects || []).forEach((o, i) => {
|
|
2327
|
+
const key = keyOf(o), el = { kind: o.sky ? 'sky' : o.ground ? 'ground' : o.plane ? 'plane' : o.insert ? 'insert' : o.kind || 'custom' };
|
|
2328
|
+
let place = null; // a point its code names (its units, from its anchor) → [x, y] on the canvas
|
|
2329
|
+
if (o.sky || o.insert) { el.box = rect(0, 0, W, H); el.seen = !!o.sky || opening(o) >= 0.002; }
|
|
2330
|
+
else if (o.ground) {
|
|
2331
|
+
const m = view(c, Z_FAR, W, H, yh), y = m[5] + m[3] * yh * H;
|
|
2332
|
+
el.box = rect(0, y, W, H); el.seen = y < H;
|
|
2333
|
+
} else if (o.plane) {
|
|
2334
|
+
const p = dzOf.objectAt(o), w = (o.width || 4) / EYE / 2, d = (o.depth || 4) / EYE / 2, a = (o.rotation || 0) * Math.PI / 180;
|
|
2335
|
+
const q = [[-w, -d], [w, -d], [w, d], [-w, d]].map(([u, v]) => project(c, p.X + u * Math.cos(a) + v * Math.sin(a), (o.elevation || 0) / EYE, p.Z - v * Math.cos(a) + u * Math.sin(a), W, H, yh)).filter(Boolean);
|
|
2336
|
+
if (q.length) { const xs = q.map(v => v[0]), ys = q.map(v => v[1]); el.box = rect(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)); }
|
|
2337
|
+
el.seen = !!el.box && inFrame(el.box);
|
|
2338
|
+
} else {
|
|
2339
|
+
// where something standing (the element, or one of its parts: a now() of it) is in the frame:
|
|
2340
|
+
// its box, anchor, distance, fade, and onFrame — its painting's point → the canvas
|
|
2341
|
+
const stand = (n, hero) => {
|
|
2342
|
+
const at = seenOf(c, n, W, H, yh, round());
|
|
2343
|
+
if (!at) return null;
|
|
2344
|
+
const [ax, ay, ppm] = anchorOf(n.o, SW, H), b = objectBox(n.o, SW, H), m = at.m;
|
|
2345
|
+
const hm = hero ? heroMatrix(c.hero, ax, ppm) : null;
|
|
2346
|
+
const onFrame = (x, y) => { if (hm) { x = hm[0] * x + hm[4]; y += hm[5]; } return [m[0] * x + m[4], m[3] * y + m[5]]; };
|
|
2347
|
+
const p0 = onFrame(b.bx, b.by), p1 = onFrame(b.bx + b.bw, b.by + b.bh), box_ = rect(p0[0], p0[1], p1[0], p1[1]);
|
|
2348
|
+
const fade = Math.round(nearFade(at.D) * 100) / 100;
|
|
2349
|
+
return { box: box_, fade, seen: fade > 0 && inFrame(box_), anchor: pt(...onFrame(ax, ay)), distance: r1(at.D * EYE), on: (x, y) => pt(...onFrame(ax + x * ppm, ay + y * ppm)) };
|
|
2350
|
+
};
|
|
2351
|
+
const own = stand(dzOf.now(o), c.hero && o.id === c.hero.id);
|
|
2352
|
+
if (!own) el.seen = false;
|
|
2353
|
+
else {
|
|
2354
|
+
const { on, ...rest } = own;
|
|
2355
|
+
Object.assign(el, rest);
|
|
2356
|
+
place = on;
|
|
2357
|
+
// its points: the one its code marks (e.at), and those it names (e.at(x, y, name)) — each
|
|
2358
|
+
// where it was painted, in the element or in one of its parts
|
|
2359
|
+
const where = f => (f.part ? stand(dzOf.part(o, f.part.dx, f.part.dz), false)?.on(f.x, f.y) ?? null : on(f.x, f.y));
|
|
2360
|
+
const f = FOCUS.get(o.id), named = NAMED.get(o.id);
|
|
2361
|
+
if (f) el.at = where(f);
|
|
2362
|
+
if (named?.size) el.points = Object.fromEntries([...named].map(([nm, p]) => [nm, where(p)]));
|
|
2363
|
+
// its parts (e.part), each standing at its own depth
|
|
2364
|
+
const parts = PARTS.get(key);
|
|
2365
|
+
if (parts?.length) el.parts = parts.map(q => { const s_ = stand(dzOf.part(o, q.dx, q.dz, q.box), false); return s_ ? { dx: q.dx, dz: q.dz, box: s_.box, seen: s_.seen, distance: s_.distance } : { dx: q.dx, dz: q.dz, seen: false }; });
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
const notes = NOTES.get(key), last = PERF.last.get(key), ms = last?.ms, read = last?.read;
|
|
2369
|
+
if (notes) el.notes = notes.map(nt => (nt.at && place ? { value: nt.value, at: place(nt.at.x, nt.at.y) } : { value: nt.value }));
|
|
2370
|
+
if (ms != null) el.paint = { ms: Math.round(ms * 100) / 100, repaint: read?.time || o.frames > 1 ? 'frame' : read?.state || read?.lights ? 'state' : 'once', ...(read?.keys.size ? { keys: [...read.keys.keys()].map(k => (k === ALL ? '*' : k)) } : {}), ...(PERF.repaints.has(key) ? { repaints: PERF.repaints.get(key) } : {}) };
|
|
2371
|
+
out[key ?? `${el.kind}#${i}`] = el;
|
|
2372
|
+
});
|
|
2373
|
+
return out;
|
|
2374
|
+
}
|
|
2375
|
+
// ?debug: over the frame, what is where — each element seen: its box and id, its anchor (a ring),
|
|
2376
|
+
// the point its code marks (a cross), what its code wrote (e.debug); the scene's second, and
|
|
2377
|
+
// what it does wrong (window.__mooodErrors, then __mooodWarnings: e.warn's too) (?debug=keeper,boy: only those)
|
|
2378
|
+
const looking = /[?&]debug\b/.test(location.search) ? overlay(new URLSearchParams(location.search).get('debug')) : null;
|
|
2379
|
+
function overlay(only) {
|
|
2380
|
+
const ids = only ? new Set(only.split(',')) : null;
|
|
2381
|
+
const c = document.createElement('canvas'), g = c.getContext('2d');
|
|
2382
|
+
c.style.cssText = 'position:fixed;pointer-events:none;z-index:8';
|
|
2383
|
+
document.body.append(c);
|
|
2384
|
+
const say = v => (typeof v === 'string' ? v : JSON.stringify(v, (k, x) => (typeof x === 'number' ? Math.round(x * 100) / 100 : x)) ?? String(v)).replace(/"/g, '');
|
|
2385
|
+
const hue = k => hash(k) % 360;
|
|
2386
|
+
return {
|
|
2387
|
+
draw(cam, t) {
|
|
2388
|
+
const r = canvas.getBoundingClientRect(), dpr = window.devicePixelRatio || 1, cw = Math.max(1, canvas.clientWidth), ch = Math.max(1, canvas.clientHeight);
|
|
2389
|
+
Object.assign(c.style, { left: `${r.left}px`, top: `${r.top}px`, width: `${r.width}px`, height: `${r.height}px` });
|
|
2390
|
+
if (c.width !== Math.round(cw * dpr) || c.height !== Math.round(ch * dpr)) { c.width = Math.round(cw * dpr); c.height = Math.round(ch * dpr); }
|
|
2391
|
+
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2392
|
+
g.clearRect(0, 0, cw, ch);
|
|
2393
|
+
g.font = '12px ui-monospace, Consolas, monospace'; g.textBaseline = 'top'; g.lineWidth = 1;
|
|
2394
|
+
const label = (text, x, y, col) => {
|
|
2395
|
+
const w = g.measureText(text).width + 6;
|
|
2396
|
+
g.fillStyle = 'rgba(0,0,0,0.6)'; g.fillRect(x, y, w, 15);
|
|
2397
|
+
g.fillStyle = col; g.fillText(text, x + 3, y + 1.5);
|
|
2398
|
+
};
|
|
2399
|
+
for (const [id, el] of Object.entries(layout(cam))) {
|
|
2400
|
+
if (!el.seen || el.kind === 'sky' || el.kind === 'ground' || !el.box || (ids && !ids.has(id))) continue;
|
|
2401
|
+
const col = `hsl(${hue(id)},85%,70%)`, [x, y, w, h] = el.box;
|
|
2402
|
+
// what spans most of the frame (a wall, a floor) is drawn faintly, so the rest reads
|
|
2403
|
+
g.globalAlpha = w * h > 0.5 * box.w * box.h ? 0.4 : 1;
|
|
2404
|
+
g.strokeStyle = col; g.setLineDash(el.kind === 'insert' || el.kind === 'plane' ? [4, 4] : []);
|
|
2405
|
+
g.strokeRect(x + 0.5, y + 0.5, w, h); g.setLineDash([]);
|
|
2406
|
+
if (el.anchor) { g.beginPath(); g.arc(...el.anchor, 4, 0, 7); g.stroke(); }
|
|
2407
|
+
if (el.at) { const [ax, ay] = el.at; g.beginPath(); g.moveTo(ax - 7, ay); g.lineTo(ax + 7, ay); g.moveTo(ax, ay - 7); g.lineTo(ax, ay + 7); g.stroke(); }
|
|
2408
|
+
// its named points (a diamond and the name), its parts (dashed, each where it stands)
|
|
2409
|
+
for (const [nm, q] of Object.entries(el.points || {})) {
|
|
2410
|
+
if (!q) continue;
|
|
2411
|
+
const [qx, qy] = q; g.beginPath(); g.moveTo(qx, qy - 5); g.lineTo(qx + 5, qy); g.lineTo(qx, qy + 5); g.lineTo(qx - 5, qy); g.closePath(); g.stroke();
|
|
2412
|
+
label(`#${nm}`, qx + 7, qy - 7, col);
|
|
2413
|
+
}
|
|
2414
|
+
g.setLineDash([2, 3]);
|
|
2415
|
+
for (const q of el.parts || []) if (q.seen) g.strokeRect(q.box[0] + 0.5, q.box[1] + 0.5, q.box[2], q.box[3]);
|
|
2416
|
+
g.setLineDash([]);
|
|
2417
|
+
const lx = Math.max(box.x, Math.min(x, box.x + box.w - 160)), ly = Math.max(box.y, y);
|
|
2418
|
+
label(`${id}${el.paint?.ms >= 0.1 ? ` · ${el.paint.ms.toFixed(1)} ms` : ''}`, lx, ly, col);
|
|
2419
|
+
(el.notes || []).forEach((nt, i) => {
|
|
2420
|
+
const text = say(nt.value).slice(0, 90);
|
|
2421
|
+
if (nt.at) { const [px, py] = nt.at; g.fillStyle = col; g.fillRect(px - 2, py - 2, 4, 4); label(text, px + 5, py - 7, col); }
|
|
2422
|
+
else label(text, lx, ly + 16 * (i + 1), col);
|
|
2423
|
+
});
|
|
2424
|
+
g.globalAlpha = 1;
|
|
2425
|
+
}
|
|
2426
|
+
// what the scene does wrong (its errors, what its modules said they do not know), the latest
|
|
2427
|
+
const wrong = [...(window.__mooodErrors || []), ...(window.__mooodWarnings || [])];
|
|
2428
|
+
if (wrong.length > 6) label(`… ${wrong.length - 6} more (window.__mooodErrors, __mooodWarnings)`, box.x + 8, box.y + 8, '#ffb4a0');
|
|
2429
|
+
wrong.slice(-6).forEach((m, i) => label(m.slice(0, 200), box.x + 8, box.y + 8 + 16 * (i + (wrong.length > 6)), '#ffb4a0'));
|
|
2430
|
+
const target = STATE.values.camera_target;
|
|
2431
|
+
label(`${t.toFixed(2)} s${target != null ? ` · camera_target ${target}` : ''}`, box.x + 8, box.y + box.h - 23, '#fff');
|
|
2432
|
+
},
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
// false: the scene goes on unseen — each frame steps it (R.tick), nothing is drawn, live elements
|
|
2436
|
+
// painted coarsely (a render seeking a late moment draws only the frames it takes); 'painted' —
|
|
2437
|
+
// unseen, each painted as a drawn frame paints it (the last seconds before a frame taken: what
|
|
2438
|
+
// keeps a memory goes on exactly as when drawn); drawn again, the next frame is drawn whatever
|
|
2439
|
+
// the frame rate (owed)
|
|
2440
|
+
let drawn = true, owed = false;
|
|
1180
2441
|
const reset = () => { Object.assign(target, { x: 0, y: lim.setup?.look ?? 0, z: 0, h: lim.setup?.h ?? 1, yaw: 0 }); };
|
|
1181
2442
|
const round = () => !!lim.setup?.round, roam = () => lim.setup?.mode === 'roam';
|
|
1182
2443
|
const platform = () => lim.setup?.mode === 'platform';
|
|
@@ -1257,13 +2518,156 @@ const Moood = (() => {
|
|
|
1257
2518
|
// phone's tilt): near things shift against far ones, the depth is felt standing still too.
|
|
1258
2519
|
// `shown` is the camera as drawn: where you are, plus the lean (eased, LEAN eye heights).
|
|
1259
2520
|
const LEAN = 0.047, leaned = { x: 0, y: 0 }, shown = { ...cam };
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
//
|
|
2521
|
+
// ── the film's camera ────────────────────────────────────────────────
|
|
2522
|
+
// A film moves the camera through the state. camera_target — an element to look at: an operator
|
|
2523
|
+
// frames it (camera_shot: how big — wide, full, medium, close, or how many times the frame's
|
|
2524
|
+
// height it is tall; camera_frame: where in the frame its point goes, [x, y] fractions of the
|
|
2525
|
+
// frame, or x alone — its point is the one its code marks, e.at, else its box's middle) and keeps
|
|
2526
|
+
// it there as it moves, leaving it room ahead the way it goes — with a cameraman's weight: setting
|
|
2527
|
+
// off and settling gently, over camera_glide seconds (from one target to the next too, and back to
|
|
2528
|
+
// the scene's own camera when the target is gone; 0 — there at once). Standing ahead the operator
|
|
2529
|
+
// dollies and slides; looking around (around, roam) it turns toward the target and comes as near
|
|
2530
|
+
// as the shot needs.
|
|
2531
|
+
// On top of that: camera_x (metres to the right), camera_z (metres ahead — a dolly in, with all
|
|
2532
|
+
// the depth the scene has), camera_look (screen heights the gaze is lowered), camera_h (metres up
|
|
2533
|
+
// — the operator still keeps the target framed); and on top of all, wherever the viewer moved from
|
|
2534
|
+
// the start. A cut (scene.cut) puts the operator where it is meant to be at once.
|
|
1263
2535
|
const num = k => (typeof STATE.values[k] === 'number' ? STATE.values[k] : 0);
|
|
1264
|
-
|
|
2536
|
+
const SHOTS = { wide: 0.3, full: 0.8, medium: 1.6, close: 3.5 }, GLIDE = 1.5, AXES = ['x', 'y', 'z', 'h', 'yaw'];
|
|
2537
|
+
// where a head goes down the frame for a shot: the whole figure in the middle while it fits, then
|
|
2538
|
+
// a head with room above it, the eyes near the upper third close up
|
|
2539
|
+
const HEAD = [[0, 0.5], [0.8, 0.18], [1.6, 0.3], [3.5, 0.38]];
|
|
2540
|
+
const headAt = shot => { const i = HEAD.findIndex(([k]) => k > shot); if (i < 0) return HEAD.at(-1)[1]; const [a, u] = HEAD[i - 1], [b, v] = HEAD[i]; return u + (v - u) * (shot - a) / (b - a); };
|
|
2541
|
+
// the operator: its pose (as the scene's start pose is — the viewer's moves go on top), how fast
|
|
2542
|
+
// it moves, the target's point a frame ago and how fast that moves; null pose: the scene's own camera
|
|
2543
|
+
let op = { pose: null, vel: null, cut: null, fresh: false, id: null, at: null, v: { X: 0, Y: 0, Z: 0 } };
|
|
2544
|
+
let byId = new Map(), dzOf = null;
|
|
2545
|
+
const glideOf = () => { const g = STATE.values.camera_glide; return typeof g === 'number' && g >= 0 ? g : GLIDE; };
|
|
2546
|
+
const startPose = () => ({ x: 0, y: lim.setup?.look ?? 0, z: 0, h: lim.setup?.h ?? 1, yaw: 0 });
|
|
2547
|
+
// the target's point in the world (eye heights) and how tall it is (metres), or null
|
|
2548
|
+
// (id, or 'id#name' — a point its painting names: aimed at, the shot measured by the element)
|
|
2549
|
+
function targetOf(ref) {
|
|
2550
|
+
const o = byId.get(ref.split('#')[0]);
|
|
2551
|
+
if (!o) { warn(`camera_target: there is no element "${ref}"`); return null; }
|
|
2552
|
+
if (o.sky || o.ground || o.plane || o.insert) { warn(`camera_target: "${ref}" is not something standing in the scene`); return null; }
|
|
2553
|
+
const custom = o.kind === 'custom' || !o.kind, tall = custom ? o.height || 2 : HEIGHT[o.kind] || 2, P = custom ? pointOf(ref, dzOf) : null;
|
|
2554
|
+
if (ref.includes('#') && !P) return null; // a point it doesn't mark now: the camera stays as it is
|
|
2555
|
+
let f = P?.f;
|
|
2556
|
+
const at = P ? P.n.at : dzOf.now(o).at;
|
|
2557
|
+
if (!f) {
|
|
2558
|
+
const b = custom && Array.isArray(o.box) && o.box.length === 4 ? o.box : [0, -tall, 0, 0];
|
|
2559
|
+
f = { x: (b[0] + b[2]) / 2, y: (b[1] + b[3]) / 2 };
|
|
2560
|
+
}
|
|
2561
|
+
return { X: at.X + f.x / EYE, Y: at.Y - f.y / EYE, Z: at.Z, tall, marked: !!P?.f, known: !custom || READS.has(o) };
|
|
2562
|
+
}
|
|
2563
|
+
const shotOf = v => {
|
|
2564
|
+
if (v == null) return SHOTS.full;
|
|
2565
|
+
if (typeof v === 'number' && v > 0) return v;
|
|
2566
|
+
if (SHOTS[v]) return SHOTS[v];
|
|
2567
|
+
warn(`camera_shot: ${JSON.stringify(v)} — one of ${Object.keys(SHOTS).join(', ')}, or how many times the frame's height the target is tall`);
|
|
2568
|
+
return SHOTS.full;
|
|
2569
|
+
};
|
|
2570
|
+
const frameOf = v => {
|
|
2571
|
+
if (v == null) return [null, null];
|
|
2572
|
+
if (typeof v === 'number') return [v, null];
|
|
2573
|
+
if (Array.isArray(v) && v.length === 2 && v.every(k => typeof k === 'number')) return v;
|
|
2574
|
+
warn(`camera_frame: ${JSON.stringify(v)} — [x, y] (fractions of the frame) or x`);
|
|
2575
|
+
return [null, null];
|
|
2576
|
+
};
|
|
2577
|
+
// where the operator would stand for target `id` now: { pose — like the start pose, lead — how far
|
|
2578
|
+
// ahead it aims and how fast the target goes }; c: the camera as the viewer has it (its height and
|
|
2579
|
+
// where it stands count)
|
|
2580
|
+
function framing(id, c, dt) {
|
|
2581
|
+
const P = targetOf(id);
|
|
2582
|
+
if (!P) return null;
|
|
2583
|
+
// how fast its point moves (smoothed): the operator aims where it is going, not where it was
|
|
2584
|
+
if (op.at && dt > 0) {
|
|
2585
|
+
const k = 1 - Math.exp(-dt / 0.3);
|
|
2586
|
+
for (const a of ['X', 'Y', 'Z']) op.v[a] += ((P[a] - op.at[a]) / dt - op.v[a]) * k;
|
|
2587
|
+
}
|
|
2588
|
+
op.at = P;
|
|
2589
|
+
const w = 5 / Math.max(0.1, glideOf()), ahead = 2 / w, s = lim.setup;
|
|
2590
|
+
const X = P.X + op.v.X * ahead, Z = P.Z + op.v.Z * ahead, shot = shotOf(STATE.values.camera_shot);
|
|
2591
|
+
// (never so near that the target itself fades as a passed thing does: nearFade)
|
|
2592
|
+
const D = Math.max(PASS_TO, P.tall / EYE / shot), A = box.w / box.h, yh = S?.yh ?? 0.6;
|
|
2593
|
+
op.D = D;
|
|
2594
|
+
const b = startPose(), yaw = s?.round ? Math.atan2(X - c.x, Z - c.z) : 0;
|
|
2595
|
+
// room ahead: a tenth of the frame at a walk across it
|
|
2596
|
+
const across = op.v.X * Math.cos(yaw) - op.v.Z * Math.sin(yaw);
|
|
2597
|
+
let [fx, fy] = frameOf(STATE.values.camera_frame);
|
|
2598
|
+
fx ??= 0.5 - 0.1 * clamp(across * EYE / 1.4, -1, 1);
|
|
2599
|
+
fy ??= P.marked ? headAt(shot) : 0.5;
|
|
2600
|
+
// closer than full height the camera comes down (or up) to the point's own height, as an
|
|
2601
|
+
// operator does: a close-up is level with the face, not looked at from far below with a shifted lens
|
|
2602
|
+
const ph = b.h + (P.Y - b.h) * clamp((shot - SHOTS.full) / (SHOTS.medium - SHOTS.full), 0, 1);
|
|
2603
|
+
const h = c.h + ph - b.h + num('camera_h') / EYE, off = Math.atan((fx - 0.5) * A);
|
|
2604
|
+
// (aimed ahead by `lead`: where an operator following it all along would be is that much behind)
|
|
2605
|
+
const lead = { x: op.v.X * ahead, z: op.v.Z * ahead, vx: op.v.X, vz: op.v.Z };
|
|
2606
|
+
if (s?.round) {
|
|
2607
|
+
const d = Math.hypot(X - c.x, Z - c.z) || 1;
|
|
2608
|
+
return { pose: { x: X - D * (X - c.x) / d, z: Z - D * (Z - c.z) / d, y: yh + (h - P.Y) / (D * Math.cos(off)) - fy, h: ph, yaw: yaw - off }, lead, known: P.known };
|
|
2609
|
+
}
|
|
2610
|
+
return { pose: { x: X - (fx - 0.5) * A * D, z: Z - D, y: yh + (h - P.Y) / D - fy, h: ph, yaw: 0 }, lead, known: P.known };
|
|
2611
|
+
}
|
|
2612
|
+
// the operator's step: c — the camera as the viewer has it — becomes the film's camera
|
|
2613
|
+
function directed(c, dt) {
|
|
2614
|
+
const id = STATE.values.camera_target, s = lim.setup, b = startPose();
|
|
2615
|
+
op.D = null; // (framing: the shot's distance, while a target is framed)
|
|
2616
|
+
if (op.cut !== STATE.cut) { op.cut = STATE.cut; op.fresh = true; op.at = null; op.v = { X: 0, Y: 0, Z: 0 }; }
|
|
2617
|
+
if (id !== op.id) { op.id = id; op.at = null; op.v = { X: 0, Y: 0, Z: 0 }; }
|
|
2618
|
+
// (not while a scene is on its way: the state may be the next one's already)
|
|
2619
|
+
const aim = id != null && S && !waiting && s?.mode !== 'platform' ? framing(String(id), c, dt) : null;
|
|
2620
|
+
// without a target: the scene's own camera (looking around, where the viewer stands)
|
|
2621
|
+
const own = { ...b, ...(s?.round ? { x: c.x, z: c.z } : {}) }, goal = aim ? aim.pose : own;
|
|
2622
|
+
if (aim || op.pose) {
|
|
2623
|
+
// just after a cut, there at once (until the target has been painted: its painting marks its
|
|
2624
|
+
// point as it is painted); else from where the camera is, along a critically damped spring (exact for
|
|
2625
|
+
// any step): it sets off and settles without overshoot
|
|
2626
|
+
const snap = op.fresh || glideOf() < 0.01;
|
|
2627
|
+
if (snap || !op.pose) {
|
|
2628
|
+
op.pose = { ...(snap ? goal : own) }; op.vel = { x: 0, y: 0, z: 0, h: 0, yaw: 0 };
|
|
2629
|
+
// at a moving target: as if following it all along — on it, moving with it
|
|
2630
|
+
if (snap && aim) { op.pose.x -= aim.lead.x; op.pose.z -= aim.lead.z; op.vel.x = aim.lead.vx; op.vel.z = aim.lead.vz; }
|
|
2631
|
+
}
|
|
2632
|
+
if (!snap) {
|
|
2633
|
+
const w = 5 / glideOf(), e = Math.exp(-w * dt);
|
|
2634
|
+
if (s?.round) goal.yaw = op.pose.yaw + ((((goal.yaw - op.pose.yaw) % TAU) + 3 * Math.PI) % TAU - Math.PI);
|
|
2635
|
+
for (const a of AXES) {
|
|
2636
|
+
const d = op.pose[a] - goal[a], k = (op.vel[a] + w * d) * dt;
|
|
2637
|
+
op.vel[a] = (op.vel[a] - w * k) * e;
|
|
2638
|
+
op.pose[a] = goal[a] + (d + k) * e;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
const p = op.pose;
|
|
2642
|
+
// settled back on the scene's own camera: the operator steps away
|
|
2643
|
+
if (!aim && AXES.every(a => Math.abs(p[a] - goal[a]) < 1e-4 && Math.abs(op.vel[a]) < 1e-4)) op.pose = null;
|
|
2644
|
+
else {
|
|
2645
|
+
if (s?.round) { c.x = p.x; c.z = p.z; } else { c.x += p.x - b.x; c.z += p.z - b.z; }
|
|
2646
|
+
c.y += p.y - b.y; c.h += p.h - b.h; c.yaw += p.yaw - b.yaw;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
if (op.fresh) op.fresh = id != null && !aim?.known;
|
|
1265
2650
|
c.x += num('camera_x') / EYE; c.h = (c.h ?? 1) + num('camera_h') / EYE; c.z += num('camera_z') / EYE; c.y += num('camera_look');
|
|
1266
2651
|
}
|
|
2652
|
+
const operating = () => !!op.pose && AXES.some(a => Math.abs(op.vel[a]) > 1e-4);
|
|
2653
|
+
// The viewer's own moves over the film's camera (target: where they moved from the start): while
|
|
2654
|
+
// the operator frames a target they are a look aside — they ease back to nothing once the viewer
|
|
2655
|
+
// lets go (VIEWER_BACK s), they come at most VIEWER_NEAR of the way to the target (never through
|
|
2656
|
+
// it, nor so near that it fades: nearFade). A cut — a new shot (scene.cut, not the scene's start: shown again, a draft, the viewer
|
|
2657
|
+
// stays where they were) — takes them back to the start at once.
|
|
2658
|
+
const VIEWER_BACK = 0.8, VIEWER_NEAR = 0.5;
|
|
2659
|
+
let viewerShot = 0;
|
|
2660
|
+
function overTarget(dt) {
|
|
2661
|
+
// (a platformer's camera rides with its hero: no moves of the viewer's own)
|
|
2662
|
+
if (viewerShot !== STATE.shots && !platform()) { viewerShot = STATE.shots; Object.assign(target, startPose()); Object.assign(cam, target); }
|
|
2663
|
+
if (!op.pose || !(op.D > 0)) return;
|
|
2664
|
+
// (looking around, the operator stands where the shot needs: the viewer only turns their head)
|
|
2665
|
+
const b = startPose(), axes = round() ? ['y', 'yaw'] : AXES;
|
|
2666
|
+
if (!round()) target.z = Math.min(target.z, b.z + Math.min(VIEWER_NEAR * op.D, op.D - PASS_TO));
|
|
2667
|
+
if (fixed() || pointers.size || keys.size || glide || performance.now() - aimAt < 600) return;
|
|
2668
|
+
const k = 1 - Math.exp(-dt / VIEWER_BACK);
|
|
2669
|
+
for (const a of axes) target[a] += (b[a] - target[a]) * k;
|
|
2670
|
+
}
|
|
1267
2671
|
function leanShown(dt) {
|
|
1268
2672
|
const k = 1 - Math.exp(-dt * 3), s = lim.setup;
|
|
1269
2673
|
leaned.x += ((s?.sway ? 0 : lean.x) - leaned.x) * k; leaned.y += ((s?.sway ? 0 : lean.y) - leaned.y) * k;
|
|
@@ -1360,7 +2764,7 @@ const Moood = (() => {
|
|
|
1360
2764
|
if (typing() || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1361
2765
|
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
1362
2766
|
if (k === '0') { reset(); return; }
|
|
1363
|
-
if (k === 'p') {
|
|
2767
|
+
if (k === 'p') { PERF.on = perf.style.display === 'none'; perf.style.display = PERF.on ? 'block' : 'none'; PERF.ms.clear(); return; }
|
|
1364
2768
|
if (KEYS.includes(k) && !fixed()) { keys.add(k); e.preventDefault(); }
|
|
1365
2769
|
});
|
|
1366
2770
|
addEventListener('keyup', e => keys.delete(e.key.length === 1 ? e.key.toLowerCase() : e.key));
|
|
@@ -1368,16 +2772,18 @@ const Moood = (() => {
|
|
|
1368
2772
|
|
|
1369
2773
|
const loop = now => {
|
|
1370
2774
|
requestAnimationFrame(loop);
|
|
1371
|
-
const moving = keys.size > 0 || pointers.size > 0 || !!glide || (plat && (Math.abs(plat.vx) > 0.01 || !plat.ground)) || Math.abs(target.x - cam.x) + Math.abs(target.y - cam.y) + Math.abs(target.z - cam.z) + Math.abs(target.h - cam.h) + Math.abs(target.yaw - cam.yaw) > 0.001;
|
|
2775
|
+
const moving = keys.size > 0 || pointers.size > 0 || !!glide || operating() || (plat && (Math.abs(plat.vx) > 0.01 || !plat.ground)) || Math.abs(target.x - cam.x) + Math.abs(target.y - cam.y) + Math.abs(target.z - cam.z) + Math.abs(target.h - cam.h) + Math.abs(target.yaw - cam.yaw) > 0.001;
|
|
1372
2776
|
if (moving) activeUntil = now + 500;
|
|
1373
2777
|
const fps = now < activeUntil ? FPS_ACTIVE : fpsIdle;
|
|
1374
2778
|
const real = (now - last) / 1000;
|
|
1375
2779
|
// skip this display refresh; the tolerance is under half a refresh so vsync jitter
|
|
1376
2780
|
// never drops a frame we meant to draw (that made 30 fps stutter at ~27)
|
|
1377
|
-
if (real < 1 / fps - 0.006) return;
|
|
1378
|
-
|
|
2781
|
+
if (real < 1 / fps - 0.006 && !owed) return;
|
|
2782
|
+
owed = false;
|
|
2783
|
+
const dt = Math.min(0.05, real), was = t;
|
|
1379
2784
|
last = now; t = Math.max(t, (performance.now() - born) / 1000);
|
|
1380
2785
|
STATE.sample?.(); // the state's moves where they are this very moment
|
|
2786
|
+
if (spec && !waiting) beforeFrame(t, t - was);
|
|
1381
2787
|
if (real < 0.5) frameEma += (real - frameEma) * 0.1; // ignore tab-switch gaps
|
|
1382
2788
|
adapt(now, fps);
|
|
1383
2789
|
// a brisk walk: ~6 m/s forward, ~3.5 m/s sideways (world units are eye heights)
|
|
@@ -1409,6 +2815,7 @@ const Moood = (() => {
|
|
|
1409
2815
|
if (fixed()) leanTo(t);
|
|
1410
2816
|
keepInView();
|
|
1411
2817
|
limitCam(target, lim);
|
|
2818
|
+
overTarget(dt);
|
|
1412
2819
|
if (glide && pointers.size === 0) {
|
|
1413
2820
|
target.x += glide.vx * dt; target.y += glide.vy * dt; target.yaw += glide.vyaw * dt;
|
|
1414
2821
|
limitCam(target, lim);
|
|
@@ -1422,14 +2829,25 @@ const Moood = (() => {
|
|
|
1422
2829
|
cam.x += (target.x - cam.x) * k; cam.y += (target.y - cam.y) * k; cam.z += (target.z - cam.z) * k; cam.h += (target.h - cam.h) * k; cam.yaw += (target.yaw - cam.yaw) * k;
|
|
1423
2830
|
cam.hero = plat && { id: plat.id, dx: plat.x - plat.x0, dy: plat.y - plat.y0, flip: plat.dir < 0 };
|
|
1424
2831
|
leanShown(dt);
|
|
1425
|
-
|
|
2832
|
+
const own = { ...shown };
|
|
2833
|
+
// on a relief (spec.relief) the viewer's eyes go over the land as they walk: 1.7 m over the
|
|
2834
|
+
// ground where they stand, as they were at the start
|
|
2835
|
+
if (SHARED.ground && !plat) shown.h += (groundAt(shown.x * EYE, shown.z * EYE) - groundAt(0, 0)) / EYE;
|
|
2836
|
+
directed(shown, dt);
|
|
2837
|
+
for (const a of AXES) off[a] = (shown[a] ?? 0) - (own[a] ?? 0);
|
|
1426
2838
|
const t0 = performance.now();
|
|
1427
|
-
if (S
|
|
2839
|
+
if (S && drawn === true) {
|
|
2840
|
+
lookOf(spec);
|
|
2841
|
+
R.frame(S, spec, shown, t, dt, now >= activeUntil, op.pose ? shown : target);
|
|
2842
|
+
if (looking) looking.draw(shown, t);
|
|
2843
|
+
} else if (S) { lookOf(spec); R.tick(S, spec, shown, t, dt, op.pose ? shown : target, drawn === 'painted'); }
|
|
1428
2844
|
drawMs += performance.now() - t0;
|
|
1429
2845
|
frames++;
|
|
1430
|
-
if (now - perfAt > 1000 &&
|
|
1431
|
-
|
|
1432
|
-
|
|
2846
|
+
if (now - perfAt > 1000 && PERF.on) {
|
|
2847
|
+
// and the elements that cost the most: their painting's ms a frame
|
|
2848
|
+
const costly = [...PERF.ms].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([k, ms]) => `\n ${(ms / frames).toFixed(2).padStart(6)} ms ${k.slice(0, 28)}`).join('');
|
|
2849
|
+
perf.textContent = `${R.name} · ${Math.round(frames * 1000 / (now - perfAt))} fps (cap ${fps})\n${canvas.width}×${canvas.height} · res ${res.toFixed(2)}\njs ${(drawMs / frames).toFixed(1)} ms/frame${costly}`;
|
|
2850
|
+
frames = 0; drawMs = 0; perfAt = now; PERF.ms.clear();
|
|
1433
2851
|
} else if (now - perfAt > 1000) { frames = 0; drawMs = 0; perfAt = now; }
|
|
1434
2852
|
};
|
|
1435
2853
|
requestAnimationFrame(loop);
|
|
@@ -1440,7 +2858,8 @@ const Moood = (() => {
|
|
|
1440
2858
|
const shared = MooodKit.shared(next, modules);
|
|
1441
2859
|
if (shared instanceof Promise) { waiting = next; shared.then(() => { if (waiting === next) { waiting = null; this.show(next, modules); } }); return; }
|
|
1442
2860
|
waiting = null; SHARED = shared;
|
|
1443
|
-
spec = next; FOCUS.clear();
|
|
2861
|
+
spec = next; FOCUS.clear(); NAMED.clear(); NOTES.clear(); PARTS.clear(); PERF.last.clear(); warned.clear(); STATE.read.clear(); BEFORE.clear(); PERF.repaints.clear();
|
|
2862
|
+
dzOf = depths(spec); byId = new Map((spec.objects || []).filter(o => o.id != null).map(o => [String(o.id), o]));
|
|
1444
2863
|
// values not set yet (the state may start after the scene is shown) are where the scene starts them
|
|
1445
2864
|
if (spec.state && typeof spec.state === 'object') STATE.values = { ...spec.state, ...STATE.values };
|
|
1446
2865
|
// a new level, or the same one edited: the hero stays where it got to
|
|
@@ -1461,19 +2880,50 @@ const Moood = (() => {
|
|
|
1461
2880
|
// the scene's state changed (scenestate.js): the painting code sees it as e.state
|
|
1462
2881
|
// was(key, ago): what a value was `ago` seconds ago (e.was); sample(): brings the values that
|
|
1463
2882
|
// are moving to where they are now — called as each frame starts, so it paints them on time
|
|
1464
|
-
|
|
2883
|
+
// cutAt: when the last cut was (the scene's start or scene.cut); shots: how many scene.cuts
|
|
2884
|
+
setState(values, was, sample, cutAt, shots) {
|
|
1465
2885
|
STATE.values = values || {}; if (was) STATE.was = was; if (sample) STATE.sample = sample;
|
|
2886
|
+
if (cutAt != null) STATE.cut = (cutAt - born) / 1000;
|
|
2887
|
+
if (shots != null) STATE.shots = shots;
|
|
1466
2888
|
STATE.ver++; activeUntil = performance.now() + 300;
|
|
1467
2889
|
},
|
|
1468
2890
|
// where in the canvas (CSS px) an element is now — the point its code marks (e.at), else its
|
|
1469
|
-
// anchor (its top, with over) —
|
|
1470
|
-
|
|
2891
|
+
// anchor (its top, with over); 'id#name' — a point its painting names — lifted by `lift`
|
|
2892
|
+
// metres: [x, y, its distance]; null when out of sight (behind the eye, passed, a point its
|
|
2893
|
+
// painting doesn't mark now)
|
|
2894
|
+
screenOf: (ref, lift = 0, over = false) => seenFrom(shown, ref, lift, over),
|
|
2895
|
+
// the ground's height (metres) at (across, distance): spec.relief, 0 without one
|
|
2896
|
+
ground: groundAt,
|
|
2897
|
+
// the state keys the shown scene's painting has read so far — its elements' code, its modules
|
|
2898
|
+
read: () => STATE.read,
|
|
2899
|
+
// the moment drawn as another camera sees it (a render's --views): `values` — the camera's keys
|
|
2900
|
+
// (camera_target, camera_shot, camera_x…) over the film's, the operator there at once; the film
|
|
2901
|
+
// itself — its state, its clock, its operator — goes on untouched (the next frame is its own)
|
|
2902
|
+
still(values) {
|
|
2903
|
+
if (!S || !spec || drawn !== true) return;
|
|
2904
|
+
const film = STATE.values, was = structuredClone(op), c = Object.fromEntries(AXES.map(a => [a, shown[a] - off[a]]));
|
|
2905
|
+
c.hero = shown.hero;
|
|
2906
|
+
try {
|
|
2907
|
+
STATE.values = { ...film, ...values, camera_glide: 0 };
|
|
2908
|
+
op.fresh = true;
|
|
2909
|
+
directed(c, 0);
|
|
2910
|
+
lookOf(spec);
|
|
2911
|
+
R.frame(S, spec, c, t, 0, false, c);
|
|
2912
|
+
if (looking) looking.draw(c, t);
|
|
2913
|
+
} finally { STATE.values = film; op = was; }
|
|
2914
|
+
},
|
|
2915
|
+
// true: drawn; false: the scene goes on unseen; 'painted': unseen, painted as when drawn (above)
|
|
2916
|
+
draw(how = true) { owed = how === true && drawn !== true; drawn = how === true || how === 'painted' ? how : false; },
|
|
2917
|
+
// what is where now, for whoever looks closely: { t (s), camera, frame (CSS px), elements (layout) }
|
|
2918
|
+
inspect: () => ({ t: Math.round(t * 1000) / 1000, camera: Object.fromEntries(AXES.map(a => [a, Math.round(shown[a] * 1e4) / 1e4])), frame: { ...box }, elements: layout(shown) }),
|
|
1471
2919
|
};
|
|
1472
2920
|
}
|
|
1473
2921
|
|
|
1474
2922
|
return {
|
|
1475
2923
|
mount,
|
|
1476
2924
|
// for the WebGL renderer (gl.js): the one-time painting code and camera maths
|
|
1477
|
-
internals: { STAGE, WIDEST, TAU, project, billboard, placeOf, camSetup, heroMatrix, STATE, READS, EXT, DEEP, Z_FAR, EYE, depths, view, limits,
|
|
2925
|
+
internals: { clock, STAGE, WIDEST, TAU, project, billboard, placeOf, camSetup, heroMatrix, STATE, READS, EXT, DEEP, Z_FAR, EYE, depths, view, limits, nearFade, objectBox, anchorOf, iris, opening, warn, seeded, hash, clamp, layer, rgb, glowSprite, grainTile, staticLayer, drawObject, PARTICLES, makeParticles, moveParticle,
|
|
2926
|
+
orderRules, arrange, clipOf, landLine, groundAt, land: () => !!SHARED.ground, CHECK, planeOf, overMirror, onPlane, cellsOf, PLANE_CELLS,
|
|
2927
|
+
lookOf, hazeReach, airOf, airGlow, sourcesNow, SOURCES, falloff, flickerNow, redrawOf, moved, PERF, shadowsOf, shadowLight, rayHit, ambientOf, shadowAt },
|
|
1478
2928
|
};
|
|
1479
2929
|
})();
|