Skip to the content

Capsid menu

app/enhance/plate.ts

75 lines and 11 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of plate.ts
FunctionLinesComplexityCoveredCRAPMutation
light12 to 143No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:26>26 to 282No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:29>29 to 312No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:32>32 to 352No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
drawLeaders43 to 611No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:44>44 to 601No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:45>45 to 593No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:64>64 to 641No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
onScreen66 to 662No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:72>72 to 762No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:73>73 to 731No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code

Imports

It imports no other module of this repo. No module of this repo imports it.

Source

const LIT = "is-lit";

const byId = new Map<string, Element[]>();
for (const el of document.querySelectorAll("[data-plaque]")) {
  const id = el.getAttribute("data-plaque") ?? "";
  byId.set(id, [...(byId.get(id) ?? []), el]);
}

/** A tap keeps its plaque lit until another tap, so a touch reader can look from one to the other. */
let pinned: string | null = null;

function light(id: string | null) {
  for (const [key, els] of byId) for (const el of els) el.classList.toggle(LIT, key === id);
}

for (const el of document.querySelectorAll<HTMLElement | SVGGElement>(".plate-plaque, .plate-key-entry")) {
  const id = el.getAttribute("data-plaque");
  if (!id) continue;
  /*
   * No focus stops: lighting is a pointer's way of matching a plaque to its key entry, and the key
   * already pairs them in text. Twelve stops on things that are not controls cost a keyboard reader
   * more than the highlight gives. The attribute is only the stylesheet's pointer cue.
   */
  el.setAttribute("data-plate-live", "");
  // Hover is a mouse's preview; a touch lands as a click below, which pins instead.
  el.addEventListener("pointerenter", (e) => {
    if ((e as PointerEvent).pointerType === "mouse") light(id);
  });
  el.addEventListener("pointerleave", (e) => {
    if ((e as PointerEvent).pointerType === "mouse") light(pinned);
  });
  el.addEventListener("click", () => {
    pinned = pinned === id ? null : id;
    light(pinned);
  });
}

/*
 * `fill: "backwards"` on purpose: when the animation ends the line drops back to its base style, a
 * real style change that forces a repaint. A CSS animation holding its end state left the leaders
 * unpainted in Chrome.
 */
function drawLeaders(svg: Element) {
  svg.querySelectorAll(".plate-leader").forEach((leader, i) => {
    leader.querySelectorAll<SVGLineElement>("line").forEach((line, j) => {
      const len = String(Math.ceil(line.getTotalLength()));
      line.animate(
        [
          { strokeDasharray: len, strokeDashoffset: len },
          { strokeDasharray: len, strokeDashoffset: "0" },
        ],
        {
          duration: j === 0 ? 600 : 260,
          delay: i * 260 + (j === 0 ? 200 : 800),
          easing: "cubic-bezier(0.2, 0.7, 0.2, 1)",
          fill: "backwards",
        },
      );
    });
  });
}

const figure = document.querySelector(".home-plate");
const shown = figure && [...figure.querySelectorAll("svg.plate")].find((s) => s.getBoundingClientRect().width > 0);
if (shown && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
  const onScreen = (r: DOMRect) => r.top < window.innerHeight && r.bottom > 0;
  if (onScreen(shown.getBoundingClientRect())) {
    // Already in view: start now, before the next paint, so the drawn plate never flashes first.
    drawLeaders(shown);
  } else if ("IntersectionObserver" in window) {
    const observer = new IntersectionObserver(
      (entries) => {
        if (!entries.some((e) => e.isIntersecting)) return;
        observer.disconnect();
        drawLeaders(shown);
      },
      { threshold: 0.25 },
    );
    observer.observe(shown);
  }
}