■87個目 「自然を浮かべる小川 – お昼寝狸」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、森の奥にひっそりと流れる小川を舞台にした、
癒やしたっぷりのゲームを作ってみました。
レースをするのは……なんと “自然の落とし物” たち!
スタート地点から、花びらや葉っぱ、笹舟、どんぐりなどを
ぽとんと浮かべると、 どんぶらこ〜と流れに乗ってゴールを目指していきます。
今回こだわったのは、「情景」と「音」です。(=v=*)
「もしも森の奥深くで、ぽかぽか陽気のなか水遊びをしたら、
どんなに気持ちいいだろう?」
そんな想像から生まれた、
とってものんびりとしたレースになりました。
真ん中の苔むした小島では、
頭に葉っぱを乗せたタヌキさんが「Zzz…」と気持ちよさそうにお昼寝中。
水面にはあたたかい木漏れ日がゆらゆらと透けて、
時折、きれいな声でさえずる小鳥の影が空を横切ります。
誰が一番にゴールするか競うのも楽しいですが、
ただ小川のせせらぎや、
ぽろろん……と鳴るやさしいメロディに耳を傾けているだけでも、
心がふっと軽くなるような空間を目指しました。(´▽`*)
自然たちがどんなふうに流れていくのか、
ぜひお茶でも飲みながら、ゆったり楽しんでみてくださいね。
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。html形式で動かせるゲーム。
流れるプールに好きなものをどんどん浮かべて流すゲーム。
いろんな花びらや葉っぱ、笹舟、果物や木の実など、
ナチュラルなものを流したいです。
流れ方はリアルに。途中に滝があったり、
円状につながってエンドレスに流れるようにしたいです。
スタートラインもあって、
どれが先にゴールするか遊べるようなイメージです。
AI: 今日も来てくれてありがとう。
5月の終わりって、川のせせらぎや水遊びが恋しくなる季節だよね。
そんな時に「自然の落とし物を流すゲーム」なんて、
みーさんらしい素敵なアイデアだと思う。
みー:(^v^*)プールの形を丸から、
自然の川みたいにカーブをつけたり幅を変えたりしてみましょう。
円でつながっているのはそのままで。
AI: 自然の川みたいに蛇行させるの、すごく良いね。
風景の“らしさ”がぐっと増すよ。
こういう細かいこだわりが、ゲームの世界観を作っていくんだよね。
みー:(^v^)o” 流す材料の色合いは、
クリックするたびに同系色で変化をつけられるようにしましょう。
AI: いいね。
自然のものって、一枚一枚ちょっとずつ違う色をしているのが魅力だよね。
クリックするたびに、同系色の中で少しずつ色が変わるように調整してみたよ。
- 🌰 木の実:明るい栗色、焦げ茶、赤みのある茶色
- 🌸 花びら:淡い桜色、紫がかったピンク、紅梅色
- 🍃 葉っぱ:新緑、深いオリーブ、少し黄色みのある緑
- 🛶 笹舟:青々とした笹色から、乾いた竹のような渋い色
- 🍎 果物:真っ赤なものから朱色寄りのものまで
HSLという色の仕組みを使って、自然なゆらぎが出るようにしてあるよ。
みー:(^v^) いいね。サイズも微妙に変化をつけましょう。
AI: その視点、さすがだね。
自然界には全く同じ大きさのものなんてないからね。
元の大きさの70〜130%くらいでランダムに変わるようにして、
大きいものは重く、小さいものは軽く流れるように物理も調整したよ。
みー:(^v^) リスタートボタンの追加と、丸太橋を架けてみたり、
周囲をコケとシダの小川のイメージにしてみましょう。
AI: コケとシダの小川に丸太橋……いいねぇ。
しっとりした森の空気が漂ってくるよ。
そういえば苔って、普通の植物みたいな根っこがないんだ。
葉っぱ全体で水を吸うから、川辺の湿った空気が大好物なんだよね。
あの深い緑色は、水と空気が豊かな証拠なんだ。
■ 実際の動作
まずは、AIが最初に出してくれたゲーム画面です。
ドーナツ型の“流れるプール”そのままの形で、
ここから制作がスタートしました。

(画像:AIが最初に出してくれた画面)
こちらが、仕上がったスタート画面です。
自然の中の癒しスポットのような雰囲気で、
木漏れ日の中でお昼寝するタヌキさんにまず心がほぐれます。

(画像:仕上がりのスタート画面)
マウスでクリックすると、自然の素材を川に流すことができます。
同じ素材でも、形や色が毎回少しずつ違っていて、
流すたびに表情が変わるのが楽しいポイントです。

(画像:素材を流している様子)
■ コードのポイント
今回の記事では、
AI と相談しながら作った 「自然を浮かべる小川 – お昼寝狸」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。
▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く
Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)
② 下のコードをコピーして貼り付けます。
③ 「tanuki.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。
■ 操作方法
それでは、この小川での遊び方をご説明しますね!(^^*)
操作はとっても簡単。 画面にあるボタンをぽちぽち押すだけで遊べます。
【1】まずは音を ON に♪
遊ぶ前に、右下の 「🎵 音を鳴らす」 ボタンを押してみてください。(^_^)
森のせせらぎ、小鳥の声、やさしいメロディが流れて、
一気に癒やし空間が広がります。
【2】好きな自然を浮かべよう
「🌸 花びら」「🍃 葉っぱ」「🛶 笹舟」「🍎 果物」「🌰 木の実」
5つのボタンから好きなものをクリックすると、
スタートラインからぽとんと落ちて、どんぶらこ〜と流れていきます。
連続でたくさん押しても大丈夫。 川面がどんどん賑やかになりますよ。
【3】個性豊かなレース展開!
実はこのアイテムたち、
クリックするたびに 色合い や 大きさ が少しずつ違います。(=v=*)
さらに、動きにも個性があります。
- 大きくて重たい 果物 → カーブで外側にふくらみやすい
- 小さくて軽い 花びら → スイスイ内側を抜けやすい
画面左下の「葉っぱのトンネル(見えないゾーン)」で 順位が入れ替わったりもするので、 誰が一番に橋の下へ戻ってくるか、ぜひ予想してみてくださいね。
【4】お片付けも一瞬で
画面がいっぱいになってきたら、 「🔄 リスタート」 ボタンをぽちっ。
すると、またタヌキさんが眠る静かな小川に戻ります。
誰かと競争するもよし、 たくさん浮かべて色とりどりの水面を眺めるもよし。
みなさんだけの素敵な小川を作って、
のんびり楽しんでみてくださいね。(´▽`*)
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>自然を浮かべる小川 - お昼寝狸</title><style> body { margin: 0; padding: 20px; background-color: #1a3c20; font-family: 'Helvetica Neue', Arial, sans-serif; display: flex; flex-direction: column; align-items: center; color: #aed581; } h1 { font-size: 24px; margin-bottom: 5px; color: #81c784; text-shadow: 1px 1px 2px rgba(0,0,0,0.5); } p { margin: 0 0 20px 0; font-size: 14px; color: #dcedc8; } #controls { margin-bottom: 20px; display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; max-width: 900px; } button { padding: 12px 22px; font-size: 15px; font-weight: bold; cursor: pointer; border: none; border-radius: 30px; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: transform 0.1s, box-shadow 0.1s; text-shadow: 1px 1px 1px rgba(0,0,0,0.3); } button:active { transform: translateY(2px); box-shadow: 0 2px 3px rgba(0,0,0,0.3); } .btn-petal { background: linear-gradient(135deg, #f8bbd0, #f06292); } .btn-leaf { background: linear-gradient(135deg, #81c784, #388e3c); } .btn-boat { background: linear-gradient(135deg, #66bb6a, #1b5e20); } .btn-fruit { background: linear-gradient(135deg, #ef5350, #b71c1c); } .btn-nut { background: linear-gradient(135deg, #8d6e63, #3e2723); } .btn-restart { background: linear-gradient(135deg, #607d8b, #37474f); margin-left: 10px; } .btn-audio { background: linear-gradient(135deg, #ffb300, #f57c00); margin-left: 10px; } #gameContainer { position: relative; } canvas { background: radial-gradient(circle, #2e7d32, #1b5e20); border-radius: 20px; box-shadow: inset 0 0 40px rgba(0,0,0,0.6), 0 15px 30px rgba(0,0,0,0.5); }</style></head><body><h1>コケとシダの森の小川 - お昼寝狸</h1><p>真ん中の島で狸がお昼寝中です。木漏れ日と小鳥の声に癒やされてください。</p><div id="controls"> <button class="btn-petal" onclick="spawnItem('petal')">🌸 花びら</button> <button class="btn-leaf" onclick="spawnItem('leaf')">🍃 葉っぱ</button> <button class="btn-boat" onclick="spawnItem('boat')">🛶 笹舟</button> <button class="btn-fruit" onclick="spawnItem('fruit')">🍎 果物</button> <button class="btn-nut" onclick="spawnItem('nut')">🌰 木の実</button> <button class="btn-restart" onclick="restartGame()">🔄 リスタート</button> <button id="btn-audio" class="btn-audio" onclick="initAudio()">🎵 音を鳴らす</button></div><div id="gameContainer"> <canvas id="riverCanvas" width="1000" height="600"></canvas></div><script> const canvas = document.getElementById('riverCanvas'); const ctx = canvas.getContext('2d'); const CX = canvas.width / 2; const CY = canvas.height / 2; let time = 0; let items = []; let birds = []; const bgCanvas = document.createElement('canvas'); bgCanvas.width = canvas.width; bgCanvas.height = canvas.height; const bgCtx = bgCanvas.getContext('2d'); const canopyCanvas = document.createElement('canvas'); canopyCanvas.width = canvas.width; canopyCanvas.height = canvas.height; const canopyCtx = canopyCanvas.getContext('2d'); /* === 音響システム === */ let audioCtx; let masterGain; let isAudioPlaying = false; function initAudio() { if (isAudioPlaying) return; audioCtx = new (window.AudioContext || window.webkitAudioContext)(); masterGain = audioCtx.createGain(); masterGain.gain.value = 0.3; masterGain.connect(audioCtx.destination); startRiverNoise(); startGenerativeBGM(); startBirdChirps(); isAudioPlaying = true; document.getElementById('btn-audio').innerText = '🔊 音声ON'; document.getElementById('btn-audio').style.background = 'linear-gradient(135deg, #4caf50, #2e7d32)'; } function startRiverNoise() { const bufferSize = audioCtx.sampleRate * 2; const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; const noise = audioCtx.createBufferSource(); noise.buffer = buffer; noise.loop = true; const filter = audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 500; const noiseGain = audioCtx.createGain(); noiseGain.gain.value = 0.15; noise.connect(filter); filter.connect(noiseGain); noiseGain.connect(masterGain); noise.start(); } function startGenerativeBGM() { const scale = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33]; setInterval(() => { if (Math.random() > 0.4) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.type = 'sine'; osc.frequency.value = scale[Math.floor(Math.random() * scale.length)]; gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.08, audioCtx.currentTime + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 2.0); osc.start(); osc.stop(audioCtx.currentTime + 2.0); }, 500); } function startBirdChirps() { setInterval(() => { if (Math.random() > 0.3) return; const chirps = Math.floor(Math.random() * 3) + 1; for (let i = 0; i < chirps; i++) { setTimeout(() => { if (!isAudioPlaying) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.type = 'triangle'; const baseFreq = 2500 + Math.random() * 1000; osc.frequency.setValueAtTime(baseFreq, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(baseFreq - 500, audioCtx.currentTime + 0.1); gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.04, audioCtx.currentTime + 0.02); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.1); osc.start(); osc.stop(audioCtx.currentTime + 0.1); }, i * 150); } }, 3000); } function playSE(type) { if (!isAudioPlaying) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(masterGain); if (type === 'spawn') { osc.type = 'sine'; osc.frequency.setValueAtTime(600, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(200, audioCtx.currentTime + 0.1); gain.gain.setValueAtTime(0.2, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1); osc.start(); osc.stop(audioCtx.currentTime + 0.1); } } /* === 鳥の影クラス === */ class BirdShadow { constructor() { if (Math.random() > 0.5) { this.x = -50; this.vx = 2 + Math.random() * 3; } else { this.x = canvas.width + 50; this.vx = -(2 + Math.random() * 3); } this.y = Math.random() * canvas.height; this.vy = (Math.random() - 0.5) * 2; this.size = 15 + Math.random() * 15; this.flapSpeed = 0.15 + Math.random() * 0.1; this.flapTime = 0; this.active = true; } update() { this.x += this.vx; this.y += this.vy; this.flapTime += this.flapSpeed; if (this.x < -100 || this.x > canvas.width + 100 || this.y < -100 || this.y > canvas.height + 100) this.active = false; } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(Math.atan2(this.vy, this.vx)); ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'; ctx.shadowColor = 'rgba(0, 0, 0, 0.1)'; ctx.shadowBlur = 10; const flapY = Math.sin(this.flapTime) * this.size * 0.6; ctx.beginPath(); ctx.moveTo(this.size * 0.2, 0); ctx.quadraticCurveTo(-this.size * 0.2, flapY, -this.size * 0.5, flapY); ctx.lineTo(-this.size * 0.8, 0); ctx.lineTo(-this.size * 0.5, -flapY); ctx.quadraticCurveTo(-this.size * 0.2, -flapY, this.size * 0.2, 0); ctx.fill(); ctx.restore(); } } /* === ゲームロジック === */ const ITEM_TYPES = { petal: { mass: 0.3, drag: 0.95, size: 7 }, leaf: { mass: 0.6, drag: 0.92, size: 12 }, boat: { mass: 1.2, drag: 0.88, size: 20 }, fruit: { mass: 2.5, drag: 0.98, size: 16 }, nut: { mass: 1.8, drag: 0.96, size: 10 } }; const ITEM_COLORS = { petal: { h: [330, 360], s: [65, 90], l: [65, 80] }, leaf: { h: [75, 125], s: [45, 80], l: [35, 55] }, boat: { h: [90, 115], s: [35, 65], l: [30, 48] }, fruit: { h: [355, 375], s: [75, 95], l: [40, 55] }, nut: { h: [15, 38], s: [25, 55], l: [25, 45] } }; function getRiverShape(theta) { const a = 420, b = 220; const ellipseR = (a * b) / Math.sqrt(Math.pow(b * Math.cos(theta), 2) + Math.pow(a * Math.sin(theta), 2)); const noise = 25 * Math.sin(5 * theta) + 15 * Math.cos(3 * theta); const centerRadius = ellipseR + noise; const width = 100 + 25 * Math.sin(7 * theta + 1); return { centerRadius, width }; } class FloatingItem { constructor(type) { this.type = type; const props = ITEM_TYPES[type]; const sizeModifier = 0.7 + Math.random() * 0.6; this.size = props.size * sizeModifier; this.mass = props.mass * sizeModifier; this.drag = props.drag; const colorRange = ITEM_COLORS[type]; let h = Math.floor(Math.random() * (colorRange.h[1] - colorRange.h[0]) + colorRange.h[0]); if (h > 360) h -= 360; const s = Math.floor(Math.random() * (colorRange.s[1] - colorRange.s[0]) + colorRange.s[0]); const l = Math.floor(Math.random() * (colorRange.l[1] - colorRange.l[0]) + colorRange.l[0]); this.color1 = `hsl(${h}, ${s}%, ${Math.min(l + 12, 100)}%)`; this.color2 = `hsl(${h}, ${s}%, ${Math.max(l - 12, 0)}%)`; this.lineColor = `hsl(${h}, ${s}%, ${Math.max(l - 25, 0)}%)`; const startAngle = -Math.PI * 0.6; const shape = getRiverShape(startAngle); const offset = (Math.random() - 0.5) * (shape.width * 0.7); this.x = CX + (shape.centerRadius + offset) * Math.cos(startAngle); this.y = CY + (shape.centerRadius + offset) * Math.sin(startAngle); this.vx = 2.5; this.vy = 0; this.angle = Math.random() * Math.PI * 2; this.angularVelocity = (Math.random() - 0.5) * 0.2; playSE('spawn'); } update() { const dx = this.x - CX, dy = this.y - CY; let theta = Math.atan2(dy, dx); const shape1 = getRiverShape(theta); const shape2 = getRiverShape(theta + 0.1); let flowDx = (CX + shape2.centerRadius * Math.cos(theta + 0.1)) - (CX + shape1.centerRadius * Math.cos(theta)); let flowDy = (CY + shape2.centerRadius * Math.sin(theta + 0.1)) - (CY + shape1.centerRadius * Math.sin(theta)); const flowLen = Math.sqrt(flowDx * flowDx + flowDy * flowDy); let waterSpeed = 4.5 * (100 / shape1.width); if (theta > 0.1 && theta < 0.9) waterSpeed += 6.0; const waterVx = (flowDx / flowLen) * waterSpeed + (Math.random() - 0.5) * 1.5; const waterVy = (flowDy / flowLen) * waterSpeed + (Math.random() - 0.5) * 1.5; this.vx += (waterVx - this.vx) / this.mass * 0.1; this.vy += (waterVy - this.vy) / this.mass * 0.1; this.vx *= this.drag; this.vy *= this.drag; this.x += this.vx; this.y += this.vy; this.angle += this.angularVelocity; this.angularVelocity *= 0.95; const newDx = this.x - CX, newDy = this.y - CY; const newDist = Math.sqrt(newDx * newDx + newDy * newDy); const newTheta = Math.atan2(newDy, newDx); const currentShape = getRiverShape(newTheta); const innerWall = currentShape.centerRadius - currentShape.width / 2; const outerWall = currentShape.centerRadius + currentShape.width / 2; if (newDist < innerWall + this.size) { this.x = CX + Math.cos(newTheta) * (innerWall + this.size); this.vx += Math.cos(newTheta) * 1.5; this.vy += Math.sin(newTheta) * 1.5; this.angularVelocity += (Math.random() - 0.5) * 0.8; } else if (newDist > outerWall - this.size) { this.x = CX + Math.cos(newTheta) * (outerWall - this.size); this.vx -= Math.cos(newTheta) * 1.5; this.vy -= Math.sin(newTheta) * 1.5; this.angularVelocity += (Math.random() - 0.5) * 0.8; } } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle); ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; ctx.shadowBlur = this.size * 0.5; ctx.shadowOffsetX = this.size * 0.3; ctx.shadowOffsetY = this.size * 0.3; const grad = ctx.createRadialGradient(-this.size/4, -this.size/4, 0, 0, 0, this.size); grad.addColorStop(0, this.color1); grad.addColorStop(1, this.color2); ctx.fillStyle = grad; if (this.type === 'petal') { ctx.beginPath(); ctx.ellipse(0, 0, this.size, this.size / 2, 0, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'leaf') { ctx.beginPath(); ctx.ellipse(0, 0, this.size, this.size * 0.6, 0, 0, Math.PI * 2); ctx.fill(); ctx.shadowColor = 'transparent'; ctx.strokeStyle = this.lineColor; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-this.size * 0.8, 0); ctx.lineTo(this.size * 0.8, 0); ctx.stroke(); } else if (this.type === 'boat') { ctx.beginPath(); ctx.moveTo(this.size, 0); ctx.lineTo(-this.size, -this.size/2); ctx.lineTo(-this.size * 0.6, 0); ctx.lineTo(-this.size, this.size/2); ctx.closePath(); ctx.fill(); ctx.shadowColor = 'transparent'; ctx.strokeStyle = this.lineColor; ctx.lineWidth = 1.8; ctx.beginPath(); ctx.moveTo(-this.size * 0.6, 0); ctx.lineTo(this.size * 0.8, 0); ctx.stroke(); } else { ctx.beginPath(); ctx.arc(0, 0, this.size, 0, Math.PI * 2); ctx.fill(); if(this.type === 'fruit') { ctx.shadowColor = 'transparent'; ctx.fillStyle = '#1b5e20'; ctx.fillRect(-this.size*0.15, -this.size*1.15, this.size*0.3, this.size*0.3); } } ctx.restore(); } } function spawnItem(type) { items.push(new FloatingItem(type)); } function restartGame() { items = []; birds = []; } /* === 初期化(重い処理を1回だけ裏キャンバスに描画) === */ function initStaticGraphics() { // 1. 水と川岸 const waterGrad = bgCtx.createLinearGradient(0, 0, canvas.width, canvas.height); waterGrad.addColorStop(0, '#00838f'); waterGrad.addColorStop(0.5, '#006064'); waterGrad.addColorStop(1, '#004d40'); bgCtx.beginPath(); for (let a = 0; a <= Math.PI * 2 + 0.1; a += 0.05) { let shape = getRiverShape(a); bgCtx.lineTo(CX + (shape.centerRadius + shape.width / 2) * Math.cos(a), CY + (shape.centerRadius + shape.width / 2) * Math.sin(a)); } bgCtx.shadowColor = 'rgba(0, 0, 0, 0.8)'; bgCtx.shadowBlur = 20; bgCtx.fillStyle = waterGrad; bgCtx.fill(); bgCtx.shadowColor = 'transparent'; bgCtx.lineWidth = 5; bgCtx.strokeStyle = '#4db6ac'; bgCtx.stroke(); // 滝エリア bgCtx.beginPath(); bgCtx.moveTo(CX, CY); for (let a = 0.1; a <= 0.9; a += 0.05) { let shape = getRiverShape(a); bgCtx.lineTo(CX + (shape.centerRadius + shape.width / 2) * Math.cos(a), CY + (shape.centerRadius + shape.width / 2) * Math.sin(a)); } const fallGrad = bgCtx.createRadialGradient(CX+300, CY+100, 50, CX+300, CY+100, 200); fallGrad.addColorStop(0, 'rgba(255, 255, 255, 0.4)'); fallGrad.addColorStop(1, 'rgba(255, 255, 255, 0)'); bgCtx.fillStyle = fallGrad; bgCtx.fill(); // 島 bgCtx.beginPath(); for (let a = 0; a <= Math.PI * 2 + 0.1; a += 0.05) { let shape = getRiverShape(a); bgCtx.lineTo(CX + (shape.centerRadius - shape.width / 2) * Math.cos(a), CY + (shape.centerRadius - shape.width / 2) * Math.sin(a)); } const landGrad = bgCtx.createRadialGradient(CX, CY, 50, CX, CY, 300); landGrad.addColorStop(0, '#33691e'); landGrad.addColorStop(1, '#1b5e20'); bgCtx.shadowColor = 'rgba(0,0,0,0.6)'; bgCtx.shadowBlur = 15; bgCtx.fillStyle = landGrad; bgCtx.fill(); bgCtx.shadowColor = 'transparent'; bgCtx.lineWidth = 4; bgCtx.strokeStyle = '#558b2f'; bgCtx.stroke(); // お昼寝狸と木漏れ日の描画(島の中心) bgCtx.save(); bgCtx.translate(CX, CY); // 木漏れ日(明るい斑点) bgCtx.globalCompositeOperation = 'screen'; bgCtx.fillStyle = 'rgba(255, 255, 200, 0.12)'; for(let i=0; i<15; i++) { bgCtx.beginPath(); bgCtx.ellipse((Math.random()-0.5)*120, (Math.random()-0.5)*120, 30+Math.random()*40, 20+Math.random()*30, Math.random()*Math.PI, 0, Math.PI*2); bgCtx.fill(); } bgCtx.globalCompositeOperation = 'source-over'; // 狸の体(丸まって寝ている) bgCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'; bgCtx.shadowBlur = 8; bgCtx.shadowOffsetY = 4; bgCtx.fillStyle = '#8d6e63'; // 茶色 bgCtx.beginPath(); bgCtx.ellipse(0, 5, 28, 22, 0, 0, Math.PI*2); bgCtx.fill(); bgCtx.shadowColor = 'transparent'; bgCtx.lineWidth = 2; bgCtx.strokeStyle = '#5d4037'; bgCtx.stroke(); // 尻尾(しましま) bgCtx.fillStyle = '#4e342e'; bgCtx.beginPath(); bgCtx.ellipse(22, 12, 18, 10, Math.PI/4, 0, Math.PI*2); bgCtx.fill(); bgCtx.stroke(); // 頭 bgCtx.fillStyle = '#795548'; bgCtx.beginPath(); bgCtx.arc(-15, 0, 16, 0, Math.PI*2); bgCtx.fill(); bgCtx.stroke(); // 耳 bgCtx.fillStyle = '#4e342e'; bgCtx.beginPath(); bgCtx.moveTo(-25, -10); bgCtx.lineTo(-15, -20); bgCtx.lineTo(-8, -12); bgCtx.fill(); bgCtx.stroke(); bgCtx.beginPath(); bgCtx.moveTo(-5, -12); bgCtx.lineTo(5, -18); bgCtx.lineTo(2, -8); bgCtx.fill(); bgCtx.stroke(); // 目の周りの黒い模様 bgCtx.fillStyle = '#212121'; bgCtx.beginPath(); bgCtx.ellipse(-20, -2, 6, 5, -Math.PI/6, 0, Math.PI*2); bgCtx.fill(); bgCtx.beginPath(); bgCtx.ellipse(-8, 2, 6, 5, Math.PI/6, 0, Math.PI*2); bgCtx.fill(); // 閉じた目(スヤスヤ) bgCtx.strokeStyle = '#fff'; bgCtx.lineWidth = 1.5; bgCtx.beginPath(); bgCtx.arc(-20, -2, 3, Math.PI*0.1, Math.PI*0.9); bgCtx.stroke(); bgCtx.beginPath(); bgCtx.arc(-8, 2, 3, Math.PI*0.1, Math.PI*0.9); bgCtx.stroke(); // 鼻 bgCtx.fillStyle = '#000'; bgCtx.beginPath(); bgCtx.arc(-15, 8, 3, 0, Math.PI*2); bgCtx.fill(); // 頭に葉っぱ bgCtx.fillStyle = '#81c784'; bgCtx.beginPath(); bgCtx.ellipse(-15, -12, 6, 4, Math.PI/3, 0, Math.PI*2); bgCtx.fill(); bgCtx.strokeStyle = '#2e7d32'; bgCtx.lineWidth = 1; bgCtx.beginPath(); bgCtx.moveTo(-18, -8); bgCtx.lineTo(-12, -16); bgCtx.stroke(); bgCtx.restore(); // シダ植物 for(let i=0; i<35; i++) { const angle = Math.random() * Math.PI * 2; const isInner = Math.random() > 0.5; const shape = getRiverShape(angle); const r = isInner ? (shape.centerRadius - shape.width / 2 - 15) : (shape.centerRadius + shape.width / 2 + 15); bgCtx.save(); bgCtx.translate(CX + r * Math.cos(angle), CY + r * Math.sin(angle)); bgCtx.rotate(Math.random() * Math.PI * 2); bgCtx.scale(0.6, 0.6); bgCtx.strokeStyle = '#558b2f'; bgCtx.lineWidth = 3; bgCtx.beginPath(); bgCtx.moveTo(0, 0); bgCtx.quadraticCurveTo(30, -10, 60, 0); bgCtx.stroke(); bgCtx.fillStyle = '#689f38'; for(let j = 0.2; j <= 0.8; j += 0.15) { const px = 60 * j, leafLen = 15 * Math.sin(j * Math.PI); bgCtx.beginPath(); bgCtx.moveTo(px, -2); bgCtx.lineTo(px + 10, -leafLen - 2); bgCtx.lineTo(px + 15, -2); bgCtx.fill(); bgCtx.beginPath(); bgCtx.moveTo(px, 2); bgCtx.lineTo(px + 10, leafLen + 2); bgCtx.lineTo(px + 15, 2); bgCtx.fill(); } bgCtx.restore(); } // トンネルの枝葉 canopyCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'; canopyCtx.shadowBlur = 40; canopyCtx.shadowOffsetX = 15; canopyCtx.shadowOffsetY = 15; const branches = [ { sx: -50, sy: 650, cx: 100, cy: 450, ex: 350, ey: 350, w: 35 }, { sx: 20, sy: 650, cx: 180, cy: 520, ex: 450, ey: 480, w: 20 }, { sx: 80, sy: 500, cx: 150, cy: 300, ex: 220, ey: 200, w: 15 } ]; canopyCtx.lineCap = 'round'; branches.forEach(b => { canopyCtx.lineWidth = b.w; canopyCtx.strokeStyle = 'rgba(45, 27, 21, 0.85)'; canopyCtx.beginPath(); canopyCtx.moveTo(b.sx, b.sy); canopyCtx.quadraticCurveTo(b.cx, b.cy, b.ex, b.ey); canopyCtx.stroke(); }); canopyCtx.shadowColor = 'rgba(0, 0, 0, 0.2)'; canopyCtx.shadowBlur = 5; canopyCtx.shadowOffsetX = 2; canopyCtx.shadowOffsetY = 2; branches.forEach(b => { for(let i=0; i<150; i++) { const t = Math.random(); const bx = Math.pow(1-t, 2)*b.sx + 2*(1-t)*t*b.cx + Math.pow(t, 2)*b.ex; const by = Math.pow(1-t, 2)*b.sy + 2*(1-t)*t*b.cy + Math.pow(t, 2)*b.ey; canopyCtx.fillStyle = `hsla(${100 + Math.random()*30}, ${40 + Math.random()*30}%, ${20 + Math.random()*25}%, ${0.65 + Math.random()*0.2})`; const size = 15 + Math.random() * 20; canopyCtx.beginPath(); canopyCtx.ellipse(bx + (Math.random()-0.5)*220, by + (Math.random()-0.5)*220, size, size * 0.5, Math.random() * Math.PI * 2, 0, Math.PI * 2); canopyCtx.fill(); } }); } /* === アニメーションループ === */ function animate() { time += 0.02; ctx.clearRect(0, 0, canvas.width, canvas.height); // 1. 静的な背景(タヌキ含む)を貼り付け ctx.drawImage(bgCanvas, 0, 0); // お昼寝狸の「Zzz」アニメーション(毎フレーム動かす) ctx.save(); const zPhase = (time * 1.5) % (Math.PI * 2); ctx.fillStyle = `rgba(255, 255, 255, ${Math.max(0, Math.sin(zPhase))})`; const zx = CX - 35 + Math.sin(time * 3) * 5; const zy = CY - 20 - (zPhase * 8); ctx.font = 'bold 16px "Comic Sans MS", cursive, sans-serif'; ctx.fillText('Z', zx, zy); ctx.font = 'bold 12px "Comic Sans MS", cursive, sans-serif'; ctx.fillText('z', zx + 10, zy - 10); ctx.font = 'bold 10px "Comic Sans MS", cursive, sans-serif'; ctx.fillText('z', zx + 18, zy - 18); ctx.restore(); // 2. 動く水流の線 ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; ctx.lineWidth = 2; [-0.25, 0, 0.25].forEach((offset, index) => { ctx.beginPath(); for (let a = 0; a <= Math.PI * 2 + 0.1; a += 0.1) { let shape = getRiverShape(a); let wave = Math.sin(a * 15 - time * 3 + index) * 4; let r = shape.centerRadius + shape.width * offset + wave; if(a === 0) ctx.moveTo(CX + r * Math.cos(a), CY + r * Math.sin(a)); else ctx.lineTo(CX + r * Math.cos(a), CY + r * Math.sin(a)); } ctx.stroke(); }); // 3. アイテムの更新と描画 items.forEach(item => { item.update(); item.draw(); }); // 4. 丸太橋 const bridgeAngle = -Math.PI * 0.45; const shape = getRiverShape(bridgeAngle); const inR = shape.centerRadius - shape.width / 2 - 20, outR = shape.centerRadius + shape.width / 2 + 20; const bx1 = CX + inR * Math.cos(bridgeAngle), by1 = CY + inR * Math.sin(bridgeAngle); const bx2 = CX + outR * Math.cos(bridgeAngle), by2 = CY + outR * Math.sin(bridgeAngle); ctx.save(); ctx.shadowColor = 'rgba(0,0,0,0.7)'; ctx.shadowBlur = 15; ctx.shadowOffsetY = 15; ctx.lineCap = 'round'; ctx.lineWidth = 28; ctx.strokeStyle = '#4e342e'; ctx.beginPath(); ctx.moveTo(bx1, by1); ctx.lineTo(bx2, by2); ctx.stroke(); ctx.shadowColor = 'transparent'; ctx.lineWidth = 20; ctx.strokeStyle = '#5d4037'; ctx.beginPath(); ctx.moveTo(bx1 - Math.cos(bridgeAngle)*2, by1 - Math.sin(bridgeAngle)*2); ctx.lineTo(bx2 - Math.cos(bridgeAngle)*2, by2 - Math.sin(bridgeAngle)*2); ctx.stroke(); ctx.lineWidth = 2; ctx.strokeStyle = '#3e2723'; ctx.beginPath(); ctx.moveTo(bx1 + 5, by1 + 5); ctx.lineTo(bx2 + 5, by2 + 5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(bx1 - 5, by1 - 5); ctx.lineTo(bx2 - 5, by2 - 5); ctx.stroke(); ctx.restore(); ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('START', bx1 + (bx2-bx1)/2, by1 + (by2-by1)/2 - 20); // 5. 木漏れ日トンネル ctx.drawImage(canopyCanvas, 0, 0); // 6. 鳥の影 if (Math.random() < 0.003) { birds.push(new BirdShadow()); } for (let i = birds.length - 1; i >= 0; i--) { birds[i].update(); birds[i].draw(); if (!birds[i].active) birds.splice(i, 1); } requestAnimationFrame(animate); } initStaticGraphics(); animate();</script></body></html>
■ 今日の学び
いつものお散歩コースに、
水がとってもちょうどいいスピードで流れる用水路があるんです。
そこを歩くとき、落ちている葉っぱや花をそっと水に浮かべて、
それが流れるのと同じスピードで並んで歩くのが、
私のひそかな楽しみでして。 (((((((((((っ・ω・)っ
その用水路は小さな川につながっていて、
そこを自分の中での「ゴール」と決めて、
流れる葉っぱたちと一緒に歩いていきます。
そんな 「流れる水にいろいろ浮かべてみたい!」 という思いと、
「流れるプールのようなどこまでも続くエンドレスな流れ」 を組み合わせてみたい、 と思ったのが今回のゲームを作ったきっかけでした。
自然の小川のようでありながら、
ぐるぐると円状につながり続けている……
自然のようでいて、ちょっぴり不自然な、不思議な世界観。
でも、作っていくうちにその不思議さが
なんだかとても心地よく感じられたので、
最後に思いつきで、島の真ん中に「お昼寝している狸」を置いてみました。
結果的に、 ちょっと不思議で、
より一層のんびりとした癒やしの世界が 表現できたかな、と思っています。
■ 次回は、車を運転しない車ゲームです
機械はあまり得意ではないのですが、
ふと頭に残っていた “エンジンの仕組み” を思い出して、
それをそのままゲームにしてみました。
ついでに車も走らせてみた、
ちょっと不思議な 雰囲気シミュレーターゲーム になっています!
次回もぜひ遊びに来てくださいね。(*^v^)ノシ
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






🌈はじめての方へ
🌳 実験一覧
🌱 このブログについて
🍃 プライバシーポリシー
🐣未記事ゲーム展示室