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

4.2 KB · 146 lines · JavaScript Raw History
  1// The art page: a lightbox over the acrylic pours, and three generative canvas
  2// pieces that play one at a time.
  3
  4import { constellations } from "../canvas/constellations.js";
  5import { retrostars } from "../canvas/retrostars.js";
  6import { slimemold } from "../canvas/slimemold.js";
  7
  8const FACTORIES = {
  9  constellations: (cvs) => constellations(cvs, { numStars: 50 }),
 10  retrostars: (cvs) => retrostars(cvs, { numStars: 50 }),
 11  slimemold: (cvs) => slimemold(cvs, {}),
 12};
 13
 14const initCanvases = () => {
 15  const running = new Map();
 16  let active = null;
 17
 18  const instanceFor = (name, cvs) => {
 19    // Built on first play, since slime mold alone allocates two Float32Arrays
 20    // over the whole grid plus 6000 agents.
 21    if (!running.has(name)) running.set(name, FACTORIES[name](cvs));
 22    return running.get(name);
 23  };
 24
 25  const setActive = (name) => {
 26    if (active && active !== name) {
 27      const previous = running.get(active);
 28      if (previous) previous.stop();
 29      const button = document.querySelector(`[data-art-toggle="${active}"]`);
 30      if (button) {
 31        button.dataset.active = "false";
 32        button.textContent = "▶";
 33      }
 34    }
 35    active = name;
 36  };
 37
 38  document.querySelectorAll("[data-art-toggle]").forEach((button) => {
 39    const name = button.dataset.artToggle;
 40    const cvs = document.querySelector(`[data-art-canvas="${name}"]`);
 41    if (!cvs || !FACTORIES[name]) return;
 42
 43    const play = () => {
 44      setActive(name);
 45      instanceFor(name, cvs).start();
 46      button.dataset.active = "true";
 47      button.textContent = "⏸";
 48    };
 49
 50    const pause = () => {
 51      const instance = running.get(name);
 52      if (instance) instance.stop();
 53      active = null;
 54      button.dataset.active = "false";
 55      button.textContent = "▶";
 56    };
 57
 58    button.addEventListener("click", () => {
 59      if (button.dataset.active === "true") pause();
 60      else play();
 61    });
 62
 63    if (button.dataset.autoplay === "true") play();
 64  });
 65};
 66
 67const initLightbox = () => {
 68  const overlay = document.querySelector(".art-lightboxOverlay");
 69  if (!overlay) return;
 70
 71  const image = overlay.querySelector(".art-lightboxImage");
 72  const loading = overlay.querySelector(".art-lightboxLoading");
 73  const close = overlay.querySelector(".art-lightboxClose");
 74
 75  const hide = () => {
 76    overlay.hidden = true;
 77    image.classList.remove("art-show");
 78    loading.classList.remove("art-hide");
 79    image.removeAttribute("src");
 80    document.body.style.overflowY = "scroll";
 81  };
 82
 83  const show = (src, alt) => {
 84    image.classList.remove("art-show");
 85    loading.classList.remove("art-hide");
 86    image.alt = alt || "";
 87    image.src = src;
 88    overlay.hidden = false;
 89    document.body.style.overflowY = "hidden";
 90
 91    // A cached image can be complete before the load listener attaches, and
 92    // then the event never fires and the spinner sits over a decoded picture.
 93    if (image.complete && image.naturalWidth > 0) revealed();
 94  };
 95
 96  const revealed = () => {
 97    image.classList.add("art-show");
 98    loading.classList.add("art-hide");
 99  };
100
101  image.addEventListener("load", revealed);
102  image.addEventListener("error", revealed);
103
104  document.querySelectorAll("[data-lightbox]").forEach((item) => {
105    item.addEventListener("click", () => {
106      show(item.dataset.lightbox, item.dataset.lightboxAlt);
107    });
108  });
109
110  overlay.addEventListener("click", hide);
111  close.addEventListener("click", (e) => {
112    e.stopPropagation();
113    hide();
114  });
115  overlay
116    .querySelector(".art-lightboxImageWrapper")
117    .addEventListener("click", (e) => e.stopPropagation());
118
119  document.addEventListener("keydown", (e) => {
120    if (e.key === "Escape" && !overlay.hidden) hide();
121  });
122};
123
124const initCardFades = () => {
125  // Anything already cached is marked at once, or it sits invisible waiting for
126  // a load event that will not fire.
127  document.querySelectorAll(".art-cardImage img").forEach((img) => {
128    if (img.complete && img.naturalWidth > 0) {
129      img.classList.add("art-imgLoaded");
130      return;
131    }
132    img.addEventListener("load", () => img.classList.add("art-imgLoaded"), {
133      once: true,
134    });
135    img.addEventListener("error", () => img.classList.add("art-imgLoaded"), {
136      once: true,
137    });
138  });
139};
140
141export const initArt = () => {
142  initCardFades();
143  initLightbox();
144  initCanvases();
145};