Files
doodle-mini/miniprogram/englishPages/letterTracing/letterTracing.ts
T
2026-04-21 15:03:38 +08:00

358 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import LetterTracingDraw from './draw/letterTracingDraw';
import { createPage, type CanvasDataState } from '../../base/pageMixin';
import { defaultShareConfig } from '../../config/config';
import { getWorksheetById } from '../../core/data/worksheets';
import {
generateLetterTracing,
mergeLetterTracingConfig,
normalizeLetter,
type LetterTracingGeneratorConfig,
type LetterTracingMode,
} from './generators/letter-tracing-generator';
import { LETTERS_UPPER, LETTERS_PAIRS } from '../shared/data/alphabet';
import {
DEFAULT_LETTER_PROFILE,
getFontProfile,
type FontProfile,
} from '../shared/data/fontProfiles';
import { loadLetterFont } from '../shared/draw/drawTools';
/** 练习模式定义(顺序与产品文档一致),同时用于模式选择器和页面元信息 */
const MODES = [
{
id: 'letter-tracing-single',
icon: 'start-a',
label: '默认字帖',
desc: '配图、例句与描红',
},
{
id: 'letter-tracing-upper-lower',
icon: 'draw-o',
// label: '字母总览',
label: '基础描红',
desc: 'Uppercase / Lowercase 总览',
},
{
id: 'letter-tracing-case-pairing',
icon: 'font-size',
label: '大小写对照',
desc: '半组字母左大写右小写',
},
{
id: 'letter-tracing-two-column',
icon: 'two-columns',
label: '两列描红',
desc: '左 AM、右 NZ 配对描红',
},
{
id: 'letter-tracing-half',
icon: 'square-half',
label: '13字母半表',
desc: '每行一个字母,13 字母半表',
},
{
id: 'letter-tracing-three',
icon: 'ABC-list',
label: '三字母精练',
desc: '每页聚焦 3 个字母深度书写',
},
] as const;
const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
string,
(typeof MODES)[number]
>;
/** 三字母精练分组:26 字母每 3 个一组 */
const TRIPLE_GROUPS: { label: string; letters: string[] }[] = [];
for (let i = 0; i < LETTERS_UPPER.length; i += 3) {
const group = LETTERS_UPPER.slice(i, i + 3);
const label =
group.length > 1 ? `${group[0]}${group[group.length - 1]}` : group[0];
TRIPLE_GROUPS.push({ label, letters: [...group] });
}
/** 根据 worksheetId 查找页面元信息 */
function pageInfoLookup(id: string) {
const m = MODE_MAP[id];
return m ? { title: m.label, desc: m.desc } : undefined;
}
type PageData = CanvasDataState & {
worksheetId: string;
traceMode: LetterTracingMode;
selectedLetter: string;
selectedLetterPair: string;
letterCaseLower: boolean;
showLetterPicker: boolean;
showCaseToggle: boolean;
showNextLetter: boolean;
showTripleGroupPicker: boolean;
tripleGroups: { label: string; letters: string[] }[];
selectedTripleGroupIdx: number;
letterGrid: string[];
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
};
createPage(
{
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as LetterTracingDraw | null,
fontProfile: DEFAULT_LETTER_PROFILE as FontProfile,
data: {
pageTitle: '字母描红',
functionId: '',
hasContent: false,
showShareDialog: false,
worksheetId: 'letter-tracing-single',
traceMode: 'letter-tracing-single',
selectedLetter: 'A',
selectedLetterPair: 'Aa',
letterCaseLower: false,
showLetterPicker: true,
showCaseToggle: false,
showNextLetter: true,
showTripleGroupPicker: false,
tripleGroups: TRIPLE_GROUPS,
selectedTripleGroupIdx: 0,
letterGrid: LETTERS_PAIRS,
modeOptions: [...MODES],
} as unknown as PageData,
/** 页面加载:从路由参数获取 worksheetId、letter、case、font 并应用,同时预加载字体 */
onLoad(options: {
id?: string;
letter?: string;
case?: 'upper' | 'lower';
font?: string;
}) {
if (options.font) {
this.fontProfile = getFontProfile(options.font);
}
loadLetterFont(this.fontProfile).catch(() => {});
const worksheetId =
options.id && MODE_MAP[options.id]
? options.id
: 'letter-tracing-single';
this.applyWorksheet(worksheetId);
const updates: Partial<PageData> = {};
if (options.letter) {
const normalized = normalizeLetter(options.letter);
if (normalized) {
updates.selectedLetter = normalized;
updates.selectedLetterPair = `${normalized}${normalized.toLowerCase()}`;
}
}
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<PageData> & WechatMiniprogram.IAnyObject,
);
}
},
/** 用户点击模式选择器时切换 worksheet */
onSelectMode(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string | undefined;
if (!id || id === this.data.worksheetId) return;
this.applyWorksheet(id, {
preserveLetter: true,
redraw: true,
});
},
/** 页面就绪:初始化 Canvas → 首次绘制(标题行前缀使用系统 emoji,无需 loadFontFace */
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, unknown>,
) => new LetterTracingDraw(canvas, ctx, options),
drawServiceOptions: {
title: this.data.pageTitle,
},
onCanvasReady: () => {
this.drawCanvas();
},
});
},
/** 构建运行时生成器配置:合并 worksheet 预设 + 当前页面状态 */
buildRuntimeConfig(): LetterTracingGeneratorConfig {
const def = getWorksheetById(this.data.worksheetId);
const base = (def?.generatorConfig ??
{}) as Partial<LetterTracingGeneratorConfig>;
const letterCase =
this.data.traceMode === 'letter-tracing-single'
? (base.letterCase ?? 'upper')
: this.data.letterCaseLower
? 'lower'
: 'upper';
const needsHalf =
this.data.traceMode === 'letter-tracing-case-pairing' ||
this.data.traceMode === 'letter-tracing-half';
const isTriple = this.data.traceMode === 'letter-tracing-three';
return mergeLetterTracingConfig(base, {
selectedLetter: this.data.selectedLetter,
letterCase,
...(needsHalf
? {
alphabetHalf: this.data.letterCaseLower
? 'N-Z'
: 'A-M',
}
: {}),
...(isTriple
? {
tripleLetters:
TRIPLE_GROUPS[this.data.selectedTripleGroupIdx]
?.letters,
}
: {}),
});
},
/** 执行 Canvas 绘制:等待字体就绪 → 生成数据 → 调用 drawService 渲染 */
async drawCanvas() {
if (!this.ctx || !this.drawService) return;
try {
await loadLetterFont(this.fontProfile);
const config = this.buildRuntimeConfig();
const data = generateLetterTracing(config);
console.log('drawCanvas data', data);
await (this.drawService as LetterTracingDraw).draw(data);
this.setData({ hasContent: true });
} catch (e) {
console.error('letterTracing draw failed', e);
this.setData({ hasContent: false });
}
},
/** 用户在字母网格中选择了一个字母(item 为 "Aa" 格式,取首字母作为 selectedLetter */
onSelectLetter(e: WechatMiniprogram.TouchEvent) {
const raw = e.currentTarget.dataset.letter as string;
if (!raw) return;
const letter = raw.charAt(0).toUpperCase();
this.setData({
selectedLetter: letter,
selectedLetterPair: `${letter}${letter.toLowerCase()}`,
});
this.drawCanvas();
},
/** 切换为大写模式并重绘 */
onSelectCaseUpper() {
this.setData({ letterCaseLower: false });
this.drawCanvas();
},
/** 切换为小写模式并重绘 */
onSelectCaseLower() {
this.setData({ letterCaseLower: true });
this.drawCanvas();
},
/** 跳到下一个字母(循环 A→Z→A)并重绘 */
onNextLetter() {
const list = LETTERS_UPPER;
const i = list.indexOf(this.data.selectedLetter);
const next = list[(i + 1) % list.length];
this.setData({
selectedLetter: next,
selectedLetterPair: `${next}${next.toLowerCase()}`,
});
this.drawCanvas();
},
/** 用户在三字母分组选择器中选择了一组 */
onSelectTripleGroup(e: WechatMiniprogram.TouchEvent) {
const idx = Number(e.currentTarget.dataset.idx);
if (isNaN(idx) || idx === this.data.selectedTripleGroupIdx) return;
this.setData({ selectedTripleGroupIdx: idx });
this.drawCanvas();
},
/**
* 应用指定的 worksheet 配置到页面状态
* 根据 worksheet 的 generatorConfig 决定 UI 控件的显隐(字母选择器、大小写切换等)
* @param worksheetId - 要应用的 worksheet ID
* @param options.preserveLetter - 是否保留当前选中的字母(模式切换时使用)
* @param options.redraw - 是否立即重绘 Canvas
*/
applyWorksheet(
worksheetId: string,
options?: {
preserveLetter?: boolean;
redraw?: boolean;
},
) {
const def = getWorksheetById(worksheetId);
console.log('def', def);
const base = (def?.generatorConfig ??
{}) as Partial<LetterTracingGeneratorConfig>;
const merged = mergeLetterTracingConfig(base, {});
const traceMode = merged.mode;
const showLetterPicker = traceMode === 'letter-tracing-single';
const showCaseToggle =
traceMode === 'letter-tracing-case-pairing' ||
traceMode === 'letter-tracing-half';
const showNextLetter = traceMode === 'letter-tracing-single';
const showTripleGroupPicker = traceMode === 'letter-tracing-three';
const selectedLetter = options?.preserveLetter
? (this.data.selectedLetter ?? 'A')
: normalizeLetter(merged.selectedLetter ?? '');
const letterCaseLower =
traceMode === 'letter-tracing-case-pairing' ||
traceMode === 'letter-tracing-half'
? merged.alphabetHalf === 'N-Z'
: traceMode === 'letter-tracing-single'
? merged.letterCase === 'lower'
: false;
this.setData(
{
worksheetId,
functionId: worksheetId,
traceMode,
selectedLetter,
selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`,
letterCaseLower,
showLetterPicker,
showCaseToggle,
showNextLetter,
showTripleGroupPicker,
},
() => {
this.initPageInfo(
worksheetId,
MODE_MAP[worksheetId]?.label ?? '字母描红',
);
if (this.drawService) {
this.drawService.options.title = this.data.pageTitle;
}
if (options?.redraw) {
this.drawCanvas();
}
},
);
},
},
{
shareConfig: defaultShareConfig,
pageInfoLookup,
},
);