fix: match note image template fonts to their visual styles
This commit is contained in:
1 parent
eb034b3c8b
commit
51973cf16e
3 files changed
+65
-17
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
NOTE_IMAGE_THEME_OPTIONS, NOTE_IMAGE_THEMES, NOTE_IMAGE_BACKGROUND_COLORS, NOTE_IMAGE_FONT_FAMILIES,
|
||||
NOTE_IMAGE_BUNDLED_FONTS, NOTE_IMAGE_TEMPLATE_TYPOGRAPHY, NOTE_IMAGE_THEME_OPTIONS, NOTE_IMAGE_THEMES, NOTE_IMAGE_BACKGROUND_COLORS, NOTE_IMAGE_FONT_FAMILIES,
|
||||
resolveTheme, resolveNoteImageTypography, buildImageExportBasename, buildNoteImageCardMarkup,
|
||||
estimateReadingStats, generateCardCss,
|
||||
} from "./note-image-card";
|
||||
@@ -8,8 +8,26 @@ import { zhCN } from "./i18n/zh-CN";
|
||||
import { enUS } from "./i18n/en-US";
|
||||
import { ja } from "./i18n/ja";
|
||||
import { pl } from "./i18n/pl";
|
||||
import { NOTE_IMAGE_FONT_SUBSETS } from "./note-image-font-manifest";
|
||||
|
||||
describe("note image templates", () => {
|
||||
test("each template references the font family registered by its bundled assets", () => {
|
||||
for (const theme of NOTE_IMAGE_THEME_OPTIONS) {
|
||||
const preset = NOTE_IMAGE_TEMPLATE_TYPOGRAPHY[theme];
|
||||
for (const role of ["body", "heading"]) {
|
||||
const familyStack = resolveNoteImageTypography(theme)[`${role}Family`];
|
||||
for (const font of preset[role]) {
|
||||
const subsets = NOTE_IMAGE_FONT_SUBSETS[font];
|
||||
expect(subsets.length).toBeGreaterThan(0);
|
||||
for (const { family } of subsets) {
|
||||
expect(family).toBe(NOTE_IMAGE_BUNDLED_FONTS[font].family);
|
||||
expect(familyStack).toContain(`"${family}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("every selectable template is localized and has renderable colors", () => {
|
||||
for (const theme of NOTE_IMAGE_THEME_OPTIONS) {
|
||||
expect(NOTE_IMAGE_THEMES[theme]).toBeDefined();
|
||||
|
||||
@@ -106,19 +106,50 @@ export const NOTE_IMAGE_FONT_FAMILIES: Record<Exclude<NoteImageFontStyle, "auto"
|
||||
...CATALOG_FONT_STACKS,
|
||||
};
|
||||
|
||||
export const NOTE_IMAGE_TEMPLATE_FONT_FAMILIES: Record<NoteImageActiveTheme, string> = {
|
||||
gradient: "Alibaba-PuHuiTi-Regular", glass: "Alibaba-PuHuiTi-Regular", quote: "Huiwen_mingchao",
|
||||
book: "SourceHanSerifCN_SemiBold", memo: "MiSans-Regular", bento: "Alibaba-PuHuiTi-Regular",
|
||||
"black-sun": "Alibaba-PuHuiTi-Regular", frame: "Alibaba-PuHuiTi-Regular", handwritten: "Long Cang", code: "Geist Mono Variable",
|
||||
/** Keep CSS families and asset selection together so defaults cannot silently fall back. */
|
||||
export const NOTE_IMAGE_BUNDLED_FONTS = {
|
||||
puhui: { family: "Alibaba-PuHuiTi-Regular", fallback: "sans" },
|
||||
misans: { family: "MiSans", fallback: "sans" },
|
||||
mincho: { family: "Huiwen_mingchao", fallback: "serif" },
|
||||
song: { family: "SourceHanSerifCN_SemiBold", fallback: "serif" },
|
||||
longcang: { family: "Long Cang", fallback: "kai" },
|
||||
geist: { family: "Geist Mono Variable", fallback: "mono" },
|
||||
} as const;
|
||||
|
||||
type BundledNoteImageFont = keyof typeof NOTE_IMAGE_BUNDLED_FONTS;
|
||||
type TemplateTypography = {
|
||||
body: readonly BundledNoteImageFont[];
|
||||
heading: readonly BundledNoteImageFont[];
|
||||
headingWeight: number;
|
||||
};
|
||||
|
||||
export const NOTE_IMAGE_TEMPLATE_TYPOGRAPHY = {
|
||||
gradient: { body: ["misans"], heading: ["misans"], headingWeight: 700 },
|
||||
glass: { body: ["misans"], heading: ["mincho"], headingWeight: 400 },
|
||||
quote: { body: ["mincho"], heading: ["mincho"], headingWeight: 700 },
|
||||
book: { body: ["song"], heading: ["song"], headingWeight: 700 },
|
||||
memo: { body: ["misans"], heading: ["misans"], headingWeight: 700 },
|
||||
bento: { body: ["misans"], heading: ["song"], headingWeight: 600 },
|
||||
"black-sun": { body: ["geist", "misans"], heading: ["geist", "misans"], headingWeight: 700 },
|
||||
frame: { body: ["song"], heading: ["song"], headingWeight: 600 },
|
||||
handwritten: { body: ["longcang"], heading: ["longcang"], headingWeight: 400 },
|
||||
code: { body: ["geist"], heading: ["geist"], headingWeight: 700 },
|
||||
} as const satisfies Record<NoteImageActiveTheme, TemplateTypography>;
|
||||
|
||||
const templateFontStack = (fonts: readonly BundledNoteImageFont[]) => [
|
||||
...fonts.map(font => `"${NOTE_IMAGE_BUNDLED_FONTS[font].family}"`),
|
||||
NOTE_IMAGE_FONT_FAMILIES[NOTE_IMAGE_BUNDLED_FONTS[fonts[0]].fallback],
|
||||
].join(", ");
|
||||
|
||||
export const resolveNoteImageTypography = (theme: NoteImageTheme, fontStyle: NoteImageFontStyle = "auto") => {
|
||||
const active = resolveTheme(undefined, theme);
|
||||
const style = fontStyle !== "auto" ? fontStyle : active === "book" || active === "quote" ? "serif" : active === "handwritten" ? "kai" : active === "code" ? "mono" : "sans";
|
||||
const family = fontStyle === "auto"
|
||||
? `"${NOTE_IMAGE_TEMPLATE_FONT_FAMILIES[active]}", ${NOTE_IMAGE_FONT_FAMILIES[style]}`
|
||||
: NOTE_IMAGE_FONT_FAMILIES[style];
|
||||
return { bodyFamily: family, headingFamily: family, headingWeight: active === "handwritten" ? 400 : 700, lineHeightAdjustment: 0 };
|
||||
const preset = NOTE_IMAGE_TEMPLATE_TYPOGRAPHY[active];
|
||||
return {
|
||||
bodyFamily: fontStyle === "auto" ? templateFontStack(preset.body) : NOTE_IMAGE_FONT_FAMILIES[fontStyle],
|
||||
headingFamily: fontStyle === "auto" ? templateFontStack(preset.heading) : NOTE_IMAGE_FONT_FAMILIES[fontStyle],
|
||||
headingWeight: fontStyle === "auto" ? preset.headingWeight : active === "handwritten" ? 400 : 700,
|
||||
lineHeightAdjustment: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const NOTE_IMAGE_FONT_SIZES = {
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { NOTE_IMAGE_FONT_NOTICES } from "./note-image-font-notices";
|
||||
import { NOTE_IMAGE_FONT_SUBSETS } from "./note-image-font-manifest";
|
||||
import { resolveTheme, type NoteImageTheme, type NoteImageFontStyle } from "./note-image-card";
|
||||
import { NOTE_IMAGE_TEMPLATE_TYPOGRAPHY, resolveTheme, type NoteImageTheme, type NoteImageFontStyle } from "./note-image-card";
|
||||
import { isRemoteNoteImageFont } from "./font-catalog";
|
||||
import { fontSubsetContains, loadCatalogFont } from "./font-loader";
|
||||
export { fontSubsetContains, createRemoteFontResourceLoader, loadRemoteFontResource } from "./font-loader";
|
||||
|
||||
export const NOTE_IMAGE_THEME_FONTS = {
|
||||
gradient: "puhui", glass: "puhui", quote: "mincho", book: "song", memo: "misans",
|
||||
bento: "puhui", "black-sun": "puhui", frame: "puhui", handwritten: "longcang", code: "geist",
|
||||
} as const;
|
||||
|
||||
type AssetUrl = string | number | { uri: string };
|
||||
const resources = new Map<string, Promise<string>>();
|
||||
const loadedFaces = new Map<string, Promise<FontFace>>();
|
||||
@@ -55,7 +50,11 @@ export const prepareNoteImageAssets = async ({ theme, fontStyle = "auto", text,
|
||||
};
|
||||
const points = new Set(Array.from(text, char => char.codePointAt(0)!));
|
||||
const remote = isRemoteNoteImageFont(fontStyle);
|
||||
const catalog = !remote && fontStyle === "auto" ? NOTE_IMAGE_FONT_SUBSETS[NOTE_IMAGE_THEME_FONTS[active]] : [];
|
||||
const preset = NOTE_IMAGE_TEMPLATE_TYPOGRAPHY[active];
|
||||
const fontIds = [...new Set([...preset.body, ...preset.heading])];
|
||||
const catalog = !remote && fontStyle === "auto"
|
||||
? fontIds.flatMap(id => [...NOTE_IMAGE_FONT_SUBSETS[id]])
|
||||
: [];
|
||||
const blocks = catalog.filter(block => fontSubsetContains(block.range, points));
|
||||
const fontFacesCss = (await Promise.all(blocks.map(async block => {
|
||||
let loaded = loadedFaces.get(block.asset);
|
||||
|
||||
Reference in new issue
Block a user