/** * 字体渲染配置(Font Profile) * * 不同手写字体的字形设计差异很大,同一 fontSize 下各字母的视觉高度、 * 基线位置各不相同。为每种字体维护一份 profile,按字母分类配置 * 缩放因子和 Y 轴偏移,保证字母在四线三格中视觉准确。 * * 设计思路(参考 Excalidraw 的 hardcoded font metrics): * - 缩放和偏移均为相对值(基于 gridH),换字体只需新增/调整 profile * - 按字母分类给出默认值,可通过 letterOverrides 对单个字母做精细微调 */ /** 字母分类 */ export type LetterCategory = | 'uppercase' | 'tallLower' | 'descender' | 'lowercase'; /** 单个分类的渲染参数 */ export interface CategoryMetrics { /** fontSize = gridH * scale */ scale: number; /** 基线 Y 偏移修正(相对于 gridH,正值=下移) */ baselineOffset: number; } /** 单个字母的渲染微调(覆盖所属分类的默认值) */ export interface LetterOverride { scale?: number; baselineOffset?: number; } /** 一套完整的字体渲染配置 */ export interface FontProfile { /** 字体 URL */ url: string; /** 各分类的默认渲染参数 */ categories: Record; /** 按字母精细微调(优先级高于分类默认值) */ letterOverrides?: Record; } // ── 字母分类集合 ── const TALL_LOWERCASE = new Set(['b', 'd', 'h', 'k', 'l']); const DESCENDERS = new Set(['g', 'p', 'q', 'y']); export { TALL_LOWERCASE, DESCENDERS }; /** 判断字母所属分类 */ export function getLetterCategory(letter: string): LetterCategory { if (letter >= 'A' && letter <= 'Z') return 'uppercase'; if (TALL_LOWERCASE.has(letter)) return 'tallLower'; if (DESCENDERS.has(letter)) return 'descender'; return 'lowercase'; } /** 获取某个字母的最终渲染参数(分类默认 + 单字母微调合并) */ export function getLetterMetrics( letter: string, profile: FontProfile, ): CategoryMetrics { const cat = getLetterCategory(letter); const base = profile.categories[cat]; const override = profile.letterOverrides?.[letter]; if (!override) return base; return { scale: override.scale ?? base.scale, baselineOffset: override.baselineOffset ?? base.baselineOffset, }; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 字体配置表 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * 四线三格字母分类目标占位(所有字体通用): * 顶线 ─── 第一格 ─── 中线 ─── 第二格 ─── 基线 ─── 第三格 ─── 底线 * * uppercase / tallLower : 第一格 + 第二格(顶线 → 基线) * descender (g,p,q,y) : 第二格 + 第三格(中线 → 底线) * lowercase : 第二格(中线 → 基线) * * f / t / j 等需跨格占位的字母不在分类里单独建模,统一走 lowercase, * 在 letterOverrides 里按字体微调(与 i 相同方式)。 */ /** ZhiyongWrite 手写字体 */ export const ZHIYONG_WRITE: FontProfile = { url: 'https://cdn.joeyone.cn/doodle/fonts/ZhiyongWrite.ttf', categories: { uppercase: { scale: 0.93, baselineOffset: 0 }, tallLower: { scale: 0.95, baselineOffset: 0 }, descender: { scale: 1.12, baselineOffset: 0.18 }, lowercase: { scale: 0.83, baselineOffset: 0.03 }, }, letterOverrides: { i: { scale: 0.78, baselineOffset: 0.02 }, f: { scale: 1.37, baselineOffset: 0.34 }, t: { scale: 0.85, baselineOffset: 0 }, j: { scale: 1, baselineOffset: 0.15 }, }, }; /** Handlee Regular — Google 手写风格 */ export const HANDLEE_REGULAR: FontProfile = { url: 'https://cdn.joeyone.cn/doodle/fonts/Handlee-Regular.ttf', categories: { uppercase: { scale: 0.9, baselineOffset: 0 }, tallLower: { scale: 0.73, baselineOffset: 0.03 }, descender: { scale: 0.8, baselineOffset: 0.05 }, lowercase: { scale: 0.73, baselineOffset: 0.02 }, }, letterOverrides: { i: { scale: 0.78, baselineOffset: 0.02 }, f: { scale: 0.82, baselineOffset: 0 }, t: { scale: 0.77, baselineOffset: 0.04 }, j: { scale: 0.85, baselineOffset: 0.08 }, }, }; /** Print Clearly Regular */ export const PRINT_CLEARLY_REGULAR: FontProfile = { url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_regular_tt.ttf', categories: { uppercase: { scale: 0.95, baselineOffset: 0 }, tallLower: { scale: 0.95, baselineOffset: 0 }, descender: { scale: 1.05, baselineOffset: 0 }, lowercase: { scale: 0.85, baselineOffset: 0 }, }, letterOverrides: { f: { scale: 1.35, baselineOffset: 0 }, t: { scale: 0.85, baselineOffset: 0 }, j: { scale: 1.15, baselineOffset: 0 }, }, }; /** Print Clearly Bold */ export const PRINT_CLEARLY_BOLD: FontProfile = { url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_bold_tt.ttf', categories: { uppercase: { scale: 0.95, baselineOffset: 0 }, tallLower: { scale: 0.95, baselineOffset: 0 }, descender: { scale: 1.05, baselineOffset: 0 }, lowercase: { scale: 0.85, baselineOffset: 0 }, }, letterOverrides: { f: { scale: 1.35, baselineOffset: 0 }, t: { scale: 0.85, baselineOffset: 0 }, j: { scale: 1.15, baselineOffset: 0 }, }, }; /** Print Clearly Dashed — 虚线描红字体 */ export const PRINT_CLEARLY_DASHED: FontProfile = { url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_dashed_tt.ttf', categories: { uppercase: { scale: 0.95, baselineOffset: 0 }, tallLower: { scale: 0.95, baselineOffset: 0 }, descender: { scale: 1.05, baselineOffset: 0 }, lowercase: { scale: 0.85, baselineOffset: 0 }, }, letterOverrides: { f: { scale: 1.35, baselineOffset: 0 }, t: { scale: 0.85, baselineOffset: 0 }, j: { scale: 1.15, baselineOffset: 0 }, }, }; /** Num 数字字体 */ export const NUM_FONT: FontProfile = { url: 'https://cdn.joeyone.cn/doodle/fonts/num.woff2', categories: { uppercase: { scale: 0.95, baselineOffset: 0 }, tallLower: { scale: 0.95, baselineOffset: 0 }, descender: { scale: 1.05, baselineOffset: 0 }, lowercase: { scale: 0.85, baselineOffset: 0 }, }, letterOverrides: { f: { scale: 1.35, baselineOffset: 0 }, t: { scale: 0.85, baselineOffset: 0 }, j: { scale: 1.15, baselineOffset: 0 }, }, }; /** 当前激活的字体配置(切换字体只需修改这里) */ // export const ACTIVE_FONT_PROFILE = ZHIYONG_WRITE; export const ACTIVE_FONT_PROFILE = HANDLEE_REGULAR;