接下来,我们继续完善功能。作为一个手写应用,undo/redo操作是必须的。要现实undo/redo,最容易想到的实现方式,就是我们记住每一次操作的结果,在用户undo的时候,显示之前的结果给用户就可以了。但是这样有一个很大的问题,就是我们的canvas,实际上一张图片。要记住结果,就需要记住这张图片的内容。而图片本身的数据量是很大的。因此我们没有办法记住很多次结果,也就是限制了undo的次数。 还有一种办法,就是我们记住用户的操作,例如我们记住第一次用户用铅笔,画了一条线,这条线上面每一个点的坐标是什么,我们都给记下来。在用户undo的时候,我们可以清空画板,然后从头到尾再画一次所有的操作,这样就可以实现undo功能了。由于用户操作相对的数据,相对于图片本身的数据来说,要小很多,因此,我们几乎可以无限制的undo/redo了。 要记住用户操作,那么在鼠标操作的时候,我们就需要记住每一个鼠标的位置,然后加入到数组里面。在用户鼠标松开的时候,我们再把整个路径的坐标,以及笔画粗细,颜色,作为一个完整的操作,添加到用户数组里面。 下面是完整的代码

<html><head><meta charset="utf-8"></head><body style='background:lightgrey'><div><button id='pencil' onclick="handleChoosePencil()">pencil</button><button id='highlighter' onclick="handleChooseHighlighter()">highlighter</button><button id='eraser' onclick="handleChooseEraser()">eraser</button><button id='undo' onclick="handleUndo()">undo</button><button id='redo' onclick="handleRedo()">redo</button></div><canvas id='pad' width='800px' height='600px' style='background:white'></canvas></body><script>//let actions = [];let points = [];let undoCursor = -1;//const pad = document.getElementById('pad');const ctx = pad.getContext('2d');ctx.lineWidth = 2;ctx.strokeStyle = 'blue';//updateButtonStatus();////pad.addEventListener('mousedown', handleMouseDown);//function handleMouseDown(event) {//if (undoCursor != -1) {actions = actions.slice(0, undoCursor);}undoCursor = -1;//ctx.beginPath();ctx.moveTo(event.offsetX, event.offsetY);//pad.addEventListener('mousemove', handleMouseMove);pad.addEventListener('mouseup', handleMouseUp);//points.push({x: event.offsetX, y: event.offsetY});      }//function handleMouseMove(event) {ctx.lineTo(event.offsetX, event.offsetY);ctx.stroke();ctx.beginPath();ctx.moveTo(event.offsetX, event.offsetY);points.push({x: event.offsetX, y: event.offsetY});      }//function handleMouseUp(event) {pad.removeEventListener('mousemove', handleMouseMove);pad.removeEventListener('mouseup', handleMouseUp);//actions.push({lineWidth: ctx.lineWidth,strokeStyle: ctx.strokeStyle,points,});//points = [];updateButtonStatus();}//function handleChoosePencil(event) {ctx.strokeStyle = 'rgb(0, 0, 255)';ctx.lineWidth = 2;}//function handleChooseHighlighter(event) {ctx.strokeStyle = 'rgba(255, 255, 0, 0.5)';ctx.lineWidth = 8;}//function handleChooseEraser(event) {ctx.strokeStyle = 'white';ctx.lineWidth = 8;}//function canUndo() {if (actions.length == 0) {return false;}//if (undoCursor == 0) {return false;}//return true;}//function canRedo() {//if (actions.length == 0) {return false;}//if (undoCursor == -1 || undoCursor == actions.length) {return false;}//return true;}//function handleUndo(event) {if (!canUndo()) {return;}//if (undoCursor == -1) {undoCursor = actions.length;}//undoCursor--;//repaint();//updateButtonStatus();}//function handleRedo(event) {if (!canRedo()) {return;}//undoCursor++;//repaint();//updateButtonStatus();}//function updateButtonStatus() {const undoButton = document.getElementById('undo');const redoButton = document.getElementById('redo');undoButton.disabled = !canUndo();redoButton.disabled = !canRedo();}//function repaint() {ctx.clearRect(0, 0, pad.width, pad.height);//let toIndex = undoCursor == -1 ? actions.length : undoCursor;for (let i = 0; i < toIndex; i++) {//let action = actions[i];//ctx.beginPath();ctx.lineWidth = action.lineWidth;ctx.strokeStyle = action.strokeStyle;//let points = action.points;if (points.length == 0) {continue;}//let firstPoint = points[0];ctx.moveTo(firstPoint.x, firstPoint.y);for (let j = 1; j < points.length; j++) {const point = points[j];ctx.lineTo(point.x, point.y);}ctx.stroke();//}}</script>
</html>
复制代码

记录用户操作:

首先,我们定义了一个points数组,用来在鼠标按下以及移动的时候,记录完整的鼠标坐标。 我们还定义了一个actions数组,这个就是用来存放用户每一个操作的数组。在用户鼠标松开的时候,我们会把当前操作添加到这个actions数组后面:

    function handleMouseUp(event) {pad.removeEventListener('mousemove', handleMouseMove);pad.removeEventListener('mouseup', handleMouseUp);//actions.push({lineWidth: ctx.lineWidth,strokeStyle: ctx.strokeStyle,points,});//points = [];updateButtonStatus();}
复制代码

我们在这里记录了当前路径所有的坐标(points),当前路径的宽度,以及颜色。把他们组合成一个对象添加到actions数组里面。在添加完之后,我们还需要记住,要把points数组重置,准备记录下一个操作的坐标。

执行undo

在用户进行undo的时候,最容易想到的办法,就是用户每执行一次undo,我们就把actions数组中最后一个元素删除,然后重新绘制actions里面的所有元素。但是这样以来,我们就没办法实现redo操作了。因此,在用户undo的时候,我们可以通过一个游标(actions数组下标),记录当前用户undo到哪一步了。当用户undo的时候,游标向数组头部移动。当用户redo的时候,游标向数组尾部移动。 首先,我们需要定义一个undoCursor,并把他的初始值设置成-1。之所以设置成-1,是因为数组的下标永远应该是大于等于0的。那么如果是-1,表示当前游标在数组最后面,用户没有任何undo操作。 下面的代码,可以判断当前是否允许undo/redo

    function canUndo() {if (actions.length == 0) {return false;}//if (undoCursor == 0) {return false;}//return true;}function canRedo() {//if (actions.length == 0) {return false;}//if (undoCursor == -1 || undoCursor == actions.length) {return false;}//return true;}
复制代码

当用户undo/redo的时候,我们去移动游标:

    function handleUndo(event) {if (!canUndo()) {return;}//if (undoCursor == -1) {undoCursor = actions.length;}//undoCursor--;//repaint();//updateButtonStatus();}//function handleRedo(event) {if (!canRedo()) {return;}//undoCursor++;//repaint();//updateButtonStatus();}
复制代码

在移动完游标后,我们还需要进行重绘。 重绘很简单,就是清空canvas,然后从头到尾重新绘制路径即可:

    function repaint() {ctx.clearRect(0, 0, pad.width, pad.height);//let toIndex = undoCursor == -1 ? actions.length : undoCursor;for (let i = 0; i < toIndex; i++) {//let action = actions[i];//ctx.beginPath();ctx.lineWidth = action.lineWidth;ctx.strokeStyle = action.strokeStyle;//let points = action.points;if (points.length == 0) {continue;}//let firstPoint = points[0];ctx.moveTo(firstPoint.x, firstPoint.y);for (let j = 1; j < points.length; j++) {const point = points[j];ctx.lineTo(point.x, point.y);}ctx.stroke();//}}
复制代码

当用户undo/redo之后,继续进行新的绘画的时候,我们还需要删除游标后面的数据,并且重置游标的位置:

      if (undoCursor != -1) {actions = actions.slice(0, undoCursor);}undoCursor = -1;
复制代码

最后,我们还需要在添加新的undo/redo按钮,并响应按钮消息,进行undo/redo处理。同时,在合适的时机,我们还需要更新undo/redo按钮的状态,以便告诉用户,什么时候可以undo/redo。

转载于:https://juejin.im/post/5cc2e60f6fb9a0321b6970c4

javascript全栈开发实践-web-4相关推荐

  1. javascript全栈开发实践-准备

    目标: 我们将会通过一些列教程,在只使用JavaScript开发的情况下,实现一个手写笔记应用.该应用具有以下特点: 全平台,有手机客户端(Android/iOS),Windows,macOS,Lin ...

  2. javascript全栈开发实践-web-7

    我们在重构代码之后,重新添加新的功能:添加一个圆形/椭圆的工具. 第一步,增加一个新的按钮: <button id='ellipse' onclick="handleDrawEllip ...

  3. python sanic orm_Sanic + 前端MVVM 一种新一代Python高性能全栈开发实践

    SanicCRUD-vue Sanic + 前端MVVM 一种新一代Python高性能全栈开发实践 背景 本项目将使用Sanic + Vue2 + Webpack2 配合最简单CRUD的逻辑来展示一个 ...

  4. 全栈开发和web开发_全栈开发人员:这是什么,以及如何成为一个完整的开发人员...

    全栈开发和web开发 一个全职的开发人员是各行各业的杰作,也是一个备受追捧的求职者. 标题暗示着知识的广度,这对于人手不足的初创公司和管理复杂应用程序的大公司而言都是无价的. 但是, 术语" ...

  5. python sanic_Sanic + 前端MVVM 一种新一代Python高性能全栈开发实践

    SanicCRUD-vue Sanic + 前端MVVM 一种新一代Python高性能全栈开发实践 背景 本项目将使用Sanic + Vue2 + Webpack2 配合最简单CRUD的逻辑来展示一个 ...

  6. 全栈开发和web开发_成为全栈开发人员

    全栈开发和web开发 This popular article has been updated in June 2017 to include modern technologies. 该热门文章已 ...

  7. python全栈开发实践入门_Python全栈开发实践入门

    Python全栈开发实践入门 编辑 锁定 讨论 上传视频 <Python全栈开发实践入门>是2017年10月电子工业出版社出版的图书,作者是谢瑛俊. 书 名 Python全栈开发实践入门 ...

  8. 预告:Javascript全栈开发的系列文章

    自从一年前发布了Vuejs小书的电子书,也有些日子没有碰过它们了,现在因为项目的缘故,需要使用JavaScript全栈开发.所以,我得把这个全栈环境搭建起来. 说起来搭建JS全栈开发环境,设计到的东西 ...

  9. python全栈开发实践入门_讲书3分钟丨《Python全栈开发实践入门》 -讲书人 谢瑛俊...

    只需3分钟就能快速了解一本书! 由作(译)者发声讲书,直指新书的特点与主旨. 只需利用碎片化时间,省时省力选到适合自己的好书 音频链接 音频内容 大家好,我是<Python全栈开发实践入门> ...

最新文章

  1. 深度学习新星:GANs的基本原理、应用和走向
  2. 为什么边缘概率密度是联合概率密度的积分_德甲前瞻|柏林赫塔VS柏林联合
  3. 直流电源端口雷击或瞬态浪涌防护设计方案图详解
  4. java中白盒测试用例_基于JAVA开发的中国象棋游戏的开发与研究白盒测试用例.doc...
  5. jsp工程防止外部注入_XPATH注入详解|OWASP Top 10安全风险实践(五)
  6. Flask使用bootstrap为HttpServer添加上传文件功能
  7. 程序员疯狂记事:如何利用众多技术栈构建一个 Web 应用程序?!
  8. 微信封杀下的互联网江湖
  9. [编程题]手机屏幕解锁模式
  10. 一种低侵入性的组件化方案 之 组件化需要考虑的几个问题
  11. 283、移动零(python)
  12. 知道是骗子 !好要撞上去!别太得瑟,没啥么好
  13. 《硅谷之谜》读书笔记
  14. 【Python爬虫】之西瓜视频地址解密20210822
  15. [全网首发]坚果Pro3 root教程 Magisk
  16. c语言水王争霸链表,水王争霸
  17. Mac显示隐藏文件目录
  18. 3G0众测靶场-0407 WriteUp
  19. Mybatis源码学习-MapperMethod
  20. 企业品牌私域化运营,私域流量只是起步

热门文章

  1. 科学计算机eq7,科学计算器HiEdu 580 Scientific Calculator
  2. ubuntu查看node的安装目录_ubuntu安装nfs服务实现共享目录
  3. python preference界面设置_偏好设置如何更改Preference的样式
  4. windows上使用的免费连接linux终端xshell6,xftp6下载
  5. 奔五的人学ios:swift竟然没有字符串包括,找个简单的解决方法
  6. mysql数据库迁移到另一台电脑上
  7. C# 对象与JSON字符串互相转换的三种方式
  8. C++的Opencv动态库遇到的问题
  9. 配置文件configSections节点使用实例      。
  10. centos7磁盘逻辑分区命令_Centos7 磁盘分区概念