#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

莫道桑榆晚,为霞尚满天。这篇文章主要讲述#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤相关的知识,希望能为你提供帮助。
春节不停更,此文正在参加「星光计划-春节更帖活动」
作者:包月东
简介今天给大家带来如何用纯JS代码的Canvas绘制一条游动的锦鲤
先看下效果图

#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

实现思路 拆分
先看设计图,我们的小鱼由头、鳍、身体、节肢、尾部五部分组成。
#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

下面给出每个部位的实现api
  • 头是一个实心圆,可以通过canvas.arc()
  • 鱼鳍则由一条直线和曲线构成,会用到path,曲线可以使用贝塞尔曲线canvas.quadraticCurveTo()。
  • 身体由两条直线和曲线构成,曲线也选用贝塞尔曲线,通过控制点可以控制小鱼胖瘦
  • 节肢1由两个实心圆和一个梯形构成。梯形可以看出四条封闭的线段构成。
  • 节肢2由一个梯形和一个圆,方案同上
  • 尾由两个三角形,也是封闭线段
参数
基准尺寸参数首先我们定一个基准尺寸参数来控制小鱼的大小,这里我们选取鱼头的半径R
其他参数如下:
小鱼身长设为:3.2R 节肢1大圆的半径设为:0.7R 节肢1中圆的半径设为:0.7*0.6R=0.42R 节肢1梯形高度设为:(0.7+0.42)R=1.12R 节肢2小圆的半径设为:0.4*0.42R=0.168R 节肢2梯形高度设为:0.42*(0.4+2.7)R=1.302R 小鱼的总长度:R+3.2R+1.12R+0.168R+1.302R=6.79R 画布的长宽:2*(6.79R-R-1.6R)=2*4.19R=8.38R

小鱼的整体尺寸如下:
#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

值得注意的是,画布的长度并不等于小鱼的长度,因为小鱼的重心并不位于鱼长中心,而是小鱼身的中心点,但是小鱼的转身却要围绕它,因此我们画布的半径需要拓展成小鱼身中心到小鱼尾的长度,整个画布的大小=4.19R*4.19R,如下
#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

基准旋转参数为了使我们的小鱼能够左右掉头,这里就需要一个旋转角度,我们选取fishMainAngle作为主角度。
实现步骤 自定义一个Canvas的组件
< canvas ref="fishcanvas" id="fishcanvas"> < /canvas>

持有绘图相关的上下文
onAttached() setTimeout(() => //这里需要延迟得到canvas this.canvas = this.$refs.fishcanvas.getContext(2d, antialias: true); this.onDraw() ,200)

这里我们延迟200ms,在js中获取hml定义的fishcanvas组件,再拿到context并持有。接着执行onDraw进行绘制
定义绘制方法
onDraw() //清除画布 this.clearCanvas() //绘制小鱼头 this.drawHead(this.canvas,fishAngle) //绘制左右小鱼鳍 this.drawFins(this.canvas, leftFinsPoint, this.FIND_FINS_LENGTH, fishAngle, false); this.drawFins(this.canvas, rightFinsPoint, this.FIND_FINS_LENGTH, fishAngle, true); //绘制节肢1 let middleCircleCenterPoint = this.drawSegment(this.canvas, bodyBottomCenterPoint, this.BIG_CIRCLE_RADIUS, this.MIDDLE_CIRCLE_RADIUS, this.FIND_MIDDLE_CIRCLE_LENGTH, fishAngle, true); //绘制节肢2 this.drawSegment(this.canvas, middleCircleCenterPoint, this.MIDDLE_CIRCLE_RADIUS, this.SMALL_CIRCLE_RADIUS, this.FIND_SMALL_CIRCLE_LENGTH, fishAngle, false); let findEdgeLength = Math.abs(Math.sin(MathUtils.toRadians(this.currentValue * 1.5)) * this.BIG_CIRCLE_RADIUS); // 绘制小鱼尾大小三角形 this.drawTriangle(this.canvas, middleCircleCenterPoint, this.FIND_TRIANGLE_LENGTH, findEdgeLength, fishAngle); let findEdgeLengthS = Math.abs(Math.sin(MathUtils.toRadians(this.currentValue * 1.5 - 90)) * this.BIG_CIRCLE_RADIUS); this.drawTriangle(this.canvas,middleCircleCenterPoint,this.FIND_TRIANGLE_LENGTH*0.8,findEdgeLengthS*0.8,fishAngle); // 绘制小鱼身 this.drawBody(this.canvas, this.headPoint, bodyBottomCenterPoint, fishAngle) ,

下面分步骤说明
清除画布每次绘制前我们都需要将画布擦除,不然会污染后续绘制
this.canvas.clearRect(0, 0, this.width(), this.height());

绘制小鱼头绘制小鱼头,需要小鱼头中心坐标,小鱼头半径。小鱼头中心坐标可由重心坐标middlePoint、距离、当前角度推算出来。
drawHead(canvas,fishAngle) canvas.fillStyle = rgba(244, 92,71,0.5) drawCircle(canvas, this.headPoint, this.HEAD_RADIUS) //绘制小鱼眼 this.drawEye(canvas,fishAngle) ,

注意画布坐标轴是原则在左上角,x轴向右,y轴向下
#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

绘制小鱼眼,小鱼眼由两个椭圆实现,使用canvas.ellipse()绘制
drawEye(canvas, fishAngle) canvas.fillStyle = rgba(0, 0,0,1) //左眼 canvas.beginPath(); let leftEye = MathUtils.calculatePoint(this.headPoint, this.HEAD_RADIUS * 0.7, fishAngle + 45) canvas.ellipse(leftEye.x, leftEye.y, this.HEAD_RADIUS * 0.12, this.HEAD_RADIUS * 0.07, -Math.PI * 0.3, Math.PI * 0, Math.PI * 2, 1 ); canvas.fill(); //右眼 canvas.beginPath() let rightEye = MathUtils.calculatePoint(this.headPoint, this.HEAD_RADIUS * 0.7, fishAngle - 45) canvas.ellipse(rightEye.x, rightEye.y, this.HEAD_RADIUS * 0.12, this.HEAD_RADIUS * 0.07, Math.PI * 0.3, Math.PI * 0, Math.PI * 2, 1 ); canvas.fill();

绘制小鱼鳍利用startPoint,controlPoint,endPoint绘制贝塞尔的封闭path
drawFins(canvas, startPoint, length, fishAngle, isRightFins) canvas.fillStyle = rgba(244, 92,71,0.5) let controlAngle = 115; // 结束点 let endPoint = MathUtils.calculatePoint(startPoint, length, fishAngle - 180); // 控制点 let controlPoint = MathUtils.calculatePoint(startPoint, 1.8 * length, isRightFins ? fishAngle - controlAngle : fishAngle + controlAngle ); drawPath(canvas, [M, startPoint, Q, controlPoint, endPoint]) ,

#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

绘制节肢节肢由圆、梯形构成,先绘制圆再讲梯形绘制上
/** * 画节肢 * * @param bottomCenterPoint梯形底部的中心点坐标(长边) * @param bigRadius大圆的半径 * @param smallRadius小圆的半径 * @param findSmallCircleLength 寻找梯形小圆的线长 * @param isBigCircle是否有大圆 */ drawSegment(canvas, bottomCenterPoint, bigRadius, smallRadius, findSmallCircleLength, fishAngle, isBigCircle) canvas.fillStyle = this.defaultFillStyle // 节肢摆动的角度 let segmentAngle = 0; // 节肢1 用 cos if (isBigCircle) segmentAngle = (fishAngle + Math.cos(MathUtils.toRadians(this.currentValue * 1.5)) * 15); else segmentAngle = (fishAngle + Math.sin(MathUtils.toRadians(this.currentValue * 1.5)) * 35); segmentAngle = fishAngle +this.currentValue // 梯形上底的中心点(短边) let upperCenterPoint = MathUtils.calculatePoint(bottomCenterPoint, findSmallCircleLength, segmentAngle - 180); // 梯形的四个顶点 let bottomLeftPoint = MathUtils.calculatePoint(bottomCenterPoint, bigRadius, segmentAngle + 90); let bottomRightPoint = MathUtils.calculatePoint(bottomCenterPoint, bigRadius, segmentAngle - 90); let upperLeftPoint = MathUtils.calculatePoint(upperCenterPoint, smallRadius, segmentAngle + 90); let upperRightPoint = MathUtils.calculatePoint(upperCenterPoint, smallRadius, segmentAngle - 90); if (isBigCircle) // 绘制大圆 drawCircle(canvas, bottomCenterPoint, bigRadius); // 绘制小圆 drawCircle(canvas, upperCenterPoint, smallRadius); // 绘制梯形 drawPath(canvas, [M, bottomLeftPoint, L, upperLeftPoint, L, upperRightPoint, L, bottomRightPoint]) return upperCenterPoint; ,

绘制小鱼尾大小三角形
drawTriangle(canvas, startPoint, findCenterLength, findEdgeLength, fishAngle) canvas.fillStyle = rgba(244, 92,71,0.5) // 三角形小鱼尾的摆动角度需要跟着节肢2走 let triangleAngle = (fishAngle + Math.sin(MathUtils.toRadians(this.currentValue * 1.5)) * 35); // 底部中心点的坐标 let centerPoint = MathUtils.calculatePoint(startPoint, findCenterLength, triangleAngle - 180); // 三角形底部两个点 let leftPoint = MathUtils.calculatePoint(centerPoint, findEdgeLength, triangleAngle + 90); let rightPoint = MathUtils.calculatePoint(centerPoint, findEdgeLength, triangleAngle - 90); // 绘制三角形 drawPath(canvas, [M, startPoint, L, leftPoint, L, rightPoint]) //console.log(`makeTriangle#startPoint:$JSON.stringify(startPoint),leftPoint:$JSON.stringify(leftPoint),rightPoint:$JSON.stringify(rightPoint)`) ,

绘制小鱼身首先得到小鱼身的四个顶点,在得到左右两边的两个控制点,根据这六个点绘制一个封闭的path
drawBody(canvas, headPoint, bodyBottomCenterPoint, fishAngle) this.canvas.globalAlpha = 160 / 255 //加深小鱼身 // 身体的四个点 let topLeftPoint = MathUtils.calculatePoint(headPoint, this.HEAD_RADIUS, fishAngle + 90); let topRightPoint = MathUtils.calculatePoint(headPoint, this.HEAD_RADIUS, fishAngle - 90); let bottomLeftPoint = MathUtils.calculatePoint(bodyBottomCenterPoint, this.BIG_CIRCLE_RADIUS, fishAngle + 90); let bottomRightPoint = MathUtils.calculatePoint(bodyBottomCenterPoint, this.BIG_CIRCLE_RADIUS, fishAngle - 90); // 二阶贝塞尔曲线的控制点 let controlLeft = MathUtils.calculatePoint(headPoint, this.BODY_LENGTH * 0.56, fishAngle + 130); let controlRight = MathUtils.calculatePoint(headPoint, this.BODY_LENGTH * 0.56, fishAngle - 130); // 画小鱼身 drawPath(canvas, [M, topLeftPoint, "Q", controlLeft, bottomLeftPoint, "L", bottomRightPoint, "Q", controlRight, topRightPoint]) this.canvas.globalAlpha = 100 / 255 ,

小鱼的摆动
摆动我们通过不断改变小鱼的旋转角度fishAngle来模拟小鱼的摆动,这里可以使用定时器,更为方便的可以使用Animator
startAnimation() if (this.animator == null) //fill: "none" | "forwards" | "backwards" | "both"; //direction: "normal" | "reverse" | "alternate" | "alternate-reverse"; var options = duration: 1* 1000, easing: ease, iterations: -1, direction: "alternate", fill: "none", begin: -5, end: 5 ; this.animator = animator.createAnimator(options)this.animator.play();

这里startAnimation方法中我们创建了一个Animator,并指定option,option中的参数说明如下
var options = duration: 1* 1000,//动画的时长为1s easing: ease,//动画的插值曲线,ease表示先加速在减速 iterations: -1,//动画的循环次数,默认是1,-1表示无线循环 direction: "alternate", //每次动画播放方向,alternate表示先正向播放在反向播放 fill: "none",//动画结束后是否动画状态设置,none表示设置为动画初始值 begin: -5,//动画插值起始值,这里表示小鱼的起始角度为-5° end: 5//动画插值结束值,这里表示小鱼的结束角度为5° ;

创建完动画我们通过指定onframe回调,currentValue是动画变化因子,currentValue影响小鱼角度。改完currentValue,我们调用onDraw()进行重绘制
this.animator.onframe = (value)=> this.currentValue = https://www.songbingjia.com/android/Number(value) this.onDraw() ;

onDraw() let fishAngle = this.fishMainAngle+this.currentValue //得到当前小鱼的角度 //清除画布 this.clearCanvas() //绘制小鱼头 this.drawHead(this.canvas) ....

效果如下:
#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

:warning:这里虽然实现了小鱼的摆动,但是感觉怪怪的。日常小鱼摆动时,小鱼尾,节肢的幅度和频率要高于小鱼身体,这样才能不能显得呆板。幅度我们在小鱼尾和节肢那里增加一个currentValue相关乘积来扩大振幅。
drawSegment(canvas, bottomCenterPoint, bigRadius, smallRadius, findSmallCircleLength, fishAngle, isBigCircle) canvas.fillStyle = this.defaultFillStyle // 节肢摆动的角度 let segmentAngle = 0; if (isBigCircle) segmentAngle = (fishAngle+this.currentValue*2); //节肢1增大2倍 else segmentAngle = (fishAngle+this.currentValue*3); //节肢1增大3倍drawTriangle(canvas, startPoint, findCenterLength, findEdgeLength, fishAngle) canvas.fillStyle = rgba(244, 92,71,0.5) // 三角形小鱼尾的摆动角度需要跟着节肢2走 let triangleAngle = fishAngle+this.currentValue*3;

#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

那如何实现动画更新时,小鱼各部位的频率不同呢,这里就涉及小鱼的变频
变频方案一:对于不同频率的部位分别创建一个Animator,每个Animator分别管理相同部位的频率并绘制。交互比较复杂
方案二:使用正弦函数的周期性。sin(nx)的频率是sin(x)的n倍,如果小鱼的角度公式与sin(nx)相关,那么我们对不同部位设置不同的n值来实现频率不一致,比如头的角度=sin(2x),尾的角度=sin(3x),那么尾的频率就是头的1.5倍。
onDraw() this.clearCanvas( let fishAngle = (this.fishMainAngle + Math.sin(MathUtils.toRadians(this.currentValue * 1.2)) * 4); ...drawSegment(canvas, bottomCenterPoint, bigRadius, smallRadius, findSmallCircleLength, fishAngle, isBigCircle) canvas.fillStyle = this.defaultFillStyle // 节肢摆动的角度 let segmentAngle = 0; if (isBigCircle) segmentAngle = (fishAngle + Math.cos(MathUtils.toRadians(this.currentValue * 1.5)) * 15); else segmentAngle = (fishAngle + Math.sin(MathUtils.toRadians(this.currentValue * 1.5)) * 35);

如上fishAngle的频率扩大了1.2倍,节肢的频率扩大了1.5倍。sin,cos的角度变化范围,也就是currentValue的end-begin,必须是360度的整数倍,这样才能保证sin,cos的周期性。
我们这里设置begin=0,如下求currentValue的end值
设m = end*1.2/360,n=end*1.5/360 m=end/(360/1.2),n=end/(360/1.5) m=end/300,n=end/240 end要整除300和240,则end是这两个数的最小公倍数 则end最小为1200

下面是变频的option
var options = duration: 5* 1000, easing: linear, iterations: -1, direction: "normal", fill: "none", begin: 0, end: 1200 //必须是1200的倍数,1.5/360与1.2/360的最小公倍数是1200

调用
< element src="https://www.songbingjia.com/fish/fish.hml"> < /element> < div class="container"> < fish id="fish"> < /fish> < /div>

总结通过本项目的演练,我们对自定义Canvas,JS动画,三角函数等有了更深的认识
这里只是实现了小鱼摆动和变频,后续有时间会接着增加点击屏幕实现小鱼的游动、游动时的转向、转向变频
源码地址 更多原创内容请关注:深开鸿技术团队入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
想了解更多关于鸿蒙的内容,请访问:
51CTO和华为官方合作共建的鸿蒙技术社区
https://harmonyos.51cto.com/#bkwz
::: hljs-center
#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤

文章图片

【#过年不停更#HarmonyOS自定义JS组件—灵动的锦鲤】:::

    推荐阅读