🔬 AI発明ギャラリー emma アゲハチョウが踊っている音ゲーだよ
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!DOCTYPE html>
<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>アゲハチョウのダンス - Rhythm Game</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@700&family=Press+Start+2P&display=swap" rel="stylesheet">
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #0a0a0a;
            color: white;
            font-family: 'M PLUS Rounded 1c', sans-serif;
            overflow: hidden;
            touch-action: none; /* スマホでのスワイプなどの誤動作防止 */
        }

        #game-container {
            position: relative;
            width: 100vw;
            height: 100vh;
            max-width: 600px;
            margin: 0 auto;
            /* 背景の色相をCSS変数で管理して動的に変更できるようにする */
            --bg-hue: 220;
            background: linear-gradient(180deg, 
                hsl(var(--bg-hue), 58%, 28%) 0%, 
                hsl(var(--bg-hue), 56%, 38%) 50%, 
                hsl(var(--bg-hue), 47%, 16%) 100%);
            box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
            overflow: hidden;
        }

        /* 歌う女の子の背景 */
        #singer-bg {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            /* マイクを持って歌う女性のフリー画像 */
            background-image: url('https://images.unsplash.com/photo-1516280440502-861f6c8d76a2?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80');
            background-size: cover;
            background-position: center;
            opacity: 0.5;
            /* luminosityにより、背景のグラデーションカラーが画像に反映され、叩くたびに色が変わる照明効果を生みます */
            mix-blend-mode: luminosity;
            z-index: 1;
            pointer-events: none; /* クリック操作を妨げない */
        }

        canvas {
            display: block;
            width: 100%;
            height: 100%;
            position: relative;
            z-index: 2;
        }

        /* 各種UIオーバーレイ */
        .overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            background: rgba(0, 0, 0, 0.8);
            z-index: 10;
            transition: opacity 0.5s;
        }

        .hidden {
            display: none !important;
        }

        /* ヘッダーUI(スコアなど) */
        #ui-header {
            position: absolute;
            top: 20px;
            left: 20px;
            right: 20px;
            display: flex;
            justify-content: space-between;
            z-index: 5;
            pointer-events: none;
            font-family: 'Press Start 2P', cursive;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
        }

        .text-glow {
            text-shadow: 0 0 10px currentColor;
        }

        /* 判定テキストアニメーション */
        #judgement-text {
            position: absolute;
            top: 40%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 2.5rem;
            font-weight: bold;
            font-style: italic;
            opacity: 0;
            pointer-events: none;
            z-index: 5;
        }

        @keyframes popUp {
            0% { transform: translate(-50%, -40%) scale(0.8); opacity: 0; }
            20% { transform: translate(-50%, -50%) scale(1.1); opacity: 1; }
            100% { transform: translate(-50%, -60%) scale(1); opacity: 0; }
        }

        .animate-judge {
            animation: popUp 0.5s ease-out forwards;
        }

        /* ボタンデザイン */
        .btn {
            background: linear-gradient(45deg, #ff00ff, #00ffff);
            border: none;
            padding: 15px 40px;
            border-radius: 30px;
            color: white;
            font-size: 1.5rem;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 0 15px rgba(255, 0, 255, 0.5);
            transition: transform 0.2s, box-shadow 0.2s;
        }

        .btn:hover {
            transform: scale(1.05);
            box-shadow: 0 0 25px rgba(0, 255, 255, 0.8);
        }
    </style>
</head>
<body>

    <div id="game-container">
        <!-- 歌う女の子の背景画像 -->
        <div id="singer-bg"></div>

        <!-- ゲーム描画用キャンバス -->
        <canvas id="gameCanvas" width="400" height="600"></canvas>

        <!-- UI情報 -->
        <div id="ui-header" class="hidden">
            <div>
                <div class="text-sm text-gray-300">スコア</div>
                <div id="score-display" class="text-2xl text-white">0</div>
            </div>
            <div class="text-right">
                <div class="text-sm text-gray-300">コンボ</div>
                <div id="combo-display" class="text-2xl text-yellow-400">0</div>
            </div>
        </div>

        <!-- 判定表示 -->
        <div id="judgement-text"></div>

        <!-- スタート画面 -->
        <div id="start-screen" class="overlay">
            <h1 class="text-3xl md:text-4xl font-bold mb-4 text-center px-4" style="color: #ffb7e6; text-shadow: 0 0 15px #ff00ff;">
                アゲハチョウのダンス
            </h1>
            <p class="mb-8 text-gray-300 text-center px-6">
                上から降ってくるノーツに合わせて<br>
                <span class="text-white font-bold bg-gray-800 px-2 py-1 rounded">D</span>
                <span class="text-white font-bold bg-gray-800 px-2 py-1 rounded">F</span>
                <span class="text-white font-bold bg-gray-800 px-2 py-1 rounded">J</span>
                <span class="text-white font-bold bg-gray-800 px-2 py-1 rounded">K</span><br>
                キーを押すか、画面下部をタップしてください!
            </p>
            <div class="flex flex-col gap-4 w-64">
                <button class="btn diff-btn" data-diff="EASY" style="background: linear-gradient(45deg, #4CAF50, #8BC34A);">かんたん</button>
                <button class="btn diff-btn" data-diff="NORMAL" style="background: linear-gradient(45deg, #ff00ff, #00ffff);">ふつう</button>
                <button class="btn diff-btn" data-diff="HARD" style="background: linear-gradient(45deg, #f44336, #E91E63);">むずかしい</button>
            </div>
        </div>

        <!-- リザルト画面 -->
        <div id="result-screen" class="overlay hidden">
            <h2 class="text-4xl font-bold mb-6 text-white">終了!</h2>
            <div class="text-center mb-8 bg-gray-900 p-6 rounded-xl border border-gray-700 shadow-2xl w-3/4 max-w-sm">
                <div class="text-gray-400 mb-1">最終スコア</div>
                <div id="final-score" class="text-4xl font-bold text-cyan-400 mb-4 font-[Press Start 2P]">0</div>
                <div class="text-gray-400 mb-1">最大コンボ</div>
                <div id="final-combo" class="text-2xl font-bold text-yellow-400 font-[Press Start 2P]">0</div>
            </div>
            <button id="retry-btn" class="btn">もう一度遊ぶ</button>
        </div>
    </div>

    <!-- オーディオ要素 -->
    <audio id="bgm" src="https://petitacode.web2050.jp/users/original/emma/%E3%82%A2%E3%82%B2%E3%83%8F%E3%83%81%E3%83%A7%E3%82%A6%E3%81%AE%E3%83%80%E3%83%B3%E3%82%B9.mp3" preload="auto"></audio>

    <script>
        // ゲーム設定(楽曲に合わせてここで調整可能です)
        const CONFIG = {
            BPM: 130,          // 楽曲の想定テンポ(1分間のビート数)
            START_OFFSET: 1.0, // 最初のノーツが来るまでの時間(秒)
            TARGET_Y: 500,     // 判定ラインのY座標(キャンバス高さ600に対して)
            LANE_WIDTH: 70,    // 各レーンの幅(ピクセル)
            KEYS: ['d', 'f', 'j', 'k'] // 対応キーボード配列
        };

        // DOM要素の取得
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const bgm = document.getElementById('bgm');
        const startScreen = document.getElementById('start-screen');
        const resultScreen = document.getElementById('result-screen');
        const uiHeader = document.getElementById('ui-header');
        const scoreDisplay = document.getElementById('score-display');
        const comboDisplay = document.getElementById('combo-display');
        const judgementText = document.getElementById('judgement-text');

        // ゲーム状態
        let isPlaying = false;
        let score = 0;
        let combo = 0;
        let maxCombo = 0;
        let notes = [];
        let particles = [];
        let laneFlashes = [0, 0, 0, 0]; // レーンが光るエフェクト用
        let animationId;
        let currentSpeed = 600; // ノーツの落下速度
        let currentDifficulty = 'NORMAL'; // 現在の難易度
        let targetBgHue = 220; // 背景の目標色相
        let currentBgHue = 220; // 現在の背景色相

        // アゲハ蝶の背景エフェクト用
        let butterflies = [];
        const BUTTERFLY_COUNT = 15; // 周りを飛んでいる感を強調するため蝶の数を増加

        // レーンの描画開始X座標(中央寄せ)
        const TOTAL_LANE_WIDTH = CONFIG.LANE_WIDTH * 4;
        const OFFSET_X = (canvas.width - TOTAL_LANE_WIDTH) / 2;

        // レーンごとのテーマカラー (シアン, ピンク, イエロー, ライム)
        const LANE_COLORS = ['#00e5ff', '#ff00a0', '#ffeb3b', '#00e676'];

        // BPMから自動的にノーツ(譜面)を生成する関数
        function generateChart(difficulty) {
            notes = [];
            const secPerBeat = 60 / CONFIG.BPM;
            let currentTime = CONFIG.START_OFFSET;
            
            // 約2分半(150秒)分のノーツを生成
            while (currentTime < 150) {
                const rand = Math.random();
                
                if (difficulty === 'EASY') {
                    // EASY: 4分音符メイン、同時押しなし、休符多め
                    if (rand < 0.6) {
                        notes.push({ time: currentTime, lane: Math.floor(Math.random() * 4), active: true });
                    }
                    currentTime += secPerBeat;
                } else if (difficulty === 'HARD') {
                    // HARD: 8分音符・16分音符多め、同時押し多め
                    if (rand < 0.3) {
                        // 同時押し
                        let l1 = Math.floor(Math.random() * 4);
                        let l2 = (l1 + 1 + Math.floor(Math.random() * 3)) % 4;
                        notes.push({ time: currentTime, lane: l1, active: true });
                        notes.push({ time: currentTime, lane: l2, active: true });
                        currentTime += secPerBeat;
                    } else if (rand < 0.7) {
                        // 8分音符連打
                        notes.push({ time: currentTime, lane: Math.floor(Math.random() * 4), active: true });
                        notes.push({ time: currentTime + secPerBeat / 2, lane: Math.floor(Math.random() * 4), active: true });
                        currentTime += secPerBeat;
                    } else {
                        // 16分音符の細かい連打
                        let l = Math.floor(Math.random() * 4);
                        notes.push({ time: currentTime, lane: l, active: true });
                        notes.push({ time: currentTime + secPerBeat / 4, lane: (l+1)%4, active: true });
                        notes.push({ time: currentTime + secPerBeat / 2, lane: (l+2)%4, active: true });
                        currentTime += secPerBeat;
                    }
                } else {
                    // NORMAL: 標準パターン
                    if (rand < 0.5) {
                        // 単押し
                        notes.push({ time: currentTime, lane: Math.floor(Math.random() * 4), active: true });
                        currentTime += secPerBeat;
                    } else if (rand < 0.75) {
                        // 同時押し(2つ)
                        let l1 = Math.floor(Math.random() * 4);
                        let l2 = (l1 + 1 + Math.floor(Math.random() * 3)) % 4; // l1以外のレーン
                        notes.push({ time: currentTime, lane: l1, active: true });
                        notes.push({ time: currentTime, lane: l2, active: true });
                        currentTime += secPerBeat;
                    } else {
                        // 8分音符(半拍ずらし)
                        notes.push({ time: currentTime, lane: Math.floor(Math.random() * 4), active: true });
                        notes.push({ time: currentTime + secPerBeat / 2, lane: Math.floor(Math.random() * 4), active: true });
                        currentTime += secPerBeat;
                    }
                }
            }
        }

        // アゲハ蝶を生成する関数
        function createButterfly() {
            return {
                x: Math.random() * canvas.width,
                y: Math.random() * canvas.height,
                vx: (Math.random() - 0.5) * 2,
                vy: -Math.random() * 2 - 0.5, // 基本的に上へ飛ぶ
                size: Math.random() * 15 + 10, // 蝶の大きさ
                // アゲハチョウをイメージした黄色や幻想的なネオンカラー
                color: ['#ffeb3b', '#ffb7e6', '#00e5ff', '#ffff8d'][Math.floor(Math.random() * 4)], 
                flapPhase: Math.random() * Math.PI * 2, // 羽ばたきのタイミング
                flapSpeed: Math.random() * 0.1 + 0.05,  // 羽ばたきの速さ
                wobble: Math.random() * Math.PI * 2     // ゆらゆら飛ぶための値
            };
        }

        // 蝶の初期化
        function initButterflies() {
            butterflies = [];
            for(let i=0; i<BUTTERFLY_COUNT; i++){
                butterflies.push(createButterfly());
            }
        }

        // 背景の蝶を更新・描画
        function updateAndDrawButterflies() {
            for (let i = 0; i < butterflies.length; i++) {
                let b = butterflies[i];
                
                // ゆらゆら動かす
                b.wobble += 0.05;
                b.x += b.vx + Math.sin(b.wobble) * 1.5;
                b.y += b.vy;
                b.flapPhase += b.flapSpeed; // 羽ばたき
                
                // 画面外に出たら再配置
                if (b.y < -50 || b.x < -50 || b.x > canvas.width + 50 || b.y > canvas.height + 50) {
                    butterflies[i] = createButterfly();
                    if (Math.random() > 0.5) {
                        butterflies[i].y = canvas.height + 30; // 下から
                        butterflies[i].vy = -Math.abs(butterflies[i].vy);
                    } else {
                        butterflies[i].x = Math.random() > 0.5 ? -30 : canvas.width + 30; // 横から
                        butterflies[i].vx = butterflies[i].x < 0 ? Math.abs(butterflies[i].vx) : -Math.abs(butterflies[i].vx);
                    }
                }
                
                // 進行方向の角度を計算
                let targetAngle = Math.atan2(b.vy, b.vx + Math.sin(b.wobble)*1.5) + Math.PI/2;
                
                ctx.save();
                ctx.translate(b.x, b.y);
                ctx.rotate(targetAngle);
                
                // 羽ばたきのスケール変形(左右にパタパタ)
                let flapScale = Math.sin(b.flapPhase);
                ctx.scale(flapScale, 1);
                
                // 蝶の描画
                ctx.globalAlpha = 0.6;
                ctx.fillStyle = b.color;
                ctx.shadowBlur = 15;
                ctx.shadowColor = b.color;
                
                // 上羽
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.bezierCurveTo(-b.size*1.2, -b.size, -b.size*1.5, b.size*0.2, 0, b.size*0.3);
                ctx.bezierCurveTo(b.size*1.5, b.size*0.2, b.size*1.2, -b.size, 0, 0);
                ctx.fill();
                
                // 下羽
                ctx.beginPath();
                ctx.moveTo(0, b.size*0.2);
                ctx.bezierCurveTo(-b.size*0.8, b.size*0.6, -b.size*0.6, b.size*1.5, 0, b.size*0.8);
                ctx.bezierCurveTo(b.size*0.6, b.size*1.5, b.size*0.8, b.size*0.6, 0, b.size*0.2);
                ctx.fill();

                // 胴体
                ctx.fillStyle = '#222';
                ctx.shadowBlur = 0;
                ctx.beginPath();
                ctx.ellipse(0, b.size*0.2, b.size*0.15, b.size*0.5, 0, 0, Math.PI*2);
                ctx.fill();

                ctx.restore();
            }
        }

        // メインループ
        function gameLoop() {
            // --- 背景の色相をなめらかに変化させる ---
            if (Math.abs(targetBgHue - currentBgHue) > 0.1) {
                currentBgHue += (targetBgHue - currentBgHue) * 0.05; // 0.05の割合で徐々に目標の色に近づける
                document.getElementById('game-container').style.setProperty('--bg-hue', currentBgHue % 360);
            }

            // 画面のクリア
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // --- 背景のアゲハ蝶の更新と描画 ---
            updateAndDrawButterflies();

            // プレイ中のみゲーム用の描画を行う
            if (isPlaying) {
                // 現在の音楽の再生時間を取得(ノーツの位置計算の基準)
                const audioTime = bgm.currentTime;

                // --- 背景/レーンの描画 ---
                drawLanes();

                // --- ノーツの更新と描画 ---
                updateAndDrawNotes(audioTime);

                // --- パーティクル(ヒットエフェクト)の更新と描画 ---
                updateAndDrawParticles();

                // --- 判定ラインの描画 ---
                drawTargetLine();

                // 楽曲が終了したらリザルト画面へ
                if (bgm.ended || (audioTime > 0 && bgm.paused)) {
                    endGame();
                }
            }

            animationId = requestAnimationFrame(gameLoop);
        }

        function drawLanes() {
            for (let i = 0; i < 4; i++) {
                const x = OFFSET_X + i * CONFIG.LANE_WIDTH;
                
                // レーンの区切り線
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(x, 0);
                ctx.lineTo(x, canvas.height);
                ctx.stroke();

                // キーを押したときのレーンのフラッシュ
                if (laneFlashes[i] > 0) {
                    const gradient = ctx.createLinearGradient(0, CONFIG.TARGET_Y, 0, 0);
                    gradient.addColorStop(0, `${LANE_COLORS[i]}${Math.floor(laneFlashes[i] * 120).toString(16).padStart(2, '0')}`); // 透過度付きHex
                    gradient.addColorStop(1, 'rgba(0,0,0,0)');
                    
                    ctx.fillStyle = gradient;
                    ctx.fillRect(x, 0, CONFIG.LANE_WIDTH, canvas.height);
                    laneFlashes[i] -= 0.05; // フラッシュを徐々に減衰
                }
            }
            // 右端の線
            ctx.beginPath();
            ctx.moveTo(OFFSET_X + 4 * CONFIG.LANE_WIDTH, 0);
            ctx.lineTo(OFFSET_X + 4 * CONFIG.LANE_WIDTH, canvas.height);
            ctx.stroke();
        }

        function updateAndDrawNotes(audioTime) {
            // 最適化のため、画面外の下に落ちたノーツは描画しない
            for (let i = 0; i < notes.length; i++) {
                let note = notes[i];
                if (!note.active) continue;

                // 落下距離を計算:Y = 判定ライン - (ノーツのタイミング - 現在時間) * スピード
                let y = CONFIG.TARGET_Y - (note.time - audioTime) * currentSpeed;

                // 画面外(まだ上すぎる)場合は描画スキップ
                if (y < -50) continue;

                // ミス判定(判定ラインを一定以上通り過ぎたら)
                if (audioTime - note.time > 0.15) {
                    note.active = false;
                    combo = 0;
                    updateUI();
                    displayJudgement("ミス", "#ff3333");
                    continue;
                }

                // ノーツの描画
                const x = OFFSET_X + note.lane * CONFIG.LANE_WIDTH + 5; // 余白をとる
                const width = CONFIG.LANE_WIDTH - 10;
                const height = 25; // ノーツの厚みを20から25に増やして少し大きく

                ctx.fillStyle = LANE_COLORS[note.lane];
                ctx.shadowColor = LANE_COLORS[note.lane];
                ctx.shadowBlur = 15;
                
                // 角丸四角形の描画(最近のブラウザ標準機能)
                ctx.beginPath();
                if (ctx.roundRect) {
                    ctx.roundRect(x, y - height/2, width, height, 5);
                } else {
                    ctx.fillRect(x, y - height/2, width, height); // フォールバック
                }
                ctx.fill();
                
                // シャドウリセット
                ctx.shadowBlur = 0;
            }
        }

        function drawTargetLine() {
            for (let i = 0; i < 4; i++) {
                const x = OFFSET_X + i * CONFIG.LANE_WIDTH;
                
                // ターゲットの枠(高さを30から50に拡大)
                ctx.strokeStyle = laneFlashes[i] > 0 ? LANE_COLORS[i] : 'rgba(255, 255, 255, 0.4)';
                ctx.lineWidth = laneFlashes[i] > 0 ? 4 : 2; // 光ったときの枠線も少し太く
                ctx.strokeRect(x + 5, CONFIG.TARGET_Y - 25, CONFIG.LANE_WIDTH - 10, 50);

                // ガイドのキー文字表示(文字サイズを16pxから20pxに拡大)
                ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
                ctx.font = '20px "Press Start 2P"';
                ctx.textAlign = 'center';
                ctx.fillText(CONFIG.KEYS[i].toUpperCase(), x + CONFIG.LANE_WIDTH / 2, CONFIG.TARGET_Y + 8);
            }
        }

        function createParticles(x, y, color) {
            for (let i = 0; i < 15; i++) {
                particles.push({
                    x: x,
                    y: y,
                    vx: (Math.random() - 0.5) * 10,
                    vy: (Math.random() - 0.5) * 10 - 2, // やや上向きに飛ぶ
                    life: 1.0,
                    color: color
                });
            }
        }

        function updateAndDrawParticles() {
            for (let i = particles.length - 1; i >= 0; i--) {
                let p = particles[i];
                p.x += p.vx;
                p.y += p.vy;
                p.life -= 0.03; // 寿命を減らす

                if (p.life <= 0) {
                    particles.splice(i, 1);
                    continue;
                }

                ctx.fillStyle = p.color;
                ctx.globalAlpha = p.life;
                ctx.beginPath();
                ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
                ctx.fill();
                ctx.globalAlpha = 1.0; // リセット
            }
        }

        // 入力に対する判定処理
        function handleInput(laneIndex) {
            if (!isPlaying) return;
            
            laneFlashes[laneIndex] = 1.0; // レーンを光らせる
            targetBgHue += 8; // キーに触れる(叩く)ごとに背景の色を少し進める
            
            const audioTime = bgm.currentTime;
            let closestNote = null;
            let minDiff = Infinity;

            // 押したレーンで、現在時刻に一番近いアクティブなノーツを探す
            for (let note of notes) {
                if (note.active && note.lane === laneIndex) {
                    let diff = Math.abs(note.time - audioTime);
                    if (diff < minDiff) {
                        minDiff = diff;
                        closestNote = note;
                    }
                }
            }

            // 判定基準(秒)
            if (closestNote && minDiff <= 0.15) {
                closestNote.active = false;
                
                // エフェクト発生
                const pX = OFFSET_X + laneIndex * CONFIG.LANE_WIDTH + CONFIG.LANE_WIDTH / 2;
                createParticles(pX, CONFIG.TARGET_Y, LANE_COLORS[laneIndex]);

                // 精度によるスコア計算
                if (minDiff <= 0.05) {
                    score += 100;
                    combo++;
                    displayJudgement("パーフェクト", "#00ffff");
                } else if (minDiff <= 0.10) {
                    score += 50;
                    combo++;
                    displayJudgement("グレート", "#00ff00");
                } else {
                    score += 10;
                    combo++;
                    displayJudgement("グッド", "#ffff00");
                }

                if (combo > maxCombo) maxCombo = combo;
                updateUI();
            }
        }

        // キーボード入力リスナー
        window.addEventListener('keydown', (e) => {
            if (e.repeat) return; // 押しっぱなしによる連続発火を防止
            const key = e.key.toLowerCase();
            const laneIndex = CONFIG.KEYS.indexOf(key);
            if (laneIndex !== -1) {
                handleInput(laneIndex);
            }
        });

        // タッチ(スマホ)入力リスナー
        canvas.addEventListener('touchstart', (e) => {
            e.preventDefault(); // スクロール防止
            const rect = canvas.getBoundingClientRect();
            
            for (let i = 0; i < e.changedTouches.length; i++) {
                const touch = e.changedTouches[i];
                // キャンバス内の相対X座標を計算
                const x = touch.clientX - rect.left;
                // キャンバスの描画スケールに合わせて調整
                const scaleX = canvas.width / rect.width;
                const canvasX = x * scaleX;
                
                // どのレーンをタップしたか判定
                if (canvasX >= OFFSET_X && canvasX <= OFFSET_X + TOTAL_LANE_WIDTH) {
                    const laneIndex = Math.floor((canvasX - OFFSET_X) / CONFIG.LANE_WIDTH);
                    handleInput(laneIndex);
                }
            }
        }, { passive: false });

        // UIの更新
        function updateUI() {
            scoreDisplay.innerText = score;
            comboDisplay.innerText = combo;
        }

        // 判定テキストを画面中央にアニメーション表示
        function displayJudgement(text, color) {
            judgementText.innerText = text;
            judgementText.style.color = color;
            judgementText.style.textShadow = `0 0 20px ${color}`;
            
            // アニメーションを再スタートさせるためにクラスを付け直す
            judgementText.classList.remove('animate-judge');
            void judgementText.offsetWidth; // DOMリフローを強制してアニメーションリセット
            judgementText.classList.add('animate-judge');
        }

        // ゲーム開始処理
        function startGame(difficulty) {
            currentDifficulty = difficulty || 'NORMAL';
            
            // 難易度に応じて落下スピードを変更
            if (currentDifficulty === 'EASY') currentSpeed = 400;
            else if (currentDifficulty === 'NORMAL') currentSpeed = 600;
            else if (currentDifficulty === 'HARD') currentSpeed = 850;

            startScreen.classList.add('hidden');
            resultScreen.classList.add('hidden');
            uiHeader.classList.remove('hidden');
            
            score = 0;
            combo = 0;
            maxCombo = 0;
            targetBgHue = 220; // 背景の色相をリセット(初期状態の青)
            currentBgHue = 220;
            document.getElementById('game-container').style.setProperty('--bg-hue', currentBgHue);
            updateUI();
            
            generateChart(currentDifficulty); // 難易度を渡して譜面生成
            
            bgm.currentTime = 0;
            bgm.play().then(() => {
                isPlaying = true;
                // gameLoopは常に回っているため、フラグをオンにするだけで開始
            }).catch(e => {
                console.error("Audio play failed:", e);
                alert("音声の再生に失敗しました。ブラウザの自動再生制限の可能性があります。");
                startScreen.classList.remove('hidden');
            });
        }

        // ゲーム終了処理
        function endGame() {
            isPlaying = false;
            bgm.pause();
            
            uiHeader.classList.add('hidden');
            resultScreen.classList.remove('hidden');
            
            document.getElementById('final-score').innerText = score;
            document.getElementById('final-combo').innerText = maxCombo;
        }

        // ボタンのイベントリスナー登録
        document.querySelectorAll('.diff-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                startGame(e.target.getAttribute('data-diff'));
            });
        });
        document.getElementById('retry-btn').addEventListener('click', () => {
            startGame(currentDifficulty);
        });

        // 起動時に蝶を初期化し、アニメーションループをスタートしておく
        initButterflies();
        gameLoop();

    </script>
</body>
</html>

アゲハチョウが踊っている音ゲーだよ

by emma

この作品はアゲハチョウが周りを飛んでて、かんたん・ふつう・むずかしいが選べる作品です。

👁 52 回閲覧
📅 投稿:2026年7月4日 🔄 更新:2026年8月13日
応援スタンプを押してみよう!