Files
doodle-mini/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts
T
2026-04-28 20:09:29 +08:00

461 lines
15 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 { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../../shared/data/alphabet';
/** 字母描红的布局模式(与产品文档六类练习一致) */
export type LetterTracingMode =
/** 1 看图识字:教学区 + 描红行(前三行大写、后三行小写) */
| 'letter-tracing-single'
/** 2 字母总览:Uppercase / Lowercase 分区,每区四行 7+7+6+6 */
| 'letter-tracing-upper-lower'
/** 3 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */
| 'letter-tracing-case-pairing'
/** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */
| 'letter-tracing-two-column'
/** 5 三字母精练:每页聚焦 3 个字母,每字母 4 行(2 行示范 + 2 行简练) */
| 'letter-tracing-three'
/** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */
| 'letter-tracing-half'
/** 7 每日打卡:整页四宫格,每宫一个字母,7 行 5 列闭合四线三格 */
| 'letter-tracing-daily-checkin';
/** 字母大小写模式(部分模式不使用) */
export type LetterCaseMode = 'upper' | 'lower' | 'both';
/** A–M / N–Z 半表(分栏对照) */
export type AlphabetHalf = 'A-M' | 'N-Z';
/** 字母描红生成器的配置项 */
export interface LetterTracingGeneratorConfig {
mode: LetterTracingMode;
letterCase: LetterCaseMode;
selectedLetter?: string;
repetitions: number;
fadePattern: 'gradient' | 'first-only';
/** 分栏对照 / 单字母逐行:练习 A–M 或 N–Z */
alphabetHalf?: AlphabetHalf;
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C'] */
tripleLetters?: string[];
/** 每日打卡:当前页聚焦的 4 个字母(如 ['A','B','C','D'] */
dailyLetters?: string[];
}
/** 单个描红格子的数据 */
export interface TracingCell {
char: string;
opacity: number;
isGuide: boolean;
/** 与 char 组成大小写一对(如 A + a),用于大小写配对格内展示 */
pairChar?: string;
}
/** 一行描红数据,包含展示字符和若干格子 */
export interface TracingRow {
displayChar: string;
cells: TracingCell[];
}
/** 单字母逐行:一行中一个大小写配对的展示格子 */
export interface SingleLineCell {
upper: string;
lower: string;
/** 'solid' 正常渲染 | 'dashed' 使用虚线字体 */
style: 'solid' | 'dashed';
color: string;
alpha: number;
}
/** 单字母逐行:一行数据(6 个位置,前 4 个有字母,后 2 个空) */
export interface SingleLineRow {
letter: string;
cells: (SingleLineCell | null)[];
}
/** 三字母精练:一个字母区域(4 行),前两行完整示范,后两行简练(黑+浅红) */
export interface TripleSection {
letter: string;
rows: SingleLineRow[];
}
/** 每日打卡:单行(5 列),支持实线/虚线/留空三种样式 */
export interface DailyCheckinRow {
style: 'solid' | 'dashed' | 'blank';
cells: (SingleLineCell | null)[];
}
/** 每日打卡:单个字母对应一个宫格 */
export interface DailyCheckinSection {
letter: string;
pageNumber: number;
rows: DailyCheckinRow[];
}
/** Highlight It 格子数据(3×6 网格,混入目标字母) */
export interface HighlightGrid {
rows: string[][];
targetLetter: string;
}
/** 看图描红教学区 */
export interface TeachingBlock {
upper: string;
lower: string;
word: string;
sentence: string;
emoji: string;
/** 字母笔顺图路径(/englishPages/shared/assets/letter/X.png */
letterImgPath: string;
/** 配图路径(/englishPages/shared/assets/letterImgs/Word.png */
wordImgPath: string;
}
export type LetterTracingData =
| {
mode: 'letter-tracing-single';
teaching: TeachingBlock;
highlightGrid: HighlightGrid;
/** Trace It 区域使用的字母笔顺图路径(同 teaching.letterImgPath */
traceImgPath: string;
/** 下部 4 行四线三格(无字符填充,纯空行) */
tracingLineCount: number;
}
| {
mode: 'letter-tracing-case-pairing';
leftRows: TracingRow[];
rightRows: TracingRow[];
}
| {
mode: 'letter-tracing-two-column';
leftRows: TracingRow[];
rightRows: TracingRow[];
}
| {
mode: 'letter-tracing-half';
rows: SingleLineRow[];
}
| {
mode: 'letter-tracing-three';
sections: TripleSection[];
}
| {
mode: 'letter-tracing-daily-checkin';
sections: DailyCheckinSection[];
}
| {
mode: 'letter-tracing-upper-lower';
upperRows: string[][];
lowerRows: string[][];
};
/** 将用户输入的字母标准化为大写单字符,默认 'A' */
export function normalizeLetter(c: string): string {
const u = c.trim().toUpperCase();
return u.length ? u[0] : 'A';
}
/** 构建 Highlight It 的 3×6 随机字母网格,保证混入 2~5 个目标字母 */
function buildHighlightGrid(
target: string,
gridRows: number,
gridCols: number,
): HighlightGrid {
const total = gridRows * gridCols;
const targetCount = 2 + Math.floor(Math.random() * 4); // 2~5
const others = LETTERS_UPPER.filter((l) => l !== target);
const chars: string[] = [];
for (let i = 0; i < targetCount; i++) chars.push(target);
for (let i = targetCount; i < total; i++) {
chars.push(others[Math.floor(Math.random() * others.length)]);
}
for (let i = chars.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[chars[i], chars[j]] = [chars[j], chars[i]];
}
const rows: string[][] = [];
for (let r = 0; r < gridRows; r++) {
rows.push(chars.slice(r * gridCols, (r + 1) * gridCols));
}
return { rows, targetLetter: target };
}
function buildCells(
char: string,
count: number,
fadePattern: 'gradient' | 'first-only',
pairChar?: string,
): TracingCell[] {
const n = Math.max(1, Math.min(12, count));
const cells: TracingCell[] = [];
for (let i = 0; i < n; i++) {
let opacity: number;
if (fadePattern === 'first-only') {
opacity = i === 0 ? 1 : 0;
} else if (n <= 1) {
opacity = 1;
} else {
opacity = Math.max(0.12, 1 - i / (n - 1));
}
cells.push({
char,
opacity,
isGuide: i === 0,
...(pairChar !== undefined ? { pairChar } : {}),
});
}
return cells;
}
function rowForChar(
ch: string,
repetitions: number,
fadePattern: 'gradient' | 'first-only',
pairChar?: string,
): TracingRow {
return {
displayChar: ch,
cells: buildCells(ch, repetitions, fadePattern, pairChar),
};
}
/** 总览区 26 字母拆成 7 + 7 + 6 + 6 四行(前两行 7 个、后两行 6 个,与四线三格排版一致) */
function overviewRowsFrom(letters: string[]): string[][] {
return [
letters.slice(0, 7),
letters.slice(7, 14),
letters.slice(14, 20),
letters.slice(20, 26),
];
}
const _COLOR_BLACK = '#1a1a1a';
const _COLOR_LIGHT_RED = '#FF8E8E';
/** 构建 6 slot 的单行字母数据:前 4 格有字母(颜色递进),后 2 格空白 */
function buildSingleLineCells(L: string): (SingleLineCell | null)[] {
const lower = L.toLowerCase();
return [
{ upper: L, lower, style: 'solid', color: _COLOR_BLACK, alpha: 1 },
{ upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 1 },
{
upper: L,
lower,
style: 'solid',
color: _COLOR_LIGHT_RED,
alpha: 0.5,
},
{
upper: L,
lower,
style: 'dashed',
color: _COLOR_LIGHT_RED,
alpha: 0.5,
},
null,
null,
];
}
/** 构建 6 slot 的简练行数据:前 2 格有字母(黑色+浅红色),后 4 格空白 */
function buildSimpleCells(L: string): (SingleLineCell | null)[] {
const lower = L.toLowerCase();
return [
{ upper: L, lower, style: 'solid', color: _COLOR_BLACK, alpha: 1 },
{ upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 1 },
null,
null,
null,
null,
];
}
function buildDailyFilledCells(
L: string,
style: 'solid' | 'dashed',
firstBlack: boolean = false,
): (SingleLineCell | null)[] {
const lower = L.toLowerCase();
return Array.from({ length: 5 }, (_, idx) => ({
upper: L,
lower,
style,
color: idx === 0 && firstBlack ? _COLOR_BLACK : _COLOR_LIGHT_RED,
alpha: 1,
}));
}
function buildDailyBlankCells(): (SingleLineCell | null)[] {
return Array.from({ length: 5 }, () => null);
}
function buildDailyCheckinRows(L: string): DailyCheckinRow[] {
return [
{ style: 'solid', cells: buildDailyFilledCells(L, 'solid', true) },
{ style: 'solid', cells: buildDailyFilledCells(L, 'solid') },
{ style: 'blank', cells: buildDailyBlankCells() },
{ style: 'dashed', cells: buildDailyFilledCells(L, 'dashed') },
{ style: 'blank', cells: buildDailyBlankCells() },
{ style: 'dashed', cells: buildDailyFilledCells(L, 'dashed') },
{ style: 'blank', cells: buildDailyBlankCells() },
];
}
/**
* 根据配置生成字母描红数据
* - picture-tracing:教学区 + 6 行描红
* - case-pairing:左 AM / 右 N–Z,每行最多 4 组大小写配对
* - column-compare:半表,左大写右小写,每行 4 个
* - overviewUppercase / Lowercase Letters,每区四行 7+7+6+6
*/
export function generateLetterTracing(
config: LetterTracingGeneratorConfig,
): LetterTracingData {
const { mode, repetitions, fadePattern } = config;
if (mode === 'letter-tracing-single') {
const key = normalizeLetter(config.selectedLetter ?? 'A');
const meta = ALPHABET_BY_LETTER[key] ?? ALPHABET_BY_LETTER.A;
const upper = key;
const lower = key.toLowerCase();
const letterImgPath = `/englishPages/shared/assets/letter/${key}.png`;
const wordImgPath = `/englishPages/shared/assets/letterImgs/Apple.png`;
const teaching: TeachingBlock = {
upper,
lower,
word: meta.word,
sentence: `${upper} is for ${meta.word}`,
emoji: meta.emoji,
letterImgPath,
wordImgPath,
};
const highlightGrid = buildHighlightGrid(upper, 3, 6);
return {
mode: 'letter-tracing-single',
teaching,
highlightGrid,
traceImgPath: letterImgPath,
tracingLineCount: 8,
};
}
if (mode === 'letter-tracing-case-pairing') {
const half =
config.alphabetHalf === 'N-Z'
? LETTERS_UPPER.slice(13)
: LETTERS_UPPER.slice(0, 13);
const leftRows = half.map((L) =>
rowForChar(L, repetitions, fadePattern),
);
const rightRows = half.map((L) =>
rowForChar(L.toLowerCase(), repetitions, fadePattern),
);
return { mode: 'letter-tracing-case-pairing', leftRows, rightRows };
}
if (mode === 'letter-tracing-two-column') {
const leftLetters = LETTERS_UPPER.slice(0, 13);
const rightLetters = LETTERS_UPPER.slice(13);
const twoColumnRepetitions = 4;
const toRows = (letters: string[]) =>
letters.map((L) =>
rowForChar(
L,
twoColumnRepetitions,
fadePattern,
L.toLowerCase(),
),
);
return {
mode: 'letter-tracing-two-column',
leftRows: toRows(leftLetters),
rightRows: toRows(rightLetters),
};
}
if (mode === 'letter-tracing-three') {
const letters = config.tripleLetters ?? ['A', 'B', 'C'];
const sections: TripleSection[] = letters.map((L) => {
const filledRow: SingleLineRow = {
letter: L,
cells: buildSingleLineCells(L),
};
const simpleRow: SingleLineRow = {
letter: L,
cells: buildSimpleCells(L),
};
return {
letter: L,
rows: [filledRow, filledRow, simpleRow, simpleRow],
};
});
return { mode: 'letter-tracing-three', sections };
}
if (mode === 'letter-tracing-half') {
const half =
config.alphabetHalf === 'N-Z'
? LETTERS_UPPER.slice(13)
: LETTERS_UPPER.slice(0, 13);
const rows: SingleLineRow[] = half.map((L) => ({
letter: L,
cells: buildSingleLineCells(L),
}));
return { mode: 'letter-tracing-half', rows };
}
if (mode === 'letter-tracing-daily-checkin') {
const letters = (config.dailyLetters ?? ['A', 'B', 'C', 'D']).map(
normalizeLetter,
);
const sections: DailyCheckinSection[] = letters.map((L) => ({
letter: L,
pageNumber: LETTERS_UPPER.indexOf(L) + 1,
rows: buildDailyCheckinRows(L),
}));
return { mode: 'letter-tracing-daily-checkin', sections };
}
// letter-tracing-upper-lower
return {
mode: 'letter-tracing-upper-lower',
upperRows: overviewRowsFrom([...LETTERS_UPPER]),
lowerRows: overviewRowsFrom(LETTERS_UPPER.map((L) => L.toLowerCase())),
};
}
/** 与页面内模式配置合并后的运行时配置 */
export function mergeLetterTracingConfig(
base: Partial<LetterTracingGeneratorConfig>,
overrides: Partial<LetterTracingGeneratorConfig>,
): LetterTracingGeneratorConfig {
return {
mode: (overrides.mode ??
base.mode ??
'letter-tracing-single') as LetterTracingMode,
letterCase: (overrides.letterCase ??
base.letterCase ??
'upper') as LetterCaseMode,
selectedLetter: overrides.selectedLetter ?? base.selectedLetter,
repetitions: Math.max(
1,
(overrides.repetitions ?? base.repetitions ?? 5) as number,
),
fadePattern: (overrides.fadePattern ??
base.fadePattern ??
'gradient') as 'gradient' | 'first-only',
alphabetHalf: (overrides.alphabetHalf ??
base.alphabetHalf ??
'A-M') as AlphabetHalf,
tripleLetters: overrides.tripleLetters ??
base.tripleLetters ?? ['A', 'B', 'C'],
dailyLetters: overrides.dailyLetters ??
base.dailyLetters ?? ['A', 'B', 'C', 'D'],
};
}