Video: leaving screens share the montage's geometry; renderer tests for names and recordings (R2.3)
This commit is contained in:
7 files changed
+70
-39
No files matched your search
Binary file not shown.
@@ -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");
|
||||
}
|
||||
});
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user