メインコンテンツへスキップ
見出し画像

HTMLだけで作る面白いビジュアルシンセサイザー

    自宅をクラブにして遊んでみた時に「シンセサイザー」があれば面白いと思いませんか?

    超簡単HTML1枚のみでそれを実現することができます。

    画像
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>生体ピクセル・シンセサイザー</title>
        <style>
            /* 制限を逆手に取る:あえてピクセルを粗くして「レトロ/生体感」を演出 */
            body {
                margin: 0;
                overflow: hidden;
                background-color: #0a0a0a;
                font-family: 'Courier New', monospace;
            }
            canvas {
                display: block;
                width: 100vw;
                height: 100vh;
                /* キモ:ピクセルをボヤけさせず、ジャギー(角張り)を強調することで生物的な揺らぎを際立たせる */
                image-rendering: crisp-edges; 
                image-rendering: pixelated;
                cursor: none;
            }
            #ui {
                position: fixed;
                bottom: 30px;
                left: 0;
                right: 0;
                text-align: center;
                color: rgba(180, 255, 200, 0.6);
                font-size: 14px;
                letter-spacing: 2px;
                pointer-events: none;
                text-shadow: 0 0 15px rgba(0, 255, 100, 0.3);
                background: rgba(0,0,0,0.4);
                padding: 12px;
                backdrop-filter: blur(4px);
                width: fit-content;
                margin: 0 auto;
                border-radius: 40px;
                border: 1px solid rgba(255,255,255,0.05);
            }
            #ui span {
                display: inline-block;
                animation: pulse 2s ease-in-out infinite;
            }
            @keyframes pulse {
                0% { opacity: 0.4; transform: scale(1); }
                50% { opacity: 1; transform: scale(1.02); }
                100% { opacity: 0.4; transform: scale(1); }
            }
            /* マウスの座標を表示するミニマルなクロスヘア(代替) */
            #freq-display {
                position: fixed;
                top: 20px;
                right: 30px;
                color: #8affb2;
                font-size: 13px;
                background: rgba(0,0,0,0.7);
                padding: 8px 16px;
                border-radius: 20px;
                border-left: 3px solid #00ff88;
                pointer-events: none;
                opacity: 0.7;
                backdrop-filter: blur(2px);
            }
        </style>
    </head>
    <body>
    
        <canvas id="canvas"></canvas>
        <div id="freq-display">🎵 440Hz</div>
        <div id="ui"><span>🖱️ 左右で音程 / 上下で音量 & 歪み | クリックで音をON</span></div>
    
        <script>
            (function(){
                "use strict";
    
                // --- 1. キャンバス設定 (制限の中での解像度戦略) ---
                const canvas = document.getElementById('canvas');
                const ctx = canvas.getContext('2d');
                
                // あえて解像度を 120x80 に制限。これが「制限を面白さに変える」魔法。
                const COLS = 120;
                const ROWS = 80;
                let W, H;
    
                function resize() {
                    W = window.innerWidth;
                    H = window.innerHeight;
                    canvas.width = COLS;
                    canvas.height = ROWS;
                    canvas.style.width = W + 'px';
                    canvas.style.height = H + 'px';
                }
                window.addEventListener('resize', resize);
                resize();
    
                // --- 2. Web Audio API (ブラウザの制限: ユーザージェスチャーで起動) ---
                let audioCtx = null;
                let oscillator = null;
                let gainNode = null;
                let isAudioReady = false;
    
                function initAudio() {
                    if (isAudioReady) return;
                    try {
                        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                        
                        // メイン音源:サイン波+少しの歪みを加えるため、2つ目のオシレーターを用意
                        oscillator = audioCtx.createOscillator();
                        const oscillator2 = audioCtx.createOscillator();
                        const gain2 = audioCtx.createGain();
                        
                        gainNode = audioCtx.createGain();
                        gainNode.gain.value = 0.15; // 初期音量
    
                        // 倍音を加えて厚みを出す(制限環境でもリッチに聞こせる工夫)
                        oscillator.type = 'sawtooth'; // 鋸歯状波で温かみ
                        oscillator.frequency.value = 440;
                        
                        oscillator2.type = 'sine';
                        oscillator2.frequency.value = 880; // 1オクターブ上
                        gain2.gain.value = 0.03; // サブ的に混ぜる
    
                        // 接続
                        oscillator.connect(gainNode);
                        oscillator2.connect(gain2);
                        gain2.connect(gainNode);
                        gainNode.connect(audioCtx.destination);
    
                        oscillator.start();
                        oscillator2.start();
    
                        isAudioReady = true;
                        document.getElementById('ui').innerHTML = '<span>🔊 音が鳴ってる! マウスを動かして遊んでね</span>';
                    } catch(e) {
                        alert('このブラウザでは音が出せませんが、ビジュアルは楽しめます!');
                    }
                }
    
                // ユーザーのクリックで音を起動 (ブラウザの自動再生ポリシー対策)
                document.addEventListener('click', () => {
                    if (!isAudioReady) {
                        initAudio();
                    } else if (audioCtx && audioCtx.state === 'suspended') {
                        audioCtx.resume();
                    }
                });
    
                // --- 3. マウス / タッチ 情報 ---
                let mouseX = 0.5; // 0-1 正規化
                let mouseY = 0.5;
                const freqDisplay = document.getElementById('freq-display');
    
                function updateMouse(clientX, clientY) {
                    mouseX = Math.min(1, Math.max(0, clientX / window.innerWidth));
                    mouseY = Math.min(1, Math.max(0, clientY / window.innerHeight));
                    
                    // 音程を更新 (100Hz ~ 1200Hz)
                    if (isAudioReady && oscillator) {
                        const freq = 80 + (mouseX * 1100);
                        oscillator.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.01);
                        freqDisplay.textContent = `🎵 ${Math.round(freq)}Hz`;
                        
                        // 音量をマウスYに連動 (0.01 ~ 0.35)
                        const vol = 0.01 + (mouseY * 0.34);
                        gainNode.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.01);
                    }
                }
    
                document.addEventListener('mousemove', (e) => {
                    updateMouse(e.clientX, e.clientY);
                });
    
                // タッチ対応 (スマホでも遊べる)
                document.addEventListener('touchmove', (e) => {
                    e.preventDefault();
                    const touch = e.touches[0];
                    if (touch) updateMouse(touch.clientX, touch.clientY);
                }, { passive: false });
    
                // マウスがウィンドウ外に出たら、中央に固定しない(最後の位置を保持)
    
                // --- 4. コア:ピクセルシェーダーごっこ (生のImageDataを操る) ---
                // ここが「マップ」や「3D」より遥かに面白いエンジン
                
                let time = 0;
    
                function draw() {
                    time += 0.02;
    
                    // ImageData を取得 (ピクセル配列)
                    const imageData = ctx.getImageData(0, 0, COLS, ROWS);
                    const data = imageData.data;
    
                    // マウスの影響をピクセル座標に変換 (0~COLS, 0~ROWS)
                    const mx = mouseX * COLS;
                    const my = mouseY * ROWS;
    
                    for (let y = 0; y < ROWS; y++) {
                        for (let x = 0; x < COLS; x++) {
                            const i = (y * COLS + x) * 4;
    
                            // --- ここが「生物的な歪み」の核 ---
                            // 1. マウスからの距離で波紋を作る
                            const dx = x - mx;
                            const dy = y - my;
                            const dist = Math.sqrt(dx*dx + dy*dy) + 0.1;
    
                            // 2. 時間と座標で変化する複雑な波 (サインとコサインを掛け合わせてノイズ感)
                            const wave1 = Math.sin(x * 0.3 + time * 1.5) * Math.cos(y * 0.2 + time * 1.2);
                            const wave2 = Math.sin(dist * 0.4 - time * 2.0) * 0.8;
                            const wave3 = Math.sin(x * 0.05 + y * 0.07 + time * 0.8) * 0.5;
                            
                            // 3. マウスYで「歪みの強さ」が変わる (上に上げると激しく歪む)
                            const distortion = 0.5 + mouseY * 1.5;
                            let val = (wave1 * 0.7 + wave2 * 0.5 + wave3 * 0.3) * distortion;
    
                            // 4. -1~1 の値を 0~1 にクランプ
                            val = (val + 1) / 2; 
                            val = Math.min(1, Math.max(0, val));
    
                            // --- カラーマッピング (単なるグレーじゃない! 有機的な蛍光色) ---
                            // マウスXで色相がシフト (左:青緑系、右:赤紫系)
                            const hueShift = mouseX * 0.8;
                            
                            // 赤: 波の値に応じて、かつX/Yで偏りをつける
                            const r = Math.sin(val * Math.PI * 2 + hueShift) * 0.8 + 0.6;
                            // 緑: 中心は強めに
                            const g = Math.cos(val * Math.PI * 1.5 - hueShift * 0.5) * 0.7 + 0.7;
                            // 青: 波の逆相で
                            const b = Math.sin(val * Math.PI * 1.8 + 1.2 - hueShift) * 0.6 + 0.6;
    
                            // 距離が近いほど白っぽく光らせる (マウス周辺がホットスポットに)
                            const glow = Math.min(1, 2.5 / (dist + 0.5));
                            
                            data[i]     = Math.min(255, (r * 0.8 + glow * 0.4) * 255);
                            data[i + 1] = Math.min(255, (g * 0.7 + glow * 0.5) * 255);
                            data[i + 2] = Math.min(255, (b * 0.9 + glow * 0.3) * 255);
                            data[i + 3] = 255; // 不透明度
                        }
                    }
    
                    // 描画
                    ctx.putImageData(imageData, 0, 0);
    
                    requestAnimationFrame(draw);
                }
    
                // --- 5. 起動 ---
                draw();
    
                // 初期表示用にダミーで音程表示
                freqDisplay.textContent = '🎵 待機中... (クリックで起動)';
    
                // ウィンドウリサイズ時はキャンバスの見た目だけ変わる (解像度は固定)
                console.log('🔥 生体ピクセルシンセ起動! 制限を楽しむ新しい遊び方だ!');
    
            })();
        </script>
    </body>
    </html>


    あなたへのおすすめ