Skip to content

Redesign: page background and topic map, drawn from prepared images - #38

Merged
meaningfool merged 3 commits into
redesignfrom
redesign-30-background-map
Nov 3, 2026
Merged

meaningfool merged 3 commits into
redesignfrom
redesign-30-background-map

Conversation

@meaningfool

@meaningfool meaningfool commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Implements #30 (part of #35). It builds on the foundations from #36 and gives #31 (home) and #33 (About) the intro block and the map to fill in.

What changes

  • No shader library, no WebGL. scripts/make-background.py (Python, with numpy and Pillow) does the prototype's heavy work ahead of time. It follows the prototype's code and the source of Paper Shaders' Halftone Dots shader (0.0.81), and writes three images to src/assets/background/, 91 KB in all. The images are committed with the script that makes them, so bun run build and CI don't need Python. The script gives the same files on every run. Rerun it after changing its settings or src/data/topic-map.json.
    • paper.png (30 KB): a 140px tile of the paper, at 2x. It is the shader's output for a white image (hex grid, gooey dots, dot colour 87%, grain 61%, 10px spacing), computed pixel by pixel and laid at 6% on the white page. The dots repeat every 14px. The grain is the shader's value noise made to wrap around, so the tile has no seam.
    • corner.png (41 KB): the size of every dot of the Ripple pattern (7px, 14°, reach 142%, 3 rings, 60%, twist 165%), after the prototype's blur and tone curve. It is stored on a polar grid around the top-right corner, in units of the reach, so it fits any window.
    • map.png (21 KB): the 420 × 300 grey image the prototype gave the shader for "Best of three". It is made from the prototype's patches, which are now in src/data/topic-map.json with the labels and dot positions.
  • The browser only lays out the dots and draws them once, on a canvas. That takes 3 KB of script for the background and 3 KB for the map. While the window is being resized, the browser stretches what is already drawn. 200ms after resizing stops, it draws again, but only if the size changed by more than a tenth or the pixel density changed.
  • Background (src/components/Background.astro): the paper and the corner pattern at 6%, fixed. It is behind every page built with Layout, the 404 page, and the not-found view of /documents/. Only the pattern is masked, so the paper keeps going:
    • around the map, clear inside the ellipse that fits its box, back to full strength at 1.55 times that size, following the map as the page scrolls;
    • on articles, a band as wide as the text column, fading out between 16px and 112px on each side.
  • Scripts are written to their own files, never inlined in the HTML (astro.config.mjs), because the Content-Security-Policy only allows the site's own scripts. No other output changes.
  • redesign is merged in (the share images and favicon, Redesign: share images and favicon #37). Layout keeps their share prop and adds textColumn and noindex next to it.

For #31, #32 and #33

Intro (#31, #33)

The prototype's .hero. Two columns, text at 500px and the map from 480px to 636px wide. The map spills 84px above and below without making the intro taller. Below 1120px the map goes under the text, up to 560px wide. The columns are top-aligned, so the label, the title and the map are at the same place on Home and About whatever the text.

<Intro label="Josselin Perrus · Product manager · Paris">
  <Fragment slot="title">I build products<br />with AI agents<span>.</span></Fragment>
  I'm a product manager building AI products with AI. My projects are an opportunity to test ideas and learn in public.
  <div slot="after" class="cta-row">…</div>
</Intro>
  • label: the small line above the title.
  • title slot: inside the <h1>. A <span> in it is in the accent colour, for the full stop.
  • Default slot: the text, in muted grey, up to 52ch.
  • after slot: anything under the text, 18px below it.
  • links and mapLabel: passed to the map (see below).

TopicMap: the orange dots (#31, #33)

  • Each dot carries its topic in data-topic: customer-success, product or ai-agents.

  • Clicking a dot sends a topicmap:pick event from the <topic-map> element. The event bubbles, event.detail.topic is the dot's topic, and it can be cancelled: preventDefault() stops a link dot from following its link.

  • Setting data-picked="<topic>" on <topic-map> marks that dot as picked and dims the other two to 40%, as in the prototype. Removing it clears the pick. Button dots get aria-pressed to match.

  • Without links, the dots are buttons. That fits the home (Redesign: home page #31):

    document.addEventListener('topicmap:pick', (event) => {
      const map = event.target;
      const topic = map.dataset.picked === event.detail.topic ? null : event.detail.topic;
      if (topic) map.dataset.picked = topic;
      else delete map.dataset.picked;
      // …filter Writing by topic
    });
  • With links, the dots are links. That fits About (Redesign: About page #33): <Intro label="About" links={{ 'customer-success': '#customer-success', product: '#product', 'ai-agents': '#ai-agents' }}>.

  • mapLabel names the map for screen readers. The default is the prototype's "Map of the writing: Customer success, Product and AI agents".

Home to About without a redraw (#33): the map is at the same place on both pages. A normal page load draws it again, which takes about 10ms once its image is cached. To keep it from one page to the next, #33 can use Astro's <ClientRouter /> with transition:persist on <topic-map>, and on <page-background> too. Both are custom elements that keep their drawing and their listeners when they are moved: moving them in the page redraws nothing. A kept element keeps the markup of the page it was first drawn on: Home's map has buttons and About's has links, so a map kept between the two is best handled through topicmap:pick on both pages, with its data-picked cleared on arrival. Keep <page-background> only between Home and About: an article's background has its own mask (data-mask="column").

Article band (#32)

Layout has a textColumn prop. It is on by default when type="article", which article pages and activity logs already pass, so #32 has nothing to do. The band is computed in CSS for a column of min(720px, frame width − 48px) centred in the window. If the column changes, change --edge in Background.astro.

Temporary, for #31 to remove

To show the map on the preview, /map-preview/ (src/pages/map-preview.astro) shows the intro with the prototype's home text and a note. Clicking a dot there sets data-picked. The page is linked from nowhere, has noindex, and is left out of the sitemap by one line in astro.config.mjs. Both are marked TEMPORARY (#30), and #31 deletes them.

Choices to check

  • /documents/: the not-found view gets the background, so it still renders pixel for pixel like /404.html. The portfolio view keeps its plain page.
  • Pages not rebuilt yet: articles already get the band, sized for Redesign: article page #32's 720px column, while the legacy column is still about 813px. Home and About show the whole pattern until Redesign: home page #31 and Redesign: About page #33 put the map there.
  • Paper grain: the shader scales its grain with the window's width. The tile fixes it at the size it has on a 1440px window, 1.31px.
  • Map grain: the shader's grain thins the map's dots in specks smaller than a pixel. Here it is averaged: the dots are 4.3% lighter.
  • Map width: below 1288px, the map is 16px wider than in the prototype, because the gutter is 24px (Redesign: foundations (fonts, colours, header, footer, 404) #29).
  • Resolution: the map is drawn at 2x to 3x whatever the screen, as the shader did (a 1x canvas loses some of the ink of dots smaller than a pixel); the pattern at up to 2x, as in the prototype.
  • Redraw: each page load draws the pattern, about 30ms of script at 1440 × 900 in headless Chromium, plus the fill. The prototype's mask update on scroll is kept: it only runs while the map is on screen.

Validation

Head commit f72416c43ab827163685f2461981a01759782f6b, on base 3df7e3843cdd06cf5d6c73d7b32e6443c3007262 (origin/redesign, with #37 merged in). The branch was started at 4cf01a4. Node 24.22.1, Bun 1.4.4, content submodule at bf783ba (git submodule update --init --remote). Every check below was run again after the merge. The review's fix, a26fefa (the map's resolution), is described in its checkpoint comment.

  • bun install, then bun run build from an empty dist/: passes, 63 pages (62, plus /map-preview/).

  • bun run test:documents: 10 tests, 10 passed.

  • astro check (TypeScript 6, in a scratch copy of the merged tree): 0 errors, 0 warnings.

  • python3 scripts/make-background.py gives byte-identical images on every run.

  • No shader library. dist/ has no reference to jsdelivr, paper-design or googleapis. In headless Chromium, the preview, /, /about/, an article, a daily log, /404.html and /documents/ request nothing from another host and log no errors.

  • Comparison with the prototype (docs/redesign/prototype/index.html), with Playwright screenshots in Chromium. The prototype was given a local copy of @paper-design/shaders@0.0.81. Under an emulated 2x screen, headless Chromium reports device-pixel sizes in CSS pixels, so the shader was made to use its fallback size, as a real 2x screen would.

    • Layout at 1440px: the label, title, text, map box (636 × 454.3), the three labels and the three dots have the same boxes as in the prototype, 8px higher because the prototype keeps the body's 8px margin. The same holds at 1000px, where the map goes under the text, 560 × 400.
    • Map: at 2x the total ink is within 0.3% of the prototype's, with a mean difference of 0.25 grey levels over 16px blocks. Dot positions match. The review measured the map alone (labels, paper and pattern hidden): it was 8% lighter than the prototype's at 1x and 3.5% at 2x. Since a26fefa it is drawn at 2x or more on every screen and is 3.5% lighter at 1x, 1.5x, 2x and 3x, most of it in the lightest dots.
    • Corner pattern: the size of every dot was checked against the prototype's algorithm. The total ink is within 1.4% at 1440 × 900, 1024 × 768 and 2560 × 1440, and within 2.5% at 375 × 812, with a mean difference of 0.002 to 0.006 of the largest dot.
    • Paper: mean 252.6 against 252.6 and spread 1.6 against 1.5 grey levels, over the whole 2x screen.
    • Article band at 1440px: transparent from 344px to 1096px, back to full at 248px and 1192px, the same stops as the prototype.
    • 375px: the map goes under the text at 327 × 234 (the prototype has 311 × 222 with its 32px gutter), the hole follows it, and nothing scrolls sideways.
  • Resizing: a 30-step drag from 1380px to 1100px draws nothing while it lasts, then each canvas once. A 4% drag draws nothing. No task over 50ms runs during resizing.

  • Interaction: clicking a dot sends topicmap:pick with its topic. data-picked dims the other dots and sets aria-pressed. Cancelling the event stops a link dot. Dots can be reached with Tab and picked with Enter.

  • Failure: with corner.png and map.png blocked, there is no script error, and the next resize draws both.

  • Pages outside the issue: I built the base (3df7e38) and the head and compared them.

    • <main> is identical on all 62 pages.
    • Outside <main>, the only changes are the background's markup, styles and script.
    • The sitemap, rss.xml, _headers, robots.txt, the share images and the favicons are unchanged. _redirects gains /map-preview, and llms*.txt differ only in their build timestamp.
    • /documents/: the not-found view matches /404.html pixel for pixel at 1440px and 375px, and the portfolio view, forced on, matches the base build pixel for pixel.
    • At widths from 320px to 2560px, no page scrolls sideways, except the daily logs' existing 418px overflow at 375px, which is unchanged.
  • Review gate: 5 passes over the whole change. The first two fixed:

    • the /documents/ not-found view, which had lost its match with /404.html;
    • a 100lvh height the CSS minifier left without a fallback;
    • images failing to load, which used to stop all later drawing;
    • listeners left on elements removed from the page;
    • settings repeated in the script.

    The last three passes, the fifth one after the merge, found nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_013MdjUerTCBDeF5DG9puDd2

The paper texture, the corner pattern and the "Best of three" topic map now
look as in the prototype without a shader library or WebGL.
scripts/make-background.py computes them ahead of time, following the
prototype's recipe and Paper Shaders' halftone shader, and writes three small
images to src/assets/background/: a 140px paper tile, the corner pattern's
ink on a polar grid, and the map's ink. The browser only lays the dots out and
draws them once on a canvas, and on resize stretches what is drawn until
resizing stops.

- Background.astro, behind every page and the 404: the paper, the corner
  pattern at 6%, the hole around the map and, on articles, the band behind
  the centred 720px column.
- TopicMap.astro: the map, with its orange dots as buttons or links that send
  a topicmap:pick event, and a data-picked state.
- Intro.astro: the two-column intro of Home and About, with the map.
- map-preview.astro: a temporary page to review the intro and the map, until
  the home (#31) uses them.
- Scripts are kept out of the HTML, as the Content-Security-Policy only allows
  the site's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MdjUerTCBDeF5DG9puDd2
Brings in the share images and favicon (#37). Layout keeps their share prop
and adds textColumn and noindex next to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MdjUerTCBDeF5DG9puDd2
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Preview: https://pr-38-meaningfool.josselin-perrus.workers.dev

Built from a26fefa with the latest content. The link stays the same and shows the latest push. Search engines are asked not to index it.

On a 1x canvas the dots smaller than a pixel lose some of their ink, and
the map came out 8% lighter than the prototype's. The shader renders at
twice the CSS pixels or more, so draw the map the same way: it is now as
dark at 1x and 1.5x as at 2x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MdjUerTCBDeF5DG9puDd2

Copy link
Copy Markdown
Owner Author

Review checkpoint: accepted at a26fefa299f196a0a15fa0fc13d92f5feeb7238c

This was an independent review over 2 passes. The Deploy build check passed on a26fefa, and the preview now serves that build.

Fix pushed

  • a26fefa Draw the topic map at 2x or more, as the shader did. With the labels, paper and pattern hidden, the map came out 8% lighter than the prototype's on a 1x screen and 3.5% lighter at 2x. The cause was that a 1x canvas loses some of the ink of dots smaller than a pixel, while the shader always renders at 2x or more. The map is now drawn at 2x to 3x on every screen. It comes out 3.5% lighter at 1x, 1.5x, 2x and 3x, most of that in the lightest dots. I updated the PR description to match.

What I checked

  • Setup: Node 24.22.1, Bun 1.4.4, content at bf783ba. bun run build passes with 63 pages, and bun run test:documents passes 10 of 10.
  • Generated images: scripts/make-background.py, run on a scratch copy, gives byte-identical images. The images carry no metadata. The patches in topic-map.json are identical to the prototype's best. CI builds without Python.
  • No shader library and no WebGL: dist/ has no @paper-design, jsdelivr, webgl or googleapis. In Chromium, on 7 pages at 1x and 2x, the only canvas contexts are 2d and no request goes to another host.
  • Prototype comparison: the prototype ran with a local copy of @paper-design/shaders@0.0.81.
    • Intro at 1440px and 1000px: every box matches (label, title, text, a 636 × 454.3 map, area labels, dots), apart from the prototype's 8px body margin.
    • Intro at 1120px and 375px: it matches apart from the 24px gutter.
    • Same place on Home and About: with About's text, the label, title and map stay put at 1120px and wider.
    • Corner pattern alone, without its mask: within 1.2 to 1.9% of the prototype's ink at 1440×900, 1024×768, 2560×1440 and 375×812.
    • Paper: mean 252.4 against 252.5.
    • Mask hole: the same ellipse as the prototype's, and it follows the map on scroll.
    • Article band at 1440px: the same stops as the prototype (248, 344, 1096, 1192).
  • Resizing: a 30-step drag draws nothing, and no long tasks run during it. Each canvas is drawn once after the drag, and a 4% drag draws nothing.
  • Map interaction: topicmap:pick bubbles and can be cancelled. data-picked sets aria-pressed and dims the other dots to 0.4. Tab order and Enter work.
  • Pages outside the scope: I compared against the base build.
    • <main> is identical on 62 of 62 pages.
    • Outside <main>, only the background's markup, CSS and script change.
    • The sitemap and RSS are unchanged.
    • With the background hidden, full-page screenshots are pixel-identical to the base on 8 pages at 1440px and 375px.
    • /documents/ not-found matches /404.html pixel for pixel.
    • No horizontal scroll from 320 to 2560px.
  • Temporary /map-preview/: it has noindex, is left out of the sitemap, is linked from nowhere and is marked TEMPORARY (Redesign: page background and topic map, prepared at build time #30).
  • Combined with redesign at 572200e (Redesign: article page #39 merged): I did a throwaway local merge and did not push it.
    • The build passes.
    • On all 58 article and log pages at widths from 320 to 1440px, the band lines up with main.post: clear from 16px outside the column, full 112px out.
    • main and the content are pixel-identical to redesign with the background hidden, and nothing is covered or shifted.

Notes, not blocking

  • The map is still 3.5% lighter than the prototype's, mostly in the smallest dots: the shader's anti-aliasing differs there.
  • The paper grain is fixed at its 1440px size, so it is coarser than the prototype's on phones. The PR lists this as a choice.
  • For Redesign: About page #33: an element kept by transition:persist keeps its first page's markup. I added a line on this to the description: the map's buttons or links, and the article mask on <page-background>.

Generated by Claude Code

@meaningfool
meaningfool marked this pull request as ready for review November 3, 2026 19:41
@meaningfool
meaningfool merged commit 78b78fd into redesign Nov 3, 2026
1 check passed
@meaningfool
meaningfool deleted the redesign-30-background-map branch November 5, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants