Canvas 实现 PS 中的正片叠底效果

前言

年前在知乎和抖音上刷到了一些关于“正片叠底”的讲解,感觉挺有意思的,于是趁现在有时间决定用 Canvas 来实现一下这个效果。

在图像和视频编辑中,“正片叠底”是一种常见的混合模式,能够创造出丰富的视觉效果。如果你用过 Photoshop 或其他设计工具,应该对这个功能不陌生。本文将带你从零开始,使用 Canvas 实现正片叠底效果,并且会分别对图片和视频进行实现。

什么是正片叠底?

简单来说,正片叠底是一种混合模式,通过将两张图片或两个视频的像素逐一叠加,计算它们的颜色值。最终的效果通常会让画面变得更暗,颜色更加浓郁。

它的核心公式是:
$$
R=\frac{A \times B}{255}
$$
其中:

  • A:底层像素的颜色值。
  • B:上层像素的颜色值。
  • R:输出像素的颜色值。

这个公式模拟了摄影中的光学效果,将两层画面的明暗部分巧妙地结合在一起。

抖音上有一个短视频讲解得非常清楚,链接放在文末,感兴趣的话可以看看。

正片叠底的原理

从数学角度来看,正片叠底通过对每个像素的 RGB 值进行相乘并归一化,减少了高亮区域的影响,因此整体效果会偏暗。

举个例子,假设有两个像素:

  • 底层像素:A=(100,150,200)
  • 上层像素:B=(50,100,150)

通过公式计算:
$$
R = \left( \frac{100 \times 50}{255}, \frac{150 \times 100}{255}, \frac{200 \times 150}{255} \right) \approx (20, 59, 118)
$$
最终输出的像素值 R 是一个较暗的颜色。

正片叠底的实现

在 Canvas 中,我们可以通过操作像素数据来实现正片叠底效果。主要步骤如下:

  1. 使用 getContext('2d') 获取 Canvas 的上下文。
  2. 绘制两张图片到 Canvas 上。
  3. 通过 getImageData 获取两层的像素数据。
  4. 对像素数据进行遍历,根据正片叠底公式计算新像素值。
  5. 将计算后的像素数据渲染回 Canvas。

接下来,我们用代码来实现!

实现图片的正片叠底效果

创建一个黑底白字的图片

首先,我们需要创建一个黑底白字的图片,作为上层的图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 创建一个带有文本内容的画布。
* @param {number} width - 画布的宽度。
* @param {number} height - 画布的高度。
* @returns {Object} - 包含画布和上下文的对象。
*/
function createTextImageCanvas(width, height) {
const textCanvas = document.createElement('canvas');
textCanvas.width = width;
textCanvas.height = height;

const textContext = textCanvas.getContext('2d');
textContext.fillStyle = 'black';
textContext.fillRect(0, 0, width, height);

textContext.font = '130px Arial Black';
textContext.fillStyle = 'white';
textContext.textAlign = 'center';
textContext.textBaseline = 'middle';
textContext.fillText('Hello, World', width / 2, height / 2);

return { canvas: textCanvas, context: textContext };
}

const textImageCanvas = createTextImageCanvas(1000, 600);
document.body.appendChild(textImageCanvas.canvas);

实现效果:

加载图片

接下来,我们使用 Canvas 加载一张图片,这张图片将作为底层图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 绘制叠加后的图像到主画布。
* @param {string} imagePath - 底层图片路径。
*/
function drawBlendedImage(imagePath) {
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
canvas.width = 1000; // 设置画布宽度
canvas.height = 700; // 设置画布高度

// 创建带有文本的画布
const textCanvas = createTextImageCanvas(canvas.width, canvas.height);

const baseImage = new Image();
baseImage.src = imagePath;

// 当图片加载完成时,开始处理图像
baseImage.onload = () => {
context.drawImage(baseImage, 0, 0, canvas.width, canvas.height); // 绘制底层图片
};
}

drawBlendedImage('./img/img.jpg')

实现效果:

正片叠底算法

现在,我们已经有了两张图片,接下来需要将文字图片和背景图片融合到一起,使文字部分镂空显示背景图片。我们需要实现一个正片叠底混合算法。

通过 ctx.getImageData 获取两张图片的像素数据,然后对每个像素点进行 $\frac{A \times B}{255}$ 的计算,最后将结果通过 ctx.putImageData 渲染回 Canvas。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 应用正片叠底混合效果。
* @param {ImageData} baseImage - 底层图像的像素数据。
* @param {ImageData} overlayImage - 叠加图像的像素数据。
* @returns {ImageData} - 混合后的像素数据。
*/
function applyMultiplyBlend(baseImage, overlayImage) {
const baseData = baseImage.data;
const overlayData = overlayImage.data;
const resultImage = new ImageData(baseImage.width, baseImage.height);
const resultData = resultImage.data;

for (let i = 0; i < baseData.length; i += 4) {
resultData[i] = (baseData[i] * overlayData[i]) / 255; // 混合红色通道
resultData[i + 1] = (baseData[i + 1] * overlayData[i + 1]) / 255; // 混合绿色通道
resultData[i + 2] = (baseData[i + 2] * overlayData[i + 2]) / 255; // 混合蓝色通道
resultData[i + 3] = baseData[i + 3]; // 保留透明度
}

return resultImage;
}

实现效果:

完整代码

以下是两张图片进行正片叠底混合的完整代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正片叠底效果示例</title>
</head>

<body>
<canvas id="canvas"></canvas>

<script>
/**
* 创建一个带有文本内容的画布。
* @param {number} width - 画布的宽度。
* @param {number} height - 画布的高度。
* @returns {Object} - 包含画布和上下文的对象。
*/
function createTextImageCanvas(width, height) {
const textCanvas = document.createElement("canvas");
textCanvas.width = width;
textCanvas.height = height;

const textContext = textCanvas.getContext("2d");
textContext.fillStyle = "black";
textContext.fillRect(0, 0, width, height);

textContext.font = "130px Arial Black";
textContext.fillStyle = "white";
textContext.textAlign = "center";
textContext.textBaseline = "middle";
textContext.fillText("Hello, World", width / 2, height / 2);

return { canvas: textCanvas, context: textContext };
}

/**
* 应用正片叠底混合效果。
* @param {ImageData} baseImage - 底层图像的像素数据。
* @param {ImageData} overlayImage - 叠加图像的像素数据。
* @returns {ImageData} - 混合后的像素数据。
*/
function applyMultiplyBlend(baseImage, overlayImage) {
const baseData = baseImage.data;
const overlayData = overlayImage.data;
const resultImage = new ImageData(baseImage.width, baseImage.height);
const resultData = resultImage.data;

for (let i = 0; i < baseData.length; i += 4) {
resultData[i] = (baseData[i] * overlayData[i]) / 255; // 混合红色通道
resultData[i + 1] = (baseData[i + 1] * overlayData[i + 1]) / 255; // 混合绿色通道
resultData[i + 2] = (baseData[i + 2] * overlayData[i + 2]) / 255; // 混合蓝色通道
resultData[i + 3] = baseData[i + 3]; // 保留透明度
}

return resultImage;
}

/**
* 绘制叠加后的图像到主画布。
* @param {string} imagePath - 底层图片路径。
*/
function drawBlendedImage(imagePath) {
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
canvas.width = 1000; // 设置画布宽度
canvas.height = 700; // 设置画布高度

// 创建带有文本的画布
const textCanvas = createTextImageCanvas(canvas.width, canvas.height);

const baseImage = new Image();
baseImage.src = imagePath;

// 当图片加载完成时,开始处理图像
baseImage.onload = () => {
context.drawImage(baseImage, 0, 0, canvas.width, canvas.height); // 绘制底层图片

// 获取底层图像和叠加文本的像素数据
const baseImageData = context.getImageData(0, 0, canvas.width, canvas.height);
const overlayImageData = textCanvas.context.getImageData(0, 0, textCanvas.canvas.width, textCanvas.canvas.height);

// 应用正片叠底混合效果
const blendedImageData = applyMultiplyBlend(baseImageData, overlayImageData);

// 将混合后的图像数据绘制回画布
context.putImageData(blendedImageData, 0, 0);
};
}

// 调用函数绘制图像(替换为实际图片路径)
drawBlendedImage('./img/tk.jpg');
</script>
</body>

</html>

实现视频的正片叠底效果

刚才我们实现了图片的正片叠底效果,接下来我们结合视频来实现一个炫酷一些的效果。

具体实现

处理视频的正片叠底与处理图片的基本思路相同,唯一的区别是,我们需要通过 requestAnimationFrame 获取视频的每一帧,然后结合上层图片进行正片叠底计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>视频与图片正片叠底效果</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}

#blend-container {
position: relative;
width: 100%;
height: 100%;
}

video,
canvas {
position: absolute;
top: 0;
left: 0;
}

video {
visibility: hidden;
}
</style>
</head>

<body>
<div id="blend-container">
<video id="source-video" autoplay loop muted playsinline></video>
<canvas id="blend-canvas"></canvas>
</div>
<script>
const videoElement = document.getElementById("source-video");
const canvasElement = document.getElementById("blend-canvas");
const canvasContext = canvasElement.getContext("2d");

/**
* 创建一个带有文本内容的画布。
* @param {number} width - 画布的宽度。
* @param {number} height - 画布的高度。
* @returns {Object} - 包含画布和上下文的对象。
*/
function createTextImageCanvas(width, height) {
const textCanvas = document.createElement("canvas");
textCanvas.width = width;
textCanvas.height = height;

const textContext = textCanvas.getContext("2d");
textContext.fillStyle = "black";
textContext.fillRect(0, 0, width, height);

textContext.font = "150px Arial Black";
textContext.fillStyle = "white";
textContext.textAlign = "center";
textContext.textBaseline = "middle";
textContext.fillText("Hello, World", width / 2, height / 2);

return { canvas: textCanvas, context: textContext };
}

/**
* 对两个图像帧应用正片叠底混合模式。
* @param {ImageData} videoFrame - 视频帧的图像数据。
* @param {ImageData} imageFrame - 图像帧的图像数据。
* @returns {ImageData} - 混合后的图像数据。
*/
function applyMultiplyBlend(videoFrame, imageFrame) {
const videoData = videoFrame.data;
const imageData = imageFrame.data;
const blendedFrame = new ImageData(videoFrame.width, videoFrame.height);

for (let i = 0; i < videoData.length; i += 4) {
blendedFrame.data[i] = (videoData[i] * imageData[i]) / 255; // 红色通道
blendedFrame.data[i + 1] = (videoData[i + 1] * imageData[i + 1]) / 255; // 绿色通道
blendedFrame.data[i + 2] = (videoData[i + 2] * imageData[i + 2]) / 255; // 蓝色通道
blendedFrame.data[i + 3] = videoData[i + 3]; // 透明通道
}

return blendedFrame;
}

/**
* 在画布上绘制混合效果。
*/
function drawBlendEffect() {
if (videoElement.readyState >= 2) {
// 绘制当前视频帧
canvasContext.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
const videoFrame = canvasContext.getImageData(0, 0, canvasElement.width, canvasElement.height);

// 创建并绘制文本图像帧
const textImage = createTextImageCanvas(canvasElement.width, canvasElement.height);
const imageFrame = textImage.context.getImageData(0, 0, textImage.canvas.width, textImage.canvas.height);

// 应用正片叠底混合模式并渲染
const blendedFrame = applyMultiplyBlend(videoFrame, imageFrame);
canvasContext.putImageData(blendedFrame, 0, 0);
}

requestAnimationFrame(drawBlendEffect);
}

// 初始化视频和画布设置
videoElement.src = "./img/video.mp4";
videoElement.addEventListener("loadeddata", () => {
canvasElement.width = videoElement.videoWidth;
canvasElement.height = videoElement.videoHeight;
});
videoElement.addEventListener("play", drawBlendEffect);
</script>
</body>

</html>

实现效果

结语

通过本文,我们了解了正片叠底的原理,并使用 Canvas 实现了图片和视频的正片叠底效果。这种技术不仅可以用于学习图像处理的基础知识,还可以帮助你在前端开发中实现一些酷炫的效果。

相关链接


Canvas 实现 PS 中的正片叠底效果
https://blog.pangcy.cn/2025/01/21/前端编程相关/Canvas/Canvas 实现 PS 中的正片叠底效果/
作者
子洋
发布于
2025年1月21日
许可协议