A classic endless runner game inspired by Chrome’s offline dinosaur game, featuring sprite-based animations, procedural obstacle generation, and responsive controls. Built with HTML5 Canvas and vanilla JavaScript for optimal performance.
dino/
├── index.html # Main game application
├── index.css # Game styling and responsive layout
├── index.js # Core game logic and mechanics
├── real.html # Alternative game version
└── assets/ # Sprite assets and media
├── default_100_percent/
│ ├── 100-offline-sprite.png
│ ├── 100-error-offline.png
│ └── 100-disabled.png
├── default_200_percent/
│ ├── 200-offline-sprite.png
│ ├── 200-error-offline.png
│ └── 200-disabled.png
├── offline-sprite-1x.png
├── offline-sprite-2x.png
└── [various demo GIFs]
// Main game controller
class Runner {
constructor(containerElement, opt_config) {
this.containerEl = containerElement;
this.config = opt_config || {};
this.dimensions = DIMENSIONS;
this.canvas = null;
this.canvasCtx = null;
this.distanceRan = 0;
this.highestScore = 0;
this.time = 0;
this.runningTime = 0;
this.msPerFrame = 1000 / FPS;
this.currentSpeed = this.config.SPEED;
this.obstacles = [];
this.activated = false;
this.playing = false;
this.crashed = false;
this.init();
}
}
// Dinosaur player class
class Trex {
constructor(canvas, spritePos) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.spritePos = spritePos;
this.xPos = 0;
this.yPos = 0;
this.groundYPos = 0;
this.currentFrame = 0;
this.currentAnimFrames = [];
this.blinkDelay = 0;
this.animStartTime = 0;
this.timer = 0;
this.msPerFrame = 1000 / FPS;
this.config = Trex.config;
this.status = Trex.status.WAITING;
this.jumping = false;
this.ducking = false;
this.jumpVelocity = 0;
this.reachedMinHeight = false;
this.speedDrop = false;
this.jumpCount = 0;
this.jumpspotX = 0;
this.init();
}
}
// Obstacle management
class Obstacle {
constructor(canvasCtx, type, spriteImgPos, dimensions, gapCoefficient, speed, opt_xOffset) {
this.canvasCtx = canvasCtx;
this.spritePos = spriteImgPos;
this.typeConfig = type;
this.gapCoefficient = gapCoefficient;
this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
this.dimensions = dimensions;
this.remove = false;
this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
this.yPos = 0;
this.width = 0;
this.collisionBoxes = [];
this.gap = 0;
this.speedOffset = 0;
this.init(speed);
}
}
// Core game constants
const DEFAULT_WIDTH = 600;
const FPS = 60;
const MAX_CLOUDS = 6;
const RUNNER_MAX_OBSTACLES = 3;
const RUNNER_CLEAR_TIME = 3000;
const RUNNER_BOTTOM_PAD = 10;
const RUNNER_TOP_PAD = 10;
// Game dimensions
const DIMENSIONS = {
WIDTH: DEFAULT_WIDTH,
HEIGHT: 150
};
// Speed configuration
const SPEED = {
DROP_VELOCITY: -5,
GRAVITY: 0.6,
INITIAL_JUMP_VELOCITY: -10,
JUMP_ACCEL: -0.3,
MAX_JUMP_HEIGHT: 30,
MIN_JUMP_HEIGHT: 35,
SPEED: 6,
SPEED_DROP_COEFFICIENT: 3
};
// Sprite positions and configurations
const TREX_CONFIG = {
DROP_VELOCITY: -5,
GRAVITY: 0.6,
HEIGHT: 47,
HEIGHT_DUCK: 25,
INIITAL_JUMP_VELOCITY: -10,
INTRO_DURATION: 1500,
MAX_JUMP_HEIGHT: 30,
MIN_JUMP_HEIGHT: 35,
SPEED_DROP_COEFFICIENT: 3,
SPRITE_WIDTH: 262,
START_X_POS: 50,
WIDTH: 44,
WIDTH_DUCK: 59
};
// Animation frame sequences
const ANIMATION_FRAMES = {
WAITING: [44, 0], // Standing position
RUNNING: [
[76, 6], // Running frame 1
[118, 6] // Running frame 2
],
CRASHED: [220, 2], // Death animation
JUMPING: [0, 2], // Jump frame
DUCKING: [
[264, 6], // Duck frame 1
[323, 6] // Duck frame 2
]
};
// Frame animation system
updateFrame: function(deltaTime) {
this.timer += deltaTime;
if (this.timer >= this.msPerFrame) {
this.currentFrame = this.currentFrame == this.currentAnimFrames.length - 1 ?
0 : this.currentFrame + 1;
this.timer = 0;
}
},
// Dynamic animation switching
setAnimationFrame: function(frames) {
this.currentAnimFrames = frames;
this.currentFrame = 0;
},
// Sprite rendering
draw: function(x, y) {
var sourceX = this.currentAnimFrames[this.currentFrame][0];
var sourceY = this.currentAnimFrames[this.currentFrame][1];
var sourceWidth = this.typeConfig.width;
var sourceHeight = this.typeConfig.height;
this.canvasCtx.drawImage(
Runner.imageSprite,
sourceX, sourceY,
sourceWidth, sourceHeight,
x, y,
sourceWidth, sourceHeight
);
}
// Dinosaur jumping physics
updateJump: function(deltaTime, speed) {
var msPerFrame = Trex.config.INIITAL_JUMP_VELOCITY / 2;
// Adjust jump velocity for game speed
if (this.speedDrop) {
this.jumpVelocity += this.config.GRAVITY * deltaTime * speed / 2;
} else {
this.jumpVelocity += this.config.GRAVITY * deltaTime;
}
// Update vertical position
this.yPos += this.jumpVelocity;
// Ground collision detection
if (this.yPos < this.minJumpHeight || this.yPos < this.config.MIN_JUMP_HEIGHT) {
this.reachedMinHeight = true;
}
// Landing detection
if (this.yPos > this.groundYPos) {
this.yPos = this.groundYPos;
this.jumpDown = false;
this.jumping = false;
this.jumpCount = 0;
this.setAnimationFrame(this.config.RUNNING_ANIM_FRAMES);
}
}
// Precise collision detection system
checkForCollision: function(obstacle, tRex) {
var obstacleBoxes = obstacle.collisionBoxes;
var tRexBox = tRex.getBoundingBox();
for (var t = 0; t < obstacleBoxes.length; t++) {
for (var i = 0; i < tRexBox.length; i++) {
// Box intersection test
if (boxCompare(tRexBox[i], obstacleBoxes[t])) {
return [tRexBox[i], obstacleBoxes[t]];
}
}
}
return false;
},
// Box intersection algorithm
function boxCompare(tRexBox, obstacleBox) {
var crashed = false;
var tRexBoxX = tRexBox.x;
var tRexBoxY = tRexBox.y;
var tRexBoxW = tRexBox.width;
var tRexBoxH = tRexBox.height;
var obstacleBoxX = obstacleBox.x;
var obstacleBoxY = obstacleBox.y;
var obstacleBoxW = obstacleBox.width;
var obstacleBoxH = obstacleBox.height;
// AABB collision detection
if (tRexBoxX < obstacleBoxX + obstacleBoxW &&
tRexBoxX + tRexBoxW > obstacleBoxX &&
tRexBoxY < obstacleBoxY + obstacleBoxH &&
tRexBoxY + tRexBoxH > obstacleBoxY) {
crashed = true;
}
return crashed;
}
// Dynamic obstacle placement
addNewObstacle: function() {
var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
var obstacleType = Obstacle.types[obstacleTypeIndex];
// Calculate spacing based on game speed
var obstacleGap = this.getObstacleGap(this.currentSpeed, obstacleType.gapCoefficient);
var obstacle = new Obstacle(
this.canvasCtx,
obstacleType,
obstacleType.spriteImgPos,
this.dimensions,
obstacleGap,
this.currentSpeed
);
this.obstacles.push(obstacle);
// Schedule next obstacle
this.scheduleNextObstacle();
},
// Intelligent gap calculation
getObstacleGap: function(speed, gapCoefficient) {
var minGap = Math.round(this.canvas.width / speed + RUNNER_MAX_OBSTACLES * Obstacle.config.MIN_GAP_COEFFICIENT);
var maxGap = minGap * 2;
var gap = getRandomNum(minGap, maxGap) * gapCoefficient;
return gap;
}
// Obstacle configurations
Obstacle.types = [
{
type: 'CACTUS_SMALL',
width: 17,
height: 35,
yPos: 105,
multipleSpeed: 4,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 7, 5, 27),
new CollisionBox(4, 0, 6, 34),
new CollisionBox(10, 4, 7, 14)
]
},
{
type: 'CACTUS_LARGE',
width: 25,
height: 50,
yPos: 90,
multipleSpeed: 7,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 12, 7, 38),
new CollisionBox(8, 0, 7, 49),
new CollisionBox(13, 10, 10, 38)
]
},
{
type: 'PTERODACTYL',
width: 46,
height: 40,
yPos: [100, 75, 50], // Multiple flight heights
multipleSpeed: 999,
minSpeed: 8.5,
minGap: 150,
collisionBoxes: [
new CollisionBox(15, 15, 16, 5),
new CollisionBox(18, 21, 24, 6),
new CollisionBox(2, 14, 4, 3),
new CollisionBox(6, 10, 4, 7),
new CollisionBox(10, 8, 6, 9)
],
animationFrames: [
{x: 134, y: 0},
{x: 180, y: 0}
]
}
];
// Multi-input support (keyboard and touch)
onKeyDown: function(e) {
if (!this.crashed && !this.paused) {
switch (e.keyCode) {
case KEYCODES.SPACE:
case KEYCODES.UP:
this.tRex.startJump(this.currentSpeed);
this.setPlayStatus(true);
break;
case KEYCODES.DOWN:
if (this.tRex.jumping) {
this.tRex.setSpeedDrop();
} else if (!this.tRex.jumping && !this.tRex.ducking) {
this.tRex.setDuck(true);
}
break;
}
} else if (this.crashed && e.type == EVENTS.KEYDOWN) {
this.restart();
}
},
// Touch controls for mobile
onTouchStart: function(e) {
if (!this.touchController.classList.contains(TOUCH_CONTROLLER)) {
e.preventDefault();
if (this.playing) {
this.tRex.startJump(this.currentSpeed);
} else {
this.play();
}
}
}
// Touch event optimization
enableTouchControls: function() {
document.addEventListener(EVENTS.TOUCHSTART, this);
document.addEventListener(EVENTS.TOUCHEND, this);
},
// Gesture detection
handleTouchGestures: function(e) {
var touch = e.touches[0];
var startY = this.touchStartY;
var currentY = touch.clientY;
var deltaY = currentY - startY;
// Swipe down for duck
if (deltaY > SWIPE_THRESHOLD) {
if (this.tRex.jumping) {
this.tRex.setSpeedDrop();
} else {
this.tRex.setDuck(true);
}
}
}
// Real-time score calculation
updateScore: function() {
var score = (this.distanceRan * 0.01) | 0;
if (score > this.highestScore) {
this.highestScore = score;
this.distanceMeter.setHighScore(this.highestScore);
}
this.distanceMeter.update(this.deltaTime, score);
},
// Achievement milestones
checkForMilestone: function(score) {
if (score > 0 && score % 100 === 0) {
this.playSound(this.soundFx.SCORE);
this.spawnNightMode();
}
}
// Local storage integration
saveHighScore: function(score) {
try {
localStorage.setItem('dino-high-score', score.toString());
} catch (e) {
// Handle storage errors
console.warn('Unable to save high score');
}
},
loadHighScore: function() {
try {
var saved = localStorage.getItem('dino-high-score');
return saved ? parseInt(saved, 10) : 0;
} catch (e) {
return 0;
}
}
// Dynamic environment changes
spawnNightMode: function() {
if (!this.nightMode.isNight) {
this.nightMode.isNight = true;
this.invertColors();
this.scheduleNightMode();
}
},
// Color inversion effect
invertColors: function() {
var ctx = this.canvasCtx;
var imageData = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // Red
data[i + 1] = 255 - data[i + 1]; // Green
data[i + 2] = 255 - data[i + 2]; // Blue
}
ctx.putImageData(imageData, 0, 0);
}
// Background cloud system
updateClouds: function(speed, deltaTime) {
var cloudSpeed = this.config.BG_CLOUD_SPEED / 1000 * deltaTime * speed;
var numClouds = this.clouds.length;
if (numClouds) {
for (var i = numClouds - 1; i >= 0; i--) {
this.clouds[i].update(cloudSpeed);
}
var lastCloud = this.clouds[numClouds - 1];
// Add new clouds as needed
if (numClouds < this.config.MAX_CLOUDS &&
(this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
this.cloudFrequency > getRandomNum(1, this.config.CLOUD_FREQUENCY)) {
this.addCloud();
}
// Remove off-screen clouds
this.clouds = this.clouds.filter(function(obj) {
return !obj.remove;
});
}
}
// Optimized update cycle
scheduleNextUpdate: function() {
if (!this.updatePending) {
this.updatePending = true;
this.raqId = requestAnimationFrame(this.update.bind(this));
}
},
update: function() {
this.updatePending = false;
var now = getTimeStamp();
var deltaTime = now - (this.time || now);
this.time = now;
if (this.playing) {
this.clearCanvas();
if (this.tRex.jumping) {
this.tRex.updateJump(deltaTime);
}
this.runningTime += deltaTime;
var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
if (this.tRex.jumpCount == 1 && !this.playingIntro) {
this.playIntro();
}
if (this.playingIntro) {
this.horizon.update(0, this.currentSpeed, hasObstacles);
} else {
deltaTime = !this.started ? 0 : deltaTime;
this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, this.inverted);
}
var collision = hasObstacles && checkForCollision(this.horizon.obstacles[0], this.tRex);
if (!collision) {
this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
if (this.currentSpeed < this.config.MAX_SPEED) {
this.currentSpeed += this.config.ACCELERATION;
}
} else {
this.gameOver();
}
var playAchievementSound = this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan * 0.01));
if (playAchievementSound) {
this.playSound(this.soundFx.SCORE);
}
if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
this.invertTimer = 0;
this.invertTrigger = false;
this.invert(false);
} else if (this.invertTimer) {
this.invertTimer += deltaTime;
}
}
if (this.playing || (!this.activated && this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
this.tRex.update(deltaTime);
this.scheduleNextUpdate();
}
}
// Efficient object pooling
clearObstacles: function() {
this.obstacles = [];
},
// Canvas optimization
clearCanvas: function() {
this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH, this.dimensions.HEIGHT);
},
// Resource cleanup
destroy: function() {
this.stop();
cancelAnimationFrame(this.raqId);
this.canvas = null;
this.canvasCtx = null;
}
/* Responsive canvas styling */
.runner-canvas {
height: 150px;
position: absolute;
top: 0;
z-index: 2;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
/* Mobile optimizations */
@media (max-width: 600px) {
.runner-container {
height: 120px;
transform: scale(0.8);
transform-origin: top left;
}
}
/* High DPI display support */
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) {
.runner-canvas {
image-rendering: -webkit-optimize-contrast;
image-rendering: pixelated;
}
}
// Dynamic canvas sizing
adjustDimensions: function() {
var devicePixelRatio = window.devicePixelRatio || 1;
var actualWidth = this.dimensions.WIDTH * devicePixelRatio;
var actualHeight = this.dimensions.HEIGHT * devicePixelRatio;
this.canvas.width = actualWidth;
this.canvas.height = actualHeight;
this.canvas.style.width = this.dimensions.WIDTH + 'px';
this.canvas.style.height = this.dimensions.HEIGHT + 'px';
this.canvasCtx.scale(devicePixelRatio, devicePixelRatio);
}
The Dino Runner game represents an excellent implementation of the classic endless runner genre, featuring sophisticated sprite animation, precise physics, and optimal performance. The combination of nostalgic pixel art aesthetics with modern web technologies creates an engaging and accessible gaming experience.
Technical Rating: 8.6/10