import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../data/alphabet'; /** 字母描红的布局模式(与产品文档六类练习一致) */ export type LetterTracingMode = /** 1 看图识字:教学区 + 描红行(前三行大写、后三行小写) */ | 'picture-tracing' /** 2 字母总览:Uppercase / Lowercase 分区,每区四行 7+7+6+6 */ | 'overview' /** 3 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */ | 'case-pairing' /** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */ | 'column-compare' /** 5 三字母精练:每页聚焦 3 个字母,各含大写行 + 小写行 */ | 'triple-focus' /** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */ | 'single-line'; /** 字母大小写模式(部分模式不使用) */ 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, string, string]; } /** 单个描红格子的数据 */ export interface TracingCell { char: string; opacity: number; isGuide: boolean; /** 与 char 组成大小写一对(如 A + a),用于大小写配对格内展示 */ pairChar?: string; } /** 一行描红数据,包含展示字符和若干格子 */ export interface TracingRow { displayChar: string; cells: TracingCell[]; } /** 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: 'picture-tracing'; teaching: TeachingBlock; highlightGrid: HighlightGrid; /** Trace It 区域使用的字母笔顺图路径(同 teaching.letterImgPath) */ traceImgPath: string; /** 下部 4 行四线三格(无字符填充,纯空行) */ tracingLineCount: number; } | { mode: 'case-pairing'; leftVisualRows: TracingRow[][]; rightVisualRows: TracingRow[][]; } | { mode: 'column-compare'; leftRows: TracingRow[]; rightRows: TracingRow[]; } | { mode: 'overview'; 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), ]; } /** * 根据配置生成字母描红数据 * - 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 === 'picture-tracing') { 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: 'picture-tracing', teaching, highlightGrid, traceImgPath: letterImgPath, tracingLineCount: 8, }; } if (mode === 'case-pairing') { const leftLetters = LETTERS_UPPER.slice(0, 13); const rightLetters = LETTERS_UPPER.slice(13); const pack = (letters: string[]) => chunk(letters, 4).map((group) => group.map((L) => rowForChar(L, repetitions, fadePattern, L.toLowerCase()), ), ); return { mode: 'case-pairing', leftVisualRows: pack(leftLetters), rightVisualRows: pack(rightLetters), }; } if (mode === 'column-compare') { 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: 'column-compare', leftRows, rightRows }; } // overview return { mode: 'overview', 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 ?? 'picture-tracing') 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'], }; }