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
+39
View File
@@ -0,0 +1,39 @@
/**
* 26 字母与示例词(首版用 emoji 占位配图,后续可换 imageUrl)
*/
export interface AlphabetEntry {
word: string;
/** 与单词相关的 emoji,Canvas 上直接绘制,无需图片资源 */
emoji: string;
}
export const ALPHABET_BY_LETTER: Record<string, AlphabetEntry> = {
A: { word: 'Apple', emoji: '🍎' },
B: { word: 'Ball', emoji: '⚽' },
C: { word: 'Cat', emoji: '🐱' },
D: { word: 'Dog', emoji: '🐶' },
E: { word: 'Egg', emoji: '🥚' },
F: { word: 'Fish', emoji: '🐟' },
G: { word: 'Girl', emoji: '👧' },
H: { word: 'House', emoji: '🏠' },
I: { word: 'Ice', emoji: '🧊' },
J: { word: 'Juice', emoji: '🧃' },
K: { word: 'Kite', emoji: '🪁' },
L: { word: 'Lion', emoji: '🦁' },
M: { word: 'Moon', emoji: '🌙' },
N: { word: 'Nest', emoji: '🪺' },
O: { word: 'Orange', emoji: '🍊' },
P: { word: 'Pig', emoji: '🐷' },
Q: { word: 'Queen', emoji: '👑' },
R: { word: 'Rain', emoji: '🌧️' },
S: { word: 'Sun', emoji: '☀️' },
T: { word: 'Tree', emoji: '🌳' },
U: { word: 'Umbrella', emoji: '☂️' },
V: { word: 'Van', emoji: '🚐' },
W: { word: 'Water', emoji: '💧' },
X: { word: 'X-ray', emoji: '🦴' },
Y: { word: 'Yacht', emoji: '⛵' },
Z: { word: 'Zebra', emoji: '🦓' },
};
export const LETTERS_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
+138 -5
View File
@@ -254,10 +254,140 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [
{ id: 'copybook', title: '练字帖', desc: '选字生成田字格笔顺练字帖', category: 'chinese', page: 'copyBook', subpackage: '', icon: '✏️', img: '', ageRange: [4, 7], difficulty: 'basic', previewBg: '#fff8e1', previewText: '横竖撇' },
{ subcategory: 'copybook', template: 'tracing-writing', generator: 'character-tracing', sortOrder: 41, generatorConfig: { functionId: 'copybook' }, layoutConfig: { ...L.trace } },
),
sheet(
{
id: 'letter-tracing-single',
title: '字母学习页',
desc: '单字母配图、阅读句与四线三格描红',
category: 'english',
page: 'letterTracing',
subpackage: 'englishPages',
icon: '🔠',
img: '',
ageRange: [4, 7],
difficulty: 'beginner',
previewBg: '#e8f4ff',
previewText: 'Aa',
},
{
subcategory: 'letter-tracing',
template: 'tracing-writing',
generator: 'letter-tracing',
sortOrder: 42,
layoutConfig: {
...L.trace,
instructionText: '看一看,读一读,再描一描',
},
generatorConfig: {
mode: 'single-letter',
letterCase: 'upper',
selectedLetter: 'A',
repetitions: 5,
fadePattern: 'gradient',
},
},
),
sheet(
{
id: 'letter-tracing-full',
title: '26 字母描红',
desc: '整页四线三格逐字母描红',
category: 'english',
page: 'letterTracing',
subpackage: 'englishPages',
icon: '🔤',
img: '',
ageRange: [4, 7],
difficulty: 'beginner',
previewBg: '#e8f4ff',
previewText: 'A→Z',
},
{
subcategory: 'letter-tracing',
template: 'tracing-writing',
generator: 'letter-tracing',
sortOrder: 43,
layoutConfig: { ...L.trace, instructionText: '沿虚线描写字母' },
generatorConfig: {
mode: 'full-alphabet',
letterCase: 'upper',
repetitions: 8,
fadePattern: 'gradient',
},
},
),
sheet(
{
id: 'letter-tracing-upper-lower',
title: '大小写字母对照',
desc: '上半部大写、下半部小写总览',
category: 'english',
page: 'letterTracing',
subpackage: 'englishPages',
icon: '🔡',
img: '',
ageRange: [4, 7],
difficulty: 'beginner',
previewBg: '#e8f4ff',
previewText: 'Aa Bb',
},
{
subcategory: 'letter-tracing',
template: 'tracing-writing',
generator: 'letter-tracing',
sortOrder: 44,
layoutConfig: {
...L.trace,
columns: 13,
instructionText: '认识大写和小写字母',
},
generatorConfig: {
mode: 'upper-lower-split',
letterCase: 'both',
repetitions: 1,
fadePattern: 'first-only',
},
},
),
sheet(
{
id: 'letter-tracing-two-column',
title: '字母描红(两列)',
desc: 'AM / NZ 两列练习',
category: 'english',
page: 'letterTracing',
subpackage: 'englishPages',
icon: '✏️',
img: '',
ageRange: [4, 7],
difficulty: 'beginner',
previewBg: '#e8f4ff',
previewText: 'A|N',
},
{
subcategory: 'letter-tracing',
template: 'tracing-writing',
generator: 'letter-tracing',
sortOrder: 45,
layoutConfig: {
...L.trace,
columns: 2,
instructionText: '沿虚线描写字母',
},
generatorConfig: {
mode: 'two-column',
letterCase: 'upper',
repetitions: 4,
fadePattern: 'gradient',
},
},
),
];
export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS
.filter((w) => w.category === 'math')
export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS.filter(
(w) => w.category === 'math',
)
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((w) => ({
id: w.id,
@@ -268,8 +398,9 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS
img: w.img,
}));
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = ALL_WORKSHEETS
.filter((w) => w.category === 'focus')
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = ALL_WORKSHEETS.filter(
(w) => w.category === 'focus',
)
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((w) => ({
id: w.id,
@@ -281,7 +412,9 @@ export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = ALL_WORKSHEETS
img: w.img,
}));
export function getWorksheetsByCategory(category: string): WorksheetDefinition[] {
export function getWorksheetsByCategory(
category: string,
): WorksheetDefinition[] {
if (category === 'all') return ALL_WORKSHEETS;
return ALL_WORKSHEETS.filter((w) => w.category === category);
}
@@ -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',
};
}