diff --git a/docs/src/components/home/Chapter.astro b/docs/src/components/home/Chapter.astro index eaa1bb5f..878c9708 100644 --- a/docs/src/components/home/Chapter.astro +++ b/docs/src/components/home/Chapter.astro @@ -33,18 +33,21 @@ if (aside && presentation !== "tabs") { style={`--chapter: var(--color-chapter-${color})`} > + title (src/styles/animations.css). Beside the intro, the title sizes + to the intro's height (the script below). -->

{name}

-

+

{title}

@@ -93,3 +96,53 @@ if (aside && presentation !== "tabs") { ) } + + diff --git a/docs/src/lib/utils/fitFontSize.ts b/docs/src/lib/utils/fitFontSize.ts new file mode 100644 index 00000000..2cd396f3 --- /dev/null +++ b/docs/src/lib/utils/fitFontSize.ts @@ -0,0 +1,34 @@ +// Sizes a homepage chapter title to the height of the intro beside it +// (src/components/home/Chapter.astro). Kept free of the DOM so it can be +// unit-tested with `node --test`. + +interface Bounds { + min: number; + max: number; + // How close the search gets before it stops, in px. + precision?: number; +} + +// The font size, within the bounds, whose height lands closest to `target`. +// `heightAt` lays the text out at a size and returns its height. Height grows +// with the size, jumping by a line wherever the text rewraps, so the closest +// fit sits either just below or just above the size where it first overshoots. +export function fitFontSize( + heightAt: (size: number) => number, + target: number, + { min, max, precision = 0.5 }: Bounds, +): number { + if (heightAt(max) <= target) return max; + if (heightAt(min) >= target) return min; + + let fits = min; + let overshoots = max; + while (overshoots - fits > precision) { + const size = (fits + overshoots) / 2; + if (heightAt(size) <= target) fits = size; + else overshoots = size; + } + const under = target - heightAt(fits); + const over = heightAt(overshoots) - target; + return under <= over ? fits : overshoots; +} diff --git a/docs/tests/fit-font-size.test.js b/docs/tests/fit-font-size.test.js new file mode 100644 index 00000000..b87115dc --- /dev/null +++ b/docs/tests/fit-font-size.test.js @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { fitFontSize } from "../src/lib/utils/fitFontSize.ts"; + +// A title that sets on one line up to `wrapsAt`, and on two lines above it. +const title = + (wrapsAt, leading = 1) => + (size) => + (size <= wrapsAt ? 1 : 2) * size * leading; + +const bounds = { min: 32, max: 64, precision: 0.25 }; + +test("shrinks the title until it matches the target height", () => { + const size = fitFontSize(title(40), 110, bounds); + assert.ok(Math.abs(size - 55) <= 0.25, `got ${size}`); +}); + +test("wraps onto another line when that lands closer to the target", () => { + // One line tops out at 50px tall; two lines at 51px are 102px tall. + const size = fitFontSize(title(50), 100, bounds); + assert.ok(size > 50 && size <= 50.25, `got ${size}`); +}); + +test("stays on fewer lines when rewrapping overshoots by more", () => { + // One line at 60px is 60px tall; two lines at 61px are 122px tall. + const size = fitFontSize(title(60), 70, bounds); + assert.ok(size >= 59.75 && size <= 60, `got ${size}`); +}); + +test("keeps within the bounds", () => { + assert.equal(fitFontSize(title(40), 400, bounds), 64); + assert.equal(fitFontSize(title(64), 10, bounds), 32); +});