Files
doodle-mini/miniprogram/englishPages/letterTracing/draw/dailyCheckinDraw.ts
T
2026-04-22 16:49:29 +08:00

318 lines
9.6 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 { BaseDrawService } from '../../../core/draw/baseDraw';
import type {
DailyCheckinRow,
DailyCheckinSection,
LetterTracingData,
} from '../generators/letter-tracing-generator';
import {
PRINT_CLEARLY_DASHED,
PRINT_CLEARLY_REGULAR,
fontFamilyOf,
} from '../../shared/data/fontProfiles';
import {
calcLetterFontSizes,
drawClosedFourLineGrid,
drawLetterInFourLineGrid,
loadLetterFont,
type LetterFontSizes,
} from '../../shared/draw/drawTools';
type DailyCheckinData = Extract<
LetterTracingData,
{ mode: 'letter-tracing-daily-checkin' }
>;
// 页面左右边距(px
const PAGE_MARGIN_X = 14;
// 页面上下边距(px
const PAGE_MARGIN_Y = 14;
// 每个分区内容区左右内边距(px)
const SECTION_PADDING_X = 18;
// 每个分区内容区顶部内边距(px)
const SECTION_PADDING_TOP = 12;
// 每个分区内容区底部内边距(px)
const SECTION_PADDING_BOTTOM = 10;
// 标题与上一元素的垂直间距(px)
const HEADER_TITLE_GAP = 24;
// 标题与元信息(如日期、学号等)的间距(px)
const HEADER_META_GAP = 22;
// 正文内容距标题/元信息顶部的间距(px)
const CONTENT_TOP_GAP = 14;
// 页脚短语与内容之间的间距(px)
const FOOTER_PHRASE_GAP = 26;
// 页脚索引与短语之间的间距(px)
const FOOTER_INDEX_GAP = 22;
// 单个格子区列数
const GRID_COLS = 5;
// 单个格子区行数
const GRID_ROWS = 7;
// 格子列间距(px
const GRID_COL_GAP = 6;
// 格子行间距(px
const GRID_ROW_GAP = 8;
// 网格描边主色
const GRID_BORDER = '#9AA9A2';
// 网格中间线虚线颜色
const GRID_MIDDLE = 'rgba(154, 169, 162, 0.65)';
// 黑色文本主色
const COLOR_BLACK = '#1a1a1a';
// 数据红色(例:未签到状态)
const COLOR_LIGHT_RED = '#FF8E8E';
export default class DailyCheckinDraw extends BaseDrawService {
async draw(data: DailyCheckinData) {
this.prepareDraw();
await Promise.all([
loadLetterFont(PRINT_CLEARLY_REGULAR),
loadLetterFont(PRINT_CLEARLY_DASHED),
]);
this.drawPageDividers();
const sections = this.getSectionRects();
for (let i = 0; i < sections.length; i++) {
this.drawSection(sections[i], data.sections[i] ?? null);
}
}
private drawPageDividers() {
const verticalX = this.canvasWidth / 2;
const horizontalY = this.canvasHeight / 2;
this.drawLine(
verticalX,
PAGE_MARGIN_Y,
verticalX,
this.canvasHeight - PAGE_MARGIN_Y,
{
isDashed: true,
dashPattern: [5, 5],
color: '#D6D2CC',
lineWidth: 1,
},
);
this.drawLine(
PAGE_MARGIN_X,
horizontalY,
this.canvasWidth - PAGE_MARGIN_X,
horizontalY,
{
isDashed: true,
dashPattern: [5, 5],
color: '#D6D2CC',
lineWidth: 1,
},
);
}
private getSectionRects() {
const halfW = this.canvasWidth / 2;
const halfH = this.canvasHeight / 2;
return [
{
x: PAGE_MARGIN_X,
y: PAGE_MARGIN_Y,
w: halfW - PAGE_MARGIN_X,
h: halfH - PAGE_MARGIN_Y,
},
{
x: halfW,
y: PAGE_MARGIN_Y,
w: this.canvasWidth - PAGE_MARGIN_X - halfW,
h: halfH - PAGE_MARGIN_Y,
},
{
x: PAGE_MARGIN_X,
y: halfH,
w: halfW - PAGE_MARGIN_X,
h: this.canvasHeight - PAGE_MARGIN_Y - halfH,
},
{
x: halfW,
y: halfH,
w: this.canvasWidth - PAGE_MARGIN_X - halfW,
h: this.canvasHeight - PAGE_MARGIN_Y - halfH,
},
];
}
private drawSection(
rect: { x: number; y: number; w: number; h: number },
section: DailyCheckinSection | null,
) {
if (!section) return;
this.drawSectionHeader(rect);
this.drawSectionFooter(rect, section.pageNumber);
this.drawSectionGrid(rect, section.rows);
}
private drawSectionHeader(rect: {
x: number;
y: number;
w: number;
h: number;
}) {
const cx = rect.x + rect.w / 2;
const titleY = rect.y + SECTION_PADDING_TOP + 12;
const metaY = titleY + HEADER_TITLE_GAP;
this.ctx.save();
this.ctx.fillStyle = '#322E25';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.font = '20px serif';
this.ctx.fillText('每日打卡', cx, titleY);
this.ctx.font = '14px serif';
this.ctx.fillText('月', rect.x + rect.w * 0.34, metaY);
this.ctx.fillText('日', rect.x + rect.w * 0.48, metaY);
this.ctx.fillText('姓名:', rect.x + rect.w * 0.7, metaY);
this.ctx.strokeStyle = '#8E897E';
this.ctx.lineWidth = 1;
this.ctx.setLineDash([]);
this.ctx.beginPath();
this.ctx.moveTo(rect.x + rect.w * 0.14, metaY + 4);
this.ctx.lineTo(rect.x + rect.w * 0.3, metaY + 4);
this.ctx.moveTo(rect.x + rect.w * 0.38, metaY + 4);
this.ctx.lineTo(rect.x + rect.w * 0.44, metaY + 4);
this.ctx.moveTo(rect.x + rect.w * 0.52, metaY + 4);
this.ctx.lineTo(rect.x + rect.w * 0.58, metaY + 4);
this.ctx.moveTo(rect.x + rect.w * 0.78, metaY + 4);
this.ctx.lineTo(rect.x + rect.w * 0.94, metaY + 4);
this.ctx.stroke();
this.ctx.restore();
}
private drawSectionFooter(
rect: { x: number; y: number; w: number; h: number },
pageNumber: number,
) {
const cx = rect.x + rect.w / 2;
const phraseY =
rect.y + rect.h - SECTION_PADDING_BOTTOM - FOOTER_PHRASE_GAP;
const indexY = rect.y + rect.h - SECTION_PADDING_BOTTOM - 6;
this.ctx.save();
this.ctx.fillStyle = '#3E3A33';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.font = '14px serif';
this.ctx.fillText('日拱一卒 不期而至', cx, phraseY);
this.ctx.font = '12px serif';
this.ctx.fillText(`- ${pageNumber} -`, cx, indexY);
this.ctx.restore();
}
private drawSectionGrid(
rect: { x: number; y: number; w: number; h: number },
rows: DailyCheckinRow[],
) {
const contentTop =
rect.y +
SECTION_PADDING_TOP +
HEADER_TITLE_GAP +
HEADER_META_GAP +
CONTENT_TOP_GAP;
const contentBottom =
rect.y +
rect.h -
SECTION_PADDING_BOTTOM -
FOOTER_PHRASE_GAP -
FOOTER_INDEX_GAP;
const contentHeight = contentBottom - contentTop;
const contentWidth = rect.w - SECTION_PADDING_X * 2;
const cellW =
(contentWidth - GRID_COL_GAP * (GRID_COLS - 1)) / GRID_COLS;
const cellH =
(contentHeight - GRID_ROW_GAP * (GRID_ROWS - 1)) / GRID_ROWS;
const pairGap = Math.min(16, cellW * 0.28);
const regularSizes = calcLetterFontSizes(
cellH * 0.86,
PRINT_CLEARLY_REGULAR,
);
const dashedSizes = calcLetterFontSizes(
cellH * 0.86,
PRINT_CLEARLY_DASHED,
);
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
const row = rows[rowIdx];
const y = contentTop + rowIdx * (cellH + GRID_ROW_GAP);
for (let colIdx = 0; colIdx < GRID_COLS; colIdx++) {
const x =
rect.x +
SECTION_PADDING_X +
colIdx * (cellW + GRID_COL_GAP);
drawClosedFourLineGrid(this.ctx, x, y, cellW, cellH, {
ink: GRID_BORDER,
middleInk: GRID_MIDDLE,
lineWidth: 1,
dash: [3, 3],
});
const cell = row.cells[colIdx];
if (!cell) continue;
const sizes =
row.style === 'dashed' ? dashedSizes : regularSizes;
const fontFamily = fontFamilyOf(
row.style === 'dashed'
? PRINT_CLEARLY_DASHED
: PRINT_CLEARLY_REGULAR,
);
const centerX = x + cellW / 2;
this.ctx.save();
this.ctx.globalAlpha = cell.alpha;
drawLetterInFourLineGrid(
this.ctx,
cell.upper,
centerX - pairGap / 2,
y,
cellH,
sizes,
{
fontFamily,
color:
cell.color === COLOR_BLACK
? COLOR_BLACK
: COLOR_LIGHT_RED,
},
);
drawLetterInFourLineGrid(
this.ctx,
cell.lower,
centerX + pairGap / 2,
y,
cellH,
sizes,
{
fontFamily,
color:
cell.color === COLOR_BLACK
? COLOR_BLACK
: COLOR_LIGHT_RED,
},
);
this.ctx.restore();
}
}
}
}