玩轉你的圖片,各類圖片效果的Canvas實現

前陣子因業務需求,須要對圖片進行一些特殊處理,例如反相,高亮,黑白等,都是使用Canvas來實現javascript

ImageData

要實現上述所說的各類效果,最核心的事情即是對圖片的ImageData對象進行改動。css

ImageData對象是一個用來描述圖片屬性的一種數據對象,它有三個屬性,分別是datawidthheight。後兩個表明的是圖片的寬高,不用多說。最重要的就是data屬性,它是一個Uint8ClampedArray(8位無符號整形固定數組)類型化數組。按照從上到下,從左到右的順序,它裏面儲存了一張圖片的全部像素的rgba信息。html

例如,一張圖片有4個像素,那data裏面就有16個值,data[0]~data[3]的值就是第一個像素中的r、g、b、a值(不瞭解rgba的看這裏)。html5

如何得到一張圖片的ImageData對象?經過canvas的getImageData即可以很簡單地得到:java

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)

const oriPeixel = ctx.getImageData(0, 0, canvas.width, canvas.height)

值得注意的是,ImageData裏面的屬性都是隻讀的,不能直接更改和賦值。css3

例如咱們把上面的oriPeixel的屬性賦值,就會報如下的錯:git

oriPeixel.data = []

> Uncaught TypeError: Cannot assign to read only property 'data' of object '#<ImageData>'

瞭解了ImageData後,咱們來看看效果demogithub

Demo 1:圖片反相漸變

先看demo:demo-1canvas

一、像素處理

能夠見到,圖片先是漸變成反相的樣子,再漸變爲下一張圖片,是否是很酷炫。要現實這個,主要是用到getImageDataputImageData這兩個API數組

剛纔咱們說過,圖片的ImageData對象儲存着該圖片的每一個像素的信息,想要獲得圖片的反相效果,要做以下處理:

threshold (ctx, idx) {
  let pixels = ctx.getImageData(0, this.height * idx, this.width, this.height)
  let d = pixels.data
  for (let i = 0; i < d.length; i += 4) {
    let r = d[i]
    let g = d[i + 1]
    let b = d[i + 2]
    // 根據rgb求灰度值公式0.2126 * r + 0.7152 * g + 0.0722 * b
    let v = (0.2126 * r + 0.7152 * g + 0.0722 * b >= 100) ? 255 : 128
    d[i] = d[i + 1] = d[i + 2] = v
  }
  return pixels
}

返回的pixels即是圖片通過反相處理後的ImageData

這裏主要是對每一個像素的灰度值做過濾,大於等於100的,直接爲白色,不然置於128

除此以外,還有黑白,高亮等其餘像素處理,具體的能夠看這篇文章

二、漸變處理

有了通過反相處理後的圖片的ImageData數據,下一步要作的天然就是漸變賦值了。原生是沒有提供相關的API自動達成這種的漸變效果的,因此就須要咱們自行實現一遍了,這個會比較麻煩。

用js寫過動畫的同窗都知道,基本上都會使用requestAnimationFrame函數來進行幀處理,這裏也不意外。

主要思路是這樣,圖片通過以下的順序進行漸變:

圖片1----->圖片1反相----->圖片2----->圖片2反相----->圖片3......

直接貼上主要代碼:

gradualChange () {
  // 圖片原始的ImageData數據
  let oriPixels = this.ctx.getImageData(0, 0, this.width, this.height)
  let oriData = oriPixels.data
  // 圖片反相後的ImageData數據
  let nextData = this.nextPixel[0].data
  let length = oriData.length
  let totalgap = 0
  let gap = 0
  let gapTemp
  for (let i = 0; i < length; i++) {
    // 計算每一個rgba的差值,同時縮小處理。除的數值表明着漸變速度,越大越慢
    gapTemp = (nextData[i] - oriData[i]) / 13

    if (oriData[i] !== nextData[i]) {
      // 每一個rgba值增量處理,簡單來講就是各類取整,[-1,1]區間直接取-1或1
      gap = gapTemp > 1 ? Math.floor(gapTemp) : gapTemp < -1 ? Math.ceil(gapTemp) : oriData[i] < nextData[i] ? 1 : oriData[i] > nextData[i] ? -1 : 0
      totalgap += Math.abs(gap)
      oriData[i] = oriData[i] + gap
    }
  }
  
  // 經過putImageData更新圖片
  this.ctx.putImageData(oriPixels, 0, 0)

  // 總值爲0,證實已經漸變完成
  if (!totalgap) {
    this.nextPixel.shift()
    if (!this.nextPixel[0]) {
      this.isChange = false
    }
  }
}

上面是漸變過程的主要代碼,完整的代碼能夠查看:我是代碼

Demo 2:光條高亮移動效果

一樣是先看demo

能夠見到,移動端的demo中,光條上有幾個亮斑在同時移動;而PC端,則是在當鼠標hover上去以後,在光條中有一個圓形光斑的高亮效果,由於圖片自己是透明的,因此背景色作了深色處理。

一、像素處理

須要說明的是,要實現這種效果,最好是找一些背景一部分透明,一部分帶有帶狀色條的圖片,例如我demo中的圖片。這類圖片有至關區域像素的rgba值爲4個0,咱們很容易對其作邊界處理

一樣的,實現這種效果也是須要對圖片像素的rgba值進行處理,可是會比圖片反相漸變複雜一些,由於這裏須要先實現一個圓形的光斑。

光斑實現

既然是圓形光斑,確定是先有圓心和半徑。在這裏,我是在橫向的方向上,取光條的中心爲圓心,半徑取50

實現的代碼在demo2的brightener函數裏面,理解起來也不困難,給定一個y座標,而後再遍歷一遍在這個y座標下的像素,找出每條光條初始點和結束點的x座標。rgba值連續兩點不爲0的,就認爲是仍處在光條中,尚未達到邊界值

brightener (y) {
  // ....完整請看源代碼
  for (let x = 0; x < cW; x++) {
    sPx = (cY * cW + x) * 4
    if (oriData[sPx] || oriData[sPx + 1] || oriData[sPx + 2]) {
      startX || (startX = x)
      tempX = sPx + 4
      if (oriData[tempX] || oriData[tempX + 1] || oriData[tempX + 2]) {
        continue
      } else {
        endX = tempX / 4 - cY * cW
        cX = Math.ceil((endX - startX) / 2) + startX
        startX = 0
        res.push({
          x: cX,
          y: cY
        })
      }
    }
  }
  return res
}

肯定了圓心以後,就能夠根據半徑肯定一個圓,並用一個數組存儲這個圓內各個點,以便後續處理。過程也很簡單,就是初中學的那一套,兩點距離小於半徑就能夠了

createArea (x, y, radius) {
  let result = []
  for (let i = x - radius; i <= x + radius; i++) {
    for (let j = y - radius; j <= y + radius; j++) {
      let dx = i - x
      let dy = j - y
      if ((dx * dx + dy * dy) <= (radius * radius)) {
        let obj = {}
        if (i > 0 && j > 0) {
          obj.x = i
          obj.y = j
          result.push(obj)
        }
      }
    }
  }
  return result
}

以後,就是實現一個光斑效果。在這裏,我是從圓心向邊緣進行一個透明度的衰減漸變

// ...
const validArr = this.createArea(x, y, radius)
validArr.forEach((px, i) => {
  sPx = (px.y * cW + px.x) * 4
  // 像素點的rgb值不全爲0
  if (oriData[sPx] || oriData[sPx + 1] || oriData[sPx + 2]) {
    distance = Math.sqrt((px.x - x) * (px.x - x) + (px.y - y) * (px.y - y))
    // 根據距離和半徑的比率進行正比衰減
    gap = Math.floor(opacity * (1 - distance / radius))
    oriData[sPx + 3] += gap
  }
})
// 更新ImageData
this.ctx.putImageData(oriPixels, 0, 0)

到這裏,一個光斑就這樣實現了

二、移動效果

光斑有了,天然就是讓它動起來。這個就簡單啦,光斑生成的咱們已經完成,那麼咱們只要把圓心動起來就能夠了

在這裏,一樣是使用requestAnimationFrame函數來進行幀處理。而光斑是從下向上移動的,能夠看到startY在不斷遞減

autoPlay (timestamp) {
  if (this.startY <= -25) {
    let timeGap
    if (!this.progress) {
      this.progress = timestamp
    }
    timeGap = timestamp - this.progress
    // 判斷間隔時間是否知足
    if (timeGap > this.autoPlayInterval) {
      this.startY = this.height - 1
      this.progress = 0
    }
  } else {
    // 根據Y座標生成圓心及光斑 
    const res = this.getBrightCenter(this.startY)
    this.brightnessCtx(res, 50, 60)
    this.startY -= 10
  }
  window.requestAnimationFrame(this.autoPlay.bind(this), false)
}

能夠看到,無非就是循環startY座標,生成新光斑的過程。而PC上的效果是當鼠標hover上去時有光斑效果,同理去掉這個自動移動的過程,對圖片的mousemove事件進行監聽,得出xy座標做爲圓心便可

值得注意的是,由於在不斷地更新ImageData,因此咱們須要一個臨時的canvas來存放原始圖片的ImageData數據。demo1也是做了一樣的處理

完整的代碼能夠查看:PC端移動端

總結

以上即是使用Canvas實現一些圖片效果的介紹,權當拋磚引玉,各類看官也能夠發揮想象力,實現本身的酷炫效果

參考

相關文章
相關標籤/搜索