使用canvas制作一个移动端画板

淩亂°似流年 2022-05-31 08:42 523阅读 0赞

概述

使用canvas做一个画板,代码里涵盖了一些canvas绘图的基本思想,各种工具的类也可以分别提出来用

详细

代码下载:http://www.demodashi.com/demo/10503.html

一、准备工作

1、如果不需要任何修改的话,直接使用dist文件夹内的文件即可

2、如果需要修改,需要安装node

3、打包js运行webpack 打包css运行gulp css 使用dist/index.html预览

4、学习es6与canvas基础知识,api

二、程序实现

文件结构:

WX20170703-154159@2x.png

retina屏兼容

retina屏会使用多个物理像素渲染一个独立像素,导致一倍图在retina屏幕上模糊,canvas也是这样,所以我们应该把canvas画布的大小设为canvas元素大小的2或3倍。元素大小在css中设置

  1. const canvas = selector('#canvas')
  2. const ctx = canvas.getContext('2d')
  3. const RATIO = 3
  4. const canvasOffset = canvas.getBoundingClientRect()
  5. canvas.width = canvasOffset.width * RATIO
  6. canvas.height = canvasOffset.height * RATIO

坐标系转化

把相对于浏览器窗口的坐标转化为canvas坐标,需要注意的是,如果兼容了retina,需要乘上devicePixelRatio。后面所有出现的坐标,都要通过这个函数转化

  1. function windowToCanvas (x, y) { return {
  2. x: (x - canvasOffset.left) * RATIO,
  3. y: (y - canvasOffset.top) * RATIO
  4. }
  5. }

不得不提的是,《HTML5 Canvas核心技术》有一个相同的函数,但是书上那个是错的(也有可能我看的那本是假书)

获取touch点的坐标

  1. function getTouchPosition (e) {
  2. let touch = e.changedTouches[0]
  3. return windowToCanvas(touch.clientX, touch.clientY)
  4. }

画布状态的储存和恢复

进行绘图操作时,我们会频繁设置canvas绘图环境的属性(线宽,颜色等),大多数情况下我们只是临时设置,比如画蓝色的线段,又要画一个红色的正方形,为了不影响两个绘图操作,我们需要在每次绘制时,先保存环境属性(save),绘图完毕后恢复(restore)

  1. ctx.save()
  2. ctx.fillStyle = "#333"
  3. ctx.strokeStyle = "#666"
  4. ctx.restore()

绘制表面的储存与恢复

主要用于临时性的绘图操作,比如用手指拖出一个方形时,首先要在touchstart事件里储存拖动开始时的绘制表面(getImageData),touchmove的事件函数中,首先要先恢复touch开始时的绘图表面(putImageData),再根据当前的坐标值画出一个方形,继续拖动时,刚才画出的方形会被事件函数的恢复绘图表面覆盖掉,在重新绘制一个方形,所以无论怎么拖动,我们看到的只是画了一个方形,下面是画板demo中方形工具的类

  1. // 工具基础 宽度,颜色,是否在绘画中,是否被选中
  2. class Basic {
  3. constructor (width = RATIO, color = '#000') {
  4. this.width = width
  5. this.color = color
  6. this.drawing = false
  7. this.isSelect = false
  8. }
  9. }
  10. class Rect extends Basic {
  11. constructor (width = RATIO, color = '#000') {
  12. super(width, color) this.startPosition = {
  13. x: 0,
  14. y: 0
  15. }
  16. this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight)
  17. }
  18. begin (loc) {
  19. this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //在这里储存绘图表面
  20. saveImageData(this.firstDot)
  21. Object.assign(this.startPosition, loc)
  22. ctx.save() // 储存画布状态
  23. ctx.lineWidth = this.width
  24. ctx.strokeStyle = this.color
  25. }
  26. draw (loc) {
  27. ctx.putImageData(this.firstDot, 0, 0) //恢复绘图表面,并开始绘制方形
  28. const rect = {
  29. x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x,
  30. y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y,
  31. width: Math.abs(this.startPosition.x - loc.x),
  32. height: Math.abs(this.startPosition.y - loc.y)
  33. }
  34. ctx.beginPath()
  35. ctx.rect(rect.x, rect.y, rect.width, rect.height)
  36. ctx.stroke()
  37. }
  38. end (loc) {
  39. ctx.putImageData(this.firstDot, 0, 0)
  40. const rect = {
  41. x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x,
  42. y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y,
  43. width: Math.abs(this.startPosition.x - loc.x),
  44. height: Math.abs(this.startPosition.y - loc.y)
  45. }
  46. ctx.beginPath()
  47. ctx.rect(rect.x, rect.y, rect.width, rect.height)
  48. ctx.stroke()
  49. ctx.restore() //恢复画布状态
  50. }
  51. bindEvent () {
  52. canvas.addEventListener('touchstart', (e) => {
  53. e.preventDefault()
  54. if (!this.isSelect) {
  55. return false
  56. }
  57. this.drawing = true
  58. let loc = getTouchPosition(e)
  59. this.begin(loc)
  60. })
  61. canvas.addEventListener('touchmove', (e) => {
  62. e.preventDefault()
  63. if (!this.isSelect) {
  64. return false
  65. }
  66. if (this.drawing) {
  67. let loc = getTouchPosition(e)
  68. this.draw(loc)
  69. }
  70. })
  71. canvas.addEventListener('touchend', (e) => {
  72. e.preventDefault()
  73. if (!this.isSelect) {
  74. return false
  75. }
  76. let loc = getTouchPosition(e)
  77. this.end(loc)
  78. this.drawing = false
  79. })
  80. }
  81. }

椭圆的绘制方法(均匀压缩法)

原理是在压缩过的坐标系中绘制一个圆形,那看起来就是一个椭圆了。因为是通过拖动绘制椭圆,所以在我们拖动时,必然拖出了一个方形,那其实就是以方形的中心为圆心,较长边的一半为半径画圆,这个圆要画在压缩过的坐标系中,压缩比例就是较窄边与较长边的比,圆心的坐标也要根据压缩比例做坐标变换,圆形工具类代码如下

  1. class Round extends Basic{
  2. constructor (width = RATIO, color = '#000') {
  3. super(width, color) this.startPosition = {
  4. x: 0,
  5. y: 0
  6. }
  7. this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight)
  8. }
  9. drawCalculate (loc) {
  10. ctx.save()
  11. ctx.lineWidth = this.width
  12. ctx.strokeStyle = this.color
  13. ctx.putImageData(this.firstDot, 0, 0) //恢复绘图表面
  14. const rect = {
  15. width: loc.x - this.startPosition.x,
  16. height: loc.y - this.startPosition.y
  17. } // 计算方形的宽高(带有正负值)
  18. const rMax = Math.max(Math.abs(rect.width), Math.abs(rect.height)) // 选出较长边
  19. rect.x = this.startPosition.x + rect.width / 2 // 计算压缩前的圆心坐标
  20. rect.y = this.startPosition.y + rect.height / 2
  21. rect.scale = {
  22. x: Math.abs(rect.width) / rMax,
  23. y: Math.abs(rect.height) / rMax
  24. } // 计算压缩比例
  25. ctx.scale(rect.scale.x, rect.scale.y)
  26. ctx.beginPath()
  27. ctx.arc(rect.x / rect.scale.x, rect.y / rect.scale.y, rMax / 2, 0, Math.PI * 2)
  28. ctx.stroke()
  29. ctx.restore()
  30. }
  31. begin (loc) {
  32. this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //储存绘图表面
  33. saveImageData(this.firstDot)
  34. Object.assign(this.startPosition, loc)
  35. }
  36. draw (loc) {
  37. this.drawCalculate(loc)
  38. }
  39. end (loc) {
  40. this.drawCalculate(loc)
  41. }
  42. bindEvent () {
  43. canvas.addEventListener('touchstart', (e) => {
  44. e.preventDefault()
  45. if (!this.isSelect) {
  46. return false
  47. }
  48. this.drawing = true
  49. let loc = getTouchPosition(e)
  50. this.begin(loc)
  51. })
  52. canvas.addEventListener('touchmove', (e) => {
  53. e.preventDefault()
  54. if (!this.isSelect) {
  55. return false
  56. }
  57. if (this.drawing) {
  58. let loc = getTouchPosition(e)
  59. this.draw(loc)
  60. }
  61. })
  62. canvas.addEventListener('touchend', (e) => {
  63. e.preventDefault()
  64. if (!this.isSelect) {
  65. return false
  66. }
  67. let loc = getTouchPosition(e)
  68. this.end(loc)
  69. this.drawing = false
  70. })
  71. }
  72. }

撤销操作

上述例子中都有个 saveImageData() 函数,这个函数是把当前绘图表面储存在一个数组中,点击撤销的时候用于恢复上一步的绘图表面

  1. const lastImageData = []
  2. function saveImageData (data) {
  3. (lastImageData.length == 5) && (lastImageData.shift()) // 上限为储存5步,太多了怕挂掉
  4. lastImageData.push(data)
  5. }
  6. document.getElementById("cancel").addEventListener('click', () => {
  7. if(lastImageData.length < 1) return false
  8. ctx.putImageData(lastImageData[lastImageData.length - 1], 0, 0)
  9. lastImageData.pop()
  10. })

三、运行效果

点击目录里index.html

WX20170703-155209.png

四、其他补充

还有一些简单地工具如线宽选择,调色板就不叙述了,有问题欢迎评论

注:本文著作权归作者,由demo大师(http://www.demodashi.com)宣传,拒绝转载,转载需要作者授权

发表评论

表情:
评论列表 (有 0 条评论,523人围观)

还没有评论,来说两句吧...

相关阅读