コード · 🪄魔法結合韻🪄

CC BY 4.0 · @SpringFairyのクレジットを残してください · v1

← ゲームの詳細

持っていく。リミックスする。授業で使う。

下のコードをChatGPT、Claude、Gemini、Grokなど好きなツールにコピーし、クラス向けに変えてから、クレジット付きでリミックスをここに公開してください。

⬇ HTMLをダウンロード ⬇ ZIPをダウンロード
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>