资讯专栏INFORMATION COLUMN

从一个画板demo学习canvas

netmou / 1431人阅读

摘要:前言初学,做了一个画板应用,地址点这里。本篇为的一些基础思想和注意事项,不是基础。主要是在于事件上的实践经验屏兼容屏会使用多个物理像素渲染一个独立像素,导致一倍图在屏幕上模糊,也是这样,所以我们应该把画布的大小设为元素大小的或倍。

前言

初学canvas,做了一个画板应用,地址点这里 。本篇为canvas的一些基础思想和注意事项,不是基础api。主要是在于touch事件上的实践经验

retina屏兼容

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

const canvas = selector("#canvas")
const ctx = canvas.getContext("2d")
const RATIO = 3
const canvasOffset = canvas.getBoundingClientRect()
canvas.width = canvasOffset.width * RATIO
canvas.height = canvasOffset.height * RATIO
坐标系转化

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

function windowToCanvas (x, y) {
  return {
    x: (x - canvasOffset.left) * RATIO,
    y: (y - canvasOffset.top) * RATIO
  }
}

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

获取touch点的坐标

function getTouchPosition (e) {
  let touch = e.changedTouches[0]
  return windowToCanvas(touch.clientX, touch.clientY)
}
画布状态的储存和恢复

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

ctx.save()
ctx.fillStyle = "#333"
ctx.strokeStyle = "#666"
ctx.restore()
绘制表面的储存与恢复

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

// 工具基础 宽度,颜色,是否在绘画中,是否被选中
class Basic {
  constructor (width = RATIO, color = "#000") {
    this.width = width
    this.color = color
    this.drawing = false
    this.isSelect = false
  }
}

class Rect extends Basic {
  constructor (width = RATIO, color = "#000") {
    super(width, color)
    this.startPosition = {
      x: 0,
      y: 0
    }
    this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight)
  }
  begin (loc) {
    this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //在这里储存绘图表面
    saveImageData(this.firstDot)
    Object.assign(this.startPosition, loc)
    ctx.save() // 储存画布状态
    ctx.lineWidth = this.width
    ctx.strokeStyle = this.color
  }
  draw (loc) {
    ctx.putImageData(this.firstDot, 0, 0) //恢复绘图表面,并开始绘制方形
    const rect = {
      x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x,
      y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y,
      width: Math.abs(this.startPosition.x - loc.x),
      height: Math.abs(this.startPosition.y - loc.y)
    }
    ctx.beginPath()
    ctx.rect(rect.x, rect.y, rect.width, rect.height)
    ctx.stroke()
  }
  end (loc) {
    ctx.putImageData(this.firstDot, 0, 0)
    const rect = {
      x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x,
      y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y,
      width: Math.abs(this.startPosition.x - loc.x),
      height: Math.abs(this.startPosition.y - loc.y)
    }
    ctx.beginPath()
    ctx.rect(rect.x, rect.y, rect.width, rect.height)
    ctx.stroke()
    ctx.restore() //恢复画布状态
  }
  bindEvent () {
    canvas.addEventListener("touchstart", (e) => {
      e.preventDefault()
      if (!this.isSelect) {
        return false
      }
      this.drawing = true
      let loc = getTouchPosition(e)
      this.begin(loc)
    })
    canvas.addEventListener("touchmove", (e) => {
      e.preventDefault()
      if (!this.isSelect) {
        return false
      }
      if (this.drawing) {
        let loc = getTouchPosition(e)
        this.draw(loc)
      }
    })
    canvas.addEventListener("touchend", (e) => {
      e.preventDefault()
      if (!this.isSelect) {
        return false
      }
      let loc = getTouchPosition(e)
      this.end(loc)
      this.drawing = false
    })
  }
}
椭圆的绘制方法(均匀压缩法)

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

class Round extends Basic{
  constructor (width = RATIO, color = "#000") {
    super(width, color)
    this.startPosition = {
      x: 0,
      y: 0
    }
    this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight)
  }
  drawCalculate (loc) {
    ctx.save()
    ctx.lineWidth = this.width
    ctx.strokeStyle = this.color
    ctx.putImageData(this.firstDot, 0, 0) //恢复绘图表面
    const rect = {
      width: loc.x - this.startPosition.x,
      height: loc.y - this.startPosition.y
    } // 计算方形的宽高(带有正负值)
    const rMax = Math.max(Math.abs(rect.width), Math.abs(rect.height)) // 选出较长边
    rect.x = this.startPosition.x + rect.width / 2 // 计算压缩前的圆心坐标
    rect.y = this.startPosition.y + rect.height / 2
    rect.scale = {
      x: Math.abs(rect.width) / rMax,
      y: Math.abs(rect.height) / rMax
    } // 计算压缩比例
    ctx.scale(rect.scale.x, rect.scale.y)
    ctx.beginPath()
    ctx.arc(rect.x / rect.scale.x, rect.y / rect.scale.y, rMax / 2, 0, Math.PI * 2) 
    ctx.stroke()
    ctx.restore()
  }
  begin (loc) {
    this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //储存绘图表面
    saveImageData(this.firstDot)
    Object.assign(this.startPosition, loc)
  }
  draw (loc) {
    this.drawCalculate(loc)
  }
  end (loc) {
    this.drawCalculate(loc)
  }
  bindEvent () {
    canvas.addEventListener("touchstart", (e) => {
      e.preventDefault()
      if (!this.isSelect) {
        return false
      }
      this.drawing = true
      let loc = getTouchPosition(e)
      this.begin(loc)
    })
    canvas.addEventListener("touchmove", (e) => {
      e.preventDefault()
      if (!this.isSelect) {
        return false
      }
      if (this.drawing) {
        let loc = getTouchPosition(e)
        this.draw(loc)
      }
    })
    canvas.addEventListener("touchend", (e) => {
      e.preventDefault()
      if (!this.isSelect) {
        return false
      }
      let loc = getTouchPosition(e)
      this.end(loc)
      this.drawing = false
    })
  }
}
撤销操作

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

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

有一些看上去高大上的东西,了解了以后就会发现很简单,有了基础的模型以后,再去一点一点丰富功能,所以有些时候不能总是看看看,一定要动手,yeah
我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82736.html

相关文章

  • canvas进阶——如何画出平滑的曲线?

    摘要:,算法就是这样,那我们基于该算法再对现有代码进行一次升级改造设置线条颜色在原有的基础上,我们创建了一个变量用于保存之前事件中鼠标经过的点,根据该算法可知要绘制二次贝塞尔曲线起码需要个点以上,因此我们只有在中的点数大于时才开始绘制。 背景概要 相信大家平时在学习canvas 或 项目开发中使用canvas的时候应该都遇到过这样的需求:实现一个可以书写的画板小工具。 嗯,相信这对canva...

    Cobub 评论0 收藏0
  • canvas进阶——如何画出平滑的曲线?

    摘要:,算法就是这样,那我们基于该算法再对现有代码进行一次升级改造设置线条颜色在原有的基础上,我们创建了一个变量用于保存之前事件中鼠标经过的点,根据该算法可知要绘制二次贝塞尔曲线起码需要个点以上,因此我们只有在中的点数大于时才开始绘制。 背景概要 相信大家平时在学习canvas 或 项目开发中使用canvas的时候应该都遇到过这样的需求:实现一个可以书写的画板小工具。 嗯,相信这对canva...

    _ivan 评论0 收藏0
  • Canvas画板---手机上也可以用的画板

    摘要:方法可以获取到上下文二制作画板画板功能可以绘制不同颜色和粗细的线条,画板上有橡皮擦功能,一键清除功能,下载功能。我们可以用来监听三种状态。 学习制作画板之前,我们先来了解一下canvas标签 一.canvas标签 1.canvas标签与img标签相似,但是canvas标签是一个闭合标签,并且没有src alt属性2.canvas标签有两个属性,width,height。我们在页面上用c...

    oogh 评论0 收藏0

发表评论

0条评论

netmou

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<