年前维护公司项目的时候完成了两个与canvas有关的两个功能,其中一个功能的实现对个人能力的提升有很大的帮助,所以记录一下。

首先说一下需求

背景及需求:在canvas里绘制了一张图片,而这个canvas上有旋转的功能,在点击旋转按钮画布旋转以后,画布上原有标记被抹除,出于某种使用者的需要,用户需要开发人员实现画布旋转后保留原标记且可继续绘制。

实现过程

这个需求的实现很复杂。先梳理需求:1、画布旋转以后将原画布上的内容保留在画布上,且为了页面显示的美观改变原画布尺寸,保留原缩放比;2、继续在画布上做标记。

首先,我想到的可以实现这个需求有两个方法:在画布点击旋转按钮,缩放完成后,将画布内容导出图片,然后将生成的图片绘制在画布上;另一种就是把整个canvas元素旋转过来,重新计算后续绘制时的坐标及缩放比。第一种方法可以很方便的实现这个功能,但是有一个致命问题:画布里的内容随着旋转会变得越来越模糊。问了我们公司的大佬,说应该是canvas在绘制图片和导出图片时的算法不够完美,在执行这些操作的时候导致像素丢失。没办法就只能使用第二个方法(第一个具体实现就不再说明,而且不建议使用该方法)。

第二个方式实现起来比较复杂,所以我要把每一步都分开:

  1. 初始化画布:首先在init函数里把图片等比缩放到宽度固定的画布上,这个是常规操作,一个简单的数学问题,不想说,只说一点:由于一个页面里的需要绘制的图片可能有多张,所以我在for循环对图片的load事件处理,在每单张图片加载完成时将其绘制在画布上(本文只讲有一张图的情况),其实这样操作是有问题的,稍后再讲。定义一个变量canvasDeg用于记录每个画布旋转的角度,初始值为0,每点击一次旋转按钮canvasDeg值+1,大于等于4时为0。
  2. 画布未旋转时操作。这里我认为要分为四种情况:头朝上、下、左、右(也可能可以分成2种情况:头朝上下、头朝左右,由于页面需求过于复杂就没有再抽出进一步的规则)。首先是没旋转即canvasDeg为0时,这时画布的操作为常规操作,不再过多叙述。
  3. 画布旋转后的展示情况。在点击旋转按钮将画布旋转到头朝左时,为了让原画布能很好的展示在页面里,所以要计算下这个画布的缩放比,记为proportion(此值后续操作会用到),然后使用css3的transform属性将画布进行移动(将旋转、缩放后的画布重新移动到原位置)、旋转、缩放操作。
  4. 这一步是核心。找出鼠标按下时鼠标在画布中的位置与绘制标记时的位置关系。画布的旋转导致了画布坐标系内,坐标轴的改变。 也就是说,此时鼠标在页面canvas元素中的位置与在画布中的横纵坐标不再是同一个值,需要将鼠标在画布元素内的位置信息转化为画布这个坐标系的位置信息 ,由于画布旋转90度,原来的横轴变为纵轴,原来的纵轴变为横轴,所以坐标点的横纵坐标数值交换,而且由于我在这设置的原点为画布左上角那个点,所以准确来说,现鼠标在canvas元素内的位置:距左侧的像素值、距上侧的像素值,在画布中的坐标值为:画布width属性值 - 距上侧的像素值,距左侧的像素值。鼠标移动时的位置信息计算公式和点击时一样。
  5. 点击旋转按钮后重复步骤3、步骤4。

然后填上面挖的坑。在经过一系列的计算终于实现了这个功能并提交提测后,测试人员发现了一个很奇怪的bug,在有些画布里鼠标的实际位置和画出来的位置不一致。经过我仔细排查发现是在有多个画布的情况下,下面的图片在上面图片未完全加载完毕的情况下就已加载完毕,且计算好该图片对应画布的位置信息,而在上面的图片加载完毕后,这个画布就被挤到下面,与之前保存的位置信息不一致,所以画笔就错位了。归根到底还是图片加载时的异步问题。解决办法:在init的for循环里新建一个promise对象将pic.onload事件包起来,在for循环后使用promise.all方法处理for循环里创建promise对象,及当所有的图片都加载完毕后在计算每个画布的位置信息。

总结

本文只提供一个解决此类问题的一个方法及实现思路,demo不再展示(没有整理)。原需求其实更复杂,还涉及旋转后清除原画布上的标记,且在这个基础上继续绘制,而且这个画布还适配屏幕缩放比,这些就不在过多描述。这个需求主要涉及canvas的操作,虽然用的技术较单一,但是这个功能的实现本质上是用代码将一个数学问题描述了出来,与平常的业务代码有很大的区别,而且在图片加载完以后才能绘制这个问题点加深了我对异步的理解,另外也是我第一次在业务代码里使用promise对象处理异步问题。这个需求是改自之前代码,算是在调试的过程中完成的,前段时间从头编写了个canvas的小程序,功能也是较复杂,也是使用了边调试边开发的方式,但是在开发的工程中总是遇见需要新加变量或合并变量的问题,为了代码精简就一遍遍修改整个程序的变量,这么做很容易打断当前代码的思路,而且容易出粗,以后在实现这类逻辑复杂的功能时,建议在一开始就有个较具体的规划,尽量不要边做边优化。