diff --git a/src/__tests__/appAccessibility.test.ts b/src/__tests__/appAccessibility.test.ts
index 6eec27a..b954c12 100644
--- a/src/__tests__/appAccessibility.test.ts
+++ b/src/__tests__/appAccessibility.test.ts
@@ -388,3 +388,14 @@ describe("the sticky nav reads on both grounds", () => {
expect(block).toContain("--primary-ink: oklch(0.55 0.19 256.4)");
});
});
+
+describe("the announcement banner reads at full strength", () => {
+ it("does not dim its own link text", () => {
+ // Measured on production: text-white/85 on the banner resolved to #d9e9fa against
+ // rgb(1,109,221), which is 4.01:1, and Lighthouse failed the page's colour contrast
+ // for it. Full white on the same background measures 4.96:1.
+ const navbar = readFileSync("src/components/Navbar.tsx", "utf8");
+ const banner = navbar.slice(navbar.indexOf("bg-primary"), navbar.indexOf("bg-primary") + 1200);
+ expect(banner, "the banner dims its link text again").not.toMatch(/text-white\/\d+/);
+ });
+});
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 8896bd3..92716ed 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -70,7 +70,7 @@ export default function Navbar({ position = "sticky" }: { position?: "fixed" | "
className="group mx-auto flex max-w-[1360px] flex-col items-center justify-center gap-x-10 gap-y-1 px-4 py-2.5 text-center text-sm sm:flex-row"
>
Three finished sites, served as the exact bundles they export to.
-
+
See the examples
diff --git a/src/components/StylePreview.tsx b/src/components/StylePreview.tsx
index 19b6cdd..3bfbd63 100644
--- a/src/components/StylePreview.tsx
+++ b/src/components/StylePreview.tsx
@@ -33,8 +33,25 @@ type FrameOptions = Parameters[4];
const REST_CACHE = new Map();
const REST_CANDIDATES = [0, 0.15, 0.3, 0.45, 0.6, 0.75, 0.9];
+function restKey(opts: FrameOptions, maxProgress: number): string {
+ return `${opts.style}|${opts.color1}|${opts.color2}|${opts.color3}|${maxProgress}`;
+}
+
+export function knownRestingProgress(opts: FrameOptions, maxProgress: number): number | undefined {
+ return REST_CACHE.get(restKey(opts, maxProgress));
+}
+
+/**
+ * Seven draws and seven readbacks, for one number.
+ *
+ * Cheap on its own and ruinous in a group: the landing page mounts four of these, and
+ * running the probe inside the first paint put 28 synchronous getImageData calls in front
+ * of the page settling. Measured with Lighthouse, that page blocked for 480ms while every
+ * other page in the site blocked for none. It runs off the critical path now, so call
+ * knownRestingProgress first and paint something while this works.
+ */
function restingProgress(opts: FrameOptions, maxProgress: number): number {
- const key = `${opts.style}|${opts.color1}|${opts.color2}|${opts.color3}|${maxProgress}`;
+ const key = restKey(opts, maxProgress);
const cached = REST_CACHE.get(key);
if (cached !== undefined) return cached;
let best = 0;
@@ -62,6 +79,13 @@ function restingProgress(opts: FrameOptions, maxProgress: number): number {
return best;
}
+/** After the page has settled, or very soon, in a browser without requestIdleCallback. */
+function whenIdle(run: () => void): void {
+ const idle = (window as unknown as { requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number }).requestIdleCallback;
+ if (typeof idle === "function") idle(run, { timeout: 1200 });
+ else setTimeout(run, 200);
+}
+
/**
* Renders a live, looping animation of a 2D style directly to a canvas via
* requestAnimationFrame — no JPEG encoding, no IndexedDB. Cheap enough to mount
@@ -122,8 +146,20 @@ export default function StylePreview({
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
sizeRef.current = { w, h };
// Repaint immediately so a resize never leaves the canvas blank; a running
- // loop overwrites this on its next tick.
- drawFrame2D(ctx, w, h, restingProgress(optsRef.current, maxRef.current), optsRef.current);
+ // loop overwrites this on its next tick. The resting frame is whichever of the
+ // style's frames is brightest, and finding it costs seven readbacks, so the first
+ // paint uses the start of the range and the better frame arrives when the browser
+ // is idle.
+ const known = knownRestingProgress(optsRef.current, maxRef.current);
+ drawFrame2D(ctx, w, h, known ?? 0, optsRef.current);
+ if (known === undefined) {
+ whenIdle(() => {
+ const context = canvas.getContext("2d");
+ if (!context) return;
+ const { w: width, h: height } = sizeRef.current;
+ drawFrame2D(context, width, height, restingProgress(optsRef.current, maxRef.current), optsRef.current);
+ });
+ }
};
resize();