本文最后更新于 2 个月前
接上一篇文章。
轨迹自动播放实现
在画布上绘制笔迹的时候,通过touchmove
事件将每一次绘制的点坐标都记录起来,同时也要将当前笔迹的颜色、宽度等数据记录起来。最终画布上所有的笔迹都转换成了一个数据列表,当播放的时候,再将列表里面的数据一条一条的在canvas
上重新绘制出来,通过setTimeout
进行自动循环不断的去绘制。
看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| class Paint {
playPath(path: Path[], completed?: () => void) { if (!path.length) return Promise.resolve();
this.path = path;
this.row = 0; this.column = 0; this.stop = false; this.isComplete = false;
const { pos, color, width } = path[0]; this.start(pos[0], color, width);
this.run(completed); if (completed) { this.completed = completed; } }
private completed() {}
private run(completed = this.completed) { if (this.stop) { return; }
if (this.column < this.path[this.row].pos.length) { this.drawLine(this.path[this.row].pos[this.column++]); setTimeout(() => this.run(), 16.7); } else { if (++this.row < this.path.length) { this.column = 0; const { pos, color, width } = this.path[this.row]; this.start(pos[0], color, width);
setTimeout(() => { setTimeout(() => this.run(), 16.7); }, 240); } else { this.isComplete = true; completed(); } } }
pause() { this.stop = true; }
play(completed?: () => void) { this.stop = false; this.run(completed); } }
JS
|
有些命名可能有点不规范,逻辑也有一点复杂。代码中path
数据列表保存的是每一次绘制的笔迹,每一条笔迹又包含了一组坐标点、颜色和宽度,类型定义如下:
1 2 3 4 5 6 7 8 9 10
| export interface Dot { x: number; y: number; }
export interface Path { pos: Dot[]; color?: string; width?: number; }
TS
|
最终绘制的时候都是在处理点坐标,就相当于是在处理一个二维数组,所有就通过row
、column
变量来进行区分。
撤销操作
主要思路就是:每在画布上进行一次绘制,在结束的时候touchend
就将当前画布上的数据保存起来,通过ctx.getImageData
api可以保存当前画布,然后放在一个列表中。同时再用一个变量作为指针来记录当前所在列表的位置,没点击一次撤销操作,指针就在列表中往前移动一步,同理点击恢复撤销时,指针往后移动一步。需要注意的一个点就是,当进行n次撤销操作后,指针不在列表最后的位置时,再进行绘制,此时应该清空列表中指针所在位置后面的数据,并保存当前画布数据。因为每次在画布上的操作,都应该是列表中的最后一条数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const handleTouchEnd = () => { if (!painting) return; painting = false;
commit(TypeKeys.OPERATION_ADD);
const path = state.path.slice(0, state.currentPathIndex); commit(TypeKeys.SET_PATH, path.concat(currentLine));
const list = state.historyStepList.slice(0, state.currentStepIndex >= MAX_HISTORY_COUNT - 1 ? state.currentStepIndex + 1 : state.currentStepIndex); commit(TypeKeys.SET_HISTORY_STEP_LIST, list.concat(paint.value?.getImageData())); };
TS
|
这里主要的难点就是对指针的控制,尤其是在列表的临界点时的处理,因为我这里还需要控制路径的列表,所有更加复杂一些,这一块差不多花了我一天的时间,处理不好在撤销的时候就很容易出bug。
由于每次保存的画布数据是比较大的,必须对历史记录列表做一个最大长度的限制,否则就会撑爆内存,小程序闪退。getImageData
获取的画布数据大小取决于画布的width*height
的大小,由于我这个小程序是全屏的,所以在大屏幕上的数据会非常的大,我目前限制了最大历史记录为10,后期考虑固定画布的大小。
不同大小的画布还会存在一个问题,就是在小屏幕手机上,可能会展示不全大屏幕手机分享的内容。
最后
再放出小程序码,欢迎大家进行体验。
如果觉得不错,可以关注我的公众号【末日码农】,我会将开发中遇到的实际问题和一些好的技术知识分享给大家。