feat: 汉字每天打卡基本完成,细节还待优化

This commit is contained in:
R524809
2026-05-21 18:23:00 +08:00
parent e86e5f5aa7
commit a9bd118ec7
15 changed files with 1013 additions and 235 deletions
@@ -9,123 +9,13 @@
* - symbol:符号
*/
import { BaseDrawService } from '../../../core/draw/baseDraw';
import { GRID_COLORS, TRACING_COLORS } from '../../../core/data/tracingStyles';
import { TRACING_COLORS } from '../../../core/data/tracingStyles';
import { CharacterItem } from '../../../types/characterType';
/**
* cnchar-data 坐标系配置
* 参考 hanzi-writer 的实现逻辑
* 字符数据的边界框:左上角 (0, -124),右下角 (1024, 900)
*/
const CHAR_BOUNDS = {
minX: 0,
minY: -124,
maxX: 1024,
maxY: 900,
};
const CHAR_WIDTH = CHAR_BOUNDS.maxX - CHAR_BOUNDS.minX; // 1024
const CHAR_HEIGHT = CHAR_BOUNDS.maxY - CHAR_BOUNDS.minY; // 1024
/**
* 计算缩放变换参数(模仿 cnchar.draw 的 getScalingTransform 函数)
* 用于将 1024x1024 坐标系的字符数据变换到目标画布尺寸
*/
interface ScalingTransform {
xOffset: number;
yOffset: number;
scale: number;
}
function getScalingTransform(
width: number,
height: number,
padding: number,
): ScalingTransform {
// 计算可用空间
const availableWidth = width - 2 * padding;
const availableHeight = height - 2 * padding;
// 计算缩放比例(取较小的比例以确保字符完整显示)
const scaleX = availableWidth / CHAR_WIDTH;
const scaleY = availableHeight / CHAR_HEIGHT;
const scale = Math.min(scaleX, scaleY);
// 计算偏移量(居中显示)
const scaledWidth = CHAR_WIDTH * scale;
const scaledHeight = CHAR_HEIGHT * scale;
const centerX = padding + (availableWidth - scaledWidth) / 2;
const centerY = padding + (availableHeight - scaledHeight) / 2;
// 计算 xOffset 和 yOffset(考虑字符边界框的偏移)
const xOffset = -CHAR_BOUNDS.minX * scale + centerX;
const yOffset = -CHAR_BOUNDS.minY * scale + centerY;
return { xOffset, yOffset, scale };
}
/**
* SVG 路径解析与绘制(支持 M/L/Q/Z 命令)
* 参考 cnchar.draw 的实现,使用 Canvas transform 进行坐标变换
*/
interface DrawSvgPathParams {
ctx: RenderingContext;
pathD: string;
}
function drawSvgPath({ ctx, pathD }: DrawSvgPathParams) {
// 使用正则表达式匹配 SVG 路径命令
// 支持 MmoveTo)、L(lineTo)、Q(二次贝塞尔曲线)、ZclosePath)
const commandRegex = /([MLQZ])([^MLQZ]*?)(?=[MLQZ]|$)/gi;
const commands: Array<{ cmd: string; coords: string }> = [];
let match;
while ((match = commandRegex.exec(pathD)) !== null) {
const cmd = match[1].toUpperCase();
const coords = match[2].trim();
commands.push({ cmd, coords });
}
// 解析坐标的辅助函数
const parseCoords = (coords: string): number[] => {
return coords
.split(/[\s,]+/)
.filter((part) => part.trim() !== '')
.map((part) => parseFloat(part));
};
for (const { cmd, coords } of commands) {
const coordValues = parseCoords(coords);
switch (cmd) {
case 'M':
if (coordValues.length >= 2) {
ctx.moveTo(coordValues[0], coordValues[1]);
}
break;
case 'L':
if (coordValues.length >= 2) {
ctx.lineTo(coordValues[0], coordValues[1]);
}
break;
case 'Q':
// 二次贝塞尔曲线:Q cpx cpy x y
if (coordValues.length >= 4) {
ctx.quadraticCurveTo(
coordValues[0],
coordValues[1],
coordValues[2],
coordValues[3],
);
}
break;
case 'Z':
ctx.closePath();
break;
default:
console.warn(`未知的 SVG 路径命令: ${cmd}`);
}
}
}
import {
getScalingTransform,
drawSvgPath,
drawTianZiGrid,
} from '../../shared/drawUtils';
interface DrawStrokesParams {
ctx: RenderingContext;
@@ -187,7 +77,7 @@ function drawStrokes({
// 绘制路径
ctx.beginPath();
drawSvgPath({ ctx, pathD: strokes[s] });
drawSvgPath(ctx, strokes[s]);
// 使用 fill 绘制(cnchar 只使用 fill
ctx.fillStyle = fillStyle;
@@ -197,48 +87,6 @@ function drawStrokes({
}
}
interface DrawTianZiGridParams {
ctx: RenderingContext;
x: number;
y: number;
size: number;
lineColor?: string;
boldColor?: string;
}
function drawTianZiGrid({
ctx,
x,
y,
size,
lineColor = GRID_COLORS.middleLine,
boldColor = GRID_COLORS.border,
}: DrawTianZiGridParams) {
ctx.strokeStyle = boldColor;
ctx.lineWidth = 1;
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
ctx.strokeStyle = lineColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, y - size / 2);
ctx.lineTo(x, y + size / 2);
ctx.moveTo(x - size / 2, y);
ctx.lineTo(x + size / 2, y);
ctx.stroke();
ctx.closePath();
ctx.strokeStyle = GRID_COLORS.diagonal;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x - size / 2, y - size / 2);
ctx.lineTo(x + size / 2, y + size / 2);
ctx.moveTo(x + size / 2, y - size / 2);
ctx.lineTo(x - size / 2, y + size / 2);
ctx.stroke();
ctx.closePath();
}
class WordDrawService extends BaseDrawService {
constructor(
canvas: Canvas,
@@ -421,7 +269,7 @@ class WordDrawService extends BaseDrawService {
for (let col = 0; col < maxCol; col++) {
const x = startX + col * (cellSize + colGap) + cellSize / 2;
const y = startY + row * (cellSize + rowGap) + cellSize / 2;
drawTianZiGrid({ ctx, x, y, size: cellSize });
drawTianZiGrid({ ctx, cx: x, cy: y, size: cellSize });
}
}
}