canvas 贝塞尔曲线绘制动态流动线
效果如下:
无意看到类似上图效果,就想着自己复现下,也熟悉下canvas方法。为了方便计算,我把每个tab列表等分10份,每个192px,渐变色长度为192 X 2;曲线是通过三次贝赛尔曲线绘制的,曲线运动是通过这个drawCurvePath方法,根据曲线的占比绘制曲线,具体代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <style> .tab { width: 1920px; height: 76px; display: flex; font-size: 30px; text-align: center; align-items: center; position: absolute; } .tab span { display: inline-block; width: 192px; } </style> <body> <div> <div> <div class="tab"> <span attr_index="1">tab1</span> <span attr_index="2">tab2</span> <span attr_index="3">tab3</span> <span attr_index="4">tab4</span> </div> <canvas width="1920" height="100" id="canvas"></canvas> </div> </div> </body> <script> const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const radius = 25; // 圆角大小 const height = 50; //高度 let current = 4; // 圆角所在索引 let process = 0.3; // 高亮线进度 function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); let currentPre = (current - 1) / 10; let startX = 0; // 起点x坐标 let startY = 60; // 起点y坐标 let startX1 = 192 * (current - 1); //第一个圆角x起点 let startY1 = 60; //第一个圆角y起点 let startX2 = startX1 + radius; // 第一个圆角y终点 let startY2 = startY1 - radius; // 第一个圆角y终点 let arcX1 = startX2; // 第一个圆角x圆心 let arcY1 = startY1; // 第一个圆角y圆心 let startX3 = startX2 + height - 2 * radius; // 第二个圆角x起点 let startY3 = startY2 - (height - 2 * radius); // 第二个圆角y起点 let startX4 = startX3 + radius; // 第二个圆角x终点 let startY4 = startY3 - radius; // 第二个圆角y终点 let arcX2 = startX3; // 第二个圆角x圆心 let arcY2 = startY4; // 第二个圆角y圆心 let startX5 = startX4 + 192 - height * 2; // 第三个圆角x起点 let startY5 = startY4; // 第三个圆角y起点 let startX6 = startX5 + radius; // 第三个圆角x终点 let startY6 = startY5 + radius; // 第三个圆角y终点 let arcX3 = startX6; // 第三个圆角x圆心 let arcY3 = startY5; // 第三个圆角y圆心 let startX7 = startX6 + height - 2 * radius; // 第四个圆角x起点 let startY7 = startY6 + height - 2 * radius; // 第四个圆角y起点 let startX8 = startX7 + radius; // 第四个圆角x终点 let startY8 = startY7 + radius; // 第四个圆角y终点 let arcX4 = startX7; // 第四个圆角x圆心 let arcY4 = startY8; // 第四个圆角y圆心 let endX = startX8 + 192 * (10 - current); // 终点x坐标 let endY = startY8; // 终点y坐标 // 绘制默认线 ctx.strokeStyle = "green"; // 设置默认线颜色为绿色 ctx.beginPath(); ctx.moveTo(startX, startY); ctx.lineTo(startX1, startY1); ctx.bezierCurveTo(arcX1, arcY1, arcX2, arcY2, startX4, startY4); ctx.lineTo(startX5, startY5); ctx.bezierCurveTo(arcX3, arcY3, arcX4, arcY4, startX8, startY8); ctx.lineTo(endX, endY); ctx.stroke(); ctx.closePath(); ctx.save(); //绘制高亮线 let gradient = ctx.createLinearGradient( 192 * smoothstep(process - 0.2) * 10, 100, 192 * process * 10, 100 ); gradient.addColorStop(0, "#ffeb3b3b"); gradient.addColorStop(0.5, "#ffc107b3"); gradient.addColorStop(1, "#ffeb3b"); ctx.strokeStyle = gradient; //设置渐变色 ctx.beginPath(); ctx.shadowColor = "red"; ctx.shadowBlur = 3; ctx.moveTo( startX + (startX1 - startX) * smoothstep(((process - 0.2) * 10) / (current - 1)), startY + (startY1 - startY) * smoothstep(((process - 0.2) * 10) / (current - 1)) ); ctx.lineTo( startX + (startX1 - startX) * smoothstep((process * 10) / (current - 1)), startY + (startY1 - startY) * smoothstep((process * 10) / (current - 1)) ); if (process > currentPre && process < currentPre + 0.2) { let t = smoothstep((process - currentPre) / 0.026); drawCurvePath( ctx, startX1, arcX1, arcX2, startX4, startY1, arcY1, arcY2, startY4, t ); } if (process > currentPre + 0.026 && process < currentPre + 0.226) { if (process > currentPre + 0.2 && process < currentPre + 0.226) { ctx.moveTo(startX4, startY4); let t = smoothstep((currentPre + 0.226 - process) / 0.026); drawCurvePath( ctx, startX4, arcX2, arcX1, startX1, startY4, arcY2, arcY1, startY1, t ); } ctx.moveTo(startX4, startY4); ctx.lineTo( startX4 + (startX5 - startX4) * smoothstep((process - (currentPre + 0.026)) / 0.048), startY5 ); } if (process > currentPre + 0.074 && process < currentPre + 0.274) { let t = smoothstep((process - (currentPre + 0.074)) / 0.026); ctx.moveTo(startX5, startY5); drawCurvePath( ctx, startX5, arcX3, arcX4, startX8, startY5, arcY3, arcY4, startY8, t ); } if (process >= currentPre + 0.226 && process < currentPre + 0.274) { ctx.moveTo(startX5, startY5); ctx.lineTo( startX4 + (startX5 - startX4) * smoothstep(1 - (currentPre + 0.274 - process) / 0.048), startY5 ); } if (process > currentPre + 0.274 && process < 0.9 - currentPre) { ctx.moveTo(startX8, startY8); let t = smoothstep((0.9 - currentPre - process) / 0.026); drawCurvePath( ctx, startX8, arcX4, arcX3, startX5, startY8, arcY4, arcY3, startY5, t ); } if (process >= currentPre + 0.1) { ctx.moveTo(startX8, startY8); ctx.moveTo( startX8 + (endX - startX8) * smoothstep( ((process - (currentPre + 0.3)) * 10) / (10 - current) ), startY8 + (endY - startY8) * smoothstep(((process - (currentPre + 0.3)) * 10) / (10 - current)) ); ctx.lineTo( startX8 + (endX - startX8) * smoothstep( ((process - (currentPre + 0.1)) * 10) / (10 - current) ), startY8 + (endY - startY8) * smoothstep(((process - (currentPre + 0.1)) * 10) / (10 - current)) ); } ctx.stroke(); ctx.restore(); if (process >= 1) { process = 0; } else { process += 0.004; } requestAnimationFrame(draw); } draw(); let tab = document.getElementsByClassName("tab")[0]; tab.addEventListener("click", function (event) { let index = event.target.getAttribute("attr_index"); current = index; }); function getBezierCurveXY(t, px0, px1, px2, px3, py0, py1, py2, py3) { //三次贝塞尔曲线方程式,根据t参数,返回对应的x,y值 //let x2 =(1-t)^3 * P0 + 3t(1-t)^2 * P1 + 3t^2(1-t) * P2 + t^3 * P3 let x = px0 * Math.pow(1 - t, 3) + px1 * 3 * t * Math.pow(1 - t, 2) + px2 * 3 * Math.pow(t, 2) * (1 - t) + px3 * Math.pow(t, 3); let y = py0 * Math.pow(1 - t, 3) + py1 * 3 * t * Math.pow(1 - t, 2) + py2 * 3 * Math.pow(t, 2) * (1 - t) + py3 * Math.pow(t, 3); let points = { x, y, }; return points; } //返回一个0-1之间的值 function smoothstep(sum, max = 1, min = 0) { if (sum < min) { sum = min; } else if (sum > max) { sum = max; } return sum; } // 绘制整个贝塞尔曲线的百分比 function drawCurvePath(ctx, px0, px1, px2, px3, py0, py1, py2, py3, t) { var p01 = [px1 - px0, py1 - py0]; // 向量 p0 -> p1 var p12 = [px2 - px1, py2 - py1]; // 向量 p1 -> p2 var p23 = [px3 - px2, py3 - py2]; // 向量 p2 -> p3 var q0 = [px0 + p01[0] * t, py0 + p01[1] * t]; var q1 = [px1 + p12[0] * t, py1 + p12[1] * t]; var q2 = [px2 + p23[0] * t, py2 + p23[1] * t]; var q01 = [q1[0] - q0[0], q1[1] - q0[1]]; // 向量 q0 -> q1 var q12 = [q2[0] - q1[0], q2[1] - q1[1]]; // 向量 q1 -> q2 var r0 = [q0[0] + q01[0] * t, q0[1] + q01[1] * t]; var r1 = [q1[0] + q12[0] * t, q1[1] + q12[1] * t]; var r01 = [r1[0] - r0[0], r1[1] - r0[1]]; // 向量 r0 -> r1 var b = [r0[0] + r01[0] * t, r0[1] + r01[1] * t]; // ctx.moveTo(px0, py0); ctx.bezierCurveTo(q0[0], q0[1], r0[0], r0[1], b[0], b[1]); } </script> </html>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具