feat: 每日打卡
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user