コード · Mega Bubble Phonics
CC BY 4.0 · @joeのクレジットを残してください · v1
持っていく。リミックスする。授業で使う。
下のコードをChatGPT、Claude、Gemini、Grokなど好きなツールにコピーし、クラス向けに変えてから、クレジット付きでリミックスをここに公開してください。
AI編集プロンプトの下書き
この教室ゲームを電子黒板向けに編集してください。大きなタッチ対象と学級全体での遊びを保ってください。トラッキング、ログイン、個人情報の収集、遠隔APIの呼び出しは禁止です。単一HTMLの構成を保ってください。教育目標は保ってください。元の制作者: ClassBoard Gamesの@joe(CC BY 4.0)。
mega-bubble-phonics.html · 26.5 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mega Bubble Phonics</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&display=swap');
body {
margin: 0;
overflow: hidden;
background-color: #87CEEB; /* Soft Sky Blue */
font-family: 'Fredoka', sans-serif;
touch-action: none; /* Prevent pull-to-refresh on mobile */
user-select: none;
}
#gameCanvas {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
/* UI Overlay */
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none; /* Let clicks pass through to canvas */
}
.pointer-events-auto {
pointer-events: auto;
}
.hud-text {
text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.2), 0 0 10px rgba(255, 255, 255, 0.8);
}
/* Cute Modal Styles */
.cute-panel {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 4px solid #FF9AA2; /* Pastel Pink */
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2), inset 0 0 20px rgba(255, 154, 162, 0.2);
border-radius: 2rem;
}
.cute-button {
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
border-bottom-width: 6px;
}
.cute-button:active {
transform: translateY(4px);
border-bottom-width: 2px;
}
/* Animations */
@keyframes bounce-in {
0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
80% { transform: translate(-50%, -50%) scale(1.05); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
.modal-animate {
animation: bounce-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.shake {
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
}
@keyframes shake {
10%, 90% { transform: translate3d(-2px, 0, 0); }
20%, 80% { transform: translate3d(4px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
40%, 60% { transform: translate3d(6px, 0, 0); }
}
@keyframes float-up {
0% { transform: translateY(0px); }
50% { transform: translateY(-15px); }
100% { transform: translateY(0px); }
}
.floating {
animation: float-up 3s ease-in-out infinite;
}
</style>
</head>
<body>
<!-- Game Canvas -->
<canvas id="gameCanvas"></canvas>
<!-- UI Layer -->
<div id="ui-layer" class="flex flex-col justify-between p-4 md:p-6">
<!-- HUD -->
<div class="flex justify-between items-start w-full pointer-events-none">
<div class="bg-white/80 border-4 border-yellow-400 p-3 rounded-2xl shadow-lg">
<div class="text-sm font-bold text-yellow-500 uppercase tracking-widest text-center">Stars</div>
<div class="text-4xl font-black text-orange-500 hud-text text-center" id="scoreDisplay">0</div>
</div>
<div class="flex gap-2 bg-white/80 border-4 border-red-300 p-3 rounded-2xl shadow-lg" id="livesContainer">
<!-- Hearts injected via JS -->
</div>
</div>
<!-- Question Modal (Hidden by default) -->
<div id="questionModal" class="hidden absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-11/12 max-w-sm cute-panel p-6 pointer-events-auto">
<div class="text-center mb-4">
<h2 class="text-2xl font-bold text-pink-500 mb-2">What's in the bubble?</h2>
<div class="text-8xl my-4 drop-shadow-md" id="questionEmoji">
<!-- Emoji goes here -->
</div>
</div>
<div class="grid grid-cols-1 gap-4" id="optionsGrid">
<!-- Options injected via JS -->
</div>
</div>
<!-- Start Screen -->
<div id="startScreen" class="absolute inset-0 bg-sky-300/90 backdrop-blur-sm z-50 flex flex-col items-center justify-center pointer-events-auto">
<div class="text-center px-6 max-w-lg floating">
<div class="text-8xl mb-4 drop-shadow-lg">🫧✨</div>
<h1 class="text-5xl md:text-7xl font-black text-white mb-2 hud-text" style="color: #FF9AA2; -webkit-text-stroke: 2px white;">MEGA BUBBLE<br>PHONICS</h1>
<div class="bg-yellow-400 text-yellow-900 font-bold px-4 py-1 rounded-full inline-block mb-4 border-2 border-white shadow-sm">50 Words Edition!</div>
<p class="text-white text-xl md:text-2xl mb-8 font-bold drop-shadow-md">Pop the cute bubbles before they float into the rainbow portal!</p>
<button onclick="startGame()" class="cute-button bg-green-400 hover:bg-green-300 text-white border-green-600 font-bold py-5 px-12 rounded-full shadow-lg text-2xl uppercase tracking-wider w-full md:w-auto">
Play Now! 🚀
</button>
</div>
</div>
<!-- Game Over Screen -->
<div id="gameOverScreen" class="hidden absolute inset-0 bg-blue-900/90 backdrop-blur-sm z-50 flex flex-col items-center justify-center pointer-events-auto">
<div class="text-center px-6 floating">
<div class="text-7xl mb-4">😴✨</div>
<h2 class="text-5xl md:text-6xl font-black text-yellow-300 mb-4 hud-text">Sleepy Time!</h2>
<div class="bg-white/20 p-6 rounded-3xl mb-8 border-4 border-white/30">
<p class="text-white text-2xl mb-2 font-bold">You collected</p>
<p class="text-6xl font-black text-yellow-400 drop-shadow-lg mb-2"><span id="finalScore">0</span></p>
<p class="text-white text-2xl font-bold">Stars! ⭐</p>
</div>
<button onclick="resetGame()" class="cute-button bg-pink-400 hover:bg-pink-300 text-white border-pink-600 font-bold py-4 px-10 rounded-full shadow-lg text-2xl uppercase tracking-wider">
Play Again! 🎈
</button>
</div>
</div>
</div>
<script>
/**
* CVC DATA REPOSITORY - 50 CVC Words!
* Includes Short A, E, I, O, U
*/
const CVC_DATA = [
// Short A (10 words)
{ word: "cat", emoji: "🐱" }, { word: "bat", emoji: "🦇" }, { word: "map", emoji: "🗺️" },
{ word: "dad", emoji: "👨" }, { word: "pan", emoji: "🍳" },
{ word: "hat", emoji: "🎩" }, { word: "mad", emoji: "😠" }, { word: "cap", emoji: "🧢" },
{ word: "rat", emoji: "🐀" }, { word: "jam", emoji: "🍓" },
// Short E (10 words)
{ word: "bed", emoji: "🛏️" }, { word: "red", emoji: "🔴" }, { word: "pen", emoji: "🖊️" },
{ word: "ten", emoji: "🔟" }, { word: "net", emoji: "🥅" },
{ word: "leg", emoji: "🦵" }, { word: "web", emoji: "🕸️" }, { word: "wet", emoji: "💦" },
{ word: "hen", emoji: "🐔" }, { word: "peg", emoji: "📍" },
// Short I (10 words)
{ word: "pig", emoji: "🐷" }, { word: "sit", emoji: "🪑" }, { word: "big", emoji: "🐘" },
{ word: "lip", emoji: "👄" }, { word: "win", emoji: "🏆" },
{ word: "fin", emoji: "🦈" }, { word: "hit", emoji: "🎯" }, { word: "zip", emoji: "🤐" },
{ word: "kid", emoji: "🧒" }, { word: "dip", emoji: "🥣" },
// Short O (10 words)
{ word: "dog", emoji: "🐶" }, { word: "hot", emoji: "♨️" }, { word: "pot", emoji: "🍲" },
{ word: "box", emoji: "📦" }, { word: "top", emoji: "🪀" },
{ word: "fox", emoji: "🦊" }, { word: "log", emoji: "🪵" }, { word: "mop", emoji: "🧹" },
{ word: "hop", emoji: "🐇" }, { word: "cot", emoji: "🏕️" },
// Short U (10 words)
{ word: "sun", emoji: "☀️" }, { word: "cup", emoji: "🍵" }, { word: "bug", emoji: "🐛" },
{ word: "run", emoji: "🏃" }, { word: "nut", emoji: "🥜" },
{ word: "bus", emoji: "🚌" }, { word: "tub", emoji: "🛁" }, { word: "mud", emoji: "🤎" },
{ word: "rug", emoji: "🧶" }, { word: "gum", emoji: "🍬" }
];
// Pastel Colors for Bubbles and Buttons
const CUTE_COLORS = [
'#FFB7B2', '#FFDAC1', '#E2F0CB', '#B5EAD7', '#C7CEEA', '#FDFD96'
];
const BUTTON_COLORS = [
{ bg: 'bg-blue-400', border: 'border-blue-600', hover: 'hover:bg-blue-300' },
{ bg: 'bg-pink-400', border: 'border-pink-600', hover: 'hover:bg-pink-300' },
{ bg: 'bg-purple-400', border: 'border-purple-600', hover: 'hover:bg-purple-300' },
{ bg: 'bg-yellow-400', border: 'border-yellow-600', hover: 'hover:bg-yellow-300' },
{ bg: 'bg-orange-400', border: 'border-orange-600', hover: 'hover:bg-orange-300' }
];
/**
* GAME ENGINE
*/
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let gameState = 'START'; // START, PLAYING, MODAL, GAMEOVER
let score = 0;
let lives = 3;
let particles = []; // Background magical dust
let confettis = []; // Confetti for correct answers
let bubbles = []; // The CVC bubbles
let portalAngle = 0;
let spawnRate = 220; // Slightly slower for kids
let spawnTimer = 0;
let gameFrame = 0;
let activeBubble = null;
let width, height, centerX, centerY;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
centerX = width / 2;
centerY = height / 2;
}
window.addEventListener('resize', resize);
resize();
// --- CLASSES ---
class Sparkle {
constructor() {
this.reset();
// Randomize initial position so screen isn't empty on load
this.x = Math.random() * width;
this.y = Math.random() * height;
}
reset() {
this.x = Math.random() * width;
this.y = height + Math.random() * 100; // Start below screen
this.size = Math.random() * 4 + 2;
this.speedY = Math.random() * -1 - 0.5; // Float up
this.speedX = (Math.random() - 0.5) * 0.5;
this.opacity = Math.random() * 0.5 + 0.2;
this.wobble = Math.random() * Math.PI * 2;
}
update() {
this.y += this.speedY;
this.x += this.speedX + Math.sin(this.wobble) * 0.5;
this.wobble += 0.05;
if (this.y < -10) {
this.reset();
}
}
draw() {
ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
class Confetti {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 10 + 5;
this.color = CUTE_COLORS[Math.floor(Math.random() * CUTE_COLORS.length)];
const angle = Math.random() * Math.PI * 2;
const velocity = Math.random() * 15 + 5;
this.vx = Math.cos(angle) * velocity;
this.vy = Math.sin(angle) * velocity - 5; // Extra upward pop
this.gravity = 0.5;
this.drag = 0.95;
this.rotation = Math.random() * 360;
this.rotationSpeed = (Math.random() - 0.5) * 20;
this.life = 1.0;
this.decay = Math.random() * 0.02 + 0.01;
}
update() {
this.vx *= this.drag;
this.vy *= this.drag;
this.vy += this.gravity;
this.x += this.vx;
this.y += this.vy;
this.rotation += this.rotationSpeed;
this.life -= this.decay;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation * Math.PI / 180);
ctx.globalAlpha = Math.max(0, this.life);
ctx.fillStyle = this.color;
ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size);
ctx.restore();
}
}
class Bubble {
constructor() {
// Spawn off-screen
const edge = Math.floor(Math.random() * 4);
if (edge === 0) { this.x = Math.random() * width; this.y = -60; } // Top
else if (edge === 1) { this.x = width + 60; this.y = Math.random() * height; } // Right
else if (edge === 2) { this.x = Math.random() * width; this.y = height + 60; } // Bottom
else { this.x = -60; this.y = Math.random() * height; } // Left
this.speed = 0.3 + (score * 0.01); // Very slow start for kids
if (this.speed > 1.5) this.speed = 1.5; // Cap speed lower
this.radius = 45; // Big friendly bubbles
this.wobble = Math.random() * Math.PI * 2;
// Pick a random CVC word out of the 50
this.cvcData = CVC_DATA[Math.floor(Math.random() * CVC_DATA.length)];
this.color = CUTE_COLORS[Math.floor(Math.random() * CUTE_COLORS.length)];
}
update() {
if (gameState !== 'PLAYING') return false;
// Move towards center portal
const dx = centerX - this.x;
const dy = centerY - this.y;
const dist = Math.sqrt(dx*dx + dy*dy);
this.x += (dx / dist) * this.speed;
this.y += (dy / dist) * this.speed;
// Add playful floating wobble
this.y += Math.sin(this.wobble) * 0.5;
this.wobble += 0.05;
// Collision with portal (Missed it!)
if (dist < 50) {
loseLife();
return true; // Mark for deletion
}
return false;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
// Bubble Body
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
// Gradient for 3D bubble effect
const grad = ctx.createRadialGradient(-10, -10, 5, 0, 0, this.radius);
grad.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
grad.addColorStop(0.3, this.color);
grad.addColorStop(1, 'rgba(255, 255, 255, 0.4)');
ctx.fillStyle = grad;
ctx.fill();
ctx.lineWidth = 3;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
ctx.stroke();
// Bubble Shine
ctx.beginPath();
ctx.ellipse(-15, -15, 10, 5, Math.PI / 4, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fill();
// Emoji
ctx.font = '40px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Slight shadow for emoji
ctx.shadowColor = 'rgba(0,0,0,0.2)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fillText(this.cvcData.emoji, 0, 5); // offset y slightly for visual center
ctx.restore();
}
}
// --- GAME LOGIC ---
function init() {
// Create background sparkles
for (let i = 0; i < 50; i++) {
particles.push(new Sparkle());
}
updateLivesUI();
animate();
}
function startGame() {
document.getElementById('startScreen').classList.add('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
resetVars();
gameState = 'PLAYING';
}
function resetVars() {
score = 0;
lives = 3;
spawnRate = 220;
bubbles = [];
confettis = [];
activeBubble = null;
document.getElementById('scoreDisplay').innerText = score;
updateLivesUI();
}
function loseLife() {
lives--;
updateLivesUI();
// Screen Shake Effect
document.body.classList.add('shake');
setTimeout(() => document.body.classList.remove('shake'), 500);
// Pop sound fallback (visual)
ctx.fillStyle = 'rgba(255, 100, 100, 0.3)';
ctx.fillRect(0, 0, width, height);
if (lives <= 0) {
gameOver();
}
}
function updateLivesUI() {
const container = document.getElementById('livesContainer');
container.innerHTML = '';
for(let i=0; i<3; i++) {
const heart = document.createElement('div');
heart.className = `text-3xl transition-all duration-300 ${i < lives ? 'scale-100 opacity-100' : 'scale-75 opacity-30 grayscale'}`;
heart.innerText = '❤️';
container.appendChild(heart);
}
}
function gameOver() {
gameState = 'GAMEOVER';
document.getElementById('finalScore').innerText = score;
document.getElementById('gameOverScreen').classList.remove('hidden');
document.getElementById('questionModal').classList.add('hidden');
}
function resetGame() {
startGame();
}
// --- INTERACTION ---
function handleInput(x, y) {
if (gameState !== 'PLAYING') return;
// Check if clicked on a bubble
for (let i = bubbles.length - 1; i >= 0; i--) { // Check newest top-most first
const b = bubbles[i];
const dx = x - b.x;
const dy = y - b.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < b.radius + 15) { // +15 forgiveness for chubby little fingers
activateRescue(b, i);
break;
}
}
}
canvas.addEventListener('mousedown', (e) => handleInput(e.clientX, e.clientY));
canvas.addEventListener('touchstart', (e) => {
handleInput(e.touches[0].clientX, e.touches[0].clientY);
}, {passive: false});
function activateRescue(bubble, index) {
gameState = 'MODAL'; // Pauses the game loop updates
activeBubble = { data: bubble, index: index };
const modal = document.getElementById('questionModal');
const qEmoji = document.getElementById('questionEmoji');
const grid = document.getElementById('optionsGrid');
// Set Emoji
qEmoji.innerText = bubble.cvcData.emoji;
// Generate Options
grid.innerHTML = '';
const correctWord = bubble.cvcData.word;
let options = [correctWord];
// Get 2 random wrong answers for 3 total options (from the giant pool of 50!)
const pool = CVC_DATA.filter(opt => opt.word !== correctWord);
while (options.length < 3) {
const randomOpt = pool[Math.floor(Math.random() * pool.length)].word;
if (!options.includes(randomOpt)) options.push(randomOpt);
}
// Shuffle
options.sort(() => Math.random() - 0.5);
// Create big cute buttons
options.forEach((opt, idx) => {
const btn = document.createElement('button');
const btnTheme = BUTTON_COLORS[idx % BUTTON_COLORS.length];
btn.className = `cute-button w-full ${btnTheme.bg} ${btnTheme.border} ${btnTheme.hover} text-white font-bold py-4 px-6 rounded-2xl text-3xl tracking-wide text-center uppercase shadow-md`;
btn.innerText = opt;
btn.onclick = () => checkAnswer(opt, correctWord);
grid.appendChild(btn);
});
// Trigger bounce animation
modal.classList.remove('hidden');
modal.classList.remove('modal-animate');
void modal.offsetWidth; // Trigger reflow
modal.classList.add('modal-animate');
}
function checkAnswer(selected, correct) {
const modal = document.getElementById('questionModal');
if (selected === correct) {
// YAY! CORRECT!
score += 1; // 1 star per correct
document.getElementById('scoreDisplay').innerText = score;
if (activeBubble) {
// Confetti explosion at bubble location
spawnConfetti(activeBubble.data.x, activeBubble.data.y);
// Remove bubble
bubbles.splice(activeBubble.index, 1);
}
// Slightly increase difficulty over time
if (spawnRate > 80 && score % 3 === 0) spawnRate -= 10;
} else {
// OOPS! WRONG!
// Push bubble closer to the center portal as a slight penalty
if (activeBubble && bubbles[activeBubble.index]) {
const b = bubbles[activeBubble.index];
const dx = centerX - b.x;
const dy = centerY - b.y;
b.x += dx * 0.4;
b.y += dy * 0.4;
// Show a little red flash
ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
ctx.fillRect(0, 0, width, height);
}
}
modal.classList.add('hidden');
gameState = 'PLAYING';
activeBubble = null;
}
function spawnConfetti(x, y) {
for(let i=0; i<40; i++) {
confettis.push(new Confetti(x, y));
}
}
function drawPortal() {
portalAngle += 0.02;
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(portalAngle);
// Draw a spinning rainbow star/portal
const numSpokes = 8;
for(let i=0; i<numSpokes; i++) {
ctx.rotate((Math.PI * 2) / numSpokes);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(30, 80);
ctx.lineTo(-30, 80);
ctx.fillStyle = `hsla(${(i * 45 + gameFrame) % 360}, 80%, 70%, 0.6)`;
ctx.fill();
}
// Center glowing circle
ctx.beginPath();
ctx.arc(0, 0, 40, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.shadowColor = '#FFB7B2';
ctx.shadowBlur = 20;
ctx.fill();
ctx.restore();
}
// --- MAIN LOOP ---
function animate() {
// Soft sky blue background wipe
ctx.fillStyle = '#87CEEB';
ctx.fillRect(0, 0, width, height);
// Draw Background Sparkles
particles.forEach(p => {
p.update();
p.draw();
});
// Draw Rainbow Portal at center
drawPortal();
// Draw and Update Confetti
for (let i = confettis.length - 1; i >= 0; i--) {
confettis[i].update();
confettis[i].draw();
if (confettis[i].life <= 0 || confettis[i].y > height + 50) {
confettis.splice(i, 1);
}
}
// Handle Bubbles
if (gameState === 'PLAYING') {
gameFrame++;
// Spawn Logic
if (gameFrame % spawnRate === 0) {
bubbles.push(new Bubble());
}
// Update & Draw Bubbles
for (let i = bubbles.length - 1; i >= 0; i--) {
let shouldRemove = bubbles[i].update();
if (shouldRemove) {
bubbles.splice(i, 1);
// Edge case safeguard
if (activeBubble && activeBubble.index === i) {
document.getElementById('questionModal').classList.add('hidden');
activeBubble = null;
}
} else {
bubbles[i].draw();
}
}
} else if (gameState === 'MODAL') {
// Just draw them frozen if modal is open
bubbles.forEach(b => b.draw());
}
requestAnimationFrame(animate);
}
// Boot
init();
</script>
</body>
</html>