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

使用HTML5 Canvas绘制带转弯的动画线条及G谱号(Sol Key)

嘿,我知道你卡在这个Canvas动画上了——要把简单的直线改成带G谱号的动态绘制确实需要调整原有的逻辑。咱们一步步来解决这个问题:

原代码的核心问题

你的初始代码只处理了直线的线性过渡,而且没有调用beginPath(),导致每次stroke()都会重绘之前的所有路径,不仅线条会越来越粗,也没法正确添加曲线和复杂图形。

解决方案:分段路径动画

我们可以把整个绘制内容拆分成G谱号的各个部分+右侧直线,分阶段控制每个部分的动画进度,这样就能实现平滑的逐步绘制效果。

下面是完整的可运行代码:

$(function() {
  var canvas = $("#paper")[0];
  var c = canvas.getContext("2d");
  var h = canvas.height;
  var startX = 50; // 调整起点,给谱号留出足够空间
  var startY = h/2;
  var amount = 0;

  // 定义每个路径段的绘制逻辑和对应的进度范围
  var pathSegments = [
    // 1. G谱号上弧:进度0 ~ 0.2
    {
      range: [0, 0.2],
      draw: (progress) => {
        c.moveTo(startX, startY);
        // 用贝塞尔曲线参数方程计算当前进度的点
        const cp1 = {x: startX + 10, y: startY - 80};
        const cp2 = {x: startX + 30, y: startY - 100};
        const end = {x: startX + 20, y: startY - 60};
        const t = progress;
        
        const currentX = Math.pow(1-t,3)*startX + 3*Math.pow(1-t,2)*t*cp1.x + 3*(1-t)*Math.pow(t,2)*cp2.x + Math.pow(t,3)*end.x;
        const currentY = Math.pow(1-t,3)*startY + 3*Math.pow(1-t,2)*t*cp1.y + 3*(1-t)*Math.pow(t,2)*cp2.y + Math.pow(t,3)*end.y;
        
        c.bezierCurveTo(
          startX + (cp1.x - startX)*t, startY + (cp1.y - startY)*t,
          startX + (cp2.x - startX)*t, startY + (cp2.y - startY)*t,
          currentX, currentY
        );
      }
    },
    // 2. G谱号下弧:进度0.2 ~ 0.4
    {
      range: [0.2, 0.4],
      draw: (progress) => {
        // 先完整绘制上弧
        c.moveTo(startX, startY);
        c.bezierCurveTo(startX + 10, startY - 80, startX + 30, startY - 100, startX + 20, startY - 60);
        
        // 绘制下弧的进度部分
        const cp1 = {x: startX + 10, y: startY - 20};
        const cp2 = {x: startX + 30, y: startY + 20};
        const end = {x: startX + 20, y: startY + 60};
        const t = progress;
        const startArc = {x: startX + 20, y: startY - 60};
        
        const currentX = Math.pow(1-t,3)*startArc.x + 3*Math.pow(1-t,2)*t*cp1.x + 3*(1-t)*Math.pow(t,2)*cp2.x + Math.pow(t,3)*end.x;
        const currentY = Math.pow(1-t,3)*startArc.y + 3*Math.pow(1-t,2)*t*cp1.y + 3*(1-t)*Math.pow(t,2)*cp2.y + Math.pow(t,3)*end.y;
        
        c.bezierCurveTo(
          startArc.x + (cp1.x - startArc.x)*t, startArc.y + (cp1.y - startArc.y)*t,
          startArc.x + (cp2.x - startArc.x)*t, startArc.y + (cp2.y - startArc.y)*t,
          currentX, currentY
        );
      }
    },
    // 3. 谱号竖线:进度0.4 ~ 0.5
    {
      range: [0.4, 0.5],
      draw: (progress) => {
        // 完整绘制上弧和下弧
        c.moveTo(startX, startY);
        c.bezierCurveTo(startX + 10, startY - 80, startX + 30, startY - 100, startX + 20, startY - 60);
        c.bezierCurveTo(startX + 10, startY - 20, startX + 30, startY + 20, startX + 20, startY + 60);
        
        // 绘制竖线的进度部分
        const endY = startY + 60;
        const currentY = startY + 60 * progress;
        c.lineTo(startX, currentY);
      }
    },
    // 4. 内部小圆圈:进度0.5 ~ 0.7
    {
      range: [0.5, 0.7],
      draw: (progress) => {
        // 完整绘制前面的部分
        c.moveTo(startX, startY);
        c.bezierCurveTo(startX + 10, startY - 80, startX + 30, startY - 100, startX + 20, startY - 60);
        c.bezierCurveTo(startX + 10, startY - 20, startX + 30, startY + 20, startX + 20, startY + 60);
        c.lineTo(startX, startY + 60);
        
        // 绘制圆弧的进度部分
        const center = {x: startX + 15, y: startY - 20};
        const radius = 8;
        c.arc(center.x, center.y, radius, 0, Math.PI * 2 * progress);
      }
    },
    // 5. 右侧延伸直线:进度0.7 ~ 1.0
    {
      range: [0.7, 1.0],
      draw: (progress) => {
        // 完整绘制G谱号
        c.moveTo(startX, startY);
        c.bezierCurveTo(startX + 10, startY - 80, startX + 30, startY - 100, startX + 20, startY - 60);
        c.bezierCurveTo(startX + 10, startY - 20, startX + 30, startY + 20, startX + 20, startY + 60);
        c.lineTo(startX, startY + 60);
        c.arc(startX + 15, startY - 20, 8, 0, Math.PI * 2);
        c.closePath();
        
        // 绘制直线的进度部分
        const lineStart = {x: startX + 20, y: startY};
        const lineEndX = canvas.width - 20;
        const currentX = lineStart.x + (lineEndX - lineStart.x) * progress;
        c.moveTo(lineStart.x, lineStart.y);
        c.lineTo(currentX, lineStart.y);
      }
    }
  ];

  const animation = setInterval(function() {
    amount += 0.003; // 调整这个值可以控制动画速度,越大越快
    if (amount >= 1) {
      amount = 1;
      clearInterval(animation); // 动画完成后停止循环
    }

    c.clearRect(0, 0, canvas.width, canvas.height);
    c.strokeStyle = "black";
    c.lineWidth = 3; // 加粗线条,更接近真实乐谱的谱号
    c.beginPath(); // 关键:每次绘制都重新创建路径,避免重复绘制

    // 根据当前进度绘制对应的路径段
    for (const segment of pathSegments) {
      const [min, max] = segment.range;
      if (amount >= min && amount <= max) {
        const segmentProgress = (amount - min) / (max - min);
        segment.draw(segmentProgress);
        break;
      } else if (amount > max) {
        segment.draw(1); // 已经过了这个段,完整绘制它
      }
    }

    c.stroke();
  }, 10);
});

对应的HTML部分(修正了canvas的border属性,用CSS设置更规范):

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="paper" width="800" height="600" style="border: 1px solid #000;"></canvas>

关键知识点说明

  1. beginPath()的必要性:每次动画循环都调用这个方法,确保我们绘制的是全新的路径,不会和之前的路径叠加导致线条变粗。
  2. 分段动画逻辑:把复杂的谱号拆分成多个小部分,每个部分对应一个进度区间,这样更容易控制每个细节的动画效果。
  3. 贝塞尔曲线参数化:用贝塞尔曲线的数学公式计算当前进度下的点,实现曲线的平滑绘制动画。
  4. 动画速度控制:调整amount += 0.003中的数值可以改变动画快慢,数值越大,绘制速度越快。

内容的提问来源于stack exchange,提问作者Martin Anders

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:16:41