Skip to content

canvas 合成

globalCompositeOperation

Canvas 2D API 的 CanvasRenderingContext2D.globalCompositeOperation 属性设置要在绘制新形状时应用的合成操作的类型,其中 type 是用于标识要使用的合成或混合模式操作的字符串。

js
ctx.globalCompositeOperation = type;

合成三个图形

代码:
vue
<template>
  <canvas id="three-composite" width="200" height="200"></canvas>
  <div class="font-bold text-fuchsia-600">合成模式:</div>
  <div v-for="(item, index) in globalCompositeOperationList"  class="flex items-start">
    <div class="w-30% flex-shrink-0 self-unset">
      <div class="w-full pt-full relative">
        <canvas 
          :id="'test-canvas-' + item[0]" 
          width="200" 
          height="200" 
          class="w-full h-full absolute top-0 left-0 border-dark-50 border-.5 border-solid"
        ></canvas>
      </div>
    </div>
    <label 
      class="ml-10px flex flex-col"
      :class="selectedIndex === index ? 'text-red' : ''"
    >
      <div class="flex items-end">
        <input type="radio" name="go" :checked="selectedIndex === index" @change.stop="select(index)"/>
        <div class="ml-5px leading-none">{{ item[0] }}</div>
      </div>
      <div class="ml-20px mt-5px">{{ item[1].desc }}</div>
    </label>
  </div>
</template>
<script setup lang='ts'>
import { onMounted, ref, nextTick } from 'vue';

const globalCompositeOperationList = [
  ['source-in', { desc: '新图形只在新图形和目标画布重叠的地方绘制。其他的都是透明的。' }],
  ['source-over', { desc: '这是默认设置,并在现有画布上下文之上绘制新图形。' }],
  ['source-out', { desc: '在不与现有画布内容重叠的地方绘制新图形。' }],
  ['source-atop', { desc: '新图形只在与现有画布内容重叠的地方绘制。' }],
  ['destination-over', { desc: '在现有的画布内容后面绘制新的图形。' }],
  ['destination-in', { desc: '现有的画布内容保持在新图形和现有画布内容重叠的位置。其他的都是透明的。' }],
  ['destination-out', { desc: '现有内容保持在新图形不重叠的地方。' }],
  ['destination-atop', { desc: '现有的画布只保留与新图形重叠的部分,新的图形是在画布内容后面绘制的。' }],
  ['lighter', { desc: '两个重叠图形的颜色是通过颜色值相加来确定的。' }],
  ['copy', { desc: '只显示新图形。' }],
  ['xor', { desc: '图像中,那些重叠和正常绘制之外的其他地方是透明的。' }],
  ['multiply', { desc: '将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片。' }],
  ['screen', { desc: '像素被倒转,相乘,再倒转,结果是一幅更明亮的图片。' }],
  ['overlay', { desc: 'multiply 和 screen 的结合,原本暗的地方更暗,原本亮的地方更亮。' }],
  ['darken', { desc: '保留两个图层中最暗的像素。' }],
  ['lighten', { desc: '保留两个图层中最亮的像素。' }],
  ['color-dodge', { desc: '将底层除以顶层的反置。' }],
  ['color-burn', { desc: '将反置的底层除以顶层,然后将结果反过来。' }],
  ['hard-light', { desc: '屏幕相乘(A combination of multiply and screen)类似于叠加,但上下图层互换了。' }],
  ['soft-light', { desc: '用顶层减去底层或者相反来得到一个正值。' }],
  ['difference', { desc: '一个柔和版本的强光(hard-light)。纯黑或纯白不会导致纯黑或纯白。' }],
  ['exclusion', { desc: '和 difference 相似,但对比度较低。' }],
  ['hue', { desc: '保留了底层的亮度(luma)和色度(chroma),同时采用了顶层的色调(hue)。' }],
  ['saturation', { desc: '保留底层的亮度(luma)和色调(hue),同时采用顶层的色度(chroma)。' }],
  ['color', { desc: '保留了底层的亮度(luma),同时采用了顶层的色调 (hue) 和色度 (chroma)。' }],
  ['luminosity', { desc: '保持底层的色调(hue)和色度(chroma),同时采用顶层的亮度(luma)。' }],
] as Array<[GlobalCompositeOperation, { desc: string }]>

const selectedIndex = ref(0)

function select(index) {
  selectedIndex.value = index
  const type = globalCompositeOperationList[selectedIndex.value][0]
  draw(type, demoCtx)
}

let demoCtx: CanvasRenderingContext2D

function draw(type: GlobalCompositeOperation, destination: CanvasRenderingContext2D) {
  // 重新绘制一个 canvas
  const sourceCanvas = document.createElement("canvas")
  sourceCanvas.width = 200
  sourceCanvas.height = 200
  const sourceCtx = sourceCanvas.getContext('2d')!
  // 清除背景
  sourceCtx.clearRect(-100, 100, 200, 200)
  sourceCtx.fillStyle = 'rgba(0,0,0, 0.3)'
  sourceCtx.fillRect(0, 0, 200, 200)
  sourceCtx.translate(100, 100)
  sourceCtx.globalCompositeOperation = type

  // 绘制顺序:红绿蓝
  sourceCtx.beginPath()
  sourceCtx.fillStyle = 'red'
  sourceCtx.arc(30, 0, 50, 0, 2 * Math.PI)
  sourceCtx.fill()
  sourceCtx.closePath()

  sourceCtx.beginPath()
  sourceCtx.fillStyle = 'green'
  sourceCtx.arc(-30, 0, 50, 0, 2 * Math.PI)
  sourceCtx.fill()
  sourceCtx.closePath()

  sourceCtx.beginPath()
  sourceCtx.fillStyle = 'blue'
  sourceCtx.arc(0, -30, 50, 0, 2 * Math.PI)
  sourceCtx.fill()
  sourceCtx.closePath()

  destination.clearRect(0, 0, 200, 200)
  destination.drawImage(sourceCanvas, 0, 0, 200, 200)
}



onMounted(() => {
  demoCtx = (document.getElementById('three-composite') as HTMLCanvasElement).getContext('2d')!

  draw(globalCompositeOperationList[selectedIndex.value][0], demoCtx)

  // 只绘制一次的示例
  for (const i of globalCompositeOperationList) {
    const type = i[0]
    const ctx = (document.getElementById(`test-canvas-${type}`) as HTMLCanvasElement).getContext('2d')!
    draw(type, ctx)
  }
})
</script>

刮刮乐 1

代码:

vue
<template>
  <div id="card">
    <canvas id="scratch-ticket" width="200" height="200"></canvas>
  </div>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';

onMounted(() => {
  const canvas = document.getElementById('scratch-ticket') as HTMLCanvasElement
  const ctx = canvas.getContext('2d')
  const width = 200
  const height = 200

  let isDrawing
  // 避免移动过快,刮出来的图形成点状
  let lastPoint;

  canvas.addEventListener('mousedown', mouseDown, false);
  canvas.addEventListener('mousemove', mouseMove, false);
  canvas.addEventListener('mouseup', mouseUp, false);

  canvas.addEventListener('touchstart', mouseDown, false);
  canvas.addEventListener('touchmove', mouseMove, false);
  canvas.addEventListener('touchend', mouseUp, false);

  function getFilledInPixels() {
    let pixels = ctx.getImageData(0, 0, width, height);
    let pdata = pixels.data;
    let len = pdata.length;
    let total = len / 32;
    let count = 0;

    // Iterate over all pixels
    for (let i = 0; i < len; i += 32) {
      if (pdata[i] === 0) {
        count++;
      }
    }

    return Math.round((count / total) * 100);
  }

  function distanceBetween(point1, point2) {
    return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
  }
  function angleBetween(point1, point2) {
    return Math.atan2(point2.x - point1.x, point2.y - point1.y);
  }

  // 返回鼠标在 canvas 内部的坐标点
  function getMouse(e: MouseEvent & TouchEvent, canvas) {
    var offsetX = 0;
    var offsetY = 0;
    var mx;
    var my;

    if (canvas.offsetParent !== undefined) {
      do {
        offsetX += canvas.offsetLeft;
        offsetY += canvas.offsetTop
      } while ((canvas = canvas.offsetParent));
    }

    mx = (e.pageX || e.touches[0].pageX) - offsetX;
    my = (e.pageY || e.touches[0].pageY) - offsetY;

    return {
      x: mx,
      y: my
    }
  }

  function handlePercentage(filledInPixels) {
    filledInPixels = filledInPixels || 0;
    if (filledInPixels > 80) {
      canvas?.parentNode?.removeChild(canvas);
    }
  }

  function mouseDown(e) {
    isDrawing = true;
    lastPoint = getMouse(e, canvas);
  }

  function mouseMove(e: MouseEvent & TouchEvent) {
    e.preventDefault()
    if (!isDrawing) {
      return;
    }
    // 移动过程中,光标当前在 canvas 中的坐标
    let currentPoint = getMouse(e, canvas);
    const dist = distanceBetween(lastPoint, currentPoint);
    // 计算角度,换句话说,计算 x、y 坐标是该加还是该减
    const angle = angleBetween(lastPoint, currentPoint);
    let x
    let y
    for (let i = 0; i < dist; i ++) {
      x = lastPoint.x + Math.sin(angle) * i * 2
      y = currentPoint.y +  Math.cos(angle) * i * 2

      ctx.globalCompositeOperation = 'destination-out';
      const radius = 8;
      ctx.beginPath();
      ctx.arc(x, y, radius, 0, Math.PI * 2, true);
      ctx.closePath();
      ctx.fill();
    }
    lastPoint = currentPoint;
    handlePercentage(getFilledInPixels());
  }

  function mouseUp() {
    isDrawing = false;
  }
  // 把 canvas 填充为灰色
  function draw() {
    ctx.save();
    ctx.fillStyle = '#ddd';
    ctx.fillRect(0, 0, width, height);
    ctx.restore();
  }

  draw();
})
</script>
<style>
#card {
  width: 200px;
  height: 200px;
  background: url('https://lukecheng2.oss-cn-chengdu.aliyuncs.com/public/img/2023-06-27-01-40-31.png');
  background-position: center;
  border: 1px solid rgba(0, 0, 0, .5);
}
</style>

刮刮乐 2

利用 drawImage API。将一张图片绘制到另一张图片上。

js
  // 1. 绘制图片 1 到 canvas 中
  const canvas = document.querySelector('canvas')
  const ctx = canvas.getContext('2d')
  // 2. 创建图片
  const one = new Image()
  one.src = './1.jpg' // 顶部图片
  // 等图片加载完、
  one.addEventListener('load', function () {
    ctx.drawImage(one, 0, 0, canvas.width, canvas.height)
  })
  const two = new Image()
  two.src = './2.jpg' // 底部图片
  // 给 canvas 注册鼠标移动事件
  let flag = false
  canvas.addEventListener('mousedown', function () {
    // 鼠标按下
    flag = true
  })
  canvas.addEventListener('mousemove', function (e) {
    // 实现擦除
    if (flag) {
      const x = e.offsetX - 5
      const y = e.offsetY - 5
      //  从第二张图片上绘制一部分到第一张图片
      // 从第二张图片中截取 20 * 20 的图片绘制到 canvas 上
      ctx.drawImage(two, x, y, 60, 60, x, y, 60, 60)
    }
  })
  canvas.addEventListener('mouseup', function () {
    // 鼠标抬起
    flag = false
  })

Released under the MIT License.