249 lines
8.8 KiB
TypeScript
249 lines
8.8 KiB
TypeScript
/**
|
||
* 字体渲染配置(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 {
|
||
/** 唯一标识,同时用作 wx.loadFontFace 的 family 和按名称查找的 key */
|
||
name: string;
|
||
/** 字体 URL */
|
||
url: string;
|
||
/** 各分类的默认渲染参数 */
|
||
categories: Record<LetterCategory, CategoryMetrics>;
|
||
/** 按字母精细微调(优先级高于分类默认值) */
|
||
letterOverrides?: Record<string, LetterOverride>;
|
||
}
|
||
|
||
/** 构建 CSS font-family 串(含回退字体) */
|
||
export function fontFamilyOf(profile: FontProfile): string {
|
||
return `${profile.name}, Roboto, sans-serif`;
|
||
}
|
||
|
||
// ── 字母分类集合 ──
|
||
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 = {
|
||
name: 'ZhiyongWrite',
|
||
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 },
|
||
// },
|
||
categories: {
|
||
uppercase: { scale: 0.95, baselineOffset: 0 },
|
||
tallLower: { scale: 0.85, baselineOffset: 0 },
|
||
descender: { scale: 1, baselineOffset: 0.12 },
|
||
lowercase: { scale: 0.75, baselineOffset: 0 },
|
||
},
|
||
letterOverrides: {
|
||
i: { scale: 0.8, baselineOffset: 0 },
|
||
f: { scale: 1, baselineOffset: 0.05 },
|
||
t: { scale: 0.85, baselineOffset: 0 },
|
||
j: { scale: 0.85, baselineOffset: 0.1 },
|
||
},
|
||
};
|
||
|
||
/** Handlee Regular — Google 手写风格 */
|
||
export const HANDLEE_REGULAR: FontProfile = {
|
||
name: 'HandleeRegular',
|
||
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 },
|
||
// },
|
||
categories: {
|
||
uppercase: { scale: 0.9, baselineOffset: 0 },
|
||
tallLower: { scale: 0.68, baselineOffset: 0 },
|
||
descender: { scale: 0.68, baselineOffset: 0 },
|
||
lowercase: { scale: 0.68, baselineOffset: 0 },
|
||
},
|
||
letterOverrides: {
|
||
i: { scale: 0.68, baselineOffset: 0 },
|
||
f: { scale: 0.68, baselineOffset: 0 },
|
||
t: { scale: 0.68, baselineOffset: 0 },
|
||
j: { scale: 0.68, baselineOffset: 0 },
|
||
},
|
||
};
|
||
|
||
/** Print Clearly Regular */
|
||
export const PRINT_CLEARLY_REGULAR: FontProfile = {
|
||
name: 'PrintClearlyRegular',
|
||
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_regular_tt.ttf',
|
||
categories: {
|
||
uppercase: { scale: 1, baselineOffset: 0 },
|
||
tallLower: { scale: 1, baselineOffset: 0 },
|
||
descender: { scale: 1, baselineOffset: 0 },
|
||
lowercase: { scale: 1, baselineOffset: 0 },
|
||
},
|
||
letterOverrides: {
|
||
i: { scale: 1, baselineOffset: 0 },
|
||
f: { scale: 1, baselineOffset: 0 },
|
||
t: { scale: 1, baselineOffset: 0 },
|
||
j: { scale: 1, baselineOffset: 0 },
|
||
},
|
||
};
|
||
|
||
/** Print Clearly Bold */
|
||
export const PRINT_CLEARLY_BOLD: FontProfile = {
|
||
name: 'PrintClearlyBold',
|
||
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_bold_tt.ttf',
|
||
categories: {
|
||
uppercase: { scale: 1, baselineOffset: 0 },
|
||
tallLower: { scale: 1, baselineOffset: 0 },
|
||
descender: { scale: 1, baselineOffset: 0 },
|
||
lowercase: { scale: 1, baselineOffset: 0 },
|
||
},
|
||
letterOverrides: {
|
||
i: { scale: 1, baselineOffset: 0 },
|
||
f: { scale: 1, baselineOffset: 0 },
|
||
t: { scale: 1, baselineOffset: 0 },
|
||
j: { scale: 1, baselineOffset: 0 },
|
||
},
|
||
};
|
||
|
||
/** Print Clearly Dashed — 虚线描红字体 */
|
||
export const PRINT_CLEARLY_DASHED: FontProfile = {
|
||
name: 'PrintClearlyDashed',
|
||
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_dashed_tt.ttf',
|
||
categories: {
|
||
uppercase: { scale: 1, baselineOffset: 0 },
|
||
tallLower: { scale: 1, baselineOffset: 0 },
|
||
descender: { scale: 1, baselineOffset: 0 },
|
||
lowercase: { scale: 1, baselineOffset: 0 },
|
||
},
|
||
letterOverrides: {
|
||
i: { scale: 1, baselineOffset: 0 },
|
||
f: { scale: 1, baselineOffset: 0 },
|
||
t: { scale: 1, baselineOffset: 0 },
|
||
j: { scale: 1, baselineOffset: 0 },
|
||
},
|
||
};
|
||
|
||
/** Num 数字字体 */
|
||
export const NUM_FONT: FontProfile = {
|
||
name: 'NumFont',
|
||
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 },
|
||
},
|
||
};
|
||
|
||
// ── 字体注册表:按 name 索引所有可用字体 ──
|
||
|
||
const FONT_PROFILES: ReadonlyArray<FontProfile> = [
|
||
ZHIYONG_WRITE,
|
||
HANDLEE_REGULAR,
|
||
PRINT_CLEARLY_REGULAR,
|
||
PRINT_CLEARLY_BOLD,
|
||
PRINT_CLEARLY_DASHED,
|
||
NUM_FONT,
|
||
];
|
||
|
||
const _profileByName = new Map<string, FontProfile>(
|
||
FONT_PROFILES.map((p) => [p.name, p]),
|
||
);
|
||
|
||
/** 默认字母字体 */
|
||
export const DEFAULT_LETTER_PROFILE = PRINT_CLEARLY_REGULAR;
|
||
|
||
/** 按名称查找字体配置,找不到返回默认 */
|
||
export function getFontProfile(name: string): FontProfile {
|
||
return _profileByName.get(name) ?? DEFAULT_LETTER_PROFILE;
|
||
}
|