feat: rewrite readme and clean up support
This commit is contained in:
parent
38af26d959
commit
7758be92b4
16 changed files with 383 additions and 453 deletions
|
|
@ -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
20
src/global.d.ts
vendored
|
|
@ -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
115
src/lib/background.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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>`;
|
||||
|
|
|
|||
20
src/main.ts
20
src/main.ts
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
}
|
||||
|
|
@ -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>`;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue