Files
zeekayandHanzo Dev 68eeb2c338 Wordmark up top, Triangle demos in motion
The header carries the LUX wordmark; the triangle stays for the chat
launcher, larger, borderless, glowing only on hover. In-page demos
brand as Triangle with the filled down triangle. Six product screens
animate as 8-second looping mp4s (~150KB each) recorded from
screens/*.html by screens/record.mjs and shown in laptop frames on
the home page.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-31 12:19:44 -07:00

64 lines
2.4 KiB
JavaScript

// Record each animated screen to a small looping mp4 + jpg poster.
// Usage: node screens/record.mjs [name ...] (default: all)
// Requires: npx playwright (chromium), ffmpeg on PATH.
import { execFileSync } from "node:child_process";
import { mkdirSync, readdirSync, statSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const outDir = join(here, "..", "public", "screens");
mkdirSync(outDir, { recursive: true });
const names = process.argv.slice(2).length
? process.argv.slice(2)
: readdirSync(here).filter((f) => f.endsWith(".html")).map((f) => f.replace(".html", ""));
const LOOP_MS = 8000;
const W = 1440, H = 900;
const { chromium } = await import("playwright");
const browser = await chromium.launch();
for (const name of names) {
const page = await browser.newPage({ viewport: { width: W, height: H }, deviceScaleFactor: 1 });
await page.goto(pathToFileURL(join(here, `${name}.html`)).href);
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(300);
// capture the second full cycle so the loop point is seamless
const fps = 30;
const frames = (LOOP_MS / 1000) * fps;
const frameDir = join(outDir, `.${name}-frames`);
rmSync(frameDir, { recursive: true, force: true });
mkdirSync(frameDir, { recursive: true });
// wait until the start of the next cycle boundary, then step frames
await page.waitForTimeout(LOOP_MS - 300);
const t0 = Date.now();
for (let i = 0; i < frames; i++) {
const target = t0 + (i * 1000) / fps;
const wait = target - Date.now();
if (wait > 0) await page.waitForTimeout(wait);
await page.screenshot({ path: join(frameDir, `f${String(i).padStart(4, "0")}.png`) });
}
await page.close();
execFileSync("ffmpeg", [
"-y", "-framerate", String(fps), "-i", join(frameDir, "f%04d.png"),
"-vf", "scale=1280:-2", "-c:v", "libx264", "-preset", "slow", "-crf", "30",
"-pix_fmt", "yuv420p", "-movflags", "+faststart", "-an",
join(outDir, `${name}.mp4`),
], { stdio: "pipe" });
execFileSync("ffmpeg", [
"-y", "-i", join(frameDir, "f0059.png"), "-vf", "scale=1280:-2", "-q:v", "6",
join(outDir, `${name}.jpg`),
], { stdio: "pipe" });
rmSync(frameDir, { recursive: true, force: true });
const kb = Math.round(statSync(join(outDir, `${name}.mp4`)).size / 1024);
console.log(`${name}.mp4 ${kb} KB`);
}
await browser.close();