第7章:合成与混合模式
7.1 章节概述
在前面的章节中,我们学习了如何绑制图形、处理图像。但当多个图形或图像叠加时,它们是如何组合在一起的呢?默认情况下,新绘制的内容会覆盖旧内容,但 Canvas 提供了丰富的合成模式和混合模式,让我们可以创建各种视觉效果。
本章将深入讲解:
- globalAlpha:全局透明度控制
- globalCompositeOperation:合成操作模式
- Porter-Duff 合成:经典的图像合成理论
- 混合模式:类似 Photoshop 的图层混合效果
- 实战应用:遮罩、剪影、高光等效果
学完本章后,你将能够创建复杂的图层效果,实现专业级的图像合成。
7.2 理解合成的基础概念
7.2.1 源与目标
在 Canvas 合成中,有两个关键概念:
| 术语 | 英文 | 说明 |
|---|---|---|
| 源(Source) | Source | 即将绘制的新内容 |
| 目标(Destination) | Destination | 已经存在于 Canvas 上的内容 |
目标(已有内容) 源(新绘制) 合成结果
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ██████ │ │ ████ │ │ ██████████ │
│ ██████ │ + │ ████ │ = │ ██████████ │
│ ██████ │ │ ████ │ │ ██████████ │
└─────────────┘ └─────────────┘ └─────────────┘7.2.2 默认合成行为
默认情况下(source-over),新绘制的内容会覆盖在旧内容之上:
javascript
// 绘制红色矩形(目标)
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);
// 绘制蓝色矩形(源)- 覆盖在红色上
ctx.fillStyle = 'blue';
ctx.fillRect(100, 100, 100, 100);结果:蓝色矩形显示在红色矩形之上,重叠部分只显示蓝色。
7.2.3 为什么需要合成模式?
不同的合成模式可以实现:
- 遮罩效果:让新内容只显示在特定区域
- 擦除效果:用新内容"擦掉"旧内容
- 混合效果:颜色混合,创建光影效果
- 剪影效果:保留形状,改变颜色
- 叠加效果:多层图像融合
7.3 全局透明度 globalAlpha
7.3.1 基本用法
globalAlpha 设置所有后续绑制操作的透明度:
javascript
ctx.globalAlpha = value; // 0(完全透明)到 1(完全不透明)7.3.2 使用示例
javascript
// 绘制不透明红色矩形
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);
// 设置 50% 透明度
ctx.globalAlpha = 0.5;
// 绘制半透明蓝色矩形
ctx.fillStyle = 'blue';
ctx.fillRect(100, 100, 100, 100);
// 重置透明度
ctx.globalAlpha = 1.0;7.3.3 globalAlpha 与 rgba 的区别
javascript
// 方式1:使用 globalAlpha
ctx.globalAlpha = 0.5;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
// 方式2:使用 rgba
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(0, 0, 100, 100);
// 效果相同,但:
// - globalAlpha 影响所有后续操作
// - rgba 只影响当前颜色
// - globalAlpha 与 rgba 的透明度会叠加!透明度叠加:
javascript
ctx.globalAlpha = 0.5;
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(0, 0, 100, 100);
// 最终透明度 = 0.5 × 0.5 = 0.257.3.4 创建淡入淡出效果
javascript
class FadeEffect {
constructor(ctx, x, y, size) {
this.ctx = ctx;
this.x = x;
this.y = y;
this.size = size;
this.alpha = 0;
this.fadeIn = true;
this.speed = 0.02;
}
update() {
if (this.fadeIn) {
this.alpha += this.speed;
if (this.alpha >= 1) {
this.alpha = 1;
this.fadeIn = false;
}
} else {
this.alpha -= this.speed;
if (this.alpha <= 0) {
this.alpha = 0;
this.fadeIn = true;
}
}
}
draw() {
this.ctx.save();
this.ctx.globalAlpha = this.alpha;
this.ctx.fillStyle = '#4D7CFF';
this.ctx.fillRect(this.x, this.y, this.size, this.size);
this.ctx.restore();
}
}7.4 合成操作 globalCompositeOperation
7.4.1 设置合成模式
javascript
ctx.globalCompositeOperation = 'mode-name';7.4.2 Porter-Duff 合成模式
这些模式基于经典的 Porter-Duff 合成算法,控制源和目标如何组合:
| 模式 | 效果 | 公式 |
|---|---|---|
source-over | 源覆盖在目标上(默认) | 正常叠加 |
source-in | 只显示源与目标重叠部分的源 | 源 ∩ 目标形状 |
source-out | 只显示源不与目标重叠的部分 | 源 - 目标形状 |
source-atop | 源只显示在目标上 | 源 叠加在 目标上(仅重叠) |
destination-over | 目标覆盖在源上 | 目标在上 |
destination-in | 只显示目标与源重叠部分的目标 | 目标 ∩ 源形状 |
destination-out | 只显示目标不与源重叠的部分 | 目标 - 源形状 |
destination-atop | 目标只显示在源上 | 目标 叠加在 源上(仅重叠) |
copy | 只显示源,忽略目标 | 完全替换 |
xor | 源和目标不重叠的部分 | 异或 |
lighter | 颜色值相加(变亮) | 加法混合 |
7.4.3 图示理解
原始图形:
┌─────────┐
│ ▓▓▓▓ │ ▓ = 目标(蓝色圆)
│ ▓▓▓▓ │ ░ = 源(红色正方形)
│ ░░░░ │
│ ░░░░ │
└─────────┘
source-over(默认) source-in source-out
┌─────────┐ ┌─────────┐ ┌─────────┐
│ ▓▓▓▓ │ │ │ │ │
│ ▓▓░░░░ │ │ ░░ │ │ ░░ │
│ ░░░░ │ │ │ │ ░░░░ │
└─────────┘ └─────────┘ └─────────┘
destination-over destination-in destination-out
┌─────────┐ ┌─────────┐ ┌─────────┐
│ ▓▓▓▓ │ │ │ │ ▓▓▓▓ │
│ ▓▓▓▓░░ │ │ ▓▓ │ │ ▓▓ │
│ ░░░░ │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘7.4.4 实际代码演示
javascript
/**
* 演示所有合成模式
*/
function demonstrateCompositeModes(ctx) {
const modes = [
'source-over', 'source-in', 'source-out', 'source-atop',
'destination-over', 'destination-in', 'destination-out', 'destination-atop',
'lighter', 'copy', 'xor'
];
const size = 100;
const cols = 4;
const padding = 20;
modes.forEach((mode, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
const x = col * (size + padding) + padding;
const y = row * (size + padding * 2) + padding;
// 创建临时 Canvas 避免模式相互影响
const tempCanvas = document.createElement('canvas');
tempCanvas.width = size;
tempCanvas.height = size;
const tempCtx = tempCanvas.getContext('2d');
// 绘制目标(蓝色圆)
tempCtx.fillStyle = '#4D7CFF';
tempCtx.beginPath();
tempCtx.arc(35, 35, 30, 0, Math.PI * 2);
tempCtx.fill();
// 设置合成模式
tempCtx.globalCompositeOperation = mode;
// 绘制源(红色正方形)
tempCtx.fillStyle = '#FF6B6B';
tempCtx.fillRect(30, 30, 50, 50);
// 复制到主 Canvas
ctx.drawImage(tempCanvas, x, y);
// 添加标签
ctx.fillStyle = '#333';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(mode, x + size / 2, y + size + 15);
});
}7.4.5 source-in 实现遮罩效果
source-in 是实现遮罩的关键模式:
javascript
/**
* 使用 source-in 创建遮罩效果
* 图像只显示在遮罩形状内
*/
async function createMaskedImage(imageSrc, maskShape) {
const img = await loadImage(imageSrc);
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
// 1. 先绘制遮罩形状(目标)
ctx.fillStyle = '#000'; // 颜色无所谓,只要不透明
maskShape(ctx, canvas.width, canvas.height);
// 2. 设置 source-in 模式
ctx.globalCompositeOperation = 'source-in';
// 3. 绘制图像(源)- 只在遮罩形状内显示
ctx.drawImage(img, 0, 0);
return canvas;
}
// 使用示例:圆形头像
const circleAvatar = await createMaskedImage('avatar.jpg', (ctx, w, h) => {
ctx.beginPath();
ctx.arc(w / 2, h / 2, Math.min(w, h) / 2, 0, Math.PI * 2);
ctx.fill();
});
// 使用示例:星形遮罩
const starMasked = await createMaskedImage('photo.jpg', (ctx, w, h) => {
drawStar(ctx, w / 2, h / 2, Math.min(w, h) / 2.5, 5);
ctx.fill();
});7.4.6 destination-out 实现擦除效果
destination-out 可以"擦掉"已绘制的内容:
javascript
/**
* 橡皮擦功能
*/
class Eraser {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.isErasing = false;
this.size = 30;
this.bindEvents();
}
bindEvents() {
this.canvas.addEventListener('mousedown', () => {
this.isErasing = true;
});
this.canvas.addEventListener('mouseup', () => {
this.isErasing = false;
});
this.canvas.addEventListener('mousemove', (e) => {
if (!this.isErasing) return;
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.erase(x, y);
});
}
erase(x, y) {
const { ctx, size } = this;
ctx.save();
// 使用 destination-out 模式
ctx.globalCompositeOperation = 'destination-out';
// 绘制擦除形状(圆形)
ctx.beginPath();
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}7.4.7 lighter 实现发光效果
lighter 模式将颜色值相加,非常适合创建发光效果:
javascript
/**
* 发光效果
*/
function drawGlow(ctx, x, y, radius, color) {
ctx.save();
ctx.globalCompositeOperation = 'lighter';
// 绘制多层逐渐变淡的圆,模拟发光
const layers = 10;
for (let i = 0; i < layers; i++) {
const alpha = 1 - i / layers;
const r = radius * (1 + i * 0.5);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
// 使用径向渐变
const gradient = ctx.createRadialGradient(x, y, 0, x, y, r);
gradient.addColorStop(0, `rgba(${color}, ${alpha * 0.5})`);
gradient.addColorStop(1, `rgba(${color}, 0)`);
ctx.fillStyle = gradient;
ctx.fill();
}
ctx.restore();
}
// 使用
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGlow(ctx, 200, 200, 30, '255, 100, 100'); // 红色发光
drawGlow(ctx, 300, 200, 30, '100, 255, 100'); // 绿色发光
drawGlow(ctx, 250, 280, 30, '100, 100, 255'); // 蓝色发光7.5 Photoshop 风格混合模式
Canvas 还支持类似 Photoshop 的混合模式,用于颜色混合:
7.5.1 混合模式列表
| 模式 | 效果 |
|---|---|
multiply | 正片叠底(变暗,常用于阴影) |
screen | 滤色(变亮,常用于高光) |
overlay | 叠加(对比增强) |
darken | 变暗(取较暗值) |
lighten | 变亮(取较亮值) |
color-dodge | 颜色减淡(高光效果) |
color-burn | 颜色加深(暗部效果) |
hard-light | 强光 |
soft-light | 柔光 |
difference | 差值(反色效果) |
exclusion | 排除(柔和差值) |
hue | 色相 |
saturation | 饱和度 |
color | 颜色 |
luminosity | 明度 |
7.5.2 混合模式原理
multiply(正片叠底):
结果 = 源 × 目标 / 255
效果:总是变暗,白色透明,黑色不变
用途:添加阴影、纹理叠加screen(滤色):
结果 = 255 - (255 - 源) × (255 - 目标) / 255
效果:总是变亮,黑色透明,白色不变
用途:添加高光、光晕效果overlay(叠加):
如果 目标 < 128:结果 = 2 × 源 × 目标 / 255
如果 目标 >= 128:结果 = 255 - 2 × (255 - 源) × (255 - 目标) / 255
效果:亮的更亮,暗的更暗
用途:增强对比度,添加质感7.5.3 multiply 实现阴影
javascript
/**
* 使用 multiply 添加阴影
*/
async function addShadowToImage(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
// 绘制原图
ctx.drawImage(img, 0, 0);
// 使用 multiply 模式添加阴影
ctx.globalCompositeOperation = 'multiply';
// 绘制渐变阴影(底部变暗)
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); // 顶部保持不变
gradient.addColorStop(0.7, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.5)'); // 底部变暗
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas;
}7.5.4 screen 实现高光
javascript
/**
* 使用 screen 添加高光
*/
function addHighlight(ctx, x, y, radius) {
ctx.save();
ctx.globalCompositeOperation = 'screen';
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.3)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}7.5.5 overlay 增强对比度
javascript
/**
* 使用 overlay 增强图像对比度
*/
function enhanceContrast(ctx, intensity = 0.3) {
const { width, height } = ctx.canvas;
ctx.save();
ctx.globalCompositeOperation = 'overlay';
ctx.globalAlpha = intensity;
// 叠加灰色可以增强对比
ctx.fillStyle = '#808080';
ctx.fillRect(0, 0, width, height);
ctx.restore();
}7.5.6 difference 实现反色选区
javascript
/**
* 使用 difference 显示选区(蚂蚁线效果的替代方案)
*/
function highlightSelection(ctx, selectionPath) {
ctx.save();
ctx.globalCompositeOperation = 'difference';
// 使用 difference 模式,白色区域会反色
ctx.fillStyle = '#fff';
ctx.fill(selectionPath);
ctx.restore();
}7.5.7 混合模式对比演示
javascript
/**
* 创建混合模式对比图
*/
async function demonstrateBlendModes(ctx, img) {
const blendModes = [
'multiply', 'screen', 'overlay',
'darken', 'lighten', 'color-dodge',
'color-burn', 'hard-light', 'soft-light',
'difference', 'exclusion', 'hue',
'saturation', 'color', 'luminosity'
];
const tileSize = 150;
const cols = 5;
const overlayColor = '#FF6B6B'; // 用于叠加的颜色
blendModes.forEach((mode, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
const x = col * tileSize;
const y = row * (tileSize + 20);
// 绘制图像
ctx.drawImage(img, x, y, tileSize, tileSize);
// 应用混合模式
ctx.save();
ctx.globalCompositeOperation = mode;
ctx.fillStyle = overlayColor;
ctx.fillRect(x, y, tileSize, tileSize);
ctx.restore();
// 标签
ctx.fillStyle = '#333';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(mode, x + tileSize / 2, y + tileSize + 15);
});
}7.6 实战应用
7.6.1 创建圆形头像
javascript
/**
* 创建圆形头像
*/
async function createCircularAvatar(imageSrc, size) {
const img = await loadImage(imageSrc);
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// 绘制圆形遮罩
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
// 使用 source-in,图像只显示在圆形内
ctx.globalCompositeOperation = 'source-in';
// 居中裁剪绘制图像
const scale = Math.max(size / img.width, size / img.height);
const newW = img.width * scale;
const newH = img.height * scale;
const x = (size - newW) / 2;
const y = (size - newH) / 2;
ctx.drawImage(img, x, y, newW, newH);
return canvas;
}7.6.2 图像水印
javascript
/**
* 添加水印
*/
function addWatermark(ctx, text, options = {}) {
const {
fontSize = 24,
color = 'rgba(255, 255, 255, 0.3)',
angle = -30,
spacing = 200
} = options;
const { width, height } = ctx.canvas;
ctx.save();
ctx.font = `${fontSize}px Arial`;
ctx.fillStyle = color;
// 使用合适的混合模式
ctx.globalCompositeOperation = 'overlay';
// 旋转绘制
ctx.rotate(angle * Math.PI / 180);
// 平铺水印
const diagonal = Math.sqrt(width * width + height * height);
for (let y = -diagonal; y < diagonal; y += spacing) {
for (let x = -diagonal; x < diagonal; x += spacing) {
ctx.fillText(text, x, y);
}
}
ctx.restore();
}7.6.3 刮刮卡效果
javascript
/**
* 刮刮卡效果
*/
class ScratchCard {
constructor(container, revealImage, coverColor = '#silver') {
this.container = container;
this.revealImage = revealImage;
this.setup(coverColor);
}
async setup(coverColor) {
// 创建显示层(底层图片)
this.revealCanvas = document.createElement('canvas');
this.revealCanvas.width = this.revealImage.width;
this.revealCanvas.height = this.revealImage.height;
const revealCtx = this.revealCanvas.getContext('2d');
revealCtx.drawImage(this.revealImage, 0, 0);
// 创建覆盖层
this.coverCanvas = document.createElement('canvas');
this.coverCanvas.width = this.revealImage.width;
this.coverCanvas.height = this.revealImage.height;
this.coverCanvas.style.position = 'absolute';
this.coverCanvas.style.top = '0';
this.coverCanvas.style.left = '0';
this.coverCtx = this.coverCanvas.getContext('2d');
// 绘制覆盖层
this.coverCtx.fillStyle = coverColor;
this.coverCtx.fillRect(0, 0, this.coverCanvas.width, this.coverCanvas.height);
// 添加到容器
this.container.style.position = 'relative';
this.container.appendChild(this.revealCanvas);
this.container.appendChild(this.coverCanvas);
this.bindEvents();
}
bindEvents() {
let isScratching = false;
this.coverCanvas.addEventListener('mousedown', () => {
isScratching = true;
});
window.addEventListener('mouseup', () => {
isScratching = false;
});
this.coverCanvas.addEventListener('mousemove', (e) => {
if (!isScratching) return;
const rect = this.coverCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.scratch(x, y);
});
}
scratch(x, y) {
// 使用 destination-out 擦除覆盖层
this.coverCtx.globalCompositeOperation = 'destination-out';
this.coverCtx.beginPath();
this.coverCtx.arc(x, y, 25, 0, Math.PI * 2);
this.coverCtx.fill();
}
}7.6.4 图层混合编辑器
javascript
/**
* 简单的图层混合编辑器
*/
class LayerBlender {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.layers = [];
}
addLayer(image, blendMode = 'source-over', opacity = 1) {
this.layers.push({
image,
blendMode,
opacity
});
this.render();
}
setLayerBlendMode(index, blendMode) {
if (this.layers[index]) {
this.layers[index].blendMode = blendMode;
this.render();
}
}
setLayerOpacity(index, opacity) {
if (this.layers[index]) {
this.layers[index].opacity = opacity;
this.render();
}
}
render() {
const { ctx, canvas, layers } = this;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 按顺序绘制图层
layers.forEach(layer => {
ctx.save();
ctx.globalCompositeOperation = layer.blendMode;
ctx.globalAlpha = layer.opacity;
ctx.drawImage(layer.image, 0, 0, canvas.width, canvas.height);
ctx.restore();
});
}
}
// 使用
const blender = new LayerBlender(canvas);
blender.addLayer(backgroundImg, 'source-over', 1);
blender.addLayer(textureImg, 'multiply', 0.5);
blender.addLayer(highlightImg, 'screen', 0.7);7.7 本章小结
本章深入介绍了 Canvas 的合成与混合系统:
核心概念
| 概念 | 说明 |
|---|---|
| 源(Source) | 即将绘制的新内容 |
| 目标(Destination) | 已存在的内容 |
| globalAlpha | 全局透明度 |
| globalCompositeOperation | 合成/混合模式 |
常用合成模式
| 模式 | 用途 |
|---|---|
source-over | 默认,正常覆盖 |
source-in | 遮罩效果 |
source-out | 反向遮罩 |
destination-out | 擦除效果 |
lighter | 发光效果 |
multiply | 阴影、纹理 |
screen | 高光、光晕 |
overlay | 对比度增强 |
应用场景
- 圆形头像、形状遮罩
- 橡皮擦、刮刮卡
- 发光、阴影效果
- 图层混合、滤镜叠加
- 水印添加
7.8 练习题
基础练习
实现一个可调节透明度的图层叠加效果
使用
source-in创建各种形状的图片遮罩实现简单的橡皮擦功能
进阶练习
创建一个混合模式选择器,可以实时预览各种混合效果
实现带有"刮刮乐"效果的抽奖卡片
挑战练习
- 构建一个简易图层编辑器:
- 支持多图层
- 每个图层可设置混合模式和透明度
- 支持图层排序
下一章预告:在第8章中,我们将学习文本渲染——如何在 Canvas 中绑制和处理文本。
文档版本:v1.0
字数统计:约 14,000 字
代码示例:40+ 个
