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 '../../../core/font/fontProfiles'; import { calcLetterFontSizes, drawClosedFourLineGrid, drawLetterInFourLineGrid, loadLetterFont, } from '../../shared/draw/drawTools'; import { SECTION_PADDING_TOP, SECTION_PADDING_BOTTOM, HEADER_TITLE_GAP, HEADER_META_GAP, CONTENT_TOP_GAP, FOOTER_PHRASE_GAP, FOOTER_INDEX_GAP, drawSectionHeader, drawSectionFooter, getRandomEncouragementPhrase, } from '../../../core/draw/dailyPracticeDrawHelper'; 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; // 单个格子区列数 const GRID_COLS = 5; // 单个格子区行数 const GRID_ROWS = 7; // 格子列间距(px) const GRID_COL_GAP = 6; // 格子行间距(px) const GRID_ROW_GAP = 8; import { GRID_COLORS, TRACING_COLORS } from '../../../core/data/tracingStyles'; const GRID_BORDER = GRID_COLORS.border; const GRID_MIDDLE = GRID_COLORS.middleLine; const COLOR_BLACK = TRACING_COLORS.strong; const COLOR_LIGHT_RED = TRACING_COLORS.guide; 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; const phrase = getRandomEncouragementPhrase(); drawSectionHeader(this.ctx, rect, '字母每日打卡'); drawSectionFooter(this.ctx, rect, phrase, `- ${section.pageNumber} -`); this.drawSectionGrid(rect, section.rows); } 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 = 16; const regularSizes = calcLetterFontSizes( cellH * 0.98, PRINT_CLEARLY_REGULAR, ); const dashedSizes = calcLetterFontSizes( cellH * 0.98, 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(); } } } }