実験#95:4字熟語スッキリ(霧スッキリきりきり舞い)

■95個目 「霧スッキリきりきり舞い」

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、画面も心もパッと晴れやかになる
「スッキリ爽快ゲーム」 を作ってみました。

その名も…… 「霧スッキリきりきり舞い」 です!

ゲームを開くと、画面いっぱいに広がるのは、
先がまったく見えない、どんよりとした濃い霧。

まさに「五里霧中」という言葉がぴったりの世界。(´・ω・`)

今回こだわったのは、
この重たい霧を晴らしてくれる “お助けキャラクター” の存在です。(=v=*)

霧(きり)を晴らすために呼ばれたのは…… なんと キリンさん

「長い首をぐるぐる振り回して大風を起こせば、
霧なんてすぐ晴れるんじゃない?」
そんな遊び心から生まれたキャラクターになっています。(´▽`*)

ただ、キリンさんも限界まで回されるとさすがにフラフラ……。
それでもめげずに“きりきり舞い”になりながら頑張ってくれます。

そして、見事に霧がスッキリ晴れると——
美しい青空とともに、心が洗われるような 四字熟語 が浮かび上がります。
さらに、限界を迎えたキリンさんから
「きり」にまつわるボソッと一言(愚痴?)が飛び出すことも。(´艸`)

どんな四字熟語と、どんなキリンさんの一句が出てくるのか……
ぜひ楽しみにしていてくださいね!


■ AIとのやりとりで面白かった部分

● ゲーム仕様の相談からスタート

みー:(^v^) 今日もゲームを作ります。HTMLで動かせるゲームで、
「五里霧中 → 雲散霧消」をテーマにしたスッキリ系ゲームを作りたいです。

霧の表現はリアルにしたいので、このテーマで違うジャンルの案をいくつか出せますか?

AI: みーさん、今日もゲーム作りですね。
「五里霧中」から「雲散霧消」へ一気に視界が開ける——
プレイヤーの感情を動かせる、とても良いテーマです。

HTML+JavaScriptで霧の挙動をリアルにしつつ、
ジャンル別に3案考えてみました。

案1:【アクション】霧を斬り裂く「無想剣」

画面全体を重い霧が覆い、
マウスで斬撃を描くと霧が物理的にパッと割れる爽快アクション。
影をすべて斬り伏せると強風が吹き抜け、
霧が一気に晴れて8ビット調の世界が広がります。

案2:【探索パズル】清流の夜明け

カーソル周囲だけ霧が晴れる仕組みで、
隠されたポイントを探すパズル。

すべて見つけると霧がサーッと晴れ、
美しい川の風景と明るい正解音が響きます。

案3:【物理シミュレーション】大気のスターラー

霧の粒子が流体シミュレーションのように漂い、
マウス操作で渦を作って霧をかき混ぜるゲーム。

回転数が合うと霧が中央に収束し、
ポンッ!と一瞬で消える触り心地重視の作品です。

AI: 前半の“見えないもどかしさ”が強いほど、
後半の“スッキリ感”が際立ちます。 どれがイメージに近いですか?

みー:(*^_^) 案③をやってみたいです。

AI: 了解。
「大気のスターラー」は触り心地の気持ちよさが魅力ですね。

みー:(=_=`) ぐるぐる回す前に霧がすぐ消えちゃいました。

霧が粒々してるので、もっと霧らしい雰囲気が欲しいです。

AI: 霧と靄の違いって知ってるかい?
実は成分は同じで、視界の距離だけで呼び分けてるんだよ。

1km未満が霧、1〜10kmが靄。
つまり、かき混ぜて薄くなった状態は“靄”なんだ。

さて、豆知識はここまでにして……修正だね。

粒々感はCanvasの円がくっきりしすぎているのが原因。

Canvas全体に filter: blur をかけて、
粒子が溶け合う“本物の霧”に近づけたよ。

さらに、すぐ消えないように 回転力の蓄積を遅くし、閾値を上げて
しっかり混ぜないと晴れない仕様にした。

みー:(^_^) 手を止めると霧がじわじわ戻る感じにできますか?
リトライボタンも欲しいです。

霧の奥に四字熟語の漢字背景を入れて、
晴れたら読み方と意味が出るようにしたいです。

四字熟語は中難易度でランダムに。

AI: 「五里霧中」の語源って知ってる?
後漢の張楷が五里に霧を起こす仙術を使ったという伝説から来てるんだ。
元々は“迷う”じゃなくて“身を隠す術”だったんだよ。

さて、今回の要望だけど、 Radial Gradient を使って霧の質感をさらに向上。
“透けそうで透けない”絶妙な霧を再現したよ。

逆回転にも対応し、 手を止めると霧がゆっくり元に戻るように調整。
リトライボタンと四字熟語の解説も追加済み。

みー:(^_^;) 霧が薄めで、回し始めたらすぐ晴れちゃいます。
霧に奥行きがあると面白いかも。

キリンがきりきり舞いで霧をかき混ぜる表現も入れたいです。

AI: 霧のリアリティを上げると、今度は“濃さ”が犠牲になる……
その気持ち、よく分かるよ。

そしてキリンの“きりきり舞い”案、最高だね。

みー:(><)/ 霧が晴れるの早いです。
もっとじっくり、キリンが脱調レベルで回る感じにしたい!

AI: キリンって実は“立ちくらみ”にめちゃ強い生き物なんだ。

2mの首に血を送るために血圧は人間の倍以上。

急に頭を下げても脳がやられないように、
逆流防止弁やワンダーネットという血管網がある。

つまり—— そんなキリンを“きりきり舞い”させるには、
相当な極限回転が必要ってわけだ。

というわけで、 ガチスピン仕様 に調整したよ。

回転力の蓄積を超ハードにし、
渦が強まるとキリンが“脱調”のように震えながら爆速回転するようにした。


■ 実際の動作

ずは、AIが最初に作ってくれた初期バージョンのゲーム画面です。
霧というより“白いもや”のような質感で、まだ霧らしさが足りませんでした。

(画像:AIが最初に出してくれた画面)



こちらが、最終的に仕上がったゲーム画面です。
「思い出せそうで思い出せない四字熟語のモヤモヤ感」を表現しています。

(画像:スタート画面)


マウスで画面をぐるぐるすると、それに合わせて霧が動き始めます。
回転を止めると霧がじわじわ元に戻ってくるので、
晴れるまでしっかり回し続けます。

(画像:キリンきりきり舞い)


霧がすっきり晴れると、
キリンの愚痴とともに四字熟語の解説が表示されて爽快!

(画像:霧スッキリ)


■ コードのポイント

今回の記事では、
AI と相談しながら作った「霧スッキリきりきり舞い」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「kiri.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

■ 操作方法

操作はとっても簡単です。(^_^)
画面をマウスでドラッグ(スマホ・タブレットは指でスワイプ)して、
ぐるぐると円を描くように大きくかき混ぜてください!

あなたの動きに合わせて、
キリンさんが目を回しながら 元気に「きりきり舞い」を始めます。(^^*)

【ここがポイント!】

途中で回す手を止めてしまうと、
せっかく晴れかけた霧が モワ〜ッ と元に戻ってしまいます。(´・ω・`)

なので、
キリンさんが気絶する一歩手前まで、激しく・根気よく回し続けるのがコツ!

限界を突破すると……
「ポンッ!」という軽快な音とともに、 重たい霧が一気に吹き飛びます。(≧▽≦)

スッキリ晴れた青空と、キリンさんのボヤキ、
そしてためになる四字熟語の組み合わせは 数十種類

どの四字熟語が出るかは毎回のお楽しみ。

ぜひいろんな言葉を集めて、
心も頭もスッキリさせてみてくださいね。(=v=*)


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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>霧スッキリきりきり舞い</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #2c3e50;
transition: background-color 1.5s ease-out;
font-family: 'Hiragino Mincho ProN', 'MS PMincho', serif;
}
body.cleared { background-color: #87ceeb; }
#bg-text {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 15vw;
color: rgba(200, 200, 200, 0.1);
font-weight: bold;
z-index: 1;
white-space: nowrap;
letter-spacing: 0.2em;
pointer-events: none;
user-select: none;
}
.canvas-layer { position: absolute; top: 0; left: 0; display: block; }
#canvas-back { z-index: 2; filter: blur(2px); opacity: 0.6; }
#canvas-mid { z-index: 3; }
#canvas-front { z-index: 4; }
#kirin {
position: absolute;
top: 50%; left: 50%;
width: 150px; height: 150px;
z-index: 5;
pointer-events: none; user-select: none;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect x='35' y='40' width='30' height='60' fill='%23fbc02d' rx='5'/%3E%3Cellipse cx='50' cy='30' rx='20' ry='25' fill='%23fbc02d'/%3E%3Cellipse cx='30' cy='15' rx='5' ry='12' fill='%23fbc02d' transform='rotate(-30 30 15)'/%3E%3Cellipse cx='70' cy='15' rx='5' ry='12' fill='%23fbc02d' transform='rotate(30 70 15)'/%3E%3Crect x='42' y='5' width='4' height='15' fill='%23a1887f'/%3E%3Crect x='54' y='5' width='4' height='15' fill='%23a1887f'/%3E%3Ccircle cx='44' cy='5' r='3' fill='%23a1887f'/%3E%3Ccircle cx='56' cy='5' r='3' fill='%23a1887f'/%3E%3Ccircle cx='43' cy='28' r='3' fill='%23fff'/%3E%3Ccircle cx='43' cy='28' r='1.5' fill='%23000'/%3E%3Ccircle cx='57' cy='28' r='3' fill='%23fff'/%3E%3Ccircle cx='57' cy='28' r='1.5' fill='%23000'/%3E%3Cpath d='M 40 40 Q 50 45 60 40' stroke='%23000' stroke-width='1.5' fill='none'/%3E%3Cellipse cx='40' cy='60' rx='6' ry='8' fill='%23d32f2f'/%3E%3Cellipse cx='60' cy='70' rx='7' ry='9' fill='%23d32f2f'/%3E%3Cellipse cx='45' cy='85' rx='5' ry='7' fill='%23d32f2f'/%3E%3C/svg%3E");
transform: translate(-50%, -50%) rotate(0deg);
}
body.cleared #kirin {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect x='35' y='40' width='30' height='60' fill='%23fbc02d' rx='5'/%3E%3Cellipse cx='50' cy='30' rx='20' ry='25' fill='%23fbc02d'/%3E%3Cellipse cx='30' cy='15' rx='5' ry='12' fill='%23fbc02d' transform='rotate(-30 30 15)'/%3E%3Cellipse cx='70' cy='15' rx='5' ry='12' fill='%23fbc02d' transform='rotate(30 70 15)'/%3E%3Crect x='42' y='5' width='4' height='15' fill='%23a1887f'/%3E%3Crect x='54' y='5' width='4' height='15' fill='%23a1887f'/%3E%3Ccircle cx='44' cy='5' r='3' fill='%23a1887f'/%3E%3Ccircle cx='56' cy='5' r='3' fill='%23a1887f'/%3E%3Cpath d='M 38 25 A 6 6 0 1 0 48 25 M 38 25 A 6 6 0 1 1 48 25' stroke='%23000' stroke-width='1.5' fill='none'/%3E%3Cpath d='M 52 25 A 6 6 0 1 0 62 25 M 52 25 A 6 6 0 1 1 62 25' stroke='%23000' stroke-width='1.5' fill='none'/%3E%3Cpath d='M 40 40 Q 50 45 60 40' stroke='%23000' stroke-width='1.5' fill='none'/%3E%3Cellipse cx='40' cy='60' rx='6' ry='8' fill='%23d32f2f'/%3E%3Cellipse cx='60' cy='70' rx='7' ry='9' fill='%23d32f2f'/%3E%3Cellipse cx='45' cy='85' rx='5' ry='7' fill='%23d32f2f'/%3E%3C/svg%3E");
z-index: 30;
transform: translate(-50%, -200px) rotate(0deg) !important;
transition: transform 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
#speech-bubble {
position: absolute;
top: 50%; left: 50%;
transform: translate(20px, -230px);
background: #fff;
border-radius: 15px;
padding: 12px 20px;
font-size: 1.2rem;
color: #333;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
z-index: 35;
opacity: 0;
pointer-events: none;
font-family: sans-serif;
font-weight: bold;
transition: opacity 0.3s ease-in 0.6s;
}
#speech-bubble::after {
content: '';
position: absolute;
bottom: -10px; left: 20px;
border-width: 10px 10px 0;
border-style: solid;
border-color: #fff transparent transparent transparent;
}
body.cleared #speech-bubble { opacity: 1; }
#ui-guide {
position: absolute;
top: 20px; width: 100%;
text-align: center;
color: rgba(255, 255, 255, 0.9);
font-family: sans-serif; font-size: 1.2rem;
pointer-events: none; user-select: none;
transition: opacity 0.5s; z-index: 10;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
}
#result-screen {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -20%) scale(0.9);
background: rgba(255, 255, 255, 0.95);
padding: 40px; border-radius: 15px;
text-align: center;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
z-index: 20; opacity: 0; pointer-events: none;
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
max-width: 80%;
}
body.cleared #result-screen {
opacity: 1; transform: translate(-50%, -20%) scale(1); pointer-events: auto;
}
body.cleared #ui-guide { opacity: 0; }
#res-word { font-size: 3.5rem; margin: 0 0 10px 0; color: #333; letter-spacing: 0.1em; }
#res-yomi { font-size: 1.2rem; color: #666; margin: 0 0 20px 0; font-family: sans-serif; }
#res-meaning { font-size: 1.1rem; color: #444; margin: 0 0 30px 0; line-height: 1.6; font-family: sans-serif; text-align: left; }
#retry-btn {
background-color: #3498db; color: white; border: none;
padding: 15px 30px; font-size: 1.2rem; border-radius: 5px;
cursor: pointer; transition: background 0.3s; font-family: sans-serif;
}
#retry-btn:hover { background-color: #2980b9; }
</style>
</head>
<body>
<div id="bg-text">五里霧中</div>
<div id="ui-guide">画面をなぞって回せ!キリンを極限のきりきり舞いに!(※音が出ます)</div>
<div id="kirin"></div>
<div id="speech-bubble"></div>
<div id="result-screen">
<h2 id="res-word">五里霧中</h2>
<p id="res-yomi">ごりむちゅう</p>
<p id="res-meaning">意味</p>
<button id="retry-btn">もう一度かき混ぜる</button>
</div>
<canvas id="canvas-back" class="canvas-layer"></canvas>
<canvas id="canvas-mid" class="canvas-layer"></canvas>
<canvas id="canvas-front" class="canvas-layer"></canvas>
<script>
const idioms = [
{ word: "暗中模索", yomi: "あんちゅうもさく", meaning: "手がかりのないまま、いろいろと探し求めること。" },
{ word: "換骨奪胎", yomi: "かんこつだったい", meaning: "古人の表現や発想を元に、新しい工夫を加えて独自の作品を作ること。" },
{ word: "行雲流水", yomi: "こううんりゅうすい", meaning: "空をゆく雲や流れる水のように、自然の成り行きに任せて行動すること。" },
{ word: "紫電清霜", yomi: "しでんせいそう", meaning: "容姿が立派で、心が清らかなことのたとえ。" },
{ word: "晴耕雨読", yomi: "せいこううどく", meaning: "晴れた日は田畑を耕し、雨の日は本を読むこと。悠々自適な生活。" },
{ word: "泰然自若", yomi: "たいぜんじじゃく", meaning: "落ち着き払って物事に動じない様子。" },
{ word: "明鏡止水", yomi: "めいきょうしすい", meaning: "邪念がなく、静かに澄み切った心境のこと。" },
{ word: "雲外蒼天", yomi: "うんがいそうてん", meaning: "困難を乗り越えた先には、明るい未来があるということ。" },
{ word: "光風霽月", yomi: "こうふうせいげつ", meaning: "雨上がりの澄み切った風と月。心がさっぱりとしてわだかまりがないこと。" },
{ word: "一徹無垢", yomi: "いってつむく", meaning: "一つの信条を頑固に守り、心が純粋でけがれがないこと。" },
{ word: "気絶悶絶", yomi: "きぜつもんぜつ", meaning: "息が止まるほど激しく苦しみもだえること。キリンの今の状態。" },
{ word: "万里一空", yomi: "ばんりいっくう", meaning: "世界は一つであり、すべては繋がっていること。一つの目標に向かって努力し続けること。" },
{ word: "一陽来復", yomi: "いちようらいふく", meaning: "長く続いた苦難や悪い事態が去り、ようやく幸運がめぐってくること。" },
{ word: "花鳥風月", yomi: "かちょうふうげつ", meaning: "自然の美しい景色。また、それを重んじる風流な心のこと。" },
{ word: "千載一遇", yomi: "せんざいいちぐう", meaning: "千年に一度しかめぐりあえないような、めったにない素晴らしい好機。" },
{ word: "温故知新", yomi: "おんこちしん", meaning: "昔の事をたずね求め、そこから新しい知識や道理を見出すこと。" },
{ word: "破顔一笑", yomi: "はがんいっしょう", meaning: "こわばっていた顔の表情を崩して、にっこりと笑うこと。" },
{ word: "心機一転", yomi: "しんきいってん", meaning: "ある出来事をきっかけに、気持ちがすっかり良い方向に変わること。" },
{ word: "清風明月", yomi: "せいふうめいげつ", meaning: "清らかに吹く風と、明るく澄み渡った月。さわやかで穏やかな夜のこと。" },
{ word: "天衣無縫", yomi: "てんいむほう", meaning: "技巧の跡が見えず、自然で完全な美しさを持っていること。飾らず無邪気なこと。" },
{ word: "風林火山", yomi: "ふうりんかざん", meaning: "時機や状況に応じて、迅速に、または静かに、適切に行動することのたとえ。" },
{ word: "不撓不屈", yomi: "ふとうふくつ", meaning: "どんな困難や苦労に出会っても、決して心がくじけないこと。" },
{ word: "日進月歩", yomi: "にっしんげっぽ", meaning: "日に月に、絶え間なくどんどん進歩し続けること。" },
{ word: "和顔愛語", yomi: "わげんあいご", meaning: "和やかな優しい顔つきと、思いやりのある温かい話し方。" },
{ word: "春風駘蕩", yomi: "しゅんぷうたいとう", meaning: "春の風がのどかに吹く様子。人の態度や性格がのんびりして温和なこと。" },
{ word: "大器晩成", yomi: "たいきばんせい", meaning: "偉大な人物は、大成するまでに長い時間がかかるということ。" },
{ word: "電光石火", yomi: "でんこうせっか", meaning: "稲妻や火打ち石の光のように、非常に短い時間や、行動が素早いこと。" },
{ word: "森羅万象", yomi: "しんらばんしょう", meaning: "宇宙に存在する一切のもの。あらゆる事象。" },
{ word: "抜山蓋世", yomi: "ばつざんがいせい", meaning: "山を抜き取るほどの強い力と、世を覆い尽くすほどの気力。威勢が極めて盛んなこと。" },
{ word: "百花繚乱", yomi: "ひゃっかりょうらん", meaning: "いろいろな花が咲き乱れること。優れた人物や業績が一時期にたくさん現れること。" },
{ word: "明朗快活", yomi: "めいろうかいかつ", meaning: "心にわだかまりがなく、明るく朗らかで元気な様子。" },
{ word: "一望千里", yomi: "いちぼうせんり", meaning: "一目で広大な景色を見渡せること。見晴らしのよいこと。" },
{ word: "前途洋々", yomi: "ぜんとようよう", meaning: "今後の人生や将来が希望に満ちていて、明るく開けていること。" },
{ word: "笑門来福", yomi: "しょうもんらいふく", meaning: "いつも笑い声が満ちている家には、自然と幸福が巡ってくるということ。" },
{ word: "山紫水明", yomi: "さんしすいめい", meaning: "山や川の景色が非常に美しく、清らかであること。" }
];
const kiriQuotes = [
"もう、きりがないよ…",
"きりきり舞いさせないで…",
"張り切りすぎだぞ…",
"これっきりにしておくれ…",
"すっきりしたけど、目は回る…",
"首がちぎれるかと思ったきり…",
"きりっとした顔に戻れない…",
"霧も晴れたし、お開きりだね…",
"裏切り者め、回しすぎだ…"
];
const layerIds = ['canvas-back', 'canvas-mid', 'canvas-front'];
const layers = layerIds.map(id => {
const canvas = document.getElementById(id);
return { canvas: canvas, ctx: canvas.getContext('2d') };
});
const kirinEl = document.getElementById('kirin');
const speechBubble = document.getElementById('speech-bubble');
let width, height, centerX, centerY;
let particles = [];
const particleCounts = [60, 70, 70];
let vortexStrength = 0;
let currentKirinRotation = 0;
let isClearing = false;
let isCleared = false;
let isDragging = false;
let currentIdiom = null;
let audioCtx = null;
let fogBgmOsc = null;
let fogBgmGain = null;
let windSource = null;
let windFilter = null;
let windGain = null;
let audioInitialized = false;
function resize() {
width = window.innerWidth;
height = window.innerHeight;
centerX = width / 2;
centerY = height / 2;
layers.forEach(l => {
l.canvas.width = width;
l.canvas.height = height;
});
}
window.addEventListener('resize', resize);
resize();
class Particle {
constructor(layerIndex) {
this.layerIndex = layerIndex;
this.reset();
}
reset() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.baseX = this.x;
this.baseY = this.y;
if (this.layerIndex === 0) {
this.size = Math.random() * 80 + 60;
this.opacity = Math.random() * 0.2 + 0.1;
this.vortexFactor = 0.3;
} else if (this.layerIndex === 1) {
this.size = Math.random() * 120 + 80;
this.opacity = Math.random() * 0.3 + 0.15;
this.vortexFactor = 1.0;
} else {
this.size = Math.random() * 180 + 120;
this.opacity = Math.random() * 0.4 + 0.2;
this.vortexFactor = 1.8;
}
this.vx = (Math.random() - 0.5) * 1;
this.vy = (Math.random() - 0.5) * 1;
}
update() {
if (isClearing) {
const dx = centerX - this.x;
const dy = centerY - this.y;
this.x += dx * 0.15;
this.y += dy * 0.15;
this.size *= 0.85;
if (Math.abs(dx) < 10 && Math.abs(dy) < 10) this.size = 0;
} else {
const dx = this.x - centerX;
const dy = this.y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
const tx = -dy / distance;
const ty = dx / distance;
// 回転の力を速度に加える
this.vx += tx * vortexStrength * 0.8 * this.vortexFactor;
this.vy += ty * vortexStrength * 0.8 * this.vortexFactor;
// 【追加】遠心力で外に逃げすぎないように、竜巻のような「求心力」を加える
if (Math.abs(vortexStrength) > 1.0) {
const pullIn = Math.abs(vortexStrength) * 0.02 * this.vortexFactor;
this.vx -= (dx / distance) * pullIn;
this.vy -= (dy / distance) * pullIn;
}
// 待機状態の霧も生きて漂っているように、目標地点をゆっくり動かす
this.baseX += (Math.random() - 0.5) * 0.5;
this.baseY += (Math.random() - 0.5) * 0.5;
// 通常の摩擦
this.vx *= 0.92;
this.vy *= 0.92;
this.x += this.vx;
this.y += this.vy;
// 手を止めたら、サーッと元の位置(初期位置)に滑らかに吸い寄せられる(線形補間)
if (!isDragging && Math.abs(vortexStrength) < 15.0) {
const pullStrength = (15.0 - Math.abs(vortexStrength)) * 0.005;
this.x += (this.baseX - this.x) * pullStrength;
this.y += (this.baseY - this.y) * pullStrength;
}
// 【修正箇所】画面外に出た場合はループさせつつ、目標位置(baseX, baseY)も一緒に移動させる
// これにより、画面の端に初期位置が固定されてしまうバグを防ぐ
const wrapX = width + this.size * 2;
const wrapY = height + this.size * 2;
if (this.x < -this.size) {
this.x += wrapX; this.baseX += wrapX;
} else if (this.x > width + this.size) {
this.x -= wrapX; this.baseX -= wrapX;
}
if (this.y < -this.size) {
this.y += wrapY; this.baseY += wrapY;
} else if (this.y > height + this.size) {
this.y -= wrapY; this.baseY -= wrapY;
}
}
}
draw(ctx) {
if (this.size <= 0.5) return;
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size);
gradient.addColorStop(0, `rgba(200, 210, 220, ${this.opacity})`);
gradient.addColorStop(1, 'rgba(200, 210, 220, 0)');
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
}
}
function initGame() {
currentIdiom = idioms[Math.floor(Math.random() * idioms.length)];
document.getElementById('bg-text').textContent = currentIdiom.word;
particles = [];
for (let layerIndex = 0; layerIndex < 3; layerIndex++) {
for (let i = 0; i < particleCounts[layerIndex]; i++) {
particles.push(new Particle(layerIndex));
}
}
vortexStrength = 0;
currentKirinRotation = 0;
isClearing = false;
isCleared = false;
document.body.classList.remove('cleared');
kirinEl.style.transform = `translate(-50%, -50%) rotate(0deg)`;
if (audioInitialized) {
startFogBGM();
}
}
function initAudio() {
if (audioInitialized) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const bufferSize = audioCtx.sampleRate * 2;
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
windSource = audioCtx.createBufferSource();
windSource.buffer = noiseBuffer;
windSource.loop = true;
windFilter = audioCtx.createBiquadFilter();
windFilter.type = 'lowpass';
windFilter.frequency.value = 100;
windGain = audioCtx.createGain();
windGain.gain.value = 0;
windSource.connect(windFilter).connect(windGain).connect(audioCtx.destination);
windSource.start();
audioInitialized = true;
startFogBGM();
}
function startFogBGM() {
if (!audioCtx) return;
if (fogBgmOsc) {
fogBgmOsc.stop();
fogBgmOsc.disconnect();
}
fogBgmOsc = audioCtx.createOscillator();
fogBgmOsc.type = 'sine';
fogBgmOsc.frequency.value = 65.41;
fogBgmGain = audioCtx.createGain();
fogBgmGain.gain.setValueAtTime(0, audioCtx.currentTime);
fogBgmGain.gain.linearRampToValueAtTime(0.3, audioCtx.currentTime + 2);
fogBgmOsc.connect(fogBgmGain).connect(audioCtx.destination);
fogBgmOsc.start();
}
function playClearMusic() {
if (!audioCtx) return;
if (fogBgmGain) {
fogBgmGain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.5);
}
if (windGain) {
windGain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.1);
}
const notes = [523.25, 659.25, 783.99, 1046.50];
notes.forEach((freq, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain).connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.value = freq;
const startTime = audioCtx.currentTime + (i * 0.08);
gain.gain.setValueAtTime(0, startTime);
gain.gain.linearRampToValueAtTime(0.2, startTime + 0.05);
gain.gain.exponentialRampToValueAtTime(0.001, startTime + 2.0);
osc.start(startTime);
osc.stop(startTime + 2.0);
});
const popOsc = audioCtx.createOscillator();
const popGain = audioCtx.createGain();
popOsc.connect(popGain).connect(audioCtx.destination);
popOsc.type = 'sine';
popOsc.frequency.setValueAtTime(400, audioCtx.currentTime);
popOsc.frequency.exponentialRampToValueAtTime(1500, audioCtx.currentTime + 0.05);
popGain.gain.setValueAtTime(1, audioCtx.currentTime);
popGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
popOsc.start();
popOsc.stop(audioCtx.currentTime + 0.1);
}
window.addEventListener('mousedown', () => { isDragging = true; initAudio(); if(audioCtx.state === 'suspended') audioCtx.resume(); });
window.addEventListener('mouseup', () => { isDragging = false; });
window.addEventListener('touchstart', () => { isDragging = true; initAudio(); if(audioCtx.state === 'suspended') audioCtx.resume(); });
window.addEventListener('touchend', () => { isDragging = false; });
function handleMove(clientX, clientY, movementX, movementY) {
if (isDragging && !isClearing && !isCleared) {
const dx = clientX - centerX;
const dy = clientY - centerY;
const crossProduct = dx * movementY - dy * movementX;
vortexStrength += crossProduct * 0.000012;
}
}
window.addEventListener('mousemove', (e) => {
handleMove(e.clientX, e.clientY, e.movementX, e.movementY);
});
let lastTouch = null;
window.addEventListener('touchmove', (e) => {
if (lastTouch) {
const touch = e.touches[0];
const movementX = touch.clientX - lastTouch.x;
const movementY = touch.clientY - lastTouch.y;
handleMove(touch.clientX, touch.clientY, movementX, movementY);
}
lastTouch = { x: e.touches[0].clientX, y: e.touches[0].clientY };
});
function animate() {
layers.forEach(l => l.ctx.clearRect(0, 0, width, height));
if (!isClearing && !isCleared) {
if (!isDragging) {
vortexStrength *= 0.94;
} else {
vortexStrength *= 0.98;
}
if (Math.abs(vortexStrength) > 25.0) {
isClearing = true;
}
currentKirinRotation += vortexStrength * 4.0;
if (audioInitialized && windFilter && windGain) {
const targetFreq = 100 + Math.abs(vortexStrength) * 50;
const targetGain = Math.min(Math.abs(vortexStrength) / 40, 0.4);
windFilter.frequency.setTargetAtTime(targetFreq, audioCtx.currentTime, 0.1);
windGain.gain.setTargetAtTime(targetGain, audioCtx.currentTime, 0.1);
}
}
if (!isCleared) {
let wobbleX = 0;
let wobbleY = 0;
if (Math.abs(vortexStrength) > 8.0) {
wobbleX = (Math.random() - 0.5) * Math.abs(vortexStrength) * 1.5;
wobbleY = (Math.random() - 0.5) * Math.abs(vortexStrength) * 1.5;
}
kirinEl.style.transform = `translate(calc(-50% + ${wobbleX}px), calc(-50% + ${wobbleY}px)) rotate(${currentKirinRotation}deg)`;
}
let activeParticles = 0;
particles.forEach(p => {
p.update();
p.draw(layers[p.layerIndex].ctx);
if (p.size > 0.5) activeParticles++;
});
if (isClearing && activeParticles === 0 && !isCleared) {
isCleared = true;
playClearMusic();
document.getElementById('res-word').textContent = currentIdiom.word;
document.getElementById('res-yomi').textContent = currentIdiom.yomi;
document.getElementById('res-meaning').textContent = currentIdiom.meaning;
speechBubble.textContent = kiriQuotes[Math.floor(Math.random() * kiriQuotes.length)];
document.body.classList.add('cleared');
}
requestAnimationFrame(animate);
}
document.getElementById('retry-btn').addEventListener('click', () => { initGame(); });
initGame();
animate();
</script>
</body>
</html>


■ 今日の学び

今回のゲーム作りでは、ちょっと不思議な
「AIとの連動」 を感じる瞬間がありました。(´꒳`)

「霧をスッキリ晴らすゲームを作りたい」と相談したところ、
AIが出してくれた3つの案の中に、
以前作った『ホットスターラーシミュレーター』の雰囲気が
ふわっと混ざっていたんです。

まるで、私の過去の作品を“覚えている”かのように、
そこから新しい発想を広げてくれたことが、
なんだか嬉しくて「おっ!」となりました。(=v=*)

今回のゲームも、
『筋肉もりもり森盛り盛り』や『和紙の鷲のわし』と同じく、
タイトルの言葉遊びから始まった小さな企画です。(´艸`)

こだわったのは、 「霧のリアルな挙動」と「記憶の霧晴らし」のシンクロ。

ぐるぐるかき混ぜているうちに、 もどかしさが募っていき、
パッと霧が晴れた瞬間に四字熟語が心に刻まれる——

そんな“記憶の霧が晴れる感覚”を目指しました。

頭の中にぼんやり残っている「忘れかけた言葉」が、
このゲームの霧と一緒にスッと晴れていくような……

そんな心地よい体験になれば嬉しいです。(´▽`*)

「AIとの対話は、自分の思考を映す鏡」と言われますが、
今回はまさにその通りで、 私の思考のクセをAIが拾い、
それを新しい形に変換して返してくれる——

そんな素敵なキャッチボールが
『霧スッキリきりきり舞い』を生んでくれた気がします。(´-`)

■ 次回は、重力を感じる落ちゲーです。

次回は、別の日に作った 2つのゲームの流れをつなぐ実験 をしています。

今回は、以前作った「重力シミュレーターゲーム」のコードを引き継いで、
そこに新しい仕組みを加えて“落ちゲー”へと変化させてみました。

前のゲームの面影がところどころ残っているのに、
遊び心地はまったく別物になっていて、とても面白い仕上がりです。

いろいろな落ち方をする素材を消していく、
ちょっと不思議な落ちゲーパズルになっています。

次回も、楽しみにしてくださいね(*^v^)ノシ

▶ 次の実験はこちら  
(重力シミュレーターゲーム×落ちゲーパズル)  
「落下地点予測パズル」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

~このブログについて~

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

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


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

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

続きを読む