从一个画板demo进修canvas

媒介

初学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…

    原文作者:Ganother
    原文地址: https://segmentfault.com/a/1190000009230759
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞