Skip to content

Bogorg/towr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TOWR

A tower stacking game where every technical decision is slightly dumb.

Fun constraint: no canvas, no in-game SVG, no text, no JS global state for the game. Everything you see is built with plain div elements, CSS transforms, CSS animations and the state is all derived from the DOM structure and CSS selectors.

Screenshot of TOWR

How It Works / Fun Tricks

1. Fake 3D with CSS

Each block is actually three div: a top, a right side and a left side. To make the whole thing look 3D, the entire stack is rotated with rotateX and rotateZ. Then each block is pushed "upward" using translateZ based on its position in the stack — block 0 sits at the bottom, block 5 floats higher up, and so on. And what's cool is that we have a bunch of CSS variables to control the size of the blocks, the rotation angles, the sliding distance, and the animation speed.

.block {
  --z: calc(var(--i) * var(--stack-step));
  transform: rotateX(var(--rotate-x)) rotateZ(var(--rotate-z))
    translateX(calc(var(--ox) + var(--slide-x)))
    translateY(calc(var(--oy) + var(--slide-y))) translateZ(var(--z));
}

.block .top {
  inset: 0;
}
.block .front-right {
  top: 100%;
  height: var(--block-h);
  transform-origin: top;
  transform: rotateX(-90deg);
}
.block .front-left {
  width: var(--block-h);
  height: var(--bh);
  transform-origin: left;
  transform: rotateY(90deg);
}

2. The Floor Grid Is Just Two Gradients

No grid elements, no SVG lines. The floor is a single div with two linear-gradient backgrounds stacked on top of each other, one draws the vertical lines, the other draws the horizontal ones. background-size controls the spacing.

.grid {
  width: calc(var(--block-l) * 9);
  height: calc(var(--block-w) * 9);
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.4) 2px, transparent 0px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 2px, transparent 0px);
  background-size: var(--block-l) var(--block-w);
  background-position: center center;
}

3. Alternating Slide Direction with Pure CSS

Blocks alternate between sliding left-right and front-back. Instead of toggling this in JavaScript, the game uses CSS :has() to check whether the tower currently has an odd or even number of blocks, then applies the matching animation.

.tower:has(.block:last-child:nth-child(odd)) .block:first-child {
  animation: first-block-y var(--block-slide-speed) ease-in-out infinite
    alternate;
}

.tower:has(.block:last-child:nth-child(even)) .block:first-child {
  animation: first-block-x var(--block-slide-speed) ease-in-out infinite
    alternate;
}

4. Haptic Feedback

Android makes the vibration easy with navigator.vibrate(). But iOS doesn't support the Vibration API. iPhones vibrate when you tap a checkbox, so we simply dispatch a click event on a hidden checkbox to trigger the vibration.

const triggerHaptic = (isIOS, times = 1, durationMs = 0, delayMs = 0) => {
  const trigger = document.getElementById("haptic-trigger");

  if (isIOS) {
    for (let pulse = 0; pulse < times; pulse += 1) {
      setTimeout(() => {
        trigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
      }, pulse * delayMs);
    }
    return;
  }

  if ("vibrate" in navigator) {
    const pattern = [];
    for (let pulse = 0; pulse < times; pulse += 1)
      pattern.push(durationMs, delayMs);
    pattern.pop();
    navigator.vibrate(pattern);
  }
};

5. Live Score in the Browser Tab Favicon

The favicon updates to show your current score in a seven-segment display style

On the JS side every time the score changes, we swap the favicon URL to point at a PHP script with the new score.

const updateFavicon = (score) => {
  const faviconLink = document.querySelector('link[rel="icon"]');
  const faviconUrl = new URL("favicon.php", window.location.href);
  faviconUrl.searchParams.set("score", String(Math.max(0, Math.floor(score))));
  faviconLink.href = faviconUrl.toString();
};

On the PHP side, the script reads the score, draws seven-segment digit shapes, and returns an SVG image on the fly.

<?php
header('Content-Type: image/svg+xml; charset=UTF-8');
header('Cache-Control: no-store, max-age=0');
$score = (string) (int) ($_GET['score'] ?? 0);
$digits = str_split($score);
echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 48">';
// render white seven-segment bars per digit
echo '</svg>';

Deployments

  • GitHub Pages: Just fork this repo and enable GitHub Pages in the repository settings. Note that the dynamic favicon generation won't work since GitHub Pages doesn't support PHP.
  • PHP Hosting: For full functionality, deploy to any hosting that supports PHP.
  • Local: Use Docker to spin up a quick PHP server with the project directory mounted:
  docker run -it --rm -p 8080:80 -v "$PWD":/var/www/html php:8.2-apache

Contributing

Contributions are welcome! If you'd like to improve the project, whether it's a weird CSS trick or just making it more absurd, feel free to open a PR.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

A tower stacking game where every technical decision is slightly dumb

Topics

Resources

License

Stars

Watchers

Forks

Contributors