import { ALPHABET_BY_LETTER, LETTERS_UPPER, } from '../data/alphabet'; export type LetterTracingMode = | 'single-letter' | 'full-alphabet' | 'upper-lower-split' | 'two-column'; export type LetterCaseMode = 'upper' | 'lower' | 'both'; export interface LetterTracingGeneratorConfig { mode: LetterTracingMode; letterCase: LetterCaseMode; selectedLetter?: string; repetitions: number; fadePattern: 'gradient' | 'first-only'; } export interface TracingCell { char: string; opacity: number; isGuide: boolean; } export interface TracingRow { displayChar: string; cells: TracingCell[]; } export interface TeachingBlock { upper: string; lower: string; word: string; sentence: string; emoji: string; } export type LetterTracingData = | { mode: 'single-letter'; teaching: TeachingBlock; rows: TracingRow[]; } | { mode: 'full-alphabet'; rows: TracingRow[]; } | { mode: 'upper-lower-split'; upperGlyphs: string[]; lowerGlyphs: string[]; } | { mode: 'two-column'; leftRows: TracingRow[]; rightRows: TracingRow[]; }; function normalizeLetter(c: string): string { const u = c.trim().toUpperCase(); return u.length ? u[0] : 'A'; } function buildCells( char: string, count: number, fadePattern: 'gradient' | 'first-only', ): 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 }); } return cells; } function rowForChar( ch: string, repetitions: number, fadePattern: 'gradient' | 'first-only', ): TracingRow { return { displayChar: ch, cells: buildCells(ch, repetitions, fadePattern), }; } export function generateLetterTracing( config: LetterTracingGeneratorConfig, ): LetterTracingData { const { mode, letterCase, repetitions, fadePattern } = config; if (mode === 'single-letter') { const key = normalizeLetter(config.selectedLetter ?? 'A'); const meta = ALPHABET_BY_LETTER[key] ?? ALPHABET_BY_LETTER.A; const upper = key; const lower = key.toLowerCase(); const teaching: TeachingBlock = { upper, lower, word: meta.word, sentence: `${upper} is for ${meta.word}`, emoji: meta.emoji, }; const rows: TracingRow[] = [ rowForChar(upper, repetitions, fadePattern), rowForChar(lower, repetitions, fadePattern), ]; return { mode: 'single-letter', teaching, rows }; } if (mode === 'full-alphabet') { const rows = LETTERS_UPPER.map((L) => { const ch = letterCase === 'lower' ? L.toLowerCase() : L; return rowForChar(ch, repetitions, fadePattern); }); return { mode: 'full-alphabet', rows }; } if (mode === 'upper-lower-split') { return { mode: 'upper-lower-split', upperGlyphs: [...LETTERS_UPPER], lowerGlyphs: LETTERS_UPPER.map((L) => L.toLowerCase()), }; } // two-column const left = LETTERS_UPPER.slice(0, 13); const right = LETTERS_UPPER.slice(13); const mapChar = (L: string) => letterCase === 'lower' ? L.toLowerCase() : L; const leftRows = left.map((L) => rowForChar(mapChar(L), repetitions, fadePattern), ); const rightRows = right.map((L) => rowForChar(mapChar(L), repetitions, fadePattern), ); return { mode: 'two-column', leftRows, rightRows }; } /** 与 WorksheetDefinition.generatorConfig 合并后的运行时配置 */ export function mergeLetterTracingConfig( base: Partial, overrides: Partial, ): LetterTracingGeneratorConfig { return { mode: (overrides.mode ?? base.mode ?? 'single-letter') 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', }; }