Initial AI-generated site

This commit is contained in:
2026-07-30 18:07:11 +00:00
parent 9c4d7b8890
commit 730e6467d0
3 changed files with 1728 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
<!doctype html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>משחק מירוץ סירות בנמל | משחק דפדפן חינמי</title>
<meta name="description" content="משחק מירוץ סירות מרהיב בדפדפן! אספו מתנות, קבלו בוסטים והגיעו הכי רחוק שאפשר. חינם, בלי התקנה.">
<meta property="og:title" content="משחק מירוץ סירות בנמל">
<meta property="og:description" content="מירוץ סירות עם מתנות ובוסטים!">
<meta property="og:type" content="website">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⛵</text></svg>">
<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=Heebo:wght@400;700;900&family=Rubik+Mono+One&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="#main-content" class="skip-link">דלג לתוכן הראשי</a>
<div class="scanlines" aria-hidden="true"></div>
<header class="site-header">
<div class="header-content">
<div class="logo">
<span class="logo-icon" aria-hidden="true"></span>
<span class="logo-text">מירוץ סירות</span>
</div>
<nav class="main-nav" aria-label="ניווט ראשי">
<button class="nav-btn" data-action="instructions">הוראות</button>
<button class="nav-btn" data-action="highscores">שיאים</button>
</nav>
</div>
</header>
<main id="main-content">
<section class="hero" aria-labelledby="hero-title">
<div class="hero-content">
<h1 id="hero-title" class="hero-title">מירוץ סירות בנמל</h1>
<p class="hero-subtitle">אספו מתנות · קבלו בוסטים · שברו שיאים!</p>
<button class="start-btn pixel-btn" id="startGame">
<span>התחל משחק</span>
</button>
</div>
<div class="hero-decoration" aria-hidden="true">
<div class="harbor-elements">
<div class="lighthouse"></div>
<div class="buoy buoy-1"></div>
<div class="buoy buoy-2"></div>
</div>
<div class="wave wave-1"></div>
<div class="wave wave-2"></div>
</div>
</section>
<section class="game-container" id="gameContainer" aria-label="אזור המשחק">
<div class="game-hud">
<div class="hud-item score">
<span class="hud-label">ניקוד</span>
<span class="hud-value" id="scoreDisplay">0</span>
</div>
<div class="hud-item distance">
<span class="hud-label">מרחק</span>
<span class="hud-value" id="distanceDisplay">0 מ׳</span>
</div>
<div class="hud-item boost">
<span class="hud-label" id="boostLabel">בוסט</span>
<div class="boost-bar" role="progressbar" aria-labelledby="boostLabel" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="boost-fill" id="boostBar"></div>
<span class="boost-value" id="boostValue">0%</span>
</div>
</div>
<div class="hud-item gifts">
<span class="hud-label">מתנות</span>
<span class="hud-value" id="giftsDisplay">0</span>
</div>
</div>
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true" id="gameStatus"></div>
<canvas id="gameCanvas" aria-label="משחק מירוץ סירות פעיל"></canvas>
<div class="game-overlay" id="gameOverlay" role="dialog" aria-labelledby="overlayTitle" aria-modal="true">
<div class="overlay-content">
<h2 id="overlayTitle">מוכנים למירוץ?</h2>
<p id="overlayMessage">לחצו על ״התחל״ או על רווח</p>
<div class="final-stats" id="finalStats" hidden>
<div class="stat-row">
<span>ניקוד סופי:</span>
<span id="finalScore">0</span>
</div>
<div class="stat-row">
<span>מתנות שנאספו:</span>
<span id="finalGifts">0</span>
</div>
<div class="stat-row">
<span>מרחק:</span>
<span id="finalDistance">0 מ׳</span>
</div>
</div>
<button class="pixel-btn" id="overlayBtn">התחל</button>
</div>
</div>
</section>
<section class="features" aria-labelledby="features-title">
<h2 id="features-title" class="section-title">איך משחקים?</h2>
<div class="features-grid">
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">????</div>
<h3>שליטה פשוטה</h3>
<p>חיצים למעלה ולמטה להזזת הסירה, או החלקה במובייל</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">????</div>
<h3>אספו מתנות</h3>
<p>מתנות צפות על המים — אספו לניקוד בונוס ולמילוי מד הבוסט</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true"></div>
<h3>בוסט מהירות</h3>
<p>כשהמד מלא, לחצו רווח או הקשה כפולה לזינוק קדימה!</p>
</article>
<article class="feature-card">
<div class="feature-icon" aria-hidden="true">????</div>
<h3>שברו שיאים</h3>
<p>התחרו בעצמכם ושפרו את השיא האישי שוב ושוב</p>
</article>
</div>
</section>
<section class="highscores-section" id="highscoresSection" aria-labelledby="highscores-title">
<h2 id="highscores-title" class="section-title">טבלת שיאים</h2>
<div class="highscores-table" role="table" aria-label="טבלת שיאים">
<div class="table-header" role="row">
<span role="columnheader">מקום</span>
<span role="columnheader">ניקוד</span>
<span role="columnheader">מתנות</span>
</div>
<div class="table-body" id="highscoresBody" role="rowgroup">
<div class="table-row table-row--empty" role="row">
<span role="cell">עדיין אין שיאים — היו הראשונים!</span>
</div>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="footer-content">
<div class="footer-logo" aria-hidden="true">⛵ מירוץ סירות בנמל</div>
<p class="footer-credits">נבנה באהבה לסגנון הרטרו</p>
</div>
</footer>
<div class="instructions-modal" id="instructionsModal" role="dialog" aria-labelledby="instructions-title" aria-modal="true" hidden>
<div class="modal-content">
<button class="modal-close" id="closeInstructions" aria-label="סגור חלון הוראות"></button>
<h2 id="instructions-title">הוראות המשחק</h2>
<div class="instructions-list">
<div class="instruction-item">
<span class="key">↑ ↓</span>
<span>הזזת הסירה למעלה ולמטה</span>
</div>
<div class="instruction-item">
<span class="key">רווח</span>
<span>הפעלת בוסט (כשהמד מלא)</span>
</div>
<div class="instruction-item">
<span class="key" lang="en">P</span>
<span>השהיית המשחק</span>
</div>
<div class="instruction-item">
<span class="key">????</span>
<span>במובייל: החלקה למעלה/למטה, הקשה כפולה לבוסט</span>
</div>
</div>
<h3>טיפים</h3>
<ul class="tips-list">
<li>אספו מתנות למילוי מד הבוסט</li>
<li>השתמשו בבוסט בזמן הנכון</li>
<li>הימנעו מסלעים ומכשולים</li>
<li>המהירות עולה עם הזמן!</li>
</ul>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
+740
View File
@@ -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();
}
})();
+800
View File
@@ -0,0 +1,800 @@
:root {
--color-bg: #0a0a12;
--color-water: #0d1b2a;
--color-water-light: #1b263b;
--color-primary: #00f5d4;
--color-secondary: #7b2cbf;
--color-accent: #f72585;
--color-gold: #ffd60a;
--color-text: #e8e8e8;
--color-text-muted: #b8b8b8;
--color-card-bg: #14142a;
--font-display: 'Rubik Mono One', 'Heebo', sans-serif;
--font-body: 'Heebo', sans-serif;
--space-xs: 0.5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 3rem;
--space-xl: 5rem;
--radius-sm: 4px;
--radius-md: 8px;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-body);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.7;
overflow-x: hidden;
min-height: 100vh;
}
/* Skip link */
.skip-link {
position: absolute;
inset-inline-start: 0;
top: 0;
padding: var(--space-sm) var(--space-md);
background: var(--color-primary);
color: var(--color-bg);
font-weight: 700;
text-decoration: none;
z-index: 1000;
transform: translateY(-100%);
transition: transform 0.2s ease;
}
.skip-link:focus {
transform: translateY(0);
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Scanlines - subtle CRT effect */
.scanlines {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.06) 2px,
rgba(0, 0, 0, 0.06) 4px
);
}
/* Header */
.site-header {
position: fixed;
top: 0;
inset-inline: 0;
z-index: 100;
padding: var(--space-sm) var(--space-md);
background: linear-gradient(to bottom, var(--color-bg) 60%, transparent);
}
.header-content {
max-width: 960px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-sm);
}
.logo {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.logo-icon {
font-size: 1.75rem;
}
.logo-text {
font-family: var(--font-display);
font-size: 1rem;
color: var(--color-primary);
text-shadow: 0 0 20px rgba(0, 245, 212, 0.5);
letter-spacing: -0.02em;
}
.main-nav {
display: flex;
gap: var(--space-xs);
}
.nav-btn {
font-family: var(--font-body);
font-weight: 700;
font-size: 0.875rem;
padding: var(--space-xs) var(--space-sm);
background: transparent;
border: 2px solid var(--color-primary);
border-radius: var(--radius-sm);
color: var(--color-primary);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.nav-btn:hover {
background: var(--color-primary);
color: var(--color-bg);
}
.nav-btn:focus-visible {
outline: 3px solid var(--color-gold);
outline-offset: 2px;
}
/* Hero */
.hero {
min-height: 100vh;
min-height: 100svh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: var(--space-xl) var(--space-md);
padding-block-start: calc(var(--space-xl) + 60px);
position: relative;
overflow: hidden;
}
.hero-content {
position: relative;
z-index: 2;
}
.hero-title {
font-family: var(--font-display);
font-size: clamp(1.75rem, 6vw, 3rem);
color: var(--color-primary);
text-shadow:
0 0 30px rgba(0, 245, 212, 0.6),
0 0 60px rgba(0, 245, 212, 0.3);
margin-block-end: var(--space-sm);
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: clamp(1rem, 2.5vw, 1.25rem);
color: var(--color-text-muted);
margin-block-end: var(--space-lg);
max-width: 400px;
margin-inline: auto;
}
.pixel-btn {
font-family: var(--font-display);
font-size: clamp(0.875rem, 2vw, 1rem);
padding: var(--space-sm) var(--space-md);
background: linear-gradient(135deg, var(--color-secondary), var(--color-accent));
border: none;
border-radius: var(--radius-sm);
color: white;
cursor: pointer;
position: relative;
overflow: hidden;
transition: transform 0.15s ease, box-shadow 0.15s ease;
box-shadow:
0 4px 0 #4a1a6b,
0 8px 24px rgba(123, 44, 191, 0.4);
}
.pixel-btn:hover {
transform: translateY(-2px);
box-shadow:
0 6px 0 #4a1a6b,
0 12px 32px rgba(123, 44, 191, 0.5);
}
.pixel-btn:focus-visible {
outline: 3px solid var(--color-gold);
outline-offset: 3px;
}
.pixel-btn:active {
transform: translateY(2px);
box-shadow:
0 2px 0 #4a1a6b,
0 4px 12px rgba(123, 44, 191, 0.4);
}
/* Hero decoration - harbor theme */
.hero-decoration {
position: absolute;
bottom: 0;
inset-inline: 0;
height: 180px;
overflow: hidden;
pointer-events: none;
}
.harbor-elements {
position: absolute;
bottom: 60px;
inset-inline: 0;
height: 120px;
}
.lighthouse {
position: absolute;
inset-inline-end: 10%;
bottom: 0;
width: 24px;
height: 80px;
background: linear-gradient(to bottom, #fff 0%, #fff 10%, #e74c3c 10%, #e74c3c 30%, #fff 30%, #fff 50%, #e74c3c 50%, #e74c3c 70%, #fff 70%);
border-radius: 4px 4px 0 0;
}
.lighthouse::before {
content: '';
position: absolute;
top: -16px;
inset-inline-start: 50%;
width: 16px;
height: 16px;
background: var(--color-gold);
border-radius: 50%;
box-shadow: 0 0 20px var(--color-gold), 0 0 40px var(--color-gold);
animation: lighthouse-glow 2s ease-in-out infinite;
/* RTL-safe centering */
margin-inline-start: -8px;
}
.buoy {
position: absolute;
bottom: 0;
width: 12px;
height: 24px;
background: linear-gradient(to bottom, #e74c3c 50%, #fff 50%);
border-radius: 6px 6px 2px 2px;
}
.buoy-1 {
inset-inline-start: 15%;
animation: buoy-bob 3s ease-in-out infinite;
}
.buoy-2 {
inset-inline-start: 35%;
animation: buoy-bob 3s ease-in-out infinite 1s;
}
@keyframes lighthouse-glow {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
@keyframes buoy-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.wave {
position: absolute;
bottom: 0;
inset-inline-start: 0;
width: 200%;
height: 60px;
}
.wave-1 {
background:
radial-gradient(ellipse 60px 20px at 30px 100%, var(--color-water) 50%, transparent 50%),
radial-gradient(ellipse 60px 20px at 90px 100%, var(--color-water) 50%, transparent 50%);
background-size: 120px 60px;
animation: wave 6s linear infinite;
opacity: 0.9;
}
.wave-2 {
background:
radial-gradient(ellipse 40px 15px at 20px 100%, var(--color-water-light) 50%, transparent 50%),
radial-gradient(ellipse 40px 15px at 60px 100%, var(--color-water-light) 50%, transparent 50%);
background-size: 80px 40px;
animation: wave 4s linear infinite reverse;
opacity: 0.5;
bottom: 10px;
}
@keyframes wave {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
/* Game Container */
.game-container {
max-width: 960px;
margin: 0 auto var(--space-xl);
padding: var(--space-md);
position: relative;
}
.game-hud {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
margin-block-end: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: var(--color-card-bg);
border: 2px solid var(--color-water-light);
border-radius: var(--radius-md);
}
.hud-item {
text-align: center;
}
.hud-label {
font-family: var(--font-body);
font-weight: 700;
font-size: 0.8125rem;
color: var(--color-text);
display: block;
margin-block-end: var(--space-xs);
letter-spacing: 0.02em;
}
.hud-value {
font-family: var(--font-display);
font-size: clamp(1rem, 3vw, 1.25rem);
color: var(--color-gold);
text-shadow: 0 0 10px rgba(255, 214, 10, 0.5);
}
.boost-bar {
height: 28px;
background: var(--color-water);
border: 2px solid var(--color-primary);
border-radius: var(--radius-sm);
position: relative;
overflow: hidden;
}
.boost-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
transition: width 0.2s ease;
position: relative;
border-radius: var(--radius-sm);
}
.boost-fill::after {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
90deg,
transparent,
transparent 6px,
rgba(255, 255, 255, 0.15) 6px,
rgba(255, 255, 255, 0.15) 12px
);
}
.boost-value {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-body);
font-weight: 700;
font-size: 0.75rem;
color: var(--color-text);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
pointer-events: none;
}
#gameCanvas {
width: 100%;
aspect-ratio: 16 / 9;
background: var(--color-water);
border: 3px solid var(--color-water-light);
border-radius: var(--radius-md);
display: block;
}
.game-overlay {
position: absolute;
inset: 0;
background: rgba(10, 10, 18, 0.95);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
border-radius: var(--radius-md);
}
.game-overlay.hidden {
display: none;
}
.overlay-content {
text-align: center;
padding: var(--space-lg);
max-width: 90%;
}
.overlay-content h2 {
font-family: var(--font-display);
font-size: clamp(1.25rem, 4vw, 1.75rem);
color: var(--color-primary);
margin-block-end: var(--space-sm);
}
.overlay-content p {
color: var(--color-text-muted);
margin-block-end: var(--space-md);
font-size: 1rem;
}
.final-stats {
background: var(--color-card-bg);
padding: var(--space-md);
margin-block-end: var(--space-md);
border: 2px solid var(--color-gold);
border-radius: var(--radius-md);
}
.final-stats[hidden] {
display: none;
}
.stat-row {
display: flex;
justify-content: space-between;
padding: var(--space-xs) 0;
font-family: var(--font-body);
font-weight: 700;
font-size: 0.9rem;
}
.stat-row span:last-child {
color: var(--color-gold);
}
/* Features */
.features {
padding: var(--space-xl) var(--space-md);
max-width: 960px;
margin: 0 auto;
}
.section-title {
font-family: var(--font-display);
font-size: clamp(1.125rem, 3vw, 1.5rem);
text-align: center;
color: var(--color-primary);
margin-block-end: var(--space-lg);
text-shadow: 0 0 30px rgba(0, 245, 212, 0.4);
}
.features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-md);
}
@media (min-width: 700px) {
.features-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.feature-card {
background: var(--color-card-bg);
padding: var(--space-md);
border: 2px solid var(--color-water-light);
border-radius: var(--radius-md);
text-align: center;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0, 245, 212, 0.15);
border-color: var(--color-primary);
}
.feature-card:focus-within {
outline: 3px solid var(--color-primary);
outline-offset: 2px;
}
.feature-icon {
font-size: 2.25rem;
margin-block-end: var(--space-sm);
display: block;
line-height: 1;
}
.feature-card h3 {
font-family: var(--font-display);
font-size: 0.875rem;
color: var(--color-gold);
margin-block-end: var(--space-xs);
}
.feature-card p {
color: var(--color-text-muted);
font-size: 0.9rem;
line-height: 1.6;
}
/* Highscores */
.highscores-section {
padding: var(--space-xl) var(--space-md);
max-width: 500px;
margin: 0 auto;
}
.highscores-table {
background: var(--color-card-bg);
border: 3px solid var(--color-water-light);
border-radius: var(--radius-md);
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
padding: var(--space-sm);
background: var(--color-water);
font-family: var(--font-body);
font-weight: 700;
font-size: 0.875rem;
color: var(--color-primary);
}
.table-body {
min-height: 120px;
}
.table-row {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
padding: var(--space-sm);
border-block-end: 1px solid var(--color-water-light);
font-family: var(--font-body);
font-weight: 700;
font-size: 0.875rem;
transition: background 0.15s ease;
}
.table-row:last-child {
border-block-end: none;
}
.table-row:hover {
background: var(--color-water);
}
.table-row--empty {
grid-template-columns: 1fr;
text-align: center;
color: var(--color-text-muted);
font-weight: 400;
padding: var(--space-md);
}
.table-row:first-child:not(.table-row--empty) span:first-child {
color: var(--color-gold);
}
.table-row:nth-child(2) span:first-child {
color: #c0c0c0;
}
.table-row:nth-child(3) span:first-child {
color: #cd7f32;
}
/* Footer */
.site-footer {
background: var(--color-water);
padding: var(--space-md) var(--space-md) var(--space-lg);
text-align: center;
border-block-start: 3px solid var(--color-water-light);
}
.footer-logo {
font-family: var(--font-display);
font-size: 1rem;
color: var(--color-primary);
margin-block-end: var(--space-xs);
}
.footer-credits {
color: var(--color-text-muted);
font-size: 0.875rem;
}
/* Modal */
.instructions-modal {
position: fixed;
inset: 0;
background: rgba(10, 10, 18, 0.95);
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
padding: var(--space-md);
}
.instructions-modal[hidden] {
display: none;
}
.modal-content {
background: var(--color-card-bg);
padding: var(--space-lg);
max-width: 480px;
width: 100%;
border: 3px solid var(--color-primary);
border-radius: var(--radius-md);
position: relative;
max-height: 90vh;
overflow-y: auto;
}
.modal-close {
position: absolute;
inset-block-start: var(--space-sm);
inset-inline-end: var(--space-sm);
background: none;
border: none;
color: var(--color-text-muted);
font-size: 1.5rem;
cursor: pointer;
padding: var(--space-xs);
line-height: 1;
transition: color 0.15s ease;
}
.modal-close:hover,
.modal-close:focus {
color: var(--color-accent);
}
.modal-close:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.modal-content h2 {
font-family: var(--font-display);
font-size: 1.125rem;
color: var(--color-primary);
margin-block-end: var(--space-md);
}
.modal-content h3 {
font-family: var(--font-display);
font-size: 1rem;
color: var(--color-gold);
margin-block-start: var(--space-md);
margin-block-end: var(--space-sm);
}
.instructions-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.instruction-item {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.instruction-item .key {
font-family: var(--font-body);
font-weight: 700;
font-size: 0.875rem;
background: var(--color-water);
padding: var(--space-xs) var(--space-sm);
border: 2px solid var(--color-primary);
border-radius: var(--radius-sm);
color: var(--color-primary);
min-width: 56px;
text-align: center;
flex-shrink: 0;
}
.tips-list {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.tips-list li {
padding: var(--space-xs) 0;
padding-inline-start: var(--space-sm);
border-inline-start: 3px solid var(--color-secondary);
}
/* Responsive */
@media (max-width: 600px) {
.game-hud {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-sm);
}
.header-content {
flex-wrap: wrap;
justify-content: center;
}
.hero {
padding-block-start: 100px;
}
.lighthouse {
inset-inline-end: 5%;
height: 60px;
width: 18px;
}
.buoy {
width: 10px;
height: 20px;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
html {
scroll-behavior: auto;
}
}
/* Focus visible for all interactive elements */
:focus-visible {
outline: 3px solid var(--color-primary);
outline-offset: 2px;
}
button:focus:not(:focus-visible) {
outline: none;
}