そのWeb演出、Claude Codeで作れます。クリエイティブWeb表現のプロンプト集 #06 Halftone Press
株式会社グルーヴサインの米沢です。
AIコーディングでクリエイティブなWeb表現を実際に作り、そのプロンプトごと公開していく「PROMPT → WEB」の第6回です。
PROMPT → WEB #006
Halftone Press
INTRODUCTION
ここまでの5回は、ずっと何を描くかの話でした。粒子(#01)、文字(#02)、線(#03)、物体(#04)、そして計算そのもの(#05)。
今回はどう刷るかの話です。
印刷は長いあいだ、原理的に不可能なことをやってきました。インクは「乗っているか、いないか」しかありません。中間の濃さを出せないメディアで、写真のなめらかな階調をどう再現するか。19世紀に出た答えが網点です。点の大小に置き換えれば、離れて見たときに濃淡に見える。いまも新聞・雑誌・パッケージのほとんどが、この方式で刷られています。
やることは、その製版処理をブラウザの中で毎フレーム回すことです。ただし今回は、ひとつ条件を足しました。原画を画像として持たないこと。 写真ファイルもピクセル配列も用意せず、「その座標の明るさ」を返す関数だけを置きます。網点1個を打つたびに、その1点の濃度だけを聞きに行く。
題材は、一点に集まる布のドレープにしました。カーソルが光源です。
RESULT
まず現物です。下のボタンを押してみてください。
クリックすると、この画面の上で Halftone Press が再生されます(何度でも再生可能)。ESC または CLOSE で終了します。
生成りの紙に、藍・紅・黄の3色の網点でドレープが刷られています。操作は2つです。
- カーソルを動かす(スマホは自動で回ります) ── カーソルが光源です。動かすとひだの陰影が変わり、それに追随して網点の一粒一粒が太ったり痩せたりします。
- クリックする(スマホはタップ) ── 製版方式が変わります。網点 → 万線 → 砂目 → 文字。切り替えの瞬間、版が少しずれて、数十フレームかけて見当が合っていきます。四隅のトンボを見ていると、ずれて合う様子がよく分かります。
近づいて点の並びを見ると、3色の網点が花のように渦を巻いているのが分かります。ロゼッタと呼ばれる模様で、印刷物をルーペで覗くと必ず出てくるものです。これは狙って出しています(後述)。
左下に、いまどの方式で何度の角度で刷っているかが出ています。ESC または CLOSE で終了します。
WHAT WE MADE
技術的に分解すると、次のようになります。
- 原画は関数 ── 画像ファイルもピクセル配列も持ちません。座標を渡すと明るさが返る関数だけがあります。持たないので、解像度の上限もありません。
- C / M / Y の3版に分解 ── 明るさから各版のインク量を決め、版ごとに別々の格子で点を打ちます。黒版(K)はありません。3色の重ね刷りで黒を作ります。
- 版ごとのスクリーン角度 ── 15° / 75° / 0°。この30°ずつという離し方に理由があります(後述)。
- 重ね刷りは乗算合成 ── インクは重なるほど暗くなります。
globalCompositeOperation = 'multiply'が、そのまま減法混色になります。 - 4つの製版方式 ── 網点・万線・砂目・文字。格子も濃度も共通で、打つマークだけが違います。
- トンボと見当ズレ ── 四隅に見当合わせの目印。刷り直すたびに版がずれ、寄っていきます。
- 静止フォールバック ── アニメーションを好まない設定では、1枚だけ刷って止まります。
HOW IT WORKS
1. 原画を、画像として持たない
素直に作るなら、原画をオフスクリーンCanvasに描いて getImageData で読み、そのピクセルを見ながら点を打ちます。実際、最初にAIが書いてきたのもその形でした。
しかしよく考えると、必要な濃度は網点の数だけです。1920×1080の画面でも、打つ点は2万個弱。207万画素を全部作ってから2万点を拾うのは、200万回の無駄です。
そこで原画を関数にしました。
// 画像ではなく「その座標の明るさ」を返す関数が原画
const tone = (x, y) => { /* … 陰影を計算して 0〜1 を返す … */ };
const a = inkOf(tone(x, y), plane); // 網点1個ぶんだけ、その場で評価する
実測すると、1フレームで評価する点は 20,343点。画面の画素数の 約1% です。副産物として、原画に解像度の概念が無くなりました。網点を細かくすれば、いくらでも細かい原画が出てきます。
2. 高さは要らない。傾きだけが要る
陰影を計算するには面の向き(法線)が要ります。ふつうは高さマップを作って、隣との差から法線を求めます。
ですが今回の布は、高さが sin の和で書けます。和の微分は微分の和なので、傾きは cos の和で直接書けます。つまり高さを一度も計算せずに、必要な傾きだけが手に入ります。
const u = ang * FOLDS + 0.6 * Math.sin(inner) + T * 0.28; // ひだの位相
const su = Math.sin(u), cu = Math.cos(u);
// h = A*r*sin(u) を px, py で微分した式をそのまま書く
const gx = 0.058 * (dxp * ir * su + cu * (-FOLDS * dyp * ir + wc * dxp)) + 0.50 * c2;
const gy = 0.058 * (dyp * ir * su + cu * ( FOLDS * dxp * ir + wc * dyp)) - 0.036 * c2;
ひだの位相を「ある一点を見込む角度」にしているのがポイントです。こうすると、ひだが一点に集まって放射状に広がり、その点に近いほど間隔が詰まります。カーテンを1か所で束ねたときの、あの形になります。
3. なぜ版ごとに角度をずらすのか
ここが今回いちばん面白かったところです。
3つの版を同じ角度で刷ると、点がぴたりと重なって色が濁ります。では少しずらせばいいかというと、逆です。試しに 15° / 20° / 25° にして刷ってみたところ、画面いっぱいに大きな波打つ縞が出ました。格子と格子のわずかな周期差が、低い周波数の唸りとして見えてしまう。これがモアレです。
対策が、30°ずつ大きく離すという古典的な答えです。周期差を大きくすると、唸りの周波数も高くなって網点の細かさに紛れ、代わりに花のような小さな渦(ロゼッタ)が残ります。ロゼッタは「モアレを、目に見えないほど細かいモアレに追い込んだ結果」です。
実装は、版ごとに傾けた格子を走査するだけです。
// 画面の4隅を回転座標へ移し、走査すべき格子の範囲を求める
const i0 = Math.floor(rx0 / cell) - 1, i1 = Math.ceil(rx1 / cell) + 1;
…
const x = rx * co - ry * sn; // 格子点を世界座標へ戻す
const y = rx * sn + ry * co;
if (x < -cell || x > W + cell || y < -cell || y > H + cell) continue; // 画面外は捨てる
回転した格子で画面全体を覆うには、回転前より広い範囲を回す必要があります。はみ出した点は座標を戻した時点で捨てています。捨てる判定は原画の評価より手前に置くこと。ここを逆にすると、見えない点の濃度まで計算することになります。
4. 色は、分解のカーブが作っている
原画はモノクロです。明るさが1つ返ってくるだけで、色の情報はどこにもありません。色は、明るさを3版へ振り分けるカーブが作っています。
let t = 1 - v; // インクの総量
t = t * (0.30 + 0.70 * t);
if (plane === 0) a = t * t * t * 1.15; // C … 遅く入る → 影が青黒くなる
else if (plane === 1) a = t * t * 0.98; // M … 中間調
else a = t * 0.70; // Y … 早く入る → 明部が暖かくなる
黄を早く、藍を遅く入れています。それだけで、明部は紙の色に近い暖かいクリーム、中間調はオレンジから赤、暗部は青黒く沈みます。同じ原画から、分解カーブを差し替えるだけで色調が全部変わります。 写真の色を再現するのではなく、色を作りにいくという意味では、リソグラフや版画の考え方に近いです。
黒版(K)は用意しませんでした。3色を重ねれば十分に黒くなるうえ、版が1つ減れば計算も1/4減ります。「黒は黒インクで刷るもの」という前提を外せたのは、実際に重ねて見てからです。
5. 同じ格子・同じ濃度、マークだけを差し替える
4つの製版方式は、別々に実装したわけではありません。走査する格子も、そこで得た濃度も共通で、最後に打つマークだけが違います。
- 網点 ── 半径
√濃度の円。面積が濃度に比例します(半径に比例させると濃くなりすぎます) - 万線 ── 格子の向きに伸びる帯。濃度ぶんだけ太らせると、隣とつながって1本の線になり、線幅が濃淡になります
- 砂目 ── 大きさは固定。格子番号のハッシュを閾値にして、打つか打たないかで濃さを出します
- 文字 ── 濃度順に並べたグリフから1つ選んで貼ります
製版方式というと大げさですが、濃淡を離散的なマークに置き換える方法の違いでしかない、というのがコードにするとよく分かります。ここが今回いちばん気持ちよかった部分です。
6. 砂目だけ、原画の評価を1回にまとめられる
砂目(FM)はスクリーン角度を持ちません。点の位置が不規則なので、そもそも格子の周期がなく、原理的にモアレが出ない方式です。
これは実装上のご褒美でもありました。3版とも角度が0なら、3つの格子は完全に一致します。ということは、同じ座標の原画を3回評価しているわけです。1回にまとめて、その場で3版へ振り分けました。
const v = tone(x, y); // 原画の評価は1点につき1回
for (let p = 0; p < 3; p++) {
const a = inkOf(v, p);
if (a < 0.012 || a * 1.25 < hash2(i, j, p + 1)) continue;
…
}
これで 6.50ms → 2.69ms。網点と万線は版ごとに格子の向きが違うので、この手は使えません。
7. 文字版は、焼いてから貼る
文字だけは毎フレーム fillText を数千回…とやると一気に破綻します。起動時に10種類のグリフを小さなCanvasへ1度だけ焼いて、あとは drawImage で貼るだけにしました。文字の描画がただの画像コピーになります。
なお文字版は3版ではなく単版(墨)です。3色で文字を重ねると、単純に読めなくなりました。ここは実際に刷ってから引き返した部分です。
THE PROMPT
ここが本題です。上のデモを作るためにClaude Codeへ渡せる、汎用プロンプト全文を掲載します。右上の COPY ボタンでそのままコピーできます。色や題材を置き換えれば、自分用になります。
あなたはCreative Codingと印刷製版に精通したフロントエンドエンジニアです。
「Halftone Press」= 画面全体を印刷の網点で刷り上げるWeb表現を、Canvas 2D で実装してください。
【最重要】原画をラスタ画像として持たないこと。「座標(x,y)を渡すと明るさ0〜1を返す関数」を
原画とし、網点1個を打つたびに、その1点だけを評価すること。
(getImageDataで全画素を作ってから点を拾う実装にはしない)
# 置き換える変数
- PAPER_COLOR = "#d7d2c4" // わずかにグレーがかった紙
- INKS = 3版。名前・色・スクリーン角度:
C 藍 "#2f7fa8" 15°
M 紅 "#c8506a" 75°
Y 黄 "#e3c46a" 0°
(黒版Kは作らない。3色の重ね刷りで黒を出す)
- SUBJECT = 一点に集まる布のドレープ(陰影のある連続階調なら何でもよい)
- LABEL = "HALFTONE PRESS"
# 原画(関数として持つ)
- tone(x, y) は、その点の明るさ 0〜1 を返す
- 布のひだ: 高さを sin の和で定義し、【重要】高さは計算せず、微分(cos の和)だけを求める。
陰影に要るのは高さではなく傾きなので、これで足りる
- ひだの位相は「画面外の一点を見込む角度」に比例させる。ひだが放射状に集まり、
その点に近いほど間隔が詰まる
- 法線と光源方向の内積で拡散光、その16乗で光沢(pow は使わず2乗を4回)
- 光源はポインタ位置。周辺光量を軽く落とす(刷り物らしい落ち)
# 製版(ここが主題)
- 版ごとに、スクリーン角度だけ傾けた格子を走査する
・画面の4隅を回転座標へ移して走査範囲を出す
・格子点を世界座標へ戻し、画面外なら【原画を評価する前に】捨てる
- 明るさ → 各版のインク量は「分解カーブ」で決める。原画はモノクロで、色はこのカーブが作る
・黄は早く入れる(明部が暖かくなる)/藍は遅く入れる(暗部が青黒く沈む)
- 1つの版に打つ数千個のマークは Path2D 1本にまとめ、fill() は版につき1回だけ呼ぶ
- インクの重なりは globalCompositeOperation = 'multiply'(減法混色になる)
- 紙のざらつきは、小さなノイズタイルを1度だけ作って createPattern で敷く
# 4つの製版方式(クリック/タップで循環)
【重要】格子と濃度は共通にし、打つマークだけを差し替える構造にすること
1. 網点(AM) … 半径が √濃度 に比例する円(面積が濃度に比例する。半径に比例させない)
2. 万線(LINE)… 格子の向きに伸びる帯を濃度ぶん太らせる。隣とつながって線幅が濃淡になる
3. 砂目(FM) … 大きさ固定の点。格子番号のハッシュを閾値にして打つ/打たないで濃さを出す。
角度を持たない=モアレが出ない。3版の格子が一致するので、
【最適化】原画の評価は1点1回にまとめ、その場で3版へ振り分ける
4. 文字(TYPE)… 濃度順のグリフ列から1つ選んで貼る。単版(墨)でよい。
【最適化】起動時にグリフを小さなCanvasへ焼き、以後は drawImage で貼る
# スクリーン角度の注意
- 3版の角度は 30° 程度ずつ大きく離すこと。数度しか離さないと、格子の周期差が
低周波の唸り(モアレ)として画面いっぱいに出る。大きく離すとロゼッタになる
- 実装後、角度を 15/20/25 に変えて刷り比べ、モアレが出ることを自分で確認すること
# 演出
- 四隅にトンボ(十字+輪)を版の色で描く。版がずれると最初にここが割れて見える
- 方式を切り替えるたびに版を数px ずらし、毎フレーム 0.9 倍して見当が合っていく
- 隅に刷り出しの表示(方式名・角度・網点ピッチ)と、3版の色玉を小さく置く
- ネオン・紫グラデ・glassmorphism・黒背景は使わない(紙とインクの質感にする)
# 品質要件(production)
- 全画面オーバーレイ。CLOSE ボタンと ESC で終了。多重起動を防ぐ
- レスポンシブ: リサイズで格子を組み直す。devicePixelRatio は最大2でクランプし、
以降の描画はすべて CSS px で行う
- 網点ピッチは画面の短辺に比例させ、上下限で頭打ちにする(大画面で点が増え続けないように)
- モバイル: ポインタが無いので光源をゆっくり自動で回す。タッチで方式を切り替える
- prefers-reduced-motion: アニメーションせず、1枚だけ刷って静止させる
- 終了時に requestAnimationFrame を停止し、全リスナを解除し、CanvasをDOMから除去する
(何度開閉してもリークしないこと)
- Canvas 2D が使えない環境では静かに何もしない。本文の閲覧を妨げない
CUSTOMIZE
- 原画を差し替える ──
tone(x, y)を書き換えるだけで、刷る対象が変わります。実写真を刷りたい場合はここを画像サンプラにしますが、そのときは画像の解像度が上限になることだけ意識してください。関数のままなら上限はありません。 - インクを2色にする ── 藍と朱の2版だけにすると、一気にリソグラフになります。角度は45°離せば十分です。
- 網点ピッチ ── 大きくすると1970年代のポップアート、小さくすると新聞紙面。ピッチだけで時代が変わります。
- 分解カーブ ── 色を決めているのはここです。黄を遅らせ藍を早めると、同じ原画のまま寒色の刷り物になります。
- 版ズレの量 ── 減衰係数を1.0に近づけると、ずれたまま合わない「刷り事故」の表現になります。これはこれで味があります。
- 静かにする ── 1枚だけ刷って止めれば、そのまま壁紙やOGP画像になります(この記事のOGP画像が実際それです)。
PERFORMANCE
全画面に毎フレーム2万個のマークを打つので、素直にやると重い題材です。今回も実測しながら決めました(1920×1080・網点ピッチ18px・計算部分をNodeで実測)。
- 網点 ── 格子の走査 20,343点 / 打つマーク 17,805個 / 2.22ms
- 万線 ── 同 20,343点 / 17,769個 / 2.18ms
- 砂目 ── 原画の評価 18,824回 / マーク 14,204個 / 2.69ms(3版まとめる前は 6.50ms)
- 評価するのは画素の約1% ── 原画を画像として持たない効果がそのまま出ています。207万画素に対して2万点です。
- fill は1フレームに3回 ── 数千個のマークを版ごとに Path2D 1本へ溜め、塗りは版につき1回。
arcのたびにfill()を呼ぶ実装とは桁が変わります。 - 画面外は、評価する前に捨てる ── 傾いた格子で画面を覆うと、はみ出す点が3割ほど出ます。この判定を原画の評価より後に置くと、見えない点の陰影まで計算することになります。
- モバイル ── ピッチの下限(9px)と
devicePixelRatioの上限(2)で、点の数とラスタライズ量の両方に天井を作っています。 - 後始末 ── 終了時にRAFを停止し、全リスナを解除してCanvasを除去。何度開閉してもリークしません。
WHAT AI DID / WHAT HUMAN DIRECTION MATTERS
「Canvasでハーフトーンを実装して」と言えば、動くものはすぐ出てきます。今回もそうでした。ただ、最初に出てきたものは印刷ではありませんでした。
出てきたのは、画像をグレースケールにして、明るさに応じた大きさの黒い円を格子状に並べるコードです。動きます。ハーフトーン風にも見えます。しかし、
- 版という概念がない(単版。だから重ね刷りも減法混色も起きない)
- スクリーン角度がない(だから、ロゼッタもモアレも存在しない)
- 原画を全画素ぶん作ってから点を拾う(必要なのは1%なのに)
つまり「網点っぽい絵」は書けても、「印刷」は書いていない。ここは知識の質の差というより、何を再現したいのかを誰が決めるかの差だと思います。
人間の仕事として残ったのは、次のような判断でした。
- 原画を持たないと決めること(AIは持つ実装を選びます。そちらが定石なので)
- 角度を30°離す理由を、説明ではなく刷り比べて確かめること(15/20/25で本当にモアレが出ました)
- 色を原画ではなく分解カーブに作らせること
- 黒版を捨てる判断
- 4つの方式を、別々の実装ではなくマークの差し替えとして設計すること
- 文字版だけ3版をやめて単版に引き返すこと
最後の2つは、性能の話でも正しさの話でもありません。「そう作ると何が言えるか」の話です。格子と濃度を共通にしたからこそ、製版方式の違いはマークの違いでしかないと示せる。ここはコードを書く前に決めておく必要があり、いまのところAIが勝手に決めてくれる部分ではありません。
6回続けて、感触は変わっていません。実装は速くなりました。速くなったぶん、何を捨て、何を並べるかに時間を使えるようになった。それだけです。
NEXT
粒子、文字、線、物理、計算、そして今回の製版。「描く」「育てる」に続いて、今回は「刷る」でした。
次回はまだ決めていませんが、音(Web Audio)、スクロール連動、ボロノイ分割あたりを候補にしています。
今回のプロンプトも丸ごとコピーして使えます。インクの色を自社のブランドカラー2色に置き換えれば、それだけでリソグラフ風の表現になります。画面をルーペで覗くように近づいて、点の渦を確かめてみてください。あれが、150年ぶん積み上がった工夫の形です。
レガシーPHPシステムの保守・引き継ぎのご相談を受け付けています。
初回のご相談・ヒアリングは無料です。