173 lines
4.7 KiB
TypeScript
173 lines
4.7 KiB
TypeScript
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',
|
|
};
|
|
}
|