fix(studio): capture storyboard tiles at review density (#3371)

* fix(studio): capture storyboard tiles at source resolution

* fix(studio): bound storyboard tile captures
This commit is contained in:
James Russo
2026-08-20 15:46:12 -07:00
committed by GitHub
parent 2be5a03b80
commit b4d5abd7b2
6 changed files with 106 additions and 28 deletions
@@ -37,24 +37,21 @@ function renderPoster(surface?: "tile" | "hero"): HTMLImageElement {
}
describe("FramePoster", () => {
// Regression: the hero and the tile shared one bounded poster, so the frame
// detail view — the sketch pass's only picture — showed a 240x135 capture
// upscaled past 7x on a retina display, and body copy was unreadable.
it("captures the focus hero at the composition's own dimensions", () => {
const url = new URL(renderPoster("hero").src);
// Regression: the contact sheet stretched a 240x135 capture across a wide,
// high-density card, making body copy, thin lines, and sprite details blurry.
it.each([
[undefined, "storyboard"],
["tile", "storyboard"],
["hero", "source"],
] as const)("captures the %s surface at %s density", (surface, output) => {
const url = new URL(renderPoster(surface).src);
expect(url.searchParams.get("output")).toBe("source");
expect(url.searchParams.get("output")).toBe(output);
expect(url.pathname).toBe("/api/projects/demo/thumbnail/frames/01-hero.html");
});
it("leaves the contact-sheet tile on the route's bounded preview capture", () => {
const url = new URL(renderPoster("tile").src);
expect(url.searchParams.has("output")).toBe(false);
});
it("defaults to the tile surface", () => {
expect(new URL(renderPoster().src).search).toBe(new URL(renderPoster("tile").src).search);
expect(renderPoster().className).toBe(renderPoster("tile").className);
});
it("letterboxes only the hero, so a tile still fills its cell", () => {
@@ -11,10 +11,8 @@ export interface FramePosterProps {
/**
* Where this poster is rendered. A contact-sheet tile is ~300px wide and there
* are many of them; the focus hero is up to 900px wide and there is exactly
* one. That single difference decides both the crop and how much resolution
* the server has to capture, so it is one prop rather than two that can
* disagree: `tile` fills+crops at the route's bounded preview density, `hero`
* letterboxes at the composition's own dimensions.
* one. Tiles use a bounded high-density capture; the hero uses source density.
* The surface also decides whether the result fills or letterboxes its cell.
*/
surface?: "tile" | "hero";
/**
@@ -57,11 +55,10 @@ export function FramePoster({
seekTime: seconds,
duration: 0,
origin: window.location.origin,
// The hero is the sketch pass's only picture (references/review-loop.md), and
// it is shown large. Bounded to the preview cap it arrives at 240x135 and
// upscales past 7x on a retina display, which is unreadable for exactly the
// body copy and labels this pass exists to confirm.
...(surface === "hero" ? { output: "source" as const } : {}),
// The normal 240x135 preview is unreadable in the contact sheet, while source
// density is unbounded across all tiles. Give tiles a capped review density
// and reserve true source output for the single focus hero.
output: surface === "hero" ? "source" : "storyboard",
});
if (posterVersion) {
const withVersion = new URL(url, window.location.origin);