feat: 分栏对照开发,字体缩放、定位调整
This commit is contained in:
@@ -3,17 +3,34 @@ import type {
|
||||
LetterTracingData,
|
||||
TracingRow,
|
||||
} from '../generators/letter-tracing-generator';
|
||||
import { drawFourLineGrid } from './drawTools';
|
||||
import {
|
||||
calcLetterFontSizes,
|
||||
drawFourLineGrid,
|
||||
drawLetterInFourLineGrid,
|
||||
FOUR_LINE_GRID_H,
|
||||
TOY_FONT_LETTER,
|
||||
type LetterFontSizes,
|
||||
} from './drawTools';
|
||||
|
||||
const LETTER_FACE =
|
||||
'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
||||
const LETTER_FACE = `${TOY_FONT_LETTER}, Roboto, sans-serif`;
|
||||
const COLOR_BLACK = '#1a1a1a';
|
||||
const COLOR_LIGHT_RED = 'rgba(252, 46, 0, 0.3)';
|
||||
|
||||
type ColumnCompareData = Extract<
|
||||
LetterTracingData,
|
||||
{ mode: 'column-compare' }
|
||||
>;
|
||||
|
||||
/**
|
||||
* 分栏对照绘制:
|
||||
* 四线三格横跨整页宽度,字母分左右两侧书写——
|
||||
* 左半写大写(repetitions 个),右半写小写(repetitions 个),
|
||||
* 每侧第一个字母黑色,后续浅红色。
|
||||
*/
|
||||
export default class ColumnCompareDraw extends BaseDrawService {
|
||||
private letterSizes: LetterFontSizes =
|
||||
calcLetterFontSizes(FOUR_LINE_GRID_H);
|
||||
|
||||
async draw(data: ColumnCompareData) {
|
||||
this.prepareDraw();
|
||||
await this.drawHeaderAndDivider();
|
||||
@@ -23,83 +40,62 @@ export default class ColumnCompareDraw extends BaseDrawService {
|
||||
|
||||
private drawContent(leftRows: TracingRow[], rightRows: TracingRow[]) {
|
||||
const M = 24;
|
||||
const y0 = this.currentY + 6;
|
||||
const bottom = this.canvasHeight - M;
|
||||
const lineW = this.canvasWidth - M * 2;
|
||||
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 + 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 + 6);
|
||||
this.drawTracingRow(rightRows[i], xR, y, rowH, colW);
|
||||
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 halfW = lineW / 2;
|
||||
|
||||
for (let i = 0; i < maxR; i++) {
|
||||
const y = y0 + i * (FOUR_LINE_GRID_H + rowGap);
|
||||
|
||||
drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H);
|
||||
|
||||
if (i < leftRows.length) {
|
||||
this.drawLettersInHalf(leftRows[i], M, y, halfW);
|
||||
}
|
||||
if (i < rightRows.length) {
|
||||
this.drawLettersInHalf(rightRows[i], M + halfW, y, halfW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private drawTracingRow(
|
||||
/**
|
||||
* 在四线三格的半侧区域内绘制字母。
|
||||
* 第一个字母黑色,后续字母浅红色。
|
||||
*/
|
||||
private drawLettersInHalf(
|
||||
row: TracingRow,
|
||||
startX: number,
|
||||
x: number,
|
||||
y: number,
|
||||
rowH: number,
|
||||
totalWidth: number,
|
||||
halfW: number,
|
||||
) {
|
||||
const gap = 4;
|
||||
const n = row.cells.length;
|
||||
const cellW = n > 0 ? (totalWidth - gap * (n - 1)) / n : totalWidth;
|
||||
if (n === 0) return;
|
||||
|
||||
const slotW = halfW / n;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const cx = startX + i * (cellW + gap);
|
||||
drawFourLineGrid(this.ctx, cx, y, cellW, rowH);
|
||||
const cell = row.cells[i];
|
||||
this.drawCharInCell(
|
||||
const color = i === 0 ? COLOR_BLACK : COLOR_LIGHT_RED;
|
||||
const cx = x + slotW * i + slotW / 2;
|
||||
|
||||
drawLetterInFourLineGrid(
|
||||
this.ctx,
|
||||
cell.char,
|
||||
cx,
|
||||
y,
|
||||
cellW,
|
||||
rowH,
|
||||
cell.opacity,
|
||||
cell.isGuide,
|
||||
cell.pairChar,
|
||||
FOUR_LINE_GRID_H,
|
||||
this.letterSizes,
|
||||
{ fontFamily: LETTER_FACE, color },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private drawCharInCell(
|
||||
ch: string,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
opacity: number,
|
||||
bold: boolean,
|
||||
pairChar?: string,
|
||||
) {
|
||||
const ctx = this.ctx;
|
||||
if (opacity <= 0) return;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = opacity;
|
||||
ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
const baselineY = y + h * 0.72;
|
||||
|
||||
if (pairChar) {
|
||||
const fs = Math.min(h * 0.5, w * 0.22);
|
||||
ctx.font = `${bold ? 'bold ' : ''}${fs}px ${LETTER_FACE}`;
|
||||
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 ${LETTER_FACE}`;
|
||||
ctx.fillText(ch, x + w / 2, baselineY);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user