Video: leaving screens share the montage's geometry; renderer tests for names and recordings (R2.3)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 05:19:34 -04:00
1 parent 44a61443fa
commit 1bf693f7ab
7 files changed
+70 -39

No files matched your search

Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { BEAT, bar } from "../video/type/draw.js";
import { nameCubes } from "../video/type/scenes.js";
import { moment } from "../video/scenes/timeline.ts";
import { recording, slabs } from "../video/scenes/turn.ts";
import { grown } from "../video/scenes/parts.ts";
import recs from "../video/montage/recs.json" with { type: "json" };
const APPS = ["Doc", "Table", "Show", "PDF", "Tree", "Mark"];
test("each app's name clears before its screen flattens", () => {
APPS.forEach((name, i) => {
const b = 9 + 4 * i;
// The screen flattens on the bar's last beat.
const flat = bar(b) + 3 * BEAT;
assert.ok(nameCubes(`Ely${name}`, b, flat - BEAT).some((c) => c.a > 0), `Ely${name} shows first`);
assert.ok(nameCubes(`Ely${name}`, b, flat).every((c) => c.a === 0), `Ely${name} is gone`);
});
});
test("leaving screens keep their place and play as they go", () => {
assert.deepEqual(slabs, recs.apps.map(({ app }) => grown(app)), "the montage's own screens");
for (let f = 2186; f < 2215; f++) {
const shown = moment(f / 30).pieces.filter(({ box, land }) => slabs.includes(box) && land > 0);
assert.ok(shown.every(({ box, land }) => recording(box, land, f)), `frame ${f}`);
if (f === 2186) assert.equal(shown.length, 6, "all six cross the cut");
}
});
+2 -11
View File
@@ -1,6 +1,6 @@
// Montage: six screens on the turning model, in WebGL.
import { model } from "../../src/model.ts";
import { type Box, SCREEN, screens } from "../scenes/parts.ts";
import { type Box, grown, screens } from "../scenes/parts.ts";
import { BEAT, glide, toward } from "../scenes/time.ts";
import recs from "./recs.json";
@@ -12,18 +12,9 @@ export const FIRST = recs.first;
export const COUNT = 129;
const TURN: [number, number] = [128, 136];
const PIVOT = [8, 5.5];
const GROW = 1.5;
const TOP = model[0].blocks[0].h;
const apps = recs.apps;
/** A screen grown about its bottom centre; Show on its base. */
function slab(app: string): Box {
if (app === "ElyShow") return { x: 8.6, y: 2, z: TOP + 0.25, w: 4.8, d: 0.08, h: 2.7, tone: SCREEN };
const b = screens[app];
const w = b.w * GROW;
return { ...b, x: b.x + (b.w - w) / 2, w, h: b.h * GROW };
}
const slabs = apps.map((a) => slab(a.app));
const slabs = apps.map((a) => grown(a.app));
const ground = model.flatMap((p) => p.blocks).filter((b) => b !== screens.ElyShow);
type Rgb = [number, number, number];
+5 -7
View File
@@ -5,8 +5,7 @@ import { ACCENT, model } from "../../src/model.ts";
import { type Box, DESK, PAPER, SCREEN, SHEET, lamp, props, screens, tiles } from "./parts.ts";
import { BAR, BEAT, glide, toward } from "./time.ts";
import { DESK_END, type Moment, SEED, TURN, moment } from "./timeline.ts";
import { film, montage, slabs, turnedBlock, turnedOrder } from "./turn.ts";
import recs from "../montage/recs.json";
import { film, montage, recording, turnedBlock, turnedOrder } from "./turn.ts";
import { view } from "../opening/plan.ts";
export const WIDTH = 1920;
@@ -167,7 +166,7 @@ const svg = document.querySelector<SVGSVGElement>("#frame");
const world = document.querySelector<SVGGElement>("#world");
if (!svg || !world) throw new Error("index.html lacks #frame or #world");
/** Draws the moment; slabs carry the montage recordings at frame `f`. */
/** Draws the moment; leaving slabs carry their recordings. */
function draw(m: Moment, cam: Camera, f: number): Promise<void>[] {
const page = mix(PAGE[0], PAGE[1], m.dark);
document.body.style.background = page;
@@ -177,10 +176,9 @@ function draw(m: Moment, cam: Camera, f: number): Promise<void>[] {
const groups = m.pieces.map(({ box, land, dark }) => {
const toned = { ...box, tone: tone(box.tone, dark) };
const g = m.angle ? turnedBlock(toned, m.angle) : block(toned);
const slab = slabs.indexOf(box);
if (slab >= 0 && land > 0) {
const index = String(f - recs.first + 1).padStart(5, "0");
const { el, ready } = film(box, m.angle, `rec/${recs.apps[slab].rec}/${index}.jpg`);
const href = recording(box, land, f);
if (href) {
const { el, ready } = film(box, m.angle, href);
g.append(el);
loads.push(ready);
}
+8
View File
@@ -163,3 +163,11 @@ export const props: Prop[] = [
]),
},
];
/** A screen grown about its bottom centre; Show on its base. */
export function grown(app: string): Box {
if (app === "ElyShow") return { x: 8.6, y: 2, z: model[0].blocks[0].h + 0.25, w: 4.8, d: 0.08, h: 2.7, tone: SCREEN };
const b = screens[app];
const w = b.w * 1.5;
return { ...b, x: b.x + (b.w - w) / 2, w, h: b.h * 1.5 };
}
+10 -9
View File
@@ -1,13 +1,12 @@
// The model turned about the desk centre, as the montage draws it.
import { project } from "../../src/iso.ts";
import { model } from "../../src/model.ts";
import recs from "../montage/recs.json";
import { type Box, SCREEN, screens } from "./parts.ts";
import recs from "../montage/recs.json" with { type: "json" };
import { type Box, grown } from "./parts.ts";
const SVG = "http://www.w3.org/2000/svg";
export const PIVOT = [8, 5.5];
const COS = Math.cos(Math.PI / 6);
const TOP = model[0].blocks[0].h;
/** A world point turned by `a`, then projected. */
export function turned(a: number, x: number, y: number, z: number): [number, number] {
@@ -16,12 +15,14 @@ export function turned(a: number, x: number, y: number, z: number): [number, num
}
/** The montage's grown screens, which leave in bar 35. */
export const slabs: Box[] = recs.apps.map(({ app }) => {
if (app === "ElyShow") return { x: 8.6, y: 2, z: TOP + 0.25, w: 4.8, d: 0.08, h: 2.7, tone: SCREEN };
// The montage drew Tree's screen at its old depth.
const b = app === "ElyTree" ? { ...screens[app], y: 7.9 } : screens[app];
return { ...b, x: b.x - b.w / 4, w: b.w * 1.5, h: b.h * 1.5 };
});
export const slabs: Box[] = recs.apps.map(({ app }) => grown(app));
/** The recording a leaving slab plays at frame `f`, if any. */
export function recording(box: Box, land: number, f: number): string | null {
const slab = slabs.indexOf(box);
if (slab < 0 || land <= 0) return null;
return `rec/${recs.apps[slab].rec}/${String(f - recs.first + 1).padStart(5, "0")}.jpg`;
}
/** A slab's recording frame, pinned affinely on its +y face. */
export function film(b: Box, a: number, href: string): { el: SVGImageElement; ready: Promise<void> } {
+16 -12
View File
@@ -118,21 +118,25 @@ function title(ctx, t) {
text(ctx, { s: "六个桌面应用", x: left, y: y + 46, size: 28, color: SOFT, a, align: "left", track: 2 });
}
/** App `name`'s letter cubes at `t` in its bar `b`. */
export function nameCubes(name, b, t) {
const t0 = bar(b);
const list = word(name, () => INK);
const build = slots(list, 8);
const out = slots(list, 4, 0);
return list.flatMap((c, i) => {
const at = arrive(t, t0 + (build[i] + 1) * SIX);
if (!at) return [];
const gone = lift(t, t0 + (9 + out[i]) * SIX, LAND);
return [box(c, at.dz + gone.dz, at.a * gone.a)];
});
}
/** An app's first bar: its name builds at left. */
function app(name, b) {
return (ctx, t) => {
const t0 = bar(b);
const list = word(name, () => INK);
const build = slots(list, 8);
const out = slots(list, 4, 0);
const v = place(list, 16, { left: 112, cy: 800 });
const boxes = list.flatMap((c, i) => {
const at = arrive(t, t0 + (build[i] + 1) * SIX);
if (!at) return [];
const gone = lift(t, t0 + (9 + out[i]) * SIX, LAND);
return [box(c, at.dz + gone.dz, at.a * gone.a)];
});
cubes(ctx, v, boxes);
const v = place(word(name, () => INK), 16, { left: 112, cy: 800 });
cubes(ctx, v, nameCubes(name, b, t));
};
}