刮刮樂模擬器

剛剛在開車羣看到一個好玩的動圖

正好新年也快到了,給家人做個“過年紅包刮刮樂”的頁面,增加點年味不也挺好。

說做就做,這裏我們用canvas2D來實現效果,核心的API是 CanvasRenderingContext2D.globalCompositeOperation,主要是用來設定圖形繪製前後的圖層混合模式,詳見該頁。簡單加上自己的理解翻譯下:(ps:source 是將要繪製的圖形,destination是指畫布上已存在的圖形)

# source-over
- This is the default setting and draws new shapes on top of the existing canvas content.
- 這是默認值。新圖覆蓋繪製在舊圖上(保留舊圖)

# source-in
- The new shape is drawn only where both the new shape and the destination canvas overlap. Everything else is made transparent.
- 新圖只在與舊圖重疊區域繪製(繪製區域外畫布透明)

# source-out
- The new shape is drawn where it doesn't overlap the existing canvas content.
- 新圖只在與舊圖不重疊區域繪製(繪製區域外畫布透明)

# source-atop
- The new shape is only drawn where it overlaps the existing canvas content.
- 新圖只在與舊圖重疊區域繪製(保留舊圖)

# destination-over
- New shapes are drawn behind the existing canvas content.
- 新圖覆蓋繪製在舊圖底下(保留舊圖)

# destination-in
- The existing canvas content is kept where both the new shape and existing canvas content overlap. Everything else is made transparent.
- 新圖與舊圖的重疊區域作爲蒙版裁剪舊圖(重疊區域外畫布透明)

# destination-out
- The existing content is kept where it doesn't overlap the new shape.
- 新圖與舊圖的非重疊區域作爲蒙版裁剪舊圖(重疊區域外畫布透明)

# destination-atop
- The existing canvas is only kept where it overlaps the new shape. The new shape is drawn behind the canvas content.
- 新圖只在與舊圖的重疊區域繪製且繪製於舊圖下(繪製區域外畫布透明)

# lighter
- Where both shapes overlap the color is determined by adding color values.
- 重疊區域顏色矩陣相加

# copy
- Only the new shape is shown.
- 只顯示新圖

# xor
- Shapes are made transparent where both overlap and drawn normal everywhere else.
- 圖像中,那些重疊和正常繪製之外的其他地方是透明的。

# multiply
- The pixels are of the top layer are multiplied with the corresponding pixel of the bottom layer. A darker picture is the result.
- 重疊區域顏色矩陣相乘

# screen
- The pixels are inverted, multiplied, and inverted again. A lighter picture is the result (opposite of multiply)
- 像素被倒轉,相乘,再倒轉,結果是一幅更明亮的圖片。

# overlay
- A combination of multiply and screen. Dark parts on the base layer become darker, and light parts become lighter.
- multiply和screen的結合,原本暗的地方更暗,原本亮的地方更亮。

# darken
- Retains the darkest pixels of both layers.
- 保留兩個圖層中最暗的像素。

# lighten
- Retains the lightest pixels of both layers.
- 保留兩個圖層中最亮的像素。

# color-dodge
- Divides the bottom layer by the inverted top layer.
- 將底層除以頂層的反置。

# color-burn
- Divides the inverted bottom layer by the top layer, and then inverts the result.
- 將反置的底層除以頂層,然後將結果反過來。

# hard-light
- A combination of multiply and screen like overlay, but with top and bottom layer swapped.
- 屏幕相乘(A combination of multiply and screen)類似於疊加,但上下圖層互換了。

# soft-light
- A softer version of hard-light. Pure black or white does not result in pure black or white.
- 用頂層減去底層或者相反來得到一個正值。

# difference
- Subtracts the bottom layer from the top layer or the other way round to always get a positive value.
- 一個柔和版本的強光(hard-light)。純黑或純白不會導致純黑或純白。

# exclusion
- Like difference, but with lower contrast.
- 和difference相似,但對比度較低。

# hue
- Preserves the luma and chroma of the bottom layer, while adopting the hue of the top layer.
- 保留了底層的亮度(luma)和色度(chroma),同時採用了頂層的色調(hue)。

# saturation
- Preserves the luma and hue of the bottom layer, while adopting the chroma of the top layer.
- 保留底層的亮度(luma)和色調(hue),同時採用頂層的色度(chroma)。

# color
- Preserves the luma of the bottom layer, while adopting the hue and chroma of the top layer.
- 保留了底層的亮度(luma),同時採用了頂層的色調(hue)和色度(chroma)。

# luminosity
- Preserves the hue and chroma of the bottom layer, while adopting the luma of the top layer.
- 保持底層的色調(hue)和色度(chroma),同時採用頂層的亮度(luma)。

看到global前綴大家應該也猜到了,這個屬性是影響整個畫布的,在一次渲染中無論被賦值幾次,最終的效果都取決於本次渲染的前globalCompositeOperation的最終值。

可以在這裏自己修改各個屬性查看效果。

說一下代碼設計的幾個要點:

① 準備兩個canvas,一個是背景,只在圖片載入的時候渲染一遍,一個是前景,用於合成前景圖和繪圖區域(destination-out)。事實上僅用一個canvas也能實現,每次繪圖時先在畫布上繪製前景圖,然後把背景與繪圖區域通過source-atop合成,再講結果繪製到畫布上。相較起來,前者性能顯然會更好。

② 通過lineTo來塗抹繪圖區域,而不是arc畫圓,避免幀率過低時連線不平滑,因此當鼠標按下時,需要調用beginPath來重置畫筆。

代碼很短,就直接放上來了:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>刮刮樂</title>
  <style>
    * {
      padding: 0;
      margin: 0;
    }
    html, body {
      width: 100%;
      height: 100%;
    }
    canvas {
      position: absolute;
      left: 0;
      top: 0;
      border: 1px dashed black;
    }
  </style>
</head>
<body>
<script>
  (function () {
    const imgBg = new Image()
    const imgFg = new Image()
    let canvasWidth = 0
    let canvasHeight = 0
    let canvasLeft = 0
    let canvasTop = 0

    const init = function () {
      const $canvasBg = document.createElement('canvas')
      $canvasBg.id = 'bg'
      const $canvasFg = document.createElement('canvas')
      $canvasFg.id = 'fg'
      $canvasFg.width = $canvasBg.width = canvasWidth
      $canvasFg.height = $canvasBg.height = canvasHeight
      $canvasFg.style.cssText = $canvasBg.style.cssText = `left:${canvasLeft}px;top:${canvasTop}px;`
      document.body.append($canvasBg)
      document.body.append($canvasFg)

      const ctxBg = $canvasBg.getContext('2d')
      const ctxFg = $canvasFg.getContext('2d')

      // 繪製背景
      ctxBg.drawImage(imgBg, 0, 0)

      ctxFg.lineWidth = 50
      ctxFg.lineCap = 'round'
      ctxFg.lineJoin = 'round'
      ctxFg.strokeStyle = '#000'
      ctxFg.drawImage(imgFg, 0, 0)
      ctxFg.globalCompositeOperation = 'destination-out'

      let posX = 0
      let posY = 0
      let drawing = false

      /**
       * 塗抹
       * @param start 重置畫筆
       */
      const draw = function (start) {
        if (start) {
          ctxFg.beginPath()
          ctxFg.moveTo(posX, posY)
        }
        ctxFg.lineTo(posX, posY)
        ctxFg.stroke()
      }

      // 按下
      const onMouseDown = function (e) {
        drawing = true
        // 獲得畫筆相對canvas位置
        if (e.touches && e.touches.length) {
          posX = e.touches[0].pageX - canvasLeft
          posY = e.touches[0].pageY - canvasTop
        }
        else {
          posX = e.pageX - canvasLeft
          posY = e.pageY - canvasTop
        }
        draw(true)
      }

      // 移動
      const onMouseMove = function (e) {
        if (drawing) {
          if (e.touches && e.touches.length) {
            posX = e.touches[0].pageX - canvasLeft
            posY = e.touches[0].pageY - canvasTop
          }
          else {
            posX = e.pageX - canvasLeft
            posY = e.pageY - canvasTop
          }
          draw()
        }
      }

      // 擡起
      const onMouseUp = function (e) {
        if (drawing) {
          drawing = false
        }
      }

      // 事件監聽
      $canvasFg.addEventListener('mousedown', onMouseDown, false)
      $canvasFg.addEventListener('touchstart', onMouseDown, false)

      window.addEventListener('mousemove', onMouseMove, false)
      window.addEventListener('touchmove', onMouseMove, false)

      window.addEventListener('mouseup', onMouseUp, false)
      window.addEventListener('touchend', onMouseUp, false)
    }

    // 載入圖片
    let loadCount = 0
    const onLoad = function () {
      loadCount++
      if (loadCount === 2) {
        canvasWidth = imgBg.width
        canvasHeight = imgBg.height
        canvasLeft = (window.innerWidth - canvasWidth) * 0.5
        canvasTop = (window.innerHeight - canvasHeight) * 0.5
        init()
      }
    }
    imgBg.src = 'after.png'
    imgBg.complete ? onLoad() : (imgBg.onload = onLoad)
    imgFg.src = 'before.png'
    imgFg.complete ? onLoad() : (imgFg.onload = onLoad)
  })()
</script>
</body>
</html>

 完整代碼戳這裏

在線演示1在線演示2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章