■44個目「癒やしの渦(Natural Vortex)」
こんにちは、AIと作る100のゲーム実験室の みー です。(^_^)/
今回は、ちょっと一息つきたい時にぴったりの、クリックするだけで“光のアート”が生まれる小さな癒やしゲームを作ってみました。(^_^)
テーマは…… “癒やしの渦(Natural Vortex)” です。
画面をそっとクリック(タップ)すると、マグネティックスターラーのように、光の粒がグルン!と心地よい渦を描きます。(=v=*)
今回こだわったのは、耳からも心からもリラックスできる「5つの世界観」です。
ボタンひとつで、その時の気分に合わせた風景に模様替えできちゃいます。
① ナチュラル:
せせらぎと鳥の声が響く、いつもの優しい空間。
② 深い森:
木々を揺らす風の音に包まれる、静かな時間。
③ 清らかな水辺:
水滴がポチャンと響く、透明感あふれる世界。
④ 花畑:
パステルカラーが舞い、陽気な鳥や蜂が遊びに来る場所。
⑤ 焚火:
暗闇の中で火がパチパチとはぜる、温かな夜。
オブジェクトたちは、止まっている時もそよ風に吹かれているように、ゆらゆらと優しく揺らいでいます。
何も考えずに、ただ光の動きを眺めたり、音に耳を澄ませたり……。
自然の中にいるような、ゆったりした時間を過ごしてもらえたら嬉しいです。
もしよかったら、あなたの好きな世界観で、 そっと光を動かしてみてください。 小さな渦が、今日の疲れをふっとほどいてくれるかもしれません。(´v`*)
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。
今回は、何も考えずにマウスクリックするだけで楽しめる「クリックアート体験型」にしてみようと思います。
マグネティックスターラーの渦のように、クリックするとグルン!とオブジェクトが変化する仕組みです。
色はナチュラル中心で癒やし要素を感じられるように。オブジェクトが止まっている時も、そよ風に揺らぐようなアニメーションをつけたいです。
効果音やBGMは、小鳥のさえずりやせせらぎのような優しい音がいいな。
AI: おっ、それはまた心地よさそうなテーマですね。 何も考えずにポチポチするだけで癒やされる…今の時代、そういう空間こそ必要です。 自然の中で深呼吸するような、優しいアート作品に仕上げていきましょう。
ちょっとした雑学ですが、自然界の癒やし音といえば「1/fゆらぎ」。
小鳥の声や川のせせらぎだけでなく、ろうそくの炎の揺れ、さらには“木星の自転周期の変動”にも含まれているらしいんです。
宇宙規模のゆらぎが心を落ち着かせるなんて、ロマンがありますよね。
さて、「滑らかな渦」と「そよ風の揺らぎ」を表現するために、今回は Canvas を使った物理演算でプログラムを組みました。
ブラウザの仕様上、音声の自動再生は制限されているので、最初のクリックでオーディオが起動し、合成した“せせらぎ”と“小鳥の声”が流れ始めます。
みー:(*^_^)色合いの調整で、森っぽいのと、今のナチュラルと、水辺の青系で、選んで変えられるようにできるかな。
AI: もちろんです。「深い森」に切り替えた時の微かな風の音や、「花畑」での賑やかな鳥や蜂の音など、音の表情も豊かになりました。
みー:(つv<)* 音も連動して切り替わるようにしたいです。 あと、花畑のテーマも増やして、4つから選べるようにしよう。
AI: 素晴らしい展開ですね! テーマと音が連動すると、いよいよ本格的な“環境アート作品”になってきました。
四万十の沈下橋の上で、川の音と鳥の声、そして風の匂いを感じる… そんな五感に訴える体験を目指しましょう。
ここでひとつ雑学を。
「共感覚」という言葉を聞いたことがあるかもしれません。
音を聞くと色を感じたり、色を見ると音を感じたりする感覚のことです。
作曲家スクリャービンは、特定の調性に特定の色を感じていて、演奏中に会場をその色で照らす“色光ピアノ”まで考案したそうです。
今回のアップデートで、プレイヤーの方にもそんな“音と色の共鳴”を感じてもらえるかもしれませんね。
みー:(^_^*)「焚火」のテーマも増やして、5つにしよう。
AI: 五感で感じる癒やしの空間、さらに深めていきましょう。
「焚火」では、暖かい色合いに加えて、あの“パチパチ”と爆ぜる音を追加しました。
これもまた、強い 1/f ゆらぎを持つ癒やし音です。
さらに、人間は太古から火を囲んで暮らしてきたため、火を見るだけで本能的に安心感を覚えるという説もあります。
今回のゲームは、その本能に語りかけるような作品になったと思います。
■ 実際の動作
今回は最初にAIが仕上げて作った基本のベースから癒しのバリエーションを増やしていきました。
最初の基本となったナチュラルの画面です。風の音と小鳥のさえずりが聞こえてきます。

(画像:ナチュラルの画面) 操作は簡単です。好きな癒しの環境を選んで、画面内の好きな場所をクリックします。クリックしたところはグルンとはじけます。少し待つとグルンと動いて何事もなかったかのようにゆっくりと流れ始めます。

(画像:クリック時のはじけた様子)
こちらが 深い森 です。静かな空間に、時折聞こえる鳥のさえずりが心地よいです。

(画像:深い森画面)
こちらが 清らかな水辺 です。やさしい水音が癒されます。

(画像:清らかな水辺画面)
こちらが 花畑です。やさしい色合いに小鳥のさえずりと時折聞こえるミツバチの羽音が心地よいです。

(画像:花畑の画面)
こちらが 焚火 です。パチパチとはぜる音が聞こえ、火の粉のように見える画面に癒されます。

(画像:焚火の画面)
■ コードのポイント
今回の記事では、AI と相談しながら作った 「癒やしの渦(Natural Vortex)」 の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。 コードも難しい部分はほとんどないので、気軽に試してみてくださいね。
【遊ぶまでの手順】
1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「iyashi.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。
🌿 遊び方・操作方法 🌿
操作はとってもシンプルです。
難しいことは抜きにして、感覚のままに触れてみてくださいね。(^^*)
ただ、眺める……
何もしないでいると、粒たちはそよ風に揺られるように、 ゆらゆらと優しく動き続けます。
焚火のテーマの時は、 熱気にふわっと押し上げられるように上昇していきます。
お気に入りのテーマを見つけて、 ぼーっと眺めているだけでも癒やされます。
画面をクリック(タップ)するだけ!
好きな場所をポチッとしてみてください。 その場所を中心に、光の粒たちが 「グルン!」 と心地よい渦を描きます。
クリックするたびに、 ポチャンと水滴が落ちるような優しい音が響きます。
「5つの世界」を切り替える 画面の下にあるボタンを押すと、いつでも世界観を変えられます。 色合いだけじゃなく、流れる「音」もその場所に合わせて変化するので、ぜひイヤホンをして耳を澄ませてみてください。(^_^)
▼ここからコード▼(クリックで開く)
<html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>Healing Stirrer Art - Five Realms</title><style> body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #f4f1ea; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; transition: background-color 1.5s ease; /* 背景色も滑らかに変化 */ } canvas { display: block; } #overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(244, 241, 234, 0.8); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 10; color: #5c5346; cursor: pointer; transition: opacity 1s ease; text-align: center; padding: 20px; } #overlay h1 { font-weight: normal; letter-spacing: 2px; margin-bottom: 10px; } #overlay p { font-size: 0.9em; opacity: 0.8; line-height: 1.6; } /* テーマ切り替えボタンのスタイル(5つに対応) */ #theme-selector { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; /* 隙間を少し詰める */ flex-wrap: wrap; justify-content: center; z-index: 5; opacity: 0; pointer-events: none; transition: opacity 1s ease 0.5s; max-width: 95%; /* スマホ対応 */ } .theme-btn { background: rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.6); padding: 8px 12px; /* 少しパディングを小さく */ border-radius: 20px; color: #5c5346; font-size: 0.8em; /* 少しフォントを小さく */ cursor: pointer; backdrop-filter: blur(4px); transition: all 0.3s ease; white-space: nowrap; } .theme-btn:hover, .theme-btn.active { background: rgba(255, 255, 255, 0.7); transform: scale(1.05); box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .theme-btn.active { border-color: #5c5346; font-weight: bold; }</style></head><body><div id="overlay"> <h1>Natural Vortex</h1> <p>画面をタップ・クリックして癒やしの空間へ<br>(音が流れます)</p></div><div id="theme-selector"> <button class="theme-btn active" onclick="changeTheme('natural', event)" id="btn-natural">ナチュラル</button> <button class="theme-btn" onclick="changeTheme('forest', event)" id="btn-forest">深い森</button> <button class="theme-btn" onclick="changeTheme('water', event)" id="btn-water">清らかな水辺</button> <button class="theme-btn" onclick="changeTheme('flower', event)" id="btn-flower">花畑</button> <button class="theme-btn" onclick="changeTheme('takibi', event)" id="btn-takibi">焚火</button></div><canvas id="canvas"></canvas><script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const overlay = document.getElementById('overlay'); const themeSelector = document.getElementById('theme-selector'); let width, height; let particles = []; // 5つのカラーテーマの定義 const palettes = { natural: ['#8FBC8F', '#A3C9A8', '#DDD8B8', '#D8C3A5', '#E9967A', '#F5DEB3'], forest: ['#2E4A4C', '#3A5A40', '#588157', '#A3B18A', '#344E41', '#84A98C'], water: ['#CAF0F8', '#90E0EF', '#00B4D8', '#0077B6', '#03045E', '#48CAE4'], flower: ['#FFC0CB', '#FFB6C1', '#FFFFE0', '#FAFAD2', '#E6E6FA', '#D8BFD8', '#FFFFFF', '#F0F8FF'], takibi: ['#FF4500', '#FF6347', '#FF8C00', '#FFA500', '#FFD700', '#FFFFE0', '#4B261D'] // OrangeRed, Tomato, DarkOrange, Orange, Gold, LightYellow, SaddleBrown }; const bgColors = { natural: '#f4f1ea', forest: '#e8ecea', water: '#eaf4f4', flower: '#fff8dc', takibi: '#1a1a1a' // 焚火用の深い夜 }; let currentTheme = 'natural'; let currentPalette = palettes.natural; let pointer = { x: -1000, y: -1000 }; let isStirring = false; let stirTimer = 0; let time = 0; // --- Web Audio API 関連 --- let audioCtx; let riverSource, riverFilter, riverGain; let windSource, windFilter, windGain; let whiteNoiseBufferShort; // 焚火音用の短いノイズバッファ // 5つのテーマごとのオーディオパラメータ const audioParams = { natural: { riverFreq: 300, riverGain: 0.1, windGain: 0, birdFreq: 0.005, beeFreq: 0, dropRelease: 0.3 }, forest: { riverFreq: 200, riverGain: 0.05, windGain: 0.03, birdFreq: 0.002, beeFreq: 0, dropRelease: 0.3 }, water: { riverFreq: 500, riverGain: 0.15, windGain: 0, birdFreq: 0.008, beeFreq: 0, dropRelease: 0.8 }, flower: { riverFreq: 300, riverGain: 0.03, windGain: 0.01, birdFreq: 0.012, beeFreq: 0.003, dropRelease: 0.3 }, takibi: { riverFreq: 100, riverGain: 0.01, windGain: 0.05, birdFreq: 0, beeFreq: 0, dropRelease: 0.3 } // 鳥や蜂は消す、川は超低音 }; function resize() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; initParticles(); } class Particle { constructor() { this.baseX = Math.random() * width; this.baseY = Math.random() * height; this.x = this.baseX; this.y = this.baseY; this.vx = 0; this.vy = 0; this.size = Math.random() * 8 + 3; this.color = currentPalette[Math.floor(Math.random() * currentPalette.length)]; this.offset = Math.random() * 100; this.targetColor = this.color; // 色変化のターゲット } update() { if (isStirring) { // スターラー物理 let dx = this.x - pointer.x; let dy = this.y - pointer.y; let dist = Math.sqrt(dx * dx + dy * dy); if (dist < 300) { let force = (300 - dist) / 15; this.vx += (dy / dist) * force; this.vy -= (dx / dist) * force; this.vx -= (dx / dist) * (force * 0.1); this.vy -= (dy / dist) * (force * 0.1); } } else { // そよ風物理 let breezeX = Math.sin(time * 0.02 + this.offset) * 0.5; let breezeY = Math.cos(time * 0.015 + this.offset) * 0.3; this.vx += (this.baseX - this.x) * 0.02 + breezeX; this.vy += (this.baseY - this.y) * 0.02 + breezeY; // 焚火テーマ限定の上昇物理 if (currentTheme === 'takibi') { this.vy -= 0.03; // 微かに上昇する } } // 摩擦と速度制限 this.vx *= 0.92; this.vy *= 0.92; this.x += this.vx; this.y += this.vy; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = this.targetColor; // 選択されたテーマの色を描画 ctx.globalAlpha = 0.8; ctx.fill(); } } function initParticles() { particles = []; const particleCount = Math.floor((width * height) / 4000); for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } } // テーマ変更関数 window.changeTheme = function(theme, e) { if(e) e.stopPropagation(); // ボタンクリック時に背景の渦反応を防ぐ currentTheme = theme; currentPalette = palettes[theme]; document.body.style.backgroundColor = bgColors[theme]; // 既存のパーティクルの色を新しいパレットからランダムに割り当て直す particles.forEach(p => { p.targetColor = currentPalette[Math.floor(Math.random() * currentPalette.length)]; }); // ボタンのアクティブ状態を更新 document.querySelectorAll('.theme-btn').forEach(btn => btn.classList.remove('active')); document.getElementById(`btn-${theme}`).classList.add('active'); // オーディオ設定を更新 updateAudioParams(); }; function animate() { ctx.clearRect(0, 0, width, height); time++; if (isStirring) { stirTimer--; if (stirTimer <= 0) { isStirring = false; } } particles.forEach(p => { p.update(); p.draw(); }); // ランダムな音の発生 if (audioCtx) { const params = audioParams[currentTheme]; // 鳥の声(花畑・水辺で多い、焚火で0) if (params.birdFreq > 0 && Math.random() < params.birdFreq) { playBirdChirp(); } // 蜂の音(花畑限定) if (params.beeFreq > 0 && Math.random() < params.beeFreq) { playBeeBuzz(); } // 焚火音(焚火テーマ限定で、多めに爆ぜさせる) if (currentTheme === 'takibi' && Math.random() < 0.02) { playTakibiCrackle(); } } requestAnimationFrame(animate); } // --- オーディオの実装 --- function initAudio() { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); // 1. せせらぎ音(ホワイトノイズ + ローパスフィルター) const riverBufferSize = audioCtx.sampleRate * 2; const riverBuffer = audioCtx.createBuffer(1, riverBufferSize, audioCtx.sampleRate); const riverData = riverBuffer.getChannelData(0); for (let i = 0; i < riverBufferSize; i++) { riverData[i] = Math.random() * 2 - 1; } riverSource = audioCtx.createBufferSource(); riverSource.buffer = riverBuffer; riverSource.loop = true; riverFilter = audioCtx.createBiquadFilter(); riverFilter.type = 'lowpass'; riverGain = audioCtx.createGain(); riverSource.connect(riverFilter); riverFilter.connect(riverGain); riverGain.connect(audioCtx.destination); riverSource.start(); // 2. 風の音(ホワイトノイズ + バンドパスフィルター) const windBufferSize = audioCtx.sampleRate * 2; const windBuffer = audioCtx.createBuffer(1, windBufferSize, audioCtx.sampleRate); const windData = windBuffer.getChannelData(0); for (let i = 0; i < windBufferSize; i++) { windData[i] = Math.random() * 2 - 1; } windSource = audioCtx.createBufferSource(); windSource.buffer = windBuffer; windSource.loop = true; windFilter = audioCtx.createBiquadFilter(); windFilter.type = 'bandpass'; windFilter.frequency.value = 100; // 低音の風 windFilter.Q.value = 1; windGain = audioCtx.createGain(); windSource.connect(windFilter); windFilter.connect(windGain); windGain.connect(audioCtx.destination); windSource.start(); // 3. 焚火音用の短いノイズバッファを作成 const shortBufferSize = audioCtx.sampleRate * 0.1; // 0.1秒のノイズ whiteNoiseBufferShort = audioCtx.createBuffer(1, shortBufferSize, audioCtx.sampleRate); const shortData = whiteNoiseBufferShort.getChannelData(0); for (let i = 0; i < shortBufferSize; i++) { shortData[i] = Math.random() * 2 - 1; } // 初期パラメータを設定 updateAudioParams(); } function updateAudioParams() { if (!audioCtx) return; const params = audioParams[currentTheme]; const now = audioCtx.currentTime; // せせらぎのフィルターと音量を滑らかに変更 riverFilter.frequency.exponentialRampToValueAtTime(params.riverFreq, now + 1); riverGain.gain.linearRampToValueAtTime(params.riverGain, now + 1); // 風の音量を滑らかに変更 windGain.gain.linearRampToValueAtTime(params.windGain, now + 1); } function playBirdChirp() { if (!audioCtx) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; // テーマによって鳥の声の高さを変える(花畑は高め、森は低め) let baseFreq = 2000; if (currentTheme === 'flower') baseFreq = 2500; if (currentTheme === 'forest') baseFreq = 1800; const freq = baseFreq + Math.random() * 1500; osc.frequency.setValueAtTime(freq, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(freq + 500, audioCtx.currentTime + 0.1); gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.05, audioCtx.currentTime + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.25); } function playBeeBuzz() { if (!audioCtx) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sawtooth'; // 蜂っぽい音 const freq = 150 + Math.random() * 50; osc.frequency.setValueAtTime(freq, audioCtx.currentTime); gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.02, audioCtx.currentTime + 0.1); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.5); } // 焚火音(パチパチ音) function playTakibiCrackle() { if (!audioCtx || currentTheme !== 'takibi') return; const noise = audioCtx.createBufferSource(); noise.buffer = whiteNoiseBufferShort; // 短いノイズを再生 // ハイパスフィルターで低音を削り、クリスピーな音に const filter = audioCtx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.setValueAtTime(500 + Math.random() * 200, audioCtx.currentTime); const gainNode = audioCtx.createGain(); gainNode.gain.setValueAtTime(0, audioCtx.currentTime); // 非常に鋭いアタックとディケイ const attack = 0.001; const decay = 0.01 + Math.random() * 0.02; gainNode.gain.linearRampToValueAtTime(0.05 + Math.random() * 0.05, audioCtx.currentTime + attack); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + attack + decay); noise.connect(filter); filter.connect(gainNode); gainNode.connect(audioCtx.destination); noise.start(); noise.stop(audioCtx.currentTime + attack + decay + 0.1); } function playWaterDrop() { if (!audioCtx) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); const params = audioParams[currentTheme]; osc.type = 'sine'; osc.frequency.setValueAtTime(400, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.1); gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + 0.02); // テーマによって余韻を変える(水辺は長く) gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + params.dropRelease); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + params.dropRelease + 0.1); } // --- イベントリスナー --- function triggerVortex(e) { // ボタンクリック時は発動させない if (e.target.classList.contains('theme-btn')) return; if (!audioCtx) { initAudio(); overlay.style.opacity = '0'; setTimeout(() => { overlay.style.display = 'none'; themeSelector.style.opacity = '1'; themeSelector.style.pointerEvents = 'auto'; }, 1000); } // スマホのタップとマウスのクリック両方に対応 if (e.touches && e.touches.length > 0) { pointer.x = e.touches[0].clientX; pointer.y = e.touches[0].clientY; } else { pointer.x = e.clientX; pointer.y = e.clientY; } isStirring = true; stirTimer = 90; // 約1.5秒間渦が続く playWaterDrop(); // クリック時にポトッという水滴音 } window.addEventListener('resize', resize); window.addEventListener('mousedown', triggerVortex); window.addEventListener('touchstart', triggerVortex, {passive: false}); // 初期化 resize(); animate();</script></body></html>
■ 今日の学び
今回のゲームは、 映像(視覚)・音(聴覚)・クリックの感触(触覚) が重なり合って、 まるでその場にいるような気分になる “錯覚の癒し” を目指した作品になりました。
HTML 形式のゲームは、 最近のゲーム機のように緻密な表現はできません。 だからこそ、想像力に寄り添うゲーム が多くなります。
それでも、少しでも雰囲気が伝わったらいいな(つv=*)
……そんな気持ちで作っています。
ゲームづくりにはクオリティの制限もありますが、 逆にそれが “実験室ゲームの楽しさ” でもあります。(^^*)
試行錯誤しながら AI と一緒に、 その場の におい・温度・湿度 まで感じられるような作品に 少しずつ近づけていきたいと思います。
そして……
R8年4月24日、ついに 100 個目のゲームが完成しました! ヽ(*⌒o⌒*)ノ
「100のゲーム実験室」は、ここからさらに “進歩” を目指して歩み続けます。 次の目標は――
「AIと考える 1,000 のゲーム実験室(*^v^*)」
ここから新しい実験が始まります。
■ 次回は、原始時代を舞台にしたゲームです。
スケールの大きな “玉入れ勝負”。
どちらが多く入れられるのか、感覚を研ぎ澄ませて挑んでみてくださいね。
次回もお楽しみに。(^v^)ノシ
今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

コメントを残す