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'; /** 字母大小写模式(部分模式不使用) */ 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[]; } /** 单个描红格子的数据 */ 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[]; } /** 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-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), }; } /** 一行内多个字母,各自占一段描红格(用于分栏对照等) */ function rowForManyChars( chars: string[], repetitions: number, fadePattern: 'gradient' | 'first-only', ): TracingRow { const cells: TracingCell[] = []; for (const ch of chars) { cells.push(...buildCells(ch, repetitions, fadePattern)); } return { displayChar: chars.join(''), cells, }; } function chunk(arr: T[], size: number): T[][] { const out: T[][] = []; for (let i = 0; i < arr.length; i += size) { out.push(arr.slice(i, i + size)); } return out; } /** 总览区 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, ]; } /** * 根据配置生成字母描红数据 * - picture-tracing:教学区 + 6 行描红 * - case-pairing:左 A–M / 右 N–Z,每行最多 4 组大小写配对 * - column-compare:半表,左大写右小写,每行 4 个 * - overview:Uppercase / 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 hasLetterAsset = key === 'A' || key === 'B'; const letterFile = hasLetterAsset ? key : 'A'; const letterImgPath = `/englishPages/shared/assets/letter/${letterFile}.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 toRows = (letters: string[]) => letters.map((L) => rowForChar(L, repetitions, 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 }; } // letter-tracing-upper-lower return { mode: 'letter-tracing-upper-lower', upperRows: overviewRowsFrom([...LETTERS_UPPER]), lowerRows: overviewRowsFrom(LETTERS_UPPER.map((L) => L.toLowerCase())), }; } /** 与 WorksheetDefinition.generatorConfig 合并后的运行时配置 */ export function mergeLetterTracingConfig( base: Partial, overrides: Partial, ): 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'], }; }