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
})