使用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>
关键知识点说明
beginPath()的必要性:每次动画循环都调用这个方法,确保我们绘制的是全新的路径,不会和之前的路径叠加导致线条变粗。- 分段动画逻辑:把复杂的谱号拆分成多个小部分,每个部分对应一个进度区间,这样更容易控制每个细节的动画效果。
- 贝塞尔曲线参数化:用贝塞尔曲线的数学公式计算当前进度下的点,实现曲线的平滑绘制动画。
- 动画速度控制:调整
amount += 0.003中的数值可以改变动画快慢,数值越大,绘制速度越快。
内容的提问来源于stack exchange,提问作者Martin Anders
相关产品推荐
相关产品推荐

