Files
ElyOffice/site/video/scenes/timeline.ts
T

107 lines
4.3 KiB
TypeScript

// The model at any moment, as pieces and texts.
import { model } from "../../src/model.ts";
import { LAMP, type Box, file, lamp, length, props, screens, tiles, trips } from "./parts.ts";
import { BEAT, LAND, begun, glide, toward } from "./time.ts";
import { slabs } from "./turn.ts";
/** A box, its landing progress, and whether it is dark. */
export interface Piece {
box: Box;
land: number;
dark: number;
}
/** Everything one frame shows, before the camera. */
export interface Moment {
pieces: Piece[];
/** The page and texts: 0 light, 1 dark. */
dark: number;
/** The model's turn about the desk centre, radians. */
angle: number;
texts: Record<string, number>;
}
/** The desk grows outward from this tile. */
export const SEED = [8, 5];
const ring = (b: Box) => Math.abs(b.x - SEED[0]) + Math.abs(b.y - SEED[1]);
const angle = (b: Box) => Math.atan2(b.y - SEED[1], b.x - SEED[0]);
/** Tiles land faster and faster: count grows as time squared. */
export const DESK_END = 12;
const byRing = tiles.map((_, i) => i).sort((a, b) => ring(tiles[a]) - ring(tiles[b]) || angle(tiles[a]) - angle(tiles[b]));
const tileBeats: number[] = [];
byRing.forEach((tile, n) => {
tileBeats[tile] = Math.round(DESK_END * Math.sqrt(n / (tiles.length - 1)) * 4) / 4;
});
const DESK_DONE = DESK_END + 1;
/** Beat each app starts on; its blocks follow on sixteenths. */
const partBeats = [16, 18, 20, 22, 24, 26, 28];
/** Fade in ending on the first beat; gone at the second. */
const TEXTS: Record<string, [number, number]> = {
title: [24, 32],
offline: [144.5, 147.5],
open: [148.5, 151.5],
local: [152.5, 155.5],
name: [160, 168.1],
version: [160.5, 168.1],
url: [161, 168.1],
};
/** Bar 35 turns back from the montage's -90°. */
export const TURN: [number, number] = [136, 140];
/** The dark wave: from the lamp, one step per sixteenth. */
export const WAVE = 140;
export const NIGHT = 144;
const lampFar = Math.max(...tiles.map((t) => Math.hypot(t.x + 0.5 - LAMP.centre[0], t.y + 0.5 - LAMP.centre[1])));
function flip(b: Box): number {
const d = Math.hypot(b.x + b.w / 2 - LAMP.centre[0], b.y + b.d / 2 - LAMP.centre[1]);
return WAVE + Math.min(15, Math.floor((16 * d) / lampFar)) / 4;
}
function landed(t: number, box: Box, beat: number, out: Piece[]) {
if (begun(t, beat)) out.push({ box, land: toward(t, beat), dark: t >= flip(box) * BEAT ? 1 : 0 });
}
/** A trip's file: dropped at its start, then hopping. */
function travel(t: number, trip: (typeof trips)[number], out: Piece[]) {
if (!begun(t, trip.lands)) return;
const step = length(trip.path) / trip.hops.length;
const s = trip.hops.reduce((sum, beat) => sum + step * toward(t, beat), 0);
out.push({ box: file(trip, s), land: toward(t, trip.lands), dark: 0 });
}
/** The model at `t`, plus a plate's screen. */
export function moment(t: number, screen?: { app: string; beat: number }): Moment {
const pieces: Piece[] = [];
const wave = t >= (WAVE - 0.01) * BEAT && t < NIGHT * BEAT;
if (t < DESK_DONE * BEAT || wave) tiles.forEach((tile, i) => landed(t, tile, tileBeats[i], pieces));
else landed(t, model[0].blocks[0], 0, pieces);
model.slice(1).forEach((part, p) => part.blocks.forEach((b, i) => landed(t, b, partBeats[p] + i / 4, pieces)));
lamp.forEach((b, i) => landed(t, b, LAMP.beat + i / 4, pieces));
for (const trip of trips) travel(t, trip, pieces);
for (const prop of props) {
prop.blocks.forEach((box, i) => {
if (begun(t, prop.bar * 4 + i / 4)) pieces.push({ box, land: toward(t, prop.bar * 4 + i / 4), dark: 1 });
});
}
if (t >= TURN[0] * BEAT - 1e-6 && t < 138 * BEAT) {
slabs.forEach((box, i) => pieces.push({ box, land: 1 - toward(t, 136.5 + i / 4), dark: 0 }));
}
if (screen) {
const box = screens[screen.app];
if (!box) throw new Error(`no screen for ${screen.app}`);
landed(t, box, screen.beat, pieces);
}
const texts: Record<string, number> = {};
for (const [name, [from, to]] of Object.entries(TEXTS)) {
if (screen || t >= to * BEAT) continue;
const out = to < 168 ? 1 - toward(t, to, LAND, glide) : 1;
texts[name] = toward(t, from, LAND, glide) * out;
}
const turn = t < TURN[0] * BEAT || t >= TURN[1] * BEAT ? 0 : 1 - glide((t / BEAT - TURN[0]) / (TURN[1] - TURN[0]));
return { pieces, dark: t >= NIGHT * BEAT ? 1 : 0, angle: (-Math.PI / 2) * turn, texts };
}