Canvas手绘动画如何实现笔/手形图标跟随绘制路径移动效果
Canvas手绘动画添加运笔跟随效果实现方案
问题背景
当前基于lineDashOffset实现的路径逐段描边动画已经可以正常运行,需要新增笔/手形素材跟随当前绘制端点移动,模拟真实白板书写效果。
原有实现代码:
<div class="container"> <div class="topButtons"> <select id="svgDropDown"> <option value="-1" selected="selected">Select SVG</option> <option value="0">Hello</option> <option value="1">Curve</option> <option value="2">Line</option> <option value="3">Happy</option> </select> <button id="playBtn" onclick="handleClick()">Play</button> </div> <canvas id='canvas' width='600' height='300'></canvas> </div>
function handleClick() { var e = document.getElementById("svgDropDown"); var option = e.value; if (option == -1) window.alert("Select SVG First"); else create(option); } function create(option) { var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); var offset = 1846; var pathMap = paths[option].path; var p = new Path2D(pathMap); function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); ctx.fillStyle = "#333"; ctx.rect(0, 0, 600, 300); ctx.fill(); ctx.beginPath(); ctx.strokeStyle = 'lightgreen'; ctx.lineCap = "round"; ctx.setLineDash([1846, 1846]); ctx.lineWidth = 4; ctx.lineDashOffset = offset; ctx.stroke(p); } var btn = document.getElementById("playBtn"); function animate() { offset -= 2; draw(); var lineDraw = setTimeout(animate, 5); if (offset <= 0) { clearTimeout(lineDraw); btn.disabled = false; } else btn.disabled = true; } animate(); };
实现步骤
核心逻辑是每帧获取当前绘制进度对应的路径坐标点,在该位置绘制笔素材即可,不需要改动原有描边动画的核心逻辑:
- 提前预加载笔/手形素材,避免动画运行时素材未加载出现闪烁
- 替换代码里写死的1846固定路径长度,用Canvas原生API动态计算当前选中路径的总长度,适配所有SVG路径
- 每帧根据当前
lineDashOffset计算已绘制的路径长度,调用原生API获取该长度对应的路径端点坐标,可选获取路径切线角度实现笔的自动旋转 - 路径绘制完成后,在端点位置绘制素材,调整素材偏移让笔尖精准对准路径端点
修改后完整代码
// 预加载笔素材,替换为你自己的本地素材路径 const penImg = new Image(); penImg.src = './pen.png'; let penReady = false; penImg.onload = () => penReady = true; function handleClick() { var e = document.getElementById("svgDropDown"); var option = e.value; if (option == -1) window.alert("Select SVG First"); else create(option); } function create(option) { var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); var pathMap = paths[option].path; var p = new Path2D(pathMap); // 动态计算路径总长度,替换原写死的固定值 const totalLength = ctx.measurePath(p).length; let offset = totalLength; // 笔素材配置,根据自己的素材实际尺寸调整 const PEN_WIDTH = 48; const PEN_HEIGHT = 48; // 笔尖在素材图上的相对坐标,比如笔尖距离素材左边缘2px、上边缘40px就填对应值 const PEN_TIP_OFFSET_X = 2; const PEN_TIP_OFFSET_Y = 40; function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制深色白板背景 ctx.fillStyle = "#333"; ctx.fillRect(0, 0, 600, 300); // 绘制路径描边动画 ctx.beginPath(); ctx.strokeStyle = 'lightgreen'; ctx.lineCap = "round"; ctx.setLineDash([totalLength, totalLength]); ctx.lineWidth = 4; ctx.lineDashOffset = offset; ctx.stroke(p); // 绘制跟随的笔素材 if(penReady && offset > 0) { // 计算当前已经绘制完成的路径长度 const drawnLength = totalLength - offset; // 获取当前绘制端点的坐标 const point = ctx.getPointAtLength(p, drawnLength); // 获取路径切线角度,让笔跟随运笔方向旋转,不需要可以删除旋转相关逻辑 const tangent = ctx.getTangentAtLength(p, drawnLength); const angle = Math.atan2(tangent.y, tangent.x); ctx.save(); // 将画布原点移动到笔尖位置 ctx.translate(point.x, point.y); ctx.rotate(angle); // 绘制笔素材,调整偏移让笔尖精准对准路径端点 ctx.drawImage(penImg, -PEN_TIP_OFFSET_X, -PEN_TIP_OFFSET_Y, PEN_WIDTH, PEN_HEIGHT); ctx.restore(); } } var btn = document.getElementById("playBtn"); function animate() { offset -= 2; draw(); var lineDraw = setTimeout(animate, 5); if (offset <= 0) { clearTimeout(lineDraw); btn.disabled = false; } else btn.disabled = true; } animate(); };
优化提示
- 笔/手素材建议使用透明背景PNG,根据白板主题调整素材大小,不要过大遮挡书写轨迹
- 如果需要兼容不支持
measurePath/getPointAtLength接口的旧版浏览器,可以手动解析SVG路径d属性,通过贝塞尔曲线采样算法计算对应长度的坐标点 - 可以给笔素材添加轻微的阴影效果,更贴合真实书写的层次感
- 动画逻辑建议替换
setTimeout为requestAnimationFrame,动画流畅度更高,不容易出现掉帧
内容的提问来源于stack exchange,提问作者Noob_Coder
相关产品推荐
相关产品推荐

