feat: 英文字母描红开发

This commit is contained in:
R524809
2026-04-03 15:21:04 +08:00
parent 206c44cf6d
commit 1ef6770810
37 changed files with 1812 additions and 179 deletions
@@ -0,0 +1,172 @@
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<LetterTracingGeneratorConfig>,
overrides: Partial<LetterTracingGeneratorConfig>,
): 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',
};
}