whalerun / index.html
offerpk3's picture
Update index.html
9f4cb12 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Whale Runner</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(180deg, #87CEEB 0%, #4682B4 50%, #191970 100%);
font-family: 'Arial', sans-serif;
overflow: hidden;
}
#gameContainer {
position: relative;
width: 800px;
height: 600px;
background: linear-gradient(180deg, #87CEEB 0%, #4682B4 30%, #1e3a8a 70%, #0f172a 100%);
border: 3px solid #2c5aa0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
}
#gameCanvas {
display: block;
width: 100%;
height: 100%;
}
#gameUI {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
color: white;
z-index: 10;
}
#score {
position: absolute;
top: 20px;
left: 20px;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
}
#gameOver {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0,0,0,0.8);
padding: 30px;
border-radius: 15px;
display: none;
pointer-events: all;
}
#gameOver h2 {
margin: 0 0 10px 0;
color: #ff6b6b;
font-size: 36px;
}
#gameOver p {
margin: 10px 0;
font-size: 18px;
}
#restartBtn {
background: #4CAF50;
color: white;
border: none;
padding: 15px 30px;
font-size: 18px;
border-radius: 8px;
cursor: pointer;
margin-top: 15px;
transition: background 0.3s;
}
#restartBtn:hover {
background: #45a049;
}
#instructions {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
font-size: 16px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="gameUI">
<div id="score">Score: 0</div>
<div id="gameOver">
<h2>Game Over!</h2>
<p>Final Score: <span id="finalScore">0</span></p>
<p>Best Score: <span id="bestScore">0</span></p>
<button id="restartBtn">Play Again</button>
</div>
<div id="instructions">
Click or Press SPACE to make the whale swim up!<br>
Avoid the obstacles and collect bubbles for points!
</div>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const gameOverElement = document.getElementById('gameOver');
const finalScoreElement = document.getElementById('finalScore');
const bestScoreElement = document.getElementById('bestScore');
const restartBtn = document.getElementById('restartBtn');
const instructionsElement = document.getElementById('instructions');
// Game state
let gameState = {
running: true,
score: 0,
bestScore: 0,
speed: 2
};
// Whale object
const whale = {
x: 150,
y: 300,
width: 60,
height: 40,
velocity: 0,
jumpPower: -12,
gravity: 0.8,
maxVelocity: 15
};
// Game objects arrays
let obstacles = [];
let bubbles = [];
let particles = [];
let backgroundFish = [];
let seaSnakes = [];
let jellyfish = [];
// Obstacle creation
function createObstacle() {
const gapSize = 180;
const minHeight = 80;
const maxHeight = canvas.height - gapSize - minHeight;
const topHeight = Math.random() * (maxHeight - minHeight) + minHeight;
obstacles.push({
x: canvas.width,
topHeight: topHeight,
bottomY: topHeight + gapSize,
bottomHeight: canvas.height - (topHeight + gapSize),
width: 60,
passed: false
});
}
// Background sea creatures creation
function createBackgroundFish() {
backgroundFish.push({
x: canvas.width + Math.random() * 200,
y: Math.random() * canvas.height,
size: Math.random() * 20 + 15,
speed: Math.random() * 1 + 0.5,
color: Math.random() > 0.5 ? '#FF6B35' : '#F7931E',
wiggle: Math.random() * Math.PI * 2,
direction: Math.random() > 0.5 ? 1 : -1
});
}
function createSeaSnake() {
const segments = 8;
let snake = {
segments: [],
speed: Math.random() * 1.5 + 1,
direction: Math.random() * Math.PI * 2,
turnSpeed: 0.02
};
for (let i = 0; i < segments; i++) {
snake.segments.push({
x: canvas.width + i * 15,
y: Math.random() * canvas.height,
size: Math.max(8 - i, 3)
});
}
seaSnakes.push(snake);
}
function createJellyfish() {
jellyfish.push({
x: canvas.width + Math.random() * 300,
y: Math.random() * (canvas.height - 100) + 50,
size: Math.random() * 30 + 25,
speed: Math.random() * 0.8 + 0.3,
pulse: Math.random() * Math.PI * 2,
tentacles: Array.from({length: 6}, () => ({
length: Math.random() * 40 + 30,
wave: Math.random() * Math.PI * 2,
offset: Math.random() * Math.PI * 2
})),
color: Math.random() > 0.5 ? 'rgba(255,192,203,0.7)' : 'rgba(173,216,230,0.7)'
});
}
// Bubble creation
function createBubble() {
bubbles.push({
x: canvas.width + Math.random() * 200,
y: Math.random() * (canvas.height - 100) + 50,
radius: Math.random() * 15 + 10,
collected: false,
bob: Math.random() * Math.PI * 2
});
}
// Particle system for effects
function createParticles(x, y, color, count = 5) {
for (let i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
life: 30,
maxLife: 30,
color: color,
size: Math.random() * 4 + 2
});
}
}
// Input handling
function jump() {
if (gameState.running) {
whale.velocity = whale.jumpPower;
createParticles(whale.x + whale.width/2, whale.y + whale.height, '#87CEEB', 3);
instructionsElement.style.display = 'none';
}
}
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
jump();
}
});
canvas.addEventListener('click', jump);
restartBtn.addEventListener('click', resetGame);
// Drawing functions
function drawBackground() {
// Animated water effect with depth layers
const time = Date.now() * 0.001;
// Deep water layers
for (let layer = 0; layer < 3; layer++) {
const alpha = 0.03 + layer * 0.02;
const speed = 0.5 + layer * 0.3;
for (let i = 0; i < canvas.width; i += 30) {
const wave1 = Math.sin(i * 0.008 + time * speed) * 15;
const wave2 = Math.sin(i * 0.012 + time * speed * 1.5) * 10;
const wave = wave1 + wave2;
ctx.fillStyle = `rgba(30, 144, 255, ${alpha})`;
ctx.fillRect(i, wave + layer * 50, 30, canvas.height);
}
}
// Underwater plants/seaweed
ctx.strokeStyle = 'rgba(34, 139, 34, 0.4)';
ctx.lineWidth = 3;
for (let i = 0; i < canvas.width; i += 80) {
const baseX = i + Math.sin(time + i * 0.01) * 5;
const seaweedHeight = 60 + Math.sin(i * 0.02) * 20;
ctx.beginPath();
ctx.moveTo(baseX, canvas.height);
for (let j = 0; j < seaweedHeight; j += 5) {
const swayX = baseX + Math.sin(time * 2 + j * 0.1) * (j * 0.2);
ctx.lineTo(swayX, canvas.height - j);
}
ctx.stroke();
}
// Ocean floor particles
ctx.fillStyle = 'rgba(160, 82, 45, 0.3)';
for (let i = 0; i < canvas.width; i += 20) {
const sandHeight = 5 + Math.sin(i * 0.02 + time * 0.5) * 3;
ctx.fillRect(i, canvas.height - sandHeight, 20, sandHeight);
}
}
function drawWhale() {
ctx.save();
// Whale body (gradient blue)
const gradient = ctx.createLinearGradient(whale.x, whale.y, whale.x + whale.width, whale.y + whale.height);
gradient.addColorStop(0, '#4682B4');
gradient.addColorStop(0.5, '#87CEEB');
gradient.addColorStop(1, '#4169E1');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.ellipse(whale.x + whale.width/2, whale.y + whale.height/2, whale.width/2, whale.height/2, 0, 0, Math.PI * 2);
ctx.fill();
// Whale tail
ctx.fillStyle = '#4169E1';
ctx.beginPath();
ctx.moveTo(whale.x - 10, whale.y + whale.height/2);
ctx.lineTo(whale.x - 25, whale.y + 5);
ctx.lineTo(whale.x - 25, whale.y + whale.height - 5);
ctx.closePath();
ctx.fill();
// Eye
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(whale.x + whale.width - 15, whale.y + 12, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(whale.x + whale.width - 12, whale.y + 12, 4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawObstacles() {
ctx.fillStyle = '#2F4F4F';
obstacles.forEach(obstacle => {
// Top obstacle
ctx.fillRect(obstacle.x, 0, obstacle.width, obstacle.topHeight);
// Bottom obstacle
ctx.fillRect(obstacle.x, obstacle.bottomY, obstacle.width, obstacle.bottomHeight);
// Add some texture
ctx.fillStyle = '#1C3A3A';
for (let i = 0; i < 5; i++) {
ctx.fillRect(obstacle.x + Math.random() * obstacle.width, Math.random() * obstacle.topHeight, 3, 3);
ctx.fillRect(obstacle.x + Math.random() * obstacle.width, obstacle.bottomY + Math.random() * obstacle.bottomHeight, 3, 3);
}
ctx.fillStyle = '#2F4F4F';
});
}
function drawBubbles() {
bubbles.forEach(bubble => {
if (!bubble.collected) {
bubble.bob += 0.1;
const bobOffset = Math.sin(bubble.bob) * 3;
const gradient = ctx.createRadialGradient(
bubble.x, bubble.y + bobOffset, 0,
bubble.x, bubble.y + bobOffset, bubble.radius
);
gradient.addColorStop(0, 'rgba(255,255,255,0.8)');
gradient.addColorStop(0.7, 'rgba(173,216,230,0.6)');
gradient.addColorStop(1, 'rgba(135,206,235,0.3)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(bubble.x, bubble.y + bobOffset, bubble.radius, 0, Math.PI * 2);
ctx.fill();
// Highlight
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath();
ctx.arc(bubble.x - bubble.radius/3, bubble.y + bobOffset - bubble.radius/3, bubble.radius/4, 0, Math.PI * 2);
ctx.fill();
}
});
}
function drawBackgroundFish() {
backgroundFish.forEach(fish => {
ctx.save();
// Fish body
const gradient = ctx.createLinearGradient(fish.x - fish.size, fish.y, fish.x + fish.size, fish.y);
gradient.addColorStop(0, fish.color);
gradient.addColorStop(1, fish.color + '80');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.ellipse(fish.x, fish.y, fish.size, fish.size * 0.6, 0, 0, Math.PI * 2);
ctx.fill();
// Fish tail
ctx.fillStyle = fish.color;
ctx.beginPath();
const tailOffset = Math.sin(fish.wiggle) * 3;
ctx.moveTo(fish.x - fish.size, fish.y);
ctx.lineTo(fish.x - fish.size * 1.5, fish.y - fish.size * 0.4 + tailOffset);
ctx.lineTo(fish.x - fish.size * 1.5, fish.y + fish.size * 0.4 + tailOffset);
ctx.closePath();
ctx.fill();
// Fish eye
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(fish.x + fish.size * 0.3, fish.y - fish.size * 0.2, fish.size * 0.15, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(fish.x + fish.size * 0.35, fish.y - fish.size * 0.2, fish.size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
}
function drawSeaSnakes() {
seaSnakes.forEach(snake => {
ctx.strokeStyle = '#228B22';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
// Draw snake body
ctx.beginPath();
snake.segments.forEach((segment, index) => {
if (index === 0) {
ctx.moveTo(segment.x, segment.y);
} else {
ctx.lineTo(segment.x, segment.y);
}
});
ctx.stroke();
// Draw snake segments
snake.segments.forEach((segment, index) => {
const alpha = 1 - (index / snake.segments.length) * 0.3;
ctx.fillStyle = `rgba(34, 139, 34, ${alpha})`;
ctx.beginPath();
ctx.arc(segment.x, segment.y, segment.size, 0, Math.PI * 2);
ctx.fill();
// Snake head
if (index === 0) {
ctx.fillStyle = 'rgba(255, 255, 0, 0.8)';
ctx.beginPath();
ctx.arc(segment.x - 3, segment.y - 2, 2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(segment.x - 3, segment.y + 2, 2, 0, Math.PI * 2);
ctx.fill();
}
});
});
}
function drawJellyfish() {
jellyfish.forEach(jelly => {
const time = Date.now() * 0.003;
const pulseFactor = 0.8 + Math.sin(jelly.pulse + time * 3) * 0.2;
const currentSize = jelly.size * pulseFactor;
// Jellyfish bell
const gradient = ctx.createRadialGradient(
jelly.x, jelly.y, 0,
jelly.x, jelly.y, currentSize
);
gradient.addColorStop(0, jelly.color);
gradient.addColorStop(0.8, jelly.color.replace('0.7', '0.4'));
gradient.addColorStop(1, jelly.color.replace('0.7', '0.1'));
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(jelly.x, jelly.y, currentSize, 0, Math.PI);
ctx.fill();
// Tentacles
ctx.strokeStyle = jelly.color.replace('0.7', '0.5');
ctx.lineWidth = 2;
ctx.lineCap = 'round';
jelly.tentacles.forEach(tentacle => {
ctx.beginPath();
ctx.moveTo(jelly.x + (Math.random() - 0.5) * currentSize, jelly.y);
for (let i = 0; i < tentacle.length; i += 5) {
const waveX = Math.sin(tentacle.wave + time * 2 + i * 0.1) * 8;
const waveY = Math.sin(tentacle.offset + time + i * 0.05) * 3;
ctx.lineTo(
jelly.x + waveX + (Math.random() - 0.5) * 3,
jelly.y + i + waveY
);
}
ctx.stroke();
});
});
}
function drawParticles() {
particles.forEach((particle, index) => {
const alpha = particle.life / particle.maxLife;
ctx.fillStyle = particle.color + Math.floor(alpha * 255).toString(16).padStart(2, '0');
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size * alpha, 0, Math.PI * 2);
ctx.fill();
});
}
// Game logic
function updateWhale() {
whale.velocity += whale.gravity;
whale.velocity = Math.min(whale.velocity, whale.maxVelocity);
whale.y += whale.velocity;
// Boundary checking
if (whale.y < 0) {
whale.y = 0;
whale.velocity = 0;
}
if (whale.y + whale.height > canvas.height) {
whale.y = canvas.height - whale.height;
whale.velocity = 0;
gameOver();
}
}
function updateObstacles() {
obstacles.forEach((obstacle, index) => {
obstacle.x -= gameState.speed;
// Score when passing obstacle
if (!obstacle.passed && obstacle.x + obstacle.width < whale.x) {
obstacle.passed = true;
gameState.score += 10;
createParticles(whale.x + whale.width/2, whale.y + whale.height/2, '#FFD700', 8);
}
// Remove off-screen obstacles
if (obstacle.x + obstacle.width < 0) {
obstacles.splice(index, 1);
}
});
}
function updateBubbles() {
bubbles.forEach((bubble, index) => {
bubble.x -= gameState.speed * 0.5;
// Check collection
if (!bubble.collected) {
const dx = bubble.x - (whale.x + whale.width/2);
const dy = bubble.y - (whale.y + whale.height/2);
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < bubble.radius + 20) {
bubble.collected = true;
gameState.score += 5;
createParticles(bubble.x, bubble.y, '#87CEEB', 10);
}
}
// Remove off-screen bubbles
if (bubble.x + bubble.radius < 0) {
bubbles.splice(index, 1);
}
});
}
function updateBackgroundCreatures() {
// Update background fish
backgroundFish.forEach((fish, index) => {
fish.x -= fish.speed;
fish.wiggle += 0.1;
fish.y += Math.sin(fish.wiggle) * 0.5 * fish.direction;
// Keep fish in bounds
if (fish.y < 20) fish.direction = 1;
if (fish.y > canvas.height - 20) fish.direction = -1;
// Remove off-screen fish
if (fish.x + fish.size < 0) {
backgroundFish.splice(index, 1);
}
});
// Update sea snakes
seaSnakes.forEach((snake, index) => {
snake.direction += (Math.random() - 0.5) * snake.turnSpeed;
// Move head
snake.segments[0].x -= snake.speed;
snake.segments[0].y += Math.sin(snake.direction) * 2;
// Keep snake in bounds
if (snake.segments[0].y < 30) snake.segments[0].y = 30;
if (snake.segments[0].y > canvas.height - 30) snake.segments[0].y = canvas.height - 30;
// Follow the head
for (let i = 1; i < snake.segments.length; i++) {
const prev = snake.segments[i - 1];
const curr = snake.segments[i];
const dx = prev.x - curr.x;
const dy = prev.y - curr.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 15) {
curr.x += (dx / distance) * 2;
curr.y += (dy / distance) * 2;
}
}
// Remove off-screen snakes
if (snake.segments[0].x < -100) {
seaSnakes.splice(index, 1);
}
});
// Update jellyfish
jellyfish.forEach((jelly, index) => {
jelly.x -= jelly.speed;
jelly.pulse += 0.05;
jelly.y += Math.sin(jelly.pulse) * 0.8;
// Update tentacle animation
jelly.tentacles.forEach(tentacle => {
tentacle.wave += 0.08;
tentacle.offset += 0.03;
});
// Remove off-screen jellyfish
if (jelly.x + jelly.size < 0) {
jellyfish.splice(index, 1);
}
});
}
function updateParticles() {
particles.forEach((particle, index) => {
particle.x += particle.vx;
particle.y += particle.vy;
particle.life--;
particle.vy += 0.2; // gravity
if (particle.life <= 0) {
particles.splice(index, 1);
}
});
}
function checkCollisions() {
obstacles.forEach(obstacle => {
// Check collision with top obstacle
if (whale.x + whale.width > obstacle.x &&
whale.x < obstacle.x + obstacle.width &&
whale.y < obstacle.topHeight) {
gameOver();
}
// Check collision with bottom obstacle
if (whale.x + whale.width > obstacle.x &&
whale.x < obstacle.x + obstacle.width &&
whale.y + whale.height > obstacle.bottomY) {
gameOver();
}
});
}
function gameOver() {
gameState.running = false;
if (gameState.score > gameState.bestScore) {
gameState.bestScore = gameState.score;
}
finalScoreElement.textContent = gameState.score;
bestScoreElement.textContent = gameState.bestScore;
gameOverElement.style.display = 'block';
// Create explosion effect
createParticles(whale.x + whale.width/2, whale.y + whale.height/2, '#ff6b6b', 15);
}
function resetGame() {
gameState.running = true;
gameState.score = 0;
gameState.speed = 2;
whale.y = 300;
whale.velocity = 0;
obstacles.length = 0;
bubbles.length = 0;
particles.length = 0;
backgroundFish.length = 0;
seaSnakes.length = 0;
jellyfish.length = 0;
gameOverElement.style.display = 'none';
instructionsElement.style.display = 'block';
}
// Game loop
let lastObstacleTime = 0;
let lastBubbleTime = 0;
let lastFishTime = 0;
let lastSnakeTime = 0;
let lastJellyfishTime = 0;
function gameLoop() {
// Clear canvas
ctx.fillStyle = 'rgba(0,0,0,0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawBackground();
if (gameState.running) {
updateWhale();
updateObstacles();
updateBubbles();
updateBackgroundCreatures();
updateParticles();
checkCollisions();
// Spawn obstacles
const now = Date.now();
if (now - lastObstacleTime > 2000) {
createObstacle();
lastObstacleTime = now;
}
// Spawn bubbles
if (now - lastBubbleTime > 3000 + Math.random() * 2000) {
createBubble();
lastBubbleTime = now;
}
// Spawn background fish
if (now - lastFishTime > 1500 + Math.random() * 2000) {
createBackgroundFish();
lastFishTime = now;
}
// Spawn sea snakes
if (now - lastSnakeTime > 4000 + Math.random() * 3000) {
createSeaSnake();
lastSnakeTime = now;
}
// Spawn jellyfish
if (now - lastJellyfishTime > 3500 + Math.random() * 4000) {
createJellyfish();
lastJellyfishTime = now;
}
// Increase difficulty
gameState.speed = Math.min(2 + gameState.score * 0.01, 6);
scoreElement.textContent = `Score: ${gameState.score}`;
}
// Draw everything
drawBackgroundFish();
drawSeaSnakes();
drawJellyfish();
drawObstacles();
drawBubbles();
drawWhale();
drawParticles();
requestAnimationFrame(gameLoop);
}
// Initialize game
bestScoreElement.textContent = gameState.bestScore;
gameLoop();
</script>
</body>
</html>