実験#87:流れるプール、浮かべて流す自然(自然を浮かべる小川 – お昼寝狸)

■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】お片付けも一瞬で

画面がいっぱいになってきたら、 「🔄 リスタート」 ボタンをぽちっ。

すると、またタヌキさんが眠る静かな小川に戻ります。

誰かと競争するもよし、 たくさん浮かべて色とりどりの水面を眺めるもよし。

みなさんだけの素敵な小川を作って、
のんびり楽しんでみてくださいね。(´▽`*)


“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<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^*)

~このブログについて~

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

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


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

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

続きを読む