canvas模拟实现高德地图的部分功能

背景

星座礼物配套的玩法活动,根据当前时间开放对应的星座馆,送出相应的星座礼物有概率为当前星座上的星星命名,依据不同的礼物价值命名不同等级的星星,每个星座星星数量固定点完即止。

选型原因

拆解一下需求:
1、共有12个星座,且每个星座的星星要依据星座的对应形状排列。
2、为了提高参与用户数量,每个星座的星星数量较多,一屏展示不下,用户不仅需要查看整个星座的点亮情况,同时也好奇某颗星星的点亮详情,所以会涉及到频繁的缩放(为了用户的参与体验要支持任意倍数平滑的缩放)和滑动

综上我们选用性能优可控性高的canvas来实现上述功能。

基本概念:

在coding之前我们首先需要了解几个基本概念:画布、画布原点、笔触点、视窗
在这里插入图片描述

结合上图笑脸所示,
画布:canvas提供的绘制位置,即笑脸所在的白色正方形是一块提前声明好的画布
画布原点:画布左上角的位置即(0,0)点,向➡️为X轴正方向,向⬇️为Y轴正方向
笔触点:画笔绘制的起始位置; ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // 表示把笔触点移动到(75,75)的位置以50为半径画一个0到360度闭合的圆。笔触点相对于画布原点定位的,可以为负值,不在画布上的绘制则不显示。
视窗:物理设备(手机屏幕或电脑屏幕)的左上角

为何要区分画布和视窗这两个概念?
画布的大小等于视窗的大小,但我们所有的操作都在手机屏幕上进行,即我们可以感知到的操作点都是视窗上的坐标点;但笔触所有的坐标点对应的都是画布的原点,所以需要将两者区别对待。

了解完了基本概念下面我将依照:绘制---->拖拽---->缩放---->优化这个顺序描述关键的实现步骤。

绘制

如下图所示,我声明了一张同视窗尺寸一致的画布,将笔触原点移动至画布(300,100),通过drawImage方法将提前加载的图片设定好宽高后绘制出来,值得一提的是通过drawImage方法实现给图片加水印和前端压缩上传图片都是不错的选择!
在这里插入图片描述

拖拽:

拖动图片向左移动1像素的过程:

向左拖拽1px
笔触原点X坐标-1
清空整个画布
重新绘制并渲染
视觉效果向左移动1px

开发过程中修正节流参数的截图(未清空画布,未处理边界问题)

请添加图片描述

缩放

1-双指缩放绘制过程:
缩放
关键点
1、双指缩放使用ontouchmove判断并非ontouchstart;
说明:使用ontouchstart判断是否为双指,是通过接触点的数量是否为2确定的。但实际情况中我们很难两只手指在同一时刻接触到手机屏幕,所以我们改为判定在某一时刻中屏幕上同时有两个接触点即认为触发了双指缩放。
2、判断是缩还是放,并计算缩放中心点
3、确定一个适当的倍数差值后进行计算并移动笔触

计算过程:
放大1.5倍实现步骤

感知到屏幕上两个触点的位置
确定缩放中心点坐标
确定放大1.5倍后对应缩放中心点的坐标
计算缩放前后中心点坐标的偏移量
清空整个画布
重新放大1.5倍后绘制并渲染
视觉效果放大1.5倍

1)获取到屏幕两个接触点的坐标后,计算缩放中心点和两点距离。
举例:触点1坐标(a,b),触点2坐标(c,d),
缩放中心点坐标(x1,x2):
x 1 = ( a + c ) / 2 x1= (a+c)/2x1=(a+c)/2x 2 = ( b + d ) / 2 x2= (b+d)/2x2=(b+d)/2

两点距离:d 2 = ( a − c ) 2 + ( b − d ) 2 d^2= (a-c)^2+(b-d)^2d2=(ac)2+(bd)2

2)通过d的实时变化进行缩放,一开始的距离/现在的距离=应该缩放的比例,

3)将视窗的缩放中心点对应到画布上,计算缩放后画布的偏移距离,将笔触原点反向移动相应的偏移距离后,再次将画布的缩放中心点和视窗的缩放中心点对应上,即可实现按照中心点缩放
简单理解:
在这里插入图片描述
如上图所示,A(x,y)点是缩放中心,放大n倍后对应的点跑到了A1(x1,y1),要想保证缩放的中心点看起来位置不动,我们需要将A1点移动到A点,即偏移(x1-x,y1-y)的距离

实际实现:
实际中我们需要把视窗考虑进去,因为我们能获取到的缩放中心点都是相对于手机屏幕来说的,我们需要把这个位置对应到画布上(当前缩放倍数下的单位距离和当前视窗的单位距离不一致),但画布的缩放移动又完全不影响视窗,所以我们需要维护两套坐标系。最终需要确定的偏移量为:每次缩放之后的图像相对于初始大小下初始位置的偏移值
具体代码:

// 先缩放再位移
			const translateX =
				((((x - this.lastTransform.x * this.lastTransform.scale) / this.lastTransform.scale) * scale -
					x) /
					scale) *
				-1;
			const translateY =
				((((y - this.lastTransform.y * this.lastTransform.scale) / this.lastTransform.scale) * scale -
					y) /
					scale) *
				-1;

解释说明:
(本次屏幕缩放中心点-之前的偏移值*之前的倍数)/倍数=相对于初始大小下的具体坐标

(初始大小下的坐标*新要缩放的倍数-屏幕缩放中心)/新要缩放的倍数=新要偏移的值(相对于最初始的位置)

4)进行绘制

	ctx.scale(this.newTransform.scale, this.newTransform.scale);
	ctx.translate(this.newTransform.x, this.newTransform.y);
	ctx.drawImage(this.image, 0, 0);

注意要先缩放后移动,canvas缩放的原理是把整个坐标轴的距离拉长,但坐标不变,这也是为什么我们最终每次要求的偏移量是相对初始大小来说的

5)处理边界情况
设定一个最小的缩放值和一个最大的缩放值,以及校验偏移量。确定画布永远把视窗包含在其中(页面中不会留白)

2-星星的展示:
1)反缩放
因为我们整个星图使用的是canvas,所以星星的反缩放就比较容易实现,在绘制下一帧之前我们已经拿到了画布的缩放比例,只需要告诉绘制星星的这个笔触进行反比例的放大和缩小就行了。

2)隐藏
因为星星不会跟随星空缩放,防止视觉区域内星星过于密集,星图要求在较小的倍率下隐藏一部分较小的星星。处理方式:在缩放到临界倍数的下一帧时,读取配置文件中的设计同学精心计算好要隐藏的星座坐标,控制相应坐标的笔触忽略本次星星的绘制。只是不展示了,但他依然真实在星空上存在。

3)点击
不同于传统的dom可以直接在相应的元素上绑定事件,canvas每一帧绘制完成的结果本质上是张图片。所以我们获取到屏幕上的点击位置后,也要将其转换为画布上的坐标(又用到了我们之前维护的画布和视窗相转换的坐标系)。获取到画布上的坐标后,遍历所有星星的坐标和其附近相应的热区,匹配上之后即认为我们点击的是这颗星星。同时我们也能处理即使是当前缩放倍率下未展示的星星,我们依然可以定位到他的位置。

部分优化细节

1)渲染频率优化
在拖拽和缩放实现过程中,我们描述的只是其中一帧的绘制过程,比如放大1.5倍实际的实现不可能从1直接到1.5,因为这样会显的很突兀,和用户的交互非常不友好。真实情况下可能是1->1.01 甚至1.001,当然每次的变更值越小,展示出来的效果越流畅。但每次的变化都要经历确定缩放中心,计算偏移量…等一系列过程,对手机的cpu和gpu性能是一大考验尤其是低端机型会明显感觉到卡顿。所以我们既要保证缩放效果的流畅,又要尽可能的降低计算频率,我采用对通过requestAnimationFrame控制缩放的触发频率和画面的刷新频率保持一致(60HZ,部分手机可能不一致)来平衡这两点。

2)星星的动效优化
因为星星的数量较多且动效全部采用svga资源进行绘制,为了节省开销,相同等级之间的星星均采用同一svga资源,并保持相同的动画频率。

最终效果:
请添加图片描述

以上是实现的大致过程,因为想解释的尽量能让我这样的菜狗看懂所以有些啰嗦,还望各位大佬多担待。如果有类似开发需求的同学欢迎一起探讨和交流。

在这里插入图片描述


版权声明:本文为weixin_44198018原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。