diff --git a/miniprogram/englishPages/letterTracing/letterTracing.ts b/miniprogram/englishPages/letterTracing/letterTracing.ts index fc4e5d0..8692385 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.ts +++ b/miniprogram/englishPages/letterTracing/letterTracing.ts @@ -10,6 +10,7 @@ import { type LetterTracingMode, } from '../shared/generators/letter-tracing-generator'; import { LETTERS_UPPER } from '../shared/data/alphabet'; +import { loadLetterFont } from '../shared/draw/drawTools'; /** 各 worksheetId 对应的页面标题和描述 */ const PAGE_META: Record = { @@ -80,13 +81,34 @@ createPage( modeOptions: [...MODE_OPTIONS], } as unknown as PageData, - /** 页面加载:从路由参数获取 worksheetId 并应用 */ - onLoad(options: { id?: string }) { + /** 页面加载:从路由参数获取 worksheetId、letter、case 并应用,同时预加载字体 */ + onLoad(options: { + id?: string; + letter?: string; + case?: 'upper' | 'lower'; + }) { + loadLetterFont().catch(() => {}); const worksheetId = options.id && PAGE_META[options.id] ? options.id : 'letter-tracing-single'; this.applyWorksheet(worksheetId); + + const updates: Partial = {}; + if (options.letter) { + const normalized = normalizeLetter(options.letter); + if (normalized) updates.selectedLetter = normalized; + } + if (options.case === 'lower') { + updates.letterCaseLower = true; + } else if (options.case === 'upper') { + updates.letterCaseLower = false; + } + if (Object.keys(updates).length > 0) { + this.setData( + updates as Partial & WechatMiniprogram.IAnyObject, + ); + } }, /** 用户点击模式选择器时切换 worksheet */ @@ -140,10 +162,11 @@ createPage( }); }, - /** 执行 Canvas 绘制:生成数据 → 调用 drawService 渲染 */ + /** 执行 Canvas 绘制:等待字体就绪 → 生成数据 → 调用 drawService 渲染 */ async drawCanvas() { if (!this.ctx || !this.drawService) return; try { + await loadLetterFont(); const config = this.buildRuntimeConfig(); const data = generateLetterTracing(config); console.log('drawCanvas data', data); diff --git a/miniprogram/englishPages/shared/data/fontProfiles.ts b/miniprogram/englishPages/shared/data/fontProfiles.ts new file mode 100644 index 0000000..2b37659 --- /dev/null +++ b/miniprogram/englishPages/shared/data/fontProfiles.ts @@ -0,0 +1,189 @@ +/** + * 字体渲染配置(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; diff --git a/miniprogram/englishPages/shared/draw/columnCompareDraw.ts b/miniprogram/englishPages/shared/draw/columnCompareDraw.ts index d7cf43a..69a8b04 100644 --- a/miniprogram/englishPages/shared/draw/columnCompareDraw.ts +++ b/miniprogram/englishPages/shared/draw/columnCompareDraw.ts @@ -3,17 +3,34 @@ import type { LetterTracingData, TracingRow, } from '../generators/letter-tracing-generator'; -import { drawFourLineGrid } from './drawTools'; +import { + calcLetterFontSizes, + drawFourLineGrid, + drawLetterInFourLineGrid, + FOUR_LINE_GRID_H, + TOY_FONT_LETTER, + type LetterFontSizes, +} from './drawTools'; -const LETTER_FACE = - 'Helvetica, Arial, "Segoe UI", Roboto, sans-serif'; +const LETTER_FACE = `${TOY_FONT_LETTER}, Roboto, sans-serif`; +const COLOR_BLACK = '#1a1a1a'; +const COLOR_LIGHT_RED = 'rgba(252, 46, 0, 0.3)'; type ColumnCompareData = Extract< LetterTracingData, { mode: 'column-compare' } >; +/** + * 分栏对照绘制: + * 四线三格横跨整页宽度,字母分左右两侧书写—— + * 左半写大写(repetitions 个),右半写小写(repetitions 个), + * 每侧第一个字母黑色,后续浅红色。 + */ export default class ColumnCompareDraw extends BaseDrawService { + private letterSizes: LetterFontSizes = + calcLetterFontSizes(FOUR_LINE_GRID_H); + async draw(data: ColumnCompareData) { this.prepareDraw(); await this.drawHeaderAndDivider(); @@ -23,83 +40,62 @@ export default class ColumnCompareDraw extends BaseDrawService { private drawContent(leftRows: TracingRow[], rightRows: TracingRow[]) { const M = 24; - const y0 = this.currentY + 6; - const bottom = this.canvasHeight - M; + const lineW = this.canvasWidth - M * 2; const maxR = Math.max(leftRows.length, rightRows.length); - const rowH = Math.max(36, (bottom - y0) / maxR - 6); - const gapCol = 16; - const colW = (this.canvasWidth - M * 2 - gapCol) / 2; - for (let i = 0; i < leftRows.length; i++) { - const y = y0 + i * (rowH + 6); - this.drawTracingRow(leftRows[i], M, y, rowH, colW); - } - const xR = M + colW + gapCol; - for (let i = 0; i < rightRows.length; i++) { - const y = y0 + i * (rowH + 6); - this.drawTracingRow(rightRows[i], xR, y, rowH, colW); + const footerReserve = 56; + const availableH = + this.canvasHeight - this.currentY - footerReserve - 12; + const rowGap = Math.max( + 4, + (availableH - maxR * FOUR_LINE_GRID_H) / (maxR + 1), + ); + + const y0 = this.currentY + rowGap; + const halfW = lineW / 2; + + for (let i = 0; i < maxR; i++) { + const y = y0 + i * (FOUR_LINE_GRID_H + rowGap); + + drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H); + + if (i < leftRows.length) { + this.drawLettersInHalf(leftRows[i], M, y, halfW); + } + if (i < rightRows.length) { + this.drawLettersInHalf(rightRows[i], M + halfW, y, halfW); + } } } - private drawTracingRow( + /** + * 在四线三格的半侧区域内绘制字母。 + * 第一个字母黑色,后续字母浅红色。 + */ + private drawLettersInHalf( row: TracingRow, - startX: number, + x: number, y: number, - rowH: number, - totalWidth: number, + halfW: number, ) { - const gap = 4; const n = row.cells.length; - const cellW = n > 0 ? (totalWidth - gap * (n - 1)) / n : totalWidth; + if (n === 0) return; + + const slotW = halfW / n; for (let i = 0; i < n; i++) { - const cx = startX + i * (cellW + gap); - drawFourLineGrid(this.ctx, cx, y, cellW, rowH); const cell = row.cells[i]; - this.drawCharInCell( + const color = i === 0 ? COLOR_BLACK : COLOR_LIGHT_RED; + const cx = x + slotW * i + slotW / 2; + + drawLetterInFourLineGrid( + this.ctx, cell.char, cx, y, - cellW, - rowH, - cell.opacity, - cell.isGuide, - cell.pairChar, + FOUR_LINE_GRID_H, + this.letterSizes, + { fontFamily: LETTER_FACE, color }, ); } } - - private drawCharInCell( - ch: string, - x: number, - y: number, - w: number, - h: number, - opacity: number, - bold: boolean, - pairChar?: string, - ) { - const ctx = this.ctx; - if (opacity <= 0) return; - ctx.save(); - ctx.globalAlpha = opacity; - ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'alphabetic'; - const baselineY = y + h * 0.72; - - if (pairChar) { - const fs = Math.min(h * 0.5, w * 0.22); - ctx.font = `${bold ? 'bold ' : ''}${fs}px ${LETTER_FACE}`; - const gap = fs * 0.2; - const totalW = fs * 2 + gap; - const x0 = x + w / 2 - totalW / 2; - ctx.fillText(ch, x0 + fs * 0.5, baselineY); - ctx.fillText(pairChar, x0 + fs * 1.5 + gap, baselineY); - } else { - const fontSize = Math.min(h * 0.72, w * 0.85); - ctx.font = `${bold ? 'bold ' : ''}${fontSize}px ${LETTER_FACE}`; - ctx.fillText(ch, x + w / 2, baselineY); - } - ctx.restore(); - } } diff --git a/miniprogram/englishPages/shared/draw/drawStyle.md b/miniprogram/englishPages/shared/draw/drawStyle.md new file mode 100644 index 0000000..e69de29 diff --git a/miniprogram/englishPages/shared/draw/drawTools.ts b/miniprogram/englishPages/shared/draw/drawTools.ts index cabd47d..efc9d72 100644 --- a/miniprogram/englishPages/shared/draw/drawTools.ts +++ b/miniprogram/englishPages/shared/draw/drawTools.ts @@ -2,6 +2,118 @@ * 绘制工具集合(后续可继续扩展其他通用绘制方法) */ +import { + ACTIVE_FONT_PROFILE, + getLetterMetrics, + type FontProfile, +} from '../data/fontProfiles'; + +// ── 四线三格统一高度(所有页面共用) ── +export const FOUR_LINE_GRID_H = 43; + +// ── 英语字母字体集合 ── +export const TOY_FONT_LETTER = 'ToyLetterFont'; + +let _fontLoaded = false; +export function loadLetterFont(): Promise { + if (_fontLoaded) return Promise.resolve(); + return new Promise((resolve, reject) => { + wx.loadFontFace({ + family: TOY_FONT_LETTER, + source: `url("${ACTIVE_FONT_PROFILE.url}")`, + scopes: ['native'], + success: () => { + _fontLoaded = true; + resolve(); + }, + fail: (err) => { + console.error('loadFontFace failed', err); + reject(err); + }, + }); + }); +} + +// ── 向后兼容:LetterFontSizes 类型保留,供外部已有调用使用 ── + +export type LetterFontSizes = { + /** 字体配置(新机制) */ + _profile: FontProfile; + /** gridH(用于运行时计算) */ + _gridH: number; +}; + +/** + * 根据四线三格总高度,构建 LetterFontSizes。 + * 内部持有 FontProfile 引用,按字母查表计算。 + */ +export function calcLetterFontSizes(gridH: number): LetterFontSizes { + return { + _profile: ACTIVE_FONT_PROFILE, + _gridH: gridH, + }; +} + +/** 根据字母和字体配置,计算对应 fontSize */ +export function getLetterFontSize( + letter: string, + sizes: LetterFontSizes, +): number { + const metrics = getLetterMetrics(letter, sizes._profile); + return sizes._gridH * metrics.scale; +} + +/** 根据字母和字体配置,计算基线 Y 偏移修正(px) */ +function getLetterBaselineOffset( + letter: string, + sizes: LetterFontSizes, +): number { + const metrics = getLetterMetrics(letter, sizes._profile); + return sizes._gridH * metrics.baselineOffset; +} + +// ── 向后兼容:导出 DESCENDERS 集合 ── +export { DESCENDERS } from '../data/fontProfiles'; + +/** + * 在四线三格中绘制单个字母(基线固定在 y + 2h/3 + baselineOffset)。 + * + * @param cx 字母水平中心 + * @param gridY 四线三格顶线 Y + * @param gridH 四线三格总高度 + * @param sizes 预计算的字号(由 calcLetterFontSizes 生成) + */ +export function drawLetterInFourLineGrid( + ctx: RenderingContext, + letter: string, + cx: number, + gridY: number, + gridH: number, + sizes: LetterFontSizes, + options?: { + fontFamily?: string; + color?: string; + bold?: boolean; + }, +) { + const fontFamily = + options?.fontFamily ?? `${TOY_FONT_LETTER}, Roboto, sans-serif`; + const color = options?.color ?? '#1a1a1a'; + const bold = options?.bold ?? false; + + const baselineY = + gridY + (gridH * 2) / 3 + getLetterBaselineOffset(letter, sizes); + const fontSize = getLetterFontSize(letter, sizes); + + ctx.save(); + ctx.font = `${bold ? 'bold ' : ''}${fontSize}px ${fontFamily}`; + ctx.fillStyle = color; + ctx.textAlign = 'center'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText(letter, cx, baselineY); + ctx.restore(); +} + export type FourLineGridStyle = { /** 顶线/底线颜色 */ ink?: string; @@ -62,4 +174,3 @@ export function drawFourLineGrid( ctx.lineTo(x + w, yBot); ctx.stroke(); } - diff --git a/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts b/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts index 8dc667d..696d5f6 100644 --- a/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts +++ b/miniprogram/englishPages/shared/draw/pictureTracingDraw.ts @@ -4,20 +4,30 @@ import type { LetterTracingData, HighlightGrid, } from '../generators/letter-tracing-generator'; -import { drawFourLineGrid } from './drawTools'; +import { + calcLetterFontSizes, + drawFourLineGrid, + drawLetterInFourLineGrid, + FOUR_LINE_GRID_H, + getLetterFontSize, + TOY_FONT_LETTER, + type LetterFontSizes, +} from './drawTools'; const LINE_INK_ALPHA = 'rgba(50, 46, 37, 1)'; const ACCENT_RED = 'rgba(252, 46, 0, 1)'; //'Helvetica, Arial, "Segoe UI", Roboto, sans-serif'; -const LETTER_FACE = 'Roboto, sans-serif'; -const TITLE_EMOJI_FONT = - "16px 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif"; +const LETTER_FACE = `${TOY_FONT_LETTER}, Roboto, sans-serif`; +// const TITLE_EMOJI_FONT = +// "16px 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif"; const TITLE_TEXT_FONT = '14px sans-serif'; const TITLE_ICON_GAP = 4; type PictureData = Extract; export default class PictureTracingDraw extends BaseDrawService { + private letterSizes: LetterFontSizes = calcLetterFontSizes(FOUR_LINE_GRID_H); + async draw(data: PictureData) { this.prepareDraw(); await this.drawHeaderAndDivider(); @@ -141,7 +151,6 @@ export default class PictureTracingDraw extends BaseDrawService { // 每行:left:24 w:547 h:43,top 分别 524.5 / 590.5 / 656.5 / 722.5 const lineX = 24; const lineW = 547; - const lineH = 43; const lineTops = [524.5, 590.5, 656.5, 722.5]; // 8 → 6 → 3 → 1 组(每组为 Upper+Lower),左对齐,形成倒置直角三角形 @@ -150,19 +159,17 @@ export default class PictureTracingDraw extends BaseDrawService { for (let r = 0; r < lineTops.length; r++) { const y = lineTops[r]; - drawFourLineGrid(ctx, lineX, y, lineW, lineH); + drawFourLineGrid(ctx, lineX, y, lineW, FOUR_LINE_GRID_H); const n = groupsPerLine[r] ?? 0; for (let i = 0; i < n; i++) { - // 第一组不透明,后面逐次更透明 - const alpha = n <= 1 ? 1 : 1 - (i / (n - 1)) * 0.7; // 1 → 0.3 + const alpha = n <= 1 ? 1 : 1 - (i / (n - 1)) * 0.7; this.drawUpperLowerPairInFourLines( teaching.upper, teaching.lower, lineX + i * slotW, y, slotW, - lineH, alpha, ); } @@ -170,17 +177,7 @@ export default class PictureTracingDraw extends BaseDrawService { } /** - * 在四线三格中绘制 Upper+Lower(遵循书写基线规范) - * - * 四线三格的 4 条线(从上到下): - * Top = y ── 顶线 - * Midline = y + h/3 ── 中线(虚线) - * Baseline = y + 2h/3 ── 基线(虚线) - * Bottom = y + h ── 底线 - * - * 大写字母:高度占满 Top → Baseline(即第一格 + 第二格),字号 ≈ h * 2/3 - * 小写字母:高度占满 Midline → Baseline(即第二格),字号 ≈ h * 1/3 - * 两个字母共享 Baseline,间距远小于组间距 + * 在四线三格中绘制 Upper+Lower 对(共享基线,间距远小于组间距) */ private drawUpperLowerPairInFourLines( upper: string, @@ -188,40 +185,45 @@ export default class PictureTracingDraw extends BaseDrawService { x: number, y: number, w: number, - h: number, alpha: number, ) { const ctx = this.ctx; + const sizes = this.letterSizes; ctx.save(); ctx.globalAlpha = Math.max(0, Math.min(1, alpha)); - ctx.fillStyle = 'rgba(26, 26, 26, 1)'; - ctx.textBaseline = 'alphabetic'; - const baselineY = y + (h * 2) / 3; - - // 大写占满 Top→Baseline(2 格高),小写占满 Midline→Baseline(1 格高) - const upperFs = h * (2 / 3); - const lowerFs = h * (1 / 3); - - // 字母间距:固定 3px,远小于组间距(slotW ≈ 68) const pairGap = 3; + const upperFs = getLetterFontSize(upper, sizes); ctx.font = `${upperFs}px ${LETTER_FACE}`; const upperW = ctx.measureText(upper).width; + const lowerFs = getLetterFontSize(lower, sizes); ctx.font = `${lowerFs}px ${LETTER_FACE}`; const lowerW = ctx.measureText(lower).width; const totalPairW = upperW + pairGap + lowerW; const pairStartX = x + (w - totalPairW) / 2; + const color = 'rgba(26, 26, 26, 1)'; - ctx.font = `${upperFs}px ${LETTER_FACE}`; - ctx.textAlign = 'left'; - ctx.fillText(upper, pairStartX, baselineY); - - ctx.font = `${lowerFs}px ${LETTER_FACE}`; - ctx.textAlign = 'left'; - ctx.fillText(lower, pairStartX + upperW + pairGap, baselineY); + drawLetterInFourLineGrid( + ctx, + upper, + pairStartX + upperW / 2, + y, + FOUR_LINE_GRID_H, + sizes, + { fontFamily: LETTER_FACE, color }, + ); + drawLetterInFourLineGrid( + ctx, + lower, + pairStartX + upperW + pairGap + lowerW / 2, + y, + FOUR_LINE_GRID_H, + sizes, + { fontFamily: LETTER_FACE, color }, + ); ctx.restore(); } @@ -257,7 +259,8 @@ export default class PictureTracingDraw extends BaseDrawService { ctx.textBaseline = 'top'; const hlIcon = '\u{1F50D}'; - ctx.font = TITLE_EMOJI_FONT; + ctx.font = + "16px 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif"; ctx.fillText(hlIcon, titleX, titleY + 1); const hlIconW = ctx.measureText(hlIcon).width; @@ -312,7 +315,8 @@ export default class PictureTracingDraw extends BaseDrawService { ctx.textBaseline = 'top'; const traceIcon = '\u{1F58A}\u{FE0F}'; - ctx.font = TITLE_EMOJI_FONT; + ctx.font = + "16px 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif"; ctx.fillText(traceIcon, titleX, titleY); const traceIconW = ctx.measureText(traceIcon).width; diff --git a/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts index d2176a9..795af27 100644 --- a/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts +++ b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts @@ -255,16 +255,11 @@ export function generateLetterTracing( config.alphabetHalf === 'N-Z' ? LETTERS_UPPER.slice(13) : LETTERS_UPPER.slice(0, 13); - const rowsOf4 = chunk(half, 4); - const leftRows = rowsOf4.map((g) => - rowForManyChars(g, repetitions, fadePattern), + const leftRows = half.map((L) => + rowForChar(L, repetitions, fadePattern), ); - const rightRows = rowsOf4.map((g) => - rowForManyChars( - g.map((L) => L.toLowerCase()), - repetitions, - fadePattern, - ), + const rightRows = half.map((L) => + rowForChar(L.toLowerCase(), repetitions, fadePattern), ); return { mode: 'column-compare', leftRows, rightRows }; } diff --git a/project.private.config.json b/project.private.config.json index e1e715b..1c1a1c6 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -23,12 +23,26 @@ "condition": { "miniprogram": { "list": [ + { + "name": "englishPages/letterTracing/letterTracing", + "pathName": "englishPages/letterTracing/letterTracing", + "query": "id=letter-tracing-full", + "scene": null, + "launchMode": "default" + }, + { + "name": "englishPages/letterTracing/letterTracing", + "pathName": "englishPages/letterTracing/letterTracing", + "query": "id=letter-tracing-single", + "launchMode": "default", + "scene": null + }, { "name": "pages/index/index", "pathName": "pages/index/index", "query": "", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null }, { "name": "englishPages/letterTracing/letterTracing",