■40個目「硝子のアトリエ」
こんにちは、AIと作る100のゲーム実験室の みー です。(^_^)/
今回は、ちょっと不思議な“二重のお絵描き体験”ができるゲームを作ってみました。
お絵描きといっても、ただ一枚の紙に描くわけではありません。
なんと、キャンバスが「奥の風景用」と「手前のキャラクター用」の2枚に分かれているんです!
今回こだわったのは、ずばり「ガラス越しの奥行き」です。(=v=*)
「少しゆがみのあるレトロなガラスケース。
その向こう側をそっと覗き込むようなワクワク感を、ブラウザ上で再現できないかな?」
そんな遊び心から生まれたアトリエになっています。
2枚の絵を描き終わると……分厚いガラスの額縁の中で、2つの絵が重なり合って一つの立体的な作品が完成します。(^^*)♪
完成した絵は、画面をマウスでぐりぐりと引っ張ることで、手前の絵と奥の絵が違うスピードで動きます。本当にそこに“空間”があるみたいで、思わずずっと眺めてしまいます。
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^^*)今日もゲームを作ります。 今回はお絵描きゲームの5つ目、「ガラス越しアート体験」です。
このゲームでは、2枚の絵を描くキャンバスがあります。 1枚目に描いた絵は、3つ目の表示画面では“背景”として同時に反映されます。 2枚目のキャンバスに描くと、3つ目の画面では“手前の絵”として重なって表示されます。
そして、1枚目と2枚目の間には少し空間があるイメージ。 3つ目の表示画面はマウスドラッグで動かせて、 まるでガラス越しに絵を覗き込んでいるような奥行きを感じられるようにしたいんです。
AI: 以前お話しした「3D視差(パララックス)」のアイデアを、 今回のお絵描き体験にとても自然な形で活かせていますね。 “2枚の絵の間に空間を持たせて、ドラッグで視点が動く”という仕組みは、 まさに視差効果を最大限に使った、没入感のあるアート体験になりそうです。
みー:(*^o^)イメージに近いです。
ただ、1枚目と2枚目の切り替え時に、 隣のキャンバスに線が引かれてしまうことがありました。
それから、線の太さ調整や消しゴムも欲しいですね。
AI: キャンバスをまたいで線が引かれてしまう現象、ご不便をおかけしました。 調べてみると、マウスの動きを“画面全体”で拾っていたため、 1枚目と2枚目のキャンバスが同時に反応してしまっていたのが原因でした。
そこで今回は、 「それぞれのキャンバスの中だけで確実に反応する」ように配線を組み直しました。 これで隣の絵に干渉することはありません。 もちろん、これまでの「ガラス越しの立体感」や「ドラッグでの視点移動」などの機能はそのまま残しています。
さらにご要望の 「線の太さ調整(スライダー)」と「消しゴム機能」も追加しました。 消しゴムは白く塗るのではなく、透明に戻す仕様なので、後ろの絵がちゃんと透けます。 動作確認もバッチリです。
〈提案したらすぐ理想の形に近づいてきて、気づいたら絵を描く方に夢中になっていました〉(^^;)
■ 実際の動作
まずは、AIと相談しながら作ってもらった 初期状態のゲーム画面 です。

(画像:AIが最初に作った基本のベース画面+自分の絵)
こちらが修正版 です。 ペンの太さ調整や消しゴムも追加されて、描き心地がぐっと良くなりました。

(画像:AIが出してくれたイメージ+自分の絵)
上の2枚のキャンバスに絵を描いていくと、 下の表示画面に“奥行きのある一枚の作品”として合成されます。
マウスでぐりぐり動かすと、 手前の絵と奥の絵が少し違うスピードで動いて、 まるで本当にガラス越しに覗き込んでいるような立体感が楽しめます。

(画像:保存機能付き修正版)
■ コードのポイント
今回の記事では、AI と相談しながら作った 「硝子のアトリエ」 の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。 コードも難しい部分はほとんどないので、気軽に試してみてくださいね。
【遊ぶまでの手順】
1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「garasu.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。
【遊び方と操作方法】
操作はとっても簡単です!
■ 2つのキャンバスでお絵描き
画面には 「1. 奥の絵(背景)」 と 「2. 手前の絵(前景)」 という2つの四角いキャンバスが並んでいます。
例えば、 奥のキャンバスに「青空と遠くのお山」、 手前のキャンバスに「大きな木とキャラクター」 を描いてみてください。(^_^)
■ 便利なツールたち
それぞれのキャンバスの下には、小さなお絵描き道具箱があります。
- カラーピッカー:好きな色を自由に選べます。
- スライダー:線の太さを細くしたり太くしたり調整できます。
- 消しゴム:間違えても大丈夫。奥の絵が透けて見えるように、ちゃんと「透明」に戻せます。
■ ガラス越しの魔法(ここが一番のポイント!)
絵を描いていくと、一番下の大きな 「完成画面」 に、 2つの絵が重なって同時に表示されていきます。
この完成画面を、マウスや指で ぐりぐりっとドラッグ してみてください。
手前の絵が大きく、奥の絵がゆっくり動くので、 まるで ガラスの箱庭を覗き込んでいるような不思議な立体感 が楽しめます。
■ 作品を保存しよう
「ここだ!」という素敵な視点(角度)が見つかったら、 一番下の 「画像を保存する」 ボタンをポチッ。
いま見えているままの画像が、パソコンやスマホに保存されます。
絵が苦手な方でも、丸や線を重ねるだけで 小さなアート空間 が出来上がります。 ぜひ、あなただけの素敵なガラスの世界を描いてみてくださいね。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>硝子のアトリエ - ガラス越しアート体験</title> <style> body { margin: 0; padding: 20px; background-color: #f0f0e8; font-family: 'Hiragino Kaku Gothic ProN', sans-serif; display: flex; flex-direction: column; align-items: center; } h1 { color: #5d4037; margin-bottom: 10px; } .editor-container { display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; justify-content: center; } .canvas-wrapper { text-align: center; background: #fff; padding: 10px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } h3 { margin-top: 0; color: #6d4c41; font-size: 16px; } canvas.drawing-area { border: 2px solid #a1887f; background: white; cursor: crosshair; touch-action: none; } #displayCanvas { border: 4px solid #5d4037; background: #e0e0e0; cursor: grab; box-shadow: 0 10px 20px rgba(0,0,0,0.2); } #displayCanvas:active { cursor: grabbing; } /* ツールバーの装飾 */ .controls { margin-top: 10px; display: flex; gap: 8px; justify-content: center; align-items: center; flex-wrap: wrap; } .tool-btn { padding: 5px 12px; cursor: pointer; background-color: #e0e0e0; color: #333; border: 2px solid transparent; border-radius: 4px; font-weight: bold; transition: 0.2s; } .tool-btn:hover { background-color: #d6ccc8; } .tool-btn.active { background-color: #8d6e63; color: white; border-color: #5d4037; } .color-picker { width: 35px; height: 35px; border: 1px solid #ccc; cursor: pointer; padding: 0; border-radius: 4px; } .weight-slider { width: 80px; cursor: pointer; } /* ボタン類の装飾 */ .action-buttons { margin-top: 20px; display: flex; gap: 15px; } .action-btn { color: white; padding: 10px 20px; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; font-size: 16px; transition: 0.2s; } #clearAll { background-color: #d32f2f; } #clearAll:hover { background-color: #ef5350; } #saveImage { background-color: #2e7d32; } #saveImage:hover { background-color: #4caf50; } /* 3D表示エリアの装飾 */ .display-wrapper { position: relative; padding: 20px; background: #d7ccc8; border-radius: 8px; border: 1px solid #a1887f; margin-top: 10px; } .display-wrapper::after { content: "※画面をドラッグして視点を移動"; position: absolute; bottom: 5px; right: 10px; font-size: 12px; color: #795548; } </style></head><body> <h1>硝子のアトリエ</h1> <div class="editor-container"> <div class="canvas-wrapper"> <h3>1. 奥の絵(背景)</h3> <canvas id="canvas1" class="drawing-area" width="300" height="400"></canvas> <div class="controls"> <input type="color" class="color-picker" id="color1" value="#81d4fa"> <input type="range" class="weight-slider" id="weight1" min="1" max="40" value="10" title="線の太さ"> <button id="pen1" class="tool-btn active" onclick="setTool(1, 'pen')">ペン</button> <button id="eraser1" class="tool-btn" onclick="setTool(1, 'eraser')">消しゴム</button> <button class="tool-btn" onclick="clearCanvas(1)">全消去</button> </div> </div> <div class="canvas-wrapper"> <h3>2. 手前の絵(前景)</h3> <canvas id="canvas2" class="drawing-area" width="300" height="400"></canvas> <div class="controls"> <input type="color" class="color-picker" id="color2" value="#388e3c"> <input type="range" class="weight-slider" id="weight2" min="1" max="40" value="10" title="線の太さ"> <button id="pen2" class="tool-btn active" onclick="setTool(2, 'pen')">ペン</button> <button id="eraser2" class="tool-btn" onclick="setTool(2, 'eraser')">消しゴム</button> <button class="tool-btn" onclick="clearCanvas(2)">全消去</button> </div> </div> </div> <div class="display-wrapper"> <canvas id="displayCanvas" width="400" height="500"></canvas> </div> <div class="action-buttons"> <button id="saveImage" class="action-btn" onclick="downloadImage()">画像を保存する</button> <button id="clearAll" class="action-btn" onclick="clearAllCanvases()">作品をすべてリセット</button> </div><script>// --- 設定と状態 ---const CW = 300;const CH = 400;const DW = 400;const DH = 500;const PARALLAX_FACTOR_BACK = 0.1;const PARALLAX_FACTOR_FRONT = 0.5;const state = { isDrawing: false, viewOffset: { x: 0, y: 0 }, isDraggingView: false, dragStart: { x: 0, y: 0 }, tools: { 1: 'pen', 2: 'pen' }};const canvases = { 1: document.getElementById('canvas1'), 2: document.getElementById('canvas2'), display: document.getElementById('displayCanvas')};const ctxs = { 1: canvases[1].getContext('2d'), 2: canvases[2].getContext('2d'), display: canvases.display.getContext('2d')};const colorPickers = { 1: document.getElementById('color1'), 2: document.getElementById('color2')};const weightSliders = { 1: document.getElementById('weight1'), 2: document.getElementById('weight2')};// --- 初期化 ---function init() { setupDrawingEvents(1); setupDrawingEvents(2); setupDisplayEvents(); [ctxs[1], ctxs[2]].forEach(ctx => { ctx.lineCap = 'round'; ctx.lineJoin = 'round'; }); updateDisplay();}// --- ツール切り替え機能 ---function setTool(id, tool) { state.tools[id] = tool; document.getElementById(`pen${id}`).classList.toggle('active', tool === 'pen'); document.getElementById(`eraser${id}`).classList.toggle('active', tool === 'eraser');}// --- お絵描き機能の実装 ---function setupDrawingEvents(id) { const canvas = canvases[id]; const ctx = ctxs[id]; const getXY = (e) => { const rect = canvas.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return { x: clientX - rect.left, y: clientY - rect.top }; }; const startDrawing = (e) => { state.isDrawing = true; ctx.beginPath(); if (state.tools[id] === 'eraser') { ctx.globalCompositeOperation = 'destination-out'; } else { ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = colorPickers[id].value; } ctx.lineWidth = weightSliders[id].value; const pos = getXY(e); ctx.moveTo(pos.x, pos.y); ctx.lineTo(pos.x, pos.y); ctx.stroke(); updateDisplay(); }; const draw = (e) => { if (!state.isDrawing) return; e.preventDefault(); const pos = getXY(e); ctx.lineTo(pos.x, pos.y); ctx.stroke(); updateDisplay(); }; const stopDrawing = () => { if(state.isDrawing) { ctx.closePath(); state.isDrawing = false; } }; canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseleave', stopDrawing); canvas.addEventListener('touchstart', startDrawing); canvas.addEventListener('touchmove', draw, { passive: false }); canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing);}// --- 表示キャンバスのイベント(ドラッグ視点移動) ---function setupDisplayEvents() { const canvas = canvases.display; const getXY = (e) => { const rect = canvas.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return { x: clientX - rect.left, y: clientY - rect.top }; }; const startDragging = (e) => { state.isDraggingView = true; const pos = getXY(e); state.dragStart = { x: pos.x - state.viewOffset.x, y: pos.y - state.viewOffset.y }; }; const drag = (e) => { if (!state.isDraggingView) return; e.preventDefault(); const pos = getXY(e); const newX = pos.x - state.dragStart.x; const newY = pos.y - state.dragStart.y; state.viewOffset.x = Math.max(-60, Math.min(60, newX)); state.viewOffset.y = Math.max(-60, Math.min(60, newY)); updateDisplay(); }; const stopDragging = () => { state.isDraggingView = false; }; canvas.addEventListener('mousedown', startDragging); window.addEventListener('mousemove', drag); window.addEventListener('mouseup', stopDragging); canvas.addEventListener('touchstart', startDragging); window.addEventListener('touchmove', drag, { passive: false }); window.addEventListener('touchend', stopDragging);}// --- 表示の更新(パララックス処理) ---function updateDisplay() { const ctx = ctxs.display; ctx.fillStyle = "#f5f5f5"; ctx.fillRect(0, 0, DW, DH); const offsetX = (DW - CW) / 2; const offsetY = (DH - CH) / 2; // 1. 奥の絵(Layer 1) ctx.save(); const backX = offsetX + state.viewOffset.x * PARALLAX_FACTOR_BACK; const backY = offsetY + state.viewOffset.y * PARALLAX_FACTOR_BACK; ctx.globalAlpha = 0.9; ctx.drawImage(canvases[1], backX, backY); ctx.restore(); // 2. ガラスの厚みの表現(影) ctx.save(); ctx.shadowColor = "rgba(0,0,0,0.3)"; ctx.shadowBlur = 15; ctx.shadowOffsetX = state.viewOffset.x * 0.2; ctx.shadowOffsetY = state.viewOffset.y * 0.2 + 5; const shadowX = offsetX + state.viewOffset.x * PARALLAX_FACTOR_FRONT; const shadowY = offsetY + state.viewOffset.y * PARALLAX_FACTOR_FRONT; ctx.fillStyle = "rgba(255,255,255,0.01)"; ctx.fillRect(shadowX, shadowY, CW, CH); ctx.restore(); // 3. 手前の絵(Layer 2) ctx.save(); const frontX = offsetX + state.viewOffset.x * PARALLAX_FACTOR_FRONT; const frontY = offsetY + state.viewOffset.y * PARALLAX_FACTOR_FRONT; ctx.drawImage(canvases[2], frontX, frontY); ctx.restore(); // 4. ガラスの表面の光沢 const gradient = ctx.createLinearGradient(0, 0, DW, DH); gradient.addColorStop(0, "rgba(255,255,255,0.1)"); gradient.addColorStop(0.5, "rgba(255,255,255,0)"); gradient.addColorStop(1, "rgba(255,255,255,0.05)"); ctx.fillStyle = gradient; ctx.fillRect(0, 0, DW, DH);}// --- 画像ダウンロード機能 ---function downloadImage() { // 3枚目のキャンバス(表示用)のデータURLを取得 const dataURL = canvases.display.toDataURL('image/png'); // 一時的なリンク要素を作成してダウンロードを発火 const link = document.createElement('a'); link.href = dataURL; link.download = 'glass_art.png'; // 保存されるファイル名 document.body.appendChild(link); link.click(); document.body.removeChild(link);}// --- ユーティリティ ---function clearCanvas(id) { ctxs[id].clearRect(0, 0, CW, CH); updateDisplay();}function clearAllCanvases() { clearCanvas(1); clearCanvas(2); state.viewOffset = { x: 0, y: 0 }; updateDisplay();}init();</script></body></html>
■ 今日の学び
今回はお絵描きゲームを作っている途中で、 気づいたら 絵を描くことの方に夢中になってしまいました(@_@;)
#28 のお絵描きゲームでも書きましたが、 絵を描くのが好き なんです(^v^) ……でも、あまり上手ではありません(つv<`)
それでも、作っているゲームには 自分の「好き」がたくさん散りばめられています。
この実験室のテーマは、 「好き・楽しい・おもしろそう」。
そして、目指しているのは “100個を作りきること”そのものではなく、 100の先に広がる世界 なんだと思っています。
気づけば 100 個が近づいてきて、 「1000 個も目指してみようかな……?」 と、ふと未来を想像するようになりました。
ゲーム作りの実験も、 AI と一緒に少しずつ成長しているはず(>v<*)
これまで作ってきたゲームたちも、 もっといろんなアイデアを盛り込んで、 みんなが楽しめる作品に育てていきたい気持ちです。
■ 次回は、アイドル猫が再登場!
次回は、あのサインスタンプゲームの主役だった アイドル猫のバラエティチャレンジです。 バニー先輩も登場します。
次回もお楽しみに。(^v^)ノシ
次の実験はこちら(拾って集めて打ってはじけて):「ねこ花火」
今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

コメントを残す