Analog digits

Current time:
inspired by a youtube video

Each digit of the current time is drawn as a 4×6 grid of mini clocks. The two hands on each cell rotate to form corners, edges, or straights, mapping to the box-drawing characters that make up the digit shape.

The digit patterns are stored as flat arrays of 24 box-drawing characters (┌ ┐ └ ┘ - |), and each character maps to a pair of rotation angles for the two hands. Every second, the angles update and CSS transitions do the rest.

astro
<section
  class="clock-clock"
  aria-label="Digital time displayed using analog clock hands"
>
  <div class="sr-only" aria-live="polite">
    Current time: <span id="accessible-time"></span>
  </div>

  <div id="clock-container" aria-hidden="true">
    {
      ["hours", "minutes", "seconds"].map((unit) => (
        <div class={`unit ${unit}`}>
          {[0, 1].map(() => (
            <div class="digit">
              {Array.from({ length: 24 }).map(() => (
                <div class="cell">
                  <div class="hand" />
                  <div class="hand" />
                </div>
              ))}
            </div>
          ))}
        </div>
      ))
    }
  </div>
</section>

<script>
  // prettier-ignore
  const DIGITS = {
    "0": [
      "┌", "-", "-", "┐",
      "|", "┌", "┐", "|",
      "|", "|", "|", "|",
      "|", "|", "|", "|",
      "|", "└", "┘", "|",
      "└", "-", "-", "┘",
    ],

    "1": [
      "┌", "-", "┐", " ",
      "└", "┐", "|", " ",
      " ", "|", "|", " ",
      " ", "|", "|", " ",
      "┌", "┘", "└", "┐",
      "└", "-", "-", "┘",
    ],

    "2": [
      "┌", "-", "-", "┐",
      "└", "-", "┐", "|",
      "┌", "-", "┘", "|",
      "|", "┌", "-", "┘",
      "|", "└", "-", "┐",
      "└", "-", "-", "┘",
    ],

    "3": [
      "┌", "-", "-", "┐",
      "└", "-", "┐", "|",
      " ", "┌", "┘", "|",
      " ", "└", "┐", "|",
      "┌", "-", "┘", "|",
      "└", "-", "-", "┘",
    ],

    "4": [
      "┌", "┐", "┌", "┐",
      "|", "|", "|", "|",
      "|", "└", "┘", "|",
      "└", "-", "┐", "|",
      " ", " ", "|", "|",
      " ", " ", "└", "┘",
    ],

    "5": [
      "┌", "-", "-", "┐",
      "|", "┌", "-", "┘",
      "|", "└", "-", "┐",
      "└", "-", "┐", "|",
      "┌", "-", "┘", "|",
      "└", "-", "-", "┘",
    ],

    "6": [
      "┌", "-", "-", "┐",
      "|", "┌", "-", "┘",
      "|", "└", "-", "┐",
      "|", "┌", "┐", "|",
      "|", "└", "┘", "|",
      "└", "-", "-", "┘",
    ],

    "7": [
      "┌", "-", "-", "┐",
      "└", "-", "┐", "|",
      " ", " ", "|", "|",
      " ", " ", "|", "|",
      " ", " ", "|", "|",
      " ", " ", "└", "┘",
    ],

    "8": [
      "┌", "-", "-", "┐",
      "|", "┌", "┐", "|",
      "|", "└", "┘", "|",
      "|", "┌", "┐", "|",
      "|", "└", "┘", "|",
      "└", "-", "-", "┘",
    ],

    "9": [
      "┌", "-", "-", "┐",
      "|", "┌", "┐", "|",
      "|", "└", "┘", "|",
      "└", "-", "┐", "|",
      "┌", "-", "┘", "|",
      "└", "-", "-", "┘",
    ]
  }

  const rotation = {
    " ": [135, 135],
    "┘": [180, 270],
    "└": [0, 270],
    "┐": [90, 180],
    "┌": [0, 90],
    "-": [0, 180],
    "|": [90, 270],
  };

  function updateClock() {
    const now = new Date();
    const hours = now.getHours().toString().padStart(2, "0");
    const minutes = now.getMinutes().toString().padStart(2, "0");
    const seconds = now.getSeconds().toString().padStart(2, "0");

    const timeString = hours + minutes + seconds;
    const digitElements = document.querySelectorAll(".digit");
    const accessibleTime = document.getElementById("accessible-time");

    if (accessibleTime) {
      accessibleTime.textContent = `${hours}:${minutes}:${seconds}`;
    }

    timeString.split("").forEach((char, digitIndex) => {
      const pattern = DIGITS[char as keyof typeof DIGITS];
      if (!pattern) return;

      const digitEl = digitElements[digitIndex];
      if (!digitEl) return;

      const cells = digitEl.querySelectorAll(".cell");

      cells.forEach((cell, cellIndex) => {
        const symbol = pattern[cellIndex];
        const angles = rotation[symbol as keyof typeof rotation] || [135, 135];
        const hands = cell.querySelectorAll(".hand");

        if (hands[0])
          (hands[0] as HTMLElement).style.rotate = `${angles[0]}deg`;
        if (hands[1])
          (hands[1] as HTMLElement).style.rotate = `${angles[1]}deg`;
      });
    });
  }

  updateClock();
  setInterval(updateClock, 1000);
</script>

<style>
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
  section {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: clamp(16px, 4vw, 32px);
    margin-bottom: 0px;
    overflow: hidden;
    position: relative;
  }
  #clock-container {
    width: 100%;
    display: flex;
    gap: 4.5%; /* Relative gap between units */
    justify-content: center;
    align-items: center;
  }
  .unit {
    display: flex;
    gap: 1.5%; /* Relative gap between digits */
    flex: 1;
    max-width: 30%;
  }
  .digit {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1px;
    flex: 1;
  }
  .cell {
    width: 100%;
    aspect-ratio: 1;
    border: 1px solid var(--border-muted);
    border-radius: 50%;
    background-color: var(--bg-soft);
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .hand {
    width: 50%;
    height: 1.5px;
    background-color: var(--text-muted);
    position: absolute;
    left: 50%;
    transform-origin: center left;
    transition: rotate 500ms cubic-bezier(0.4, 0, 0.2, 1);
    border-radius: 1px;
  }
</style>