feat: 汉字每日打卡优化
This commit is contained in:
@@ -144,7 +144,7 @@ export default class WordDailyCheckinDraw extends BaseDrawService {
|
|||||||
sectionIndex: number,
|
sectionIndex: number,
|
||||||
) {
|
) {
|
||||||
const phrase = getRandomEncouragementPhrase();
|
const phrase = getRandomEncouragementPhrase();
|
||||||
drawSectionHeader(this.ctx, rect, '汉字每日打卡');
|
drawSectionHeader(this.ctx, rect, '汉字每日打卡', { centerMeta: true });
|
||||||
drawSectionFooter(this.ctx, rect, phrase, `- ${sectionIndex} -`);
|
drawSectionFooter(this.ctx, rect, phrase, `- ${sectionIndex} -`);
|
||||||
this.drawSectionContent(rect, items);
|
this.drawSectionContent(rect, items);
|
||||||
}
|
}
|
||||||
@@ -241,6 +241,7 @@ export default class WordDailyCheckinDraw extends BaseDrawService {
|
|||||||
ctx.lineTo(startX + cellW, y2);
|
ctx.lineTo(startX + cellW, y2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = GRID_COLORS.border;
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
const vx = startX + cellW;
|
const vx = startX + cellW;
|
||||||
ctx.moveTo(vx, startY);
|
ctx.moveTo(vx, startY);
|
||||||
@@ -248,37 +249,56 @@ export default class WordDailyCheckinDraw extends BaseDrawService {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// 列分隔线
|
|
||||||
// ctx.strokeStyle = GRID_COLORS.border;
|
|
||||||
// ctx.setLineDash([]);
|
|
||||||
// for (let c = 1; c < COLUMNS; c++) {
|
|
||||||
// const vx = startX + c * cellW;
|
|
||||||
// ctx.beginPath();
|
|
||||||
// ctx.moveTo(vx, startY);
|
|
||||||
// ctx.lineTo(vx, yBot);
|
|
||||||
// ctx.stroke();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 第一格:绘制拼音
|
// 第一格:绘制拼音
|
||||||
if (item.pinyin) {
|
if (item.pinyin) {
|
||||||
const cx = startX + cellW / 2;
|
const cx = startX + cellW / 2;
|
||||||
const fontSize = Math.round(rowH * 0.65);
|
const baseFontSize = Math.round(rowH * 0.65);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `${fontSize}px ${fontFamilyOf(TONEOZ_PINYIN)}`;
|
|
||||||
ctx.fillStyle = TRACING_COLORS.strong;
|
ctx.fillStyle = TRACING_COLORS.strong;
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'alphabetic';
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
|
||||||
|
// 拼音超过 4 个字母时按需缩字,避免超出单元格宽度
|
||||||
|
let fontSize = baseFontSize;
|
||||||
|
if (item.pinyin.length > 4) {
|
||||||
|
ctx.font = `${baseFontSize}px ${fontFamilyOf(TONEOZ_PINYIN)}`;
|
||||||
|
const maxWidth = cellW * 0.9;
|
||||||
|
const measured = (ctx as any).measureText(item.pinyin).width;
|
||||||
|
if (measured > maxWidth) {
|
||||||
|
fontSize = Math.max(
|
||||||
|
8,
|
||||||
|
Math.floor(baseFontSize * (maxWidth / measured)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.font = `${fontSize}px ${fontFamilyOf(TONEOZ_PINYIN)}`;
|
||||||
ctx.fillText(item.pinyin, cx, y2);
|
ctx.fillText(item.pinyin, cx, y2);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 笔画格:第 2 格起,按笔画顺序展示当前进度
|
// 笔画格:第 2 格起,按笔画顺序展示当前进度
|
||||||
|
// 每一格绘制整个字:已绘制的笔画为黑色,当前笔画为红色,未绘制的为浅灰色
|
||||||
const strokeCount = item.strokes.length;
|
const strokeCount = item.strokes.length;
|
||||||
for (let k = 1; k <= strokeCount && k < COLUMNS; k++) {
|
for (let k = 1; k <= strokeCount && k < COLUMNS; k++) {
|
||||||
const cellX = startX + cellW / 2 + k * 15;
|
const cellX = startX + cellW / 2 + k * 20;
|
||||||
const currentIdx = k - 1;
|
const currentIdx = k - 1;
|
||||||
|
const lastIdx = strokeCount - 1;
|
||||||
|
|
||||||
// 已绘制的历史笔画:浅灰
|
// 未绘制的笔画(当前之后):浅灰
|
||||||
|
if (currentIdx < lastIdx) {
|
||||||
|
drawStrokesRange({
|
||||||
|
ctx,
|
||||||
|
strokes: item.strokes,
|
||||||
|
offsetX: cellX,
|
||||||
|
offsetY: startY,
|
||||||
|
width: cellW,
|
||||||
|
height: rowH,
|
||||||
|
fromIndex: currentIdx + 1,
|
||||||
|
toIndex: lastIdx,
|
||||||
|
fillStyle: TRACING_COLORS.guide,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 已绘制的历史笔画:黑色
|
||||||
if (currentIdx > 0) {
|
if (currentIdx > 0) {
|
||||||
drawStrokesRange({
|
drawStrokesRange({
|
||||||
ctx,
|
ctx,
|
||||||
@@ -289,7 +309,7 @@ export default class WordDailyCheckinDraw extends BaseDrawService {
|
|||||||
height: rowH,
|
height: rowH,
|
||||||
fromIndex: 0,
|
fromIndex: 0,
|
||||||
toIndex: currentIdx - 1,
|
toIndex: currentIdx - 1,
|
||||||
fillStyle: TRACING_COLORS.guide,
|
fillStyle: TRACING_COLORS.strong,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 当前笔画:红色高亮
|
// 当前笔画:红色高亮
|
||||||
@@ -302,7 +322,7 @@ export default class WordDailyCheckinDraw extends BaseDrawService {
|
|||||||
height: rowH,
|
height: rowH,
|
||||||
fromIndex: currentIdx,
|
fromIndex: currentIdx,
|
||||||
toIndex: currentIdx,
|
toIndex: currentIdx,
|
||||||
fillStyle: '#D94B4B',
|
fillStyle: TRACING_COLORS.active,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
export const GRID_COLORS = {
|
export const GRID_COLORS = {
|
||||||
/** 外框、实线、垂直分隔线 — 中等绿色,清晰可见 */
|
/** 外框、实线、垂直分隔线 — 中等绿色,清晰可见 */
|
||||||
border: '#7fb069',
|
border: '#7fb069',
|
||||||
/** 中线、虚线(田字格十字线 / 四线三格中间两线)— 浅绿色,柔和护眼 */
|
/** 中线、虚线(田字格十字线 / 四线三格中间两线)— 喷墨可见的中绿色 */
|
||||||
middleLine: '#a8d5a8',
|
middleLine: '#7ec27e',
|
||||||
/** 对角线 — 极淡绿色,辅助参考线(田字格专用) */
|
/** 对角线 — 浅绿色,辅助参考线(田字格专用),喷墨可打印 */
|
||||||
diagonal: '#d4f0d4',
|
diagonal: '#9bcd9b',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** 四线三格默认虚线样式 */
|
/** 四线三格默认虚线样式 */
|
||||||
@@ -30,10 +30,12 @@ export const GRID_DASH = [3, 3] as const;
|
|||||||
export const TRACING_COLORS = {
|
export const TRACING_COLORS = {
|
||||||
/** 参照字/预览字 — 深灰色,完整展示供对照 */
|
/** 参照字/预览字 — 深灰色,完整展示供对照 */
|
||||||
reference: '#555555',
|
reference: '#555555',
|
||||||
/** 描红引导字 — 中性灰色,跟着描写 */
|
/** 描红引导字 — 中灰色,跟着描写,喷墨可清晰打印 */
|
||||||
guide: '#d0d0d0',
|
guide: '#aaaaaa',
|
||||||
/** 极浅引导字 — 浅灰色,水印效果 */
|
/** 极浅引导字 — 浅灰色,水印效果,喷墨可见 */
|
||||||
guideLight: '#e0e0e0',
|
guideLight: '#c8c8c8',
|
||||||
/** 首笔/强调 — 近黑色,用于首个参照格 */
|
/** 首笔/强调 — 近黑色,用于首个参照格 */
|
||||||
strong: '#1a1a1a',
|
strong: '#1a1a1a',
|
||||||
|
/** 当前笔画高亮 — 红色,用于笔顺进度 */
|
||||||
|
active: '#D94B4B',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -52,11 +52,13 @@ export function getRandomEncouragementPhrase(): string {
|
|||||||
* @param ctx 微信小程序 Canvas 绘制上下文
|
* @param ctx 微信小程序 Canvas 绘制上下文
|
||||||
* @param rect 目标 Section 的位置与宽高信息
|
* @param rect 目标 Section 的位置与宽高信息
|
||||||
* @param title 标题文本,如 “每日打卡” 或 “拼音每日一练”
|
* @param title 标题文本,如 “每日打卡” 或 “拼音每日一练”
|
||||||
|
* @param options.centerMeta 是否将 “月/日/姓名” 元信息行整体水平居中(默认 false,保持原左侧布局)
|
||||||
*/
|
*/
|
||||||
export function drawSectionHeader(
|
export function drawSectionHeader(
|
||||||
ctx: RenderingContext,
|
ctx: RenderingContext,
|
||||||
rect: { x: number; y: number; w: number; h: number },
|
rect: { x: number; y: number; w: number; h: number },
|
||||||
title: string,
|
title: string,
|
||||||
|
options?: { centerMeta?: boolean },
|
||||||
): void {
|
): void {
|
||||||
const cx = rect.x + rect.w / 2;
|
const cx = rect.x + rect.w / 2;
|
||||||
const titleY = rect.y + SECTION_PADDING_TOP + 12;
|
const titleY = rect.y + SECTION_PADDING_TOP + 12;
|
||||||
@@ -74,21 +76,30 @@ export function drawSectionHeader(
|
|||||||
canvasCtx.font = '20px serif';
|
canvasCtx.font = '20px serif';
|
||||||
canvasCtx.fillText(title, cx, titleY);
|
canvasCtx.fillText(title, cx, titleY);
|
||||||
|
|
||||||
|
// 元信息行布局:原始版本以 rect.x 为基准(左侧偏移 40~248,宽 208)
|
||||||
|
// 当 centerMeta=true 时,整体水平居中到 rect 中部,仅影响调用方显式开启的场景
|
||||||
|
const META_BLOCK_WIDTH = 208;
|
||||||
|
const META_BASE_OFFSET = 40;
|
||||||
|
const metaLeft = options?.centerMeta
|
||||||
|
? rect.x + (rect.w - META_BLOCK_WIDTH) / 2
|
||||||
|
: rect.x + META_BASE_OFFSET;
|
||||||
|
const offset = (n: number) => metaLeft + (n - META_BASE_OFFSET);
|
||||||
|
|
||||||
canvasCtx.font = '12px serif';
|
canvasCtx.font = '12px serif';
|
||||||
canvasCtx.fillText('月', rect.x + 76, metaY);
|
canvasCtx.fillText('月', offset(76), metaY);
|
||||||
canvasCtx.fillText('日', rect.x + 122, metaY);
|
canvasCtx.fillText('日', offset(122), metaY);
|
||||||
canvasCtx.fillText('姓名:', rect.x + 176, metaY);
|
canvasCtx.fillText('姓名:', offset(176), metaY);
|
||||||
|
|
||||||
canvasCtx.strokeStyle = '#8E897E';
|
canvasCtx.strokeStyle = '#8E897E';
|
||||||
canvasCtx.lineWidth = 1;
|
canvasCtx.lineWidth = 1;
|
||||||
canvasCtx.setLineDash([]);
|
canvasCtx.setLineDash([]);
|
||||||
canvasCtx.beginPath();
|
canvasCtx.beginPath();
|
||||||
canvasCtx.moveTo(rect.x + 40, metaUnderlineY);
|
canvasCtx.moveTo(offset(40), metaUnderlineY);
|
||||||
canvasCtx.lineTo(rect.x + 68, metaUnderlineY);
|
canvasCtx.lineTo(offset(68), metaUnderlineY);
|
||||||
canvasCtx.moveTo(rect.x + 85, metaUnderlineY);
|
canvasCtx.moveTo(offset(85), metaUnderlineY);
|
||||||
canvasCtx.lineTo(rect.x + 113, metaUnderlineY);
|
canvasCtx.lineTo(offset(113), metaUnderlineY);
|
||||||
canvasCtx.moveTo(rect.x + 195, metaUnderlineY);
|
canvasCtx.moveTo(offset(195), metaUnderlineY);
|
||||||
canvasCtx.lineTo(rect.x + 248, metaUnderlineY);
|
canvasCtx.lineTo(offset(248), metaUnderlineY);
|
||||||
canvasCtx.stroke();
|
canvasCtx.stroke();
|
||||||
|
|
||||||
canvasCtx.restore();
|
canvasCtx.restore();
|
||||||
|
|||||||
Reference in New Issue
Block a user