feat: 增加字母笔顺图片

This commit is contained in:
R524809
2026-04-28 20:09:29 +08:00
parent bb69d5c49d
commit 63fa08ff1a
38 changed files with 458 additions and 163 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -5,6 +5,13 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "47465822",
"name": "task-o",
"font_class": "task-o",
"unicode": "e62d",
"unicode_decimal": 58925
},
{
"icon_id": "47422029",
"name": "refresh",
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ import {
type LetterFontSizes,
} from '../../shared/draw/drawTools';
const SLOTS_PER_ROW = 6;
const SLOTS_PER_ROW = 7;
const PAIR_GAP = 25;
type SingleLineData = Extract<
@@ -26,11 +26,11 @@ type SingleLineData = Extract<
/**
*
* 13 线 6
* 4 Aa 2
* 13 线 7
* 4 Aa
* 线+
*/
export default class SingleLineDraw extends BaseDrawService {
export default class HalfLetterDraw extends BaseDrawService {
private letterSizes: LetterFontSizes =
calcLetterFontSizes(FOUR_LINE_GRID_H);
@@ -4,7 +4,7 @@ import singleLetterDraw from './singleLetterDraw';
import CasePairingDraw from './casePairingDraw';
import TwoColumnDraw from './twoColumnDraw';
import UpperLowerDraw from './upperLowerDraw';
import SingleLineDraw from './singleLineDraw';
import HalfLetterDraw from './halfLetterDraw';
import TripleDraw from './tripleDraw';
import DailyCheckinDraw from './dailyCheckinDraw';
@@ -19,7 +19,7 @@ export default class LetterTracingDraw extends BaseDrawService {
'letter-tracing-upper-lower': UpperLowerDraw,
'letter-tracing-case-pairing': CasePairingDraw,
'letter-tracing-two-column': TwoColumnDraw,
'letter-tracing-half': SingleLineDraw,
'letter-tracing-half': HalfLetterDraw,
'letter-tracing-three': TripleDraw,
'letter-tracing-daily-checkin': DailyCheckinDraw,
}[data.mode];
@@ -4,13 +4,18 @@ import type {
LetterTracingData,
HighlightGrid,
} from '../generators/letter-tracing-generator';
import { fontFamilyOf } from '../../shared/data/fontProfiles';
import {
fontFamilyOf,
PRINT_CLEARLY_BOLD,
PRINT_CLEARLY_DASHED,
} from '../../shared/data/fontProfiles';
import {
calcLetterFontSizes,
drawFourLineGrid,
drawLetterInFourLineGrid,
FOUR_LINE_GRID_H,
getLetterFontSize,
loadLetterFont,
type LetterFontSizes,
} from '../../shared/draw/drawTools';
@@ -27,29 +32,37 @@ type SingleLetterData = Extract<
export default class SingleLetterDraw extends BaseDrawService {
private letterSizes: LetterFontSizes =
calcLetterFontSizes(FOUR_LINE_GRID_H);
private dashedSizes: LetterFontSizes = calcLetterFontSizes(
FOUR_LINE_GRID_H,
PRINT_CLEARLY_DASHED,
);
private get letterFace(): string {
return fontFamilyOf(this.letterSizes._profile);
}
async draw(data: SingleLetterData) {
this.prepareDraw();
await this.drawHeaderAndDivider();
await Promise.all([
this.drawHeaderAndDivider(),
loadLetterFont(PRINT_CLEARLY_BOLD),
loadLetterFont(PRINT_CLEARLY_DASHED),
]);
await this.drawContent(data);
this.drawPrintFooter();
}
private async drawContent(data: SingleLetterData) {
const { teaching, highlightGrid, traceImgPath } = data;
const { teaching, highlightGrid } = data;
const ctx = this.ctx;
// ── 上区:left:78 top:120 w:451 h:180 ──
const TOP_X = 78;
const TOP_Y = 120;
const TOP_Y = 110;
const letterImgX = TOP_X + 15;
const letterImgY = TOP_Y + 9;
const letterImgW = 180;
const letterImgH = 103.5;
const letterImgH = 130;
try {
const letterImg = (await getImage(
@@ -85,8 +98,8 @@ export default class SingleLetterDraw extends BaseDrawService {
this.drawSingleLetterSentence(
teaching.sentence,
teaching.upper,
TOP_X,
TOP_Y + 9 + 120,
letterImgX + letterImgW / 2,
TOP_Y + 16 + 120,
);
// 右侧配图
@@ -142,13 +155,7 @@ export default class SingleLetterDraw extends BaseDrawService {
ctx.strokeRect(TR_X, TR_Y, TR_W, TR_H);
this.drawHighlightSection(highlightGrid, HL_X, HL_Y);
await this.drawTraceItSection(
traceImgPath,
teaching.upper,
teaching.lower,
TR_X,
TR_Y,
);
this.drawTraceItSection(teaching.upper, teaching.lower, TR_X, TR_Y);
// ── 下区:4 行四线三格 + 字母临摹(倒置直角三角形)──
// 每行:left:24 w:547 h:43top 分别 524.5 / 590.5 / 656.5 / 722.5
@@ -159,21 +166,27 @@ export default class SingleLetterDraw extends BaseDrawService {
// 8 → 6 → 3 → 1 组(每组为 Upper+Lower),左对齐,形成倒置直角三角形
const groupsPerLine = [8, 6, 3, 1];
const slotW = lineW / groupsPerLine[0]; // 以第一行的 8 组为基准
const lineStyles = [
{ color: 'rgba(26, 26, 26, 1)', alpha: 1, dashed: false },
{ color: '#FF8E8E', alpha: 1, dashed: false },
{ color: '#FF8E8E', alpha: 0.5, dashed: false },
{ color: '#FF8E8E', alpha: 0.5, dashed: true },
] as const;
for (let r = 0; r < lineTops.length; r++) {
const y = lineTops[r];
drawFourLineGrid(ctx, lineX, y, lineW, FOUR_LINE_GRID_H);
const n = groupsPerLine[r] ?? 0;
const style = lineStyles[r] ?? lineStyles[lineStyles.length - 1];
for (let i = 0; i < n; i++) {
const alpha = n <= 1 ? 1 : 1 - (i / (n - 1)) * 0.7;
this.drawUpperLowerPairInFourLines(
teaching.upper,
teaching.lower,
lineX + i * slotW,
y,
slotW,
alpha,
style,
);
}
}
@@ -188,26 +201,32 @@ export default class SingleLetterDraw extends BaseDrawService {
x: number,
y: number,
w: number,
alpha: number,
style: {
color: string;
alpha: number;
dashed: boolean;
},
) {
const ctx = this.ctx;
const sizes = this.letterSizes;
const sizes = style.dashed ? this.dashedSizes : this.letterSizes;
const fontFamily = fontFamilyOf(
style.dashed ? PRINT_CLEARLY_DASHED : sizes._profile,
);
ctx.save();
ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
ctx.globalAlpha = Math.max(0, Math.min(1, style.alpha));
const pairGap = 3;
const upperFs = getLetterFontSize(upper, sizes);
ctx.font = `${upperFs}px ${this.letterFace}`;
ctx.font = `${upperFs}px ${fontFamily}`;
const upperW = ctx.measureText(upper).width;
const lowerFs = getLetterFontSize(lower, sizes);
ctx.font = `${lowerFs}px ${this.letterFace}`;
ctx.font = `${lowerFs}px ${fontFamily}`;
const lowerW = ctx.measureText(lower).width;
const totalPairW = upperW + pairGap + lowerW;
const pairStartX = x + (w - totalPairW) / 2;
const color = 'rgba(26, 26, 26, 1)';
drawLetterInFourLineGrid(
ctx,
@@ -216,7 +235,7 @@ export default class SingleLetterDraw extends BaseDrawService {
y,
FOUR_LINE_GRID_H,
sizes,
{ fontFamily: this.letterFace, color },
{ fontFamily, color: style.color },
);
drawLetterInFourLineGrid(
ctx,
@@ -225,7 +244,7 @@ export default class SingleLetterDraw extends BaseDrawService {
y,
FOUR_LINE_GRID_H,
sizes,
{ fontFamily: this.letterFace, color },
{ fontFamily, color: style.color },
);
ctx.restore();
@@ -234,15 +253,20 @@ export default class SingleLetterDraw extends BaseDrawService {
private drawSingleLetterSentence(
sentence: string,
letter: string,
x: number,
centerX: number,
y: number,
) {
const ctx = this.ctx;
ctx.font = `36px ${this.letterFace}`;
ctx.font = `bold 40px ${this.letterFace}`;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
let cx = x;
let totalWidth = 0;
for (let i = 0; i < sentence.length; i++) {
totalWidth += ctx.measureText(sentence[i]).width;
}
let cx = centerX - totalWidth / 2;
for (let i = 0; i < sentence.length; i++) {
const ch = sentence[i];
ctx.fillStyle =
@@ -302,8 +326,7 @@ export default class SingleLetterDraw extends BaseDrawService {
}
}
private async drawTraceItSection(
traceImgPath: string,
private drawTraceItSection(
upper: string,
lower: string,
sx: number,
@@ -357,37 +380,39 @@ export default class SingleLetterDraw extends BaseDrawService {
ctx.lineTo(lx + lw, lines[3]);
ctx.stroke();
// 笔顺图
const imgX = 337,
imgY = 365,
imgW = 159,
imgH = 80;
try {
const img = (await getImage(
this.canvas,
traceImgPath,
)) as WechatMiniprogram.Image;
const aspect = (img.width || 1) / (img.height || 1);
let dw = imgW;
let dh = dw / aspect;
if (dh > imgH) {
dh = imgH;
dw = dh * aspect;
}
ctx.drawImage(
img,
imgX + (imgW - dw) / 2,
imgY + (imgH - dh) / 2,
dw,
dh,
const gridH = lines[3] - lines[0];
const boldSizes = calcLetterFontSizes(gridH, PRINT_CLEARLY_BOLD);
const boldFace = fontFamilyOf(PRINT_CLEARLY_BOLD);
const pairGap = 20;
const upperFs = getLetterFontSize(upper, boldSizes);
ctx.font = `${upperFs}px ${boldFace}`;
const upperW = ctx.measureText(upper).width;
const lowerFs = getLetterFontSize(lower, boldSizes);
ctx.font = `${lowerFs}px ${boldFace}`;
const lowerW = ctx.measureText(lower).width;
const totalW = upperW + pairGap + lowerW;
const startX = lx + (lw - totalW) / 2;
drawLetterInFourLineGrid(
ctx,
upper,
startX + upperW / 2,
lines[0],
gridH,
boldSizes,
{ fontFamily: boldFace, color: '#222' },
);
drawLetterInFourLineGrid(
ctx,
lower,
startX + upperW + pairGap + lowerW / 2,
lines[0],
gridH,
boldSizes,
{ fontFamily: boldFace, color: '#222' },
);
} catch {
const fs = Math.min(imgH * 0.6, imgW * 0.18);
ctx.font = `bold ${fs}px ${this.letterFace}`;
ctx.fillStyle = '#222';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${upper} ${lower}`, imgX + imgW / 2, imgY + imgH / 2);
}
}
}
@@ -17,8 +17,8 @@ import {
type LetterFontSizes,
} from '../../shared/draw/drawTools';
// 每行可容纳的格子数(用于三字母精练模板,一行展示 6 个位置)
const SLOTS_PER_ROW = 6;
// 每行按 7 个位置预留空间,当前字母绘制占用其中前几组
const SLOTS_PER_ROW = 7;
// 大小写字母配对间的间隔(px,字母间距)
const PAIR_GAP = 25;
// 同一字母区域内相邻两行四线三格之间的间距(px)
@@ -317,9 +317,7 @@ export function generateLetterTracing(
const upper = key;
const lower = key.toLowerCase();
const hasLetterAsset = key === 'A' || key === 'B';
const letterFile = hasLetterAsset ? key : 'A';
const letterImgPath = `/englishPages/shared/assets/letter/${letterFile}.png`;
const letterImgPath = `/englishPages/shared/assets/letter/${key}.png`;
const wordImgPath = `/englishPages/shared/assets/letterImgs/Apple.png`;
@@ -361,9 +359,15 @@ export function generateLetterTracing(
if (mode === 'letter-tracing-two-column') {
const leftLetters = LETTERS_UPPER.slice(0, 13);
const rightLetters = LETTERS_UPPER.slice(13);
const twoColumnRepetitions = 4;
const toRows = (letters: string[]) =>
letters.map((L) =>
rowForChar(L, repetitions, fadePattern, L.toLowerCase()),
rowForChar(
L,
twoColumnRepetitions,
fadePattern,
L.toLowerCase(),
),
);
return {
mode: 'letter-tracing-two-column',
@@ -450,11 +454,7 @@ export function mergeLetterTracingConfig(
'A-M') as AlphabetHalf,
tripleLetters: overrides.tripleLetters ??
base.tripleLetters ?? ['A', 'B', 'C'],
dailyLetters: overrides.dailyLetters ?? base.dailyLetters ?? [
'A',
'B',
'C',
'D',
],
dailyLetters: overrides.dailyLetters ??
base.dailyLetters ?? ['A', 'B', 'C', 'D'],
};
}
@@ -19,7 +19,7 @@ const MODES = [
title: '默认字帖',
subtitle: '配图、例句与描红',
difficulty: 1,
tags: ['字母描红', '英语启蒙', '看图描红'],
tags: ['字母', '描红', '看图描红'],
sortOrder: 42,
},
{
@@ -28,7 +28,7 @@ const MODES = [
title: '基础描红',
subtitle: 'Uppercase / Lowercase 总览',
difficulty: 1,
tags: ['字母描红', '英语启蒙', '字母总览'],
tags: ['字母', '描红', '字母总览'],
sortOrder: 44,
},
{
@@ -37,7 +37,7 @@ const MODES = [
title: '大小写对照',
subtitle: '半组字母左大写右小写',
difficulty: 1,
tags: ['字母描红', '英语启蒙', '大小写练习'],
tags: ['字母', '描红', '大小写练习'],
sortOrder: 45,
},
{
@@ -46,7 +46,7 @@ const MODES = [
title: '两列描红',
subtitle: '左 AM、右 NZ 配对描红',
difficulty: 1,
tags: ['字母描红', '英语启蒙', '两列练习'],
tags: ['字母', '描红', '两列练习'],
sortOrder: 43,
},
{
@@ -55,7 +55,7 @@ const MODES = [
title: '13字母半表',
subtitle: '每行一个字母,13 字母半表',
difficulty: 2,
tags: ['字母描红', '英语启蒙', '单字母逐行'],
tags: ['字母', '描红', '半表练习'],
sortOrder: 47,
},
{
@@ -64,25 +64,26 @@ const MODES = [
title: '三字母精练',
subtitle: '每页聚焦 3 个字母深度书写',
difficulty: 2,
tags: ['字母描红', '英语启蒙', '三字母精练'],
tags: ['字母', '描红', '三字母精练'],
sortOrder: 46,
},
{
id: 'letter-tracing-daily-checkin',
icon: 'draw-o',
icon: 'task-o',
title: '每日打卡',
subtitle: '四宫格每日字母打卡练习',
difficulty: 2,
tags: ['字母描红', '英语启蒙', '每日打卡'],
tags: ['字母', '描红', '每日打卡'],
sortOrder: 48,
},
] as const satisfies ReadonlyArray<ModeDefinition>;
type Mode = (typeof MODES)[number];
const MODE_BY_ID = Object.fromEntries(
MODES.map((m) => [m.id, m]),
) as Record<string, Mode>;
const MODE_BY_ID = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
string,
Mode
>;
/** 页面渲染用:模式选择器列表 */
export const LETTER_TRACING_MODE_OPTIONS = MODES;
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+5 -5
View File
@@ -24,16 +24,16 @@
"miniprogram": {
"list": [
{
"name": "supportPages/debug/debug",
"pathName": "supportPages/debug/debug",
"name": "englishPages/letterTracing/letterTracing",
"pathName": "englishPages/letterTracing/letterTracing",
"query": "",
"scene": null,
"launchMode": "default"
},
{
"name": "englishPages/letterTracing/letterTracing",
"pathName": "englishPages/letterTracing/letterTracing",
"query": "id=letter-tracing-daily-checkin",
"name": "supportPages/debug/debug",
"pathName": "supportPages/debug/debug",
"query": "",
"launchMode": "default",
"scene": null
},