feat: 汉字每天打卡基本完成,细节还待优化
This commit is contained in:
@@ -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 路径命令
|
||||
// 支持 M(moveTo)、L(lineTo)、Q(二次贝塞尔曲线)、Z(closePath)
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user