Browser-based App Store screenshot composer with a 3D iPhone stage.
One batch manifest produced all of these — the App Store set for Inkput — Mindful Journaling: stacked editorial titles, localized zh-Hant captions from the same raw capture, and — via the optional AI layer — generated paper backgrounds and sheets popping out of the device frame.
npm install
npm run devOpen http://127.0.0.1:5173/screenshot-stage.html.
The page exposes window.ScreenshotStage after load:
await window.ScreenshotStage.setState({
title: "Made by an LLM",
subtitle: "Deterministic scene control",
bgMode: "solid",
solid: "#123456",
textColor: "#f4f6fb",
rotation: { x: -0.14, y: 0.17 },
phoneOffset: { x: 0, y: 0 },
phoneScale: 1
});
const state = window.ScreenshotStage.getState();state.displayRect is computed from the iPhone model screen aspect and the
1290 × 2796 screenshot aspect, so screenshots fit the usable display area
without manual inset tuning.
Use examples/scene.json as the stable JSON shape for LLM-generated presets.
Other agents can render a final App Store screenshot from any local PNG:
npm run render -- \
--screenshot /Users/victor/Documents/Workspace/Projects/Inkput/screenshots/iphone-04b-capsule-seal.png \
--output /Users/victor/Documents/Workspace/Projects/Inkput/screenshots/app-store-shot-1290x2796.png \
--state examples/scene.jsonFor a full set, prefer batch mode — one Vite + one Chromium for all renders (~7s for 2 items vs ~20s+ each when spawning per render):
npm run render -- --batch manifest.json{
"base": { "bgMode": "gradient", "gradA": "#FDEEE7", "gradB": "#F6D3C2",
"textColor": "#2B2B2B", "titleSize": 82, "subtitleSize": 40 },
"items": [
{ "screenshot": "raw/editor.png", "output": "framed/01-editor.png",
"state": { "title": "Turn moments into cards", "subtitle": "Style any quote" } },
{ "screenshot": "raw/capsule.png", "output": "framed/{locale}/02-capsule.png",
"locales": {
"en-US": { "title": "Seal it in a capsule", "subtitle": "Open it later" },
"zh-Hant": { "title": "封存進時光膠囊", "subtitle": "留待日後開啟" }
} }
]
}base is shared state, each item's state shallow-merges over it; paths
resolve relative to the manifest file. An item with locales renders once
per locale from the same raw screenshot — its output must contain a
{locale} placeholder, and each locale's state merges last so per-locale
captions win. Add --gallery out/index.html to also write an HTML contact
sheet of the whole set, grouped by locale, for quick visual review.
CLI contract (agent-facing): every rendered output prints one path (WxH)
line on stdout; warnings and [stage] browser errors go to stderr. A failed
batch item does not stop the remaining items — each failure is reported as
FAILED item N/M (...): reason and the exit code is 1 when anything failed.
Unknown CLI options and unrecognized scene keys are called out explicitly
(scene typos warn but don't fail). --help documents the full scene schema.
The command starts the local Vite stage when --url is omitted. If an agent
already has the stage open, pass --url http://127.0.0.1:5173/screenshot-stage.html.
Scene JSON can control:
-
rotation.x/rotation.y: iPhone model angle in radians ({x: 0, y: 0}= flat-on). -
phoneOffset.x/phoneOffset.y: phone pan in final 1290 × 2796 pixels. -
phoneWidthRatio: fraction of stage width the phone body occupies (default0.72, clamped0.4...0.9). This is the primary size knob. -
phoneScale: multiplier on top ofphoneWidthRatio, clamped0.5...1.5. -
captionTop: caption block top as a fraction of canvas height (default0.11, clamped0.02...0.35); smaller starts the title higher. -
theme: palette preset macro —warm-light(peach gradient, brown text, coral accent) orcream(cream gradient, brown text, green accent); both proven in shipped listings. Explicit keys in the same state override it. -
bgMode,solid,gradA,gradB,gradAngle: background;bgImage(data URL) withbgMode: "image". -
title,subtitle,titleSize,subtitleSize,textColor,align: text.subtitleColorstyles the subtitle as an accent (default:textColor).\nin a caption forces a line break (stacked editorial titles); CJK text wraps naturally without injected spaces. -
allow2DFallback: defaultfalse— the export always uses the 3D model and fails loudly when it isn't ready. Settrueto allow the flat 2D-drawn phone as a fallback. -
frame:"iphone-3d"(default) or"none"— plain rounded screenshot with a soft shadow, no device frame or 3D model (works for iPad captures). -
canvas: App Store slot preset —iphone-6.9(1290 × 2796, default; also the 6.7" slot),iphone-6.9-alt(1320 × 2868),ipad-13(2064 × 2752, framed with a 3D iPad Pro 13" model;frame: "none"also works). Also available as the CLI flag--canvas <preset>, which overrides scene state. Device models load on demand, so one batch can mix iPhone and iPad items.
Sizing note: the export re-renders the 3D model at the target pixel size,
so large phoneWidthRatio/phoneScale values stay crisp — only the live
preview uses a CSS transform. Input PNGs can be any portrait capture
(e.g. 1206 × 2622 iPhone, 2048 × 2732 iPad); output size follows the canvas
preset. titleSize/subtitleSize are defined at a 1290-wide reference and
scale with canvas width, so the same state renders proportionally everywhere.
Set craft: capture raw screenshots with a clean status bar
(xcrun simctl status_bar booted override --time "9:41", and wait ~40s
after launch so the simulator's Dynamic Island pill clears), and vary
rotation per item — straight, tilted left, tilted right — so the set
reads as a designed row rather than one repeated pose.
Fonts: captions use two typefaces — the title renders in EB Garamond
(loaded from Google Fonts, so rendering needs network access; offline it
silently falls back to Georgia) and the subtitle uses the system sans-serif
stack (-apple-system, …). Fonts are not yet configurable via scene state.
Known gaps and next improvements live in docs/agent-backlog.md.
In the browser preview, drag the phone to rotate it. Hold Space while dragging to pan the phone placement.
Everything above is deterministic and produces upload-ready sets on its own. This layer is an optional extra touch for users who have a Gemini API key and want hand-crafted-looking panels: it enriches the flat background (paper texture, light, tasteful garnish) and makes the device pop — while keeping captions, layout, and exact dimensions:
export GOOGLE_API_KEY=... # or GEMINI_API_KEY; https://aistudio.google.com/apikey
node scripts/enhance-screenshot.mjs --input framed/en-US # whole folder
node scripts/enhance-screenshot.mjs --input shot.png --output shot-final.pngDefaults: gemini-3-pro-image-preview at 2K (use --model flash for the
cheaper gemini-2.5-flash-image); custom art direction via --prompt /
--prompt-file. Non-PNG or slightly off-size model output is converted and
cover-cropped/resized back to the source's exact slot size; far-off-aspect
output is rejected and retried rather than cropped into the content.
Enhancement is generative — always review the output before uploading. See
node scripts/enhance-screenshot.mjs --help for the full contract.
Two focused modes compose into a text-safe pipeline:
# 1. Generate ONLY a background in the panel's style…
node scripts/enhance-screenshot.mjs --mode background --input framed/en-US/03.png --output backgrounds/03-bg.png
# 2. …re-render deterministically on it (captions never touch the model):
# manifest state: { "bgMode": "image", "bgImage": "backgrounds/03-bg.png" }
# 3. Optionally make the presented sheet break out of the device frame:
node scripts/enhance-screenshot.mjs --mode popout --input framed/en-US/03.png --output framed/en-US/03-popout.pngbgImage accepts a file path (resolved relative to the manifest) as well as
a data URL, so generated backgrounds drop straight into batch manifests.
Popout notes (learned the hard way — the default prompt encodes all of it):
the sheet is enlarged "like zooming into a photograph" so its text scales
up with the card and internal padding stays true to the app's real sheet.
Generation is a dice roll per run — re-run on a bad roll, and always
inspect edges at full resolution. Known tradeoff: the popped sheet usually
comes out opaque, losing the translucent "liquid glass" material of the
real iOS sheet; if that matters, re-roll with a custom --prompt asking to
keep the frosted translucency.
Recommended per-panel use: pop out only where one UI element is the panel's pitch; keep hero-screen panels flat.
npm test- iPhone 17 Pro: "Iphone 17 pro"
by Ibrahim.Bhl, licensed under
CC Attribution. Committed
as the raw download (
iphone-17-pro/source/iphone 17_4.glb) and a derivative (iphone-17-pro-clean.glb, viascripts/create-clean-iphone-model.mjs) with a few internal meshes removed. - iPad Pro 13" M4: "Ipad pro 13in silver m4"
by polyman Studio, licensed under
CC Attribution. The
committed files are the raw download (
ipad-pro-13/source/ipad-pro-13-raw.glb) and a derivative (ipad-pro-13-clean.glb, viascripts/create-clean-ipad-model.mjs) that extracts the standalone iPad, straightens it, and renames its display material.
