From ec5bc44741986c9a3fe702ea7c662e62695bd0e0 Mon Sep 17 00:00:00 2001 From: R524809 Date: Tue, 14 Apr 2026 17:34:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=8B=B1=E8=AF=AD=E5=AD=97=E6=AF=8D?= =?UTF-8?q?=E6=8F=8F=E7=BA=A2=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/core/data/worksheets.ts | 38 ++- miniprogram/core/draw/baseDraw.ts | 2 +- miniprogram/core/draw/drawHeader.ts | 2 +- .../generators/letter-tracing-generator.ts | 172 ---------- .../letterTracing/letterTracing.ts | 86 +++-- .../letterTracing/letterTracing.wxml | 6 +- .../letterTracing/字母描红产品文档.md | 23 ++ .../englishPages/shared/assets/letter/A.png | Bin 0 -> 19594 bytes .../englishPages/shared/assets/letter/B.png | Bin 0 -> 16871 bytes .../shared/draw/letterTracingDraw.ts | 299 +++++++++++------- .../generators/letter-tracing-generator.ts | 260 +++++++++++++++ 11 files changed, 546 insertions(+), 342 deletions(-) delete mode 100644 miniprogram/core/generators/letter-tracing-generator.ts create mode 100644 miniprogram/englishPages/letterTracing/字母描红产品文档.md create mode 100644 miniprogram/englishPages/shared/assets/letter/A.png create mode 100644 miniprogram/englishPages/shared/assets/letter/B.png create mode 100644 miniprogram/englishPages/shared/generators/letter-tracing-generator.ts diff --git a/miniprogram/core/data/worksheets.ts b/miniprogram/core/data/worksheets.ts index d267823..ca014b4 100644 --- a/miniprogram/core/data/worksheets.ts +++ b/miniprogram/core/data/worksheets.ts @@ -258,8 +258,8 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ sheet( { id: 'letter-tracing-single', - title: '字母学习页', - desc: '单字母配图、阅读句与四线三格描红', + title: '看图描红', + desc: '单字母配图、例句与六行四线三格描红', category: 'english', page: 'letterTracing', subpackage: 'englishPages', @@ -280,7 +280,7 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ instructionText: '看一看,读一读,再描一描', }, generatorConfig: { - mode: 'single-letter', + mode: 'picture-tracing', letterCase: 'upper', selectedLetter: 'A', repetitions: 5, @@ -291,8 +291,8 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ sheet( { id: 'letter-tracing-full', - title: '26 字母描红', - desc: '整页四线三格逐字母描红', + title: '分栏对照', + desc: '半组字母左大写右小写,每行四个', category: 'english', page: 'letterTracing', subpackage: 'englishPages', @@ -308,11 +308,15 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ template: 'tracing-writing', generator: 'letter-tracing', sortOrder: 43, - layoutConfig: { ...L.trace, instructionText: '沿虚线描写字母' }, + layoutConfig: { + ...L.trace, + instructionText: '左栏大写、右栏小写,逐行对应描红', + }, generatorConfig: { - mode: 'full-alphabet', - letterCase: 'upper', - repetitions: 8, + mode: 'column-compare', + letterCase: 'both', + alphabetHalf: 'A-M', + repetitions: 4, fadePattern: 'gradient', }, }, @@ -320,8 +324,8 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ sheet( { id: 'letter-tracing-upper-lower', - title: '大小写字母对照', - desc: '上半部大写、下半部小写总览', + title: '字母总览', + desc: 'Uppercase / Lowercase 分区,9+9+8 行', category: 'english', page: 'letterTracing', subpackage: 'englishPages', @@ -340,10 +344,10 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ layoutConfig: { ...L.trace, columns: 13, - instructionText: '认识大写和小写字母', + instructionText: '认读全部大写与小写字母', }, generatorConfig: { - mode: 'upper-lower-split', + mode: 'overview', letterCase: 'both', repetitions: 1, fadePattern: 'first-only', @@ -353,8 +357,8 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ sheet( { id: 'letter-tracing-two-column', - title: '字母描红(两列)', - desc: 'A–M / N–Z 两列练习', + title: '大小写配对', + desc: '左 A–M、右 N–Z,每行四组大小写', category: 'english', page: 'letterTracing', subpackage: 'englishPages', @@ -376,8 +380,8 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [ instructionText: '沿虚线描写字母', }, generatorConfig: { - mode: 'two-column', - letterCase: 'upper', + mode: 'case-pairing', + letterCase: 'both', repetitions: 4, fadePattern: 'gradient', }, diff --git a/miniprogram/core/draw/baseDraw.ts b/miniprogram/core/draw/baseDraw.ts index 7fbff1e..296725b 100644 --- a/miniprogram/core/draw/baseDraw.ts +++ b/miniprogram/core/draw/baseDraw.ts @@ -14,7 +14,7 @@ import drawHeader from './drawHeader'; * - 打印配置管理 */ const LINE_COLOR = 'rgba(50, 46, 37, 0.4)'; -const LINE_WIDTH = 3; +const LINE_WIDTH = 2; export class BaseDrawService { canvas: WechatMiniprogram.Canvas; diff --git a/miniprogram/core/draw/drawHeader.ts b/miniprogram/core/draw/drawHeader.ts index 0d6f01e..00a92b7 100644 --- a/miniprogram/core/draw/drawHeader.ts +++ b/miniprogram/core/draw/drawHeader.ts @@ -24,7 +24,7 @@ const META_FONT_PX = 12; const META_UNDERLINE_GAP = 3; const TITLE_COLOR = '#322E25'; const LINE_COLOR = 'rgba(50, 46, 37, 0.4)'; -const LINE_WIDTH = 3; +const LINE_WIDTH = 2; const META_COLOR = '#7C766A'; const META_ROW_MARGIN_X = 130; diff --git a/miniprogram/core/generators/letter-tracing-generator.ts b/miniprogram/core/generators/letter-tracing-generator.ts deleted file mode 100644 index 4e73039..0000000 --- a/miniprogram/core/generators/letter-tracing-generator.ts +++ /dev/null @@ -1,172 +0,0 @@ -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', - }; -} diff --git a/miniprogram/englishPages/letterTracing/letterTracing.ts b/miniprogram/englishPages/letterTracing/letterTracing.ts index 2cafadc..dbf286d 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.ts +++ b/miniprogram/englishPages/letterTracing/letterTracing.ts @@ -5,39 +5,43 @@ import { getWorksheetById } from '../../core/data/worksheets'; import { generateLetterTracing, mergeLetterTracingConfig, + normalizeLetter, type LetterTracingGeneratorConfig, type LetterTracingMode, -} from '../../core/generators/letter-tracing-generator'; +} from '../shared/generators/letter-tracing-generator'; import { LETTERS_UPPER } from '../../core/data/alphabet'; +/** 各 worksheetId 对应的页面标题和描述 */ const PAGE_META: Record = { 'letter-tracing-single': { - title: '字母学习页', - desc: '单字母配图与描红', + title: '看图描红', + desc: '配图、例句与六行描红', }, 'letter-tracing-full': { - title: '26 字母描红', - desc: '整页字母书写练习', + title: '分栏对照', + desc: '半组字母左大写右小写', }, 'letter-tracing-upper-lower': { - title: '大小写字母对照', - desc: '一页认识 A-Z 与 a-z', + title: '字母总览', + desc: 'Uppercase / Lowercase 总览', }, 'letter-tracing-two-column': { - title: '字母描红(两列)', - desc: 'A–M / N–Z 两列练习', + title: '大小写配对', + desc: '左 A–M、右 N–Z 配对描红', }, }; +/** 根据 worksheetId 查找页面元信息 */ function pageInfoLookup(id: string) { return PAGE_META[id]; } +/** 模式选择器的选项列表 */ const MODE_OPTIONS = [ - { id: 'letter-tracing-single', label: '单字母学习页' }, - { id: 'letter-tracing-full', label: '整页描红' }, - { id: 'letter-tracing-upper-lower', label: '大小写对照' }, - { id: 'letter-tracing-two-column', label: '两列描红' }, + { id: 'letter-tracing-single', label: '看图描红' }, + { id: 'letter-tracing-full', label: '分栏对照' }, + { id: 'letter-tracing-upper-lower', label: '字母总览' }, + { id: 'letter-tracing-two-column', label: '大小写配对' }, ] as const; type PageData = CanvasDataState & { @@ -66,7 +70,7 @@ createPage( hasContent: false, showShareDialog: false, worksheetId: 'letter-tracing-single', - traceMode: 'single-letter', + traceMode: 'picture-tracing', selectedLetter: 'A', letterCaseLower: false, showLetterPicker: true, @@ -76,6 +80,7 @@ createPage( modeOptions: [...MODE_OPTIONS], } as unknown as PageData, + /** 页面加载:从路由参数获取 worksheetId 并应用 */ onLoad(options: { id?: string }) { const worksheetId = options.id && PAGE_META[options.id] @@ -84,6 +89,7 @@ createPage( this.applyWorksheet(worksheetId); }, + /** 用户点击模式选择器时切换 worksheet */ onSelectMode(e: WechatMiniprogram.TouchEvent) { const id = e.currentTarget.dataset.id as string | undefined; if (!id || id === this.data.worksheetId) return; @@ -93,6 +99,7 @@ createPage( }); }, + /** 页面就绪:初始化 Canvas 和绘制服务,完成后触发首次绘制 */ onReady() { this.initCanvas({ createDrawService: ( @@ -102,7 +109,6 @@ createPage( ) => new LetterTracingDraw(canvas, ctx, options), drawServiceOptions: { title: this.data.pageTitle, - subTitle: this.data.subTitle || '', }, onCanvasReady: () => { this.drawCanvas(); @@ -110,12 +116,13 @@ createPage( }); }, + /** 构建运行时生成器配置:合并 worksheet 预设 + 当前页面状态 */ buildRuntimeConfig(): LetterTracingGeneratorConfig { const def = getWorksheetById(this.data.worksheetId); const base = (def?.generatorConfig ?? {}) as Partial; const letterCase = - this.data.traceMode === 'single-letter' + this.data.traceMode === 'picture-tracing' ? (base.letterCase ?? 'upper') : this.data.letterCaseLower ? 'lower' @@ -123,14 +130,23 @@ createPage( return mergeLetterTracingConfig(base, { selectedLetter: this.data.selectedLetter, letterCase, + ...(this.data.traceMode === 'column-compare' + ? { + alphabetHalf: this.data.letterCaseLower + ? 'N-Z' + : 'A-M', + } + : {}), }); }, + /** 执行 Canvas 绘制:生成数据 → 调用 drawService 渲染 */ async drawCanvas() { if (!this.ctx || !this.drawService) return; try { const config = this.buildRuntimeConfig(); const data = generateLetterTracing(config); + console.log('drawCanvas data', data); await (this.drawService as LetterTracingDraw).draw(data); this.setData({ hasContent: true }); } catch (e) { @@ -139,6 +155,7 @@ createPage( } }, + /** 用户在字母网格中选择了一个字母 */ onSelectLetter(e: WechatMiniprogram.TouchEvent) { const letter = e.currentTarget.dataset.letter as string; if (!letter) return; @@ -146,16 +163,19 @@ createPage( this.drawCanvas(); }, + /** 切换为大写模式并重绘 */ onSelectCaseUpper() { this.setData({ letterCaseLower: false }); this.drawCanvas(); }, + /** 切换为小写模式并重绘 */ onSelectCaseLower() { this.setData({ letterCaseLower: true }); this.drawCanvas(); }, + /** 跳到下一个字母(循环 A→Z→A)并重绘 */ onNextLetter() { const list = LETTERS_UPPER; const i = list.indexOf(this.data.selectedLetter); @@ -164,6 +184,13 @@ createPage( this.drawCanvas(); }, + /** + * 应用指定的 worksheet 配置到页面状态 + * 根据 worksheet 的 generatorConfig 决定 UI 控件的显隐(字母选择器、大小写切换等) + * @param worksheetId - 要应用的 worksheet ID + * @param options.preserveLetter - 是否保留当前选中的字母(模式切换时使用) + * @param options.redraw - 是否立即重绘 Canvas + */ applyWorksheet( worksheetId: string, options?: { @@ -172,22 +199,24 @@ createPage( }, ) { const def = getWorksheetById(worksheetId); + console.log('def', def); const base = (def?.generatorConfig ?? {}) as Partial; const merged = mergeLetterTracingConfig(base, {}); const traceMode = merged.mode; - const showLetterPicker = traceMode === 'single-letter'; - const showCaseToggle = - traceMode === 'full-alphabet' || traceMode === 'two-column'; - const showNextLetter = traceMode === 'single-letter'; + const showLetterPicker = traceMode === 'picture-tracing'; + const showCaseToggle = traceMode === 'column-compare'; + const showNextLetter = traceMode === 'picture-tracing'; const selectedLetter = options?.preserveLetter - ? this.data.selectedLetter - : normalizePick(merged.selectedLetter); + ? (this.data.selectedLetter ?? 'A') + : normalizeLetter(merged.selectedLetter ?? ''); const letterCaseLower = - traceMode === 'single-letter' - ? merged.letterCase === 'lower' - : this.data.letterCaseLower; + traceMode === 'column-compare' + ? merged.alphabetHalf === 'N-Z' + : traceMode === 'picture-tracing' + ? merged.letterCase === 'lower' + : false; this.setData( { @@ -208,8 +237,6 @@ createPage( if (this.drawService) { this.drawService.options.title = this.data.pageTitle; - this.drawService.options.subTitle = - this.data.subTitle || ''; } if (options?.redraw) { @@ -224,8 +251,3 @@ createPage( pageInfoLookup, }, ); - -function normalizePick(c?: string): string { - if (!c || c.length === 0) return 'A'; - return c.trim().toUpperCase().charAt(0); -} diff --git a/miniprogram/englishPages/letterTracing/letterTracing.wxml b/miniprogram/englishPages/letterTracing/letterTracing.wxml index e2454ab..6c98f38 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.wxml +++ b/miniprogram/englishPages/letterTracing/letterTracing.wxml @@ -53,7 +53,7 @@ - 大小写 + 半组字母 - 大写 A–Z + A–M - 小写 a–z + N–Z diff --git a/miniprogram/englishPages/letterTracing/字母描红产品文档.md b/miniprogram/englishPages/letterTracing/字母描红产品文档.md new file mode 100644 index 0000000..8756557 --- /dev/null +++ b/miniprogram/englishPages/letterTracing/字母描红产品文档.md @@ -0,0 +1,23 @@ +## 练习类型 + +1. 大小写配对 — 26 字母双栏,每组大写+小写并排 +2. 分栏对照 — 半组字母,左栏大写右栏小写 +3. 字母总览 — 上下分区展示全部大小写 +4. 看图描红 — 单字母教学配图 + 描红练习 + +### 类型描述 + +1. 一页展示 26 个字母,每个包含大小写,分左右两栏,左栏A-M,右栏N-Z,每栏的一行展示 4 组字母,每一组都是一个大写和小写字母的组合。同一行是相同的字母 +2. 一页展示 A-M 或N-Z的字母,分左右两栏,左栏每行展示 4 个大小字母,右栏展示 4 个对应的小写字母。同一行是相同的字母 +3. 一页分上下两块区域展示,上面展示大写字母,下面展示小写字母,上面是Uppercase Letters 标题加三行,前面两行展示 9 个字母,最后一行展示剩余的 8 个字母。下面区域title是 lowercase Letters,行的展示逻辑和上面一致 +4. 一页分为上下两部分,上部分有分左右两部分,左边展示超大字体的大小写字母,下面展示一段方面记忆的短句,例如:“L is for Lion”,上部分右边展示对应的Lion 的图片。下部分展示 6 行字母描红,前三行大写,后三行小写。 + +## 生成要求 + +字母行统一使用 四线三格,颜色为偏黑色,其中最上边和最下边的线条为实线,中间的两个线条为虚线。 + +## 字母书写顺序资料 + +https://zhuanlan.zhihu.com/p/107632161 + +https://www.ld4.net/26zimu.html diff --git a/miniprogram/englishPages/shared/assets/letter/A.png b/miniprogram/englishPages/shared/assets/letter/A.png new file mode 100644 index 0000000000000000000000000000000000000000..c58d840f18d185d8cb09935025cedb63894f28fb GIT binary patch literal 19594 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 literal 0 HcmV?d00001 diff --git a/miniprogram/englishPages/shared/draw/letterTracingDraw.ts b/miniprogram/englishPages/shared/draw/letterTracingDraw.ts index e5353e0..2da623a 100644 --- a/miniprogram/englishPages/shared/draw/letterTracingDraw.ts +++ b/miniprogram/englishPages/shared/draw/letterTracingDraw.ts @@ -2,14 +2,15 @@ import { BaseDrawService } from '../../../core/draw/baseDraw'; import type { LetterTracingData, TracingRow, -} from '../../../core/generators/letter-tracing-generator'; +} from '../generators/letter-tracing-generator'; const M = 24; -const BLUE = '#4A90E2'; -const ORANGE = '#FF8C42'; +/** 四线三格线色:偏黑,实线与虚线统一色相,仅以线型区分 */ +const LINE_INK = '#2a2a2a'; /** - * 字母描红绘制(tracing-writing + letter-tracing 的客户端实现) + * 字母描红绘制 + * 四线三格:顶线、上中线、下中线为格内三线间距;最上最下为实线,中间两虚线(产品文档) */ export default class LetterTracingDraw extends BaseDrawService { constructor( @@ -25,53 +26,65 @@ export default class LetterTracingDraw extends BaseDrawService { await this.drawHeaderAndDivider(); switch (data.mode) { - case 'single-letter': - this.drawSingleLetter(data); + case 'picture-tracing': + this.drawPictureTracing(data); + this.drawPrintFooter(); break; - case 'full-alphabet': - this.drawFullAlphabet(data.rows); + case 'case-pairing': + this.drawCasePairing(data.leftVisualRows, data.rightVisualRows); + this.drawPrintFooter(); break; - case 'upper-lower-split': - this.drawUpperLowerSplit(data.upperGlyphs, data.lowerGlyphs); + case 'column-compare': + this.drawColumnCompare(data.leftRows, data.rightRows); + this.drawPrintFooter(); break; - case 'two-column': - this.drawTwoColumn(data.leftRows, data.rightRows); + case 'overview': + this.drawOverview(data.upperRows, data.lowerRows); + this.drawPrintFooter(); break; } } - /** 四线三格:顶线、中虚线、基线、底线 */ + /** + * 四线三格:y 顶 → y+h 底,中间两条虚线 + */ private drawFourLines(x: number, y: number, w: number, h: number) { const ctx = this.ctx; const yTop = y; - const yMid = y + h * 0.28; - const yBase = y + h * 0.62; + const yMid1 = y + h * 0.28; + const yMid2 = y + h * 0.58; const yBot = y + h; - ctx.strokeStyle = BLUE; - ctx.lineWidth = 1; - ctx.setLineDash([]); + const strokeSolid = () => { + ctx.strokeStyle = LINE_INK; + ctx.lineWidth = 1; + ctx.setLineDash([]); + }; + const strokeDash = () => { + ctx.strokeStyle = LINE_INK; + ctx.lineWidth = 1; + ctx.setLineDash([5, 5]); + }; + + strokeSolid(); ctx.beginPath(); ctx.moveTo(x, yTop); ctx.lineTo(x + w, yTop); ctx.stroke(); - ctx.strokeStyle = ORANGE; - ctx.setLineDash([6, 6]); + strokeDash(); ctx.beginPath(); - ctx.moveTo(x, yMid); - ctx.lineTo(x + w, yMid); + ctx.moveTo(x, yMid1); + ctx.lineTo(x + w, yMid1); ctx.stroke(); - ctx.strokeStyle = BLUE; + ctx.beginPath(); + ctx.moveTo(x, yMid2); + ctx.lineTo(x + w, yMid2); + ctx.stroke(); + + strokeSolid(); ctx.setLineDash([]); - ctx.lineWidth = 1.2; - ctx.beginPath(); - ctx.moveTo(x, yBase); - ctx.lineTo(x + w, yBase); - ctx.stroke(); - - ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, yBot); ctx.lineTo(x + w, yBot); @@ -86,18 +99,30 @@ export default class LetterTracingDraw extends BaseDrawService { h: number, opacity: number, bold: boolean, + pairChar?: string, ) { const ctx = this.ctx; if (opacity <= 0) return; - const fontSize = Math.min(h * 0.72, w * 0.85); ctx.save(); ctx.globalAlpha = opacity; - ctx.fillStyle = bold ? '#111' : '#666'; - ctx.font = `${bold ? 'bold ' : ''}${fontSize}px sans-serif`; + ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; - const baselineY = y + h * 0.62; - ctx.fillText(ch, x + w / 2, baselineY); + const baselineY = y + h * 0.72; + + if (pairChar) { + const fs = Math.min(h * 0.5, w * 0.22); + ctx.font = `${bold ? 'bold ' : ''}${fs}px sans-serif`; + const gap = fs * 0.2; + const totalW = fs * 2 + gap; + const x0 = x + w / 2 - totalW / 2; + ctx.fillText(ch, x0 + fs * 0.5, baselineY); + ctx.fillText(pairChar, x0 + fs * 1.5 + gap, baselineY); + } else { + const fontSize = Math.min(h * 0.72, w * 0.85); + ctx.font = `${bold ? 'bold ' : ''}${fontSize}px sans-serif`; + ctx.fillText(ch, x + w / 2, baselineY); + } ctx.restore(); } @@ -110,7 +135,7 @@ export default class LetterTracingDraw extends BaseDrawService { ) { const gap = 4; const n = row.cells.length; - const cellW = (totalWidth - gap * (n - 1)) / n; + const cellW = n > 0 ? (totalWidth - gap * (n - 1)) / n : totalWidth; for (let i = 0; i < n; i++) { const cx = startX + i * (cellW + gap); @@ -124,140 +149,182 @@ export default class LetterTracingDraw extends BaseDrawService { rowH, cell.opacity, cell.isGuide, + cell.pairChar, ); } } - private drawSingleLetter( - data: Extract, + /** + * 看图描红:上左超大字母+例句,上右「配图」大 emoji;下 6 行四线三格 + */ + private drawPictureTracing( + data: Extract, ) { const ctx = this.ctx; const { teaching, rows } = data; const cw = this.canvasWidth; let y = this.currentY + 8; - const teachH = Math.min(220, (this.canvasHeight - y) * 0.42); + const teachH = Math.min(240, (this.canvasHeight - y) * 0.38); + const innerW = cw - M * 2; + const splitX = M + innerW * 0.52; - // 教学区背景 ctx.fillStyle = '#f8fafc'; - ctx.fillRect(M, y, cw - M * 2, teachH); + ctx.fillRect(M, y, innerW, teachH); - // 左侧 emoji - ctx.font = `${Math.min(88, teachH * 0.45)}px sans-serif`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(teaching.emoji, M + (cw - M * 2) * 0.22, y + teachH / 2); - - // 右侧文字 - const tx = M + (cw - M * 2) * 0.42; + // 左侧:大小写 + 例句(短句在字母下方) + const leftPad = M + 20; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#111'; - ctx.font = 'bold 44px sans-serif'; - ctx.fillText(`${teaching.upper} ${teaching.lower}`, tx, y + 16); - ctx.font = '22px sans-serif'; + const bigFs = Math.min(56, teachH * 0.26); + ctx.font = `bold ${bigFs}px sans-serif`; + ctx.fillText( + `${teaching.upper} ${teaching.lower}`, + leftPad, + y + teachH * 0.12, + ); + ctx.font = `${Math.min(22, teachH * 0.1)}px sans-serif`; ctx.fillStyle = '#333'; - ctx.fillText(teaching.sentence, tx, y + 72); - ctx.fillStyle = '#666'; - ctx.font = '18px sans-serif'; - ctx.fillText(teaching.word, tx, y + 104); + ctx.fillText(teaching.sentence, leftPad, y + teachH * 0.48); + + // 右侧:配图区(以 emoji 作为插图占位) + ctx.font = `${Math.min(120, teachH * 0.55)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText( + teaching.emoji, + splitX + (cw - M - splitX) / 2, + y + teachH / 2, + ); y += teachH + 12; this.drawDashedDivider(y, M, '#ccc'); y += 14; const rest = this.canvasHeight - y - M; - const rowH = Math.max(36, (rest - 8) / rows.length); - const rowWidth = cw - M * 2; + const rowH = Math.max(34, (rest - 10) / rows.length); + const rowWidth = innerW; for (let r = 0; r < rows.length; r++) { this.drawTracingRow( rows[r], M, - y + r * (rowH + 10), + y + r * (rowH + 8), rowH, rowWidth, ); } } - private drawFullAlphabet(rows: TracingRow[]) { + /** + * 大小写配对:左右两栏,每行最多 4 组子行并排 + */ + private drawCasePairing( + leftVisualRows: TracingRow[][], + rightVisualRows: TracingRow[][], + ) { const y0 = this.currentY + 6; const bottom = this.canvasHeight - M; - const totalH = bottom - y0; - const rowH = Math.max(14, totalH / rows.length - 2); - const rowWidth = this.canvasWidth - M * 2; + const gapCol = 18; + const colW = (this.canvasWidth - M * 2 - gapCol) / 2; + const maxVisual = Math.max( + leftVisualRows.length, + rightVisualRows.length, + ); + const rowSlotH = Math.max(40, (bottom - y0) / maxVisual - 6); - for (let i = 0; i < rows.length; i++) { - const y = y0 + i * (rowH + 2); - this.drawTracingRow(rows[i], M, y, rowH, rowWidth); - } - } - - private drawUpperLowerSplit(upper: string[], lower: string[]) { - const ctx = this.ctx; - const cw = this.canvasWidth; - let y = this.currentY + 6; - const innerW = cw - M * 2; - - const drawBlockTitle = (t: string) => { - ctx.fillStyle = '#111'; - ctx.font = 'bold 18px sans-serif'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - ctx.fillText(t, M, y); - y += 26; - }; - - const drawGlyphGrid = (chars: string[], cols: number) => { - const rows = Math.ceil(chars.length / cols); - const cellW = (innerW - (cols - 1) * 4) / cols; - const cellH = 32; - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const idx = r * cols + c; - if (idx >= chars.length) break; - const cx = M + c * (cellW + 4); - this.drawFourLines(cx, y, cellW, cellH); - this.drawCharInCell( - chars[idx], - cx, + const drawSide = ( + blocks: TracingRow[][], + startX: number, + width: number, + ) => { + let y = y0; + for (const visualRow of blocks) { + const nSeg = visualRow.length; + const segGap = 6; + const segW = (width - (nSeg - 1) * segGap) / nSeg; + for (let s = 0; s < nSeg; s++) { + this.drawTracingRow( + visualRow[s], + startX + s * (segW + segGap), y, - cellW, - cellH, - 0.85, - true, + rowSlotH, + segW, ); } - y += cellH + 6; + y += rowSlotH + 8; } }; - drawBlockTitle('大写字母 Uppercase'); - drawGlyphGrid(upper, 13); - y += 8; - this.drawDashedDivider(y, M, '#bbb'); - y += 14; - drawBlockTitle('小写字母 Lowercase'); - drawGlyphGrid(lower, 13); + drawSide(leftVisualRows, M, colW); + drawSide(rightVisualRows, M + colW + gapCol, colW); } - private drawTwoColumn(leftRows: TracingRow[], rightRows: TracingRow[]) { + /** 分栏对照:左大写、右小写,逐行对齐 */ + private drawColumnCompare(leftRows: TracingRow[], rightRows: TracingRow[]) { const y0 = this.currentY + 6; const bottom = this.canvasHeight - M; - const totalH = bottom - y0; - const maxRows = Math.max(leftRows.length, rightRows.length); - const rowH = Math.max(16, totalH / maxRows - 3); + const maxR = Math.max(leftRows.length, rightRows.length); + const rowH = Math.max(36, (bottom - y0) / maxR - 6); const gapCol = 16; const colW = (this.canvasWidth - M * 2 - gapCol) / 2; for (let i = 0; i < leftRows.length; i++) { - const y = y0 + i * (rowH + 3); + const y = y0 + i * (rowH + 6); this.drawTracingRow(leftRows[i], M, y, rowH, colW); } const xR = M + colW + gapCol; for (let i = 0; i < rightRows.length; i++) { - const y = y0 + i * (rowH + 3); + const y = y0 + i * (rowH + 6); this.drawTracingRow(rightRows[i], xR, y, rowH, colW); } } + + /** + * 字母总览:Uppercase Letters / lowercase Letters,各行 9+9+8,四线三格 + */ + private drawOverview(upperRows: string[][], lowerRows: string[][]) { + const ctx = this.ctx; + let y = this.currentY + 6; + const innerW = this.canvasWidth - M * 2; + + const title = (t: string) => { + ctx.fillStyle = '#111'; + ctx.font = 'bold 17px sans-serif'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText(t, M, y); + y += 24; + }; + + const drawBand = (rows: string[][]) => { + for (const line of rows) { + const n = line.length; + const gap = 4; + const cellW = (innerW - (n - 1) * gap) / n; + const cellH = 30; + for (let i = 0; i < n; i++) { + const cx = M + i * (cellW + gap); + this.drawFourLines(cx, y, cellW, cellH); + this.drawCharInCell( + line[i], + cx, + y, + cellW, + cellH, + 0.9, + true, + ); + } + y += cellH + 8; + } + }; + + title('Uppercase Letters'); + drawBand(upperRows); + y += 6; + this.drawDashedDivider(y, M, '#bbb'); + y += 12; + title('lowercase Letters'); + drawBand(lowerRows); + } } diff --git a/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts new file mode 100644 index 0000000..53d80ee --- /dev/null +++ b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts @@ -0,0 +1,260 @@ +import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../../../core/data/alphabet'; + +/** 字母描红的布局模式(与产品文档四类练习一致) */ +export type LetterTracingMode = + /** 4 看图描红:教学区 + 6 行描红(前三行大写、后三行小写) */ + | 'picture-tracing' + /** 1 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */ + | 'case-pairing' + /** 2 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */ + | 'column-compare' + /** 3 字母总览:Uppercase / Lowercase 分区,每区 9+9+8 */ + | 'overview'; + +/** 字母大小写模式(部分模式不使用) */ +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; +} + +/** 单个描红格子的数据 */ +export interface TracingCell { + char: string; + opacity: number; + isGuide: boolean; + /** 与 char 组成大小写一对(如 A + a),用于大小写配对格内展示 */ + pairChar?: string; +} + +/** 一行描红数据,包含展示字符和若干格子 */ +export interface TracingRow { + displayChar: string; + cells: TracingCell[]; +} + +/** 看图描红教学区 */ +export interface TeachingBlock { + upper: string; + lower: string; + word: string; + sentence: string; + emoji: string; +} + +export type LetterTracingData = + | { + mode: 'picture-tracing'; + teaching: TeachingBlock; + rows: TracingRow[]; + } + | { + 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'; +} + +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 字母拆成 9 + 9 + 8 三行 */ +function overviewRowsFrom(letters: string[]): string[][] { + return [letters.slice(0, 9), letters.slice(9, 18), letters.slice(18, 26)]; +} + +/** + * 根据配置生成字母描红数据 + * - picture-tracing:教学区 + 6 行描红 + * - case-pairing:左 A–M / 右 N–Z,每行最多 4 组大小写配对 + * - column-compare:半表,左大写右小写,每行 4 个 + * - overview:Uppercase / Lowercase Letters,9+9+8 + */ +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 teaching: TeachingBlock = { + upper, + lower, + word: meta.word, + sentence: `${upper} is for ${meta.word}`, + emoji: meta.emoji, + }; + const rows: TracingRow[] = [ + rowForChar(upper, repetitions, fadePattern), + rowForChar(upper, repetitions, fadePattern), + rowForChar(upper, repetitions, fadePattern), + rowForChar(lower, repetitions, fadePattern), + rowForChar(lower, repetitions, fadePattern), + rowForChar(lower, repetitions, fadePattern), + ]; + return { mode: 'picture-tracing', teaching, rows }; + } + + 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 rowsOf4 = chunk(half, 4); + const leftRows = rowsOf4.map((g) => + rowForManyChars(g, repetitions, fadePattern), + ); + const rightRows = rowsOf4.map((g) => + rowForManyChars( + g.map((L) => 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, + }; +}