diff --git a/site/public/film/ely-office.mp4 b/site/public/film/ely-office.mp4 index 266225a..882cc2f 100644 Binary files a/site/public/film/ely-office.mp4 and b/site/public/film/ely-office.mp4 differ diff --git a/site/test/film.test.ts b/site/test/film.test.ts new file mode 100644 index 0000000..0267a85 --- /dev/null +++ b/site/test/film.test.ts @@ -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"); + } +}); diff --git a/site/video/montage/scene.ts b/site/video/montage/scene.ts index 9a39e21..f97306e 100644 --- a/site/video/montage/scene.ts +++ b/site/video/montage/scene.ts @@ -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]; diff --git a/site/video/scenes/page.ts b/site/video/scenes/page.ts index 9e6c8dc..872d993 100644 --- a/site/video/scenes/page.ts +++ b/site/video/scenes/page.ts @@ -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("#frame"); const world = document.querySelector("#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[] { 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[] { 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); } diff --git a/site/video/scenes/parts.ts b/site/video/scenes/parts.ts index c137bda..76c4d48 100644 --- a/site/video/scenes/parts.ts +++ b/site/video/scenes/parts.ts @@ -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 }; +} diff --git a/site/video/scenes/turn.ts b/site/video/scenes/turn.ts index c49c231..2337cd1 100644 --- a/site/video/scenes/turn.ts +++ b/site/video/scenes/turn.ts @@ -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 } { diff --git a/site/video/type/scenes.js b/site/video/type/scenes.js index 663ef48..d9ffc74 100644 --- a/site/video/type/scenes.js +++ b/site/video/type/scenes.js @@ -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)); }; }