summaryrefslogtreecommitdiff
path: root/apps/web/src/fonts/google-fonts.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/fonts/google-fonts.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/fonts/google-fonts.ts')
-rw-r--r--apps/web/src/fonts/google-fonts.ts88
1 files changed, 88 insertions, 0 deletions
diff --git a/apps/web/src/fonts/google-fonts.ts b/apps/web/src/fonts/google-fonts.ts
new file mode 100644
index 00000000..36fca068
--- /dev/null
+++ b/apps/web/src/fonts/google-fonts.ts
@@ -0,0 +1,88 @@
+import type { FontAtlas } from "@/fonts/types";
+import { SYSTEM_FONTS } from "@/fonts/system-fonts";
+
+const GOOGLE_FONTS_CSS = "https://fonts.googleapis.com/css2";
+const FONT_ATLAS_PATH = "/fonts/font-atlas.json";
+const FONT_CHUNK_PATH_PREFIX = "/fonts/font-chunk-";
+
+const fullLoaded = new Set<string>();
+
+let cachedAtlas: FontAtlas | null = null;
+let atlasFetchPromise: Promise<FontAtlas | null> | null = null;
+
+function encodeGoogleFontsFamily(family: string): string {
+ return family.replace(/ /g, "+");
+}
+
+export function getCachedFontAtlas(): FontAtlas | null {
+ return cachedAtlas;
+}
+
+export function clearFontAtlasCache(): void {
+ cachedAtlas = null;
+ atlasFetchPromise = null;
+ fullLoaded.clear();
+}
+
+export function loadFontAtlas(): Promise<FontAtlas | null> {
+ if (cachedAtlas) return Promise.resolve(cachedAtlas);
+ if (atlasFetchPromise) return atlasFetchPromise;
+
+ atlasFetchPromise = fetch(FONT_ATLAS_PATH)
+ .then(async (response) => {
+ if (!response.ok) return null;
+ const data: FontAtlas = await response.json();
+ cachedAtlas = data;
+ preloadChunkImages({ atlas: data });
+ return data;
+ })
+ .catch(() => null);
+
+ return atlasFetchPromise;
+}
+
+function preloadChunkImages({ atlas }: { atlas: FontAtlas }): void {
+ const maxChunk = Math.max(
+ ...Object.values(atlas.fonts).map((entry) => entry.ch),
+ );
+ for (let i = 0; i <= maxChunk; i++) {
+ // hint browser to preload chunk images without blocking
+ const img = new Image();
+ img.src = `${FONT_CHUNK_PATH_PREFIX}${i}.avif`;
+ }
+}
+
+export async function loadFullFont({
+ family,
+ weights = [400, 700],
+}: {
+ family: string;
+ weights?: number[];
+}): Promise<void> {
+ if (fullLoaded.has(family)) return;
+
+ const url = `${GOOGLE_FONTS_CSS}?family=${encodeGoogleFontsFamily(family)}:wght@${weights.join(";")}&display=swap`;
+ const link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.href = url;
+ document.head.appendChild(link);
+ await new Promise<void>((resolve) => {
+ link.addEventListener("load", () => resolve(), { once: true });
+ link.addEventListener("error", () => resolve(), { once: true });
+ });
+ await Promise.all(
+ weights.map((weight) =>
+ document.fonts.load(`${weight} 16px "${family.replace(/"/g, '\\"')}"`),
+ ),
+ );
+ fullLoaded.add(family);
+}
+
+export async function loadFonts({
+ families,
+}: {
+ families: string[];
+}): Promise<void> {
+ const googleFonts = families.filter((family) => !SYSTEM_FONTS.has(family));
+ await Promise.all(googleFonts.map((family) => loadFullFont({ family })));
+}