Files
doodle-mini/miniprogram/pinyinPages/pinyinDictation/draw/PinyinDictationSheetDraw.ts
T
2026-05-20 15:33:06 +08:00

297 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
PINYIN_SECTIONS,
type PinyinSection,
type PinyinSubCategory,
SHENGMU,
DAN_YUNMU,
FU_YUNMU,
QIANBI_YUNMU,
HOUBI_YUNMU,
TESHU_YUNMU,
ZHENGTI_RENDU,
PINGSHEYIN,
QIAOSHEYIN,
} from '../../../core/data/pinyin';
import { PINYIN_STYLE } from '../../shared/pinyinConstants';
import { PinyinBaseDraw } from '../../shared/pinyinBaseDraw';
import type { PinyinDictationData } from './pinyinDictationDraw';
const {
MARGIN_X,
CELL_H,
CELL_H_V2,
ROW_GAP,
ROW_GAP_V2,
SECTION_GAP,
TITLE_AFTER_GAP,
CONTENT_TOP_GAP,
SECTION_TITLE_FONT,
SUB_LABEL_FONT,
SECTION_TITLE_COLOR,
SUB_LABEL_COLOR,
PINYIN_FONT_SIZE_RATIO,
PINYIN_FONT_SIZE_RATIO_V2,
} = PINYIN_STYLE;
const MIN_CELL_W = 44;
const SUB_INDENT = 20;
const SECTION_TO_SUB_GAP = 8;
const QIZHONG_GAP = 16;
const SUB_LABEL_H = 14;
const SUB_LABEL_GAP = 4;
const SUB_BLOCK_GAP = 8;
/** V1 + V2 描红 / 默写练习 */
export default class PinyinDictationSheetDraw extends PinyinBaseDraw {
private cellW = MIN_CELL_W;
private contentW = 0;
private currentCellH = CELL_H;
private currentRowGap = ROW_GAP;
private currentFontSizeRatio = PINYIN_FONT_SIZE_RATIO;
async draw(data: PinyinDictationData) {
this.prepareDraw();
this.contentW = this.canvasWidth - 2 * MARGIN_X;
if (data.mode.endsWith('-v2')) {
this.cellW = this.contentW / 8;
this.currentCellH = CELL_H_V2;
this.currentRowGap = ROW_GAP_V2;
this.currentFontSizeRatio = PINYIN_FONT_SIZE_RATIO_V2;
} else {
const maxCols = Math.floor(this.contentW / MIN_CELL_W);
this.cellW = this.contentW / maxCols;
this.currentCellH = CELL_H;
this.currentRowGap = ROW_GAP;
this.currentFontSizeRatio = PINYIN_FONT_SIZE_RATIO;
}
await this.drawHeaderAndDivider();
this.drawContent(data);
}
// ── 内容入口 ──
private drawContent(data: PinyinDictationData) {
const isTracing = data.mode.includes('tracing');
let y = this.currentY + CONTENT_TOP_GAP;
if (data.mode.endsWith('-v2')) {
this.drawV2Content(y, isTracing);
} else {
for (const section of PINYIN_SECTIONS) {
y = this.drawV1Section(section, y, isTracing);
}
}
}
// ── V1:原有布局(声母 / 韵母 / 整体认读 + 子分类) ──
private drawV1Section(
section: PinyinSection,
startY: number,
isTracing: boolean,
): number {
let y = startY;
y = this.drawTitle(section.title, section.count, y);
y += TITLE_AFTER_GAP;
y = this.drawItemRows(
section.items,
MARGIN_X,
y,
isTracing,
this.cellW,
this.currentCellH,
);
if (section.subCategories && section.subCategories.length > 0) {
y += SECTION_TO_SUB_GAP;
y = this.drawSubCategories(section.subCategories, y, isTracing, section.key);
}
y += SECTION_GAP;
return y;
}
private drawSubCategories(
subCategories: PinyinSubCategory[],
startY: number,
isTracing: boolean,
sectionKey: string,
): number {
const ctx = this.ctx;
let y = startY;
ctx.font = SUB_LABEL_FONT;
ctx.fillStyle = SUB_LABEL_COLOR;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('其中——', MARGIN_X, y);
y += QIZHONG_GAP;
const subX = MARGIN_X + SUB_INDENT;
if (sectionKey === 'shengmu') {
y = this.drawShengmuSubs(subCategories, subX, y, isTracing);
} else {
for (const sub of subCategories) {
y = this.drawLabeledBlock(
`${sub.label}${sub.count}个)`,
sub.items,
subX,
y,
isTracing,
);
y += SUB_BLOCK_GAP;
}
}
return y;
}
private drawShengmuSubs(
subs: PinyinSubCategory[],
subX: number,
startY: number,
isTracing: boolean,
): number {
const qiaoshe = subs.find((s) => s.key === 'qiaoshe');
const pingshe = subs.find((s) => s.key === 'pingshe');
const col2X = MARGIN_X + Math.round(this.contentW / 2);
let bottomY = startY;
if (qiaoshe) {
const endY = this.drawLabeledBlock(
`翘舌音(${qiaoshe.count}个)`,
qiaoshe.items,
subX,
startY,
isTracing,
);
bottomY = Math.max(bottomY, endY);
}
if (pingshe) {
const endY = this.drawLabeledBlock(
`平舌音(${pingshe.count}个)`,
pingshe.items,
col2X,
startY,
isTracing,
);
bottomY = Math.max(bottomY, endY);
}
return bottomY + SUB_BLOCK_GAP;
}
private drawLabeledBlock(
label: string,
items: string[],
startX: number,
startY: number,
isTracing: boolean,
): number {
const ctx = this.ctx;
let y = startY;
ctx.save();
ctx.font = SUB_LABEL_FONT;
ctx.fillStyle = SECTION_TITLE_COLOR;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(label, startX, y);
ctx.restore();
y += SUB_LABEL_H + SUB_LABEL_GAP;
return this.drawItemRows(items, startX, y, isTracing, this.cellW, this.currentCellH);
}
// ── V2:8 列 + 扁平分块 ──
private drawV2Content(startY: number, isTracing: boolean): void {
const sections = [
{ title: '声母', count: 23, items: SHENGMU },
{ title: '单韵母', count: 6, items: DAN_YUNMU },
{ title: '复韵母', count: 8, items: FU_YUNMU },
{ title: '前鼻韵母', count: 5, items: QIANBI_YUNMU },
{ title: '后鼻韵母', count: 4, items: HOUBI_YUNMU },
{ title: '特殊韵母', count: 1, items: TESHU_YUNMU },
{ title: '整体认读音节', count: 16, items: ZHENGTI_RENDU },
{ title: '平舌音', count: 3, items: PINGSHEYIN },
{ title: '翘舌音', count: 4, items: QIAOSHEYIN },
];
let y = startY;
for (const section of sections) {
y = this.drawTitle(section.title, section.count, y);
y += TITLE_AFTER_GAP;
y = this.drawFixedColsRows(section.items, MARGIN_X, y, isTracing, 8);
y += SECTION_GAP;
}
}
// ── 共用绘制方法 ──
private drawTitle(title: string, count: number, y: number): number {
const ctx = this.ctx;
ctx.font = SECTION_TITLE_FONT;
ctx.fillStyle = SECTION_TITLE_COLOR;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(title, MARGIN_X, y);
const titleW = ctx.measureText(title).width;
ctx.fillStyle = SUB_LABEL_COLOR;
ctx.fillText(`${count}个)`, MARGIN_X + titleW, y);
return y;
}
private drawItemRows(
items: string[],
startX: number,
startY: number,
isTracing: boolean,
cellW: number,
cellH: number,
): number {
const maxWidth = this.canvasWidth - startX - MARGIN_X;
const maxCols = Math.max(1, Math.floor(maxWidth / cellW));
return this.drawFixedColsRows(items, startX, startY, isTracing, maxCols);
}
private drawFixedColsRows(
items: string[],
startX: number,
startY: number,
isTracing: boolean,
cols: number,
): number {
let y = startY;
const cellW = this.cellW;
const cellH = this.currentCellH;
for (let i = 0; i < items.length; i += cols) {
const rowItems = items.slice(i, i + cols);
this.drawFourLineRow(startX, y, cols, cellW, cellH);
if (isTracing) {
for (let j = 0; j < rowItems.length; j++) {
const cx = startX + j * cellW + cellW / 2;
this.drawPinyinText(rowItems[j], cx, y, cellH, this.currentFontSizeRatio);
}
}
y += cellH + this.currentRowGap;
}
return y;
}
}