当我们在canvas上绘制图形的时候,有时候可能在某一步上画错,又或者对图像的位置进行反复的移动调整,觉得这个位置不合适 要返回上一个位置,这种情况下用户不可能删除当前这个图形重画,用户肯定是希望能撤销当前的步骤回到上一个步骤就可以,这就是所谓的撤销,那么说起来简单,但我们又应该如何去实现这样的功能呢?
要做某一件事之前我们往往需要有一个可行性的思路,即可实施的方案。
图形的绘制
首先我们来想想,都知道canvas画布的原点是做左上角的,当用户画一个图形时会有哪些步骤, 一般会分为3步:
第一步:开始绘制,就是确定开始绘制的位置,从哪里开始画这个图形,即确定起始坐标。
第二步:绘制中,就是绘制过程中根据用户的需要不断的变换图形的大小、结束坐标。
第三步:绘制完成,这一步简而言之就是确定这个图形的结束坐标在哪里。
图1由此可见,所以第一步 确定开始坐标就是图中的 [x1,y1] , 第二步就是图中的 [ [x2,y2], [x3,y3], [x4,y4] ],第三步就是确定结束位置毫无疑问便是图中的 [xn,yn] 的位置。以上就是绘制图形的过程。
图形的移动拖拽
上面我们介绍了图形绘制的步骤分为3步,那么移动又该分为几步呢,其实移动也是确定坐标的过程,显而易见也是分为3步,不过这3步是一个实时计算的过程,不像绘制它是直接获取鼠标移动的指针坐标的过程,不会设计到计算。那么说到移动是一个坐标实时计算的来确定的过程又是如何理解呢。
当我们鼠标点击在画布上确定鼠标指针开始从哪个位置拖拽,然后进行拖拽,最后在哪个位置停止。这个过程中从开始到结束图形的左上角和右下角这两对参数是实时变换的,那么这个变换是如何得来的呢。 接下来我们看一张图:
假如这两个色块是同一张图,只是位置变换了而已,当用户指针落在c1的位置上时并进行拖拽鼠标移动到了c2这个位置,那么图形原来的位置 a1和a2也会相对移动到了b1和b2的位置,那么问题来了,这个偏移量的计算是如何得来的呢。其实就是鼠标偏移量加上鼠标当前位置减去落下的位置,从而又要及时变换鼠标的落下位置,就是当前位置赋值给鼠标落下的位置。
假如axis是我们要求的偏移量,那么axis的横纵坐标等于:
[ axis.x, axis.y ] = [ axis.x + ( c2.x - c1.x ), axis.y + ( c2.y - c1.y ) ];
[ c1.x, c1.y ] = [ c2.x, c2.y ];
这么两步加减法运算就完成我们拖拽的功能,有人问说了这么多还没有切人正题,如何实现撤销功能。别急,下面马上给你一一道来。
撤销与恢复的实现思路
上面我们知道了画图的时候往往会需要对所有图形不断的操作变换的过程,那么如何对不满意的步骤进行撤销呢,其实就是我们每绘制完一个图形的时候就把这个图形的任何参数都记录保存起来添加到一个数组 arr 中。比如上面在我们说了在拖拽图形的时候有3个过程,鼠标落下、鼠标移动、鼠标停止这三步,当鼠标落下的时候其实只要做一步动作,那就是确定我们鼠标点击的这个位置有没有图形,也就是选择哪个图形,当我们鼠标落下选中图形的时候,那么鼠标拖拽的过程中会不停的清空画布重绘图形,这个过程中我们并不需要做任何的记录,尽管清空、绘制不断重复这两步动作就可以,重要一点就是当我们鼠标弹起松开结束绘制的时候就把当前绘制的这个图形最终状态添加到上面的数组 arr 记录栈中就可以,这样就是每一步绘制完成都把当前这个图形存起来。简单的理解如下图:
arr 是一个历史记录栈,记录了所有图形的每一次绘制操作,只要它移动位置、缩放比例、旋转角度、删除等任何参数的变化并重绘了就记录一次栈中的变动情况。有一个特殊情况就是当创建、编辑、移动、缩放、旋转操作就使用push方式把图形最终状态压入栈中来,只有涉及到撤销出栈的操作时就需要使用 pop() 方式删除记录栈中最后一个状态,并把删除的这个状态放到deleteArr回收栈中。重绘分两种情况:(1). 同时需要去判断被删除的这个状态在arr记录栈中是否还存在相同的图形 id,如果有的话就把最近一个相同id 的状态取出并使用 splice() 方法替换渲染 map 集合对应 id 的图形状态,(2). 如何记录栈中没有和被删除的记录相同id的状态记录了,那么就到渲染集合map中找到相同id的图形使用splice()方法删除掉图形并重绘即可。
那当用户又想恢复被撤销的那一步记录了怎么办,好办,刚才从arr删除的记录不是放到了回收站deleteArr中了吗,又把它从回收站中拿出来添加到记录栈arr的末尾并(并回复map上一步最后状态)重绘画布即可。
下面展示历史记录栈 arr、渲染集合 map、回收站deleteArr三者之间的撤销图解关系如下:
来吧,展示:
/**
* 撤销源码实现逻辑
* arr 历史记录栈
* deleteArr 回收站
* map 渲染集合
* del 当前被撤销的状态记录
* ars 被撤销这个状态在历史记录栈中是否还有这个图形的状态,是一个数组
**/
//1 先从记录栈中删除最后一个记录状态
let del = arr.pop();
//2 把删除的状态存到回收站中
deleteArr.push(del);
//3 通过id查找记录栈中是否还要同一个图形的记录
let ars = arr.map( item => item.id === del.id);
//4 通过id找到被撤销的这个记录状态在渲染map集合中的位置
let index = map.findIndex( ite => ite.id == del.id );
//5 判断,如果被撤销的这个记录在记录栈中是否还有记录
if(ars.length > 0){
//5.1 如果还有记录状态的话就把最后一个记录更新成渲染map集合的当前状态
map.splice(index, 1, ars[ars.length - 1])
}else{
//5.2 如果没有记录的话就直接把被撤销的这个状态在渲染集合中对应的图形删掉
map.splice(index, 1);
}
/**以下就可以重绘画布**/
下面是图形绘制或者恢复撤销两种情形操作画布的图解关系分析:
map集合中存放的每一个元素(图形),其实记录的是画布上每一个图形,每个图形有两个属性,一个是id 代表每个图形的唯一标识,一个是图形的最终状态。当我们需要恢复被撤销的状态的时候,按照上图的步骤实现。
来吧,展示:
/**
* 撤销源码实现逻辑
* arr 历史记录栈
* deleteArr 回收站
* map 渲染集合
* obj 当前要恢复或绘制的图形状态
**/
//1. 从回收站中取出最后一个状态
let obj = deleteArr.pop();
//2. 把恢复的状态重新加入到历史记录栈中
arr.push(obj);
/**
*3. 通过被恢复状态的id到渲染map集合查找这个图形存不存在,分两种情况:
* 3.1 如果渲染集合中没有这个图形了,上一步撤销是最终撤销了
* 那么现在就要重新加进来。
* 3.2 如果渲染集合中还有这个图形那么就直接替换为恢复的状态
* 为最终状态即可。
**/
let index = map.findIndex( item => item.id === obj.id );
index > -1 ? map.splice( index, 1, obj ) : map.splice( map.length, 1, obj );
/** 以下就可以重绘画布了 **/
这样就恢复了上一步撤销的状态了,这样思路看官们GET到了没,是不是就清晰很多了呢。如何能给你带来帮助就帮忙点个心心支持一下吧。
如果觉得对你有启发有用的话点个赞再走吧。