Skip to content

Commit fed21ab

Browse files
Merge pull request #53 from posecode-dev/docs/launch-gif-set
docs: launch asset set (GIFs, screenshots, logo lockup) + shared capture pipeline
2 parents e4d01b4 + 3703af1 commit fed21ab

14 files changed

Lines changed: 352 additions & 105 deletions

docs/brand/posecode-logo-dark.png

15.9 KB
Loading

docs/brand/posecode-logo.png

17 KB
Loading

docs/launch-media/dead-bug.gif

1.05 MB
Loading

docs/launch-media/jab-cross.gif

533 KB
Loading
419 KB
Loading

docs/launch-media/play-desktop.png

427 KB
Loading

docs/launch-media/play-mobile.png

338 KB
Loading
6.48 KB
Loading

docs/launch-media/wall-sit.gif

1.6 MB
Loading

scripts/capture-gifs.mjs

Lines changed: 5 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,17 @@
22
/**
33
* Regenerates the README movement GIFs from the real renderer.
44
*
5-
* Boots the playground with Vite, drives the live viewer headlessly with
6-
* Playwright (seek → captureFrame per animation frame), and encodes looping
7-
* GIFs with gifenc. Frames are composed to the target size inside the page
8-
* (cover-crop of the viewer canvas), so no image tooling is needed in node.
9-
*
10-
* Not wired into `npm run build`: run it manually when the figure or a
11-
* showcased movement changes, and commit the output.
5+
* Thin wrapper over scripts/lib/gif-capture.mjs (the shared capture core, also
6+
* used by capture-launch-gifs.mjs). Not wired into `npm run build`: run it
7+
* manually when the figure or a showcased movement changes, and commit output.
128
*
139
* Usage:
1410
* node scripts/capture-gifs.mjs # all README gifs
1511
* node scripts/capture-gifs.mjs squat # just one
16-
*
17-
* The Chromium binary is resolved from PLAYWRIGHT_BROWSERS_PATH/chromium or
18-
* POSECODE_CHROMIUM.
1912
*/
20-
import { createServer } from "vite";
21-
import { chromium } from "playwright-core";
22-
import gifencPkg from "gifenc"; // CJS: no named ESM exports
23-
const { GIFEncoder, quantize, applyPalette } = gifencPkg;
24-
import { writeFile } from "node:fs/promises";
25-
import { existsSync } from "node:fs";
2613
import { fileURLToPath } from "node:url";
2714
import { dirname, resolve } from "node:path";
15+
import { captureGifs } from "./lib/gif-capture.mjs";
2816

2917
const here = dirname(fileURLToPath(import.meta.url));
3018
const repoRoot = resolve(here, "..");
@@ -37,99 +25,11 @@ const TARGETS = [
3725
{ id: "lateral", out: "lateral-raise", size: [420, 582], fps: 14 },
3826
];
3927

40-
function chromiumPath() {
41-
if (process.env.POSECODE_CHROMIUM) return process.env.POSECODE_CHROMIUM;
42-
const base = process.env.PLAYWRIGHT_BROWSERS_PATH;
43-
if (base && existsSync(`${base}/chromium`)) return `${base}/chromium`;
44-
return chromium.executablePath();
45-
}
46-
4728
const only = process.argv[2];
4829
const targets = TARGETS.filter((t) => !only || t.id === only || t.out === only);
4930
if (targets.length === 0) {
5031
console.error(`no such target: ${only}`);
5132
process.exit(1);
5233
}
5334

54-
const server = await createServer({
55-
configFile: resolve(repoRoot, "playground/vite.config.ts"),
56-
server: { port: 0, host: "127.0.0.1" },
57-
logLevel: "error",
58-
});
59-
await server.listen();
60-
// Use the ACTUAL bound port: with `port: 0`, config.server.port stays 0 (and
61-
// `0 ?? …` keeps 0), so read the listening socket instead.
62-
const port = server.httpServer.address().port;
63-
const origin = `http://127.0.0.1:${port}`;
64-
65-
const browser = await chromium.launch({ executablePath: chromiumPath() });
66-
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
67-
page.on("pageerror", (e) => console.error("[page]", e.message));
68-
69-
for (const t of targets) {
70-
const [w, h] = t.size;
71-
// `/play/<id>` is a production rewrite (vercel); in the vite dev server used
72-
// here it doesn't resolve, so use the SPA entry + hash which loads the doc.
73-
await page.goto(`${origin}/play.html#doc=${t.id}`, { waitUntil: "load" });
74-
await page.reload({ waitUntil: "load" });
75-
await page.waitForFunction(() => window.__posecodeViewer?.duration > 0, null, {
76-
timeout: 60000,
77-
});
78-
await page.waitForFunction(() => window.__posecodeViewer.characterActive === true, null, {
79-
timeout: 60000,
80-
});
81-
await page.waitForTimeout(1600); // let the auto-framing camera settle
82-
83-
const duration = await page.evaluate(() => {
84-
const v = window.__posecodeViewer;
85-
v.pause();
86-
return v.duration;
87-
});
88-
const frameCount = Math.round(duration * t.fps);
89-
const delayMs = Math.round(1000 / t.fps);
90-
91-
const gif = GIFEncoder();
92-
let palette = null;
93-
for (let i = 0; i < frameCount; i++) {
94-
const time = (i / t.fps) % duration;
95-
const b64 = await page.evaluate(
96-
({ time, w, h }) => {
97-
const v = window.__posecodeViewer;
98-
v.seek(time);
99-
v.captureFrame();
100-
const src = document.getElementById("canvas");
101-
// Cover-crop the viewer canvas into the target frame.
102-
const scale = Math.max(w / src.width, h / src.height);
103-
const sw = w / scale;
104-
const sh = h / scale;
105-
const sx = (src.width - sw) / 2;
106-
const sy = (src.height - sh) / 2;
107-
const out = new OffscreenCanvas(w, h);
108-
const ctx = out.getContext("2d");
109-
ctx.drawImage(src, sx, sy, sw, sh, 0, 0, w, h);
110-
const data = ctx.getImageData(0, 0, w, h).data;
111-
let bin = "";
112-
for (let j = 0; j < data.length; j += 8192) {
113-
bin += String.fromCharCode.apply(null, data.subarray(j, j + 8192));
114-
}
115-
return btoa(bin);
116-
},
117-
{ time, w, h },
118-
);
119-
const rgba = Uint8Array.from(Buffer.from(b64, "base64"));
120-
// One palette for the whole clip keeps the loop flicker-free (the scene
121-
// lighting is static; only the figure moves).
122-
if (!palette) palette = quantize(rgba, 256);
123-
const indexed = applyPalette(rgba, palette);
124-
gif.writeFrame(indexed, w, h, { palette: i === 0 ? palette : undefined, delay: delayMs });
125-
}
126-
gif.finish();
127-
128-
const outName = `${t.out ?? t.id}.gif`;
129-
const outPath = resolve(repoRoot, "docs/media", outName);
130-
await writeFile(outPath, gif.bytes());
131-
console.log(`wrote docs/media/${outName} (${frameCount} frames @ ${t.fps}fps, ${w}x${h})`);
132-
}
133-
134-
await browser.close();
135-
await server.close();
35+
await captureGifs(targets, { repoRoot, outDir: "docs/media" });

0 commit comments

Comments
 (0)