repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

2.4 KB · 78 lines · JavaScript Raw History
 1// The about page: nine skill words stacked down the left, each pinned to a
 2// vertical slot. Every four seconds two of them trade places, wiping out and
 3// back in rather than sliding, so the column never looks like it is shuffling.
 4
 5import { enter, exit } from "../transition.js";
 6
 7const SWAP_MS = 4000;
 8const TRANSITION_MS = 1000;
 9const SLOT_HEIGHT_VH = 11;
10
11const CLASSES = {
12  enter: {
13    from: "about-wordEnter",
14    active: "about-wordEnterActive",
15    done: "about-wordEnterDone",
16  },
17  exit: {
18    from: "about-wordExit",
19    active: "about-wordExitActive",
20    clear: ["about-wordEnterDone", "about-wordAppearDone"],
21  },
22};
23
24export const initAbout = () => {
25  const container = document.querySelector(".about-words");
26  if (!container) return;
27
28  const words = Array.from(container.querySelectorAll(".about-word"));
29  if (words.length < 2) return;
30
31  // Slot index is the word's position down the column. Go renders the initial
32  // slots as inline styles so the column is laid out correctly before any JS
33  // runs; from here the two are kept in sync.
34  const slots = new Map(words.map((el, i) => [el, i]));
35
36  const place = (el) => {
37    el.style.top = `${slots.get(el) * SLOT_HEIGHT_VH}vh`;
38  };
39
40  words.forEach((el) => el.classList.add("about-wordAppearDone"));
41
42  window.setInterval(() => {
43    // Same as the home page word cycle. The work is deferred behind
44    // requestAnimationFrame, which is suspended in a hidden tab, so without
45    // this the queue bursts on return.
46    if (document.hidden) return;
47
48    let a = Math.floor(Math.random() * words.length);
49    let b = Math.floor(Math.random() * words.length);
50    while (a === b) b = Math.floor(Math.random() * words.length);
51
52    const first = words[a];
53    const second = words[b];
54
55    const swap = () => {
56      const slotA = slots.get(first);
57      slots.set(first, slots.get(second));
58      slots.set(second, slotA);
59      place(first);
60      place(second);
61    };
62
63    let done = 0;
64    const afterExit = () => {
65      done += 1;
66      // Both have to be gone before either moves, otherwise the one that
67      // finishes first jumps while the other is still visible in its old slot.
68      if (done < 2) return;
69      swap();
70      enter(first, CLASSES.enter, TRANSITION_MS);
71      enter(second, CLASSES.enter, TRANSITION_MS);
72    };
73
74    exit(first, CLASSES.exit, TRANSITION_MS, afterExit);
75    exit(second, CLASSES.exit, TRANSITION_MS, afterExit);
76  }, SWAP_MS);
77};