<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.