Pretext
目次
DOM を使わない文字組みで、ブラウザで動く作品を作ります。
skill の情報
| 提供元 | 追加で入れるもの — hermes skills install official/creative/pretext で導入します |
| パス | optional-skills/creative/pretext |
| バージョン | 1.0.0 |
| 作者 | Hermes Agent |
| ライセンス | MIT |
| 対応プラットフォーム | linux, macos, windows |
| タグ | creative-coding, typography, pretext, ascii-art, canvas, generative, text-layout, kinetic-typography |
| 関連 skill | p5js, claude-design, excalidraw, architecture-diagram |
参考: SKILL.md 全文
Pretext で作る作品
概要
@chenglou/pretext は、Cheng Lou 氏(React コア、ReasonML、Midjourney)が作った 15KB・依存ゼロの TypeScript ライブラリで、DOM を使わずに複数行のテキストを測って組むためのものです。やることは 1 つだけです。(text, font, width) を渡すと、改行位置、行ごとの幅、書記素ごとの座標、全体の高さを返します。すべて canvas での計測で行い、レイアウトの再計算は起きません。
裏方の仕組みに聞こえますが、そうではありません。速くて座標を扱えるので、これは作品づくりの部品になります。動き回るスプライトのまわりに段落を 60fps で回り込ませる、本物の単語で地形を組んだゲームを作る、文章で ASCII のロゴを描く、書記素ごとの正確な開始位置を使って文字を粒子に砕く、getBoundingClientRect を何度も呼ばずに複数行の UI をぴったりの幅に収める、といったことができます。
この skill は、Hermes がそれを使って人に見せたくなる作品を作るためにあります。X に投稿されるようなものです。みんなが作った作品は pretext.cool と chenglou.me/pretext にまとまっています。
使いどころ
次のような依頼があったときに使います。
- 「pretext のデモ」「pretext で面白いもの」「文字で〇〇を作る」
- 動く図形のまわりを流れるテキスト(ヒーロー領域、雑誌風の紙面、動きのある長文ページ)
- 等幅の点描ではなく、本物の単語や文章を使った ASCII アート風の表現
- 遊び場や障害物、ブロックが文字でできたゲーム(文字で作るテトリス、文章で作るブロック崩し)
- 文字ごとに物理を効かせた動く文字組み(砕く、散らす、群れさせる、流す)
- 文字を使った生成アート。とくに欧文以外の文字や、複数の文字体系が混ざるもの
- 複数行を「ぴったり包む」UI(テキストが収まる最小の幅)
- 描画する*前に*改行位置を知る必要があるもの
次の用途には使いません。
- CSS でレイアウトが済む静的な SVG / HTML のページ。CSS だけで書いてください
- リッチテキストエディタや、汎用のインライン整形エンジン(pretext はあえて用途を絞っています)
- 画像からテキストへの変換(
ascii-art/ascii-videoの skill を使ってください) - テキストが出てこない純粋な canvas の生成アート。
p5jsを使ってください
作品としての水準
これはブラウザで描く視覚作品です。pretext が返すのは数値で、実際に描くのはあなた自身です。
- 「hello world」で終わらせないでください。
hello-orb-flow.htmlのひな形は*出発点*です。仕上げる作品には必ず、意図のある色、動き、構図と、頼まれてはいないけれど喜ばれる視覚的な工夫を 1 つ加えてください。 - 暗い背景、暖かい中心、練った配色。 黒地に琥珀色(CRT やターミナル風)は王道ですが、炭色に冷たい白(雑誌風)や、彩度を落としたパステル(リソグラフ風)も合います。1 つ選んで、そこに徹してください。
- プロポーショナルフォントこそが要です。 pretext の持ち味は「等幅ではない」ところにあります。そこを活かしてください。Iowan Old Style、Inter、JetBrains Mono、Helvetica Neue、あるいは可変フォントを使います。既定のサンセリフのままにはしないでください。
- 本物の文章やソースコードを使い、ダミー文を使わないでください。 並べる文章には意味がほしいところです。短い宣言文、詩、実際のソースコード、拾ってきた文章、ライブラリ自身の README などを使い、
lorem ipsumは避けます。 - 開いた瞬間に完成して見えること。 読み込み中の表示も、真っ白な一瞬もなしにします。開いたその場で人に見せられる状態にしてください。
使うもの
デモ 1 つにつき、それだけで完結する HTML ファイル 1 枚です。ビルドは要りません。
| 層 | ツール | 役割 |
|---|---|---|
| 中心 | esm.sh の CDN から読む @chenglou/pretext |
テキストの計測と行の組み立て |
| 描画 | HTML5 Canvas 2D | 文字の描画、フレームごとの構成 |
| 文字の区切り | Intl.Segmenter(ブラウザ内蔵) |
絵文字・CJK・結合文字のための書記素分割 |
| 操作 | 素の DOM イベント | マウス・タッチ・ホイール。フレームワークなし |
<script type="module">
prepare, layout, // use-case 1: simple height
prepareWithSegments, layoutWithLines, // use-case 2a: fixed-width lines
layoutNextLineRange, materializeLineRange, // use-case 2b: streaming / variable width
measureLineStats, walkLineRanges, // stats without string allocation
} from "https://esm.sh/@chenglou/pretext@0.0.6";
</script>バージョンは固定してください。執筆時点では @0.0.6 です。デモの動きがおかしいときは npm で最新版を確認してください。
2 つの使い方
ほとんどの場合、次の 2 つの形のどちらかに収まります。両方とも覚えてください。
使い方 1 — 測って、描画は CSS / DOM に任せる
const prepared = prepare(text, "16px Inter");
const { height, lineCount } = layout(prepared, 320, 20);文字を描くのはブラウザのままです。pretext は、ある幅にしたときの箱の高さを、DOM を読まずに教えてくれるだけです。次のような場面で使います。
- 折り返すテキストが入る行を持つ、仮想スクロールの一覧
- カードの高さを正確に出したいタイル状の配置
- 「このラベルは収まるか」を開発中に確かめるとき
- 遠くから取ってきたテキストが届いたときの、表示のずれを防ぐため
font と letterSpacing は CSS とぴったり合わせてください。 canvas の ctx.font の書き方(たとえば "16px Inter"、"500 17px 'JetBrains Mono'")が実際の CSS と一致していないと、計測結果がずれていきます。
使い方 2 — 測って、描画も自分でやる
const prepared = prepareWithSegments(text, FONT);
const { lines } = layoutWithLines(prepared, 320, 26);
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26);
}作品づくりはこちらの側にあります。描画を自分で持つので、次のことができます。
- canvas、SVG、WebGL、その他どんな座標系にも描ける
- 文字ごとに変形をかけられる(回転、ゆらぎ、拡大縮小、不透明度)
- 行の情報(幅、書記素の位置)を図形の情報として使える
行ごとに幅が変わる流し込み(図形のまわりを回るテキスト、ドーナツ状の帯に沿うテキスト、長方形でない段組み)は次のようにします。
let cursor = { segmentIndex: 0, graphemeIndex: 0 };
let y = 0;
while (true) {
const lineWidth = widthAtY(y); // your function: how wide is the corridor at this y?
const range = layoutNextLineRange(prepared, cursor, lineWidth);
if (!range) break;
const line = materializeLineRange(prepared, range);
ctx.fillText(line.text, leftEdgeAtY(y), y);
cursor = range.end;
y += lineHeight;
}これがこのライブラリで一番大事な形です。X で広まった「ドラッグしたスプライトのまわりをテキストが流れる」デモは、これで作られています。
覚えておきたい補助関数
measureLineStats(prepared, maxWidth)→{ lineCount, maxLineWidth }。一番長い行の幅、つまり複数行をぴったり包むときの幅です。walkLineRanges(prepared, maxWidth, callback)。文字列を作らずに行をたどります。文字そのものが要らず、書記素の統計や物理計算だけをしたいときに使います。@chenglou/pretext/rich-inline。同じ仕組みを、フォントやチップ、メンションが混ざる段落向けにしたものです。サブパスから読み込みます。
作り方の型
みんなが作った作品(references/patterns.md を参照)は、いくつかの強い型に分かれます。1 つ選んで手を加えてください。頼まれない限り、新しい型を考え出す必要はありません。
| 型 | 中心となる API | 例 |
|---|---|---|
| 障害物のまわりを回り込ませる | layoutNextLineRange と、行ごとに幅を返す関数 |
ドラッグしたカーソルのスプライトを避ける、雑誌風の段落 |
| 文字を地形にしたゲーム | layoutWithLines と、行ごとの当たり判定の矩形 |
ブロック 1 つ 1 つが実測された単語のブロック崩し |
| 砕く・粒子にする | walkLineRanges → 書記素ごとの (x,y) → 物理計算 |
クリックすると文字に弾け飛ぶ 1 文 |
| ASCII の障害物で組む文字 | layoutNextLineRange と、実測した行ごとの障害物の区間 |
ビットマップの ASCII ロゴ、形の変化、ドラッグできるワイヤーの立体。その実際の形に沿ってテキストが開きます |
| 雑誌風の多段組み | 段ごとの layoutNextLineRange と共通のカーソル |
引用を抜き出した、動きのある雑誌の見開き |
| 動く文字組み | layoutWithLines と、行ごとの時間変化する変形 |
スター・ウォーズ風の流れ、波、跳ね、グリッチ |
| 複数行をぴったり包む | measureLineStats |
一番小さく収まる大きさに自動で合う引用カード |
そのまま動く 1 枚もののひな形として、templates/donut-orbit.html と templates/hello-orb-flow.html を見てください。
作業の流れ
- 依頼の内容に合わせて、上の表から型を選びます。
- ひな形から始めます。
templates/hello-orb-flow.html— 動く球のまわりを回り込むテキスト(障害物を避ける型)templates/donut-orbit.html— 発展的な例。実測した ASCII ロゴの障害物、ドラッグできるワイヤーの球と立方体、形が移り変わる場、選択できる DOM テキスト、開発時だけの操作パネルwrite_fileで~/.hermes/cache/scratch/かユーザーの作業場所に新しい.htmlを作ります。
- 依頼に合った文章に差し替えます。本物の文章を 10〜100 文ほど。ダミー文は使いません。
- 見た目を詰めます。フォント、配色、構図、操作。ここが本番なので、飛ばさないでください。
- 手元で確認します。
cd <dir-with-html> && python -m http.server 8765
# then open http://localhost:8765/<file>.html- コンソールを見ます。
prepareWithSegmentsに不正なフォント指定を渡すと pretext は例外を投げます。Intl.Segmenterは最近のブラウザならどれでも使えます。 - コードだけでなく、ファイルの場所も伝えてください。開いてみたいはずです。
速さについて
- 重いのは
prepare()/prepareWithSegments()です。テキストとフォントの組み合わせごとに一度だけ呼び、返り値を取っておいてください。 - 大きさが変わったときは
layout()/layoutWithLines()だけをやり直します。準備からやり直してはいけません。 - テキストは変わらず形だけが動くアニメーションなら、
layoutNextLineRangeをループで回しても十分に軽く、ふつうの長さの段落なら毎フレーム 60fps で回せます。 - 毎フレーム ASCII のマスクを描くときは、セルの並びを(
Uint8Arrayなどの型付き配列で)保持し、そこから、あるいは投影した形から行ごとの障害物の区間を実測して求め、区間をつないでからlayoutNextLineRangeに渡し、そのあとで文字を描きます。 - 見た目の動きと組み直しの動きは連動させてください。球が立方体に変わるなら、描画するセルの並びと障害物の区間を同じ値で動かします。そうしないと、実際に回り込んでいるのではなく、上から絵を貼ったように見えます。
- 淡く消すときは、文字の濃さや障害物の大きさを変えるのではなく、レイヤーの不透明度を使ってください。一時的な ASCII のスプライトは専用の canvas に置き、CSS か GSAP の不透明度で消すと、形が縮んで見えずに済みます。
- canvas の
ctx.fontの設定は思いのほか遅いので、フォントが変わらないならfillTextごとではなく 1 フレームに一度だけ設定してください。
つまずきやすいところ
- CSS と canvas でフォント指定がずれる。
ctx.font = "16px Inter"で測ったのに、CSS にはfont-family: Inter, sans-serif; font-size: 16pxと書いてある、という場合です。Inter が読み込めていれば問題ありません。Inter が 404 になると CSS は sans-serif に落ち、計測が 5〜20% ずれます。フォントは必ずpreloadするか、どの環境にもある書体を使ってください。
- アニメーションのループの中で準備をやり直している。 軽いのは
layout*だけです。毎フレームprepareを呼び直すと速度が落ちます。準備した返り値はモジュールの外側に持っておいてください。
- 書記素の分割で
Intl.Segmenterを忘れる。 絵文字、結合文字、CJK があるとき、"é".split("")は 2 文字になってしまいます。見える文字を 1 つずつ取り出すときはnew Intl.Segmenter(undefined, { granularity: "grapheme" })を使ってください。
extraWidthなしのbreak: 'never'のチップ。rich-inlineで、分割したくないチップやメンションにbreak: 'never'を使うときは、丸い枠の余白ぶんのextraWidthも渡してください。渡さないと枠が容器からはみ出します。
- TypeScript のままの入口を
unpkgから@chenglou/pretextとして読む。esm.shを使ってください。TS の書き出しをブラウザで動く ESM に変換してくれます。unpkgは 404 になるか、TS をそのまま返します。
- 等幅への落ち込みで、持ち味が消える。 等幅に見える出力になっているときは、CSS の
font-familyがmonospaceまで落ちていることが多いです。実際に使われている書体を DevTools で確かめてください。
- 図形のまわりを流すとき、行を飛ばすか幅を詰めるか。 その行の通り道が狭くて 1 行分も入らないなら、
layoutNextLineRangeに極端に小さな maxWidth を渡すのではなく、*その行を飛ばして*ください(y += lineHeight; continue;)。渡してしまうと、1 文字ずつの壊れた行が返ってきます。
- 冷たいままの作品を出す。 手を入れない初期状態は、チュートリアルの域を出ません。周辺を暗く落とす処理、うっすらした走査線、放っておいても動く仕掛け、よく考えた操作への反応(ドラッグ、ホバー、スクロール、クリック)のどれかを足してください。これがないと、「面白い pretext のデモ」ではなく「README を写しただけ」に見えます。
確認する項目
- [ ] デモが 1 枚で完結する
.htmlになっていて、ダブルクリックかpython -m http.serverで開ける - [ ]
@chenglou/pretextをesm.shからバージョン固定で読み込んでいる - [ ] 並べる文章がダミー文ではなく本物で、デモの主題に合っている
- [ ]
prepareに渡すフォント指定が CSS のフォントと完全に一致している - [ ]
prepare()/prepareWithSegments()を毎フレームではなく一度だけ呼んでいる - [ ] 背景が暗く、配色を練ってある。canvas の既定の白のままではない
- [ ] 操作への反応(ドラッグ・ホバー・スクロール・クリック)か、放っておいても動く仕掛けが 1 つ以上ある
- [ ]
python -m http.serverで手元で動かし、コンソールにエラーが出ないことを確かめた - [ ] そこそこの性能のノートパソコンで 60fps 出る(出ないなら、どう見た目を落とすかを書いてある)
- [ ] 頼まれていないけれど一歩踏み込んだ工夫が 1 つある
参考: みんなが作った作品
着想や型の参考に、次のものを取ってきて見てください(どれも MIT に準じるもので、pretext.cool からたどれます)。
- Pretext Breaker — 単語をブロックにしたブロック崩し —
github.com/rinesh/pretext-breaker - Tetris × Pretext —
github.com/shinichimochizuki/tetris-pretext - ドラゴンのアニメーション —
github.com/qtakmalay/PreTextExperiments - Somnai の雑誌風エンジン —
github.com/somnai-dreams/pretext-demos - Bad Apple!! の ASCII 版 —
github.com/frmlinn/bad-apple-pretext - ドラッグするスプライトの回り込み —
github.com/dokobot/pretext-demo - Alarmy の雑誌風の時計 —
github.com/SmisLee/alarmy-pretext-demo
公式の遊び場: chenglou.me/pretext — アコーディオン、バブル、動的レイアウト、雑誌風エンジン、両端揃えの比較、タイル状の配置、markdown のチャット、リッチなメモ。