orchard
mirrorEvery 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
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};