実験#28:縮小とコレクション(プラパン工房)

■28個目「プラパン工房

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、お絵描きゲーム第3弾「プラパン工房」のゲームです。
“縮む楽しさ”をテーマにした実験になっています。

左の枠内で自由に絵が描けます。 ”Φ(・_・*)ヵ≠ヵ≠
仕上がった絵には、名前札を付けることができます。

「焼いて完成」ボタンを押すと、絵が2分の1サイズに縮んで、
右側の棚にプラパンとして飾られます。
棚の壁紙は好きな柄に変更できます。

飾ったプラパンは、マウスでつつくとゆらゆら揺れます。
作ったプラパンは保存もできます。

小さく縮んでいく感じが楽しい、実験らしいゲームになりました。
ぜひ遊んでみてください。(*^v^*)

■ AIとのやりとりで面白かった部分

(^^*)今日もゲームを作ります。今日は創作系のゲームにします。 マウスで絵を描いて、焼くと2分の1サイズになる「プラパンゲーム」。 線の色や太さを調整できて、囲んだ部分は塗りつぶしボタンで色を入れられます。 消しゴム、一つ戻る、全部消すなどの操作もあります。

作ったプラパンは少し厚みが出て、色も濃くなる感じ。 保存して飾れる箱があって、キーホルダー風のひもを付けて引っ掛けることもできます。 引っ掛けたプラパンは、マウスでつつくと揺れます。

(AI)こんにちは!今日も創作意欲がわく素敵なアイデアですね。 オーブンで焼くと「ギュ〜ッ」と縮んで色が濃くなる、あのプラ板のワクワク感を再現したゲームを作成しました。

(^_^) 透明塗りつぶしもできるといいね。

(AI)透明塗りつぶし(くり抜き)、すごく良いアイデアですね。
ステンドグラス風の透けるプラ板や、真ん中に穴の空いたドーナツ型のプラ板を作る時に大活躍します。

もちろん、前回までの「壁紙変更」「名前ラベル」「ドラッグ&ドロップ」「ゴミ箱」「揺れる物理演算」などの機能は一つも削ることなく全て残してありますのでご安心くださいね。

ちなみにプラ板が縮むのは、工場で作られる時に「無理やり引き伸ばされて固められている」からなんです。
熱を加えると元の形に戻ろうとして、面積が4〜6分の1に縮み、厚みが4〜6倍になります。
元の姿に戻ろうとする力を利用して遊んでいると思うと、なんだか可愛いですよね。

(^^*)作った作品のコレクション全体をセーブできるようにしましょう。

(AI)こうやって少しずつゲームが育っていくのを見るのは、私としてもワクワクしますよ。 せっかく作った作品ですから、ブラウザを閉じても消えないように「セーブ&ロード」機能を追加しましょう!

プラ板雑学をもう一つ。 プラ板に絵を描く時、油性ペンが定番ですが、表面を紙やすりで軽くこすると「色鉛筆」でも描けるようになります。 焼くと色がギュッと濃縮されて、ふんわりしたタッチが残るアンティーク風の作品になるんですよ。

それでは、これまでの機能は一つも削らず全て残したまま、コレクションを保存できる「プラパン・クリエイター V5」をお出しします!

■ 実際の動作

 一番最初に、AI との相談で作ってもらったゲームの初期状態です。
 ほぼ出来上がりな感じですが、仕上げるとコレクションケースから
 はみ出てしまい、そこから動かすことはできません。
(画像:AIが最初に作った状態)

 少し改善された V2 です。左で描いた雑な絵も、  小さくするとなんだかいい感じになります。

(画像:V2の状態)

 背景の種類を変えられるようにしました。
 作品名もつけられるように変更しています。

(画像:V3の状態)

 V4 になると、飾り棚の背景も変えられるようになりました。

(画像:V4の状態)

 V5 では、保存とロード機能を付け足しました。

(画像:V5の状態)

■ コードのポイント

今回の記事では、AI と相談しながら作った
「プラパン工房」の HTML コードを公開します。

ブラウザだけで動く、とてもシンプルなゲームです。
コードといっても難しい部分はありません。
貼り付けて保存するだけで動くので、気軽に試してみてください。

PC の「メモ帳」(※Windows の場合)を開き、
下のコードをコピーして貼り付け、「purapan.html」など
好きな名前で保存してください。

保存したファイルをブラウザで開くだけで遊べます。

【操作方法】
・マウスで絵を描きます
・線の色・太さを変更できます
・囲んだ部分は塗りつぶしボタンで色を入れられます
・消しゴム、一つ戻る、全部消すボタンがあります
・「焼いて完成」を押すと絵が 2 分の 1 サイズに縮み、棚に飾られます

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>プラパン・クリエイター V5</title>
<style>
body { background-color: #e0e7ff; font-family: 'Hiragino Kaku Gothic ProN', sans-serif; display: flex; flex-direction: column; align-items: center; margin: 0; padding: 20px; overflow-x: hidden;}
h2 { color: #4338ca; margin-bottom: 10px; text-shadow: 1px 1px 2px white; }
.main-container { display: flex; gap: 30px; align-items: flex-start; }
/* 制作エリア */
#workspace { background: #f8fafc; padding: 20px; border-radius: 15px; border: 4px solid #94a3b8; box-shadow: 0 10px 25px rgba(0,0,0,0.1); width: 400px; }
canvas {
background-color: #ffffff;
background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5), linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5);
background-size: 20px 20px; background-position: 0 0, 10px 10px;
border: 1px solid #ddd; cursor: crosshair; touch-action: none; image-rendering: pixelated; width: 100%; height: auto;
}
.controls { margin-top: 15px; display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
button { padding: 8px 4px; cursor: pointer; border-radius: 8px; border: 1px solid #ccc; background: white; transition: 0.2s; font-size: 11px; font-weight: bold; }
button:hover { background: #f1f5f9; transform: translateY(-2px); }
button.active { background: #4338ca; color: white; border-color: #312e81; }
.tool-group { display: flex; align-items: center; gap: 10px; grid-column: span 5; background: #eee; padding: 5px; border-radius: 5px; }
/* 特大展示ボックス */
#display-outer { display: flex; flex-direction: column; align-items: center; gap: 10px; }
/* セーブ・ロードのUI */
.system-controls { display: flex; gap: 10px; align-items: center; background: #fff; padding: 10px 15px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); border: 2px solid #cbd5e1; width: 100%; box-sizing: border-box; justify-content: space-between;}
.sys-btn { padding: 6px 12px; border-radius: 6px; border: none; font-weight: bold; cursor: pointer; color: white; font-size: 12px; }
.btn-save { background: #10b981; } .btn-save:hover { background: #059669; }
.btn-load { background: #3b82f6; } .btn-load:hover { background: #2563eb; }
#display-case { width: 600px; height: 500px; border: 12px solid #78350f; border-radius: 15px; position: relative; box-shadow: inset 0 0 60px rgba(0,0,0,0.6); overflow: hidden; transition: background 0.3s, border-color 0.3s; }
/* 壁紙パターン */
.bg-wood { background: #451a03; border-color: #78350f !important; }
.bg-brick { background-color: #9d3a14; background-image: linear-gradient(335deg, #71240c 23px, transparent 23px), linear-gradient(155deg, #71240c 23px, transparent 23px), linear-gradient(335deg, #71240c 23px, transparent 23px), linear-gradient(155deg, #71240c 23px, transparent 23px); background-size: 58px 58px; background-position: 0px 2px, 4px 35px, 29px 31px, 34px 6px; border-color: #471505 !important; }
.bg-dots { background-color: #fca5a5; background-image: radial-gradient(#ffffff 20%, transparent 20%), radial-gradient(#ffffff 20%, transparent 20%); background-size: 30px 30px; background-position: 0 0, 15px 15px; border-color: #b91c1c !important; }
.bg-check { background-color: #93c5fd; background-image: linear-gradient(45deg, #ffffff 25%, transparent 25%), linear-gradient(-45deg, #ffffff 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ffffff 75%), linear-gradient(-45deg, transparent 75%, #ffffff 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; border-color: #1e3a8a !important; }
.shelf { position: absolute; width: 100%; height: 3px; background: rgba(0,0,0,0.3); box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
/* ゴミ箱 */
#trash-bin { width: 80px; height: 80px; background: #64748b; border: 4px solid #475569; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 30px; position: relative; transition: 0.3s; color: white; }
#trash-bin.drag-over { background: #ef4444; transform: scale(1.1); }
/* プラパンアイテム */
.plapan-item { position: absolute; cursor: grab; transform-origin: top center; transition: transform 0.1s ease-out; filter: drop-shadow(3px 5px 4px rgba(0,0,0,0.4)); user-select: none; display: flex; flex-direction: column; align-items: center; }
.plapan-item:active { cursor: grabbing; }
.plapan-string { width: 4px; height: 25px; background: #fcd34d; margin: 0 auto; position: relative; }
.plapan-string::before { content: ''; position: absolute; top: -5px; left: -1px; width: 6px; height: 6px; background: #9ca3af; border-radius: 50%; }
.plapan-body { border-radius: 4px; border: 1px solid rgba(255,255,255,0.4); pointer-events: none; display: block; }
.plapan-label { display: block; background: rgba(255,255,255,0.8); color: #333; font-size: 10px; padding: 2px 5px; border-radius: 4px; margin-top: 5px; text-align: center; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; box-shadow: 1px 1px 2px rgba(0,0,0,0.2); pointer-events: none; }
.cutting-effect { position: fixed; pointer-events: none; width: 100%; height: 100%; top: 0; left: 0; background: white; opacity: 0; transition: opacity 0.5s; z-index: 100; }
</style>
</head>
<body>
<h2>🎨 プラパン・クリエイター V5</h2>
<div class="main-container">
<div id="workspace">
<canvas id="canvas" width="400" height="400"></canvas>
<div class="controls">
<div class="tool-group">
🎨 <input type="color" id="colorPicker" value="#ff0000">
📏 <input type="range" id="sizePicker" min="1" max="20" value="5">
</div>
<div class="tool-group">
🏷️ <input type="text" id="itemName" placeholder="作品名(任意)" style="width: 150px; padding: 5px; border-radius: 5px; border: 1px solid #ccc;">
</div>
<button id="drawBtn" class="active">ペン</button>
<button id="fillBtn">塗りつぶし</button>
<button id="transFillBtn" style="color: #0284c7;">透明塗り</button>
<button id="eraserBtn">消しゴム</button>
<button id="undoBtn">戻る</button>
<button id="clearBtn" style="grid-column: span 2;">全部消す</button>
<button id="bakeBtn" style="grid-column: span 3; background: #f59e0b; color: white; font-weight: bold; font-size: 12px;">焼いて完成!(1/2)</button>
</div>
</div>
<div id="display-outer">
<div class="system-controls">
<div>
<b>背景:</b>
<select id="bgSelector">
<option value="wood">木目</option>
<option value="brick">レンガ</option>
<option value="dots">水玉</option>
<option value="check">格子</option>
</select>
</div>
<div style="display: flex; gap: 5px;">
<button class="sys-btn btn-save" onclick="saveCollection()">💾 セーブ</button>
<button class="sys-btn btn-load" onclick="loadCollection()">📂 ロード</button>
</div>
</div>
<div id="display-case" class="bg-wood">
<div class="shelf" style="top: 100px;"></div>
<div class="shelf" style="top: 250px;"></div>
<div class="shelf" style="top: 400px;"></div>
</div>
<div id="trash-bin">🗑️</div>
<div style="font-size: 12px; color: #475569;">※ゴミ箱に重ねると捨てられます</div>
</div>
</div>
<div id="flash" class="cutting-effect"></div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const displayCase = document.getElementById('display-case');
const trashBin = document.getElementById('trash-bin');
const undoStack = [];
let drawing = false;
let mode = 'draw';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
saveState();
function saveState() {
if (undoStack.length > 15) undoStack.shift();
undoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
}
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
window.addEventListener('mouseup', stopDrawing);
function startDrawing(e) {
if (mode === 'fill' || mode === 'transFill') { fill(e.offsetX, e.offsetY); return; }
drawing = true;
saveState();
draw(e);
}
function draw(e) {
if (!drawing || mode === 'fill' || mode === 'transFill') return;
ctx.lineWidth = document.getElementById('sizePicker').value;
ctx.strokeStyle = (mode === 'eraser') ? '#ffffff' : document.getElementById('colorPicker').value;
ctx.globalCompositeOperation = (mode === 'eraser') ? 'destination-out' : 'source-over';
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(e.offsetX, e.offsetY);
}
function stopDrawing() { drawing = false; ctx.beginPath(); }
function fill(startX, startY) {
saveState();
const targetColor = ctx.getImageData(startX, startY, 1, 1).data;
let fillColor, fillAlpha;
if (mode === 'transFill') {
fillAlpha = 0;
if (targetColor[3] === 0) return;
} else {
fillColor = hexToRgb(document.getElementById('colorPicker').value);
fillAlpha = 255;
if (colorsMatch(targetColor, [fillColor.r, fillColor.g, fillColor.b, 255])) return;
}
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
const stack = [[startX, startY]];
while (stack.length > 0) {
const [x, y] = stack.pop();
const pos = (y * canvas.width + x) * 4;
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) continue;
if (!colorsMatch([pixels[pos], pixels[pos+1], pixels[pos+2], pixels[pos+3]], targetColor)) continue;
if (mode === 'transFill') {
pixels[pos] = 0; pixels[pos+1] = 0; pixels[pos+2] = 0; pixels[pos+3] = 0;
} else {
pixels[pos] = fillColor.r; pixels[pos+1] = fillColor.g; pixels[pos+2] = fillColor.b; pixels[pos+3] = 255;
}
stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
}
ctx.putImageData(imageData, 0, 0);
}
function hexToRgb(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return { r, g, b }; }
function colorsMatch(c1, c2) { return Math.abs(c1[0] - c2[0]) < 5 && Math.abs(c1[1] - c2[1]) < 5 && Math.abs(c1[2] - c2[2]) < 5 && Math.abs(c1[3] - c2[3]) < 5; }
document.getElementById('drawBtn').onclick = () => setMode('draw');
document.getElementById('eraserBtn').onclick = () => setMode('eraser');
document.getElementById('fillBtn').onclick = () => setMode('fill');
document.getElementById('transFillBtn').onclick = () => setMode('transFill');
function setMode(newMode) {
mode = newMode;
document.querySelectorAll('button').forEach(b => b.classList.remove('active'));
document.getElementById(newMode + 'Btn').classList.add('active');
}
document.getElementById('undoBtn').onclick = () => { if (undoStack.length > 0) ctx.putImageData(undoStack.pop(), 0, 0); };
document.getElementById('clearBtn').onclick = () => { saveState(); ctx.clearRect(0, 0, canvas.width, canvas.height); };
const bgSelector = document.getElementById('bgSelector');
bgSelector.onchange = (e) => {
displayCase.classList.remove('bg-wood', 'bg-brick', 'bg-dots', 'bg-check');
displayCase.classList.add('bg-' + e.target.value);
};
document.getElementById('bakeBtn').onclick = () => {
const itemName = document.getElementById('itemName').value;
document.getElementById('itemName').value = '';
const flash = document.getElementById('flash');
flash.style.opacity = '1';
setTimeout(() => flash.style.opacity = '0', 300);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width / 2;
tempCanvas.height = canvas.height / 2;
const tCtx = tempCanvas.getContext('2d');
tCtx.globalAlpha = 0.8;
tCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height);
tCtx.globalCompositeOperation = 'multiply';
tCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height);
createPlapan(tempCanvas.toDataURL(), itemName);
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
function escapeHtml(str) { return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); }
// V5修正:位置情報を引数で受け取れるように変更
function createPlapan(imgSrc, name, left = '50px', top = '50px') {
const container = document.createElement('div');
container.className = 'plapan-item';
container.style.left = left;
container.style.top = top;
let innerHTML = `<div class="plapan-string"></div><img src="${imgSrc}" class="plapan-body">`;
if (name) {
innerHTML += `<span class="plapan-label">${escapeHtml(name)}</span>`;
}
container.innerHTML = innerHTML;
let angle = 0, velocity = 0, isDragging = false;
let offsetX, offsetY;
function animate() {
if (!isDragging) {
velocity -= angle * 0.1;
velocity *= 0.95;
angle += velocity;
container.style.transform = `rotate(${angle}deg)`;
}
requestAnimationFrame(animate);
}
animate();
container.onmousedown = (e) => {
isDragging = true;
offsetX = e.clientX - container.offsetLeft;
offsetY = e.clientY - container.offsetTop;
container.style.zIndex = 1000;
velocity = (Math.random() - 0.5) * 15;
};
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
let x = e.clientX - offsetX;
let y = e.clientY - offsetY;
container.style.left = x + 'px';
container.style.top = y + 'px';
const binRect = trashBin.getBoundingClientRect();
if (e.clientX > binRect.left && e.clientX < binRect.right && e.clientY > binRect.top && e.clientY < binRect.bottom) {
trashBin.classList.add('drag-over');
} else {
trashBin.classList.remove('drag-over');
}
});
window.addEventListener('mouseup', (e) => {
if (!isDragging) return;
isDragging = false;
container.style.zIndex = 10;
const binRect = trashBin.getBoundingClientRect();
if (e.clientX > binRect.left && e.clientX < binRect.right && e.clientY > binRect.top && e.clientY < binRect.bottom) {
container.remove();
trashBin.classList.remove('drag-over');
}
});
displayCase.appendChild(container);
}
// V5追加:セーブ&ロード機能
function saveCollection() {
const items = [];
document.querySelectorAll('.plapan-item').forEach(el => {
const img = el.querySelector('.plapan-body').src;
const labelEl = el.querySelector('.plapan-label');
const name = labelEl ? labelEl.textContent : '';
const left = el.style.left;
const top = el.style.top;
items.push({ img, name, left, top });
});
const bg = document.getElementById('bgSelector').value;
const data = { bg: bg, items: items };
localStorage.setItem('plapan_collection', JSON.stringify(data));
alert('現在のコレクションをセーブしました!');
}
function loadCollection() {
const dataStr = localStorage.getItem('plapan_collection');
if (!dataStr) {
alert('セーブデータが見つかりません。');
return;
}
const data = JSON.parse(dataStr);
// 背景の復元
const selector = document.getElementById('bgSelector');
selector.value = data.bg;
displayCase.classList.remove('bg-wood', 'bg-brick', 'bg-dots', 'bg-check');
displayCase.classList.add('bg-' + data.bg);
// 既存のアイテムをすべて消去
document.querySelectorAll('.plapan-item').forEach(el => el.remove());
// セーブデータから復元
data.items.forEach(item => {
createPlapan(item.img, item.name, item.left, item.top);
});
}
</script>
</body>
</html>


■ 今日の学び

 絵を描くの好きです(^v^
 絵はあまり上手ではありません(つv<`)

 そこで、小さく縮んだらいい感じに見えるこの現象を使って、
 触れるコレクションの感じを再現してみました。
 つい、記事用の絵を描くのに夢中になってしまいました。

 AI が「一つも削ることなく全て残してあります」と
 太字で返事してくれています。

 きっとこれは、最初のころのゲーム作りの時に、
 作っていくうちにどんどん基本のゲームを作り変えていたことを
 覚えてくれているからだと思います。

 「削らずに作ってください(;_;)」と何度もお願いしたのを、
 AI が学習して配慮してくれた対応だと感じました(>v<*)

■ 次回は、癒しの連鎖

 連鎖と音が心地よい。
 何度も繰り返し遊びたくなるゲームです。

 次回もお楽しみに。(*^v^*)ノシ

次の実験はこちら(広がるつながる消えていく):「シーグラスチェイン」



 ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
 もし応援していただけたら、とても励みになります。

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー


AIと作る100のゲーム実験室(*^v^*)をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む