前言
年前在知乎和抖音上刷到了一些关于“正片叠底”的讲解,感觉挺有意思的,于是趁现在有时间决定用 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 中,我们可以通过操作像素数据来实现正片叠底效果。主要步骤如下:
- 使用
getContext('2d')
获取 Canvas 的上下文。
- 绘制两张图片到 Canvas 上。
- 通过
getImageData
获取两层的像素数据。
- 对像素数据进行遍历,根据正片叠底公式计算新像素值。
- 将计算后的像素数据渲染回 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
|
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
|
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
|
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>
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 }; }
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; }
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");
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 }; }
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 实现了图片和视频的正片叠底效果。这种技术不仅可以用于学习图像处理的基础知识,还可以帮助你在前端开发中实现一些酷炫的效果。
相关链接