Skip to content

第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.25

7.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 练习题

基础练习

  1. 实现一个可调节透明度的图层叠加效果

  2. 使用 source-in 创建各种形状的图片遮罩

  3. 实现简单的橡皮擦功能

进阶练习

  1. 创建一个混合模式选择器,可以实时预览各种混合效果

  2. 实现带有"刮刮乐"效果的抽奖卡片

挑战练习

  1. 构建一个简易图层编辑器:
    • 支持多图层
    • 每个图层可设置混合模式和透明度
    • 支持图层排序

下一章预告:在第8章中,我们将学习文本渲染——如何在 Canvas 中绑制和处理文本。


文档版本:v1.0
字数统计:约 14,000 字
代码示例:40+ 个

如有转载或 CV 的请标注本站原文地址