127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||
import type {
|
||
LetterTracingData,
|
||
TracingRow,
|
||
} from '../generators/letter-tracing-generator';
|
||
import { fontFamilyOf } from '../../../core/font/fontProfiles';
|
||
import {
|
||
calcLetterFontSizes,
|
||
drawFourLineGrid,
|
||
drawLetterInFourLineGrid,
|
||
FOUR_LINE_GRID_H,
|
||
type LetterFontSizes,
|
||
} from '../../shared/draw/drawTools';
|
||
|
||
import { TRACING_COLORS } from '../../../core/data/tracingStyles';
|
||
|
||
const COLOR_BLACK = TRACING_COLORS.strong;
|
||
const COLOR_LIGHT_RED = TRACING_COLORS.guide;
|
||
const COLUMN_GUTTER = 18;
|
||
|
||
type TwoColumnData = Extract<
|
||
LetterTracingData,
|
||
{ mode: 'letter-tracing-two-column' }
|
||
>;
|
||
|
||
/**
|
||
* 两列练习绘制:
|
||
* 左栏 A–M(Aa Bb … Mm)、右栏 N–Z(Nn Oo … Zz),每栏 13 行。
|
||
* 每行一个完整四线三格,格内绘制大小写配对字母。
|
||
* 第一行黑色示范,后续行浅红色渐淡。两栏间距 18px。
|
||
*/
|
||
export default class TwoColumnDraw extends BaseDrawService {
|
||
private letterSizes: LetterFontSizes =
|
||
calcLetterFontSizes(FOUR_LINE_GRID_H);
|
||
|
||
async draw(data: TwoColumnData) {
|
||
this.prepareDraw();
|
||
await this.drawHeaderAndDivider();
|
||
this.drawContent(data.leftRows, data.rightRows);
|
||
this.drawPrintFooter();
|
||
}
|
||
|
||
private drawContent(leftRows: TracingRow[], rightRows: TracingRow[]) {
|
||
const M = 24;
|
||
const lineW = this.canvasWidth - M * 2;
|
||
const colW = (lineW - COLUMN_GUTTER) / 2;
|
||
const maxR = Math.max(leftRows.length, rightRows.length);
|
||
|
||
const footerReserve = 56;
|
||
const availableH =
|
||
this.canvasHeight - this.currentY - footerReserve - 12;
|
||
const rowGap = Math.max(
|
||
4,
|
||
(availableH - maxR * FOUR_LINE_GRID_H) / (maxR + 1),
|
||
);
|
||
const y0 = this.currentY + rowGap;
|
||
|
||
const leftX = M;
|
||
const rightX = M + colW + COLUMN_GUTTER;
|
||
|
||
for (let i = 0; i < maxR; i++) {
|
||
const y = y0 + i * (FOUR_LINE_GRID_H + rowGap);
|
||
|
||
if (i < leftRows.length) {
|
||
drawFourLineGrid(this.ctx, leftX, y, colW, FOUR_LINE_GRID_H);
|
||
this.drawPairRow(leftRows[i], leftX, y, colW);
|
||
}
|
||
if (i < rightRows.length) {
|
||
drawFourLineGrid(this.ctx, rightX, y, colW, FOUR_LINE_GRID_H);
|
||
this.drawPairRow(rightRows[i], rightX, y, colW);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在一个四线三格内绘制大小写配对(如 A a)。
|
||
* rowIndex=0 黑色示范,>0 浅红色。
|
||
* cells 里每个 cell 的 char 是大写、pairChar 是小写,
|
||
* 按 repetitions 排列(第一个实色 → 后续渐淡)。
|
||
*/
|
||
private drawPairRow(row: TracingRow, x: number, y: number, colW: number) {
|
||
const fontFamily = fontFamilyOf(this.letterSizes._profile);
|
||
const cells = row.cells;
|
||
const n = cells.length;
|
||
if (n === 0) return;
|
||
|
||
const pairCount = n;
|
||
const slotW = colW / pairCount;
|
||
|
||
const pairGap = 25;
|
||
|
||
for (let i = 0; i < pairCount; i++) {
|
||
const cell = cells[i];
|
||
const color = i === 0 ? COLOR_BLACK : COLOR_LIGHT_RED;
|
||
const alpha = i >= pairCount - 2 ? 0.5 : 1;
|
||
const pairCx = x + i * slotW + slotW / 2;
|
||
|
||
this.ctx.save();
|
||
this.ctx.globalAlpha = alpha;
|
||
|
||
drawLetterInFourLineGrid(
|
||
this.ctx,
|
||
cell.char,
|
||
pairCx - pairGap / 2,
|
||
y,
|
||
FOUR_LINE_GRID_H,
|
||
this.letterSizes,
|
||
{ fontFamily, color },
|
||
);
|
||
|
||
if (cell.pairChar) {
|
||
drawLetterInFourLineGrid(
|
||
this.ctx,
|
||
cell.pairChar,
|
||
pairCx + pairGap / 2,
|
||
y,
|
||
FOUR_LINE_GRID_H,
|
||
this.letterSizes,
|
||
{ fontFamily, color },
|
||
);
|
||
}
|
||
|
||
this.ctx.restore();
|
||
}
|
||
}
|
||
}
|