■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^)ノシ
次の実験(コーヒーゲーム?):「うんちくの長い喫茶店」

コメントを残す