feat:优化练字贴
This commit is contained in:
@@ -21,13 +21,21 @@ function drawSvgPathCommands({
|
||||
}: DrawSvgPathCommandsParams) {
|
||||
// console.log('drawSvgPathCommands 参数:', { pathD, offsetX, offsetY, scale });
|
||||
|
||||
// 精度处理函数:避免浮点数精度问题
|
||||
const roundToPrecision = (num: number, precision: number = 2): number => {
|
||||
// 精度处理函数:避免浮点数精度问题(提高到6位小数精度,确保高精度)
|
||||
const roundToPrecision = (num: number, precision: number = 6): number => {
|
||||
return (
|
||||
Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision)
|
||||
);
|
||||
};
|
||||
|
||||
// 像素对齐函数:优化对齐策略,提升平滑度
|
||||
// 对于Fill+Stroke模式,使用更精细的对齐策略,避免过度对齐导致的锯齿
|
||||
// 对齐到0.5像素可以获得更好的平滑效果
|
||||
const alignToPixel = (num: number): number => {
|
||||
// 对齐到0.5像素,而不是整数像素,可以获得更平滑的线条
|
||||
return Math.round(num * 2) / 2;
|
||||
};
|
||||
|
||||
// 新的解析方法:按命令片段解析
|
||||
// 使用正则表达式匹配从字母开头到下一个字母(或结尾)的片段
|
||||
const commandRegex = /([MLZ])([^MLZ]*?)(?=[MLZ]|$)/g;
|
||||
@@ -60,9 +68,14 @@ function drawSvgPathCommands({
|
||||
const y = parseFloat(coordParts[1]);
|
||||
|
||||
if (!isNaN(x) && !isNaN(y) && isFinite(x) && isFinite(y)) {
|
||||
// 计算最终坐标并处理精度
|
||||
const finalX = roundToPrecision(offsetX + x * scale);
|
||||
const finalY = roundToPrecision(offsetY + y * scale);
|
||||
// 计算最终坐标:先进行高精度计算,再对齐到整数像素
|
||||
// 使用高精度计算确保路径的准确性,然后对齐到像素避免亚像素渲染
|
||||
const calculatedX = offsetX + x * scale;
|
||||
const calculatedY = offsetY + y * scale;
|
||||
const preciseX = roundToPrecision(calculatedX);
|
||||
const preciseY = roundToPrecision(calculatedY);
|
||||
const finalX = alignToPixel(preciseX);
|
||||
const finalY = alignToPixel(preciseY);
|
||||
|
||||
// console.log(`${cmd}: 绘制到 (${finalX}, ${finalY}) [原始: (${x}, ${y})]`);
|
||||
drawFunction(finalX, finalY);
|
||||
@@ -85,8 +98,8 @@ function drawSvgPathCommands({
|
||||
L: (coords) =>
|
||||
parseAndDrawCoords(coords, 'L', (x, y) => ctx.lineTo(x, y)),
|
||||
Z: () => {
|
||||
// console.log('Z: 闭合路径');
|
||||
// 注意:不要在这里调用 closePath(),因为 drawStrokes 中会统一处理
|
||||
// 关键修复:处理Z命令,闭合路径
|
||||
ctx.closePath();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -124,21 +137,40 @@ function drawStrokes({
|
||||
offsetY,
|
||||
size,
|
||||
uptoInclusive,
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
lineWidth,
|
||||
fillStyle, // 填充颜色,用于 Fill + Stroke 模式
|
||||
strokeStyle, // 描边颜色,用于绘制笔画
|
||||
lineWidth, // 线条宽度,控制笔画粗细
|
||||
}: DrawStrokesParams) {
|
||||
// 设置 Canvas 绘制质量(关键优化:提升线条流畅度)
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high'; // 高质量平滑
|
||||
|
||||
// 设置路径绘制属性(关键:这些设置直接影响笔画粗细和流畅度)
|
||||
// 注意:不使用 save/restore,与测试页面保持一致,避免状态管理问题
|
||||
ctx.lineCap = 'round'; // 圆角端点,使笔画末端更自然
|
||||
ctx.lineJoin = 'round'; // 圆角连接,使转折处更平滑
|
||||
ctx.miterLimit = 10; // 斜接限制
|
||||
|
||||
// 优化渲染策略:使用 Fill + Stroke 模式
|
||||
// 先 fill 填充内部,再 stroke 描边,stroke 宽度略小,避免双重渲染导致的毛糙
|
||||
// 这样可以获得自然的笔画粗细,同时保持流畅度
|
||||
|
||||
// 模板中使用 54x54 的视窗尺寸
|
||||
const viewBoxSize = 54;
|
||||
|
||||
// 添加内边距:在田字格四周预留空间,避免笔画贴边
|
||||
const padding = size * 0.1; // 内边距为田字格大小的10%
|
||||
const contentSize = size - padding * 2; // 实际绘制区域大小
|
||||
const contentScale = contentSize / viewBoxSize; // 调整后的缩放比例
|
||||
|
||||
// 优化缩放比例:使用更精确的缩放,不进行四舍五入,保持原始精度
|
||||
const contentScale = contentSize / viewBoxSize;
|
||||
|
||||
// 计算内容区域的起始位置(居中)
|
||||
const contentOffsetX = offsetX + padding;
|
||||
const contentOffsetY = offsetY + padding;
|
||||
// 计算内容区域的起始位置(居中),优化对齐策略提升平滑度
|
||||
const rawOffsetX = offsetX + padding;
|
||||
const rawOffsetY = offsetY + padding;
|
||||
// 对齐到0.5像素,而不是整数像素,可以获得更平滑的渲染效果
|
||||
const contentOffsetX = Math.round(rawOffsetX * 2) / 2;
|
||||
const contentOffsetY = Math.round(rawOffsetY * 2) / 2;
|
||||
|
||||
// console.log('drawStrokes 参数:', {
|
||||
// strokesCount: strokes.length,
|
||||
@@ -160,6 +192,10 @@ function drawStrokes({
|
||||
for (let s = 0; s <= uptoInclusive && s < strokes.length; s++) {
|
||||
// console.log(`绘制第 ${s} 个笔画:`, strokes[s]);
|
||||
ctx.beginPath();
|
||||
|
||||
// 记录路径是否包含Z命令(已闭合)
|
||||
const hasClosePath = strokes[s].includes('Z');
|
||||
|
||||
drawSvgPathCommands({
|
||||
ctx,
|
||||
pathD: strokes[s],
|
||||
@@ -167,13 +203,27 @@ function drawStrokes({
|
||||
offsetY: contentOffsetY,
|
||||
scale: contentScale,
|
||||
});
|
||||
|
||||
// 优化渲染策略:使用 Fill + Stroke 模式
|
||||
// 先 fill 填充内部,再 stroke 描边,stroke 宽度略小,避免双重渲染导致的毛糙
|
||||
// 如果路径没有Z命令,手动闭合(fill需要闭合路径)
|
||||
if (!hasClosePath) {
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
// 1. 先填充内部
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.fill();
|
||||
|
||||
// 2. 再描边(stroke宽度设为fill视觉宽度的70%,避免明显的双重渲染)
|
||||
// 与测试页面保持一致:使用相同的stroke宽度计算方式
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
const strokeWidth = Math.max(1.5, lineWidth * 0.7);
|
||||
ctx.lineWidth = strokeWidth;
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
// 注意:不使用 restore,与测试页面保持一致
|
||||
}
|
||||
|
||||
interface DrawTianZiGridParams {
|
||||
@@ -190,15 +240,15 @@ function drawTianZiGrid({
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
lineColor = '#e0e0e0',
|
||||
boldColor = '#cccccc',
|
||||
lineColor = '#a8d5a8', // 浅绿色(中线)
|
||||
boldColor = '#7fb069', // 中等绿色(外框)
|
||||
}: DrawTianZiGridParams) {
|
||||
// 外框(逻辑像素)
|
||||
// 外框(逻辑像素)- 使用中等绿色,清晰可见
|
||||
ctx.strokeStyle = boldColor;
|
||||
ctx.lineWidth = 1; // 2/3≈1,逻辑像素
|
||||
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
|
||||
|
||||
// 中线(逻辑像素)
|
||||
// 中线(逻辑像素)- 使用浅绿色,柔和护眼
|
||||
ctx.strokeStyle = lineColor;
|
||||
ctx.lineWidth = 1; // 逻辑像素
|
||||
ctx.beginPath();
|
||||
@@ -211,8 +261,8 @@ function drawTianZiGrid({
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
|
||||
// 对角线(淡,逻辑像素)
|
||||
ctx.strokeStyle = '#eeeeee';
|
||||
// 对角线(淡绿色,逻辑像素)- 使用很淡的绿色,提供辅助参考线
|
||||
ctx.strokeStyle = '#d4f0d4'; // 很淡的绿色
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - size / 2, y - size / 2);
|
||||
@@ -519,7 +569,8 @@ class WordDrawService extends BaseDrawService {
|
||||
|
||||
// console.log(`预览格 [${rowIndex}, ${columnIndex}] 坐标: (${x}, ${y}), 笔画数: ${strokes.length}`);
|
||||
|
||||
// 绘制完整汉字(黑色,较粗)
|
||||
// 绘制完整汉字(深灰色,Fill + Stroke模式)
|
||||
// 参考一般练字贴:预览字使用深灰色,不是纯黑色,更柔和护眼
|
||||
drawStrokes({
|
||||
ctx,
|
||||
strokes,
|
||||
@@ -527,9 +578,9 @@ class WordDrawService extends BaseDrawService {
|
||||
offsetY: y,
|
||||
size: cellSize,
|
||||
uptoInclusive: strokes.length - 1,
|
||||
fillStyle: 'rgb(0,0,0)', // 黑色填充
|
||||
strokeStyle: 'rgb(0,0,0)', // 黑色描边
|
||||
lineWidth: 1, // 较粗的线条(4/3≈1,逻辑像素)
|
||||
fillStyle: 'rgb(85,85,85)', // 深灰色填充(#555555),比#666666更深一点
|
||||
strokeStyle: 'rgb(85,85,85)', // 深灰色描边
|
||||
lineWidth: 0.6, // Fill + Stroke 模式,线条宽度调小一点(逻辑像素)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -564,7 +615,8 @@ class WordDrawService extends BaseDrawService {
|
||||
|
||||
// console.log(`练习格 [${rowIndex}, ${columnIndex}] 坐标: (${x}, ${y}), 笔画: ${strokeIndex + 1}/${strokes.length}`);
|
||||
|
||||
// 绘制到指定笔画的汉字(灰色,中等粗细)
|
||||
// 绘制到指定笔画的汉字(浅灰色,Fill + Stroke模式)
|
||||
// 参考一般练字贴:练习字使用浅灰色,便于临摹
|
||||
drawStrokes({
|
||||
ctx,
|
||||
strokes,
|
||||
@@ -572,14 +624,14 @@ class WordDrawService extends BaseDrawService {
|
||||
offsetY: y,
|
||||
size: cellSize,
|
||||
uptoInclusive: strokeIndex,
|
||||
fillStyle: '#ccc',
|
||||
strokeStyle: '#ccc',
|
||||
lineWidth: 1, // 中等粗细(3/3=1,逻辑像素)
|
||||
fillStyle: 'rgb(170,170,170)', // 浅灰色填充(#aaaaaa),参考练字贴颜色
|
||||
strokeStyle: 'rgb(170,170,170)', // 浅灰色描边
|
||||
lineWidth: 0.6, // Fill + Stroke 模式,线条宽度调小一点(逻辑像素)
|
||||
});
|
||||
}
|
||||
|
||||
// drawDivider 已在基类中实现,此方法保留以保持兼容
|
||||
drawDivider(linY?: number) {
|
||||
drawDivider() {
|
||||
super.drawDivider();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user