Hall of Fame
+ ++ Celebrating the key contributors whose efforts helped build, refine, and expand Img2Num. Featured members are hand-picked by the maintainers. See everyone on the{" "} + + full contributors list + + . +
+diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h
index d01e5a2ad..6fdbd2a40 100644
--- a/core/include/internal/gpu.h
+++ b/core/include/internal/gpu.h
@@ -189,14 +189,13 @@ class GPU {
cleanup();
return;
}
- if (wgpu::AdapterInfo info;
- adapter.GetInfo(&info) != wgpu::Status::Success ||
- info.backendType == wgpu::BackendType::Null) {
+ if (wgpu::AdapterInfo info; adapter.GetInfo(&info) != wgpu::Status::Success ||
+ info.backendType == wgpu::BackendType::Null) {
IMG2NUM_LOG_INFO("No usable WebGPU adapter found. Falling back to CPU.");
cleanup();
return;
}
-
+
// ---------------------------------------------------------
// 2. Get Device
// ---------------------------------------------------------
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 0b058d6be..20b2a9638 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -193,6 +193,7 @@ const config = {
},
],
"./plugins/github-stats/index.js",
+ "./plugins/hallOfFame/index.js",
[
"docusaurus-plugin-copy-page-button",
{
diff --git a/docs/lib/hallOfFame.js b/docs/lib/hallOfFame.js
new file mode 100644
index 000000000..45c3a2f22
--- /dev/null
+++ b/docs/lib/hallOfFame.js
@@ -0,0 +1,109 @@
+/*
+ *IMPORTANT: GitHub rate limits unauthenticated requests, so
+ *this may break during development. To fix it, authenticate manually
+ */
+
+import hallOfFameInput from "../src/data/hall-of-fame.json";
+
+const REPO = "Ryan-Millard/Img2Num";
+const API = "https://api.github.com";
+const PER_PAGE = 50;
+const MAX_PAGES = 10;
+
+const isBot = (user) => user?.type === "Bot" || user?.login?.endsWith("[bot]");
+
+const withAvatarSize = (url) => (url ? `${url}${url.includes("?") ? "&" : "?"}s=96` : null);
+
+async function fetchAllContributors(headers) {
+ const all = [];
+
+ try {
+ for (let page = 1; page <= MAX_PAGES; page++) {
+ const res = await fetch(`${API}/repos/${REPO}/contributors?per_page=${PER_PAGE}&page=${page}`, { headers });
+ if (!res.ok) break;
+
+ const batch = await res.json();
+ if (!Array.isArray(batch) || batch.length === 0) break;
+
+ all.push(...batch);
+ if (batch.length < PER_PAGE) break;
+ }
+ } catch {}
+
+ return all
+ .filter((c) => !isBot(c))
+ .map((c) => ({
+ username: c.login,
+ avatarUrl: withAvatarSize(c.avatar_url),
+ profileUrl: c.html_url,
+ contributions: c.contributions,
+ }));
+}
+
+async function fetchFeaturedMember(entry, headers) {
+ const fallback = {
+ username: entry.username,
+ name: entry.username,
+ avatarUrl: null,
+ profileUrl: `https://github.com/${entry.username}`,
+ year: entry.year,
+ blurb: entry.blurb || "",
+ mergedPRCount: null,
+ recentPRs: [],
+ };
+
+ try {
+ const userRes = await fetch(`${API}/users/${entry.username}`, { headers });
+ if (!userRes.ok) return fallback;
+
+ const userData = await userRes.json();
+ if (isBot(userData)) return null;
+
+ const user = {
+ ...fallback,
+ username: userData.login || entry.username,
+ name: userData.name || userData.login || entry.username,
+ avatarUrl: withAvatarSize(userData.avatar_url),
+ profileUrl: userData.html_url || fallback.profileUrl,
+ };
+
+ const searchUrl = `${API}/search/issues?q=repo:${REPO}+type:pr+author:${entry.username}+is:merged&sort=created&order=desc&per_page=5`;
+ const prRes = await fetch(searchUrl, { headers });
+
+ if (prRes.ok) {
+ const prData = await prRes.json();
+ user.mergedPRCount = prData.total_count ?? 0;
+ user.recentPRs = (prData.items || []).map((pr) => ({
+ title: pr.title,
+ url: pr.html_url,
+ number: pr.number,
+ }));
+ }
+
+ return user;
+ } catch {
+ return fallback;
+ }
+}
+
+export async function fetchHallOfFameData({ token } = {}) {
+ const headers = {
+ "User-Agent": "Img2Num-Docs",
+ Accept: "application/vnd.github+json",
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ };
+
+ const [contributors, featuredRaw] = await Promise.all([fetchAllContributors(headers), Promise.all(hallOfFameInput.map((e) => fetchFeaturedMember(e, headers)))]);
+
+ // Group featured members by year, newest first
+ const grouped = featuredRaw.filter(Boolean).reduce((acc, item) => {
+ (acc[item.year] ||= []).push(item);
+ return acc;
+ }, {});
+
+ const featured = Object.keys(grouped)
+ .sort((a, b) => Number(b) - Number(a))
+ .map((year) => ({ year: Number(year), members: grouped[year] }));
+
+ return { featured, contributors };
+}
diff --git a/docs/plugins/hallOfFame/index.js b/docs/plugins/hallOfFame/index.js
new file mode 100644
index 000000000..70546f5ef
--- /dev/null
+++ b/docs/plugins/hallOfFame/index.js
@@ -0,0 +1,17 @@
+import { fetchHallOfFameData } from "../../lib/hallOfFame.js";
+
+export default function hallOfFamePlugin() {
+ return {
+ name: "hall-of-fame",
+
+ async loadContent() {
+ const token = process.env.GITHUB_TOKEN;
+
+ return await fetchHallOfFameData({ token });
+ },
+
+ async contentLoaded({ content, actions }) {
+ actions.setGlobalData(content);
+ },
+ };
+}
diff --git a/docs/src/data/hall-of-fame.json b/docs/src/data/hall-of-fame.json
new file mode 100644
index 000000000..e8c7c786c
--- /dev/null
+++ b/docs/src/data/hall-of-fame.json
@@ -0,0 +1,42 @@
+[
+ {
+ "username": "08abhinav",
+ "year": 2026,
+ "blurb": "Built this page"
+ },
+ {
+ "username": "siddharth1407",
+ "year": 2026,
+ "blurb": "Built a reusable WebGPU helper layer, ported both kernels to it, and documented gpu_utils"
+ },
+ {
+ "username": "hjmillard",
+ "year": 2025,
+ "blurb": "Wrote the original K-means and thresholding utilities and later split K-means into its own module"
+ },
+ {
+ "username": "fransafu",
+ "year": 2025,
+ "blurb": "Collaborated with @Krasner on the first implementation of the bilateral filter"
+ },
+ {
+ "username": "krasner",
+ "year": 2025,
+ "blurb": "Turned Img2Num into what it is today"
+ },
+ {
+ "username": "Prachi-Gupta2808",
+ "year": 2026,
+ "blurb": "Did a lot...★★★"
+ },
+ {
+ "username": "dougwithseismic",
+ "year": "2025",
+ "blurb": "Set up the Vitest unit-testing infrastructure and co-authored the Docker/CMake cross-platform build"
+ },
+ {
+ "username": "Ryan-Millard",
+ "year": 2025,
+ "blurb": "Did nothing"
+ }
+]
diff --git a/docs/src/pages/hall-of-fame.jsx b/docs/src/pages/hall-of-fame.jsx
new file mode 100644
index 000000000..2e95a3a20
--- /dev/null
+++ b/docs/src/pages/hall-of-fame.jsx
@@ -0,0 +1,303 @@
+import React from "react";
+import Layout from "@theme/Layout";
+import { usePluginData } from "@docusaurus/useGlobalData";
+import { GitPullRequest, ExternalLink, GitCommit } from "lucide-react";
+
+const CONTRIBUTORS_URL = "https://github.com/Ryan-Millard/Img2Num/graphs/contributors";
+
+/**
+ * Each featured card has 4 stacked parts: header, blurb, PR count, PR list.
+ * Cards use CSS subgrid across these rows, so the same part lines up across
+ * every card in a row regardless of how much content each card has.
+ */
+const ROWS_PER_CARD = 4;
+
+const cardBase = {
+ backgroundColor: "var(--ifm-card-background-color)",
+ border: "1px solid var(--ifm-toc-border-color)",
+ borderRadius: "12px",
+ boxShadow: "var(--ifm-global-shadow-lw)",
+};
+
+const sectionHeadingStyle = {
+ fontSize: "1.75rem",
+ borderBottom: "2px solid var(--ifm-toc-border-color)",
+ paddingBottom: "0.5rem",
+ marginBottom: "1.5rem",
+};
+
+// Year groups sit side by side when there is room (e.g. 2026 | 2025), and
+// stack on narrower screens, so wide monitors are not left mostly empty.
+const yearsLayoutStyle = {
+ display: "grid",
+ // 664px = two 320px cards + the 24px gap between them
+ gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 664px), 1fr))",
+ columnGap: "clamp(1.5rem, 3vw, 4rem)",
+ alignItems: "start",
+ marginBottom: "2rem",
+};
+
+const featuredGridStyle = {
+ display: "grid",
+ // min(100%, 320px) stops a single column overflowing very narrow screens
+ gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 320px), 1fr))",
+ columnGap: "1.5rem",
+ rowGap: 0, // vertical spacing comes from each card's margin-bottom
+};
+
+const featuredCardStyle = {
+ ...cardBase,
+ padding: "1.5rem",
+ marginBottom: "1.5rem",
+ display: "grid",
+ gridRow: `span ${ROWS_PER_CARD}`,
+ gridTemplateRows: "subgrid",
+ rowGap: 0,
+};
+
+const contributorGridStyle = {
+ display: "grid",
+ gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 260px), 1fr))",
+ gap: "1rem",
+};
+
+/** Round avatar with an initial-letter fallback when no image URL exists. */
+function Avatar({ src, name, size }) {
+ if (src) {
+ return ;
+ }
+
+ return (
+
+ "{member.blurb}" +
+ ) : ( + + )} + + {/* Row 3: merged PR count */} + {member.mergedPRCount !== null ? ( ++ Celebrating the key contributors whose efforts helped build, refine, and expand Img2Num. Featured members are hand-picked by the maintainers. See everyone on the{" "} + + full contributors list + + . +
+