《HarmonyOS技术精讲-ArkGraphics 2D(方舟2D图形服务)》第二篇:路径的艺术——形状绘制与路径操作详解

发布时间:2026/7/7 7:53:39
《HarmonyOS技术精讲-ArkGraphics 2D(方舟2D图形服务)》第二篇:路径的艺术——形状绘制与路径操作详解 HarmonyOS技术精讲-ArkGraphics 2D方舟2D图形服务第二篇路径的艺术——形状绘制与路径操作详解路径操作从“画线”到“画形状”HarmonyOS NEXT 的 Canvas 绘图能力里Path对象是一个容易被低估的 API。不少人初次接触时习惯用多个moveTolineTo拼接多边形或者用多个arc拼合圆弧。这种做法虽然能跑但代码会迅速膨胀且复用性差——画个五角星要写几十行重复的坐标计算。Path解决的正是这类问题它提供了一套独立的“绘图指令集合”你可以先定义好路径的轮廓直线、贝塞尔曲线、弧线再统一填充或描边。更重要的是Path支持路径复用——同一个Path对象可以被多次绘制配合translate/rotate能轻松实现复杂图形的组合。这篇文章通过三个典型图形星形、心形、平滑曲线来演示Path的核心用法重点展示moveTo、lineTo、quadTo、cubicTo等命令的实际场景。代码全部基于 ArkTS在 DevEco Studio 6.1.0 及以上版本可完整运行。背景什么场景需要 Path场景适合 Path不适合 Path多边形星形、六边形✔️ 一条路径搞定❌ 单条线段用 lineTo 即可贝塞尔曲线心形、平滑线✔️ quadTo/cubicTo 原生支持❌ 多段直线拼接会出现尖锐折点重复绘制相同形状✔️ 复用 Path 对象性能更好❌ 每次新建 Path 会频繁 GC动态修改形状路径动画✔️ 修改 Path 的指令后重绘❌ 适合 Canvas2D 的更新机制环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机 / 平板核心实现用 Path 绘制星形、心形与平滑曲线1. 星形多边形的路径复用技巧星形的难点在于顶点的计算。我们用固定的半径和角度步长生成 10 个顶点5 个外顶点5 个内顶点然后通过moveTo和lineTo串联成一条闭合路径。// StarPath.etsimport{draw2D}fromkit.ArkGraphics2D;EntryComponentstruct StarPath{privatecanvasWidth:number300;privatecanvasHeight:number300;build(){Column(){Canvas(this.canvasWidth,this.canvasHeight).width(100%).height(300).onReady((ctx:draw2D.CanvasRenderingContext2D){this.drawStar(ctx,150,150,5,80,40);});}.padding(10);}/** * 绘制星形 * param ctx 渲染上下文 * param cx 中心点 x * param cy 中心点 y * param spikes 尖角数量5 就是五角星 * param outerR 外顶点半径 * param innerR 内顶点半径 */drawStar(ctx:draw2D.CanvasRenderingContext2D,cx:number,cy:number,spikes:number,outerR:number,innerR:number){// 创建一个 Path 对象// 注意path 在 onReady 中创建避免在 build() 中频繁对象constpathnewdraw2D.Path();conststepMath.PI/spikes;// 每个尖角占 π/spikes 弧度// 从第一个外点开始letangle-Math.PI/2;// 从正上方开始path.moveTo(cxouterR*Math.cos(angle),cyouterR*Math.sin(angle));for(leti1;ispikes*2;i){// 交替使用外顶点和内顶点constr(i%20)?outerR:innerR;anglestep;path.lineTo(cxr*Math.cos(angle),cyr*Math.sin(angle));}path.closePath();// 从最后一个点连线回到第一个点// 描边用 2px 红色ctx.strokeStyle#FF0000;ctx.lineWidth2;ctx.stroke(path);// 填充半透明金色ctx.fillStylergba(255, 215, 0, 0.5);ctx.fill(path);}}注意事项Path对象创建在onReady回调中而不是build()里 —— 在build()中创建对象会导致每次组件刷新都重新创建造成不必要的内存抖动。closePath()会自动用一条直线连接最后一个点和第一个点不需要手动调用lineTo。2. 心形贝塞尔曲线的典型应用心形曲线很难用直线段拟合。最佳方案是用两段二次贝塞尔曲线quadTo组合左半心形和右半心形各用一条quadTo得到平滑弧线。// HeartPath.etsdrawHeart(ctx:draw2D.CanvasRenderingContext2D,cx:number,cy:number,scale:number){constpathnewdraw2D.Path();// 心形控制点参数参考标准心形公式做适当缩放constsscale;// 起点心形底部尖点正下方path.moveTo(cx,cy0.8*s);// 右半心形两个控制点一个在右上一个在右中// 使用 quadTo (二次贝塞尔)cpx, cpy, x, y// 参数含义控制点坐标终点坐标path.quadTo(cx0.8*s,cy-0.3*s,// 控制点1右外侧cx,cy-0.6*s// 终点顶部中央);// 左半心形对称的控制点path.quadTo(cx-0.8*s,cy-0.3*s,cx,cy0.8*s);path.closePath();ctx.strokeStyle#E91E63;ctx.lineWidth3;ctx.stroke(path);ctx.fillStyle#FF4081;ctx.fill(path);}调用示例在onReady中this.drawHeart(ctx, 150, 150, 100);为什么用 quadTo 而不是 cubicTo心形只需要一组控制点控制每个半边的弧度二次贝塞尔足够了。cubicTo三次贝塞尔有两个控制点适合需要更精细调整形状的场景如下面的平滑曲线。3. 平滑曲线通过三次贝塞尔实现过点曲线如果只有一组离散点希望绘制一条经过这些点的平滑曲线可以用cubicTo配合样条插值计算控制点。这里给出一个简化版本假设点集是波浪形手动设定控制点来生成平滑过渡。// SmoothCurve.etsdrawSmoothCurve(ctx:draw2D.CanvasRenderingContext2D){constpathnewdraw2D.Path();// 手动定义一组点从左到右的波浪constpoints[{x:20,y:150},{x:60,y:80},{x:100,y:200},{x:140,y:100},{x:180,y:180},{x:220,y:120},{x:260,y:160},];// 起始点path.moveTo(points[0].x,points[0].y);// 遍历中间点用 cubicTo 连接for(leti1;ipoints.length-2;i){constp0points[i];constp1points[i1];// 控制点取前一点和后一点的加权平均使曲线平滑constcp1xp0.x(p1.x-points[i-1].x)/6;constcp1yp0.y(p1.y-points[i-1].y)/6;constcp2xp1.x-(points[i2].x-p0.x)/6;constcp2yp1.y-(points[i2].y-p0.y)/6;path.cubicTo(cp1x,cp1y,cp2x,cp2y,p1.x,p1.y);}// 处理最后一段constlastpoints[points.length-2];constendpoints[points.length-1];constcp1xlast.x(end.x-points[points.length-3].x)/6;constcp1ylast.y(end.y-points[points.length-3].y)/6;constcp2xend.x-(end.x-last.x)/6;constcp2yend.y-(end.y-last.y)/6;path.cubicTo(cp1x,cp1y,cp2x,cp2y,end.x,end.y);ctx.strokeStyle#2196F3;ctx.lineWidth2;ctx.stroke(path);}实际效果这条曲线会自然穿过每个给定的点且拐点平滑没有“折线感”。这是贝塞尔曲线在数据可视化或手写板绘制中的常用技巧。踩坑记录坑1closePath 与 stroke 的“不闭合”现象现象绘制多边形后调用closePath()但stroke描边时最后一个线段没有画出看起来图形缺了一个边。原因closePath()的作用是从当前点绘制一条直线到起点但这条直线是否被描边取决于是否在stroke之前调用了closePath。如果在stroke之后才closePath描边已经执行不会自动补充那条连线。此外closePath会改变路径的当前点但不会清除路径本身。解决始终在stroke之前完成所有路径指令并调用closePath。如果需要绘制开放路径如 U 形不要调用closePath。坑2Path 对象复用导致绘制残留现象用一个Path对象在不同位置绘制多个相同形状时第二次绘制的结果包含了第一次的路径导致重叠混乱。原因Path对象是一个可变的指令序列。调用draw2D.Path()创建后所有moveTo、lineTo等指令都会追加到该路径对象中。复用同一个Path对象时如果不先清空它保留上次的所有指令。解决两种方案每次重新创建Path对象简单但 GC 压力大使用path.reset()清空路径推荐减少对象创建// 清空路径后复用path.reset();path.moveTo(...);// ... 添加新指令特别注意reset()是 API 11 新增方法如果在 API 10 及之前版本需要每次new Path()。最佳实践将 Path 创建与指令定义分离到独立函数不要在onReady中写大段路径代码。抽取成createStarPath(cx, cy, ...): Path这样的纯函数方便测试和复用。原因路径生成逻辑常常与绘图上下文无关分离后更容易在不同 Canvas 间复用。填充和描边使用相同的 Path 对象先调stroke(path)再调fill(path)可以共用同一个路径对象避免重复计算。官方示例经常先 fill 后 stroke但实际项目里先 stroke 再 fill 效果无差异建议统一顺序以减少混淆。性能敏感时使用beginPathclosePath模式ArkGraphics 2D 中Path对象本身不依赖beginPath概念beginPath是 Canvas2D 标准接口但在 ArkUI 中通过ctx.beginPath()开启新路径然后使用ctx.moveTo等指令。这里需要明确本文使用的draw2D.Path是独立对象与ctx.beginPath是两套机制。混合使用时容易造成混乱——要么全部用Path对象要么全部用ctx的指令流。推荐统一用Path对象因为可以离线缓存路径且不会被其他绘制操作干扰。FAQQ为什么模拟器上显示正常真机不显示A检查onReady是否被正确触发。真机上 Canvas 组件可能因为布局延迟而未就绪。建议在onReady中添加日志确认并确保 Canvas 的width和height属性不为 0。Q绘制出来的路径锯齿感明显如何抗锯齿AArkGraphics 2D 默认开启了抗锯齿。如果仍然出现锯齿检查是否设置了过大的lineWidth比如小于 0.5px或者 Canvas 的物理分辨率较高需要适配pixelRatio。常见做法是在绘制前缩放上下文ctx.scale(pixelRatio, pixelRatio)。Q多个Path对象一起绘制时为什么最后一个路径会覆盖前面的A这是正常的绘制顺序。stroke和fill会按照调用顺序叠加。如果需要先绘制下层路径再上层请调整代码调用顺序。Demo 入口// entry/src/main/ets/pages/Index.etsimport{draw2D}fromkit.ArkGraphics2D;import{StarPath}from./StarPath;import{HeartPath}from./HeartPath;import{SmoothCurve}from./SmoothCurve;EntryComponentstruct Index{build(){Scroll(){Column({space:20}){Text(星形)StarPath()Text(心形)HeartPath()Text(平滑曲线)SmoothCurve()}.width(100%).padding(10)}}}示例代码地址项目地址如果你也遇到路径绘制中的“不闭合”或“绘制残留”问题可以重点检查closePath的调用时机和Path对象复用前的reset()。官方文档对Path的复用行为描述得比较简略建议结合实际运行效果测试确认。不同设备上的绘制效果可能因 GPU 抗锯齿算法略有差异建议以真机最终效果为准。