Spaces:
Running
Running
File size: 31,696 Bytes
da53305 06d337f da53305 06d337f a2c9644 06d337f da53305 06d337f da53305 06d337f da53305 06d337f da53305 06d337f da53305 06d337f da53305 ebf2f95 dfb4c98 da53305 ebf2f95 06d337f da53305 a2c9644 da53305 5c14976 da53305 5c14976 da53305 5c14976 da53305 5c14976 da53305 06d337f 3d0ed23 06d337f 3d0ed23 137f900 3d0ed23 137f900 3d0ed23 06d337f 3d0ed23 06d337f 3d0ed23 06d337f da53305 3d0ed23 06d337f 3d0ed23 06d337f a2c9644 06d337f da53305 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 |
<!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>
<!-- 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> |