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.
|
// Montage: six screens on the turning model, in WebGL.
|
||||||
import { model } from "../../src/model.ts";
|
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 { BEAT, glide, toward } from "../scenes/time.ts";
|
||||||
import recs from "./recs.json";
|
import recs from "./recs.json";
|
||||||
|
|
||||||
@@ -12,18 +12,9 @@ export const FIRST = recs.first;
|
|||||||
export const COUNT = 129;
|
export const COUNT = 129;
|
||||||
const TURN: [number, number] = [128, 136];
|
const TURN: [number, number] = [128, 136];
|
||||||
const PIVOT = [8, 5.5];
|
const PIVOT = [8, 5.5];
|
||||||
const GROW = 1.5;
|
|
||||||
const TOP = model[0].blocks[0].h;
|
|
||||||
const apps = recs.apps;
|
const apps = recs.apps;
|
||||||
|
|
||||||
/** A screen grown about its bottom centre; Show on its base. */
|
const slabs = apps.map((a) => grown(a.app));
|
||||||
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 ground = model.flatMap((p) => p.blocks).filter((b) => b !== screens.ElyShow);
|
const ground = model.flatMap((p) => p.blocks).filter((b) => b !== screens.ElyShow);
|
||||||
|
|
||||||
type Rgb = [number, number, number];
|
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 { type Box, DESK, PAPER, SCREEN, SHEET, lamp, props, screens, tiles } from "./parts.ts";
|
||||||
import { BAR, BEAT, glide, toward } from "./time.ts";
|
import { BAR, BEAT, glide, toward } from "./time.ts";
|
||||||
import { DESK_END, type Moment, SEED, TURN, moment } from "./timeline.ts";
|
import { DESK_END, type Moment, SEED, TURN, moment } from "./timeline.ts";
|
||||||
import { film, montage, slabs, turnedBlock, turnedOrder } from "./turn.ts";
|
import { film, montage, recording, turnedBlock, turnedOrder } from "./turn.ts";
|
||||||
import recs from "../montage/recs.json";
|
|
||||||
import { view } from "../opening/plan.ts";
|
import { view } from "../opening/plan.ts";
|
||||||
|
|
||||||
export const WIDTH = 1920;
|
export const WIDTH = 1920;
|
||||||
@@ -167,7 +166,7 @@ const svg = document.querySelector<SVGSVGElement>("#frame");
|
|||||||
const world = document.querySelector<SVGGElement>("#world");
|
const world = document.querySelector<SVGGElement>("#world");
|
||||||
if (!svg || !world) throw new Error("index.html lacks #frame or #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>[] {
|
function draw(m: Moment, cam: Camera, f: number): Promise<void>[] {
|
||||||
const page = mix(PAGE[0], PAGE[1], m.dark);
|
const page = mix(PAGE[0], PAGE[1], m.dark);
|
||||||
document.body.style.background = page;
|
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 groups = m.pieces.map(({ box, land, dark }) => {
|
||||||
const toned = { ...box, tone: tone(box.tone, dark) };
|
const toned = { ...box, tone: tone(box.tone, dark) };
|
||||||
const g = m.angle ? turnedBlock(toned, m.angle) : block(toned);
|
const g = m.angle ? turnedBlock(toned, m.angle) : block(toned);
|
||||||
const slab = slabs.indexOf(box);
|
const href = recording(box, land, f);
|
||||||
if (slab >= 0 && land > 0) {
|
if (href) {
|
||||||
const index = String(f - recs.first + 1).padStart(5, "0");
|
const { el, ready } = film(box, m.angle, href);
|
||||||
const { el, ready } = film(box, m.angle, `rec/${recs.apps[slab].rec}/${index}.jpg`);
|
|
||||||
g.append(el);
|
g.append(el);
|
||||||
loads.push(ready);
|
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.
|
// The model turned about the desk centre, as the montage draws it.
|
||||||
import { project } from "../../src/iso.ts";
|
import { project } from "../../src/iso.ts";
|
||||||
import { model } from "../../src/model.ts";
|
import { model } from "../../src/model.ts";
|
||||||
import recs from "../montage/recs.json";
|
import recs from "../montage/recs.json" with { type: "json" };
|
||||||
import { type Box, SCREEN, screens } from "./parts.ts";
|
import { type Box, grown } from "./parts.ts";
|
||||||
|
|
||||||
const SVG = "http://www.w3.org/2000/svg";
|
const SVG = "http://www.w3.org/2000/svg";
|
||||||
export const PIVOT = [8, 5.5];
|
export const PIVOT = [8, 5.5];
|
||||||
const COS = Math.cos(Math.PI / 6);
|
const COS = Math.cos(Math.PI / 6);
|
||||||
const TOP = model[0].blocks[0].h;
|
|
||||||
|
|
||||||
/** A world point turned by `a`, then projected. */
|
/** A world point turned by `a`, then projected. */
|
||||||
export function turned(a: number, x: number, y: number, z: number): [number, number] {
|
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. */
|
/** The montage's grown screens, which leave in bar 35. */
|
||||||
export const slabs: Box[] = recs.apps.map(({ app }) => {
|
export const slabs: Box[] = recs.apps.map(({ app }) => grown(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.
|
/** The recording a leaving slab plays at frame `f`, if any. */
|
||||||
const b = app === "ElyTree" ? { ...screens[app], y: 7.9 } : screens[app];
|
export function recording(box: Box, land: number, f: number): string | null {
|
||||||
return { ...b, x: b.x - b.w / 4, w: b.w * 1.5, h: b.h * 1.5 };
|
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. */
|
/** 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> } {
|
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 });
|
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. */
|
/** An app's first bar: its name builds at left. */
|
||||||
function app(name, b) {
|
function app(name, b) {
|
||||||
return (ctx, t) => {
|
return (ctx, t) => {
|
||||||
const t0 = bar(b);
|
const v = place(word(name, () => INK), 16, { left: 112, cy: 800 });
|
||||||
const list = word(name, () => INK);
|
cubes(ctx, v, nameCubes(name, b, t));
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user