flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until October 2026, serving four audiences from a single dated list, which is the only format that can hold unrelated things at once. flux.vision is recovered from the Wayback Machine, where it had sat since being deleted in 2021: 90 articles, closed and maintained rather than updated. tobias.raayoni-last.com is the professional record, 97 articles back to 1999. randogoth.com is the technical work, 41 articles across six areas, served as an ANSI bulletin board. History starts here deliberately. The development history, the 266-row inventory that records why each article went where it did, the Wayback pipelines, the pruned articles, the source archives and the full-size images are kept beside the repo rather than in it, so a clone is the web presence and not the argument that produced it. REFACTOR.md is gitignored for the same reason. See README.md for where that material lives, and keep it: the sort is not reproducible from these three directories alone. Each site documents itself in its own README, including the traps that cost time and look like nothing when you get them wrong.
169 lines
7.2 KiB
JavaScript
169 lines
7.2 KiB
JavaScript
/* raayoni — grid, filtering, sorting, light/dark.
|
||
*
|
||
* The grid is Muuri (muuri.dev), as on dominofactory.net. Filtering is by
|
||
* discipline (Category) and by engagement (Project) — two independent axes, so
|
||
* they compose. Sorting is by year, which spans 2000–2022.
|
||
*/
|
||
(function () {
|
||
"use strict";
|
||
|
||
/* ── light / dark ──────────────────────────────────────────────────────
|
||
* The initial value is applied by an inline script in <head> so there is no
|
||
* flash of the wrong ground colour before this file loads. Here we only
|
||
* handle the toggle and keep the button label honest.
|
||
*/
|
||
var root = document.documentElement;
|
||
|
||
function currentTheme() {
|
||
return root.getAttribute("data-theme") ||
|
||
(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
|
||
}
|
||
|
||
function paintToggle(btn) {
|
||
if (!btn) return;
|
||
var dark = currentTheme() === "dark";
|
||
btn.textContent = dark ? "light" : "dark";
|
||
btn.setAttribute("aria-label", "Switch to " + (dark ? "light" : "dark") + " mode");
|
||
}
|
||
|
||
var toggle = document.querySelector("[data-theme-toggle]");
|
||
paintToggle(toggle);
|
||
if (toggle) {
|
||
toggle.addEventListener("click", function () {
|
||
var next = currentTheme() === "dark" ? "light" : "dark";
|
||
root.setAttribute("data-theme", next);
|
||
try { localStorage.setItem("theme", next); } catch (e) { /* private mode */ }
|
||
paintToggle(toggle);
|
||
});
|
||
}
|
||
|
||
|
||
/* ── contact address ──────────────────────────────────────────────────────
|
||
* The address is stored rot13'd, so the page source contains no string an
|
||
* email regex will match, and is written out as a real mailto only once the
|
||
* document is live. Without JS the visible fallback stays human-readable
|
||
* ("tobias at raayoni-last dot com") rather than silently breaking.
|
||
*
|
||
* This stops regex harvesters, not a scraper that executes JS. The previous
|
||
* version used document.write inside Markdown, which rendered its own source
|
||
* as visible text and was neither clickable nor hidden.
|
||
*/
|
||
Array.prototype.forEach.call(document.querySelectorAll(".mail[data-r]"), function (el) {
|
||
var addr = el.getAttribute("data-r").replace(/[a-zA-Z]/g, function (c) {
|
||
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
||
});
|
||
var a = document.createElement("a");
|
||
a.href = "mailto:" + addr;
|
||
a.textContent = addr;
|
||
el.replaceWith(a);
|
||
});
|
||
|
||
/* ── the grid ─────────────────────────────────────────────────────────── */
|
||
var el = document.querySelector(".grid");
|
||
if (!el || typeof Muuri === "undefined") return;
|
||
|
||
var grid = new Muuri(el, {
|
||
items: ".item",
|
||
layout: { fillGaps: false, rounding: true },
|
||
layoutDuration: 280,
|
||
layoutEasing: "cubic-bezier(.2,.7,.3,1)",
|
||
sortData: {
|
||
year: function (item, element) {
|
||
return parseInt(element.getAttribute("data-year"), 10) || 0;
|
||
},
|
||
title: function (item, element) {
|
||
return (element.getAttribute("data-title") || "").toLowerCase();
|
||
}
|
||
}
|
||
});
|
||
|
||
/* Tiles are images of wildly varying size fetched from an archive, so the
|
||
first layout is usually wrong until they have decoded. Relayout as they
|
||
arrive rather than guessing dimensions up front. */
|
||
Array.prototype.forEach.call(el.querySelectorAll("img"), function (img) {
|
||
if (img.complete) return;
|
||
img.addEventListener("load", relayout, { once: true });
|
||
img.addEventListener("error", function () {
|
||
img.closest(".hoverthumb").classList.add("empty");
|
||
relayout();
|
||
}, { once: true });
|
||
});
|
||
var pending = null;
|
||
function relayout() {
|
||
clearTimeout(pending);
|
||
pending = setTimeout(function () { grid.refreshItems().layout(); }, 60);
|
||
}
|
||
window.addEventListener("load", relayout);
|
||
|
||
/* ── grouping, filtering, sorting ─────────────────────────────────────── */
|
||
/* Headers are grid items too, so they must be hidden whenever the grid is
|
||
not in its grouped arrangement — a filtered or re-sorted grid would
|
||
otherwise strand them above the wrong tiles. */
|
||
function grouped(on) {
|
||
if (on) { el.setAttribute("data-grouped", "true"); }
|
||
else { el.removeAttribute("data-grouped"); }
|
||
}
|
||
function isGrouped() { return el.hasAttribute("data-grouped"); }
|
||
|
||
var active = { category: "", project: "" };
|
||
var countEl = document.querySelector("[data-count]");
|
||
|
||
function apply() {
|
||
var filtering = !!(active.category || active.project);
|
||
if (filtering) grouped(false);
|
||
grid.filter(function (item) {
|
||
var e = item.getElement();
|
||
if (e.classList.contains("head")) return isGrouped() && !filtering;
|
||
var okCat = !active.category || e.getAttribute("data-category") === active.category;
|
||
var okPrj = !active.project || e.getAttribute("data-project") === active.project;
|
||
return okCat && okPrj;
|
||
});
|
||
if (countEl) {
|
||
var n = grid.getItems().filter(function (i) {
|
||
return i.isVisible() && !i.getElement().classList.contains("head");
|
||
}).length;
|
||
countEl.textContent = n + (n === 1 ? " entry" : " entries");
|
||
}
|
||
}
|
||
|
||
Array.prototype.forEach.call(document.querySelectorAll("[data-filter]"), function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
var axis = btn.getAttribute("data-axis") || "category";
|
||
var val = btn.getAttribute("data-filter");
|
||
active[axis] = active[axis] === val ? "" : val; /* click again to clear */
|
||
document.querySelectorAll('[data-axis="' + axis + '"]').forEach(function (b) {
|
||
b.setAttribute("aria-pressed", String(b.getAttribute("data-filter") === active[axis]));
|
||
});
|
||
apply();
|
||
});
|
||
});
|
||
|
||
/* ── sorting ──────────────────────────────────────────────────────────── */
|
||
Array.prototype.forEach.call(document.querySelectorAll("[data-sort]"), function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
var how = btn.getAttribute("data-sort");
|
||
if (how === "group") {
|
||
active.category = active.project = "";
|
||
document.querySelectorAll("[data-filter]").forEach(function (b) {
|
||
b.setAttribute("aria-pressed", "false");
|
||
});
|
||
grouped(true);
|
||
grid.sort(function (a, b) { return a._dom - b._dom; }); /* DOM order */
|
||
} else {
|
||
grouped(false);
|
||
grid.sort(how === "year-asc" ? "year:asc" : how === "title" ? "title:asc" : "year:desc");
|
||
}
|
||
document.querySelectorAll("[data-sort]").forEach(function (b) {
|
||
b.setAttribute("aria-pressed", String(b === btn));
|
||
});
|
||
apply();
|
||
});
|
||
});
|
||
|
||
/* The grid is rendered grouped by engagement so that arrangement can be
|
||
restored from DOM order; newest is the default view, so sort on init. */
|
||
grid.getItems().forEach(function (item, i) { item._dom = i; });
|
||
grid.sort("year:desc");
|
||
|
||
apply();
|
||
})();
|