1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

社内BINGOゲーム大会用にルーレットアプリを開発しました

Last updated at Posted at 2024-10-18

はじめに

全社研修の際に、BINGO大会を行うので、そのためのルーレットを開発しました。

デモ

初期画面:
スクリーンショット 2024-10-18 13.06.59.png

会議室の大画面に映すので、左右にBINGOの一覧表を作成しています。

仕様

ボタン

スタートボタン、もしくはスペースキーを押下すると、ルーレットが開始ます。
ストップボタン、もしくはスペースキーを押下すると、ルーレットが停止ます。
リセットボタンを押下すると、初期化されます。
ビンゴボタン、もしくはエンターキーを押下すると、BINGOになった人を祝福します。

効果音

効果音は4種類で、htmlと同じ階層に音源ファイルを置いてます。

  • rolling.mp3: ルーレットが回っている時
  • stop.mp3: ストップボタンが押下された時
  • confirm.mp3: 番号が確定した時
  • win.mp3: 祝福された時

一覧表

1度選択された番号は、いつでも一覧表で確認できます。
直近の番号が赤色で、それよりも前の番号は黄色になります。

一覧表:
スクリーンショット 2024-10-18 13.07.35.png

祝福:
スクリーンショット 2024-10-18 13.08.21.png

コード

bingo.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>BINGOルーレット</title>
    <style>
        /* ページ全体の基本スタイル */
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);
            overflow: hidden;
            position: relative;
        }

        /* ビンゴ番号表示コンテナ */
        #bingoNumberContainer {
            position: fixed;
            top: 3%; /* 表示位置を上に少し調整 */
            left: 50%;
            transform: translateX(-50%);
            width: 80%;
            height: calc(100% - 170px); /* コントロールボタンの高さを考慮 */
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 1;
            background: none; /* 背景は初期状態では透明 */
        }

        /* ビンゴ番号の表示スタイル */
        #bingoNumber {
            font-size: 50vw; /* 画面の50%の幅に基づいたフォントサイズ */
            color: #fff;
            text-shadow: 4px 4px 0 #000; /* 黒い縁取り */
        }

        /* 点滅アニメーション(数字が選ばれた際の点滅効果) */
        @keyframes blink {
            0%, 100% {
                opacity: 1;
            }
            50% {
                opacity: 0;
            }
        }

        /* テーブルのセルが点滅する際のアニメーション */
        @keyframes tableBlink {
            0%, 100% {
                background-color: #ff0000; /* 赤色 */
                color: #000;
            }
            50% {
                background-color: transparent;
                color: #fff;
            }
        }

        /* コントロールボタンのコンテナ */
        #controls {
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            display: flex;
            justify-content: center;
            gap: 20px;
            padding: 20px;
            background: rgba(0, 0, 0, 0.5);
            z-index: 2;
        }

        /* ボタンの基本スタイル */
        .button {
            padding: 15px 30px;
            font-size: 1.5em;
            color: #fff;
            background: #ff6f61;
            border: none;
            border-radius: 30px;
            cursor: pointer;
            transition: background 0.3s ease;
            box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);
            z-index: 2;
        }

        /* ボタンのホバー時のスタイル */
        .button:hover {
            background: #ff4e42;
        }

        /* ボタンが無効化された時のスタイル */
        .button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }

        /* 番号リストの共通スタイル */
        .numberList {
            position: absolute;
            top: 10%;
            width: 15%;
            height: 80%;
            overflow: auto;
            z-index: 1;
        }

        /* 左右に配置する番号リスト */
        #leftNumberList {
            left: 0;
        }
        #rightNumberList {
            right: 0;
        }

        /* 番号テーブルのスタイル */
        .numberTable {
            font-size: 1.5em;
            color: #fff;
            background: rgba(0, 0, 0, 0.2);
            padding: 10px;
            border-radius: 10px;
            box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1);
            text-align: center;
            width: 100%;
            box-sizing: border-box;
        }

        /* テーブルのスタイル */
        table {
            border-collapse: collapse;
            width: 100%;
        }

        /* テーブルのセルやヘッダのスタイル */
        table, th, td {
            border: 1px solid #fff;
        }
        th, td {
            padding: 5px;
            text-align: center;
            color: #fff;
        }

        /* テーブルのヘッダ(BINGO文字)の背景色 */
        th {
            background-color: rgba(0, 0, 0, 0.5);
        }

        /* 番号が選ばれた際に強調されるスタイル */
        .highlight {
            background-color: #ff6f61;
        }

        /* 選ばれた番号を黄色で表示 */
        .marked {
            background-color: #ffff00;
            color: #000;
        }

        /* 最近選ばれた番号を赤で表示 */
        .recently-marked {
            background-color: #ff0000;
            color: #000;
        }

        /* ビンゴ成功時のメッセージ */
        #congratsMessage {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 8vw;
            color: #ff0000;
            text-shadow: 2px 2px 0 #000;
            z-index: 3;
            display: none;
        }

        /* 紙吹雪エフェクト用のキャンバス */
        #confettiCanvas {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 3;
        }
    </style>
</head>
<body>
    <!-- ビンゴ番号表示 -->
    <div id="bingoNumberContainer">
        <div id="bingoNumber">0</div>
    </div>

    <!-- コントロールボタン -->
    <div id="controls">
        <button id="startButton" class="button">スタート</button>
        <button id="resetButton" class="button" style="background: #6fafff;">リセット</button>
        <button id="bingoButton" class="button" style="background: #4caf50;">ビンゴ</button>
    </div>

    <!-- 番号リスト(左右) -->
    <div id="leftNumberList" class="numberList"></div>
    <div id="rightNumberList" class="numberList"></div>

    <!-- ビンゴ成功時のメッセージ -->
    <div id="congratsMessage">おめでとう!</div>

    <!-- 紙吹雪エフェクト用のキャンバス -->
    <canvas id="confettiCanvas"></canvas>

    <!-- 効果音 -->
    <audio id="rollingSound" src="rolling.mp3" loop></audio>
    <audio id="stopSound" src="stop.mp3"></audio>
    <audio id="confirmSound" src="confirm.mp3"></audio>
    <audio id="winSound" src="win.mp3"></audio>

    <script>
        let isRolling = false; // ルーレットが回転中かどうかのフラグ
        let bingoNumbers = Array.from({ length: 75 }, (_, i) => i + 1); // 1から75までのビンゴ番号リスト
        let chosenNumbers = []; // 選ばれた番号を格納する配列
        let interval; // ルーレット回転時のインターバル管理用
        let timeout; // タイマー管理用
        let lastChosenNumber = null; // 最後に選ばれた番号

        // 効果音の要素
        const rollingSound = document.getElementById('rollingSound');
        const stopSound = document.getElementById('stopSound');
        const confirmSound = document.getElementById('confirmSound');
        const winSound = document.getElementById('winSound');

        /**
         * 番号テーブルを生成する関数
         * 各BINGOの列に対応する番号をテーブルとして生成
         */
        const generateBingoTable = () => {
            return `
                <div class="numberTable">
                    <table>
                        <thead>
                            <tr>${'BINGO'.split('').map(v => `<th>${v}</th>`).join('')}</tr>
                        </thead>
                        <tbody>
                            ${Array.from({ length: 15 }, (_, j) =>
                                `<tr>${Array.from({ length: 5 }, (_, i) => `<td>${1 + i * 15 + j}</td>`).join('')}</tr>`
                            ).join('')}
                        </tbody>
                    </table>
                </div>
            `;
        };

        // 左右の番号リストにテーブルを挿入
        document.getElementById('leftNumberList').innerHTML = generateBingoTable();
        document.getElementById('rightNumberList').innerHTML = generateBingoTable();

        /**
         * 番号が選ばれた際にテーブル上の該当セルにハイライトを付ける
         * @param {number} number 選ばれた番号
         */
        const highlightNumber = (number) => {
            document.querySelectorAll('.numberTable td').forEach(cell => {
                cell.classList.remove('highlight');
                if (parseInt(cell.textContent) === number) {
                    cell.classList.add('highlight');
                }
            });
        };

        /**
         * 番号を「marked」状態にする(黄色)
         * @param {number} number マークする番号
         */
        const markNumber = (number) => {
            document.querySelectorAll('.numberTable td').forEach(cell => {
                if (parseInt(cell.textContent) === number) {
                    cell.classList.add('marked');
                }
            });
        };

        /**
         * 番号を「recently-marked」状態にする(赤色)
         * @param {number} number マークする番号
         */
        const markRecentNumber = (number) => {
            document.querySelectorAll('.numberTable td').forEach(cell => {
                if (parseInt(cell.textContent) === number) {
                    cell.classList.add('recently-marked');
                }
            });
        };

        /**
         * 直前に選ばれた番号を通常の「marked」状態に戻す(赤から黄色へ)
         */
        const unmarkRecentNumber = () => {
            if (lastChosenNumber !== null) {
                document.querySelectorAll('.numberTable td').forEach(cell => {
                    if (parseInt(cell.textContent) === lastChosenNumber) {
                        cell.classList.remove('recently-marked');
                        cell.classList.add('marked');
                    }
                });
            }
        };

        /**
         * ルーレットを開始する関数
         * 番号がランダムで高速に表示され、ストップ時に確定される
         */
        const startRoulette = () => {
            const startButton = document.getElementById('startButton');
            const bingoButton = document.getElementById('bingoButton');
            const bingoNumberDisplay = document.getElementById('bingoNumber');
            const bingoNumberContainer = document.getElementById('bingoNumberContainer');

            if (isRolling) {
                // ルーレットを停止
                clearInterval(interval);
                clearTimeout(timeout);
                isRolling = false;
                startButton.textContent = 'スタート';
                startButton.style.backgroundColor = '#ff6f61';
                startButton.disabled = false;
                bingoButton.disabled = false;
                rollingSound.pause();
                rollingSound.currentTime = 0;
            } else {
                // ルーレットを開始
                isRolling = true;
                startButton.textContent = 'ストップ';
                startButton.style.backgroundColor = '#ff4e42';
                bingoButton.disabled = true;
                bingoNumberContainer.style.background = 'none';
                rollingSound.play();
                interval = setInterval(() => {
                    if (bingoNumbers.length === 0) {
                        clearInterval(interval);
                        isRolling = false;
                        startButton.textContent = 'スタート';
                        startButton.style.backgroundColor = '#ff6f61';
                        startButton.disabled = false;
                        bingoButton.disabled = false;
                        rollingSound.pause();
                        rollingSound.currentTime = 0;
                        return;
                    }

                    // ランダムに番号を表示
                    const randomIndex = Math.floor(Math.random() * bingoNumbers.length);
                    const randomNumber = bingoNumbers[randomIndex];
                    bingoNumberDisplay.textContent = randomNumber;
                    highlightNumber(randomNumber);
                }, 100);
            }
        };

        /**
         * ルーレットを停止して番号を確定する関数
         */
        const stopRoulette = () => {
            const startButton = document.getElementById('startButton');
            const bingoButton = document.getElementById('bingoButton');
            const bingoNumberContainer = document.getElementById('bingoNumberContainer');
            startButton.disabled = true;

            stopSound.play(); // ストップボタンの効果音を再生

            timeout = setTimeout(() => {
                clearInterval(interval);
                isRolling = false;
                rollingSound.pause();
                rollingSound.currentTime = 0;
                confirmSound.play(); // 数字確定時の効果音を再生

                const bingoNumberDisplay = document.getElementById('bingoNumber');
                const chosenNumber = parseInt(bingoNumberDisplay.textContent);

                if (!chosenNumbers.includes(chosenNumber)) {
                    unmarkRecentNumber(); // 前回の番号を「marked」に戻す
                    lastChosenNumber = chosenNumber;
                    chosenNumbers.push(chosenNumber);
                    bingoNumbers = bingoNumbers.filter(number => number !== chosenNumber);
                    markRecentNumber(chosenNumber); // 今回の番号を「recently-marked」にする
                }

                // 番号表示とテーブルの該当セルを点滅させる
                bingoNumberDisplay.style.animation = 'blink 0.5s step-start 4';
                document.querySelectorAll('.numberTable td').forEach(cell => {
                    if (parseInt(cell.textContent) === chosenNumber) {
                        cell.style.animation = 'tableBlink 0.5s step-start 4';
                    }
                });

                // ボタンの状態を戻す
                startButton.textContent = 'スタート';
                startButton.style.backgroundColor = '#ff6f61';
                startButton.disabled = false;
                bingoButton.disabled = false;

                // 2秒後にアニメーションを終了
                setTimeout(() => {
                    bingoNumberDisplay.style.animation = 'none';
                    document.querySelectorAll('.numberTable td').forEach(cell => {
                        if (parseInt(cell.textContent) === chosenNumber) {
                            cell.style.animation = 'none';
                        }
                    });
                    bingoNumberContainer.style.background = 'none';
                }, 2000);
            }, 3000);
        };

        /**
         * ルーレットをリセットする関数
         * 状態を初期化し、選ばれた番号もリセット
         */
        const resetRoulette = () => {
            clearInterval(interval);
            clearTimeout(timeout);
            isRolling = false;
            bingoNumbers = Array.from({ length: 75 }, (_, i) => i + 1);
            chosenNumbers = [];
            lastChosenNumber = null;
            document.getElementById('bingoNumber').textContent = '0';
            document.querySelectorAll('.numberTable td').forEach(cell => {
                cell.classList.remove('highlight', 'marked', 'recently-marked');
                cell.style.animation = 'none';
            });
            const startButton = document.getElementById('startButton');
            startButton.textContent = 'スタート';
            startButton.style.backgroundColor = '#ff6f61';
            startButton.disabled = false;
            document.getElementById('bingoNumberContainer').style.background = 'none';
            rollingSound.pause();
            rollingSound.currentTime = 0;
            stopSound.pause();
            stopSound.currentTime = 0;
            confirmSound.pause();
            confirmSound.currentTime = 0;
        };

        /**
         * ビンゴ成功時に紙吹雪エフェクトとメッセージを表示する関数
         */
        const showCongratsMessage = () => {
            const congratsMessage = document.getElementById('congratsMessage');
            const confettiCanvas = document.getElementById('confettiCanvas');
            const confettiContext = confettiCanvas.getContext('2d');
            const winSound = document.getElementById('winSound');
            const bingoButton = document.getElementById('bingoButton');

            confettiCanvas.width = window.innerWidth;
            confettiCanvas.height = window.innerHeight;

            const confettiColors = ['#ff0', '#f00', '#0f0', '#00f', '#ff0', '#f0f', '#0ff'];
            const confetti = [];

            // 紙吹雪のデータを生成
            for (let i = 0; i < 300; i++) {
                confetti.push({
                    x: Math.random() * confettiCanvas.width,
                    y: Math.random() * confettiCanvas.height,
                    color: confettiColors[Math.floor(Math.random() * confettiColors.length)],
                    size: Math.random() * 4 + 1,
                    speed: Math.random() * 2 + 1,
                    angle: Math.random() * 360
                });
            }

            // 紙吹雪をレンダリングする関数
            function renderConfetti() {
                confettiContext.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);

                confetti.forEach((confetto, index) => {
                    confettiContext.fillStyle = confetto.color;
                    confettiContext.beginPath();
                    confetto.y += confetto.speed;
                    confetto.x += Math.cos(confetto.angle) * confetto.speed * 0.5;

                    confetto.angle += Math.random() * 0.05 - 0.025;
                    confettiContext.arc(confetto.x, confetto.y, confetto.size, 0, 2 * Math.PI);
                    confettiContext.fill();

                    if (confetto.y > confettiCanvas.height) {
                        confetti[index].y = 0;
                    }
                    if (confetto.x > confettiCanvas.width) {
                        confetti[index].x = 0;
                    }
                    if (confetto.x < 0) {
                        confetti[index].x = confettiCanvas.width;
                    }
                });

                requestAnimationFrame(renderConfetti);
            }

            renderConfetti();

            congratsMessage.style.display = 'block';
            winSound.play();
            bingoButton.disabled = true;

            // 5秒後に紙吹雪を停止し、ビンゴボタンを再度有効にする
            setTimeout(() => {
                congratsMessage.style.display = 'none';
                confettiCanvas.width = 0;
                confettiCanvas.height = 0;
                bingoButton.disabled = false;
            }, 5000);
        };

        // 各ボタンに対応するイベントリスナーの設定
        document.getElementById('startButton').addEventListener('click', () => {
            if (isRolling) {
                stopRoulette();
            } else {
                startRoulette();
            }
        });

        document.getElementById('resetButton').addEventListener('click', resetRoulette);
        document.getElementById('bingoButton').addEventListener('click', showCongratsMessage);

        // キーボード操作(スペースでルーレット開始/停止、エンターでビンゴ)
        document.addEventListener('keydown', (event) => {
            if (event.code === 'Space') {
                event.preventDefault();
                if (isRolling) {
                    stopRoulette();
                } else {
                    startRoulette();
                }
            }
            if (event.code === 'Enter') {
                event.preventDefault();
                if (!document.getElementById('bingoButton').disabled) {
                    showCongratsMessage();
                }
            }
        });
    </script>
</body>
</html>
1
2
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?