Spaces:
Running
Running
| <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> | |
| <!-- Font Awesome (for icons) --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;500;700&family=Orbitron:wght@500;700&display=swap" rel="stylesheet"> | |
| <!-- html2canvas (截圖功能核心) --> | |
| <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['"Noto Sans TC"', 'sans-serif'], | |
| tech: ['"Orbitron"', 'sans-serif'], | |
| }, | |
| colors: { | |
| magic: { | |
| dark: '#0f172a', | |
| light: '#1e293b', | |
| accent: '#06b6d4', // Cyan | |
| secondary: '#8b5cf6', // Violet | |
| glow: 'rgba(6, 182, 212, 0.6)' | |
| } | |
| }, | |
| animation: { | |
| 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |
| 'float': 'float 6s ease-in-out infinite', | |
| }, | |
| keyframes: { | |
| float: { | |
| '0%, 100%': { transform: 'translateY(0)' }, | |
| '50%': { transform: 'translateY(-10px)' }, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| body { | |
| background-color: #0f172a; | |
| background-image: | |
| radial-gradient(circle at 15% 50%, rgba(139, 92, 246, 0.15) 0%, transparent 25%), | |
| radial-gradient(circle at 85% 30%, rgba(6, 182, 212, 0.15) 0%, transparent 25%); | |
| color: #e2e8f0; | |
| overflow-x: hidden; | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #1e293b; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #475569; | |
| border-radius: 4px; | |
| } | |
| /* Magic Card Effect */ | |
| .magic-card { | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 0 15px rgba(6, 182, 212, 0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .magic-border { | |
| position: relative; | |
| } | |
| .magic-border::before { | |
| content: ''; | |
| position: absolute; | |
| top: -2px; left: -2px; right: -2px; bottom: -2px; | |
| background: linear-gradient(45deg, #06b6d4, #8b5cf6, #06b6d4); | |
| z-index: -1; | |
| border-radius: inherit; | |
| opacity: 0.5; | |
| filter: blur(5px); | |
| } | |
| .option-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 0 20px rgba(6, 182, 212, 0.4); | |
| border-color: #06b6d4; | |
| background: rgba(30, 41, 59, 0.9); | |
| } | |
| /* New: Selected Option Style */ | |
| .option-btn.selected { | |
| background: linear-gradient(90deg, rgba(6, 182, 212, 0.25), rgba(139, 92, 246, 0.25)); | |
| border-color: #06b6d4; | |
| box-shadow: 0 0 25px rgba(6, 182, 212, 0.6); | |
| transform: scale(1.02); | |
| z-index: 10; | |
| } | |
| /* Disabled option style */ | |
| .option-btn:disabled { | |
| pointer-events: none; | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s ease-out forwards; | |
| } | |
| .fade-out { | |
| animation: fadeOut 0.5s ease-out forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes fadeOut { | |
| from { opacity: 1; transform: translateY(0); } | |
| to { opacity: 0; transform: translateY(-10px); } | |
| } | |
| /* Hexagon shape for decorative elements */ | |
| .hex-bg { | |
| clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex items-center justify-center font-sans selection:bg-magic-accent selection:text-black"> | |
| <!-- Background Decorative Elements --> | |
| <div class="fixed top-10 left-10 w-32 h-32 bg-magic-secondary opacity-10 blur-[50px] rounded-full animate-pulse-slow"></div> | |
| <div class="fixed bottom-10 right-10 w-48 h-48 bg-magic-accent opacity-10 blur-[50px] rounded-full animate-pulse-slow" style="animation-delay: 1.5s;"></div> | |
| <!-- Main Container --> | |
| <div id="app-container" class="relative w-full max-w-2xl px-6 py-8 mx-auto z-10"> | |
| <!-- START SCREEN --> | |
| <div id="start-screen" class="text-center space-y-8 fade-in flex flex-col items-center justify-center min-h-[60vh]"> | |
| <div class="relative inline-block mb-4 animate-float"> | |
| <div class="absolute inset-0 bg-magic-accent blur-[20px] opacity-30 rounded-full"></div> | |
| <i class="fa-solid fa-dice-d20 text-6xl text-cyan-400 relative z-10 drop-shadow-[0_0_10px_rgba(6,182,212,0.8)]"></i> | |
| </div> | |
| <h1 class="text-4xl md:text-5xl font-bold font-tech text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-purple-400 drop-shadow-sm leading-tight"> | |
| 遊戲化教學<br>天賦覺醒 | |
| </h1> | |
| <p class="text-slate-300 text-lg md:text-xl max-w-md mx-auto leading-relaxed border-t border-b border-slate-700 py-4"> | |
| 尋找你在遊戲化課堂中的<br><span class="text-cyan-300 font-semibold">天命職業</span> | |
| </p> | |
| <button onclick="quizApp.startQuiz()" class="group relative px-8 py-4 bg-transparent overflow-hidden rounded-full mt-8 magic-border"> | |
| <div class="absolute inset-0 w-full h-full bg-slate-800 transition-all duration-300 group-hover:bg-slate-700"></div> | |
| <span class="relative text-xl font-bold text-cyan-300 tracking-wider group-hover:text-white transition-colors"> | |
| 開始測驗 <i class="fa-solid fa-arrow-right ml-2 group-hover:translate-x-1 transition-transform"></i> | |
| </span> | |
| </button> | |
| </div> | |
| <!-- QUIZ SCREEN --> | |
| <div id="quiz-screen" class="hidden"> | |
| <!-- Header / Progress --> | |
| <div class="mb-8 flex flex-col gap-2"> | |
| <div class="flex justify-between items-end text-sm text-cyan-400 font-tech"> | |
| <span id="level-display">Level 1</span> | |
| <span id="progress-text">1 / 6</span> | |
| </div> | |
| <div class="h-2 w-full bg-slate-800 rounded-full overflow-hidden border border-slate-700"> | |
| <div id="progress-bar" class="h-full bg-gradient-to-r from-cyan-500 to-purple-500 w-0 transition-all duration-500 ease-out shadow-[0_0_10px_#06b6d4]"></div> | |
| </div> | |
| </div> | |
| <!-- Question Card --> | |
| <div id="question-container" class="magic-card p-8 rounded-2xl mb-6 text-center min-h-[160px] flex items-center justify-center relative overflow-hidden"> | |
| <div class="absolute top-0 left-0 w-1 h-full bg-gradient-to-b from-cyan-500 to-transparent"></div> | |
| <h2 id="question-text" class="text-xl md:text-2xl font-bold text-slate-100 leading-relaxed"> | |
| 載入題目中... | |
| </h2> | |
| </div> | |
| <!-- Options Grid --> | |
| <div id="options-container" class="grid grid-cols-1 gap-4"> | |
| <!-- Options will be injected here JS --> | |
| </div> | |
| </div> | |
| <!-- RESULT SCREEN --> | |
| <div id="result-screen" class="hidden text-center space-y-6"> | |
| <!-- Result Content Wrapper for Capture (ID added here) --> | |
| <div id="result-capture-area" class="bg-slate-900/0 rounded-2xl p-2 md:p-4 transition-colors"> | |
| <!-- Modified Image Container for Banner (1375x763) --> | |
| <div class="magic-card p-1 rounded-2xl w-full relative magic-border mt-4 hover:scale-[1.02] transition-transform duration-500"> | |
| <div class="bg-slate-900 rounded-xl overflow-hidden relative z-10"> | |
| <!-- Changed classes to support landscape banner --> | |
| <!-- 圖片來源將會是相對路徑,無需 crossorigin,且因為同源,截圖沒問題 --> | |
| <img id="result-image" src="" alt="Role Image" class="w-full h-auto object-contain shadow-lg"> | |
| </div> | |
| </div> | |
| <div class="space-y-2 fade-in mt-6" style="animation-delay: 0.2s;"> | |
| <p class="text-cyan-400 font-tech tracking-widest text-sm uppercase">Class Awakened</p> | |
| <h2 id="result-title" class="text-3xl md:text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400"> | |
| 職業名稱 | |
| </h2> | |
| </div> | |
| <div class="magic-card p-6 rounded-xl text-left space-y-4 fade-in mt-6" style="animation-delay: 0.4s;"> | |
| <div class="flex items-start gap-3"> | |
| <i class="fa-solid fa-scroll text-purple-400 mt-1"></i> | |
| <div> | |
| <h3 class="text-purple-300 font-bold mb-1">天賦解析</h3> | |
| <p id="result-desc" class="text-slate-300 text-sm leading-relaxed"> | |
| 描述文字... | |
| </p> | |
| </div> | |
| </div> | |
| <div class="border-t border-slate-700 my-2"></div> | |
| <div class="flex items-start gap-3"> | |
| <i class="fa-solid fa-dungeon text-cyan-400 mt-1"></i> | |
| <div> | |
| <h3 class="text-cyan-300 font-bold mb-1">本場研習使命</h3> | |
| <p id="result-mission" class="text-slate-200 font-medium text-sm leading-relaxed bg-slate-800/50 p-3 rounded-lg border-l-2 border-cyan-500"> | |
| 使命文字... | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- End Capture Area --> | |
| <div class="pt-4 fade-in flex flex-col md:flex-row justify-center gap-4" style="animation-delay: 0.6s;"> | |
| <button onclick="quizApp.restartQuiz()" class="px-8 py-3 bg-slate-800 hover:bg-slate-700 border border-slate-600 rounded-full text-slate-300 transition-all hover:text-white hover:border-cyan-400 flex items-center justify-center gap-2 order-2 md:order-1"> | |
| <i class="fa-solid fa-rotate-right"></i> 重新測驗 | |
| </button> | |
| <button id="share-btn" onclick="quizApp.shareResult()" class="px-8 py-3 bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600 border border-transparent rounded-full text-white transition-all flex items-center justify-center gap-2 order-1 md:order-2 magic-border shadow-lg shadow-cyan-500/30"> | |
| <i class="fa-solid fa-share-nodes"></i> 分享結果圖片 | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- JavaScript Logic --> | |
| <script> | |
| const quizApp = { | |
| // Data | |
| questions: [ | |
| { | |
| id: 1, | |
| text: "當你聽到學生在下課時熱烈討論某款手機遊戲,你的反應是?", | |
| options: [ | |
| { text: "立刻湊過去聽,甚至問好不好玩。", scores: { world: 1, adv: 1, arch: 1 } }, | |
| { text: "心想:如果他們算數學也這麼專注就好了。", scores: { adv: 1, alch: 1 } }, | |
| { text: "思考這款遊戲機制能不能用在課堂上。", scores: { world: 1, alch: 1 } }, | |
| { text: "覺得浪費時間,應該多花時間在課業。", scores: { truth: 2 } } | |
| ] | |
| }, | |
| { | |
| id: 2, | |
| text: "形容你理想中的「完美課堂」:", | |
| options: [ | |
| { text: "驚喜不斷:充滿未知挑戰與彩蛋。", scores: { world: 1, adv: 1 } }, | |
| { text: "熱血沸騰:學生充滿動力搶答。", scores: { adv: 1, alch: 1 } }, | |
| { text: "有效轉化:學生能真正吸收知識。", scores: { world: 1, alch: 1 } }, | |
| { text: "井然有序:目標明確,專注自律。", scores: { truth: 2 } } | |
| ] | |
| }, | |
| { | |
| id: 3, | |
| text: "關於「玩遊戲」,你的經驗是?", | |
| options: [ | |
| { text: "資深玩家,對機制如數家珍。", scores: { world: 1, adv: 1 } }, | |
| { text: "喜歡玩遊戲的放鬆感,但不懂設計。", scores: { adv: 1, alch: 1 } }, | |
| { text: "偶爾玩休閒遊戲,不是狂熱者。", scores: { world: 1, alch: 1 } }, | |
| { text: "很少玩,不懂為何大家花時間在虛擬世界。", scores: { truth: 2 } } | |
| ] | |
| }, | |
| { | |
| id: 4, | |
| text: "將遊戲引入教學,最大的風險是?", | |
| options: [ | |
| { text: "設計得不好玩,學生覺得尷尬。", scores: { alch: 1, adv: 1 } }, | |
| { text: "只有好玩,但沒學到東西。", scores: { world: 1, adv: 1, alch: 1 } }, | |
| { text: "準備時間太長,無法保證成效。", scores: { world: 1, alch: 1 } }, | |
| { text: "秩序失控,學生只想玩不想上課。", scores: { truth: 2 } } | |
| ] | |
| }, | |
| { | |
| id: 5, | |
| text: "如果有現成的遊戲化教案,你願意使用的原因是?", | |
| options: [ | |
| { text: "我會拆解邏輯,修改成我的版本。", scores: { world: 1, adv: 1 } }, | |
| { text: "太棒了!只要好玩我就想試試。", scores: { adv: 1, alch: 1 } }, | |
| { text: "能證明對成績或動機有效,我就試。", scores: { world: 1, alch: 1 } }, | |
| { text: "需嚴格審視教學目標是否清晰。", scores: { truth: 2 } } | |
| ] | |
| }, | |
| { | |
| id: 6, | |
| text: "今天來到這場研習,你內心的想法?", | |
| options: [ | |
| { text: "想找新點子優化教學設計。", scores: { world: 1, adv: 1 } }, | |
| { text: "不知從何下手,想知道怎麼帶氣氛。", scores: { adv: 1, alch: 1 } }, | |
| { text: "聽說有幫助,為了學生來學看看。", scores: { world: 1, alch: 1 } }, | |
| { text: "持保留態度,來看看是不是噱頭。", scores: { truth: 2 } } | |
| ] | |
| } | |
| ], | |
| // 圖片已改為本地路徑 (Local Paths) | |
| // 請確保 1.png, 2.png, 3.png, 4.png 與 index.html 位於同一目錄下 | |
| resultsData: { | |
| world: { | |
| title: "世界架構師", | |
| role: "World Architect", | |
| desc: "富有遠見與創造力,腦中充滿點子。擁有極強邏輯解構能力,天生為了創造規則而生。", | |
| mission: "【引領者】你的使命是思考如何將講師分享的內容「進化」,協助夥伴突破框架。", | |
| img: "1.png" | |
| }, | |
| adv: { | |
| title: "熱血冒險家", | |
| role: "Passionate Adventurer", | |
| desc: "擁有感染力與同理心,內心住著長不大的孩子。相信直覺,是點燃氣氛的火把。", | |
| mission: "【連結者】你的使命是保持「好玩」的初衷。當設計變枯燥時,請提醒大家:「這樣學生會覺得好玩嗎?」", | |
| img: "2.png" | |
| }, | |
| alch: { | |
| title: "靈魂煉金術師", | |
| role: "Soul Alchemist", | |
| desc: "溫暖務實,重視價值與成效。像在調配配方的智者,只在乎能否治癒學生的學習動機。", | |
| mission: "【轉化者】你的使命是確保機制與目標融合。請反問:「這能幫學生學到什麼?」將樂趣轉化為養分。", | |
| img: "3.png" | |
| }, | |
| truth: { | |
| title: "真理守望者", | |
| role: "Truth Guardian", | |
| desc: "理性冷靜,擁有批判性思維。不是為了反對而反對,而是為了保護教育本質不被模糊。", | |
| mission: "【優化者】你的使命是「挑戰」與「質疑」。找出可能導致混亂的漏洞,讓教案更經得起考驗。", | |
| img: "4.png" | |
| } | |
| }, | |
| // State | |
| currentQuestionIndex: 0, | |
| scores: { world: 0, adv: 0, alch: 0, truth: 0 }, | |
| isTransitioning: false, | |
| currentWinnerKey: '', | |
| init: function() { | |
| this.cacheDOM(); | |
| this.preloadImages(); // Start preloading images | |
| }, | |
| cacheDOM: function() { | |
| this.startScreen = document.getElementById('start-screen'); | |
| this.quizScreen = document.getElementById('quiz-screen'); | |
| this.resultScreen = document.getElementById('result-screen'); | |
| this.questionText = document.getElementById('question-text'); | |
| this.optionsContainer = document.getElementById('options-container'); | |
| this.levelDisplay = document.getElementById('level-display'); | |
| this.progressText = document.getElementById('progress-text'); | |
| this.progressBar = document.getElementById('progress-bar'); | |
| }, | |
| preloadImages: function() { | |
| console.log("Preloading images..."); | |
| Object.values(this.resultsData).forEach(data => { | |
| const img = new Image(); | |
| img.src = data.img; | |
| }); | |
| }, | |
| startQuiz: function() { | |
| this.currentQuestionIndex = 0; | |
| this.scores = { world: 0, adv: 0, alch: 0, truth: 0 }; | |
| this.isTransitioning = false; | |
| this.currentWinnerKey = ''; | |
| // Transition | |
| this.startScreen.classList.add('fade-out'); | |
| setTimeout(() => { | |
| this.startScreen.classList.add('hidden'); | |
| this.startScreen.classList.remove('fade-out'); | |
| this.quizScreen.classList.remove('hidden'); | |
| this.quizScreen.classList.add('fade-in'); | |
| this.renderQuestion(); | |
| }, 500); | |
| }, | |
| renderQuestion: function() { | |
| const q = this.questions[this.currentQuestionIndex]; | |
| // Update Progress | |
| const progressPercent = ((this.currentQuestionIndex) / this.questions.length) * 100; | |
| this.progressBar.style.width = `${progressPercent}%`; | |
| this.levelDisplay.textContent = `Level ${this.currentQuestionIndex + 1}`; | |
| this.progressText.textContent = `${this.currentQuestionIndex + 1} / ${this.questions.length}`; | |
| // Animate Text Change | |
| this.questionText.parentElement.classList.remove('fade-in'); | |
| void this.questionText.parentElement.offsetWidth; // trigger reflow | |
| this.questionText.parentElement.classList.add('fade-in'); | |
| this.questionText.textContent = q.text; | |
| // Render Options | |
| this.optionsContainer.innerHTML = ''; | |
| q.options.forEach((opt, index) => { | |
| const btn = document.createElement('button'); | |
| btn.className = `option-btn w-full p-4 rounded-xl text-left border border-slate-700 bg-slate-800/80 text-slate-200 transition-all duration-300 relative overflow-hidden group`; | |
| btn.style.animation = `fadeIn 0.5s ease-out forwards ${index * 0.1}s`; | |
| btn.style.opacity = '0'; // Start invisible for animation | |
| btn.disabled = false; // Ensure button is enabled initially | |
| // Option Label (A, B, C, D) | |
| const labels = ['A', 'B', 'C', 'D']; | |
| btn.innerHTML = ` | |
| <div class="flex items-center gap-4 relative z-10"> | |
| <span class="flex-shrink-0 w-8 h-8 rounded-full border border-cyan-500/50 flex items-center justify-center text-cyan-400 font-bold font-tech group-hover:bg-cyan-500 group-hover:text-black transition-colors"> | |
| ${labels[index]} | |
| </span> | |
| <span class="text-base md:text-lg group-hover:text-white">${opt.text}</span> | |
| </div> | |
| <div class="absolute inset-0 bg-gradient-to-r from-cyan-900/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> | |
| `; | |
| // Pass the button element (e.currentTarget) to handleAnswer | |
| btn.onclick = (e) => this.handleAnswer(opt.scores, e.currentTarget); | |
| this.optionsContainer.appendChild(btn); | |
| }); | |
| }, | |
| handleAnswer: function(points, btnElement) { | |
| // Prevent double clicking | |
| if (this.isTransitioning) return; | |
| this.isTransitioning = true; | |
| // Disable all buttons | |
| const allBtns = this.optionsContainer.querySelectorAll('.option-btn'); | |
| allBtns.forEach(b => { | |
| b.disabled = true; | |
| if (b !== btnElement) { | |
| b.style.opacity = '0.5'; | |
| } | |
| b.style.cursor = 'default'; | |
| }); | |
| // Visual Feedback: Highlight selected | |
| btnElement.classList.add('selected'); | |
| // Add scores | |
| for (let key in points) { | |
| if (this.scores.hasOwnProperty(key)) { | |
| this.scores[key] += points[key]; | |
| } | |
| } | |
| // Delay logic to allow user to see the selection (500ms delay) | |
| setTimeout(() => { | |
| this.currentQuestionIndex++; | |
| if (this.currentQuestionIndex < this.questions.length) { | |
| this.renderQuestion(); | |
| this.isTransitioning = false; // Unlock for next question | |
| } else { | |
| this.showResult(); | |
| this.isTransitioning = false; | |
| } | |
| }, 500); | |
| }, | |
| showResult: function() { | |
| // Determine Winner with Tie-breaking Logic | |
| let maxScore = -1; | |
| let winners = []; | |
| for (const [key, value] of Object.entries(this.scores)) { | |
| if (value > maxScore) { | |
| maxScore = value; | |
| winners = [key]; | |
| } else if (value === maxScore) { | |
| winners.push(key); | |
| } | |
| } | |
| // Randomly select one winner from the top scorers | |
| this.currentWinnerKey = winners[Math.floor(Math.random() * winners.length)]; | |
| const resultData = this.resultsData[this.currentWinnerKey]; | |
| // DOM Updates | |
| document.getElementById('result-title').textContent = resultData.title; | |
| document.getElementById('result-image').src = resultData.img; | |
| document.getElementById('result-desc').textContent = resultData.desc; | |
| document.getElementById('result-mission').textContent = resultData.mission; | |
| // Screen Transition | |
| this.quizScreen.classList.add('fade-out'); | |
| setTimeout(() => { | |
| this.quizScreen.classList.add('hidden'); | |
| this.quizScreen.classList.remove('fade-out'); | |
| this.resultScreen.classList.remove('hidden'); | |
| this.resultScreen.classList.add('fade-in'); | |
| // Fill bar to 100% | |
| this.progressBar.style.width = '100%'; | |
| }, 500); | |
| }, | |
| shareResult: async function() { | |
| const shareBtn = document.getElementById('share-btn'); | |
| const originalContent = shareBtn.innerHTML; | |
| // Loading State | |
| shareBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> 圖片生成中...'; | |
| shareBtn.disabled = true; | |
| try { | |
| // Capture specific area | |
| const captureElement = document.getElementById('result-capture-area'); | |
| const resultImage = document.getElementById('result-image'); | |
| const originalSrc = resultImage.src; | |
| // ------------------------------------------------------------- | |
| // FIX: SecurityError: Tainted canvases may not be exported | |
| // ------------------------------------------------------------- | |
| // 即使是同源圖片,有時瀏覽器快取或載入方式仍會導致 Taint。 | |
| // 最穩定的解法:先用 fetch 把圖片抓下來,轉成 Base64,再塞回去 img.src | |
| // 這樣對 html2canvas 來說就是「純資料」,絕對不會有安全問題。 | |
| // 1. Fetch the image as a blob | |
| const response = await fetch(originalSrc); | |
| const blob = await response.blob(); | |
| // 2. Convert blob to Base64 Data URL | |
| const base64Url = await new Promise((resolve) => { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => resolve(reader.result); | |
| reader.readAsDataURL(blob); | |
| }); | |
| // 3. Temporarily replace image source with Base64 and WAIT for it to load | |
| // This fixes the race condition where image isn't ready for canvas | |
| const imageLoaded = new Promise((resolve) => { | |
| resultImage.onload = resolve; | |
| }); | |
| resultImage.src = base64Url; | |
| await imageLoaded; | |
| // ------------------------------------------------------------- | |
| // Temporarily add a background to the capture area | |
| const originalBg = captureElement.style.backgroundColor; | |
| captureElement.style.backgroundColor = '#0f172a'; // Dark blue bg | |
| captureElement.style.padding = '20px'; // Add padding for screenshot | |
| // Generate Canvas | |
| const canvas = await html2canvas(captureElement, { | |
| backgroundColor: '#0f172a', | |
| scale: 2, // High resolution | |
| useCORS: true, | |
| logging: false | |
| }); | |
| // Restore styles & Image source | |
| captureElement.style.backgroundColor = originalBg; | |
| captureElement.style.padding = ''; | |
| resultImage.src = originalSrc; // Restore original path | |
| // Convert to Blob | |
| canvas.toBlob(async (blob) => { | |
| if (!blob) { | |
| alert('截圖生成失敗,請檢查網路連線或使用原生截圖功能。'); | |
| shareBtn.innerHTML = originalContent; | |
| shareBtn.disabled = false; | |
| return; | |
| } | |
| const file = new File([blob], 'gamified-teaching-result.png', { type: 'image/png' }); | |
| // Mobile Native Share (Web Share API Level 2) | |
| if (navigator.canShare && navigator.canShare({ files: [file] })) { | |
| try { | |
| await navigator.share({ | |
| files: [file], | |
| title: '遊戲化教學天賦覺醒', | |
| text: `我是『${this.resultsData[this.currentWinnerKey].title}』!測測看你是哪種角色:` | |
| }); | |
| } catch (err) { | |
| console.log('Share cancelled or failed', err); | |
| } | |
| } else { | |
| // Fallback for Desktop: Download | |
| const link = document.createElement('a'); | |
| link.download = `teaching-style-${this.currentWinnerKey}.png`; | |
| link.href = canvas.toDataURL(); | |
| link.click(); | |
| alert('您的裝置不支援直接圖片分享,已為您下載結果圖片!您現在可以手動發送到 Line 或 IG。'); | |
| } | |
| // Restore Button | |
| shareBtn.innerHTML = originalContent; | |
| shareBtn.disabled = false; | |
| }, 'image/png'); | |
| } catch (error) { | |
| console.error('Error generating image:', error); | |
| alert('圖片生成失敗,請稍後再試或使用手機原生截圖。'); | |
| shareBtn.innerHTML = originalContent; | |
| shareBtn.disabled = false; | |
| } | |
| } | |
| }; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| quizApp.init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |