■85個目 「朝空のフェザー・ドリフト」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、さわやかな朝の空を舞台にしたアクションゲームを作ってみました。
主役は……
気持ちよさそうに大空を飛ぶ “ツバメ” たちです!
昨日、ふわふわと舞う羽毛を、
ツバメたちが飛びながらくわえては離し、
まるでパス回しをして遊んでいるような光景を見かけました。
その楽しそうな姿が忘れられなくて、
「このままゲームにしたい!」と思ったのが今回のきっかけです。(=v=*)
今回こだわったのは、仲間ツバメたちとの息の合った連携プレイ。
画面には自分と、2羽の仲間ツバメが飛んでいます。
仲間たちは、下に落ちてきた羽毛をサッと拾い上げると、
「ほらっ、次よろしく!」と言わんばかりに、
ふんわり優しくパスを出してくれます。(´▽`)
そして、自分が羽毛を受け取ったときだけ、
一番高い空までグーンと運んでいけるんです。
ツバメ特有の「ビューン」と風に乗る滑空の気持ちよさと、
空中に舞う5つの羽毛を地面に落とさないように繋ぐハラハラ感が合わさって、
ちょっと忙しいけれど癒やされるゲームになりました。
小鳥のさえずりのようなBGMも流れるので、
ぜひ音を出して遊んでみてくださいね。
ツバメたちと一緒に、さわやかな朝の空の散歩をお楽しみください!
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲームです。
昨日、ツバメたちがふわふわ舞う羽毛をくわえては離し、
まるでパス遊びをしているような姿を見ました。
とても楽しそうだったので、これをゲームにしてみたいです。
AI: ツバメたちの羽毛パス遊び……
想像するだけで微笑ましいですね!
あの「自然の中の無邪気な一瞬」をゲームに落とし込むのは、
とても面白いテーマです。
みー:(=v=;)
仲間のツバメたちが上空でパスし続けてしまって、
羽毛が落ちてこないです。
AI: ああ、なるほど! 仲間が優秀すぎて、
ずっと上空で羽毛を回し続けてしまうんですね。
これだとプレイヤー(緑のツバメ)が下で待つだけになってしまいます。
原因は、羽毛を離した直後に
全員がまた全力で追いかけてしまう ことです。
そこで、
- 羽毛をトスしたツバメはしばらく「見守りモード」で休憩
- トスはもっと高く・横へ大きく という調整を入れてみましょう。
これで羽毛が画面全体をふわふわ落ちてくるようになります!
みー:(* ̄v ̄)
羽毛をくわえた後、上に投げてる感じになってるけど、
「くわえて上昇する」感じがいいな。
AI: なるほど!
「羽毛をくわえたツバメ自身が羽ばたいて上昇する」動きですね。
前回は“物理的に弾く”動きだったので、
今回はツバメが自分でぐんぐん高度を上げるように修正します。
そして上空で「ここまで来れば十分」と羽毛を離すと、
またふわふわ落ちてくる……
そんな自然なサイクルにします。
みー:(^v^) いいね。場面は早朝の晴れたさわやかな朝だよ。
ツバメの見た目や背景ももっとリアルにしたい。
AI: 「早朝の晴れたさわやかな朝」
……最高のロケーションです!
透明感のある空気、朝露の光、ツバメの滑らかな飛行。
一気に世界観が深まりました。
みー:(^v^) 自分のツバメももっと自由に飛び回りたい。
AI: 確かに!
せっかくの朝の空を、プレイヤー自身も自由に楽しめる方が絶対に良いです。
そこで、
マウスの動きに合わせて画面のどこへでも飛べるフリーフライトにしました。
✨ 今回のバージョンアップのポイント
- 完全フリーフライト
マウスカーソルの位置へ自由に飛べます。
仲間と一緒に上空で遊んだり、急降下したり、空を満喫できます。 - 自分で上空へ運ぶ楽しさ
羽毛をキャッチしたら、自動ではなく「自分の操作」で上空へ運べます。
上まで行くと、気持ちよくパッと離してくれます。 - 運べる時間は3秒に延長
羽毛をくわえていられる時間を少し長めにして、
ゆっくり上空へ運べるようにしました。
みーさん自身も、仲間ツバメたちと一緒に
朝の空を自由に飛び回れる最新版になりました。
■ 実際の動作
まずは、AIが最初に出してくれた画像です。
朝というより夕空のようで、
どこかお魚がエサを取り合っているような雰囲気でした。
下の方で待機している緑のツバメがプレイヤーで、
落ちてきた羽毛を上へ運んでいきます。

(画像:AIが最初に生成したツバメ案)
こちらが、仕上がりのゲーム画面です。
プレイヤーのツバメは、マウスの動きに合わせて
朝の空を自由に飛び回れるようになりました。

(画像:仕上がりのゲーム画面)
羽毛をくわえて、左クリックで「トス!」できます。
仲間ツバメたちも、それぞれのタイミングでふんわりトスしてくれます。

(画像:トス!の様子)
羽毛が画面下に ひとつでも落ちたらゲーム終了 です。
朝の空気の中で、どれだけ長く羽毛をつなげるか挑戦してみてください。

(画像:ゲームオーバー画面)
■ コードのポイント
今回の記事では、
AI と相談しながら作った 「朝空のフェザー・ドリフト」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。
▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く
Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)
② 下のコードをコピーして貼り付けます。
③ 「tubame.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。
■ 操作方法
ここからは、遊び方と操作方法のご紹介です!(^_^)
● 基本操作
■ 移動(マウス)
マウスを動かすと、 自分のツバメ(「▼ じぶん」のマーク付き)がスーッとついてきます。 画面のどこへでも自由に飛び回れますよ~!
■ キャッチ(羽毛をくわえる)
落ちてくる羽毛に触れると、 ツバメがパッとくわえてくれます。
■ トス(クリック)
羽毛をくわえた状態で クリック すると、 上に向かって ポワン! とパスを出します。(=v=*)
飛びながらクリックすると、 その勢いで遠くまで投げられますよ!
● ルール
羽毛は 全部で5つ。 仲間ツバメたちと協力して、 1つでも地面に落としたらゲームオーバー です。
仲間たちはとっても優秀で、 一番下まで落ちてきてピンチな羽毛を優先して拾い、 「みーさん、お願い!」と言わんばかりに こちらへふんわりパスしてくれます。(^^*)
パスを受け取ったら、 好きなタイミングでクリックして、また空へ返してあげましょう。
● 空中ジャグリングの楽しさ
5つの羽毛が飛び交う、 ちょっと忙しいけれど楽しい 空中ジャグリング。
果たして、何回パスを繋げるか……!? ぜひツバメになりきって、大空を自由に飛び回ってみてくださいね♪(^_^)
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>朝空のフェザー・ドリフト - Swallow Morning Drift</title> <style> body { background-color: #e0f2f1; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Helvetica Neue', Arial, sans-serif; overflow: hidden; user-select: none; } canvas { background: linear-gradient(to top, #fff3e0, #e0f7fa, #80deea); box-shadow: 0 15px 35px rgba(0,0,0,0.15); border-radius: 12px; cursor: none; } </style></head><body><canvas id="gameCanvas" width="600" height="500"></canvas><script>const canvas = document.getElementById("gameCanvas");const ctx = canvas.getContext("2d");// --- ゲーム状態 ---let gameState = "start"; // start, playing, gameoverlet rallyCount = 0;let highScore = 0;let mouseX = canvas.width / 2;let mouseY = canvas.height - 100; let particles = [];let clouds = [ {x: 0, y: 350, speed: 0.1, size: 150}, {x: 300, y: 380, speed: 0.08, size: 200}];// --- サウンドマネージャー ---const Sound = { ctx: null, bgmTimer: null, init() { if (this.ctx) return; window.AudioContext = window.AudioContext || window.webkitAudioContext; this.ctx = new AudioContext(); this.startBGM(); }, playCatch() { if (!this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(880, this.ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(1760, this.ctx.currentTime + 0.1); gain.gain.setValueAtTime(0.05, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.1); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.1); }, playToss() { if (!this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(1200, this.ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.15); gain.gain.setValueAtTime(0.05, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.15); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.15); }, playDrop() { if (!this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, this.ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.5); gain.gain.setValueAtTime(0.08, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.5); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.5); }, startBGM() { if (this.bgmTimer) clearInterval(this.bgmTimer); this.bgmTimer = setInterval(() => { if (!this.ctx || gameState !== "playing") return; if (Math.random() > 0.4) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; const baseFreq = 2000 + Math.random() * 1500; osc.frequency.setValueAtTime(baseFreq, this.ctx.currentTime); osc.frequency.linearRampToValueAtTime(baseFreq + 600, this.ctx.currentTime + 0.05); osc.frequency.linearRampToValueAtTime(baseFreq - 200, this.ctx.currentTime + 0.1); gain.gain.setValueAtTime(0, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0.015, this.ctx.currentTime + 0.02); gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.1); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.1); }, 1200); }};class Feather { constructor(x, y) { this.x = x; this.y = y; this.vx = (Math.random() - 0.5) * 2.5; this.vy = 0.5; this.angle = 0; this.wobbleSpeed = 0.03 + Math.random() * 0.02; this.wobbleCount = Math.random() * 10; this.holder = null; this.holdTimer = 0; }}let feathers = [];class Swallow { constructor(x, y, color, isPlayer, name) { this.x = x; this.y = y; this.vx = (Math.random() - 0.5) * 4; this.vy = (Math.random() - 0.5) * 2; this.color = color; this.isPlayer = isPlayer; this.name = name; this.wingAngle = Math.random() * 10; this.angle = 0; this.aiTargetX = x; this.aiTargetY = y; this.aiTimer = 0; this.satietyTimer = 0; this.isGliding = false; } update() { if (this.satietyTimer > 0) this.satietyTimer--; let myFeather = feathers.find(f => f.holder === this); if (this.isPlayer) { let targetX = mouseX; let targetY = mouseY; if (myFeather) { this.wingAngle += 0.15; if (Math.random() < 0.3) { particles.push(new Particle(this.x, this.y + 10, '#fff', 2.5)); } } this.vx = (targetX - this.x) * 0.15; this.vy = (targetY - this.y) * 0.15; this.x += this.vx; this.y += this.vy; this.angle = this.vx * 0.04; if (Math.abs(this.vx) > 0.5 || Math.abs(this.vy) > 0.5 || myFeather) { this.wingAngle += 0.25; this.isGliding = false; } else { this.isGliding = true; } } else { this.aiTimer--; if (myFeather) { this.aiTargetX = this.x + this.vx; this.aiTargetY = this.y - 50; this.isGliding = false; this.wingAngle += 0.15; } else { let availableFeathers = feathers.filter(f => f.holder === null && f.y > 100); if (availableFeathers.length > 0 && this.satietyTimer <= 0) { let targetFeather = availableFeathers.sort((a, b) => b.y - a.y)[0]; this.aiTargetX = targetFeather.x + targetFeather.vx * 4; this.aiTargetY = targetFeather.y + 5; this.isGliding = false; } else { let distToTarget = Math.sqrt((this.aiTargetX - this.x)**2 + (this.aiTargetY - this.y)**2); if (this.aiTimer <= 0 || distToTarget < 50) { this.aiTargetX = Math.random() * (canvas.width - 120) + 60; this.aiTargetY = 100 + Math.random() * 200; this.aiTimer = 100 + Math.random() * 100; this.isGliding = Math.random() < 0.5; } } } let dx = this.aiTargetX - this.x; let dy = this.aiTargetY - this.y; let dist = Math.sqrt(dx*dx + dy*dy); let speedLimit = myFeather ? 5.0 : (this.isGliding ? 3.0 : 5.5); let accel = myFeather ? 0.2 : (this.isGliding ? 0.05 : 0.2); if (dist > 10) { this.vx += (dx / dist) * accel; this.vy += (dy / dist) * accel; } let currentSpeed = Math.sqrt(this.vx*this.vx + this.vy*this.vy); if (currentSpeed > speedLimit) { this.vx = (this.vx / currentSpeed) * speedLimit; this.vy = (this.vy / currentSpeed) * speedLimit; } else if (currentSpeed < 1.5) { this.vx = (this.vx / (currentSpeed || 1)) * 1.5; this.vy = (this.vy / (currentSpeed || 1)) * 1.5; } this.x += this.vx; this.y += this.vy; this.angle = this.vx * 0.04; if (this.isGliding) { this.wingAngle = 0; } else { this.wingAngle += 0.22; } } let margin = 30; if (this.x < margin) { this.x = margin; this.vx *= -1; this.aiTimer = 0; } if (this.x > canvas.width - margin) { this.x = canvas.width - margin; this.vx *= -1; this.aiTimer = 0; } if (this.y < 40) { this.y = 40; this.vy = Math.abs(this.vy); this.aiTimer = 0; } if (this.y > canvas.height - 80) { this.y = canvas.height - 80; this.vy = -Math.abs(this.vy); this.aiTimer = 0; } } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle); ctx.fillStyle = this.color; ctx.beginPath(); ctx.moveTo(0, -10); ctx.quadraticCurveTo(7, -3, 3, 12); ctx.lineTo(8, 32); ctx.lineTo(2, 22); ctx.lineTo(0, 26); ctx.lineTo(-2, 22); ctx.lineTo(-8, 32); ctx.lineTo(-3, 12); ctx.quadraticCurveTo(-7, -3, 0, -10); ctx.closePath(); ctx.fill(); let wSin = Math.sin(this.wingAngle); ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(24, -6 + wSin * 14, 38, -12 + wSin * 24); ctx.quadraticCurveTo(15, 6 + wSin * 8, 0, 5); ctx.moveTo(0, 0); ctx.quadraticCurveTo(-24, -6 + wSin * 14, -38, -12 + wSin * 24); ctx.quadraticCurveTo(-15, 6 + wSin * 8, 0, 5); ctx.fill(); ctx.fillStyle = "rgba(255, 255, 255, 0.85)"; ctx.beginPath(); ctx.ellipse(0, 3, 3.5, 9, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }}class Particle { constructor(x, y, color, size=1.8) { this.x = x + (Math.random() - 0.5) * 8; this.y = y; this.vx = (Math.random() - 0.5) * 0.6; this.vy = 0.2 + Math.random() * 0.4; this.alpha = 1; this.color = color; this.size = size; } update() { this.x += this.vx; this.y += this.vy; this.alpha -= 0.02; } draw() { ctx.save(); ctx.globalAlpha = this.alpha; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }}const flock = [ new Swallow(canvas.width / 2, canvas.height - 100, "#0d1b2a", true, "You"), new Swallow(120, 250, "#1b263b", false, "Swallow A"), new Swallow(480, 280, "#415a77", false, "Swallow B")];window.addEventListener("mousemove", (e) => { const rect = canvas.getBoundingClientRect(); mouseX = e.clientX - rect.left; mouseY = e.clientY - rect.top;});window.addEventListener("mousedown", (e) => { if (gameState === "start") { Sound.init(); resetGame(); gameState = "playing"; return; } if (gameState === "gameover") { resetGame(); gameState = "playing"; return; } if (gameState === "playing") { let pSwallow = flock.find(s => s.isPlayer); let heldFeather = feathers.find(f => f.holder === pSwallow); if (heldFeather) { heldFeather.holder = null; Sound.playToss(); heldFeather.vx = pSwallow.vx * 1.0 + (Math.random() - 0.5) * 1.5; heldFeather.vy = pSwallow.vy - 3.0; pSwallow.satietyTimer = 10; for(let i=0; i<6; i++) { particles.push(new Particle(heldFeather.x, heldFeather.y, '#fff', 2.0)); } } }});function resetGame() { rallyCount = 0; feathers = [ new Feather(canvas.width * 0.15, 60), new Feather(canvas.width * 0.35, 120), new Feather(canvas.width * 0.50, 40), new Feather(canvas.width * 0.65, 100), new Feather(canvas.width * 0.85, 80) ]; flock.forEach(s => { s.satietyTimer = 0; s.x = Math.random() * canvas.width; s.y = 200 + Math.random() * 150; s.vx = (Math.random() - 0.5) * 3; s.vy = (Math.random() - 0.5) * 2; }); particles = [];}function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop);}function update() { if (gameState !== "playing") return; clouds.forEach(c => { c.x += c.speed; if (c.x > canvas.width) c.x = -c.size; }); flock.forEach(swallow => swallow.update()); particles.forEach((p, idx) => { p.update(); if (p.alpha <= 0) particles.splice(idx, 1); }); feathers.forEach(f => { if (f.holder !== null) { let holder = f.holder; let rad = holder.angle + Math.PI / 2; f.x = holder.x + Math.cos(rad) * 4; f.y = holder.y + Math.sin(rad) * 4; f.vx = holder.vx; f.vy = holder.vy; f.angle = holder.angle; f.holdTimer--; if (!holder.isPlayer && f.holdTimer <= 0) { let pSwallow = flock.find(s => s.isPlayer); f.holder = null; Sound.playToss(); let dxToPlayer = pSwallow.x - f.x; f.vx = holder.vx * 0.5 + (dxToPlayer * 0.012); f.vy = -2.5; // ★仲間はパスを出したあと、たっぷり3秒間(180フレーム)見守ります holder.satietyTimer = 180; holder.isGliding = true; for(let i=0; i<4; i++) { particles.push(new Particle(f.x, f.y, '#fff', 2.0)); } } } else { f.wobbleCount += f.wobbleSpeed; f.vx += Math.sin(f.wobbleCount) * 0.035; f.vy += 0.012; if (f.vy > 0.85) f.vy = 0.85; f.vx *= 0.96; f.x += f.vx; f.y += f.vy; f.angle = Math.sin(f.wobbleCount * 0.6) * 0.4; let fMargin = 12; if (f.x < fMargin) { f.x = fMargin; f.vx = Math.abs(f.vx) * 0.6; } if (f.x > canvas.width - fMargin) { f.x = canvas.width - fMargin; f.vx = -Math.abs(f.vx) * 0.6; } if (f.y < fMargin) { f.y = fMargin; f.vy = Math.abs(f.vy) * 0.6; } for (let swallow of flock) { if (swallow.satietyTimer > 0) continue; if (feathers.some(other => other.holder === swallow)) continue; let dx = f.x - swallow.x; let dy = f.y - swallow.y; let dist = Math.sqrt(dx*dx + dy*dy); if (!swallow.isPlayer && f.y < 120) continue; if (dist < 24) { f.holder = swallow; Sound.playCatch(); if (!swallow.isPlayer) { f.holdTimer = 10; } rallyCount++; if (rallyCount > highScore) highScore = rallyCount; break; } } if (f.y > canvas.height) { if (gameState === "playing") { Sound.playDrop(); } gameState = "gameover"; } } });}function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "rgba(255, 255, 255, 0.22)"; clouds.forEach(c => { ctx.beginPath(); ctx.arc(c.x, c.y, c.size * 0.6, 0, Math.PI * 2); ctx.arc(c.x + c.size * 0.4, c.y - 20, c.size * 0.5, 0, Math.PI * 2); ctx.fill(); }); if (gameState === "playing" || gameState === "gameover") { particles.forEach(p => p.draw()); flock.forEach(swallow => swallow.draw()); feathers.forEach(f => { ctx.save(); ctx.translate(f.x, f.y); ctx.rotate(f.angle); ctx.strokeStyle = "rgba(255, 255, 255, 0.9)"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(0, -7); ctx.quadraticCurveTo(1.5, 0, 0, 9); ctx.stroke(); ctx.fillStyle = "rgba(255, 255, 255, 0.65)"; ctx.beginPath(); ctx.moveTo(0, -7); ctx.quadraticCurveTo(4.5, -1.5, 2, 3); ctx.quadraticCurveTo(3.5, 7, 0, 9); ctx.quadraticCurveTo(-3.5, 7, -2, 3); ctx.quadraticCurveTo(-4.5, -1.5, 0, -7); ctx.fill(); ctx.restore(); }); ctx.fillStyle = "#37474f"; ctx.font = "bold 14px 'Helvetica Neue', Arial, sans-serif"; ctx.textAlign = "left"; ctx.fillText(`✨ SCORE: ${rallyCount}`, 20, 35); ctx.fillStyle = "#78909c"; ctx.fillText(`🏆 BEST: ${highScore}`, 20, 55); flock.forEach(s => { if (s.isPlayer) { ctx.textAlign = "center"; ctx.font = "bold 11px sans-serif"; if (feathers.some(f => f.holder === s)) { ctx.fillStyle = "rgba(255, 87, 34, 0.9)"; ctx.fillText("▼ クリックでトス!", s.x, s.y - 25); } else { ctx.fillStyle = "rgba(13, 27, 42, 0.4)"; ctx.fillText("▼ じぶん", s.x, s.y - 25); } } }); } if (gameState === "start") { ctx.fillStyle = "rgba(255, 255, 255, 0.4)"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#263238"; ctx.font = "bold 32px sans-serif"; ctx.textAlign = "center"; ctx.fillText("朝空のフェザー・ドリフト", canvas.width / 2, canvas.height / 2 - 20); ctx.fillStyle = "#00acc1"; ctx.font = "bold 16px sans-serif"; ctx.fillText("画面をクリックしてスタート", canvas.width / 2, canvas.height / 2 + 30); } if (gameState === "gameover") { ctx.fillStyle = "rgba(255, 255, 255, 0.75)"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#263238"; ctx.font = "bold 26px sans-serif"; ctx.textAlign = "center"; ctx.fillText("Drop...", canvas.width / 2, canvas.height / 2 - 20); ctx.font = "16px sans-serif"; ctx.fillStyle = "#37474f"; ctx.fillText(`繋いだパス回数: ${rallyCount} 回`, canvas.width / 2, canvas.height / 2 + 15); ctx.font = "bold 14px sans-serif"; ctx.fillStyle = "#00acc1"; ctx.fillText("画面をクリックして、もう一度空へ", canvas.width / 2, canvas.height / 2 + 55); }}gameLoop();</script></body></html>
■ 今日の学び
今回の「朝空のフェザー・ドリフト」は、
私の日常のほんの一コマから生まれました。
今年、初めて我が家にツバメが巣を作ってくれたんです。
家の周りを元気に飛び交うツバメたちを眺めるのが、
毎朝のささやかな楽しみになっていました。
でも、自然は時に厳しくて……。
ある風の強い日、せっせと作っていた途中の巣が落ちて壊れてしまったんです。
「ああっ……」と胸がぎゅっとなったのですが、
ツバメたちは決してめげませんでした。
すぐにまた土を運び、
前よりもっとしっかりとした立派な巣を作り上げたんです。
その小さな体のどこにそんなパワーがあるんだろうって、
本当に驚かされました。ε(๑•ө•๑)з ブワッ!!
今では毎朝、元気な鳴き声が家に響き渡っています。
ツバメが家に巣を作ると縁起が良いと言われますが、
晴れた朝の光の中で楽しそうに遊ぶ姿を見ていると、
まさに「幸せを運んできてくれる存在」そのものだと感じます。
ゲームの中で、絶対に立ち止まらずに
大空をビューンと駆け抜けるツバメたち。
それは、何度巣が壊れても前を向く、
あの力強い生命力そのものなのかもしれません。
忙しい日々の中でも、 このゲームのツバメたちと一緒に空を飛んで、
ほんの少しでも「朝のあたたかな幸せ」を感じていただけたら嬉しいです。
今年初めて作られたツバメの巣

(画像:ツバメの巣)
■ 次回は、虹リボンの鮮やかなゲームです
都会の暗闇にふわりと浮かび上がる、一本の虹のリボン。
思い通りに描けるパーティクルの軌跡が、
夜の空気の中で光をまとって広がっていきます。
“ひかりを追いかける”ような、
ちょっとアートで、ちょっと不思議な感覚のゲームです。
次回もぜひ遊びに来てくださいね。(*^v^)ノシ
▶ 次の実験はこちら
(虹とパーティクル)「虹リボン」
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






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