Spanish Word Fall - Learn Spanish | Arcabelab.ai
by HyperScout89613 lines20.1 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Spanish Word Fall - Learn Spanish | Arcabelab.ai</title>
<style>
* {
user-select: none;
-webkit-tap-highlight-color: transparent;
box-sizing: border-box;
}
body {
background: linear-gradient(145deg, #1a2a3a 0%, #0f1a24 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: system-ui, 'Segoe UI', 'Comic Neue', 'Roboto', sans-serif;
margin: 0;
padding: 16px;
}
.game-container {
background: rgba(0, 0, 0, 0.45);
border-radius: 56px;
padding: 18px;
box-shadow: 0 20px 35px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1);
backdrop-filter: blur(2px);
width: 100%;
max-width: 1000px;
}
canvas {
display: block;
margin: 0 auto;
border-radius: 32px;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);
cursor: pointer;
background: #fef9e8;
width: 100%;
height: auto;
touch-action: none;
}
.info-panel {
display: flex;
justify-content: space-between;
align-items: center;
background: #2c3e2fcc;
backdrop-filter: blur(4px);
padding: 12px 24px;
border-radius: 80px;
margin-bottom: 20px;
color: #f5e7d9;
text-shadow: 0 1px 2px rgba(0,0,0,0.4);
gap: 12px;
flex-wrap: wrap;
}
.score-box, .target-box, .timer-box {
background: #1e2a1c;
padding: 6px 18px;
border-radius: 60px;
font-weight: bold;
font-size: 1.2rem;
backdrop-filter: blur(2px);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.3), 0 2px 3px rgba(255,255,255,0.1);
font-family: monospace;
letter-spacing: 0.5px;
}
.target-box {
background: #b4422b;
color: #ffefc0;
font-size: 1.5rem;
font-weight: 800;
text-transform: capitalize;
}
.feedback {
background: #000000aa;
backdrop-filter: blur(8px);
padding: 10px 20px;
border-radius: 60px;
font-size: 1.1rem;
font-weight: bold;
text-align: center;
transition: all 0.2s ease;
margin-top: 12px;
color: #f9eec1;
}
button {
background: #ff914d;
border: none;
font-size: 1rem;
font-weight: bold;
padding: 8px 24px;
border-radius: 80px;
color: #2d2b1f;
cursor: pointer;
transition: transform 0.1s, background 0.2s;
box-shadow: 0 4px 0 #a23b0a;
font-family: inherit;
}
button:active {
transform: translateY(2px);
box-shadow: 0 1px 0 #a23b0a;
}
.status {
font-size: 0.85rem;
background: #00000088;
padding: 6px 14px;
border-radius: 40px;
text-align: center;
margin-top: 12px;
color: #e9f5db;
backdrop-filter: blur(4px);
}
@media (max-width: 650px) {
.info-panel {
justify-content: center;
}
.score-box, .target-box, .timer-box {
font-size: 1rem;
padding: 4px 14px;
}
.target-box {
font-size: 1.2rem;
}
.feedback {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<div>
<div class="game-container">
<div class="info-panel">
<div class="score-box">๐ฏ SCORE: <span id="scoreValue">0</span></div>
<div class="target-box">๐ MATCH: <span id="targetWord">hello</span></div>
<div class="timer-box">โฑ๏ธ TIME: <span id="timerValue">60</span>s</div>
<button id="restartButton">๐ RESTART</button>
</div>
<canvas id="gameCanvas" width="900" height="500" style="width:100%; height:auto; max-width:900px; aspect-ratio:900/500"></canvas>
<div id="feedbackMessage" class="feedback">โจ Tap the correct Spanish translation! โจ</div>
<div class="status">๐ก TIP: Click / Tap the FALLING word that matches the English target โ +10 | wrong = -5</div>
</div>
</div>
<script>
(function(){
// ========== SPANISH -> ENGLISH DICTIONARY (rich set) ==========
const wordPairs = [
{ spanish: "Hola", english: "hello" },
{ spanish: "Amigo", english: "friend" },
{ spanish: "Uno", english: "one" },
{ spanish: "Dos", english: "two" },
{ spanish: "Tres", english: "three" },
{ spanish: "Cuatro", english: "four" },
{ spanish: "Cinco", english: "five" },
{ spanish: "Adiรณs", english: "bye" },
{ spanish: "Gracias", english: "thanks" },
{ spanish: "Por favor", english: "please" },
{ spanish: "Rojo", english: "red" },
{ spanish: "Azul", english: "blue" },
{ spanish: "Verde", english: "green" },
{ spanish: "Perro", english: "dog" },
{ spanish: "Gato", english: "cat" },
{ spanish: "Casa", english: "house" },
{ spanish: "Feliz", english: "happy" },
{ spanish: "Triste", english: "sad" }
];
// ---------- DOM elements ----------
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreValue');
const targetSpan = document.getElementById('targetWord');
const timerSpan = document.getElementById('timerValue');
const feedbackDiv = document.getElementById('feedbackMessage');
const restartBtn = document.getElementById('restartButton');
// ---------- GAME VARIABLES ----------
let score = 0;
let currentTargetEnglish = "hello";
let gameActive = true;
let timeLeft = 60;
let timerInterval = null;
let wordGeneratorInterval = null;
let activeWords = []; // each word obj: {text, x, y, speed, correct, width, height}
let animationId = null;
// Dynamic difficulty base values
let baseSpeed = 2.2; // pixels per frame (60fps target)
// Helper: measure text width reliably
function measureTextWidth(text, fontSize = 26) {
ctx.font = `bold ${fontSize}px "Segoe UI", system-ui, "Comic Neue", sans-serif`;
return ctx.measureText(text).width + 18; // generous click padding
}
// --- Update UI and difficulty dynamically based on score ---
function refreshDifficultySettings() {
// speed increases with score: floor(score/80) * 0.5
let extraSpeed = Math.min(4.0, Math.floor(score / 80) * 0.55);
let currentGameSpeed = baseSpeed + extraSpeed;
currentGameSpeed = Math.min(7.2, currentGameSpeed);
// speed is applied per word on spawn, but we also store base for updates.
// we'll handle in spawnWord individually.
return currentGameSpeed;
}
// adjust spawn interval based on score (more points = faster spawning)
function getDynamicSpawnInterval() {
let interval = Math.max(700, 2100 - Math.floor(score / 28) * 45);
return Math.min(interval, 2100);
}
function restartSpawnGenerator() {
if (wordGeneratorInterval) clearInterval(wordGeneratorInterval);
const newInterval = getDynamicSpawnInterval();
wordGeneratorInterval = setInterval(() => {
if (gameActive) spawnWord();
}, newInterval);
}
// play subtle beep (Web Audio - works in arcabelab.ai if user interaction exists)
function playSound(type) {
try {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return;
const audioCtx = new AudioContextClass();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'sine';
oscillator.frequency.value = type === 'correct' ? 880 : 480;
gainNode.gain.value = 0.18;
oscillator.start();
gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.28);
oscillator.stop(audioCtx.currentTime + 0.25);
// resume if suspended (user gesture already happened)
if (audioCtx.state === 'suspended') audioCtx.resume();
} catch(e) { /* silent fallback */ }
}
function showFeedback(message, isCorrect = true) {
feedbackDiv.style.color = isCorrect ? '#d4ffb0' : '#ffc2a1';
feedbackDiv.style.textShadow = '0 0 2px black';
feedbackDiv.innerHTML = message;
setTimeout(() => {
if (feedbackDiv.innerHTML === message && gameActive) {
feedbackDiv.style.color = '#f9eec1';
feedbackDiv.innerHTML = 'โจ Tap the correct falling word! โจ';
} else if (!gameActive && feedbackDiv.innerHTML === message) {
// keep game over message style
}
}, 1300);
}
function updateScoreUI() {
scoreSpan.textContent = score;
}
// pick fresh random target (English word)
function pickNewTarget() {
let newIndex;
do {
newIndex = Math.floor(Math.random() * wordPairs.length);
} while (wordPairs.length > 1 && wordPairs[newIndex].english === currentTargetEnglish);
currentTargetEnglish = wordPairs[newIndex].english;
targetSpan.textContent = currentTargetEnglish;
}
// spawn a new falling Spanish word (with dynamic speed)
function spawnWord() {
if (!gameActive) return;
const randomPair = wordPairs[Math.floor(Math.random() * wordPairs.length)];
const spanishWord = randomPair.spanish;
const isCorrectWord = (randomPair.english === currentTargetEnglish);
// dynamic speed based on score (global difficulty)
let calculatedSpeed = baseSpeed + Math.min(4.2, Math.floor(score / 70) * 0.5);
calculatedSpeed = Math.min(7.0, calculatedSpeed);
const individualSpeed = calculatedSpeed + (Math.random() * 1.1 - 0.3);
const finalSpeed = Math.max(1.9, individualSpeed);
const fontSize = 26;
ctx.font = `bold ${fontSize}px "Segoe UI", system-ui, "Comic Neue", sans-serif`;
const textWidth = ctx.measureText(spanishWord).width + 18;
const maxX = canvas.width - textWidth - 12;
const minX = 12;
const xPos = minX + Math.random() * Math.max(0, (maxX - minX));
activeWords.push({
text: spanishWord,
x: xPos,
y: -28,
speed: finalSpeed,
correct: isCorrectWord,
width: textWidth,
height: 38
});
}
// initial warm spawns
function initialWordWave() {
for(let i=0;i<3;i++) {
setTimeout(() => { if(gameActive) spawnWord(); }, i * 220);
}
}
// DRAW everything (canvas updated) with polished UI
function draw() {
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// light paper texture bg lines
ctx.strokeStyle = "#e2d5b6";
ctx.lineWidth = 0.6;
for (let i = 0; i < canvas.width; i += 45) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i % canvas.height);
ctx.lineTo(canvas.width, i % canvas.height);
ctx.stroke();
}
for (let w of activeWords) {
const isTarget = w.correct;
const wordX = w.x;
const wordY = w.y;
const wWidth = w.width;
const wHeight = 36;
ctx.shadowBlur = 5;
ctx.shadowColor = "rgba(0,0,0,0.3)";
// card-like rounded background
ctx.beginPath();
ctx.roundRect(wordX - 6, wordY - 5, wWidth + 12, wHeight + 6, 24);
ctx.fillStyle = isTarget ? "#fdebb3dd" : "#fffaecd9";
ctx.fill();
ctx.shadowBlur = 2;
ctx.font = `bold 26px "Segoe UI", system-ui, "Comic Neue", sans-serif`;
ctx.fillStyle = isTarget ? "#b64926" : "#2f4f2f";
ctx.fillText(w.text, wordX, wordY + 24);
if (isTarget) {
// subtle glow as indicator for correct match
ctx.shadowBlur = 10;
ctx.fillStyle = "#e67e22";
ctx.fillText(w.text, wordX-1, wordY+23);
ctx.fillStyle = "#ffb347";
ctx.fillText(w.text, wordX+1, wordY+25);
ctx.fillStyle = "#b64926";
ctx.fillText(w.text, wordX, wordY+24);
}
}
ctx.shadowBlur = 0;
// if gameActive is false, show semi transparent overlay & end text inside draw as fallback
if (!gameActive) {
ctx.font = "bold 30px system-ui, 'Segoe UI'";
ctx.fillStyle = "#2b2b1fcc";
ctx.shadowBlur = 0;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#fdebb3";
ctx.font = "bold 32px system-ui";
ctx.shadowColor = "black";
ctx.fillText("โฐ GAME OVER", canvas.width/2-110, canvas.height/2-30);
ctx.font = "26px system-ui";
ctx.fillStyle = "#fff0c0";
ctx.fillText(`โญ FINAL SCORE: ${score} โญ`, canvas.width/2-140, canvas.height/2+30);
ctx.font = "20px system-ui";
ctx.fillStyle = "#ffd58c";
ctx.fillText("๐ Press RESTART to play again", canvas.width/2-135, canvas.height/2+90);
}
}
function updatePositions() {
if (!gameActive) return;
for (let i = 0; i < activeWords.length; i++) {
const w = activeWords[i];
w.y += w.speed;
if (w.y > canvas.height + 100 || w.y + 50 < -50) {
activeWords.splice(i,1);
i--;
}
}
}
function animate() {
if (!gameActive) {
draw();
return;
}
updatePositions();
draw();
animationId = requestAnimationFrame(animate);
}
// ========== CORE GAMEPLAY: HANDLE CLICK/TAP ON FALLING WORDS ==========
function handleCanvasInteraction(e) {
if (!gameActive) return false;
let clientX, clientY;
if (e.touches) {
e.preventDefault();
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const canvasX = (clientX - rect.left) * scaleX;
const canvasY = (clientY - rect.top) * scaleY;
// check from topmost (last in array) to first for natural feel
for (let i = activeWords.length-1; i >= 0; i--) {
const word = activeWords[i];
const leftBound = word.x - 12;
const rightBound = word.x + word.width + 10;
const topBound = word.y - 8;
const bottomBound = word.y + 38;
if (canvasX >= leftBound && canvasX <= rightBound && canvasY >= topBound && canvasY <= bottomBound) {
if (word.correct) {
// CORRECT MATCH!
score += 10;
updateScoreUI();
playSound('correct');
showFeedback(`๐ CORRECT! +10 pts (${word.text} = ${currentTargetEnglish})`, true);
// remove matched word
activeWords.splice(i,1);
// change target to new random Spanish-English word
pickNewTarget();
// update difficulty: speed & spawn interval adapter
restartSpawnGenerator();
} else {
// wrong click penalty
score = Math.max(0, score - 5);
updateScoreUI();
playSound('wrong');
showFeedback(`โ WRONG! -5 pts. โ${word.text}โ โ โ${currentTargetEnglish}โ`, false);
activeWords.splice(i,1);
// target remains same, but difficulty may still adapt to lower score
restartSpawnGenerator();
}
return true;
}
}
return false;
}
// ----- TIMER MANAGEMENT -----
function startTimer() {
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (!gameActive) return;
if (timeLeft <= 1) {
timeLeft = 0;
timerSpan.textContent = "0";
endGame();
} else {
timeLeft--;
timerSpan.textContent = timeLeft;
}
}, 1000);
}
function endGame() {
if (!gameActive) return;
gameActive = false;
if (wordGeneratorInterval) clearInterval(wordGeneratorInterval);
if (timerInterval) clearInterval(timerInterval);
if (animationId) cancelAnimationFrame(animationId);
draw(); // show final game over overlay
showFeedback(`๐ GAME COMPLETE! Final Score: ${score} ๐`, true);
feedbackDiv.style.color = "#ffd966";
}
// ------ RESTART FULL GAME with clean state ------
function restartGame() {
// kill existing loops
if (wordGeneratorInterval) clearInterval(wordGeneratorInterval);
if (timerInterval) clearInterval(timerInterval);
if (animationId) cancelAnimationFrame(animationId);
// reset state
gameActive = true;
score = 0;
timeLeft = 60;
activeWords = [];
baseSpeed = 2.2;
// UI reset
updateScoreUI();
timerSpan.textContent = "60";
// pick random initial target (avoid always hello)
const initPair = wordPairs[Math.floor(Math.random() * wordPairs.length)];
currentTargetEnglish = initPair.english;
targetSpan.textContent = currentTargetEnglish;
feedbackDiv.style.color = "#f9eec1";
feedbackDiv.innerHTML = "โจ New round! Find the matching Spanish word โจ";
// restart dynamic generator
restartSpawnGenerator();
startTimer();
// start animation again
animationId = requestAnimationFrame(animate);
// create initial falling words to populate canvas
setTimeout(() => {
if (gameActive) {
for(let s=0; s<3; s++) spawnWord();
}
}, 80);
}
// ----- canvas round rect helper (smooth) -----
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
this.moveTo(x+r, y);
this.lineTo(x+w-r, y);
this.quadraticCurveTo(x+w, y, x+w, y+r);
this.lineTo(x+w, y+h-r);
this.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
this.lineTo(x+r, y+h);
this.quadraticCurveTo(x, y+h, x, y+h-r);
this.lineTo(x, y+r);
this.quadraticCurveTo(x, y, x+r, y);
return this;
};
}
// ---- set up canvas event listeners with full compatibility for arcabelab.ai (both mouse + touch) ----
function attachCanvasEvents() {
canvas.removeEventListener('click', onClickHandler);
canvas.removeEventListener('touchstart', onTouchHandler);
canvas.addEventListener('click', onClickHandler);
canvas.addEventListener('touchstart', onTouchHandler, { passive: false });
}
function onClickHandler(e) {
if (!gameActive) return;
e.preventDefault();
handleCanvasInteraction(e);
}
function onTouchHandler(e) {
if (!gameActive) return;
e.preventDefault();
handleCanvasInteraction(e);
}
// additional restart button behavior
restartBtn.addEventListener('click', (e) => {
e.preventDefault();
restartGame();
});
// window resize handling โ maintain canvas ratio / redraw on resize ? just call draw refresh
window.addEventListener('resize', () => {
if (gameActive) draw();
else draw();
});
// optional: preload AudioContext on first user interaction (improves sound)
function primeAudioOnFirstTap() {
const resumeAudio = () => {
try {
const actx = new (window.AudioContext || window.webkitAudioContext)();
if (actx.state === 'suspended') actx.resume();
} catch(e) {}
document.removeEventListener('click', resumeAudio);
canvas.removeEventListener('touchstart', resumeAudio);
};
document.addEventListener('click', resumeAudio);
canvas.addEventListener('touchstart', resumeAudio);
}
primeAudioOnFirstTap();
// ---- final initialization ----
function initGame() {
attachCanvasEvents();
restartGame(); // kickstarts everything
}
initGame();
})();
</script>
</body>
</html>Game Source: Spanish Word Fall - Learn Spanish | Arcabelab.ai
Creator: HyperScout89
Libraries: none
Complexity: complex (613 lines, 20.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: spanish-word-fall-learn-spanish-hyperscout89" to link back to the original. Then publish at arcadelab.ai/publish.