実験 #03:お絵描きゲームで実験(シュミラクラお絵描き)

■3つ目「シュミラクラお絵描き」

 シュミラクラ現象を使った「お絵描きゲーム」を作ってみました

 今回は、ちょっと変わったお絵描きゲームを作ってみました。
 テーマは 「シュミラクラ現象」

 シュミラクラ現象とは、3つの点が集まると人の顔に見えてしまう、あの錯覚のことです。 この現象をゲームに取り入れたら面白いのでは?と思い、実験的に作ってみました。

 今回の目標は、描いた絵の中で“顔として認識された部分が動き出す”ゲームを作ることです。

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

(^^*)お絵描きゲームで、色を選んで線の太さをバーで調整して、マウスの左ボタンをドラッグすると線が自由にひける。シミュラクラ現象を反映させて、目と口を認識した際にはその形が動くようになるゲーム。作れるかな。

(AI)「シミュラクラ現象」をゲーム性に落とし込むという発想、かなりセンスを感じます。3つの点(目と口)が逆三角形に並ぶと顔に見えてしまうあの現象ですね。
 
 技術的には、描いた線の座標から「目」と「口」の候補を探し出し、それらが特定の配置(逆三角形)になった時に、CSSアニメーションやJavaScriptで「まばたき」させたり「パクパク」させたりすることで実現できそうです。

 お絵描き部分は最初から動いたのですが、思っていたものとは少し違っていて、
描いた絵の上に“目と口のアイコン”が乗る形になってしまいました。

 そこで、さらにAIへ指示を出します。

(>v</)目が出るではなく、目と口と認識された図形自体が変形して動き出す感じでお願い。

(AI)

Σ( ゜o ゜)変な絵で返事が返ってきた!

■ 実際の動作

修正前の状態。絵を描くと小さな点の目や口っぽいのが出てきて少し動いてます。

今回のコードのお絵描きでは、描いた線が不思議な動きをして、線を引くほどに面白い動きをします。(*^_^*)「

■ コードのポイント

 今回の記事では、AI と相談しながら作った「シュミラクラお絵描き」の HTML コードを公開します。

 ブラウザだけで動く、とてもシンプルな仕組みです。

PC の「メモ帳」を開いて、下のコードを全部コピーして貼り付け、
「oekaki.html」などの名前で保存 → ブラウザで開く
これだけでそのまま遊べます。

マウスで自由に線を引くことができます。

 今回もAI が作ってくれたコードをそのまま載せているので、
 自分で改造したりして遊んでみてください。(^^*)

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Simulacra Master - Full Package</title>
<style>
body { margin: 0; background: #eef2f3; font-family: sans-serif; overflow: hidden; }
#menu { position: absolute; top: 10px; right: 10px; background: rgba(40, 44, 52, 0.95); color: white; padding: 15px; border-radius: 12px; z-index: 100; width: 220px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
#menu.closed { transform: translateX(250px); }
#menuToggle { position: absolute; left: -45px; top: 0; background: #282c34; color: white; border: none; padding: 12px; border-radius: 8px 0 0 8px; cursor: pointer; }
.control-group { margin-bottom: 12px; border-bottom: 1px solid #555; padding-bottom: 8px; }
label { font-size: 11px; display: block; margin-bottom: 4px; color: #aaa; }
svg { width: 100vw; height: 100vh; background: white; cursor: crosshair; touch-action: none; }
.creature-group { cursor: move; }
.outline { fill: none; stroke: #4caf50; stroke-width: 1; stroke-dasharray: 4; pointer-events: none; opacity: 0; }
.creature-group:hover .outline { opacity: 1; }
#collectionBox { position: absolute; bottom: 0; left: 0; width: 100%; background: white; border-top: 2px solid #ddd; transition: 0.3s; z-index: 90; }
#collectionBox.closed { transform: translateY(140px); }
#collToggle { width: 120px; height: 35px; background: #ddd; margin: -35px auto 0; border: none; border-radius: 12px 12px 0 0; cursor: pointer; display: block; font-weight: bold; }
#collection { height: 120px; display: flex; gap: 12px; padding: 10px; overflow-x: auto; }
.face-card { min-width: 100px; text-align: center; border: 1px solid #ccc; border-radius: 8px; background: #f9f9f9; cursor: pointer; padding: 8px; font-size: 12px; }
.active { background: #4caf50 !important; color: white; }
button { cursor: pointer; padding: 8px; border-radius: 6px; border: none; background: #eee; width: 100%; margin-bottom: 4px; }
.living { animation: jiggle 0.4s infinite; }
.mouth-living { animation: pulse 0.6s infinite; }
@keyframes jiggle { 0%, 100% { transform: translate(0,0); } 25% { transform: translate(1px, -1px); } 75% { transform: translate(-1px, 1px); } }
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05, 0.95); } }
</style>
</head>
<body>
<div id="menu">
<button id="menuToggle" onclick="document.getElementById('menu').classList.toggle('closed')">⚙️</button>
<div class="control-group">
<label>描画設定</label>
<div style="display:flex; gap:10px;">
<input type="color" id="colorPicker" value="#333333" style="width:40px; height:40px; border:none; cursor:pointer;">
<input type="range" id="lineWidth" min="2" max="30" value="8" style="flex-grow:1;">
</div>
</div>
<div class="control-group">
<label>モード選択</label>
<button id="penBtn" class="active" onclick="setMode('pen')">🖊️ ペン(描く)</button>
<button id="moveBtn" onclick="setMode('move')">🖐️ 操作(動かす)</button>
</div>
<div class="control-group">
<label>召喚サイズ: <span id="scaleLabel">0.5</span></label>
<input type="range" id="summonScale" min="0.1" max="2.0" step="0.1" value="0.5" oninput="document.getElementById('scaleLabel').innerText=this.value">
</div>
<button onclick="undo()">↩️ 一つ戻る</button>
<input type="text" id="nameInput" placeholder="この作品の名前..." style="width:calc(100% - 10px); padding:5px; margin-bottom:5px;">
<button onclick="packageAndSave()" style="background:#4caf50; color:white;">💾 全部まとめて保存</button>
<button onclick="location.reload()" style="background:#f44336; color:white;">🧹 画面をクリア</button>
</div>
<svg id="svgCanvas" xmlns="http://www.w3.org/2000/svg">
<g id="mainLayer"></g>
</svg>
<div id="collectionBox" class="closed">
<button id="collToggle" onclick="document.getElementById('collectionBox').classList.toggle('closed')">▼ ギャラリー</button>
<div id="collection"></div>
</div>
<script>
const svg = document.getElementById('svgCanvas');
const mainLayer = document.getElementById('mainLayer');
const collection = document.getElementById('collection');
let mode = 'pen', drawing = false, dragging = false;
let currentPath = null, activeGroup = null;
let startCoords = {x: 0, y: 0}, initialMatrix = null;
let strokes = [];
function setMode(m) {
mode = m;
document.getElementById('penBtn').classList.toggle('active', m === 'pen');
document.getElementById('moveBtn').classList.toggle('active', m === 'move');
}
svg.addEventListener('mousedown', (e) => {
if (mode === 'move') {
const group = e.target.closest('.creature-group');
if (group) {
dragging = true; activeGroup = group;
startCoords = { x: e.clientX, y: e.clientY };
const transform = group.getAttribute('transform') || 'translate(0,0)';
const match = transform.match(/translate\(([^,]+),?([^)]+)\)/);
initialMatrix = {
x: match ? parseFloat(match[1]) : 0,
y: match ? parseFloat(match[2]) : 0
};
mainLayer.appendChild(group);
}
return;
}
drawing = true;
currentPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
currentPath.setAttribute("fill", "none");
currentPath.setAttribute("stroke", document.getElementById('colorPicker').value);
currentPath.setAttribute("stroke-width", document.getElementById('lineWidth').value);
currentPath.setAttribute("stroke-linecap", "round");
currentPath.setAttribute("d", `M ${e.clientX} ${e.clientY}`);
mainLayer.appendChild(currentPath);
});
window.addEventListener('mousemove', (e) => {
if (drawing) {
currentPath.setAttribute("d", currentPath.getAttribute("d") + ` L ${e.clientX} ${e.clientY}`);
} else if (dragging && activeGroup) {
const dx = e.clientX - startCoords.x;
const dy = e.clientY - startCoords.y;
const currentTransform = activeGroup.getAttribute('transform');
const scaleMatch = currentTransform.match(/scale\([^)]+\)/);
const scaleStr = scaleMatch ? ' ' + scaleMatch[0] : '';
activeGroup.setAttribute("transform", `translate(${initialMatrix.x + dx},${initialMatrix.y + dy})${scaleStr}`);
}
});
window.addEventListener('mouseup', () => {
if (drawing) {
drawing = false;
strokes.push(currentPath);
checkSimulacraForAnimation(); // アニメーション付与のみ行う
}
dragging = false; activeGroup = null;
});
// 3点あればアニメーションだけつける(グループ化は保存時まで待つ)
function checkSimulacraForAnimation() {
if (strokes.length < 3) return;
const last3 = strokes.slice(-3);
const pts = last3.map(s => {
const b = s.getBBox();
return {x: b.x + b.width/2, y: b.y + b.height/2};
}).sort((a,b) => a.y - b.y);
const dx = Math.abs(pts[0].x - pts[1].x);
const dy = pts[2].y - pts[0].y;
if (dx < 250 && dy > 20 && dy < 250) {
last3.forEach((p, i) => {
p.classList.add(i < 2 ? 'living' : 'mouth-living');
const b = p.getBBox();
p.style.transformOrigin = `${b.x + b.width/2}px ${b.y + b.height/2}px`;
});
}
}
function packageAndSave() {
const name = document.getElementById('nameInput').value || "無題の生命";
// 1. キャンバス上の全要素を取得(既存のグループと単独のパス)
const allElements = Array.from(mainLayer.children).filter(el => !el.classList.contains('outline'));
if (allElements.length === 0) return;
// 2. 全体を包む新しいグループを作成
const wrapper = document.createElementNS("http://www.w3.org/2000/svg", "g");
wrapper.setAttribute("class", "creature-group");
wrapper.setAttribute("transform", "translate(0,0)");
// ガイド枠
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("class", "outline");
wrapper.appendChild(rect);
// すべてをラッパーに移動
allElements.forEach(el => wrapper.appendChild(el));
// 枠のサイズ更新
const b = wrapper.getBBox();
rect.setAttribute("x", b.x - 10); rect.setAttribute("y", b.y - 10);
rect.setAttribute("width", b.width + 20); rect.setAttribute("height", b.height + 20);
// 3. データのクローンを保存
const savedContent = wrapper.innerHTML;
const savedBBox = b;
const card = document.createElement('div');
card.className = 'face-card';
card.innerHTML = `<strong>${name}</strong><br><small>全パーツ保存済み</small>`;
card.onclick = () => {
const s = document.getElementById('summonScale').value;
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttribute("class", "creature-group");
g.innerHTML = savedContent;
const tx = window.innerWidth/2 - (savedBBox.x + savedBBox.width/2) * s;
const ty = window.innerHeight/2 - (savedBBox.y + savedBBox.height/2) * s;
g.setAttribute("transform", `translate(${tx},${ty}) scale(${s})`);
mainLayer.appendChild(g);
setMode('move');
};
collection.appendChild(card);
// 4. 画面を一度リセット(保存したので)
mainLayer.innerHTML = '';
strokes = [];
document.getElementById('nameInput').value = '';
document.getElementById('collectionBox').classList.remove('closed');
}
function undo() {
const last = mainLayer.lastChild;
if (last) last.remove();
strokes.pop();
}
</script>
</body>
</html>

■ 今日の学び

 世の中のアイデアは「すでにあるものの組み合わせ」でできている、という話を聞いたことがあります。
 その人が見てきたもの、体験してきたことが、自然とアイデアの材料になっているらしいです。(@_@*)

 今回作ったお絵描きゲームも、最近気になっていた言葉と組み合わせてみたら、作っていくうちに「なんだかよくわからないけど楽しい」ゲームになっていました。
 ゲームという形に落とし込んでみることで、ちょっとした化学反応が起きたように感じます。

 ぜひ、この不思議なお絵描きを体験してみてください (*^v^*)o”

 次回は、「なんでこんなの作ったんだろう?」というゲームです。
 次もよろしくお願いします(*^v^)ノシ

次の実験(コーヒーゲーム?):「うんちくの長い喫茶店」
 


コメントを残す