從一個畫板demo學習canvas

前言

初學canvas,作了一個畫板應用,地址點這裏 。本篇爲canvas的一些基礎思想和注意事項,不是基礎api。主要是在於touch事件上的實踐經驗javascript

retina屏兼容

retina屏會使用多個物理像素渲染一個獨立像素,致使一倍圖在retina屏幕上模糊,canvas也是這樣,因此咱們應該把canvas畫布的大小設爲canvas元素大小的2或3倍。元素大小在css中設置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。後面全部出現的座標,都要經過這個函數轉化java

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

不得不提的是,《HTML5 Canvas核心技術》有一個相同的函數,可是書上那個是錯的(也有可能我看的那本是假書)git

獲取touch點的座標github

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

畫布狀態的儲存和恢復

進行繪圖操做時,咱們會頻繁設置canvas繪圖環境的屬性(線寬,顏色等),大多數狀況下咱們只是臨時設置,好比畫藍色的線段,又要畫一個紅色的正方形,爲了避免影響兩個繪圖操做,咱們須要在每次繪製時,先保存環境屬性(save),繪圖完畢後恢復(restore)canvas

ctx.save()
ctx.fillStyle = "#333"
ctx.strokeStyle = "#666"
ctx.restore()

繪製表面的儲存與恢復

主要用於臨時性的繪圖操做,好比用手指拖出一個方形時,首先要在touchstart事件裏儲存拖動開始時的繪製表面(getImageData),touchmove的事件函數中,首先要先恢復touch開始時的繪圖表面(putImageData),再根據當前的座標值畫出一個方形,繼續拖動時,剛纔畫出的方形會被事件函數的恢復繪圖表面覆蓋掉,在從新繪製一個方形,因此不管怎麼拖動,咱們看到的只是畫了一個方形,下面是畫板demo中方形工具的類api

// 工具基礎 寬度,顏色,是否在繪畫中,是否被選中
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...函數

相關文章
相關標籤/搜索