feat: rewrite readme and clean up support

This commit is contained in:
Jet 2026-03-26 21:27:26 -07:00
parent 38af26d959
commit 7758be92b4
No known key found for this signature in database
16 changed files with 383 additions and 453 deletions

View file

@ -1,10 +0,0 @@
---
title: Conway's Game of Life
description: WebAssembly implementation of Conway's Game of Life, running as the background of this website.
---
The background animation on this site is a WebAssembly implementation of
Conway's Game of Life, written in Rust and compiled to WASM.
It runs directly in your browser using the HTML5 Canvas API, simulating
cellular automata in real-time.

20
src/global.d.ts vendored
View file

@ -14,23 +14,3 @@ declare module "*.utf8ans?raw" {
const content: string;
export default content;
}
declare module "virtual:projects" {
interface Project {
slug: string;
title: string;
description: string;
html: string;
}
const projects: Project[];
export default projects;
}
declare module "gray-matter" {
interface GrayMatterResult {
data: Record<string, string>;
content: string;
}
function matter(input: string): GrayMatterResult;
export = matter;
}

115
src/lib/background.ts Normal file
View file

@ -0,0 +1,115 @@
const STORAGE_KEY = "background-motion-preference";
const MOTION_QUERY = "(prefers-reduced-motion: reduce)";
const STILL_STEPS = 5;
type MotionPreference = "auto" | "off" | "on";
type BackgroundMode = "animated" | "still" | "failed";
interface BackgroundActions {
start: () => void;
stop: () => void;
renderStill: (steps: number) => void;
}
function readPreference(): MotionPreference {
const stored = window.localStorage.getItem(STORAGE_KEY);
return stored === "off" || stored === "on" || stored === "auto"
? stored
: "auto";
}
function writePreference(preference: MotionPreference) {
window.localStorage.setItem(STORAGE_KEY, preference);
}
function getMode(
preference: MotionPreference,
reducedMotion: boolean,
): BackgroundMode {
if (preference === "on") return "animated";
if (preference === "off") return "still";
return reducedMotion ? "still" : "animated";
}
function applyCanvasState(mode: BackgroundMode) {
const canvas = document.getElementById("canvas");
document.body.dataset.backgroundMode = mode;
if (canvas) {
canvas.toggleAttribute("hidden", mode === "failed");
}
}
function updateControls(
preference: MotionPreference,
mode: BackgroundMode,
reducedMotion: boolean,
) {
const button = document.getElementById(
"background-toggle",
) as HTMLButtonElement | null;
const status = document.getElementById("background-status");
if (!button || !status) return;
button.textContent = `motion ${preference}`;
if (mode === "failed") {
status.textContent = "background unavailable";
return;
}
if (mode === "still") {
status.textContent =
preference === "auto" && reducedMotion
? "still frame"
: "background still";
return;
}
status.textContent = "background live";
}
export function initBackgroundControls(actions: BackgroundActions) {
const media = window.matchMedia(MOTION_QUERY);
let preference = readPreference();
let mode: BackgroundMode = getMode(preference, media.matches);
const applyMode = () => {
mode = getMode(preference, media.matches);
applyCanvasState(mode);
updateControls(preference, mode, media.matches);
if (mode === "animated") {
actions.start();
return;
}
actions.stop();
actions.renderStill(STILL_STEPS);
};
const button = document.getElementById(
"background-toggle",
) as HTMLButtonElement | null;
button?.addEventListener("click", () => {
preference =
preference === "auto" ? "off" : preference === "off" ? "on" : "auto";
writePreference(preference);
applyMode();
});
media.addEventListener("change", () => {
applyMode();
});
return {
applyInitialMode() {
applyMode();
},
setFailed() {
mode = "failed";
applyCanvasState(mode);
updateControls(preference, mode, media.matches);
},
};
}

View file

@ -38,6 +38,9 @@ export function renderFooter() {
<span aria-hidden="true">|</span>
<a href="/ssh.txt" data-native-link>ssh</a>
<span aria-hidden="true">|</span>
<button type="button" id="background-toggle" class="qa-inline-action">motion auto</button>
<span id="background-status" class="qa-meta" aria-live="polite"></span>
<span aria-hidden="true">|</span>
<a href="${mirror.href}">${mirror.label}</a>
</div>
</div>`;

View file

@ -1,6 +1,7 @@
import "~/styles/globals.css";
import init, { start } from "cgol";
import init, { render_still, start, stop } from "cgol";
import { route, initRouter } from "~/router";
import { initBackgroundControls } from "~/lib/background";
import { renderFooter } from "~/lib/site";
import { homePage } from "~/pages/home";
import { qaPage } from "~/pages/qa";
@ -10,12 +11,25 @@ route("/", "Jet Pham - Home", homePage);
route("/qa", "Jet Pham - Q+A", qaPage);
route("*", "404 - Jet Pham", notFoundPage);
renderFooter();
const background = initBackgroundControls({
start() {
start();
},
stop() {
stop();
},
renderStill(steps) {
render_still(steps);
},
});
try {
await init();
start();
background.applyInitialMode();
} catch (e) {
background.setFailed();
console.error("WASM init failed:", e);
}
initRouter();
renderFooter();

View file

@ -46,20 +46,22 @@ export function homePage(outlet: HTMLElement) {
) as HTMLSpanElement;
let resetTimer: number | null = null;
copyButton.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText("jet@extremist.software");
copyStatus.textContent = "copied";
copyStatus.style.color = "var(--light-green)";
} catch {
copyStatus.textContent = "copy failed";
copyStatus.style.color = "var(--light-red)";
}
copyButton.addEventListener("click", () => {
void (async () => {
try {
await navigator.clipboard.writeText("jet@extremist.software");
copyStatus.textContent = "copied";
copyStatus.style.color = "var(--light-green)";
} catch {
copyStatus.textContent = "copy failed";
copyStatus.style.color = "var(--light-red)";
}
if (resetTimer !== null) window.clearTimeout(resetTimer);
resetTimer = window.setTimeout(() => {
copyStatus.textContent = "";
resetTimer = null;
}, 1400);
if (resetTimer !== null) window.clearTimeout(resetTimer);
resetTimer = window.setTimeout(() => {
copyStatus.textContent = "";
resetTimer = null;
}, 1400);
})();
});
}

View file

@ -1,24 +0,0 @@
import projects from "virtual:projects";
import { frostedBox } from "~/components/frosted-box";
export function projectPage(outlet: HTMLElement, params: Record<string, string>) {
const project = projects.find((p) => p.slug === params.slug);
if (!project) {
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start px-4">
${frostedBox(`
<h1 style="color: var(--light-red);">Project not found</h1>
<p class="mt-[1ch]"><a href="/projects">[BACK TO PROJECTS]</a></p>
`)}
</div>`;
return;
}
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start px-4">
${frostedBox(`
<h1 style="color: var(--yellow);">${project.title}</h1>
<div class="project-content mt-[2ch]">${project.html}</div>
`)}
</div>`;
}

View file

@ -1,22 +0,0 @@
import projects from "virtual:projects";
import { frostedBox } from "~/components/frosted-box";
export function projectsPage(outlet: HTMLElement) {
const list = projects
.map(
(p) => `
<li class="mb-[1ch]">
<a href="/projects/${p.slug}" style="color: var(--light-cyan);">${p.title}</a>
<p style="color: var(--light-gray);">${p.description}</p>
</li>`,
)
.join("");
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start px-4">
${frostedBox(`
<h1 style="color: var(--yellow);">Projects</h1>
<ul class="mt-[2ch]">${list}</ul>
`)}
</div>`;
}

View file

@ -347,10 +347,15 @@ a[aria-current="page"] {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 1ch;
color: var(--dark-gray);
}
#background-status {
margin-top: 0;
}
.sr-only {
position: absolute;
width: 1px;
@ -364,39 +369,3 @@ a[aria-current="page"] {
}
/* Project markdown content */
.project-content h2 {
color: var(--light-cyan);
margin-top: 2ch;
margin-bottom: 1ch;
}
.project-content h3 {
color: var(--light-green);
margin-top: 1.5ch;
margin-bottom: 0.5ch;
}
.project-content p {
margin-bottom: 1ch;
}
.project-content ul,
.project-content ol {
margin-left: 2ch;
margin-bottom: 1ch;
}
.project-content code {
color: var(--yellow);
}
.project-content pre {
border: 2px solid var(--dark-gray);
padding: 1ch;
overflow-x: auto;
margin-bottom: 1ch;
}
.project-content a {
color: var(--light-blue);
}