はじめに
全社研修の際に、BINGO大会を行うので、そのためのルーレットを開発しました。
会議室の大画面に映すので、左右にBINGOの一覧表を作成しています。
仕様
ボタン
スタートボタン、もしくはスペースキーを押下すると、ルーレットが開始ます。
ストップボタン、もしくはスペースキーを押下すると、ルーレットが停止ます。
リセットボタンを押下すると、初期化されます。
ビンゴボタン、もしくはエンターキーを押下すると、BINGOになった人を祝福します。
効果音
効果音は4種類で、htmlと同じ階層に音源ファイルを置いてます。
- rolling.mp3: ルーレットが回っている時
- stop.mp3: ストップボタンが押下された時
- confirm.mp3: 番号が確定した時
- win.mp3: 祝福された時
一覧表
1度選択された番号は、いつでも一覧表で確認できます。
直近の番号が赤色で、それよりも前の番号は黄色になります。
コード
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>