<main>
	<div class="wrap">
		<h1>Uh-Oh! Not Found</h1>
		<canvas width="560" height="312"></canvas>
		<p>You’re in the middle of nowhere. The page you requested either was moved or doesn’t exist.</p>
		<p>What you can do:</p>
		<ul>
			<li>Go back <a href="#">home</a></li>
			<li><a href="#">Contact</a> to me if you believe this happened in error</li>
		</ul>
	</div>
</main>
* {
	border: 0;
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
:root {
	font-size: calc(16px + (20 - 16)*(100vw - 320px)/(1024 - 320));
}
body {
	background: #f1f1f1;
	color: #242424;
	font: 1em "Hind", Arial, sans-serif;
	line-height: 1.5;
}
a {
	color: #2762f3;
	text-decoration: none;
}
a:hover {
	text-decoration: underline;
}
a:active {
	color: #0c48db;
}
a:visited {
	color: #5785f6;
}
h1 {
	font: 2em "Ubuntu", Arial, sans-serif;
	line-height: 1.5;
	margin-bottom: .75em;
}
p, ul {
	margin-bottom: 1.5em;
}
ul {
	margin-left: 1.5em;
}
main, canvas {
	display: block;
}
canvas {
	display: block;
	margin: 0 auto 1.5em auto;
	width: 100%;
	height: auto;
	-webkit-tap-highlight-color: transparent;
}
.wrap {
	margin: auto;
	padding: 1.5em;
	max-width: 37.5em;
}
@media (prefers-color-scheme: dark) {
	body {
		background: #242424;
		color: #f1f1f1;
	}
	a {
		color: #5785f6;
	}
	a:active {
		color: #2762f3;
	}
	a:visited {
		color: #87a9f9;
	}
}
window.addEventListener("DOMContentLoaded",game);

function game() {
	var canvas = document.querySelector("canvas"),
		c = canvas.getContext("2d"),
		W = canvas.width,
		H = canvas.height,
		S = 2,
		assets = [
			"https://assets.codepen.io/416221/nowhere.png",
			"https://assets.codepen.io/416221/tumbleweed.png"
		],
		sprites = [],
		score = 0,
		world = {
			friction: 0.1,
			gravity: 0.1
		},
		tumbleweed = {
			inPlay: false,
			x: -160,
			y: 200,
			r: 32,
			rotation: 0,
			xVel: 10,
			yVel: 0,
			mass: 2.5,
			restitution: 0.3
		},
		loadSprite = url => {
			return new Promise((resolve,reject) => {
				let sprite = new Image();
				sprite.src = url;
				sprite.onload = () => {
					resolve(sprite);
				};
				sprite.onerror = () => {
					reject(url);
				};
			});
		},
		spritePromises = assets.map(loadSprite),
		applyForce = e => {
			let ex = e.clientX - canvas.offsetLeft,
				ey = e.clientY - (canvas.offsetTop - window.pageYOffset);

			ex = ex / canvas.offsetWidth * W;
			ey = ey / canvas.offsetHeight * H;

			let insideX = Math.abs(ex - tumbleweed.x) <= tumbleweed.r,
				insideY = Math.abs(ey - tumbleweed.y) <= tumbleweed.r;

			if (insideX && insideY) {
				let xForce = tumbleweed.x - ex, 
					yForce = tumbleweed.y - ey,
					xAccel = xForce / tumbleweed.mass,
					yAccel = yForce / tumbleweed.mass;

				tumbleweed.xVel += xAccel;
				tumbleweed.yVel += yAccel;

				++score;
				
				// when enabled, the tumbleweed will be allowed to touch the left side after rolling in
				if (!tumbleweed.inPlay)
					tumbleweed.inPlay = true;
			}
		},
		update = () => {
			// A. Background
			c.clearRect(0,0,W,H);
			c.drawImage(sprites[0],0,0,W,H);

			// B. Tumbleweed
			tumbleweed.x += tumbleweed.xVel;
			
			// 1. Friction to the right
			if (tumbleweed.xVel > 0) {
				tumbleweed.xVel -= world.friction;
				if (tumbleweed.xVel < 0)
					tumbleweed.xVel = 0;
			
			// 2. Friction to the left
			} else if (tumbleweed.xVel < 0) {
				tumbleweed.xVel += world.friction;
				if (tumbleweed.xVel > 0)
					tumbleweed.xVel = 0;
			}
			
			// 3. Horizontal collision
			let hitLeftBound = tumbleweed.x <= tumbleweed.r && tumbleweed.inPlay,
				hitRightBound = tumbleweed.x >= W - tumbleweed.r;

			if (hitLeftBound)
				tumbleweed.x = tumbleweed.r;
			else if (hitRightBound)
				tumbleweed.x = W - tumbleweed.r;

			if (hitLeftBound || hitRightBound)
				tumbleweed.xVel *= -tumbleweed.restitution;
			
			// 4. Vertical collision
			tumbleweed.y += tumbleweed.yVel;
			tumbleweed.yVel += world.gravity;

			let hitTopBound = tumbleweed.y <= tumbleweed.r,
				hitBottomBound = tumbleweed.y >= H - tumbleweed.r;

			if (hitTopBound) {
				tumbleweed.y = tumbleweed.r;

			} else if (hitBottomBound) {
				tumbleweed.y = H - tumbleweed.r;
				score = 0;
			}
			if (hitTopBound || hitBottomBound)
				tumbleweed.yVel *= -tumbleweed.restitution;
			
			// 5. Rotation
			tumbleweed.rotation += tumbleweed.xVel;

			if (tumbleweed.rotation >= 360)
				tumbleweed.rotation -= 360;
			else if (tumbleweed.rotation < 0)
				tumbleweed.rotation += 360;
			
			// 6. Drawing
			c.save();
			c.translate(tumbleweed.x,tumbleweed.y);
			c.rotate(tumbleweed.rotation * Math.PI/180);
			c.drawImage(
				sprites[1],
				-tumbleweed.r,
				-tumbleweed.r,
				tumbleweed.r * 2,
				tumbleweed.r * 2
			);
			c.translate(-tumbleweed.x,-tumbleweed.y);
			c.restore();

			// C. Score
			if (score > 0) {
				c.fillStyle = "#7f7f7f";
				c.font = "48px Hind, sans-serif";
				c.textAlign = "center";
				c.fillText(score,W/2,48);
			}
		},
		render = () => {
			update();
			requestAnimationFrame(render);
		};
	
	// ensure proper resolution
	canvas.width = W * S;
	canvas.height = H * S;
	c.scale(S,S);
	
	// load sprites
	Promise.all(spritePromises).then(loaded => {
		for (let sprite of loaded)
			sprites.push(sprite);

		render();
		canvas.addEventListener("click",applyForce);

	}).catch(urls => {
		console.log(urls+" couldn’t be loaded");
	});
}

External CSS

  1. https://fonts.googleapis.com/css?family=Ubuntu:700&amp;display=swap
  2. https://fonts.googleapis.com/css?family=Hind:400&amp;display=swap

External JavaScript

This Pen doesn't use any external JavaScript resources.