Math_Adventure_8thgrade / algebra.html
Lashtw's picture
Upload algebra.html
9c0a211 verified
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>數學探險島 - 代數之丘</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Noto Sans TC', sans-serif;
touch-action: none; /* 防止在觸控拖曳時滾動頁面 */
background-image: url('https://i.meee.com.tw/AF01kln.png');
background-size: cover;
background-position: center;
}
/* 自訂積木顏色 */
.block-a-squared { background-color: #fca5a5; } /* red-300 */
.block-b-squared { background-color: #818cf8; } /* indigo-400 */
.block-ab { background-color: #fcd34d; } /* amber-300 */
.block-distractor { background-color: #9ca3af; } /* gray-400 */
/* 拼圖區格線 */
#grid-container {
display: grid;
background-image:
linear-gradient(to right, #d1d5db 1px, transparent 1px),
linear-gradient(to bottom, #d1d5db 1px, transparent 1px);
background-size: var(--unit-size) var(--unit-size);
border: 2px solid #6b7280;
}
/* 拖曳中的複製物件 */
.dragging-clone {
position: absolute;
pointer-events: none; /* 讓滑鼠事件穿透複製物件 */
z-index: 1000;
opacity: 0.8;
border: 2px dashed #4f46e5;
}
/* 已放置在選項區的積木 */
.palette-block.placed {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
}
/* 已放置在拼圖區的積木 */
.placed-block {
position: absolute;
cursor: pointer;
border: 2px solid #1f2937;
transition: all 0.2s ease-in-out;
}
.placed-block:hover {
transform: scale(1.05);
box-shadow: 0 0 15px rgba(0,0,0,0.5);
}
/* 過關時的特別框線 */
.highlight-win {
background-color: #fef9c3; /* yellow-100 */
border: 2px solid #f59e0b; /* amber-500 */
border-radius: 8px;
padding: 2px 6px;
display: inline-block;
}
/* 隱藏滾動條,但在觸控設備上仍可滾動 */
#block-palette::-webkit-scrollbar {
display: none;
}
#block-palette {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
</head>
<body class="w-screen h-screen overflow-hidden flex items-center justify-center relative">
<!-- 任務說明畫面 -->
<div id="start-screen" class="absolute inset-0 w-screen h-screen flex items-center justify-center p-4 transition-opacity duration-500 bg-gray-900/50">
<div class="container mx-auto p-6 md:p-8 bg-white/90 backdrop-blur-sm rounded-xl shadow-2xl max-w-2xl text-center">
<h1 class="text-3xl md:text-4xl font-bold text-indigo-600 mb-6">任務說明:代數之丘</h1>
<p class="text-lg text-gray-700 mb-4">歡迎來到代數之丘!在這裡,我們不用死背公式,而是用雙手「拼」出數學!</p>
<p class="text-lg text-gray-700 mb-8">你的任務是拖曳下方的彩色積木,將左邊的灰色正方形完全填滿,親身體驗 <span class="font-mono font-bold text-indigo-700">(a+b)² = a² + 2ab + b²</span> 這個公式是如何誕生的!</p>
<button id="start-button" class="w-full md:w-1/2 bg-indigo-600 text-white font-bold py-3 md:py-4 px-6 rounded-lg hover:bg-indigo-700 transition-colors shadow-lg text-lg md:text-xl">
開始挑戰
</button>
</div>
</div>
<!-- 遊戲主畫面 -->
<div id="game-container" class="flex flex-row w-full h-full max-w-screen-xl mx-auto p-4 lg:p-8 gap-8 items-start transition-opacity duration-500 opacity-0 hidden">
<!-- 左欄:包含拼圖區和過關訊息 -->
<div class="w-1/3 flex flex-col items-center justify-start gap-6">
<div id="grid-container" class="relative bg-gray-200/80 backdrop-blur-sm shadow-xl rounded-lg">
<!-- 拼圖區的積木會由 JS 動態加入這裡 -->
</div>
<!-- 過關訊息與按鈕 (已移至此處) -->
<div id="win-message-container" class="w-full text-center p-4 bg-white/90 backdrop-blur-sm rounded-xl shadow-lg hidden">
<p class="text-2xl font-bold text-green-600">恭喜過關!</p>
<p id="win-formula-explanation" class="text-lg text-gray-700 mt-2"></p>
<button id="next-level-button" class="mt-4 w-full bg-indigo-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-indigo-700 transition-colors shadow-md">
挑戰下一關
</button>
</div>
</div>
<!-- 右欄:控制項 -->
<div id="controls-container" class="w-2/3 flex-shrink-0 flex flex-col bg-white/90 backdrop-blur-sm rounded-xl shadow-lg p-6 h-full">
<h1 class="text-3xl font-bold text-gray-800 text-center">代數之丘</h1>
<hr class="my-4">
<!-- 算式區 -->
<div class="bg-blue-50/80 p-4 rounded-lg mb-4 space-y-2">
<!-- 題目行 -->
<div class="grid grid-cols-4 items-baseline">
<p class="text-lg text-gray-600 font-semibold col-span-1">題目:</p>
<p id="equation-title" class="col-span-3 text-2xl font-bold text-indigo-700 text-center"></p>
</div>
<!-- 面積結果行 -->
<div class="grid grid-cols-4 items-baseline">
<p class="text-lg text-gray-600 font-semibold col-span-1">面積結果:</p>
<div id="equation-result" class="col-span-3 text-2xl font-mono text-gray-800 text-center"></div>
</div>
</div>
<!-- 說明文字 -->
<div id="instructions" class="text-center text-gray-500 mb-4 text-sm">
<p>請從下方拖曳積木,拼滿左邊的正方形。</p>
<p class="mt-1">💡 <span class="font-semibold">提示:</span>點擊已放置的積木可以將它移除。</p>
</div>
<!-- 積木選項區 (包含捲動按鈕) -->
<div class="relative mt-auto">
<div id="block-palette" class="flex flex-row flex-nowrap overflow-x-auto items-center gap-4 p-4 bg-gray-100/80 rounded-md">
<!-- 積木選項會由 JS 動態加入這裡 -->
</div>
<!-- 左捲動按鈕 -->
<button id="scroll-left-button" class="absolute left-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-1 shadow-md hidden transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<!-- 右捲動按鈕 -->
<button id="scroll-right-button" class="absolute right-0 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-1 shadow-md hidden transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</div>
<!-- 秘密揭曉畫面 (從 secret.html 整合進來) -->
<div id="secret-view" class="absolute inset-0 w-screen h-screen flex items-center justify-center p-4 opacity-0 hidden transition-opacity duration-500 bg-gray-900/50">
<div class="container mx-auto p-6 md:p-8 bg-white rounded-xl shadow-2xl max-w-4xl text-center overflow-y-auto max-h-[90vh]">
<h1 class="text-3xl md:text-4xl font-bold text-indigo-600 mb-8">代數之丘最大的秘密</h1>
<div class="grid md:grid-cols-3 gap-6 md:gap-8 mb-8">
<!-- Image 1 -->
<div class="flex flex-col items-center p-2 border rounded-lg">
<img src="https://i.meee.com.tw/Sx68qrS.png" alt="拼圖 (6+4)²" class="rounded-md shadow-md mb-4 w-full" onerror="this.onerror=null;this.src='https://placehold.co/400x400/fca5a5/ffffff?text=(6%2B4)%C2%B2';">
<p class="text-lg md:text-xl font-mono font-bold">(6+4)² = 6² + <span class="highlight-win">2×6×4</span> + 4²</p>
</div>
<!-- Image 2 -->
<div class="flex flex-col items-center p-2 border rounded-lg">
<img src="https://i.meee.com.tw/7gGAB80.png" alt="拼圖 (4+9)²" class="rounded-md shadow-md mb-4 w-full" onerror="this.onerror=null;this.src='https://placehold.co/400x400/818cf8/ffffff?text=(4%2B9)%C2%B2';">
<p class="text-lg md:text-xl font-mono font-bold">(4+9)² = 4² + <span class="highlight-win">2×4×9</span> + 9²</p>
</div>
<!-- Image 3 -->
<div class="flex flex-col items-center p-2 border rounded-lg">
<img src="https://i.meee.com.tw/0hdfRvP.png" alt="拼圖 (7+5)²" class="rounded-md shadow-md mb-4 w-full" onerror="this.onerror=null;this.src='https://placehold.co/400x400/fcd34d/ffffff?text=(7%2B5)%C2%B2';">
<p class="text-lg md:text-xl font-mono font-bold">(7+5)² = 7² + <span class="highlight-win">2×7×5</span> + 5²</p>
</div>
</div>
<div class="text-left text-base md:text-lg text-gray-700 space-y-4 border-t-2 pt-8">
<p>選舉最大的祕密就是:票多的贏,票少的輸...<span class="italic text-gray-500">咳咳不是這個啦</span></p>
<p class="text-xl md:text-2xl font-bold text-red-600">代數之丘的最大秘密就是:(a+b)²展開後一定會有ab這項,而且還是<span class="underline decoration-wavy decoration-amber-500">2ab</span>,學會這個,這單元就已經學會一半了!</p>
<p class="font-semibold text-indigo-700">(請在學習單上做紀錄!)</p>
</div>
<div class="mt-12">
<a href="index.html" class="inline-block w-full md:w-1/2 bg-indigo-600 text-white font-bold py-3 md:py-4 px-6 rounded-lg hover:bg-indigo-700 transition-colors shadow-lg text-lg md:text-xl">
回到探險島地圖
</a>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- 常數與設定 ---
const UNIT_SIZE = 30; // 每個單位格的像素大小 (px)
const levels = [
{ a: 6, b: 4, distractors: [] },
{ a: 4, b: 9, distractors: [{w: 5, h: 8}, {w: 7, h: 6}] },
{ a: 7, b: 5, distractors: [{w: 8, h: 6}, {w: 6, h: 8}] }
];
let currentLevel = 0;
// --- DOM 元素 ---
const startScreen = document.getElementById('start-screen');
const startButton = document.getElementById('start-button');
const gameContainer = document.getElementById('game-container');
const secretView = document.getElementById('secret-view');
const gridContainer = document.getElementById('grid-container');
const blockPalette = document.getElementById('block-palette');
const equationTitle = document.getElementById('equation-title');
const equationResult = document.getElementById('equation-result');
const winMessageContainer = document.getElementById('win-message-container');
const winFormulaExplanation = document.getElementById('win-formula-explanation');
const nextLevelButton = document.getElementById('next-level-button');
const scrollLeftButton = document.getElementById('scroll-left-button');
const scrollRightButton = document.getElementById('scroll-right-button');
// --- 遊戲狀態 ---
let gridState = []; // 2D 陣列,記錄拼圖區每個格子的佔用情況
let placedBlocks = new Map(); // 記錄已放置的積木 (key: placedId, value: { el, originalId, type })
let draggingElement = null; // 目前正在拖曳的原始積木
let clone = null; // 拖曳時的複製物件
let offset = { x: 0, y: 0 }; // 拖曳時的滑鼠偏移量
// --- 核心函式 ---
function initLevel(levelIndex) {
currentLevel = levelIndex;
const level = levels[levelIndex];
const totalSize = level.a + level.b;
gridContainer.innerHTML = '';
blockPalette.innerHTML = '';
winMessageContainer.classList.add('hidden');
placedBlocks.clear();
gridContainer.style.width = `${totalSize * UNIT_SIZE}px`;
gridContainer.style.height = `${totalSize * UNIT_SIZE}px`;
gridContainer.style.setProperty('--unit-size', `${UNIT_SIZE}px`);
gridState = Array(totalSize).fill(null).map(() => Array(totalSize).fill(null));
createBlocks(level);
updateEquation();
if (currentLevel === levels.length - 1) {
nextLevelButton.textContent = '查看代數的秘密';
} else {
nextLevelButton.textContent = '挑戰下一關';
}
// 延遲執行以確保 DOM 尺寸已更新
setTimeout(updateScrollButtonsVisibility, 100);
}
function createBlocks(level) {
const { a, b, distractors } = level;
const blocksData = [
{ id: 'a_squared', w: a, h: a, type: 'correct', class: 'block-a-squared', label: `${a}×${a}` },
{ id: 'b_squared', w: b, h: b, type: 'correct', class: 'block-b-squared', label: `${b}×${b}` },
{ id: 'ab1', w: a, h: b, type: 'correct', class: 'block-ab', label: `${a}×${b}` },
{ id: 'ab2', w: b, h: a, type: 'correct', class: 'block-ab', label: `${b}×${a}` },
...distractors.map((d, i) => ({
id: `distractor_${i}`, w: d.w, h: d.h, type: 'distractor', class: 'block-distractor', label: `${d.w}×${d.h}`
}))
];
blocksData.sort(() => Math.random() - 0.5);
blocksData.forEach(data => {
const blockEl = document.createElement('div');
blockEl.id = `palette-${data.id}`;
blockEl.className = `palette-block flex-shrink-0 ${data.class} rounded-md shadow-sm cursor-grab flex items-center justify-center text-white font-bold text-base`;
blockEl.style.width = `${data.w * UNIT_SIZE}px`;
blockEl.style.height = `${data.h * UNIT_SIZE}px`;
blockEl.textContent = data.label;
blockEl.dataset.id = data.id;
blockEl.dataset.w = data.w;
blockEl.dataset.h = data.h;
blockEl.dataset.type = data.type;
blockEl.dataset.class = data.class;
blockPalette.appendChild(blockEl);
});
}
function updateEquation() {
const level = levels[currentLevel];
equationTitle.textContent = `(${level.a} + ${level.b})²`;
const placedCorrectBlocks = Array.from(placedBlocks.values()).filter(b => b.type === 'correct');
let parts = [];
if (placedCorrectBlocks.some(b => b.originalId === 'a_squared')) parts.push(`${level.a}²`);
if (placedCorrectBlocks.some(b => b.originalId === 'b_squared')) parts.push(`${level.b}²`);
const abCount = placedCorrectBlocks.filter(b => b.originalId.startsWith('ab')).length;
if (abCount === 1) parts.push(`(${level.a}×${level.b})`);
if (abCount === 2) parts.push(`2(${level.a}×${level.b})`);
if (parts.length > 0) {
equationResult.innerHTML = parts.join(' + ');
} else {
equationResult.innerHTML = '...';
}
}
// --- 拖曳事件處理 ---
function onDragStart(e) {
const target = e.target.closest('.palette-block');
if (!target || target.classList.contains('placed')) return;
e.preventDefault();
draggingElement = target;
const rect = target.getBoundingClientRect();
const pointer = getPointer(e);
offset.x = pointer.x - rect.left;
offset.y = pointer.y - rect.top;
clone = target.cloneNode(true);
clone.classList.remove('palette-block');
clone.classList.add('dragging-clone');
clone.style.width = `${target.dataset.w * UNIT_SIZE}px`;
clone.style.height = `${target.dataset.h * UNIT_SIZE}px`;
document.body.appendChild(clone);
moveClone(pointer.x, pointer.y);
document.addEventListener('mousemove', onDragMove);
document.addEventListener('touchmove', onDragMove, { passive: false });
document.addEventListener('mouseup', onDragEnd);
document.addEventListener('touchend', onDragEnd);
}
function onDragMove(e) {
if (!clone) return;
e.preventDefault();
const pointer = getPointer(e);
moveClone(pointer.x, pointer.y);
}
function onDragEnd(e) {
if (!draggingElement || !clone) return;
const gridRect = gridContainer.getBoundingClientRect();
const pointer = getPointer(e);
const blockTopLeftX = pointer.x - gridRect.left - offset.x;
const blockTopLeftY = pointer.y - gridRect.top - offset.y;
const gridX = Math.round(blockTopLeftX / UNIT_SIZE);
const gridY = Math.round(blockTopLeftY / UNIT_SIZE);
const w = parseInt(draggingElement.dataset.w);
const h = parseInt(draggingElement.dataset.h);
const type = draggingElement.dataset.type;
if (canPlace(gridX, gridY, w, h, type)) {
placeBlock(gridX, gridY, w, h);
}
document.body.removeChild(clone);
clone = null;
draggingElement = null;
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('touchmove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
document.removeEventListener('touchend', onDragEnd);
}
function canPlace(gridX, gridY, w, h, type) {
const totalSize = levels[currentLevel].a + levels[currentLevel].b;
if (currentLevel === 1 && type === 'distractor') {
return false;
}
if (gridX < 0 || gridY < 0 || gridX + w > totalSize || gridY + h > totalSize) {
return false;
}
for (let i = gridY; i < gridY + h; i++) {
for (let j = gridX; j < gridX + w; j++) {
if (i >= totalSize || j >= totalSize || gridState[i][j] !== null) {
return false;
}
}
}
return true;
}
function placeBlock(gridX, gridY, w, h) {
const placedId = `placed-${Date.now()}`;
for (let i = gridY; i < gridY + h; i++) {
for (let j = gridX; j < gridX + w; j++) {
gridState[i][j] = placedId;
}
}
const placedEl = document.createElement('div');
placedEl.id = placedId;
placedEl.className = `placed-block ${draggingElement.dataset.class} flex items-center justify-center text-white font-bold`;
placedEl.style.left = `${gridX * UNIT_SIZE}px`;
placedEl.style.top = `${gridY * UNIT_SIZE}px`;
placedEl.style.width = `${w * UNIT_SIZE}px`;
placedEl.style.height = `${h * UNIT_SIZE}px`;
placedEl.textContent = `${w}×${h}`;
gridContainer.appendChild(placedEl);
placedBlocks.set(placedId, {
el: placedEl,
originalId: draggingElement.dataset.id,
type: draggingElement.dataset.type,
gridX, gridY, w, h
});
draggingElement.classList.add('placed');
placedEl.addEventListener('click', () => removeBlock(placedId));
updateEquation();
checkWinCondition();
}
function removeBlock(placedId) {
const block = placedBlocks.get(placedId);
if (!block) return;
gridContainer.removeChild(block.el);
for (let i = block.gridY; i < block.gridY + block.h; i++) {
for (let j = block.gridX; j < block.gridX + block.w; j++) {
if (gridState[i][j] === placedId) {
gridState[i][j] = null;
}
}
}
placedBlocks.delete(placedId);
const originalBlock = document.getElementById(`palette-${block.originalId}`);
if (originalBlock) {
originalBlock.classList.remove('placed');
}
updateEquation();
}
function checkWinCondition() {
const level = levels[currentLevel];
const totalSize = level.a + level.b;
const placedCorrectCount = Array.from(placedBlocks.values()).filter(b => b.type === 'correct').length;
if (placedCorrectCount !== 4) return;
let isFull = true;
for (let i = 0; i < totalSize; i++) {
for (let j = 0; j < totalSize; j++) {
if (gridState[i][j] === null) {
isFull = false;
break;
}
}
if (!isFull) break;
}
if (isFull) {
showWinState();
}
}
function showWinState() {
const level = levels[currentLevel];
const finalFormula = `(${level.a}+${level.b})² = ${level.a}² + <span class="highlight-win">2×${level.a}×${level.b}</span> + ${level.b}²`;
winFormulaExplanation.innerHTML = finalFormula;
winMessageContainer.classList.remove('hidden');
blockPalette.style.pointerEvents = 'none';
gridContainer.style.pointerEvents = 'none';
}
// --- 捲動與輔助函式 ---
function updateScrollButtonsVisibility() {
const palette = blockPalette;
const scrollLeft = palette.scrollLeft;
const scrollWidth = palette.scrollWidth;
const clientWidth = palette.clientWidth;
if (scrollWidth <= clientWidth) {
scrollLeftButton.classList.add('hidden');
scrollRightButton.classList.add('hidden');
return;
}
if (scrollLeft > 0) {
scrollLeftButton.classList.remove('hidden');
} else {
scrollLeftButton.classList.add('hidden');
}
if (scrollLeft < scrollWidth - clientWidth - 1) {
scrollRightButton.classList.remove('hidden');
} else {
scrollRightButton.classList.add('hidden');
}
}
function getPointer(e) {
// For touchend event, we need to use changedTouches because touches is empty.
if (e.changedTouches && e.changedTouches.length > 0) {
return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
}
// For touchstart and touchmove events.
if (e.touches && e.touches.length > 0) {
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
// Fallback for mouse events.
return { x: e.clientX, y: e.clientY };
}
function moveClone(x, y) {
if (!clone) return;
clone.style.left = `${x - offset.x}px`;
clone.style.top = `${y - offset.y}px`;
}
// --- 事件監聽 ---
startButton.addEventListener('click', () => {
startScreen.style.opacity = '0';
gameContainer.classList.remove('hidden');
setTimeout(() => {
startScreen.classList.add('hidden');
gameContainer.style.opacity = '1';
initLevel(currentLevel);
}, 500);
});
blockPalette.addEventListener('mousedown', onDragStart);
blockPalette.addEventListener('touchstart', onDragStart, { passive: false });
blockPalette.addEventListener('scroll', updateScrollButtonsVisibility);
window.addEventListener('resize', updateScrollButtonsVisibility);
scrollLeftButton.addEventListener('click', () => {
blockPalette.scrollBy({ left: -200, behavior: 'smooth' });
});
scrollRightButton.addEventListener('click', () => {
blockPalette.scrollBy({ left: 200, behavior: 'smooth' });
});
nextLevelButton.addEventListener('click', () => {
if (currentLevel < levels.length - 1) {
initLevel(currentLevel + 1);
blockPalette.style.pointerEvents = 'auto';
gridContainer.style.pointerEvents = 'auto';
} else {
// 顯示秘密畫面
gameContainer.style.opacity = '0';
secretView.classList.remove('hidden');
setTimeout(() => {
gameContainer.classList.add('hidden');
secretView.style.opacity = '1';
}, 500);
}
});
});
</script>
</body>
</html>