/** * The film on framefields.dev, built with the framefields package from npm. * Three scenes, each a function of the frame number: a headline, a chart, * and a word in 3D. The page scrolls through it next to this file. */ import { Composition, FilmGrain, Layer, Layer3D, LayerAnimation, Lighting, TextAnimator, Transition, Vignette, type LayoutNode, } from "framefields"; import { BLACK, DIM, FPS, H, MONO, OCHRE, PAPER, registerFonts, SANS, SURROUND, W, YELLOW, } from "./theme.js"; const FADE = 15; /** Where each scene sits on the film's timeline, in frames. */ export const SCENES = [ { id: "typeScene", from: 0, frames: 90 }, { id: "chartScene", from: 90 - FADE, frames: 90 }, { id: "threeDScene", from: 180 - 2 * FADE, frames: 105 }, ] as const; export const DURATION = SCENES[2].from + SCENES[2].frames; /** A full-frame black scene root; `Layer.sequence` sets its start and length. */ function scene(id: string, children: LayoutNode[]) { return Layer.box({ id, position: "absolute", x: 0, y: 0, width: W, height: H, background: BLACK, children, }); } function typeScene() { // Condensed headline lines that rise glyph by glyph, one after the other. const line = (id: string, text: string, y: number, start: number) => Layer.text(text, { id, position: "absolute", x: 140, y, fontFamily: SANS, fontWeight: 800, fontSize: 200, letterSpacing: -4, fill: PAPER, animators: [TextAnimator.waveRise({ y: 90, rotationX: 70 })], }).animate(LayerAnimation.create().kineticSweep(-1, 1, start, start + 40)); return scene("type", [ line("type-in", "TypeScript in.", 290, 0), line("type-out", "MP4 out.", 520, 16), Layer.text("$ pnpm render", { id: "type-cmd", position: "absolute", x: 148, y: 840, fontFamily: MONO, fontSize: 44, fill: YELLOW, }).animate(LayerAnimation.create().typewriter(40, 64)), ]); } function chartScene() { return scene("chart", [ Layer.text("Memory per render worker", { id: "chart-title", position: "absolute", x: 140, y: 120, fontFamily: SANS, fontWeight: 700, fontSize: 96, fill: PAPER, }).animate( LayerAnimation.create() .fadeIn(0, 14, "power2.out") .fromTo("y", 150, 120, { start: 0, end: 20, ease: "power3.out" }), ), // Two stacked series, one per bar, so each bar keeps its own colour. Layer.chart( { type: "bar", stacked: true, width: W - 280, height: 700, categories: ["Headless Chromium", "framefields"], series: [ { name: "Headless Chromium", color: SURROUND, data: [3, 0] }, { name: "framefields", color: YELLOW, data: [0, 0.3] }, ], yAxis: { min: 0, max: 3, ticks: 3, format: (v) => `${v} GB` }, legend: false, fontFamily: SANS, fontSize: 34, textColor: PAPER, gridColor: DIM, cornerRadius: 4, animate: { start: 14, duration: 34, stagger: 10, ease: "power3.out" }, }, { id: "chart-bars", position: "absolute", x: 140, y: 300 }, ), ]); } function threeDScene() { const frames = SCENES[2].frames; return scene("three-d", [ ...Lighting.studio({ target: [W / 2, H / 2, 0], shadows: false }), Layer3D.text({ id: "x-name", text: "framefields", fontFamily: SANS, fontWeight: 800, fontSize: 280, extrusionDepth: 26, fill: YELLOW, bevelColor: OCHRE, roughness: 0.35, x: W / 2, y: H / 2 - 60, z: 0, }).animate( // A slow turn that settles, so the side walls catch the light. LayerAnimation.create().keys("rotateY", [ [0, -20], [frames, 6, "sine.inOut"], ]), ), Layer.text("pnpm add framefields", { id: "x-cmd", position: "absolute", x: 0, y: H - 240, width: W, align: "center", fontFamily: MONO, fontSize: 46, fill: PAPER, }).animate(LayerAnimation.create().fadeIn(36, 56, "power2.out")), ]); } const BUILD = { typeScene, chartScene, threeDScene }; export async function buildFilm(): Promise { // Fonts first: extruded text and layout read the real outlines. await registerFonts(); const film = new Composition({ width: W, height: H, fps: FPS, durationMs: (DURATION / FPS) * 1000, backgroundColor: BLACK, }); const scenes = Layer.sequence( SCENES.map((s, i) => ({ scene: BUILD[s.id](), duration: s.frames, transition: i === 0 ? undefined : Transition.fadeThrough(FADE), })), { width: W, height: H }, ); film.add(scenes); film.apply(new Vignette({ strength: 0.12, radius: 0.95, softness: 0.7 })); film.apply(new FilmGrain({ strength: 0.035, size: 1.4, animated: true })); return film; }