← Posts

記事の図をブラウザで描いて PNG にする

このサイトの記事に、仕組みや構造を説明する図を貼れるようにしたい。 条件は二つある。 一つは、本文と同じ配色やフォントで、サイトのトーンに収まること。 もう一つは、一度きりの手作業で終わらせず、同じ手順で何度でも作れること。

方針はこうした。 図を HTML と CSS で書き、ブラウザに描いて、その要素を切り取って PNG にする。 トーンは一つの CSS にまとめて共有し、描いて撮る手順はスクリプトにする。 作り方の約束ごとは、あとで見返せるようスキルとして書き残す。

作る図は、大きく二つの役割に分かれる。 一つは、関係や流れを線でつなぐダイアグラムである。

ブランチの分岐とマージを描いたダイアグラム

もう一つは、文字列や数式そのものに色と区切りを添えて読み解かせる装飾である。

URL の各部を色分けしてラベルを引いた図

どちらも、ブラウザで描いて要素を切り取れば、サイトと同じトーンの PNG になる。 この記事は、その仕組みを作った順に並べたものである。

なぜブラウザで描くのか

ブラウザを選ぶ理由は、表現力が高いことである。 gridbox-shadow も、SVG の任意の図形も、そのまま効く。 要素をつなぐ矢印、コミットの分岐と合流を描くベジェ曲線、判定を表すひし形。 図に要る線や囲みは、SVG の <path> か CSS で素直に描ける。

外部のライブラリを読み込むこともできて、数式は KaTeX1 を CDN から読み込んで描いた。

残るのは、描いたものを画像にする手立てだけである。 ここは Playwright2 に任せる。 ブラウザでページを開き、目的の要素を切り取って PNG にする。

トーンを一つの CSS に固める

図を一枚ごとに一から組むと、色やフォントが少しずつずれる。 サイトと同じトーンにそろえるため、デザイントークンと共通部品を一つの CSS にまとめた。 図の HTML は、これを読み込むだけで見た目が決まる。

frame.css
:root {
--bg: #fbfbfa;
--text-primary: #37352f;
--border: #e9e9e7;
--accent: #2f6f4f; /* 控えめな差し色 */
}
.canvas { /* スクリーンショットの対象。ドットグリッド背景を持つ */ }
.box { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; }
.chip { /* 見出しの小さなラベル */ }

色は生の値を図に直接書かず、必ずこのトークンを通す。 こうしておくと、図が何枚に増えても、差し色や境界の色は一箇所で決まる。

要素を切り取って PNG にする

図は .canvas という要素で囲む。 背景のドットグリッドと余白はこの要素が持ち、スクリーンショットもこの要素だけを撮る。 ページ全体ではなく要素を撮るので、周りに余分な余白が入らない。

Playwright でページを開き、.canvas を切り取る。 文字がぼやけないよう、2 倍の解像度で撮る。

render.mjs
const browser = await chromium.launch();
const context = await browser.newContext({ deviceScaleFactor: 2 });
const page = await context.newPage();
await page.goto(pathToFileURL(inPath).href, { waitUntil: 'networkidle' });
const el = await page.$('.canvas');
await el.screenshot({ path: outPath });

あとは HTML を書き換えてスクリプトを走らせ、出た PNG を見て直す。 この繰り返しで図を詰める。

つまずいた三か所

素朴な HTML と CSS の範囲でも、撮って初めて気づくずれがいくつかあった。

見出しが図の下に潜る

SVG で描いた図では、上に置いたはずの見出しが図の下へ回り込んだ。 原因は、svg 要素の既定の表示が inline だったことである。 見出しも inline の並びなので、両者が同じ行に入り、背の高い SVG に押されて見出しが下端へ沈む。

svgblock にすると、見出しは自分の行に収まり、図はその下に来る。 共通の CSS の側で一度そろえておけば、図ごとに気にせずに済む。

frame.css
.canvas svg { display: block; }

SVG の下端でラベルが切れる

SVG は高さを固定して描く。 頂点の下にラベルを置いたとき、その位置が指定した高さを超えていると、はみ出た分が切れる。 三角形で CAP 定理を描いた図では、下辺に添えた二つのラベルがこれで欠けた。

ブラウザは中身に合わせて要素の高さを自動で広げるが、SVG の描画領域は指定した高さで打ち切られる。 下に置くものの分だけ height に余裕を持たせると収まる。

フォントが間に合わない

撮った画像で、日本語や数式が別の書体に化けることがあった。 シャッターを切る時点で、Web フォントの読み込みがまだ終わっていないためである。

撮る前に、フォントの準備が終わるのを待つ。 document.fonts.ready が解決してから撮ると、化けは出なくなった。 CDN から読み込む KaTeX のフォントも、この待ちに含まれる。

render.mjs
await page.evaluate(() => document.fonts.ready); // フォントが揃うまで待つ
await el.screenshot({ path: outPath });

図解にとどまらない

同じ仕組みは、線でつなぐ図だけのものではない。 .canvas の中に置けるものなら、何でも同じトーンで撮れる。

まず、関係や流れを描くダイアグラムは、線のつなぎ方を変えるだけで型が増える。 時間を追うシーケンス、一つの入口から枝分かれする扇形、条件で道が分かれる判定フロー、親子で階層を作る木。

二者間のやりとりを追うシーケンス図

ロードバランサから枝分かれする扇形の図

キャッシュの読み取りを分岐で描いた判定フロー

二分探索木の木構造

次に、文字列やデータ、数式そのものに手を入れる装飾も、同じ枠で作れる。 表は <table> をそのまま組み、コードは行に色を付け、端末の出力は擬似的に再現し、数式は KaTeX に描かせる。

パッケージマネージャを比べる表

コマンドと出力を並べた擬似ターミナル

キー操作を並べたチートシート

各項を色分けしたベイズの定理

どれも共通の CSS を読み込んだ HTML を、Playwright で切り取っただけである。

本文の図は成果物が PNG なので3、ブラウザで出せる表現をそのまま持ち込める。

図の HTML は、Claude Code に書かせている。 使う部品と、つまずいた三か所を、あらかじめスキルに書き残しておく。 すると、毎回それを踏まえた HTML が返るので、トーンを外さずに図を足せる。

Footnotes

  1. KaTeX は数式を HTML と CSS で描く JavaScript ライブラリ。CDN の CSS と JS を読み込めば使える。

  2. Playwright はブラウザを自動操作するライブラリ。ここでは Chromium でページを開き、要素のスクリーンショットを撮るのに使う。

  3. ページの OG 画像はこの方法ではなく、ビルド時に satori で生成している。別記事で扱った。