My App Store screenshots are rendered by the game itself
Every pixel in the store listing comes from the real engine running on CanvasKit, posed and composited in a browser tool. No mockups, and no drift.
Store screenshots are a genuinely annoying deliverable. You need a handful of images at exact pixel dimensions, they need to look designed rather than captured, and they have to be redone every time the game changes.
The usual workflow is to grab some captures off a device, drop them into a design tool, add a phone frame and some text, and export. It works once. Then you retune the trail colour and every screenshot is subtly wrong, and redoing them is a half-day of fiddly work you keep putting off.
Overbloom’s screenshots are generated by a tool that runs the real game engine in
a browser. Every game pixel comes from the same render.ts the device build
uses.
Posters, not photos
The first decision was to stop treating a screenshot as a picture of a phone.
At the size the store actually shows these, a phone frame wastes most of the canvas on bezel and the gameplay ends up too small to read. So the shots are built as posters with five fixed layers. The game appears twice: once as a posed capture inset into a rounded card, and once as real hero objects that escape the card and float over the background.
That second appearance is the reason the tool needed the real engine rather than exported PNGs. Those floating objects are the same orb, coins and power-ups the game draws, baked through their real draw functions.
Four stages
Pose. Put the engine into a play state and run the real simulation forward so the field fills with genuine spawns rather than hand-placed props. Then freeze it and drag things where you want them.
Freezing the sim while keeping the frame loop alive took a specific carve-out in the game loop. The world holds still, but the clock keeps bumping so cosmetic changes still redraw:
if (poseFrozen.value) {
// Screenshot Studio: sim frozen. The world holds exactly as posed; a drag on
// the field repositions the hero orb directly (no steering).
const pt = pointer.value;
if (pt.holding) {
// Decide what this drag owns ONCE, on the press, and keep it for the whole
// gesture: re-testing every frame would let a pickup dragged over the orb
// hand the touch across mid-drag.
if (poseGrab.value === STUDIO_GRAB_NONE) poseGrab.value = studioGrabAt(g, pt.x, pt.y);
// ...
}
} else {
update(g, pointer.value, dt, worldHSV.value);
}Dragging the orb needed one more fix. Only update() advances the trail ring
buffer, so moving the player while frozen left its tail stranded at the old
position. The pose path re-lays the tail each frame the orb moves.
Every pose control is a call into src/game/pose.ts, the same headless API the
in-app pose studio drives, so the browser tool and the on-device tool cannot
drift apart.
Objects. Bake each hero object through its real draw function onto a transparent surface at 2x with the glow baked in. The tool tracks which bakes are stale, because that is where inaccuracy would creep in first:
Re-snap all re-bakes after a renderer change; it is the whole drift story.
Compose. Place the card and the objects. The aids here are deliberately soft: a focal lock, a straddle guide for objects crossing the card edge, a warning when there are too many objects, and a 25% thumbnail strip so you can check legibility at the size the store browse view actually uses. No snapping, no grid, because these are compositions rather than layouts.
Export. html-to-image at native pixel size, then a sharp pass that
flattens, strips alpha, forces sRGB and asserts the exact dimensions. A mismatch
fails loudly instead of resampling, because a silently resampled screenshot is
the kind of thing you discover after upload.
Proving the risky assumptions first
The tool rests on things that could each have been false, so those got tested before any UI existed:
// Phase 0 gate spike. Four load-bearing assumptions that the rest of the tool is
// built on. Each one has a listed fallback in the plan, and the point of running
// them BEFORE any UI work is that picking a fallback later would mean rewriting
// the tool around it.Each gate returns a verdict plus images for an eyeball check, and they are driven headless by a script that asserts the verdicts and saves the PNGs.
This is the part I would repeat on any tool with an uncertain foundation. The question is not whether the plan is good. It is which assumption, if wrong, forces a rewrite, and how cheaply you can find out.
What it actually buys
Regenerating the whole set after a visual change takes minutes and produces something correct rather than something approximately correct.
A screenshot cannot show a state the game cannot reach, because the field is populated by running the real sim. No accidentally impossible arrangement of obstacles, which is a thing that happens when you place props by hand and it is exactly the kind of detail a player notices without being able to say why.
And experimenting is cheap enough to be worth doing. Trying eight variations of a composition is a coffee break instead of an afternoon, which means the listing gets iterated on rather than shipped once and left alone.
The listing this produces is here. The browser tooling it runs in is covered in running my React Native game engine in the browser.