如何在JavaScript中实现类似Adobe Illustrator的平滑画笔笔触?
实现平滑书法画笔笔触的优化方案
针对你当前画笔笔触存在锯齿、不够流畅的问题,可以通过以下几个关键优化来达到专业绘图应用的平滑效果:
核心优化点
- 高DPI画布适配:匹配屏幕像素密度,避免因画布分辨率不足导致的锯齿
- Catmull-Rom样条路径平滑:对鼠标轨迹点进行插值处理,生成连续流畅的曲线
- 增强抗锯齿配置:开启高质量图像平滑,配合圆角线帽/线连接强化边缘柔和度
- 轨迹点缓存机制:收集鼠标移动轨迹点后批量绘制,减少分段直线的生硬感
修改后的完整代码
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); let isDrawing = false; let lastX = 0; let lastY = 0; let undoStack = []; // 存储鼠标轨迹点(包含对应线宽) let points = []; // 适配屏幕像素密度 const dpr = window.devicePixelRatio || 1; // 初始化画布尺寸与配置 function initCanvas() { const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; canvas.style.width = `${rect.width}px`; canvas.style.height = `${rect.height}px`; ctx.scale(dpr, dpr); // 开启高质量抗锯齿 ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "high"; } // 用二次贝塞尔曲线实现平滑路径(简化版Catmull-Rom样条) function drawSmoothPath(points) { if (points.length < 3) return; ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); // 遍历生成平滑曲线段 for (let i = 1; i < points.length - 2; i++) { const p0 = points[i]; const p1 = points[i + 1]; const cpX = (p0.x + p1.x) / 2; const cpY = (p0.y + p1.y) / 2; ctx.quadraticCurveTo(p0.x, p0.y, cpX, cpY); } // 绘制最后一段曲线 const lastPoint = points[points.length - 1]; const secondLast = points[points.length - 2]; ctx.quadraticCurveTo(secondLast.x, secondLast.y, lastPoint.x, lastPoint.y); ctx.stroke(); } function startDrawing(e) { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; // 初始化轨迹点数组 points = [{x: lastX, y: lastY, width: 11}]; } function draw(e) { if (!isDrawing) return; const currentX = e.offsetX; const currentY = e.offsetY; const dist = Math.sqrt((lastX - currentX) ** 2 + (lastY - currentY) ** 2); const maxLineWidth = 11; const minLineWidth = 4; const maxDistance = 75; let lineWidth = maxLineWidth - (dist / maxDistance) * (maxLineWidth - minLineWidth); lineWidth = Math.max(Math.min(lineWidth, maxLineWidth), minLineWidth); // 记录当前轨迹点及对应线宽 points.push({x: currentX, y: currentY, width: lineWidth}); // 清空画布并恢复上一次绘制内容 ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr); if (undoStack.length > 0) { ctx.putImageData(undoStack[undoStack.length - 1], 0, 0); } // 设置画笔样式 ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = "#000"; ctx.lineWidth = lineWidth; // 绘制平滑路径 drawSmoothPath(points); [lastX, lastY] = [currentX, currentY]; } function stopDrawing() { isDrawing = false; // 保存当前画布状态到撤销栈 undoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); } // 初始化画布并监听窗口尺寸变化 initCanvas(); window.addEventListener("resize", initCanvas); canvas.addEventListener("mousedown", startDrawing); canvas.addEventListener("mousemove", draw); canvas.addEventListener("mouseup", stopDrawing); canvas.addEventListener("mouseout", stopDrawing);
关键改动说明
- 高DPI适配:通过
devicePixelRatio缩放画布实际像素,解决Retina屏幕下绘制模糊、锯齿严重的问题。 - 轨迹点缓存:不再每次鼠标移动都绘制小段直线,而是收集所有轨迹点后批量生成平滑曲线,避免分段生硬感。
- 平滑路径生成:用二次贝塞尔曲线对轨迹点插值,让笔触过渡自然流畅,替代原有的直线段拼接。
- 抗锯齿增强:开启
high级别的图像平滑,配合圆角线帽和线连接,进一步弱化边缘锯齿。 - 窗口适配:监听窗口resize事件,自动调整画布尺寸,保证不同屏幕下的绘制效果一致。
当前笔触效果:
优化后,笔触会变得流畅平滑,边缘锯齿基本消失,同时保留书法笔的粗细随速度变化的特性。
内容的提问来源于stack exchange,提问作者Quốc Cường
相关产品推荐
相关产品推荐

