diff --git a/api/derive.ts b/api/derive.ts index 03653b7..03343ff 100644 --- a/api/derive.ts +++ b/api/derive.ts @@ -62,8 +62,27 @@ import { mediaPath } from './media'; */ export const LADDER = [320, 480, 640, 960, 1280, 1920, 2560] as const; -/** WebP quality. 82 is the usual knee: visually clean on photographs, well off the top of the curve. */ +/** + * WebP quality, by rung. + * + * 82 is the usual knee for a photograph being LOOKED at — the lightbox, a detail page, anything at or + * near its real size. It was applied to every rung, which meant a thumbnail 173 CSS pixels wide in a + * gallery grid carried the same fidelity as the copy someone opens full screen. + * + * Artefacts scale with display size. At thumbnail size 74 is indistinguishable from 82 and 15% smaller, + * measured on the worst image in this gallery: 108,388 bytes to 92,568. Nine of those on a page is + * most of what an audit calls "increasing the compression factor". + * + * 640 is the line because it is the widest rung a phone ever picks for a grid slot. Anything above it + * is being chosen because something is showing the picture large. + */ +const WEBP_QUALITY_SMALL = 74; const WEBP_QUALITY = 82; +const THUMB_MAX_WIDTH = 640; + +/** The quality a rung is worth. `null` is `full.webp`, which is the one people open. */ +const qualityFor = (rung: number | null): number => + rung !== null && rung <= THUMB_MAX_WIDTH ? WEBP_QUALITY_SMALL : WEBP_QUALITY; /** * The placeholder: a picture small enough to travel INSIDE the manifest. @@ -454,7 +473,7 @@ async function encodeWebp( { rotate: {} }, ...(rung === null ? [] : [{ resize: { width: rung, fit: 'inside', withoutEnlargement: true } }]), ], - output: { format: 'webp', options: { quality: WEBP_QUALITY } }, + output: { format: 'webp', options: { quality: qualityFor(rung) } }, }); const width = rung ?? sourceWidth;