Skip to content

Commit 1f43d05

Browse files
committed
docs: add launch static images (screenshots, logo lockup)
Phase 2 "static images" set, all rendered from the real playground at 2x: - docs/launch-media/play-{desktop,transport,mobile}.png — editor+viewer, transport close-up, and mobile viewport of /play (jumping-jacks hero frame) - docs/brand/posecode-logo{,-dark}.png — logo lockup (lime figure + Pose/code wordmark in Hanken Grotesk) on transparent and ink backgrounds Adds scripts/capture-screenshots.mjs and scripts/capture-logo.mjs, and extends scripts/lib/gif-capture.mjs with a shared bootPlayground/gotoDoc helper (the GIF core now reuses it, so there is one boot path).
1 parent 6a2f1ba commit 1f43d05

9 files changed

Lines changed: 204 additions & 25 deletions

File tree

docs/brand/posecode-logo-dark.png

15.9 KB
Loading

docs/brand/posecode-logo.png

17 KB
Loading

docs/launch-media/jab-cross.gif

-2.24 KB
Loading

docs/launch-media/play-desktop.png

427 KB
Loading

docs/launch-media/play-mobile.png

338 KB
Loading
6.48 KB
Loading

scripts/capture-logo.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Renders the Posecode logo lockup (lime figure glyph + "Posecode" wordmark) to
4+
* PNG on a transparent and a dark background, for profiles / Product Hunt / decks.
5+
* Uses the real brand font (Hanken Grotesk) via Google Fonts so the wordmark
6+
* matches the site exactly. Output → docs/brand/.
7+
*
8+
* posecode-logo.png transparent background
9+
* posecode-logo-dark.png ink (#0a0d12) background
10+
*
11+
* Usage: node scripts/capture-logo.mjs
12+
*/
13+
import { chromium } from "playwright-core";
14+
import { fileURLToPath } from "node:url";
15+
import { dirname, resolve } from "node:path";
16+
import { mkdir } from "node:fs/promises";
17+
import { chromiumPath } from "./lib/gif-capture.mjs";
18+
19+
const here = dirname(fileURLToPath(import.meta.url));
20+
const repoRoot = resolve(here, "..");
21+
const outDir = resolve(repoRoot, "docs/brand");
22+
await mkdir(outDir, { recursive: true });
23+
24+
const LIME = "#c6f24a";
25+
const INK = "#0a0d12";
26+
27+
// Figure glyph = the favicon paths, forced lime (no color-scheme dependence).
28+
const glyph = `
29+
<svg width="132" height="132" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
30+
<g fill="none" stroke="${LIME}" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
31+
<path d="M50 34V55"/>
32+
<path d="M50 35L34 26L23 15"/>
33+
<path d="M50 35L66 26L77 15"/>
34+
<path d="M50 54L36 72L28 88"/>
35+
<path d="M50 54L64 72L72 88"/>
36+
</g>
37+
<circle fill="${LIME}" cx="50" cy="20" r="10"/>
38+
</svg>`;
39+
40+
const html = (dark) => `<!doctype html><html><head><meta charset="utf-8">
41+
<link rel="preconnect" href="https://fonts.googleapis.com">
42+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
43+
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@800&display=swap" rel="stylesheet">
44+
<style>
45+
html,body{margin:0}
46+
#lockup{
47+
display:inline-flex;align-items:center;gap:34px;
48+
padding:56px 72px;
49+
${dark ? `background:${INK};` : "background:transparent;"}
50+
font-family:"Hanken Grotesk",system-ui,sans-serif;
51+
}
52+
#lockup .mark{display:flex}
53+
#lockup .word{font-weight:800;font-size:132px;line-height:1;letter-spacing:-0.02em}
54+
#lockup .word .p{color:#f4f7fb}
55+
#lockup .word .c{color:${LIME}}
56+
</style></head>
57+
<body><div id="lockup">
58+
<span class="mark">${glyph}</span>
59+
<span class="word"><span class="p">Pose</span><span class="c">code</span></span>
60+
</div></body></html>`;
61+
62+
const browser = await chromium.launch({ executablePath: chromiumPath() });
63+
const page = await browser.newPage({ deviceScaleFactor: 2 });
64+
try {
65+
for (const [name, dark] of [["posecode-logo", false], ["posecode-logo-dark", true]]) {
66+
await page.setContent(html(dark), { waitUntil: "networkidle" });
67+
await page.evaluate(() => document.fonts.ready);
68+
await page.waitForTimeout(200);
69+
const el = await page.$("#lockup");
70+
await el.screenshot({
71+
path: resolve(outDir, `${name}.png`),
72+
omitBackground: !dark,
73+
});
74+
console.log(`wrote docs/brand/${name}.png${dark ? " (ink bg)" : " (transparent)"}`);
75+
}
76+
} finally {
77+
await browser.close();
78+
}

scripts/capture-screenshots.mjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Launch-asset screenshots (Phase 2 "static images"), rendered from the real
4+
* playground at 2x device scale for crisp PNGs. Output → docs/launch-media/.
5+
*
6+
* play-desktop.png full editor + viewer (1440x900 @2x)
7+
* play-transport.png close-up of the transport bar
8+
* play-mobile.png mobile viewport (390x844 @3x)
9+
*
10+
* Usage: node scripts/capture-screenshots.mjs
11+
*/
12+
import { fileURLToPath } from "node:url";
13+
import { dirname, resolve } from "node:path";
14+
import { mkdir } from "node:fs/promises";
15+
import { bootPlayground, gotoDoc } from "./lib/gif-capture.mjs";
16+
17+
const here = dirname(fileURLToPath(import.meta.url));
18+
const repoRoot = resolve(here, "..");
19+
const outDir = resolve(repoRoot, "docs/launch-media");
20+
await mkdir(outDir, { recursive: true });
21+
22+
const HERO_DOC = "jumping-jacks";
23+
const HERO_SEEK = 0.5; // fraction of duration: arms overhead, legs wide
24+
25+
// --- Desktop: full editor + viewer, plus a transport close-up ---------------
26+
{
27+
const { page, origin, close } = await bootPlayground({
28+
repoRoot,
29+
viewport: { width: 1440, height: 900 },
30+
deviceScaleFactor: 2,
31+
});
32+
try {
33+
await gotoDoc(page, origin, HERO_DOC);
34+
await page.evaluate((f) => {
35+
const v = window.__posecodeViewer;
36+
v.pause();
37+
v.seek(v.duration * f);
38+
v.captureFrame?.();
39+
}, HERO_SEEK);
40+
await page.waitForTimeout(300);
41+
42+
await page.screenshot({ path: resolve(outDir, "play-desktop.png") });
43+
console.log("wrote docs/launch-media/play-desktop.png (1440x900 @2x)");
44+
45+
const transport = await page.$(".transport");
46+
if (transport) {
47+
await transport.screenshot({ path: resolve(outDir, "play-transport.png") });
48+
console.log("wrote docs/launch-media/play-transport.png");
49+
} else {
50+
console.warn("!! .transport not found — skipped transport close-up");
51+
}
52+
} finally {
53+
await close();
54+
}
55+
}
56+
57+
// --- Mobile: portrait viewport ----------------------------------------------
58+
{
59+
const { page, origin, close } = await bootPlayground({
60+
repoRoot,
61+
viewport: { width: 390, height: 844 },
62+
deviceScaleFactor: 3,
63+
});
64+
try {
65+
await gotoDoc(page, origin, HERO_DOC);
66+
await page.evaluate((f) => {
67+
const v = window.__posecodeViewer;
68+
v.pause();
69+
v.seek(v.duration * f);
70+
v.captureFrame?.();
71+
}, HERO_SEEK);
72+
await page.waitForTimeout(300);
73+
74+
await page.screenshot({ path: resolve(outDir, "play-mobile.png") });
75+
console.log("wrote docs/launch-media/play-mobile.png (390x844 @3x)");
76+
} finally {
77+
await close();
78+
}
79+
}

scripts/lib/gif-capture.mjs

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,21 @@ import { writeFile, mkdir } from "node:fs/promises";
1717
import { existsSync } from "node:fs";
1818
import { resolve } from "node:path";
1919

20-
function chromiumPath() {
20+
export function chromiumPath() {
2121
if (process.env.POSECODE_CHROMIUM) return process.env.POSECODE_CHROMIUM;
2222
const base = process.env.PLAYWRIGHT_BROWSERS_PATH;
2323
if (base && existsSync(`${base}/chromium`)) return `${base}/chromium`;
2424
return chromium.executablePath();
2525
}
2626

2727
/**
28-
* @param {Array<{id:string,out?:string,size:[number,number],fps:number}>} targets
29-
* @param {{repoRoot:string,outDir:string}} opts outDir is relative to repoRoot.
28+
* Boots the Vite playground + a headless Chromium page against it. Returns the
29+
* page, its origin, and a `close()` that tears both down. Shared by the GIF and
30+
* screenshot capture scripts so there is one boot path.
31+
*
32+
* @param {{repoRoot:string, viewport?:{width:number,height:number}, deviceScaleFactor?:number}} opts
3033
*/
31-
export async function captureGifs(targets, { repoRoot, outDir }) {
32-
if (targets.length === 0) throw new Error("no capture targets");
33-
34-
const outAbs = resolve(repoRoot, outDir);
35-
await mkdir(outAbs, { recursive: true });
36-
34+
export async function bootPlayground({ repoRoot, viewport, deviceScaleFactor }) {
3735
const server = await createServer({
3836
configFile: resolve(repoRoot, "playground/vite.config.ts"),
3937
server: { port: 0, host: "127.0.0.1" },
@@ -45,27 +43,52 @@ export async function captureGifs(targets, { repoRoot, outDir }) {
4543
const origin = `http://127.0.0.1:${port}`;
4644

4745
const browser = await chromium.launch({ executablePath: chromiumPath() });
48-
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
46+
const page = await browser.newPage({
47+
viewport: viewport ?? { width: 1600, height: 1000 },
48+
deviceScaleFactor: deviceScaleFactor ?? 1,
49+
});
4950
page.on("pageerror", (e) => console.error("[page]", e.message));
5051

52+
const close = async () => {
53+
await browser.close();
54+
await server.close();
55+
};
56+
return { page, origin, close };
57+
}
58+
59+
/** Wait for the viewer + Xbot character to be ready for a given doc. */
60+
export async function gotoDoc(page, origin, id) {
61+
await page.goto(`${origin}/play.html#doc=${id}`, { waitUntil: "load" });
62+
await page.reload({ waitUntil: "load" });
63+
await page.waitForFunction(() => window.__posecodeViewer?.duration > 0, null, {
64+
timeout: 60000,
65+
});
66+
await page.waitForFunction(
67+
() => window.__posecodeViewer.characterActive === true,
68+
null,
69+
{ timeout: 60000 },
70+
);
71+
await page.waitForTimeout(1600); // let the auto-framing camera settle
72+
}
73+
74+
/**
75+
* @param {Array<{id:string,out?:string,size:[number,number],fps:number}>} targets
76+
* @param {{repoRoot:string,outDir:string}} opts outDir is relative to repoRoot.
77+
*/
78+
export async function captureGifs(targets, { repoRoot, outDir }) {
79+
if (targets.length === 0) throw new Error("no capture targets");
80+
81+
const outAbs = resolve(repoRoot, outDir);
82+
await mkdir(outAbs, { recursive: true });
83+
84+
const { page, origin, close } = await bootPlayground({ repoRoot });
85+
5186
const written = [];
5287
try {
5388
for (const t of targets) {
5489
const [w, h] = t.size;
5590
const anchorY = t.anchorY ?? 0.5; // vertical crop bias (0 top, 1 bottom)
56-
// `/play/<id>` is a production rewrite (vercel); the vite dev server used
57-
// here doesn't resolve it, so use the SPA entry + hash which loads the doc.
58-
await page.goto(`${origin}/play.html#doc=${t.id}`, { waitUntil: "load" });
59-
await page.reload({ waitUntil: "load" });
60-
await page.waitForFunction(() => window.__posecodeViewer?.duration > 0, null, {
61-
timeout: 60000,
62-
});
63-
await page.waitForFunction(
64-
() => window.__posecodeViewer.characterActive === true,
65-
null,
66-
{ timeout: 60000 },
67-
);
68-
await page.waitForTimeout(1600); // let the auto-framing camera settle
91+
await gotoDoc(page, origin, t.id);
6992

7093
const duration = await page.evaluate(() => {
7194
const v = window.__posecodeViewer;
@@ -125,8 +148,7 @@ export async function captureGifs(targets, { repoRoot, outDir }) {
125148
written.push(outPath);
126149
}
127150
} finally {
128-
await browser.close();
129-
await server.close();
151+
await close();
130152
}
131153
return written;
132154
}

0 commit comments

Comments
 (0)