コード · 🪄魔法結合韻🪄
CC BY 4.0 · @SpringFairyのクレジットを残してください · v1
持っていく。リミックスする。授業で使う。
下のコードをChatGPT、Claude、Gemini、Grokなど好きなツールにコピーし、クラス向けに変えてから、クレジット付きでリミックスをここに公開してください。
AI編集プロンプトの下書き
この教室ゲームを電子黒板向けに編集してください。大きなタッチ対象と学級全体での遊びを保ってください。トラッキング、ログイン、個人情報の収集、遠隔APIの呼び出しは禁止です。単一HTMLの構成を保ってください。教育目標は保ってください。元の制作者: ClassBoard Gamesの@SpringFairy(CC BY 4.0)。
game-4.html · 20.4 KB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>快樂結合韻轉盤遊戲</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Canvas Confetti -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@600;700&family=Noto+Sans+TC:wght@700;900&display=swap');
body {
font-family: 'Noto Sans TC', 'Fredoka', sans-serif;
background-color: #cbd5e1;
user-select: none;
overflow-x: hidden;
}
/* Classroom TV / Smartboard Frame */
.smartboard-container {
background: #f8fafc;
border: 12px solid #334155;
border-radius: 24px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
position: relative;
}
/* Header Pastel Wave Bar matching photo smartboard header */
.smartboard-header {
height: 36px;
background: linear-gradient(90deg, #f472b6 0%, #fbbf24 25%, #34d399 50%, #38bdf8 75%, #c084fc 100%);
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
/* Purple Initial Buttons matching photo 2x4 grid */
.purple-card {
background-color: #a855f7;
color: #ffffff;
font-weight: 900;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.15), inset 0 2px 4px rgba(255, 255, 255, 0.3);
transition: all 0.15s ease-in-out;
}
.purple-card:hover {
background-color: #9333ea;
transform: scale(1.03);
}
.purple-card.active {
background-color: #c084fc;
border: 4px solid #facc15;
color: #3b0764;
box-shadow: 0 0 15px rgba(250, 204, 21, 0.8);
}
/* Traditional Hanging Scroll Banner replica */
.hanging-scroll {
background-color: #ffffff;
border: 3px solid #dc2626;
border-radius: 4px;
position: relative;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
}
/* Red Scroll Rod Top and Bottom */
.scroll-rod {
height: 14px;
background: #b91c1c;
width: 116%;
margin-left: -8%;
border-radius: 7px;
position: relative;
}
.scroll-knob {
width: 22px;
height: 22px;
background: #f43f5e;
border: 2px solid #881337;
border-radius: 50%;
position: absolute;
top: -4px;
}
.scroll-knob.left { left: -10px; }
.scroll-knob.right { right: -10px; }
/* Hanging String */
.scroll-string {
width: 0;
height: 0;
border-left: 60px solid transparent;
border-right: 60px solid transparent;
border-bottom: 30px solid #f43f5e;
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
z-index: 0;
}
/* Result Toast Animation */
.pop-in {
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
@keyframes popIn {
0% { transform: scale(0.7); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-2 sm:p-6">
<!-- Main Smartboard Screen Outer Border -->
<div class="smartboard-container w-full max-w-6xl aspect-[16/10] sm:aspect-auto flex flex-col overflow-hidden my-auto">
<!-- Top Pastel Header Bar matching photo top border -->
<div class="smartboard-header w-full flex items-center justify-between px-4">
<div class="flex gap-2">
<div class="w-3 h-3 rounded-full bg-white/60"></div>
<div class="w-3 h-3 rounded-full bg-white/60"></div>
</div>
<div class="text-xs font-bold text-slate-700 tracking-wider">教育部注音符號教學系統</div>
</div>
<!-- Inner Classroom Display Area -->
<main class="flex-1 p-4 sm:p-8 grid grid-cols-12 gap-4 items-center justify-between relative bg-slate-100">
<!-- LEFT COLUMN: Specified Purple Cards (2x4 Grid: ㄉ, ㄌ, ㄏ, ㄊ, ㄐ, ㄒ, ㄅ, ㄑ) -->
<section class="col-span-3 flex flex-col items-center justify-center h-full">
<div id="initialsContainer" class="grid grid-cols-2 gap-3 sm:gap-4 w-full max-w-[200px]">
<!-- 8 Purple Initial Buttons -->
</div>
<!-- Pagination Controls for Initials -->
<div class="flex items-center gap-2 mt-4">
<button onclick="prevInitialsPage()" class="px-2.5 py-1 bg-purple-200 text-purple-900 rounded-lg text-xs font-bold hover:bg-purple-300 transition">
<i class="fa-solid fa-chevron-left"></i> 上頁
</button>
<span id="pageIndicator" class="text-xs font-black text-purple-800">1 / 3</span>
<button onclick="nextInitialsPage()" class="px-2.5 py-1 bg-purple-200 text-purple-900 rounded-lg text-xs font-bold hover:bg-purple-300 transition">
下頁 <i class="fa-solid fa-chevron-right"></i>
</button>
</div>
</section>
<!-- CENTER COLUMN: Spinner Wheel & Blue Start Button -->
<section class="col-span-6 flex flex-col items-center justify-center relative my-2">
<!-- Wheel Outer Container -->
<div class="relative w-full max-w-[380px] sm:max-w-[440px] aspect-square flex items-center justify-center">
<!-- CUTE KIRBY (星之卡比) POINTER ARROW POINTING DOWN -->
<div class="absolute -top-10 left-1/2 -translate-x-1/2 z-30 pointer-events-none filter drop-shadow-xl transition-transform duration-300 hover:scale-105">
<svg width="92" height="112" viewBox="0 0 92 112" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Kirby Soft Pink Body Gradient -->
<linearGradient id="kirbyBodyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffb3d1"/>
<stop offset="45%" stop-color="#ff80b3"/>
<stop offset="100%" stop-color="#ff4d94"/>
</linearGradient>
<!-- Kirby Red Shoes Gradient -->
<linearGradient id="kirbyShoesGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ff3366"/>
<stop offset="100%" stop-color="#b3002d"/>
</linearGradient>
<!-- Strawberry Candy Arrow Pointer Gradient -->
<linearGradient id="strawberryGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff668c"/>
<stop offset="60%" stop-color="#ff1a53"/>
<stop offset="100%" stop-color="#cc0033"/>
</linearGradient>
<!-- Soft Glow Filter -->
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="3" flood-color="#881337" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Outer White Cookie Border for Pointer -->
<path d="M 46 107 C 41 107 36 94 23 68 C 11 44 15 20 46 20 C 77 20 81 44 69 68 C 57 94 51 107 46 107 Z"
fill="#ffffff" filter="url(#softShadow)"/>
<!-- Main Strawberry Candy Red Arrow Pointer Body -->
<path d="M 46 102 C 42 102 38 90 26 66 C 16 44 19 24 46 24 C 73 24 76 44 66 66 C 54 90 50 102 46 102 Z"
fill="url(#strawberryGrad)"/>
<!-- Glossy Highlight on Arrow -->
<path d="M 30 30 C 38 26 54 26 62 30 C 54 34 38 34 30 30 Z" fill="#ffffff" opacity="0.65"/>
<!-- CUTE KIRBY (星之卡比) CHARACTER AT TOP -->
<g transform="translate(46, 32)">
<!-- Kirby White Base Border Circle -->
<circle cx="0" cy="0" r="25" fill="#ffffff"/>
<!-- Kirby Red Feet / Boots -->
<!-- Left Foot -->
<ellipse cx="-15" cy="18" rx="10" ry="6" fill="url(#kirbyShoesGrad)" stroke="#800020" stroke-width="0.8" transform="rotate(-15 -15 18)"/>
<!-- Right Foot -->
<ellipse cx="15" cy="18" rx="10" ry="6" fill="url(#kirbyShoesGrad)" stroke="#800020" stroke-width="0.8" transform="rotate(15 15 18)"/>
<!-- Kirby Main Pink Round Body -->
<circle cx="0" cy="0" r="22.5" fill="url(#kirbyBodyGrad)" stroke="#e6005c" stroke-width="1.2"/>
<!-- Body Soft Top Highlight -->
<ellipse cx="-6" cy="-12" rx="10" ry="5" fill="#ffffff" opacity="0.45" transform="rotate(-15 -6 -12)"/>
<!-- Kirby Cute Arms (Waving / Cheering) -->
<!-- Left Arm -->
<ellipse cx="-20" cy="-3" rx="6.5" ry="5" fill="url(#kirbyBodyGrad)" stroke="#e6005c" stroke-width="1" transform="rotate(25 -20 -3)"/>
<!-- Right Arm -->
<ellipse cx="20" cy="-3" rx="6.5" ry="5" fill="url(#kirbyBodyGrad)" stroke="#e6005c" stroke-width="1" transform="rotate(-25 20 -3)"/>
<!-- Rosy Soft Pink Cheeks -->
<ellipse cx="-12" cy="4" rx="4.5" ry="2.8" fill="#ff1a66" opacity="0.75"/>
<ellipse cx="12" cy="4" rx="4.5" ry="2.8" fill="#ff1a66" opacity="0.75"/>
<!-- Signature Sparkly Kirby Blue Eyes -->
<!-- Left Eye -->
<g transform="translate(-6.5, -4)">
<ellipse cx="0" cy="0" rx="3.2" ry="7.5" fill="#111111"/>
<ellipse cx="0" cy="3" rx="2.5" ry="4" fill="#0066cc"/>
<circle cx="0" cy="-3" r="1.8" fill="#ffffff"/>
</g>
<!-- Right Eye -->
<g transform="translate(6.5, -4)">
<ellipse cx="0" cy="0" rx="3.2" ry="7.5" fill="#111111"/>
<ellipse cx="0" cy="3" rx="2.5" ry="4" fill="#0066cc"/>
<circle cx="0" cy="-3" r="1.8" fill="#ffffff"/>
</g>
<!-- Big Happy Open Smile Mouth -->
<path d="M -4.5 5 C -4.5 11 4.5 11 4.5 5 Z" fill="#800020" stroke="#4a0013" stroke-width="0.8"/>
<!-- Tongue in mouth -->
<path d="M -3 7.5 C -1.5 6 1.5 6 3 7.5 C 2.5 10 -2.5 10 -3 7.5 Z" fill="#ff6699"/>
</g>
</svg>
</div>
<!-- Wheel Canvas -->
<canvas id="wheelCanvas" width="460" height="460" class="w-full h-full drop-shadow-2xl"></canvas>
</div>
<!-- Wheel Yellow Stand Support (Pedestal) -->
<div class="w-16 h-10 bg-amber-400 border-x-4 border-amber-500 -mt-2 z-0"></div>
<div class="w-36 h-5 bg-amber-500 rounded-t-lg -mt-1 z-0 shadow-md"></div>
<!-- Blue Start / Stop Button & Visual Combination Display -->
<div class="w-full flex items-center justify-between max-w-[420px] -mt-10 z-20 px-2">
<!-- Visual Combination Result Box (No Audio) -->
<div id="resultCard" class="opacity-0 transition-opacity duration-300 bg-amber-100 border-2 border-amber-400 rounded-xl px-4 py-1.5 shadow-md flex items-center gap-2">
<span class="text-xs font-bold text-amber-800">轉出結果:</span>
<span id="resultText" class="text-2xl font-black text-slate-900 tracking-wider"></span>
</div>
<!-- Blue Spin Button -->
<button id="spinBtn" onclick="toggleSpin()" class="px-5 py-2 sm:px-6 sm:py-2.5 bg-blue-700 hover:bg-blue-600 active:scale-95 text-white font-black text-base sm:text-lg rounded-xl shadow-lg border-2 border-blue-400 transition cursor-pointer flex items-center gap-2">
<i class="fa-solid fa-play text-sm"></i>
<span>開始 / 停止</span>
</button>
</div>
</section>
<!-- RIGHT COLUMN: Hanging Scroll -->
<section class="col-span-3 flex flex-col items-center justify-center h-full relative">
<!-- Hanging String -->
<div class="scroll-string"></div>
<!-- Main Scroll Container -->
<div class="hanging-scroll w-full max-w-[170px] sm:max-w-[190px] px-3 py-4 flex flex-col items-center z-10 my-4">
<!-- Top Rod -->
<div class="scroll-rod top-0">
<div class="scroll-knob left"></div>
<div class="scroll-knob right"></div>
</div>
<!-- Vertical Scroll Text without Phonetics -->
<div class="flex flex-col items-center gap-3 sm:gap-4 my-4 text-slate-900">
<span class="text-3xl sm:text-4xl font-black tracking-widest">結</span>
<span class="text-3xl sm:text-4xl font-black tracking-widest">合</span>
<span class="text-3xl sm:text-4xl font-black tracking-widest">韻</span>
<span class="text-3xl sm:text-4xl font-black tracking-widest">轉</span>
<span class="text-3xl sm:text-4xl font-black tracking-widest">盤</span>
</div>
<!-- Bottom Rod -->
<div class="scroll-rod bottom-0">
<div class="scroll-knob left"></div>
<div class="scroll-knob right"></div>
</div>
</div>
</section>
</main>
</div>
<script>
// Specified Initials List
const initials = [
'ㄉ', 'ㄌ', 'ㄏ', 'ㄊ', 'ㄐ', 'ㄒ', 'ㄅ', 'ㄑ',
'ㄇ', 'ㄈ', 'ㄍ', 'ㄎ', 'ㄓ', 'ㄔ', 'ㄕ', 'ㄖ',
'ㄗ', 'ㄘ', 'ㄙ', 'ㄆ', 'ㄧ'
];
// Specified Finals List for the 8 sectors
const specifiedFinals = ['ㄩㄢ', 'ㄧㄥ', 'ㄩㄥ', 'ㄨㄥ', 'ㄨㄛ', 'ㄧㄢ', 'ㄧㄚ', 'ㄩㄝ'];
// State Variables
let currentPage = 0;
const itemsPerPage = 8;
let isSpinning = false;
let spinSpeed = 0;
let startAngle = -Math.PI / 2 - Math.PI / 8; // Top sector offset
let animId = null;
// Wedge colors matching photo clockwise
const wedgeColors = [
'#facc15', // 1. Yellow
'#38bdf8', // 2. Light Blue
'#22c55e', // 3. Green
'#eab308', // 4. Gold Yellow
'#f97316', // 5. Orange
'#3b82f6', // 6. Blue
'#ec4899', // 7. Pink
'#c084fc' // 8. Purple
];
window.onload = function() {
renderInitialsGrid();
drawWheel();
};
// Render Static Purple Cards (2x4 Grid) on the Left (Non-clickable to avoid invalid combinations)
function renderInitialsGrid() {
const container = document.getElementById('initialsContainer');
container.innerHTML = '';
const startIndex = currentPage * itemsPerPage;
const visibleInitials = initials.slice(startIndex, startIndex + itemsPerPage);
visibleInitials.forEach(init => {
const card = document.createElement('div');
card.className = `purple-card h-14 sm:h-16 text-2xl sm:text-3xl flex items-center justify-center cursor-default select-none`;
card.innerText = init;
container.appendChild(card);
});
const totalPages = Math.ceil(initials.length / itemsPerPage);
document.getElementById('pageIndicator').innerText = `${currentPage + 1} / ${totalPages}`;
}
function prevInitialsPage() {
if (currentPage > 0) {
currentPage--;
renderInitialsGrid();
}
}
function nextInitialsPage() {
const totalPages = Math.ceil(initials.length / itemsPerPage);
if (currentPage < totalPages - 1) {
currentPage++;
renderInitialsGrid();
}
}
function drawWheel() {
const canvas = document.getElementById('wheelCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const numSlices = 8;
const arc = (2 * Math.PI) / numSlices;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const outerRadius = 205;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw 8 Wedge Slices
for (let i = 0; i < numSlices; i++) {
const angle = startAngle + i * arc;
// Draw Wedge Sector
ctx.fillStyle = wedgeColors[i % wedgeColors.length];
ctx.beginPath();
ctx.arc(centerX, centerY, outerRadius, angle, angle + arc, false);
ctx.arc(centerX, centerY, 35, angle + arc, angle, true);
ctx.fill();
ctx.lineWidth = 3;
ctx.strokeStyle = '#FFFFFF';
ctx.stroke();
// Draw Vertical Upright Finals ONLY
ctx.save();
const midAngle = angle + arc / 2;
const textRadius = 122;
const textX = centerX + Math.cos(midAngle) * textRadius;
const textY = centerY + Math.sin(midAngle) * textRadius;
ctx.translate(textX, textY);
ctx.fillStyle = '#000000';
ctx.font = '900 28px "Noto Sans TC", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const finalStr = specifiedFinals[i];
const allSymbols = finalStr.split('');
// Vertical spacing offset based on final symbol count
if (allSymbols.length === 2) {
ctx.fillText(allSymbols[0], 0, -16);
ctx.fillText(allSymbols[1], 0, 16);
} else if (allSymbols.length === 3) {
ctx.fillText(allSymbols[0], 0, -26);
ctx.fillText(allSymbols[1], 0, 0);
ctx.fillText(allSymbols[2], 0, 26);
} else {
ctx.fillText(allSymbols[0], 0, 0);
}
ctx.restore();
}
// Outer Thick Yellow Frame Ring
ctx.beginPath();
ctx.arc(centerX, centerY, outerRadius + 8, 0, 2 * Math.PI);
ctx.lineWidth = 16;
ctx.strokeStyle = '#eab308';
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX, centerY, outerRadius + 16, 0, 2 * Math.PI);
ctx.lineWidth = 3;
ctx.strokeStyle = '#ca8a04';
ctx.stroke();
// Center Knob
ctx.beginPath();
ctx.arc(centerX, centerY, 38, 0, 2 * Math.PI);
ctx.fillStyle = '#facc15';
ctx.fill();
ctx.lineWidth = 5;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX, centerY, 16, 0, 2 * Math.PI);
ctx.fillStyle = '#ca8a04';
ctx.fill();
}
function toggleSpin() {
if (!isSpinning) {
isSpinning = true;
spinSpeed = Math.random() * 0.15 + 0.35;
updateResultDisplay('');
animateSpin();
} else {
isSpinning = false;
}
}
function animateSpin() {
if (isSpinning) {
startAngle += spinSpeed;
drawWheel();
animId = requestAnimationFrame(animateSpin);
} else {
spinSpeed *= 0.96;
startAngle += spinSpeed;
drawWheel();
if (spinSpeed > 0.002) {
animId = requestAnimationFrame(animId ? animateSpin : animateSpin);
} else {
cancelAnimationFrame(animId);
calculateResult();
}
}
}
function calculateResult() {
const numSlices = 8;
const arc = (2 * Math.PI) / numSlices;
// Pointer angle is top-center at 270 degrees (3/2 PI) pointing DOWN
const pointerAngle = (270 * Math.PI) / 180;
let normalizedAngle = (pointerAngle - (startAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
let index = Math.floor(normalizedAngle / arc) % numSlices;
const chosenFinal = specifiedFinals[index];
// Update Result Display directly with the target 結合韻
updateResultDisplay(chosenFinal);
// Confetti Effect
triggerConfetti();
}
function updateResultDisplay(text) {
const card = document.getElementById('resultCard');
const target = document.getElementById('resultText');
if (text) {
target.innerText = text;
card.classList.remove('opacity-0');
card.classList.add('pop-in');
} else {
card.classList.add('opacity-0');
card.classList.remove('pop-in');
}
}
function triggerConfetti() {
if (typeof confetti === 'function') {
confetti({
particleCount: 50,
spread: 60,
origin: { y: 0.6 }
});
}
}
</script>
</body>
</html>