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
+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();
}
})();