刮刮乐模拟器

刚刚在开车群看到一个好玩的动图

正好新年也快到了,给家人做个“过年红包刮刮乐”的页面,增加点年味不也挺好。

说做就做,这里我们用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

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