You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 07:12:28