diff --git a/miniprogram/englishPages/letterTracing/letterTracing.json b/miniprogram/englishPages/letterTracing/letterTracing.json index d46f4ce..c87b3cf 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.json +++ b/miniprogram/englishPages/letterTracing/letterTracing.json @@ -10,6 +10,7 @@ "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", "draw-ad": "../../components/draw-ad/draw-ad", "preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions", - "toy-icon": "../../toy/icon/icon" + "toy-icon": "../../toy/icon/icon", + "van-icon": "@vant/weapp/icon/index" } } diff --git a/miniprogram/englishPages/letterTracing/letterTracing.less b/miniprogram/englishPages/letterTracing/letterTracing.less index c0ef8fc..9466b4f 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.less +++ b/miniprogram/englishPages/letterTracing/letterTracing.less @@ -66,134 +66,93 @@ page { box-shadow: @shadow; } -/* ===== 参数面板 (bg-surface-container rounded-xl p-6 space-y-6) ===== */ -.lt-params { - /* surface-container = #f0e7d6 */ - background: @bg-card; - border-radius: @radius-xl; - padding: 48rpx; - box-sizing: border-box; +/* ===== 独立 Section 区块(参考 UI 稿 space-y-4) ===== */ +.lt-section { display: flex; flex-direction: column; - gap: 48rpx; - box-shadow: @shadow; + gap: 32rpx; } -/* 标题行 (gap-2 = 8px → 16rpx) */ -.lt-params-head { - display: flex; - flex-direction: row; - align-items: center; - gap: 16rpx; -} - -.lt-params-emoji { - /* text-xl = 20px → 40rpx */ - font-size: 40rpx; - line-height: 1; -} - -.lt-params-title { - /* text-lg = 18px → 36rpx */ - font-size: @fs-section-head-title; +.lt-section-title { + font-size: 32rpx; font-weight: 700; - color: @text-title; + color: #6d3b00; + padding-left: 8rpx; } -/* 参数区块内间距 (space-y-3 = 12px → 24rpx) */ -.param-block { +.lt-section-body { display: flex; flex-direction: column; - /* space-y-3 = 12px → 24rpx */ gap: 24rpx; } -/* 字段标签 (text-sm font-bold text-on-surface-variant ml-1) */ -.lt-field-label { - display: block; - /* text-sm = 14px → 28rpx */ - font-size: 28rpx; - font-weight: 700; - /* on-surface-variant = #605b50 */ - color: @text-secondary; - /* ml-1 = 4px → 8rpx */ - margin-left: 8rpx; -} - -/* ===== 练习类型 Tab (grid-cols-2 gap-2 bg-surface-container-high p-1.5 rounded-lg) ===== */ -.lt-mode-seg { +/* ===== 练习类型 2列卡片网格 (grid-cols-2 gap-4) ===== */ +.lt-mode-grid { display: grid; grid-template-columns: 1fr 1fr; - /* gap-2 = 8px → 16rpx */ - gap: 16rpx; - /* p-1.5 = 6px → 12rpx */ - padding: 12rpx; - /* surface-container-high = #eae2d0 */ - background: #eae2d0; - border-radius: 32rpx; - box-sizing: border-box; + gap: 32rpx; } -/* 未选中 item (py-2.5 px-3 text-sm font-medium rounded-lg text-on-surface-variant) */ -.lt-mode-item { +.lt-mode-card { display: flex; + flex-direction: column; align-items: center; justify-content: center; - /* py-2.5 = 10px → 20rpx; px-3 = 12px → 24rpx */ - padding: 20rpx 24rpx; - /* rounded-lg = 8px → 16rpx */ - border-radius: 32rpx; - background: transparent; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - // transition: all 1s ease; + gap: 16rpx; + padding: 32rpx 24rpx; + border-radius: 24rpx; + background: #f8f0e0; box-sizing: border-box; + transition: + transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), + background 0.2s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); } -/* 选中 item (bg-primary-container text-on-primary-container font-bold shadow-sm) */ -.lt-mode-item--active { - /* primary-container = #ffd709 */ - background: @brand; - box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08); +.lt-mode-card__icon { + display: inline-flex; + align-items: center; + justify-content: center; + margin-bottom: 4rpx; } -.lt-mode-item__text { - /* text-sm = 14px → 28rpx */ - font-size: 28rpx; - /* 未选中 font-medium = 500 */ - font-weight: 500; - /* on-surface-variant = #605b50 */ - color: @text-secondary; //#5b4b00 - line-height: 40rpx; +.lt-mode-card--active { + background: linear-gradient(145deg, #ffd709 0%, #efc900 100%); + box-shadow: 0 8rpx 32rpx rgba(50, 46, 37, 0.06); +} + +.lt-mode-card--pressed { + transform: scale(0.95); +} + +.lt-mode-card__label { + font-size: 30rpx; + font-weight: 700; + color: @text-secondary; text-align: center; } -/* 选中态字色 (on-primary-container = #5b4b00, font-bold = 700) */ -.lt-mode-item--active .lt-mode-item__text { - /* on-primary-container = #5b4b00 */ - color: @text-selected-btn; - font-weight: 700; +.lt-mode-card--active .lt-mode-card__label { + color: #453900; } -/* ===== 字母选择网格 (grid-cols-6 gap-2) ===== */ +/* ===== 字母选择网格 (grid-cols-6 gap-3) ===== */ .lt-letter-grid { display: grid; - /* 稿 grid-cols-6 */ grid-template-columns: repeat(6, 1fr); - /* gap-2 = 8px → 16rpx */ - gap: 16rpx; + gap: 28rpx; + padding: 0 8rpx; } -/* 字母格子 (aspect-square rounded-lg bg-surface-container-lowest) */ +/* 字母格子 (aspect-square rounded-[12px] bg-surface-container) */ .lt-letter-cell { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; - border-radius: 32rpx; - /* surface-container-lowest = #ffffff */ - background: @bg-white; + border-radius: 24rpx; + background: @bg-card; box-sizing: border-box; - /* 不用 :active:小程序 view 上背景色常在松手后才刷新;用 hover-class 才能按下即变色,并做过渡 */ transition: background-color 0.2s ease, transform 0.2s ease, @@ -215,10 +174,8 @@ page { } .lt-letter-text { - /* text-lg = 18px → 36rpx */ - font-size: 36rpx; + font-size: 32rpx; font-weight: 700; - /* on-surface-variant = #605b50 */ color: @text-secondary; line-height: 1; transition: color 0.2s ease; @@ -233,27 +190,79 @@ page { color: @text-selected-btn; } -/* ===== 大小写切换 ===== */ +/* ===== 展开/收起按钮(UI 稿 rounded-full bg-surface-container-low) ===== */ +.lt-expand-toggle { + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + padding: 12rpx 32rpx; + border-radius: 999rpx; + background: #f8f0e0; + align-self: center; + margin: 16rpx auto 0; + transition: + background-color 0.2s ease, + transform 0.2s ease; +} + +.lt-expand-toggle--hover { + background: @bg-card; + transform: scale(0.96); +} + +.lt-expand-toggle__text { + font-size: 28rpx; + font-weight: 500; + color: @text-secondary; + line-height: 1; +} + +.lt-expand-toggle__icon { + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* ===== 大小写切换 / 字母分组选择 ===== */ .lt-chip-row { display: flex; flex-direction: row; /* gap-2 = 8px → 16rpx */ - gap: 16rpx; + gap: 32rpx; } -/* 未选中 chip (同练习类型 item 风格) */ +.lt-chip-row--wrap { + flex-wrap: wrap; + + .lt-chip { + flex: none; + } +} + +.lt-chip-row--triple { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24rpx; + + .lt-chip { + flex: unset; + width: 100%; + padding: 24rpx 16rpx; + } +} + +/* 未选中 chip(与字母格子统一底色) */ .lt-chip { flex: 1; + box-sizing: border-box; text-align: center; - /* py-2.5 px-3 */ padding: 20rpx 24rpx; - /* text-sm = 14px → 28rpx */ font-size: 28rpx; - font-weight: 500; + font-weight: 700; line-height: 40rpx; color: @text-secondary; - background: #fff; - /* rounded-lg */ + background: @bg-card; border-radius: 32rpx; transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), diff --git a/miniprogram/englishPages/letterTracing/letterTracing.ts b/miniprogram/englishPages/letterTracing/letterTracing.ts index dad1c1a..fbf7d91 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.ts +++ b/miniprogram/englishPages/letterTracing/letterTracing.ts @@ -9,7 +9,7 @@ import { type LetterTracingGeneratorConfig, type LetterTracingMode, } from '../shared/generators/letter-tracing-generator'; -import { LETTERS_UPPER } from '../shared/data/alphabet'; +import { LETTERS_UPPER, LETTERS_PAIRS } from '../shared/data/alphabet'; import { DEFAULT_LETTER_PROFILE, getFontProfile, @@ -21,34 +21,35 @@ import { loadLetterFont } from '../shared/draw/drawTools'; const MODES = [ { id: 'letter-tracing-single', - label: '看图识字', + label: '默认字帖', desc: '配图、例句与描红', }, { id: 'letter-tracing-upper-lower', - label: '字母总览', + // label: '字母总览', + label: '基础描红', desc: 'Uppercase / Lowercase 总览', }, { id: 'letter-tracing-case-pairing', - label: '大小写练习', + label: '大小写对照', desc: '半组字母左大写右小写', }, { id: 'letter-tracing-two-column', - label: '两列练习', + label: '两列描红', desc: '左 A–M、右 N–Z 配对描红', }, + { + id: 'letter-tracing-single-line', + label: '13字母半表', + desc: '每行一个字母,13 字母半表', + }, { id: 'letter-tracing-triple', label: '三字母精练', desc: '每页聚焦 3 个字母深度书写', }, - { - id: 'letter-tracing-single-line', - label: '单字母逐行', - desc: '每行一个字母,13 字母半表', - }, ] as const; const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record< @@ -56,6 +57,15 @@ const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record< (typeof MODES)[number] >; +/** 三字母精练分组:26 字母每 3 个一组 */ +const TRIPLE_GROUPS: { label: string; letters: string[] }[] = []; +for (let i = 0; i < LETTERS_UPPER.length; i += 3) { + const group = LETTERS_UPPER.slice(i, i + 3); + const label = + group.length > 1 ? `${group[0]}–${group[group.length - 1]}` : group[0]; + TRIPLE_GROUPS.push({ label, letters: [...group] }); +} + /** 根据 worksheetId 查找页面元信息 */ function pageInfoLookup(id: string) { const m = MODE_MAP[id]; @@ -66,11 +76,17 @@ type PageData = CanvasDataState & { worksheetId: string; traceMode: LetterTracingMode; selectedLetter: string; + selectedLetterPair: string; letterCaseLower: boolean; showLetterPicker: boolean; showCaseToggle: boolean; showNextLetter: boolean; + showTripleGroupPicker: boolean; + tripleGroups: { label: string; letters: string[] }[]; + selectedTripleGroupIdx: number; letterGrid: string[]; + letterGridExpanded: boolean; + letterGridPreview: string[]; modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>; }; @@ -91,11 +107,17 @@ createPage( worksheetId: 'letter-tracing-single', traceMode: 'letter-tracing-single', selectedLetter: 'A', + selectedLetterPair: 'Aa', letterCaseLower: false, showLetterPicker: true, showCaseToggle: false, showNextLetter: true, - letterGrid: LETTERS_UPPER, + showTripleGroupPicker: false, + tripleGroups: TRIPLE_GROUPS, + selectedTripleGroupIdx: 0, + letterGrid: LETTERS_PAIRS, + letterGridExpanded: false, + letterGridPreview: LETTERS_PAIRS.slice(0, 6), modeOptions: [...MODES], } as unknown as PageData, @@ -119,7 +141,10 @@ createPage( const updates: Partial = {}; if (options.letter) { const normalized = normalizeLetter(options.letter); - if (normalized) updates.selectedLetter = normalized; + if (normalized) { + updates.selectedLetter = normalized; + updates.selectedLetterPair = `${normalized}${normalized.toLowerCase()}`; + } } if (options.case === 'lower') { updates.letterCaseLower = true; @@ -174,6 +199,7 @@ createPage( const needsHalf = this.data.traceMode === 'letter-tracing-case-pairing' || this.data.traceMode === 'letter-tracing-single-line'; + const isTriple = this.data.traceMode === 'letter-tracing-triple'; return mergeLetterTracingConfig(base, { selectedLetter: this.data.selectedLetter, letterCase, @@ -184,6 +210,13 @@ createPage( : 'A-M', } : {}), + ...(isTriple + ? { + tripleLetters: + TRIPLE_GROUPS[this.data.selectedTripleGroupIdx] + ?.letters, + } + : {}), }); }, @@ -203,11 +236,22 @@ createPage( } }, - /** 用户在字母网格中选择了一个字母 */ + /** 展开/收起字母网格 */ + toggleLetterGrid() { + this.setData({ + letterGridExpanded: !this.data.letterGridExpanded, + }); + }, + + /** 用户在字母网格中选择了一个字母(item 为 "Aa" 格式,取首字母作为 selectedLetter) */ onSelectLetter(e: WechatMiniprogram.TouchEvent) { - const letter = e.currentTarget.dataset.letter as string; - if (!letter) return; - this.setData({ selectedLetter: letter }); + const raw = e.currentTarget.dataset.letter as string; + if (!raw) return; + const letter = raw.charAt(0).toUpperCase(); + this.setData({ + selectedLetter: letter, + selectedLetterPair: `${letter}${letter.toLowerCase()}`, + }); this.drawCanvas(); }, @@ -228,7 +272,18 @@ createPage( const list = LETTERS_UPPER; const i = list.indexOf(this.data.selectedLetter); const next = list[(i + 1) % list.length]; - this.setData({ selectedLetter: next }); + this.setData({ + selectedLetter: next, + selectedLetterPair: `${next}${next.toLowerCase()}`, + }); + this.drawCanvas(); + }, + + /** 用户在三字母分组选择器中选择了一组 */ + onSelectTripleGroup(e: WechatMiniprogram.TouchEvent) { + const idx = Number(e.currentTarget.dataset.idx); + if (isNaN(idx) || idx === this.data.selectedTripleGroupIdx) return; + this.setData({ selectedTripleGroupIdx: idx }); this.drawCanvas(); }, @@ -253,13 +308,12 @@ createPage( const merged = mergeLetterTracingConfig(base, {}); const traceMode = merged.mode; - const showLetterPicker = - traceMode === 'letter-tracing-single' || - traceMode === 'letter-tracing-triple'; + const showLetterPicker = traceMode === 'letter-tracing-single'; const showCaseToggle = traceMode === 'letter-tracing-case-pairing' || traceMode === 'letter-tracing-single-line'; const showNextLetter = traceMode === 'letter-tracing-single'; + const showTripleGroupPicker = traceMode === 'letter-tracing-triple'; const selectedLetter = options?.preserveLetter ? (this.data.selectedLetter ?? 'A') : normalizeLetter(merged.selectedLetter ?? ''); @@ -277,10 +331,12 @@ createPage( functionId: worksheetId, traceMode, selectedLetter, + selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`, letterCaseLower, showLetterPicker, showCaseToggle, showNextLetter, + showTripleGroupPicker, }, () => { this.initPageInfo( diff --git a/miniprogram/englishPages/letterTracing/letterTracing.wxml b/miniprogram/englishPages/letterTracing/letterTracing.wxml index 6c98f38..9fe689b 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.wxml +++ b/miniprogram/englishPages/letterTracing/letterTracing.wxml @@ -15,33 +15,38 @@ - - - ⚙️ - 参数设置 - - - - 练习类型 - - - {{item.label}} - + + + 练习类型 + + + + {{item.label}} + - - 选择字母 - + + + 选择字母 + + {{item}} + + + {{item}} + + + + {{letterGridExpanded ? '收起' : '展开全部'}} + + + + - - 半组字母 - - - A–M - - - N–Z - + + + 选择字母组 + + + A–M + + + N–Z + + + + + + + 选择字母组 + + + {{item.label}} diff --git a/miniprogram/englishPages/shared/data/alphabet.ts b/miniprogram/englishPages/shared/data/alphabet.ts index de8ce90..fefedf5 100644 --- a/miniprogram/englishPages/shared/data/alphabet.ts +++ b/miniprogram/englishPages/shared/data/alphabet.ts @@ -37,3 +37,4 @@ export const ALPHABET_BY_LETTER: Record = { }; export const LETTERS_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); +export const LETTERS_PAIRS = LETTERS_UPPER.map((l) => `${l}${l.toLowerCase()}`); diff --git a/miniprogram/englishPages/shared/draw/letterTracingDraw.ts b/miniprogram/englishPages/shared/draw/letterTracingDraw.ts index 3d91afa..0977695 100644 --- a/miniprogram/englishPages/shared/draw/letterTracingDraw.ts +++ b/miniprogram/englishPages/shared/draw/letterTracingDraw.ts @@ -4,6 +4,8 @@ import singleLetterDraw from './singleLetterDraw'; import CasePairingDraw from './casePairingDraw'; import TwoColumnDraw from './twoColumnDraw'; import UpperLowerDraw from './upperLowerDraw'; +import SingleLineDraw from './singleLineDraw'; +import TripleDraw from './tripleDraw'; /** * 字母描红绘制分发层。 @@ -16,6 +18,8 @@ export default class LetterTracingDraw extends BaseDrawService { 'letter-tracing-upper-lower': UpperLowerDraw, 'letter-tracing-case-pairing': CasePairingDraw, 'letter-tracing-two-column': TwoColumnDraw, + 'letter-tracing-single-line': SingleLineDraw, + 'letter-tracing-triple': TripleDraw, }[data.mode]; const delegate = new DrawClass(this.canvas, this.ctx, this.options); diff --git a/miniprogram/englishPages/shared/draw/singleLineDraw.ts b/miniprogram/englishPages/shared/draw/singleLineDraw.ts new file mode 100644 index 0000000..5f65b2a --- /dev/null +++ b/miniprogram/englishPages/shared/draw/singleLineDraw.ts @@ -0,0 +1,110 @@ +import { BaseDrawService } from '../../../core/draw/baseDraw'; +import type { + LetterTracingData, + SingleLineRow, +} from '../generators/letter-tracing-generator'; +import { + fontFamilyOf, + PRINT_CLEARLY_DASHED, +} from '../data/fontProfiles'; +import { + calcLetterFontSizes, + drawFourLineGrid, + drawLetterInFourLineGrid, + FOUR_LINE_GRID_H, + loadLetterFont, + type LetterFontSizes, +} from './drawTools'; + +const SLOTS_PER_ROW = 6; +const PAIR_GAP = 25; + +type SingleLineData = Extract< + LetterTracingData, + { mode: 'letter-tracing-single-line' } +>; + +/** + * 单字母逐行绘制: + * 13 行四线三格横跨整页宽度,每行划分为 6 组位置。 + * 前 4 组展示大小写字母(Aa),后 2 组空白供用户书写。 + * 颜色递进:黑色 → 浅红 → 半透明浅红 → 虚线字体+半透明浅红。 + */ +export default class SingleLineDraw extends BaseDrawService { + private letterSizes: LetterFontSizes = + calcLetterFontSizes(FOUR_LINE_GRID_H); + + private dashedSizes: LetterFontSizes = + calcLetterFontSizes(FOUR_LINE_GRID_H, PRINT_CLEARLY_DASHED); + + async draw(data: SingleLineData) { + this.prepareDraw(); + await Promise.all([ + this.drawHeaderAndDivider(), + loadLetterFont(PRINT_CLEARLY_DASHED), + ]); + this.drawContent(data.rows); + this.drawPrintFooter(); + } + + private drawContent(rows: SingleLineRow[]) { + const M = 24; + const lineW = this.canvasWidth - M * 2; + const rowCount = rows.length; + + const footerReserve = 56; + const availableH = + this.canvasHeight - this.currentY - footerReserve - 12; + const rowGap = Math.max( + 4, + (availableH - rowCount * FOUR_LINE_GRID_H) / (rowCount + 1), + ); + const y0 = this.currentY + rowGap; + + const slotW = lineW / SLOTS_PER_ROW; + + for (let i = 0; i < rowCount; i++) { + const y = y0 + i * (FOUR_LINE_GRID_H + rowGap); + + drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H); + + const row = rows[i]; + for (let j = 0; j < row.cells.length; j++) { + const cell = row.cells[j]; + if (!cell) continue; + + const pairCx = M + j * slotW + slotW / 2; + const isDashed = cell.style === 'dashed'; + const sizes = isDashed ? this.dashedSizes : this.letterSizes; + const fontFamily = fontFamilyOf( + isDashed ? PRINT_CLEARLY_DASHED : sizes._profile, + ); + + this.ctx.save(); + this.ctx.globalAlpha = cell.alpha; + + drawLetterInFourLineGrid( + this.ctx, + cell.upper, + pairCx - PAIR_GAP / 2, + y, + FOUR_LINE_GRID_H, + sizes, + { fontFamily, color: cell.color }, + ); + + drawLetterInFourLineGrid( + this.ctx, + cell.lower, + pairCx + PAIR_GAP / 2, + y, + FOUR_LINE_GRID_H, + sizes, + { fontFamily, color: cell.color }, + ); + + this.ctx.restore(); + } + } + } +} diff --git a/miniprogram/englishPages/shared/draw/tripleDraw.ts b/miniprogram/englishPages/shared/draw/tripleDraw.ts new file mode 100644 index 0000000..c6cf6a3 --- /dev/null +++ b/miniprogram/englishPages/shared/draw/tripleDraw.ts @@ -0,0 +1,122 @@ +import { BaseDrawService } from '../../../core/draw/baseDraw'; +import type { + LetterTracingData, + TripleSection, + SingleLineRow, +} from '../generators/letter-tracing-generator'; +import { fontFamilyOf, PRINT_CLEARLY_DASHED } from '../data/fontProfiles'; +import { + calcLetterFontSizes, + drawFourLineGrid, + drawLetterInFourLineGrid, + FOUR_LINE_GRID_H, + loadLetterFont, + type LetterFontSizes, +} from './drawTools'; + +// 每行可容纳的格子数(用于三字母精练模板,一行展示 6 个位置) +const SLOTS_PER_ROW = 6; +// 大小写字母配对间的间隔(px,字母间距) +const PAIR_GAP = 25; +// 同一字母区域内相邻两行四线三格之间的间距(px) +const TRIPLE_ROW_GAP = 10; +// 相邻两个字母区域之间的间距(px),大于 TRIPLE_ROW_GAP +const TRIPLE_SECTION_GAP = 20; + +type TripleData = Extract; + +/** + * 三字母精练绘制: + * 3 个字母区域,每区域 4 行四线三格。 + * 前两行展示完整颜色递进(黑→浅红→半透明浅红→虚线), + * 后两行只展示黑色+浅红色两组,其余空白供用户书写。 + * 区域之间间距大于行间距。 + */ +export default class TripleDraw extends BaseDrawService { + private letterSizes: LetterFontSizes = + calcLetterFontSizes(FOUR_LINE_GRID_H); + + private dashedSizes: LetterFontSizes = calcLetterFontSizes( + FOUR_LINE_GRID_H, + PRINT_CLEARLY_DASHED, + ); + + async draw(data: TripleData) { + this.prepareDraw(); + await Promise.all([ + this.drawHeaderAndDivider(), + loadLetterFont(PRINT_CLEARLY_DASHED), + ]); + this.drawContent(data.sections); + this.drawPrintFooter(); + } + + private drawContent(sections: TripleSection[]) { + const M = 24; + const lineW = this.canvasWidth - M * 2; + const slotW = lineW / SLOTS_PER_ROW; + + let y = this.currentY + 10; + + for (let s = 0; s < sections.length; s++) { + const section = sections[s]; + + for (let r = 0; r < section.rows.length; r++) { + drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H); + this.drawRow(section.rows[r], M, y, slotW); + y += FOUR_LINE_GRID_H; + if (r < section.rows.length - 1) { + y += TRIPLE_ROW_GAP; + } + } + + if (s < sections.length - 1) { + y += TRIPLE_SECTION_GAP; + } + } + } + + private drawRow( + row: SingleLineRow, + marginX: number, + y: number, + slotW: number, + ) { + for (let j = 0; j < row.cells.length; j++) { + const cell = row.cells[j]; + if (!cell) continue; + + const pairCx = marginX + j * slotW + slotW / 2; + const isDashed = cell.style === 'dashed'; + const sizes = isDashed ? this.dashedSizes : this.letterSizes; + const fontFamily = fontFamilyOf( + isDashed ? PRINT_CLEARLY_DASHED : sizes._profile, + ); + + this.ctx.save(); + this.ctx.globalAlpha = cell.alpha; + + drawLetterInFourLineGrid( + this.ctx, + cell.upper, + pairCx - PAIR_GAP / 2, + y, + FOUR_LINE_GRID_H, + sizes, + { fontFamily, color: cell.color }, + ); + + drawLetterInFourLineGrid( + this.ctx, + cell.lower, + pairCx + PAIR_GAP / 2, + y, + FOUR_LINE_GRID_H, + sizes, + { fontFamily, color: cell.color }, + ); + + this.ctx.restore(); + } + } +} diff --git a/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts index 7ccd63f..0a0ca09 100644 --- a/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts +++ b/miniprogram/englishPages/shared/generators/letter-tracing-generator.ts @@ -10,7 +10,7 @@ export type LetterTracingMode = | 'letter-tracing-case-pairing' /** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */ | 'letter-tracing-two-column' - /** 5 三字母精练:每页聚焦 3 个字母,各含大写行 + 小写行 */ + /** 5 三字母精练:每页聚焦 3 个字母,每字母 4 行(2 行示范 + 2 行简练) */ | 'letter-tracing-triple' /** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */ | 'letter-tracing-single-line'; @@ -31,7 +31,7 @@ export interface LetterTracingGeneratorConfig { /** 分栏对照 / 单字母逐行:练习 A–M 或 N–Z */ alphabetHalf?: AlphabetHalf; /** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */ - tripleLetters?: [string, string, string]; + tripleLetters?: string[]; } /** 单个描红格子的数据 */ @@ -49,6 +49,28 @@ export interface TracingRow { cells: TracingCell[]; } +/** 单字母逐行:一行中一个大小写配对的展示格子 */ +export interface SingleLineCell { + upper: string; + lower: string; + /** 'solid' 正常渲染 | 'dashed' 使用虚线字体 */ + style: 'solid' | 'dashed'; + color: string; + alpha: number; +} + +/** 单字母逐行:一行数据(6 个位置,前 4 个有字母,后 2 个空) */ +export interface SingleLineRow { + letter: string; + cells: (SingleLineCell | null)[]; +} + +/** 三字母精练:一个字母区域(4 行),前两行完整示范,后两行简练(黑+浅红) */ +export interface TripleSection { + letter: string; + rows: SingleLineRow[]; +} + /** Highlight It 格子数据(3×6 网格,混入目标字母) */ export interface HighlightGrid { rows: string[][]; @@ -88,6 +110,14 @@ export type LetterTracingData = leftRows: TracingRow[]; rightRows: TracingRow[]; } + | { + mode: 'letter-tracing-single-line'; + rows: SingleLineRow[]; + } + | { + mode: 'letter-tracing-triple'; + sections: TripleSection[]; + } | { mode: 'letter-tracing-upper-lower'; upperRows: string[][]; @@ -200,6 +230,35 @@ function overviewRowsFrom(letters: string[]): string[][] { ]; } +const _COLOR_BLACK = '#1a1a1a'; +const _COLOR_LIGHT_RED = '#FF8E8E'; + +/** 构建 6 slot 的单行字母数据:前 4 格有字母(颜色递进),后 2 格空白 */ +function buildSingleLineCells(L: string): (SingleLineCell | null)[] { + const lower = L.toLowerCase(); + return [ + { upper: L, lower, style: 'solid', color: _COLOR_BLACK, alpha: 1 }, + { upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 1 }, + { upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 0.5 }, + { upper: L, lower, style: 'dashed', color: _COLOR_LIGHT_RED, alpha: 0.5 }, + null, + null, + ]; +} + +/** 构建 6 slot 的简练行数据:前 2 格有字母(黑色+浅红色),后 4 格空白 */ +function buildSimpleCells(L: string): (SingleLineCell | null)[] { + const lower = L.toLowerCase(); + return [ + { upper: L, lower, style: 'solid', color: _COLOR_BLACK, alpha: 1 }, + { upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 1 }, + null, + null, + null, + null, + ]; +} + /** * 根据配置生成字母描红数据 * - picture-tracing:教学区 + 6 行描红 @@ -273,6 +332,39 @@ export function generateLetterTracing( }; } + if (mode === 'letter-tracing-triple') { + const letters = config.tripleLetters ?? ['A', 'B', 'C']; + const sections: TripleSection[] = letters.map((L) => { + const filledRow: SingleLineRow = { + letter: L, + cells: buildSingleLineCells(L), + }; + const simpleRow: SingleLineRow = { + letter: L, + cells: buildSimpleCells(L), + }; + return { + letter: L, + rows: [filledRow, filledRow, simpleRow, simpleRow], + }; + }); + return { mode: 'letter-tracing-triple', sections }; + } + + if (mode === 'letter-tracing-single-line') { + const half = + config.alphabetHalf === 'N-Z' + ? LETTERS_UPPER.slice(13) + : LETTERS_UPPER.slice(0, 13); + + const rows: SingleLineRow[] = half.map((L) => ({ + letter: L, + cells: buildSingleLineCells(L), + })); + + return { mode: 'letter-tracing-single-line', rows }; + } + // letter-tracing-upper-lower return { mode: 'letter-tracing-upper-lower', diff --git a/stitch-ui/letterTracingV2/DESIGN.md b/stitch-ui/letterTracingV2/DESIGN.md new file mode 100644 index 0000000..9e50d18 --- /dev/null +++ b/stitch-ui/letterTracingV2/DESIGN.md @@ -0,0 +1,54 @@ +# Design System Strategy: Doodle Mini + +## 1. Overview & Creative North Star: "The Sun-Drenched Studio" +The creative direction for this design system is **"The Sun-Drenched Studio."** We are moving away from the cold, clinical "educational app" look and toward a space that feels like a physical, tactile craft room bathed in morning light. + +Instead of a rigid, boxed-in grid, we embrace **Organic Asymmetry**. Elements should feel like they were placed on a table by hand, not snapped to a pixel-perfect wireframe. We break the "template" look by using exaggerated corner radii, overlapping "paper" layers, and a hierarchy that prioritizes warmth and play over traditional data density. + +## 2. Colors: Tonal Radiance +The palette is anchored by `primary` (#6c5a00) and `primary_container` (#ffd709), creating a "Yaya Yellow" experience that is vibrant yet sophisticated. + +* **The "No-Line" Rule:** Under no circumstances should 1px solid borders be used to separate content. We define boundaries exclusively through background shifts. Use `surface_container_low` (#f8f0e0) for main content areas and `surface` (#fef6e7) for the global backdrop. +* **Surface Hierarchy & Nesting:** Treat the UI as stacked sheets of heavy-weight construction paper. + * **Level 0:** `background` (#fef6e7) – The desk surface. + * **Level 1:** `surface_container` (#f0e7d6) – The worksheet base. + * **Level 2:** `surface_container_lowest` (#ffffff) – Active drawing cards or "Paper" modals. +* **The Glass & Gradient Rule:** For navigation bars or floating tool palettes, use Glassmorphism. Apply `surface_container_low` at 80% opacity with a `20px` backdrop blur. This ensures the "sunny" background bleeds through, maintaining the "Sun-Drenched" atmosphere. +* **Signature Textures:** Main CTAs should not be flat. Apply a subtle linear gradient from `primary_fixed` (#ffd709) to `primary_fixed_dim` (#efc900) at a 145-degree angle to give buttons a "pillowy" 3D volume. + +## 3. Typography: The Friendly Guide +We use **Plus Jakarta Sans** exclusively. Its geometric yet soft terminals provide a modern, legible, and approachable voice. + +* **Display & Headlines:** Use `display-lg` (3.5rem) for "Doodle Mini" brand moments. Headlines (`headline-md`) should use a tighter letter-spacing (-0.02em) to feel "hugged" and friendly. +* **Body & Labels:** Use `body-lg` for all instructional text. In a children's context, readability is paramount; ensure a line height of at least 1.6x the font size. +* **Hierarchy as Personality:** Use `on_secondary_container` (#6d3b00) for sub-headers to provide a warm, "chocolatey" contrast against the yellow, moving away from harsh blacks. + +## 4. Elevation & Depth: Tonal Layering +Depth in this system is a result of light and shadow, not lines and strokes. + +* **The Layering Principle:** To lift a component, place a `surface_container_highest` (#e5dcc9) element inside a `surface_container_low` (#f8f0e0) environment. This creates a "recessed" or "elevated" feel through color value alone. +* **Ambient Shadows:** Use shadows sparingly. When an object "floats" (like a floating action button), use a shadow color derived from `on_surface` (#322e25) at 6% opacity, with a blur of `32px` and a Y-offset of `8px`. It should look like a soft glow, not a drop shadow. +* **The "Ghost Border" Fallback:** If a separator is required for accessibility in input fields, use `outline_variant` (#b3ac9f) at **15% opacity**. It should be felt, not seen. + +## 5. Components: Tactile & Safe +Every component must feel "squishy" and safe to touch. + +* **Buttons:** + * **Primary:** Uses `primary_container` (#ffd709) with a `full` (9999px) radius. + * **States:** On press, scale the button down to 96% size to simulate a physical "click" into the screen. +* **Cards & Lists:** Forbid divider lines. Separate list items using a `spacing-3` (1rem) gap and alternating background colors between `surface_container_low` and `surface_container`. +* **Input Fields:** Use `surface_container_highest` for the input track. Corners must be `xl` (3rem). Labels sit *above* the field in `title-sm` to ensure children can always see the context. +* **Selection Chips:** Use `secondary_container` (#ffc697) for selected states. The roundedness must be `full` to mimic smooth river stones. +* **The "Joy" Progress Bar:** A custom component for Doodle Mini. Use a thick track (`1.5rem` height) in `surface_container_highest` with the progress indicator using a gradient of `tertiary` (#006b1b) to `tertiary_fixed` (#91f78e). + +## 6. Do’s and Don’ts + +### Do: +* **Do** use `xl` and `full` corner radii for almost everything. Sharp corners are "scary." +* **Do** lean into white space. Use `spacing-12` (4rem) between major sections to let the design breathe. +* **Do** use `on_tertiary_container` (dark green) for success states; it feels organic and "earthy" against the yellow. + +### Don’t: +* **Don’t** use pure black (#000000). Use `on_surface` (#322e25) for all dark text to maintain the warm, sunny tone. +* **Don’t** use 1px dividers. If you feel you need one, use a `surface_variant` (#e5dcc9) color block that is `4px` thick with `full` rounded caps. +* **Don’t** crowd the interface. If a screen feels busy, increase the background "paper" size and use a scrollable layout instead of shrinking elements. \ No newline at end of file diff --git a/stitch-ui/letterTracingV2/code.html b/stitch-ui/letterTracingV2/code.html new file mode 100644 index 0000000..45b5f22 --- /dev/null +++ b/stitch-ui/letterTracingV2/code.html @@ -0,0 +1,245 @@ + + + + + +英文字母描红配置 + + + + + + + + + +
+ +

字母描红配置

+ +
+ +
+ +
+
+ + + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+Aa +
+
+
+ +
+

选择字母

+
+ +
+ + + +
+ +
+
+
+ +
+

练习类型

+
+ + + + + + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+ \ No newline at end of file diff --git a/stitch-ui/letterTracingV2/screen.png b/stitch-ui/letterTracingV2/screen.png new file mode 100644 index 0000000..d2e12e5 Binary files /dev/null and b/stitch-ui/letterTracingV2/screen.png differ