音ゲーっぽいものを作ってみたOtoGee

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>MIDI Game Final Fix</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
    <script src="https://unpkg.com/@tonejs/midi"></script>
    <style>
        body { margin: 0; background: #000; color: white; font-family: monospace; overflow: hidden; }
        
        /* キャンバスの配置と枠線(存在がわかるように赤枠を一瞬だけ表示して確認可能に) */
        canvas { 
            display: block; 
            margin: 10px auto; 
            background: #222; 
            border: 2px solid #555; 
            cursor: crosshair; 
        }

        #ui { 
            position: absolute; 
            top: 0; 
            left: 0; 
            width: 100%;
            background: rgba(0, 0, 0, 0.8); 
            padding: 10px; 
            box-sizing: border-box;
            border-bottom: 1px solid #444;
            display: flex;
            gap: 10px;
            align-items: center;
            justify-content: center;
        }

        button { 
            background: #444; color: white; border: 1px solid #888; 
            padding: 5px 15px; cursor: pointer; font-size: 14px; 
        }
        button:hover { background: #666; }
        
        /* ステータス表示 */
        #debugInfo { font-size: 12px; color: #aaa; margin-left: 10px; }
    </style>
</head>
<body>

<div id="ui">
    <span>MIDI音ゲー</span>
    <input type="file" id="fileInput" accept=".mid,.midi" style="width: 200px;">
    <button id="testBtn">🔊 テスト音</button>
    <button id="startBtn" disabled>スタート</button>
    <span id="debugInfo">待機中...</span>
</div>

<canvas id="gameCanvas" width="600" height="500"></canvas>

<script>
    // --- 設定 ---
    // 画面サイズを小さくして、確実に見えるように変更
    const CANVAS_WIDTH = 600;
    const CANVAS_HEIGHT = 500;
    const TARGET_Y = 400;        // 判定エリア(上から400pxの位置)
    const TARGET_HEIGHT = 60;
    const TARGET_BOTTOM = TARGET_Y + TARGET_HEIGHT;
    
    // --- 変数 ---
    let canvas = document.getElementById('gameCanvas');
    let ctx = canvas.getContext('2d');
    let notes = [];
    let isPlaying = false;
    let startTime = 0;
    let synth = null;

    // 演出用
    let areaFlash = 0.0;
    let debugClick = null; // クリック位置表示用 {x, y, timer}

    const debugInfo = document.getElementById('debugInfo');

    // --- 初期化 ---
    window.onload = () => {
        console.log("初期化開始");
        
        // 1. シンセサイザー作成(ビープ音)
        synth = new Tone.PolySynth(Tone.Synth, {
            volume: -5,
            oscillator: { type: "square" } 
        }).toDestination();

        // 2. 描画ループを即座に開始(これで画面が真っ黒ということはなくなります)
        requestAnimationFrame(loop);
    };

    // --- メインループ(常に回り続ける) ---
    function loop() {
        // 背景クリア
        ctx.fillStyle = "#222";
        ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

        // 1. 判定エリアの描画
        // 点滅計算
        if (areaFlash > 0) areaFlash -= 0.1;
        if (areaFlash < 0) areaFlash = 0;

        let r = 0, g = 255, b = 255; // 水色
        if (areaFlash > 0) { // 点滅時は白っぽく
            r = 255; 
        }
        
        ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${0.3 + areaFlash * 0.4})`;
        ctx.fillRect(0, TARGET_Y, CANVAS_WIDTH, TARGET_HEIGHT);
        
        ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 1)`;
        ctx.lineWidth = 2;
        ctx.strokeRect(0, TARGET_Y, CANVAS_WIDTH, TARGET_HEIGHT);
        
        ctx.fillStyle = "white";
        ctx.font = "14px monospace";
        ctx.fillText("JUDGEMENT AREA (CLICK HERE)", 10, TARGET_Y + 35);

        // 2. ノートの描画
        const now = isPlaying ? (Tone.now() - startTime) : 0;
        
        notes.forEach(note => {
            // ゲーム中なら落下、そうでなければ待機位置
            if (isPlaying) {
                note.y = TARGET_Y - (note.time - now) * 200; // 落下速度
            }

            // フラッシュ減衰
            if (note.flash > 0) {
                note.flash -= 0.1;
                if (note.flash < 0) note.flash = 0;
            }

            // 画面内なら描画
            if (note.y > -50 && note.y < CANVAS_HEIGHT) {
                // 通常色 or 光った色
                ctx.fillStyle = note.flash > 0 ? "white" : note.color;
                ctx.fillRect(note.x, note.y, note.w, note.h);
                ctx.strokeRect(note.x, note.y, note.w, note.h);
            }
        });

        // 3. デバッグ:クリック位置に黄色い丸を表示
        if (debugClick) {
            ctx.beginPath();
            ctx.arc(debugClick.x, debugClick.y, 10, 0, Math.PI * 2);
            ctx.fillStyle = "yellow";
            ctx.fill();
            debugClick.timer--;
            if (debugClick.timer <= 0) debugClick = null;
        }

        requestAnimationFrame(loop);
    }

    // --- 入力イベント(マウス&タッチ) ---
    canvas.addEventListener('mousedown', onInput);
    canvas.addEventListener('touchstart', (e) => { e.preventDefault(); onInput(e.touches[0]); }, {passive: false});

    async function onInput(e) {
        // AudioContextの再開確認
        if (Tone.context.state !== 'running') {
            await Tone.start();
        }

        // 座標計算
        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX || e.pageX) - rect.left;
        const y = (e.clientY || e.pageY) - rect.top;

        // デバッグ表示更新
        debugInfo.innerText = `Click: X=${Math.floor(x)} Y=${Math.floor(y)}`;
        // 画面に黄色い丸を出す
        debugClick = { x: x, y: y, timer: 10 };

        // 判定エリア内か?
        if (y >= TARGET_Y && y <= TARGET_BOTTOM) {
            // エリア点滅開始
            areaFlash = 1.0;
            
            // 強制発音(テスト)
            // エリアをクリックしたら、とりあえず音を出す(ゲーム中でなくても)
            synth.triggerAttackRelease("C5", "32n"); 

            // エリア内のノートを探して鳴らす
            notes.forEach(note => {
                if (note.y + note.h >= TARGET_Y && note.y <= TARGET_BOTTOM) {
                    playNote(note);
                }
            });
        } 
        // ノート直接クリック
        else {
            notes.forEach(note => {
                if (x >= note.x && x <= note.x + note.w &&
                    y >= note.y && y <= note.y + note.h) {
                    playNote(note);
                }
            });
        }
    }

    function playNote(note) {
        synth.triggerAttackRelease(note.name, "8n");
        note.flash = 1.0; // ノートを光らせる
    }

    // --- UIイベント ---
    document.getElementById('testBtn').addEventListener('click', async () => {
        await Tone.start();
        synth.triggerAttackRelease("C4", "8n");
        debugInfo.innerText = "テスト音再生";
        areaFlash = 1.0; // テストでも画面を光らせる
    });

    document.getElementById('fileInput').addEventListener('change', async (e) => {
        const file = e.target.files[0];
        if (!file) return;
        
        debugInfo.innerText = "MIDI解析中...";
        const reader = new FileReader();
        reader.onload = function(ev) {
            try {
                const midiData = new Midi(ev.target.result);
                notes = [];
                const colors = ["#F00", "#0F0", "#00F", "#FF0", "#0FF", "#F0F"];
                
                midiData.tracks.forEach((track, i) => {
                    track.notes.forEach(n => {
                        notes.push({
                            name: n.name, time: n.time, 
                            x: 50 + (n.midi % 12) * 42, y: -100, w: 40, h: 20,
                            color: colors[i % colors.length], flash: 0
                        });
                    });
                });
                notes.sort((a,b) => a.time - b.time);
                
                document.getElementById('startBtn').disabled = false;
                debugInfo.innerText = `読込完了: ${notes.length}音`;
            } catch(err) {
                console.error(err);
                debugInfo.innerText = "エラー";
            }
        };
        reader.readAsArrayBuffer(file);
    });

    document.getElementById('startBtn').addEventListener('click', async () => {
        await Tone.start();
        startTime = Tone.now() + 1.0;
        isPlaying = true;
        debugInfo.innerText = "再生中";
    });

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

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
音ゲーっぽいものを作ってみたOtoGee|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1