そのWeb演出、Claude Codeで作れます。クリエイティブWeb表現のプロンプト集 #09 Luminogram
株式会社グルーヴサインの米沢です。
AIコーディングでクリエイティブなWeb表現を実際に作り、そのプロンプトごと公開していく「PROMPT → WEB」の第9回です。
PROMPT → WEB #009
Luminogram
INTRODUCTION
前回(#08)の最後に、次は「影だけで形を見せる表現」と書きました。今回がそれです。
このデモには、物体を描いている行が1行もありません。 葉も、輪も、格子も、画面に描画してはいません。あるのは光源と、光が届いた範囲の計算だけです。にもかかわらず、画面には形がはっきりと見えます。光が届かなかった場所として、そこにあるものが浮かび上がるからです。
下敷きにしたのはフォトグラムという写真技法です。カメラを使わず、印画紙の上に直接ものを置いて光を当てる。光の当たった紙は黒く、ものの下は白く残る。マン・レイがレイヨグラフと呼んで作品にした、写真の最も原始的な形です。
もう一つ、今回は時間を入れました。光源を動かすと、通った跡が紙に焼き付いて残ります。長時間露光です。一瞬の影は形の半分しか教えてくれませんが、動かし続けると、影は流れ、動かないものだけが残ります。 見えているのは現在の影ではなく、光の履歴です。
RESULT
まず現物です。下のボタンを押してみてください。
クリックすると、この画面の上で Luminogram が再生されます(何度でも再生可能)。ESC または CLOSE で終了します。
暗い紙の上に灯りがひとつ。カーソルを動かすと、光の届いた場所から紙が感光していきます。操作は2つです。
- カーソルを動かす(スマホは画面をなぞる) ── 灯りが動きます。動かした分だけ紙が焼け、物の影が流れ、輪郭が残っていきます。
- クリックする(スマホはタップ) ── 次の印画に変わります。銀 → セピア → 青写真 → 鉄。置かれているものと調色が同時に変わります。
しばらく触らずにいると、灯りはひとりでにゆっくり漂いはじめます。放っておいても露光は進み、像は勝手に焼き上がっていきます。左下に露光時間が出ています。ESC または CLOSE で終了します。
WHAT WE MADE
技術的に分解すると、次のようになります。
- 可視多角形 ── 光源から見える範囲を、角度を一周なめて求めます。これが「光の当たっている場所」そのものです。
- 露光の蓄積 ── 焼けた量を溜めておく裏のキャンバスを1枚持ち、毎フレーム、可視多角形を薄く塗り重ねます。画面に出しているのは、その蓄積です。
- 半影を時間で作る ── 光源を点ではなく小さな円とみなし、その円の中に散らした数点を毎フレーム焼きます。重なるうちに影の縁がぼけて半影になります。
- 距離による減衰 ── 可視範囲を一様に塗ると光に見えません。光源からの距離で暗くしています。
- 物の足跡は焼かない ── 遮蔽物の内側は、光源がその上を通っても未感光のまま保ちます(紙の上に置かれているので当然です)。
- 立体に見せる2つの手がかり ── 接地影(輪郭のすぐ外に落ちる濃い影)と、縁の光(灯り側の縁だけが光る)。物が紙の上に載っている厚みが出ます。
- 空気と艶 ── 光の中でだけきらめく塵、灯りの光条、印画紙の艶と周辺光量落ち。
- 4種類の印画 ── 置くものと調色の組み合わせを4つ。葉、ガラスの輪、格子、破片。
- 静止フォールバック ── アニメーションを好まない設定では、露光を620回まとめて回し、焼き上がった1枚だけを表示します。
HOW IT WORKS
1. 光源から見えている範囲を求める
遮蔽物をすべて線分の集まりとして持ちます。光源から、すべての頂点に向かって光線を飛ばし、最初にぶつかった線分までの距離を求める。それを角度順に並べてつなぐと、光の届く範囲が多角形になります。
for (const s of segs) {
const a1 = Math.atan2(s.ay - ly, s.ax - lx);
// 頂点そのものだけでなく、その両脇へも撃つ(±ε)
angles.push(a1 - EPS, a1, a1 + EPS);
}
±ε の2本が要ります。頂点ぴったりの1本だけだと、光はそこで必ず止まってしまい、物の角を回り込んだ先へ到達できません。 角のわずかに外側へ撃った光線が奥の壁まで抜けて、影の境界が決まります。1行ですが、これが無いと影の形になりません。
もう一つ、画面の四隅へ向かう角度も足しておきます。遮蔽物が何も無い方向では光線が画面の縁で止まるだけなので、四隅を入れないと多角形が閉じません。
2. 半影は「時間」で作る
現実の影の縁がぼけるのは、光源に大きさがあるからです。電球のどの一点から見るかで影の位置がわずかに違い、その重なりが半影になります。
まじめにやるなら、光源の円周上に何十点も取って、そのたびに可視多角形を求めて平均します。当然重い。今回は1フレームに4点だけ、円の中にランダムに散らして焼きます。1フレームでは粗いのですが、露光は蓄積されるので、数十フレームのうちに自然と滑らかな半影になります。
for (let i = 0; i < SAMPLES; i++) {
const a = Math.random() * TAU, d = Math.sqrt(Math.random()) * r;
burn(light.x + Math.cos(a) * d, light.y + Math.sin(a) * d, 0.05);
}
これは手抜きの帳尻合わせに見えて、実は題材そのものです。長時間露光とは、時間をかけて光を溜めることでした。 技術的な都合と表現の理屈が一致したので、そのまま採用しています。
3. 一様に塗ると、光に見えない
最初の実装は、可視多角形を紙の色でべた塗りしていました。動くには動くのですが、まったく光に見えません。平らな図形が切り替わるだけです。
原因は明快で、現実の光は距離で弱まるのに、それを無視していたからです。塗りを光源中心の放射グラデーションに変えたら、一度で光になりました。
const g = ectx.createRadialGradient(lx, ly, 0, lx, ly, reach);
g.addColorStop(0, paper); // 光源のそば
g.addColorStop(1, hexA(paper, 0)); // 遠く
4. 物の足跡は、決して焼かない
灯りが物の上を通り過ぎたとき、そのままだと物の内側まで感光してしまいます。紙に置かれた物の下に光は回り込まないので、毎フレーム、遮蔽物の形を destination-out で未感光に戻しています。
ここで1つ、はまりました。一部の葉だけが黒く抜けない。 位置も形も正しいのに、画面の左側の2枚だけ中途半端に灰色のまま残る。
原因は destination-out の仕様でした。この合成は「塗りのアルファ」の分だけ消します。そして fillStyle には、直前の露光で使った放射グラデーションがそのまま残っていた。 グラデーションは光源から遠いほど透明なので、光源から遠い形ほど消え残る。だから左側だけだったわけです。
ectx.globalCompositeOperation = 'destination-out';
ectx.fillStyle = '#000'; // ← これが無いと、直前のグラデーションで「薄く」消える
見つけ方も書いておきます。目で見ても「なんとなく灰色」としか分からなかったので、葉の中心の画素値を8枚ぶん読み出しました。 6枚が18前後(=未感光)で、2枚だけが68と41。数字にした瞬間に「左側だけ」という規則性が見え、原因に直行できました。画面を睨む時間を、計測に置き換えるのが結局いちばん速いです。
5. 同じ計算を2度しない
画面には、焼き付いた履歴だけでなく「いま光が届いている範囲」も重ねています。最初はこれを別途計算していたのですが、焼き付けのために計算した可視多角形と同じものでした。
焼くときに求めた多角形を配列に取っておき、現在の灯りの描画でも使い回すようにして、1フレームの可視計算を7回から4回に減らしました。
6. 影だけでも、立体には見せられる
ここまでで形は出ますが、最初の版は平らな切り絵のように見えていました。影は正しいのに、物が紙の上に「載っている」感じがしない。足したのは2つだけです。
接地影。 輪郭のすぐ外側に、短く濃い影を落とします。線幅を変えて3回なぞるだけの安上がりなぼかしですが、これで物に厚みが出ます。
縁の光。 灯り側の縁だけを細く明るく抜きます。作り方は、形の内側にクリップしてから、輪郭を灯りと逆方向へ少しずらして描く。ずらした輪郭のうち、灯り側の縁が形の内側に残り、三日月状のハイライトになります。
vctx.save();
traceShape(vctx, poly);
vctx.clip(); // 光は形の内側にとどめる
vctx.translate((-dx / len) * push, (-dy / len) * push); // 灯りと「逆」へずらす
traceShape(vctx, poly);
vctx.stroke();
vctx.restore();
最初は灯りの方向へずらして書いていて、ハイライトが物の裏側に出ました。 クリップで内側に閉じ込める以上、残るのはずらした先とは反対側です。符号ひとつですが、逆にすると立体感が消えて、むしろ平たく見えます。
もう一つ効いたのが色温度の差です。灯りは暖かく、影は冷たく。地の色をわずかに青へ寄せ、縁の光を冷たい銀色にすると、同じ明暗でも奥行きが出ます。絵画で古くから使われてきた手ですが、画面でもそのまま効きました。
最後に、光の中でだけきらめく塵を浮かべ、灯りに光条を立て、紙全体に薄い艶と周辺光量落ちを掛けています。どれも1つずつは地味ですが、重なると空気が写ります。
THE PROMPT
ここが本題です。上のデモを作るためにClaude Codeへ渡せる、汎用プロンプト全文を掲載します。右上の COPY ボタンでそのままコピーできます。色や置くものを差し替えれば、自分用になります。
あなたはCreative Codingと計算幾何に精通したフロントエンドエンジニアです。
「Luminogram」= 物体を一切描画せず、光が届かなかった場所として形を見せる表現を、
Canvas 2D で実装してください。フォトグラム(印画紙にものを置いて感光させる技法)が下敷きです。
(物理エンジンや幾何ライブラリは使わない。可視多角形は自前で書く)
# 置き換える変数
- PRINTS = 4種類。それぞれ「置くもの」と「調色」を持つ:
銀 SILVER 地 "#121317" 紙 "#e9eaec" 灯 "#ffd9a4" … 葉を散らす
セピア SEPIA 地 "#17110b" 紙 "#f0dfc0" 灯 "#ffcf8a" … ガラスの輪と粒
青写真 CYANOTYPE 地 "#0b1622" 紙 "#cfe2f2" 灯 "#ffe4bb" … 格子
鉄 IRON 地 "#14120f" 紙 "#ded6c8" 灯 "#ffd6a0" … 破片と輪
- LABEL = "LUMINOGRAM"
# 遮蔽物
- 多角形の配列として持つ。座標は 0..1 の正規化(リサイズで構図が崩れないように)
- 【重要】遮蔽物そのものは絶対に描画しない。影としてのみ現れること
- 形は葉・円・中空の輪・細い棒などを関数で生成する。1シーンあたり線分150〜200本を上限の目安に
# 可視多角形(光源から見える範囲)
- 遮蔽物を線分の集合に展開する
- 各線分の両端点への角度に加え、その両脇 ±0.00012 の角度へも光線を飛ばす
(±εが無いと物の角を回り込めず、影の形にならない)
- 画面の四隅への角度も足す(遮蔽の無い方向で多角形が閉じないため)
- 角度でソートし、各光線について最も近い交点までの距離を求め、点を順に結ぶ
- 画面の枠でも光線を止める
# 露光(ここが主題)
- 蓄積用のオフスクリーンcanvasを1枚持つ。毎フレーム、可視多角形を薄く塗り重ねる
- 塗りは source-over(lighter は白に飛ぶ。source-over なら紙の色に収束する)
- 【重要】可視多角形は一様に塗らない。光源中心の放射グラデーションで塗り、
距離で減衰させる。一様塗りだと光に見えず、ただの図形になる
- 光源は点ではなく小さな円とみなし、円内に散らした数点(4点程度)を毎フレーム焼く。
重なるうちに影の縁が半影になる(面光源を「時間」で解く)
- 毎フレームの焼き付けのあと、遮蔽物の形を destination-out で未感光に戻す。
【罠】destination-out は塗りのアルファで消すので、fillStyle を不透明色に
戻してから塗ること。直前のグラデーションのままだと遠い形ほど消え残る
- 焼き付けで求めた可視多角形は、画面に重ねる「現在の灯り」の描画にも使い回す
(同じ計算を2度しない)
# 描画
- 画面 = 地の色 → 蓄積canvas → 現在の灯り → 立体の手がかり → 塵 → 灯り本体 → 艶 → 粒子
- 現在の灯りは弱めに。強くすると焼き付いた像が消え、3D CGのような絵になる
- 【立体】接地影: 遮蔽物の輪郭を、線幅を変えて3回、地の色で薄くなぞる(安上がりなぼかし)
- 【立体】縁の光: 形の内側にクリップし、輪郭を灯りと「逆」方向へ少しずらして描く。
灯り側の縁が内側に残り三日月状のハイライトになる。
【罠】灯りの方向へずらすと、ハイライトが物の裏側に出て、かえって平たく見える
- 【色温度】灯りは暖色、地と縁の光は寒色に寄せる。同じ明暗でも奥行きが出る
- 【空気】光の当たる範囲にだけ塵を浮かべ、灯りからの距離で明るさと瞬きを変える。
粒ごとにグラデーションを作らず、32px程度のスプライトを1枚焼いて drawImage で使い回す
- 【灯り】芯の白熱に加え、レンズのような十字の光条を2本(横を長く、縦を短く)
- 【艶】紙全体に斜めの淡いハイライトと、周辺光量落ち(vignette)を薄く掛ける
- 粒子(グレイン)は 168px 程度のノイズタイルを1枚作り、overlay で薄く敷く
- 隅に小さく「印画 NN / 調色名 / 露光 00:00」を置く(等幅でも明朝でもない、細いサンセリフで)
- ネオン・rainbow・紫グラデ・glassmorphism は使わない
# インタラクション
- ポインタを動かすと灯りが追う(補間して滑らかに)
- クリック/タップで次の印画へ(置くものと調色が同時に変わり、露光はリセット)
- 1.6秒ほど操作が無ければ、灯りをゆっくり漂わせて露光を続ける(長時間露光)
- 開幕は無地だと寂しいので、光を一周漂わせた助走を先に焼いてから始める
# 品質要件(production)
- 全画面オーバーレイ。CLOSE ボタンと ESC で終了。多重起動を防ぐ
- レスポンシブ: リサイズでキャンバスを作り直す。
【重要】作り直したら、それまでの露光を拡大コピーして引き継ぐこと(作り直した瞬間に像が消える)
- devicePixelRatio は最大2でクランプ。モバイルは1フレームの焼き付け点数を減らす
- prefers-reduced-motion: アニメーションせず、十分に露光した1枚を描いて静止させる
- 終了時に requestAnimationFrame を停止し、全リスナを解除し、CanvasをDOMから除去する
- Canvas 2D が使えない環境では静かに何もしない。本文の閲覧を妨げない
CUSTOMIZE
- 置くもの ── 遮蔽物の多角形を差し替えれば、何でも印画できます。自社ロゴの輪郭を多角形にして置けば、光を動かすうちにロゴが浮かび上がります(線分数だけ注意)。
- 調色 ── 地・紙・灯りの3色。青写真を銅版調や白金調にするだけで、時代が変わります。
- 光源の大きさ ── 半影の量です。大きくすると輪郭がぼけた柔らかい印画に、小さくすると硬く鋭い影になります。写真の引き伸ばし機と同じ挙動です。
- 減衰の距離 ── 短くすると光が手元しか届かず、探るような暗い画面に。長くすると部屋全体が均一に明るくなります。
- 1回の露光量 ── 大きくすると数秒で真っ白に焼き上がり、小さくするとじわじわ現れます。焦らすなら小さく。
- 粒子 ── ノイズの強さ。落とせば清潔なベクター調、上げれば粗い増感現像のような質感になります。
- 立体感の強さ ── 接地影の線幅と、縁の光の太さ・ずらし量。強くすると立体的なオブジェに、弱くすると平らな切り絵になります。同じ影のまま、質感だけを振れます。
- きらめき ── 塵の数と瞬きの速さ。減らせば静謐な標本、増やせば埃の舞う映写室のような空気になります。
PERFORMANCE
今回のコストは、ほぼすべて遮蔽物の線分数で決まります。光線の数が線分数に比例し、その1本ごとに全線分との交点を調べるので、素直に書くと線分数の2乗で効いてきます。
- 実測 ── 172線分の場面で、可視多角形1枚あたり0.64ms(Node実測)。1フレーム4枚で約2.6msです。
- 形の分割を落とす ── 葉を11分割から8分割に落としました。線分は220本から172本に減り、1枚あたり1.0ms → 0.64ms。輪郭は見た目にはほとんど変わりません。 曲線の滑らかさに払っていたコストが、そのまま影の計算に乗っていたわけです。
- 計算の使い回し ── 前述のとおり、焼き付け用の多角形を現在の灯りにも使い、1フレーム7枚 → 4枚に。
- 塗りはクリップより直塗り ──
clip()してから全画面を塗るより、多角形そのものをグラデーションで塗るほうが、無駄な塗り面積が出ません。 - 助走は一度きり ── 開幕とクリック時に150回ぶん焼きます(約0.1秒)。毎フレームの負担ではないので、ここは素直に回しています。
- 塵はスプライトを1枚焼いて使い回す ── 130粒ぶんの放射グラデーションを毎フレーム作ると馬鹿になりません。32pxのスプライトを起動時に1枚だけ作り、あとは
drawImageで拡大して並べています。 - モバイル ── 1フレームの焼き付けを3点に、塵を70粒に落とします。半影の滑らかさは少し落ちますが、蓄積するので数フレームで追いつきます。
- 後始末 ── 終了時にRAFを停止し、全リスナを解除してCanvasを除去。開閉を繰り返してもリークしません。
WHAT AI DID / WHAT HUMAN DIRECTION MATTERS
可視多角形は、計算幾何では教科書に載っている題材です。「Canvasで光源から見える範囲を角度走査で求めて」と言えば、動くものはすぐ出てきます。 ±εの3本撃ちも、画面の枠での打ち切りも、AIは知っています。ここは完全にAIの領分でした。
その上で、人間が決めたのは次のような点です。
- そもそも「物体を描かない」という題材を選ぶこと
- 一様塗りをやめて距離で減衰させる判断 ── 物理的に正しくすることが、そのまま絵の説得力になった
- 現在の灯りを弱くする判断 ── 最初は明るく作っていて、たしかに派手なのですが、焼き付いた像が飛んでしまい、3D CGのような絵になっていました。主役は履歴のほうです
- 面光源の半影を、サンプル数ではなく時間で解くという選び方
- 葉の分割数を落として、見た目を変えずに計算量を半分にすること
- 消え残りを「なんとなく灰色」で済ませず、画素値を読んで原因を特定すること
- 影が正しいだけでは平らに見えると気づき、接地影と縁の光を足す判断(と、ずらす向きを逆にすること)
最後の一つは、AIに任せきりにできない部分の典型でした。バグの症状を言葉で説明してAIに投げても、返ってくるのは一般論です。どの数字を読めば規則性が出るかを決めるのは、まだ人の仕事です。 8枚の葉の中心を読む、と決めた時点で答えは出ていました。
9回続けて、実装の速さはもう問題ではなくなりました。残っているのは、何を作るかを決めること、そして出てきたものが良くない理由を言葉にすることです。「なんか嘘っぽい」を「距離で減衰していない」に翻訳できるかどうか。そこだけは、手が速くなっても短縮されません。
NEXT
粒子、文字、線、物理、計算、製版、音、分割、そして今回の光と影。
次回は、幾何の証明が組み上がっていく線か、スクロール連動あたりを考えています。
今回のプロンプトも丸ごとコピーして使えます。置くものを自社のロゴの輪郭に差し替えれば、光を動かすうちに社名が浮かび上がる1枚になります。描かずに見せるというのは、なかなか贅沢な表現です。
レガシーPHPシステムの保守・引き継ぎのご相談を受け付けています。
初回のご相談・ヒアリングは無料です。