Initial AI-generated site
This commit is contained in:
@@ -0,0 +1,740 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Game State
|
||||
const state = {
|
||||
isPlaying: false,
|
||||
isPaused: false,
|
||||
score: 0,
|
||||
distance: 0,
|
||||
gifts: 0,
|
||||
boost: 0,
|
||||
maxBoost: 100,
|
||||
isBoostActive: false,
|
||||
speed: 3,
|
||||
baseSpeed: 3,
|
||||
boostSpeed: 7,
|
||||
playerY: 0,
|
||||
playerVelocity: 0,
|
||||
obstacles: [],
|
||||
giftItems: [],
|
||||
particles: [],
|
||||
highscores: [],
|
||||
lastBoostAnnounce: false
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const startBtn = document.getElementById('startGame');
|
||||
const overlayBtn = document.getElementById('overlayBtn');
|
||||
const gameOverlay = document.getElementById('gameOverlay');
|
||||
const overlayTitle = document.getElementById('overlayTitle');
|
||||
const overlayMessage = document.getElementById('overlayMessage');
|
||||
const finalStats = document.getElementById('finalStats');
|
||||
const scoreDisplay = document.getElementById('scoreDisplay');
|
||||
const distanceDisplay = document.getElementById('distanceDisplay');
|
||||
const giftsDisplay = document.getElementById('giftsDisplay');
|
||||
const boostBar = document.getElementById('boostBar');
|
||||
const boostValue = document.getElementById('boostValue');
|
||||
const boostBarContainer = boostBar.parentElement;
|
||||
const instructionsModal = document.getElementById('instructionsModal');
|
||||
const closeInstructions = document.getElementById('closeInstructions');
|
||||
const highscoresBody = document.getElementById('highscoresBody');
|
||||
const gameStatus = document.getElementById('gameStatus');
|
||||
|
||||
// Check for reduced motion preference
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// Focus trap for modal
|
||||
let focusableElements = [];
|
||||
let firstFocusable = null;
|
||||
let lastFocusable = null;
|
||||
|
||||
function setupFocusTrap(modal) {
|
||||
focusableElements = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
firstFocusable = focusableElements[0];
|
||||
lastFocusable = focusableElements[focusableElements.length - 1];
|
||||
}
|
||||
|
||||
function handleFocusTrap(e) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === firstFocusable) {
|
||||
e.preventDefault();
|
||||
lastFocusable.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === lastFocusable) {
|
||||
e.preventDefault();
|
||||
firstFocusable.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Canvas setup
|
||||
function resizeCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
state.playerY = rect.height / 2;
|
||||
}
|
||||
|
||||
function getCanvasSize() {
|
||||
return {
|
||||
width: canvas.getBoundingClientRect().width,
|
||||
height: canvas.getBoundingClientRect().height
|
||||
};
|
||||
}
|
||||
|
||||
// Boat drawing
|
||||
const boatSprite = {
|
||||
width: 48,
|
||||
height: 32,
|
||||
draw(ctx, x, y, isBoost) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
// Boat body
|
||||
ctx.fillStyle = isBoost ? '#ff6b6b' : '#8b4513';
|
||||
ctx.fillRect(0, 8, 40, 16);
|
||||
ctx.fillRect(-4, 12, 8, 8);
|
||||
|
||||
// Boat front (pointed)
|
||||
ctx.fillStyle = isBoost ? '#ff6b6b' : '#a0522d';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(40, 8);
|
||||
ctx.lineTo(48, 16);
|
||||
ctx.lineTo(40, 24);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Sail
|
||||
ctx.fillStyle = isBoost ? '#ffd60a' : '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, 8);
|
||||
ctx.lineTo(20, -12);
|
||||
ctx.lineTo(35, 8);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Mast
|
||||
ctx.fillStyle = '#654321';
|
||||
ctx.fillRect(18, -12, 4, 20);
|
||||
|
||||
// Flag
|
||||
ctx.fillStyle = '#f72585';
|
||||
ctx.fillRect(18, -16, 8, 6);
|
||||
|
||||
// Wake effect when boosting
|
||||
if (isBoost && !prefersReducedMotion) {
|
||||
ctx.fillStyle = 'rgba(0, 245, 212, 0.6)';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const yOffset = Math.sin(Date.now() / 100 + i) * 2;
|
||||
ctx.fillRect(-8 - i * 8, 14 + yOffset, 4, 4);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
// Gift item
|
||||
function drawGift(ctx, x, y, type) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
const bounce = prefersReducedMotion ? 0 : Math.sin(Date.now() / 200 + x) * 3;
|
||||
ctx.translate(0, bounce);
|
||||
|
||||
if (type === 'boost') {
|
||||
// Lightning bolt
|
||||
ctx.fillStyle = '#ffd60a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -12);
|
||||
ctx.lineTo(8, -4);
|
||||
ctx.lineTo(2, -4);
|
||||
ctx.lineTo(8, 12);
|
||||
ctx.lineTo(-4, 0);
|
||||
ctx.lineTo(2, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else {
|
||||
// Gift box
|
||||
ctx.fillStyle = '#f72585';
|
||||
ctx.fillRect(-8, -4, 16, 12);
|
||||
ctx.fillStyle = '#ffd60a';
|
||||
ctx.fillRect(-2, -4, 4, 12);
|
||||
ctx.fillRect(-8, 0, 16, 4);
|
||||
// Bow
|
||||
ctx.fillRect(-6, -10, 4, 6);
|
||||
ctx.fillRect(2, -10, 4, 6);
|
||||
ctx.fillRect(-2, -8, 4, 4);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Obstacle (rock)
|
||||
function drawObstacle(ctx, x, y, size) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
ctx.fillStyle = '#4a4a4a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-size/2, size/2);
|
||||
ctx.lineTo(-size/3, -size/3);
|
||||
ctx.lineTo(0, -size/2);
|
||||
ctx.lineTo(size/3, -size/4);
|
||||
ctx.lineTo(size/2, size/2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Highlights
|
||||
ctx.fillStyle = '#5a5a5a';
|
||||
ctx.fillRect(-size/4, -size/4, size/4, size/4);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Water background
|
||||
function drawWater(ctx, canvasSize) {
|
||||
const { width, height } = canvasSize;
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, '#0d1b2a');
|
||||
gradient.addColorStop(0.5, '#1b263b');
|
||||
gradient.addColorStop(1, '#0d1b2a');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Animated waves (skip if reduced motion or paused)
|
||||
if (!prefersReducedMotion && state.isPlaying && !state.isPaused) {
|
||||
ctx.strokeStyle = 'rgba(0, 245, 212, 0.1)';
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
ctx.beginPath();
|
||||
for (let x = 0; x < width; x += 20) {
|
||||
const waveY = (height / 5) * (i + 1) + Math.sin((x + state.distance * 2 + i * 50) / 50) * 8;
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, waveY);
|
||||
} else {
|
||||
ctx.lineTo(x, waveY);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Particle system
|
||||
function createParticle(x, y, color) {
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
state.particles.push({
|
||||
x,
|
||||
y,
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: (Math.random() - 0.5) * 4,
|
||||
life: 1,
|
||||
color,
|
||||
size: Math.random() * 4 + 2
|
||||
});
|
||||
}
|
||||
|
||||
function updateParticles() {
|
||||
for (let i = state.particles.length - 1; i >= 0; i--) {
|
||||
const p = state.particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life -= 0.03;
|
||||
if (p.life <= 0) {
|
||||
state.particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawParticles(ctx) {
|
||||
state.particles.forEach(p => {
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.globalAlpha = p.life;
|
||||
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Spawn obstacles and gifts
|
||||
function spawnObstacle(canvasSize) {
|
||||
const { width, height } = canvasSize;
|
||||
const maxY = height - 40;
|
||||
const minY = 40;
|
||||
|
||||
state.obstacles.push({
|
||||
x: width + 50,
|
||||
y: Math.random() * (maxY - minY) + minY,
|
||||
size: Math.random() * 20 + 30
|
||||
});
|
||||
}
|
||||
|
||||
function spawnGift(canvasSize) {
|
||||
const { width, height } = canvasSize;
|
||||
const maxY = height - 30;
|
||||
const minY = 30;
|
||||
|
||||
state.giftItems.push({
|
||||
x: width + 30,
|
||||
y: Math.random() * (maxY - minY) + minY,
|
||||
type: Math.random() < 0.3 ? 'boost' : 'gift'
|
||||
});
|
||||
}
|
||||
|
||||
// Collision detection
|
||||
function checkCollision(ax, ay, aw, ah, bx, by, bw, bh) {
|
||||
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
||||
}
|
||||
|
||||
// Update game
|
||||
let lastSpawnTime = 0;
|
||||
let lastGiftTime = 0;
|
||||
|
||||
function update(timestamp) {
|
||||
if (!state.isPlaying || state.isPaused) return;
|
||||
|
||||
const canvasSize = getCanvasSize();
|
||||
const { height } = canvasSize;
|
||||
|
||||
// Increase difficulty over time
|
||||
state.speed = state.baseSpeed + state.distance / 1200;
|
||||
const currentSpeed = state.isBoostActive ? state.boostSpeed : state.speed;
|
||||
|
||||
// Update distance
|
||||
state.distance += currentSpeed * 0.5;
|
||||
|
||||
// Player physics
|
||||
state.playerY += state.playerVelocity;
|
||||
state.playerVelocity *= 0.9;
|
||||
state.playerY = Math.max(24, Math.min(height - 24, state.playerY));
|
||||
|
||||
// Spawn obstacles
|
||||
const spawnInterval = Math.max(800, 1500 - state.distance * 0.5);
|
||||
if (timestamp - lastSpawnTime > spawnInterval) {
|
||||
spawnObstacle(canvasSize);
|
||||
lastSpawnTime = timestamp;
|
||||
}
|
||||
|
||||
// Spawn gifts
|
||||
if (timestamp - lastGiftTime > 2200) {
|
||||
spawnGift(canvasSize);
|
||||
lastGiftTime = timestamp;
|
||||
}
|
||||
|
||||
// Update obstacles
|
||||
for (let i = state.obstacles.length - 1; i >= 0; i--) {
|
||||
state.obstacles[i].x -= currentSpeed;
|
||||
|
||||
// Collision with player
|
||||
if (checkCollision(
|
||||
60, state.playerY - 16, 36, 28,
|
||||
state.obstacles[i].x - state.obstacles[i].size/2,
|
||||
state.obstacles[i].y - state.obstacles[i].size/2,
|
||||
state.obstacles[i].size,
|
||||
state.obstacles[i].size
|
||||
)) {
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.obstacles[i].x < -50) {
|
||||
state.obstacles.splice(i, 1);
|
||||
state.score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Update gifts
|
||||
for (let i = state.giftItems.length - 1; i >= 0; i--) {
|
||||
state.giftItems[i].x -= currentSpeed;
|
||||
|
||||
// Collision with player
|
||||
if (checkCollision(
|
||||
60, state.playerY - 16, 36, 28,
|
||||
state.giftItems[i].x - 12,
|
||||
state.giftItems[i].y - 12,
|
||||
24, 24
|
||||
)) {
|
||||
// Collect gift
|
||||
const gift = state.giftItems[i];
|
||||
if (gift.type === 'boost') {
|
||||
state.boost = Math.min(state.boost + 35, state.maxBoost);
|
||||
state.score += 25;
|
||||
} else {
|
||||
state.gifts++;
|
||||
state.boost = Math.min(state.boost + 15, state.maxBoost);
|
||||
state.score += 50;
|
||||
}
|
||||
|
||||
// Particles
|
||||
for (let j = 0; j < 8; j++) {
|
||||
createParticle(gift.x, gift.y, gift.type === 'boost' ? '#ffd60a' : '#f72585');
|
||||
}
|
||||
|
||||
state.giftItems.splice(i, 1);
|
||||
updateHUD();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state.giftItems[i].x < -30) {
|
||||
state.giftItems.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Boost drain
|
||||
if (state.isBoostActive) {
|
||||
state.boost -= 0.6;
|
||||
if (state.boost <= 0) {
|
||||
state.boost = 0;
|
||||
state.isBoostActive = false;
|
||||
}
|
||||
// Boost particles
|
||||
createParticle(60, state.playerY, 'rgba(0, 245, 212, 0.8)');
|
||||
}
|
||||
|
||||
// Announce boost ready (only once)
|
||||
const boostFull = state.boost >= state.maxBoost;
|
||||
if (boostFull && !state.lastBoostAnnounce) {
|
||||
gameStatus.textContent = 'בוסט מוכן! לחצו רווח להפעלה';
|
||||
state.lastBoostAnnounce = true;
|
||||
} else if (!boostFull && state.lastBoostAnnounce) {
|
||||
state.lastBoostAnnounce = false;
|
||||
gameStatus.textContent = '';
|
||||
}
|
||||
|
||||
updateParticles();
|
||||
updateHUD();
|
||||
}
|
||||
|
||||
function updateHUD() {
|
||||
scoreDisplay.textContent = Math.floor(state.score);
|
||||
distanceDisplay.textContent = Math.floor(state.distance) + ' מ׳';
|
||||
giftsDisplay.textContent = state.gifts;
|
||||
|
||||
const boostPercent = Math.floor(state.boost / state.maxBoost * 100);
|
||||
boostBar.style.width = boostPercent + '%';
|
||||
boostValue.textContent = boostPercent + '%';
|
||||
boostBarContainer.setAttribute('aria-valuenow', boostPercent);
|
||||
}
|
||||
|
||||
// Render game
|
||||
function render() {
|
||||
const canvasSize = getCanvasSize();
|
||||
const { width, height } = canvasSize;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
drawWater(ctx, canvasSize);
|
||||
|
||||
// Draw obstacles
|
||||
state.obstacles.forEach(obs => {
|
||||
drawObstacle(ctx, obs.x, obs.y, obs.size);
|
||||
});
|
||||
|
||||
// Draw gifts
|
||||
state.giftItems.forEach(gift => {
|
||||
drawGift(ctx, gift.x, gift.y, gift.type);
|
||||
});
|
||||
|
||||
// Draw player
|
||||
boatSprite.draw(ctx, 60, state.playerY, state.isBoostActive);
|
||||
|
||||
// Draw particles
|
||||
drawParticles(ctx);
|
||||
|
||||
// Boost ready indicator
|
||||
if (state.boost >= state.maxBoost && state.isPlaying) {
|
||||
ctx.font = 'bold 14px Heebo, sans-serif';
|
||||
ctx.fillStyle = '#ffd60a';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText('⚡ בוסט מוכן! לחצו רווח ⚡', width / 2, 12);
|
||||
}
|
||||
}
|
||||
|
||||
// Game loop
|
||||
let animationId;
|
||||
function gameLoop(timestamp) {
|
||||
update(timestamp);
|
||||
render();
|
||||
if (state.isPlaying) {
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
// Start game
|
||||
function startGame() {
|
||||
const canvasSize = getCanvasSize();
|
||||
|
||||
state.isPlaying = true;
|
||||
state.isPaused = false;
|
||||
state.score = 0;
|
||||
state.distance = 0;
|
||||
state.gifts = 0;
|
||||
state.boost = 0;
|
||||
state.isBoostActive = false;
|
||||
state.speed = state.baseSpeed;
|
||||
state.playerY = canvasSize.height / 2;
|
||||
state.playerVelocity = 0;
|
||||
state.obstacles = [];
|
||||
state.giftItems = [];
|
||||
state.particles = [];
|
||||
state.lastBoostAnnounce = false;
|
||||
|
||||
gameOverlay.classList.add('hidden');
|
||||
gameStatus.textContent = 'המשחק התחיל!';
|
||||
updateHUD();
|
||||
|
||||
lastSpawnTime = performance.now();
|
||||
lastGiftTime = performance.now();
|
||||
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Game over
|
||||
function gameOver() {
|
||||
state.isPlaying = false;
|
||||
cancelAnimationFrame(animationId);
|
||||
|
||||
// Save highscore
|
||||
const entry = {
|
||||
score: Math.floor(state.score),
|
||||
gifts: state.gifts,
|
||||
distance: Math.floor(state.distance)
|
||||
};
|
||||
|
||||
state.highscores.push(entry);
|
||||
state.highscores.sort((a, b) => b.score - a.score);
|
||||
state.highscores = state.highscores.slice(0, 5);
|
||||
|
||||
try {
|
||||
localStorage.setItem('boatRaceHighscores', JSON.stringify(state.highscores));
|
||||
} catch (e) {
|
||||
// Storage might be unavailable
|
||||
}
|
||||
|
||||
// Show overlay
|
||||
overlayTitle.textContent = 'המשחק נגמר!';
|
||||
overlayMessage.textContent = 'כל הכבוד! הגעתם למרחק ' + Math.floor(state.distance) + ' מטר';
|
||||
finalStats.hidden = false;
|
||||
document.getElementById('finalScore').textContent = Math.floor(state.score);
|
||||
document.getElementById('finalGifts').textContent = state.gifts;
|
||||
document.getElementById('finalDistance').textContent = Math.floor(state.distance) + ' מ׳';
|
||||
overlayBtn.textContent = 'שחק שוב';
|
||||
gameOverlay.classList.remove('hidden');
|
||||
overlayBtn.focus();
|
||||
|
||||
gameStatus.textContent = 'המשחק נגמר. ניקוד סופי: ' + Math.floor(state.score);
|
||||
|
||||
renderHighscores();
|
||||
}
|
||||
|
||||
// Render highscores
|
||||
function renderHighscores() {
|
||||
if (state.highscores.length === 0) {
|
||||
highscoresBody.innerHTML = '<div class="table-row table-row--empty" role="row"><span role="cell">עדיין אין שיאים — היו הראשונים!</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
highscoresBody.innerHTML = state.highscores.map((entry, i) => `
|
||||
<div class="table-row" role="row">
|
||||
<span role="cell">#${i + 1}</span>
|
||||
<span role="cell">${entry.score}</span>
|
||||
<span role="cell">${entry.gifts}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Input handling
|
||||
function handleKeyDown(e) {
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
if (!state.isPlaying) {
|
||||
startGame();
|
||||
} else if (state.boost >= state.maxBoost && !state.isBoostActive) {
|
||||
state.isBoostActive = true;
|
||||
gameStatus.textContent = 'בוסט פעיל!';
|
||||
}
|
||||
}
|
||||
|
||||
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
|
||||
e.preventDefault();
|
||||
state.playerVelocity = -7;
|
||||
}
|
||||
|
||||
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
|
||||
e.preventDefault();
|
||||
state.playerVelocity = 7;
|
||||
}
|
||||
|
||||
if (e.code === 'KeyP' && state.isPlaying) {
|
||||
state.isPaused = !state.isPaused;
|
||||
gameStatus.textContent = state.isPaused ? 'המשחק מושהה' : 'המשחק ממשיך';
|
||||
if (!state.isPaused) {
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.code === 'Escape') {
|
||||
if (!instructionsModal.hidden) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Touch handling
|
||||
let touchStartY = 0;
|
||||
let lastTap = 0;
|
||||
|
||||
function handleTouchStart(e) {
|
||||
if (!state.isPlaying) {
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
|
||||
touchStartY = e.touches[0].clientY;
|
||||
|
||||
// Double tap for boost
|
||||
const now = Date.now();
|
||||
if (now - lastTap < 300 && state.boost >= state.maxBoost && !state.isBoostActive) {
|
||||
state.isBoostActive = true;
|
||||
gameStatus.textContent = 'בוסט פעיל!';
|
||||
}
|
||||
lastTap = now;
|
||||
}
|
||||
|
||||
function handleTouchMove(e) {
|
||||
if (!state.isPlaying) return;
|
||||
e.preventDefault();
|
||||
|
||||
const touchY = e.touches[0].clientY;
|
||||
const diff = touchStartY - touchY;
|
||||
state.playerVelocity = -diff * 0.12;
|
||||
touchStartY = touchY;
|
||||
}
|
||||
|
||||
// Modal handling
|
||||
let previousActiveElement = null;
|
||||
|
||||
function openModal() {
|
||||
previousActiveElement = document.activeElement;
|
||||
instructionsModal.hidden = false;
|
||||
setupFocusTrap(instructionsModal);
|
||||
closeInstructions.focus();
|
||||
document.body.style.overflow = 'hidden';
|
||||
instructionsModal.addEventListener('keydown', handleFocusTrap);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
instructionsModal.hidden = true;
|
||||
document.body.style.overflow = '';
|
||||
instructionsModal.removeEventListener('keydown', handleFocusTrap);
|
||||
if (previousActiveElement) {
|
||||
previousActiveElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
resizeCanvas();
|
||||
|
||||
// Load highscores
|
||||
try {
|
||||
const saved = localStorage.getItem('boatRaceHighscores');
|
||||
if (saved) {
|
||||
state.highscores = JSON.parse(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
// Storage might be unavailable
|
||||
}
|
||||
|
||||
renderHighscores();
|
||||
|
||||
// Event listeners
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
canvas.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
|
||||
startBtn.addEventListener('click', startGame);
|
||||
overlayBtn.addEventListener('click', startGame);
|
||||
|
||||
// Navigation
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.dataset.action;
|
||||
if (action === 'instructions') {
|
||||
openModal();
|
||||
} else if (action === 'highscores') {
|
||||
document.getElementById('highscoresSection').scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
closeInstructions.addEventListener('click', closeModal);
|
||||
|
||||
instructionsModal.addEventListener('click', (e) => {
|
||||
if (e.target === instructionsModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll reveal animations (skip if reduced motion)
|
||||
if (!prefersReducedMotion) {
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -40px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('revealed');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('.feature-card, .section-title').forEach(el => {
|
||||
el.classList.add('reveal-on-scroll');
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Add reveal styles dynamically
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.reveal-on-scroll {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
.reveal-on-scroll.revealed {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
render();
|
||||
}
|
||||
|
||||
// Start
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user