charactertest / index.html
Lashtw's picture
Update index.html
137f900 verified
<!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>