Video: app names clear before the flatten; montage screens play while they lift (R2.3)
This commit is contained in:
7 files changed
+104
-18
No files matched your search
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"first": 2057,
|
||||
"apps": [
|
||||
{
|
||||
"app": "ElyMark",
|
||||
"rec": "mark",
|
||||
"from": 7.0,
|
||||
"beat": 128
|
||||
},
|
||||
{
|
||||
"app": "ElyTable",
|
||||
"rec": "table",
|
||||
"from": 1.6,
|
||||
"beat": 128.25
|
||||
},
|
||||
{
|
||||
"app": "ElyShow",
|
||||
"rec": "show",
|
||||
"from": 12.2,
|
||||
"beat": 128.5
|
||||
},
|
||||
{
|
||||
"app": "ElyDoc",
|
||||
"rec": "doc",
|
||||
"from": 3.6,
|
||||
"beat": 128.75
|
||||
},
|
||||
{
|
||||
"app": "ElyPDF",
|
||||
"rec": "pdf",
|
||||
"from": 8.8,
|
||||
"beat": 129
|
||||
},
|
||||
{
|
||||
"app": "ElyTree",
|
||||
"rec": "tree",
|
||||
"from": 11.0,
|
||||
"beat": 129
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,27 +2,19 @@
|
||||
import { model } from "../../src/model.ts";
|
||||
import { type Box, SCREEN, screens } from "../scenes/parts.ts";
|
||||
import { BEAT, glide, toward } from "../scenes/time.ts";
|
||||
import recs from "./recs.json";
|
||||
|
||||
const WIDTH = 1920;
|
||||
const HEIGHT = 1080;
|
||||
const FPS = 30;
|
||||
/** Bar 33 downbeat to the frame before bar 35. */
|
||||
export const FIRST = 2057;
|
||||
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;
|
||||
|
||||
/** Recording file, its start second, and landing beat. */
|
||||
export const apps = [
|
||||
{ app: "ElyMark", rec: "mark", from: 7.0, beat: 128 },
|
||||
{ app: "ElyTable", rec: "table", from: 1.6, beat: 128.25 },
|
||||
{ app: "ElyShow", rec: "show", from: 12.2, beat: 128.5 },
|
||||
{ app: "ElyDoc", rec: "doc", from: 3.6, beat: 128.75 },
|
||||
{ app: "ElyPDF", rec: "pdf", from: 8.8, beat: 129 },
|
||||
{ app: "ElyTree", rec: "tree", from: 11.0, beat: 129 },
|
||||
];
|
||||
const apps = recs.apps;
|
||||
|
||||
/** A screen grown about its bottom centre; Show on its base. */
|
||||
function slab(app: string): Box {
|
||||
|
||||
@@ -5,7 +5,8 @@ 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 { montage, turnedBlock, turnedOrder } from "./turn.ts";
|
||||
import { film, montage, slabs, turnedBlock, turnedOrder } from "./turn.ts";
|
||||
import recs from "../montage/recs.json";
|
||||
import { view } from "../opening/plan.ts";
|
||||
|
||||
export const WIDTH = 1920;
|
||||
@@ -166,14 +167,23 @@ const svg = document.querySelector<SVGSVGElement>("#frame");
|
||||
const world = document.querySelector<SVGGElement>("#world");
|
||||
if (!svg || !world) throw new Error("index.html lacks #frame or #world");
|
||||
|
||||
function draw(m: Moment, cam: Camera) {
|
||||
/** Draws the moment; slabs carry the montage recordings at frame `f`. */
|
||||
function draw(m: Moment, cam: Camera, f: number): Promise<void>[] {
|
||||
const page = mix(PAGE[0], PAGE[1], m.dark);
|
||||
document.body.style.background = page;
|
||||
svg!.style.background = page;
|
||||
world!.setAttribute("transform", `matrix(${cam.s} 0 0 ${cam.s} ${cam.x} ${cam.y})`);
|
||||
const loads: 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`);
|
||||
g.append(el);
|
||||
loads.push(ready);
|
||||
}
|
||||
if (tiles.includes(box)) g.classList.add("tile");
|
||||
if (land < 1) {
|
||||
g.setAttribute("transform", `translate(0 ${(-UNIT * (1 - land)).toFixed(3)})`);
|
||||
@@ -189,6 +199,7 @@ function draw(m: Moment, cam: Camera) {
|
||||
const [day, dark] = el.dataset.soft === undefined ? INK : SOFT;
|
||||
el.style.color = mix(day, dark, m.dark);
|
||||
}
|
||||
return loads;
|
||||
}
|
||||
|
||||
/** Texts another layer draws instead. */
|
||||
@@ -202,7 +213,7 @@ export interface Face {
|
||||
}
|
||||
|
||||
/** Draws global frame `f` of `scene`; plates return the face. */
|
||||
function render(name: string, f: number): Face | null {
|
||||
async function render(name: string, f: number): Promise<Face | null> {
|
||||
const scene = scenes.find((s) => s.name === name);
|
||||
if (!scene) throw new Error(`no scene ${name}`);
|
||||
const [first, end] = frames(scene);
|
||||
@@ -212,7 +223,8 @@ function render(name: string, f: number): Face | null {
|
||||
const beat = scene.from * 4;
|
||||
const m = moment(t, app ? { app, beat: beat + 1 } : undefined);
|
||||
const { cam, flat } = app ? push(app, t, beat) : { cam: camera(t), flat: 0 };
|
||||
draw(m, cam);
|
||||
await Promise.all(draw(m, cam, f));
|
||||
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
|
||||
if (!app) return null;
|
||||
const piece = m.pieces.find((p) => p.box === screens[app]);
|
||||
if (!piece) return { opacity: 0, flat, corners: [] };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Renders iso scenes to PNG frames, faces and MP4s.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright";
|
||||
@@ -16,6 +17,9 @@ const hide = [["type-title", ["title"]], ["type-end", ["name", "version", "url"]
|
||||
.filter(([d]) => existsSync(join(extra, d)))
|
||||
.flatMap(([, t]) => t);
|
||||
|
||||
const recs = JSON.parse(readFileSync(resolve(here, "../montage/recs.json"), "utf8"));
|
||||
const frames = mkdtempSync(join(tmpdir(), "slabs-"));
|
||||
|
||||
const server = await createServer({ root: site, configFile: false, logLevel: "error", server: { port: 0 } });
|
||||
await server.listen();
|
||||
const url = `${server.resolvedUrls.local[0]}video/scenes/index.html`;
|
||||
@@ -30,6 +34,10 @@ async function open() {
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") throw new Error(m.text());
|
||||
});
|
||||
await page.route("**/rec/**", (route) => {
|
||||
const path = new URL(route.request().url()).pathname.split("/rec/")[1];
|
||||
route.fulfill({ body: readFileSync(join(frames, path)), contentType: "image/jpeg" });
|
||||
});
|
||||
await page.goto(url, { waitUntil: "networkidle" });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.evaluate((names) => window.ely.hide(names), hide);
|
||||
@@ -69,6 +77,18 @@ async function scene(page, s) {
|
||||
return { name: s.name, app: s.app ?? null, bars: [s.from, s.to], first, frames: end - first };
|
||||
}
|
||||
|
||||
/** The montage's recording frames, continued for the leaving slabs. */
|
||||
const files = scenes.find((s) => s.name === "files");
|
||||
if (files) {
|
||||
for (const { rec, from } of recs.apps) {
|
||||
mkdirSync(join(frames, rec));
|
||||
const args = ["-ss", String(from), "-i", resolve(here, `../out/rec/${rec}.mp4`), "-frames:v", String(files.range[1] - recs.first)];
|
||||
const vf = ["-vf", "fps=30,scale=1280:720:flags=lanczos", "-q:v", "2", join(frames, rec, "%05d.jpg")];
|
||||
const run = spawnSync("ffmpeg", ["-y", "-v", "error", ...args, ...vf], { stdio: "inherit" });
|
||||
if (run.status !== 0) throw new Error(`ffmpeg failed for ${rec}`);
|
||||
}
|
||||
}
|
||||
|
||||
const queue = [...scenes];
|
||||
const done = [];
|
||||
await Promise.all(
|
||||
@@ -82,3 +102,4 @@ if (!wanted.length) {
|
||||
}
|
||||
await browser.close();
|
||||
await server.close();
|
||||
rmSync(frames, { recursive: true, force: true });
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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";
|
||||
|
||||
const SVG = "http://www.w3.org/2000/svg";
|
||||
@@ -15,13 +16,32 @@ 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[] = ["ElyMark", "ElyTable", "ElyShow", "ElyDoc", "ElyPDF", "ElyTree"].map((app) => {
|
||||
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 };
|
||||
});
|
||||
|
||||
/** 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> } {
|
||||
const Y = b.y + b.d;
|
||||
const [o, r, d] = [turned(a, b.x, Y, b.z + b.h), turned(a, b.x + b.w, Y, b.z + b.h), turned(a, b.x, Y, b.z)];
|
||||
const [w, h] = [1280, 720];
|
||||
const el = document.createElementNS(SVG, "image");
|
||||
const m = [(r[0] - o[0]) / w, (r[1] - o[1]) / w, (d[0] - o[0]) / h, (d[1] - o[1]) / h, o[0], o[1]];
|
||||
el.setAttribute("width", String(w));
|
||||
el.setAttribute("height", String(h));
|
||||
el.setAttribute("preserveAspectRatio", "none");
|
||||
el.setAttribute("transform", `matrix(${m.map((v) => v.toFixed(5)).join(" ")})`);
|
||||
const ready = new Promise<void>((ok, fail) => {
|
||||
el.addEventListener("load", () => ok());
|
||||
el.addEventListener("error", () => fail(new Error(`no frame ${href}`)));
|
||||
});
|
||||
el.setAttribute("href", href);
|
||||
return { el, ready };
|
||||
}
|
||||
|
||||
/** The montage's fixed framing, in this renderer's camera terms. */
|
||||
export const montage = (() => {
|
||||
const boxes = [...model.flatMap((p) => p.blocks), ...slabs, ...slabs.map((b) => ({ ...b, z: b.z + 1 }))];
|
||||
|
||||
@@ -129,7 +129,7 @@ function app(name, b) {
|
||||
const boxes = list.flatMap((c, i) => {
|
||||
const at = arrive(t, t0 + (build[i] + 1) * SIX);
|
||||
if (!at) return [];
|
||||
const gone = lift(t, t0 + (13 + out[i]) * SIX, LAND);
|
||||
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