使用 HTML canvas 将多个蒙版图像叠加在一起
Multiple masked images on top of each other using HTML canvas
我是 canvas 的新手,之前没有使用过它,但我认为它非常适合以下任务。在处理它时我有疑问,我仍然不知道是否可以使用 canvas.
来实现该任务
Exemplary graphic of the masks and images and the result that I want to achieve (and the actual results that I got).
- 轮廓只是为了更好地说明图像
尺寸。
- 掩码是之前使用 promises 预加载的 SVG 图像
它们被绘制并且每次迭代都会改变。所以首先
迭代它是图像 1 的掩码 A 和第二次迭代掩码
图 2 的 B。
简化的伪代码示例:
const items = [1, 2];
for (let i = 0; i < items.length; i++) {
ctx.drawImage(preloadedMask[i], x, y, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-in';
img[i] = new Image();
img[i].onload = () => {
ctx.drawImage(img[i], 0, 0, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-over';
//ctx.globalCompositeOperation = 'source-out';
};
img[i].src = `images/${i+1}.jpg`;
}
当我删除 globalCompositeOperation 和图像时,蒙版会像我预期的那样完美地并排绘制。
但是一旦我添加了一个 globalCompositeOperation,它就变得复杂了,老实说我非常困惑。
我在 onload 回调中尝试了所有可能的 globalCompositeOperation 值 - 但变化不大。我想我必须在为每次迭代绘制掩码后将 globalCompositeOperation 更改为不同的值 - 但我没有想法。
是否有任何方法可以实现图中描述的所需输出,或者我应该放弃 canvas 来完成这项任务?
不幸的是,您想要实现的目标并不那么容易 - 至少如果您使用的 SVG 被视为图像并直接绘制到 canvas。
假设我们有以下 svg 蒙版和图像
如果我们获取第一个蒙版和第一张图像并使用以下代码:
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
我们得到了想要的输出:
如果我们重复这个过程并对第二个面具做同样的事情:
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
我们只会看到一个空 canvas。为什么?我们将 globalCompositeOperation 设置为 'source-in' 并且之前的 canvas 和第二个掩码 (maskB) 没有任何重叠区域。这意味着我们正在有效地擦除 canvas.
如果我们尝试补偿 save/restore 上下文或将 globalCompositeOperation 重置为其初始状态
context.save();
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
context.restore();
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
我们还没有成功:
所以这里的技巧是这样的:
- 确保要屏蔽的 svg 和图像都已完全加载
- 创建一个新的空canvas目标大小canvas
- 将第一个蒙版画到新的 canvas
- 将其 globalCompositeOperation 设置为 'source-in'
- 将第一张图片绘制到新的 canvas
- 将新的canvas画到目标canvas
- 擦除新的 canvas 并重复前面的步骤来合成您的最终图像
举个例子(点击'Run code snippet'):
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let imagesLoaded = 0;
let imageA = document.getElementById("imageA");
let imageB = document.getElementById("imageB");
let width = canvas.width;
let height = canvas.height;
function loaded() {
imagesLoaded++;
if (imagesLoaded == 4) {
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempCanvas.width = width;
tempCanvas.height = height;
tempContext.save();
tempContext.drawImage(document.getElementById("semiCircleA"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageA, 0, 0, width, 160);
ctx.drawImage(tempCanvas, 0, 0, width, height);
tempContext.restore();
tempContext.clearRect(0, 0, width, height);
tempContext.drawImage(document.getElementById("semiCircleB"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageB, 0, 0, width, height);
ctx.drawImage(tempCanvas, 0, 0, width, height);
}
}
document.getElementById("semiCircleA").onload = loaded;
document.getElementById("semiCircleB").onload = loaded;
imageA.onload = loaded;
imageA.src = "https://picsum.photos/id/237/160/160";
imageB.onload = loaded;
imageB.src = "https://picsum.photos/id/137/160/160";
<h1>Final Canvas</h1>
<canvas id="canvas" width=160 height=160>
</canvas>
<br>
<h1>Sources</h1>
<img id="semiCircleA" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,0 80,160"/>
</svg>'>
<img id="semiCircleB" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,1 80,160"/>
</svg>'>
<img id="imageA">
<img id="imageB">
一个canvas可以是一层
像任何元素一样,canvas 很容易创建,可以像图像一样处理,或者如果您熟悉 photoshop,canvas 可以是图层。
创建空白canvas
// Returns the renderable image (canvas)
function CreateImage(width, height) {
return Object.assign(document.createElement("canvas"), {width, height});
}
复制 canvas 或图像之类的对象
// Image can be any image like element including canvas. Returns the renderable image
function CopyImage(img, width = img.width, height = img.height, smooth = true) {
const can = createImage(width, height});
can.ctx = can.getContext("2d");
can.ctx.imageSmoothingEnabled = smooth;
can.ctx.drawImage(img, 0, 0, width, height);
return can;
}
正在加载
切勿在渲染循环中加载图像。图片 onload
事件将不遵循您分配给 src
的顺序。因此 onload
中的图像渲染并不总是按照您希望的顺序进行。
加载所有图像并等待渲染。
加载一组图像的示例。函数 loadImages
returns 承诺将在所有图像加载后解析。
const images = {
maskA: "imageUrl",
maskB: "imageUrl",
imgA: "imageUrl",
imgB: "imageUrl",
};
function loadImages(imgList, data) {
return new Promise((done, loadingError) => {
var count = 0;
const imgs = Object.entries();
for (const [name, src] of imgs) {
imgList[name] = new Image;
imgList[name].src = src;
count ++;
imgList[name].addEventListener("load", () => {
count--;
if (count === 0) { done({imgs: imgList, data}) }
}, {once, true)
);
imgList[name].addEventListener("error", () => {
for (const [name, src] of imgs) { imgList[name] = src }
loadingError(new Error("Could not load all images"));
}, {once, true)
);
}
});
}
渲染
最好创建函数来完成重复的任务。您正在重复的一项任务是遮罩,以下函数使用 canvas 作为目标、图像和遮罩
function maskImage(ctx, img, mask, x = 0, y = 0, w = ctx.canvas.height, h = ctx.canvas.width, clear = true) {
ctx.globalCompositeOperation = "source-over";
clear && ctx.clearRect(0, 0, ctx.canvas.height, ctx.canvas.width);
ctx.drawImage(img, x, y, w, h);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(mask, 0, 0, w, h);
return ctx.canvas; // return the renderable image
}
一旦你设置了一些实用程序来帮助协调加载和渲染,你就可以合成你的最终结果
// assumes ctx is the context to render to
loadImages(images, {ctx}).then(({imgs, {ctx}} => {
const w = ctx.canvas.width, h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const layer = copyImage(ctx.canvas);
ctx.drawImage(maskImage(layer.ctx, imgs.imgA, imgs.maskA), 0, 0, w, h);
ctx.drawImage(maskImage(layer.ctx, imgs.imgB, imgs.maskB), 0, 0, w, h);
// if you no longer need the images always remove them from memory to avoid hogging
// client's resources.
imgs = {}; // de-reference images so that GC can clean up.
}
您现在可以根据需要对任意数量的蒙版图像进行分层。由于为每个子任务创建的函数,因此在本项目和未来的项目中都可以轻松创建更复杂的渲染,而无需编写冗长和重复的代码。
我是 canvas 的新手,之前没有使用过它,但我认为它非常适合以下任务。在处理它时我有疑问,我仍然不知道是否可以使用 canvas.
来实现该任务Exemplary graphic of the masks and images and the result that I want to achieve (and the actual results that I got).
- 轮廓只是为了更好地说明图像 尺寸。
- 掩码是之前使用 promises 预加载的 SVG 图像 它们被绘制并且每次迭代都会改变。所以首先 迭代它是图像 1 的掩码 A 和第二次迭代掩码 图 2 的 B。
简化的伪代码示例:
const items = [1, 2];
for (let i = 0; i < items.length; i++) {
ctx.drawImage(preloadedMask[i], x, y, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-in';
img[i] = new Image();
img[i].onload = () => {
ctx.drawImage(img[i], 0, 0, canvasWidth, canvasHeight);
ctx.globalCompositeOperation = 'source-over';
//ctx.globalCompositeOperation = 'source-out';
};
img[i].src = `images/${i+1}.jpg`;
}
当我删除 globalCompositeOperation 和图像时,蒙版会像我预期的那样完美地并排绘制。
但是一旦我添加了一个 globalCompositeOperation,它就变得复杂了,老实说我非常困惑。
我在 onload 回调中尝试了所有可能的 globalCompositeOperation 值 - 但变化不大。我想我必须在为每次迭代绘制掩码后将 globalCompositeOperation 更改为不同的值 - 但我没有想法。
是否有任何方法可以实现图中描述的所需输出,或者我应该放弃 canvas 来完成这项任务?
不幸的是,您想要实现的目标并不那么容易 - 至少如果您使用的 SVG 被视为图像并直接绘制到 canvas。
假设我们有以下 svg 蒙版和图像
如果我们获取第一个蒙版和第一张图像并使用以下代码:
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
我们得到了想要的输出:
如果我们重复这个过程并对第二个面具做同样的事情:
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
我们只会看到一个空 canvas。为什么?我们将 globalCompositeOperation 设置为 'source-in' 并且之前的 canvas 和第二个掩码 (maskB) 没有任何重叠区域。这意味着我们正在有效地擦除 canvas.
如果我们尝试补偿 save/restore 上下文或将 globalCompositeOperation 重置为其初始状态
context.save();
context.drawImage(maskA,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageA,0,0,width,height);
context.restore();
context.drawImage(maskB,0,0,width,height);
context.globalCompositeOperation = "source-in";
context.drawImage(imageB,0,0,width,height);
我们还没有成功:
所以这里的技巧是这样的:
- 确保要屏蔽的 svg 和图像都已完全加载
- 创建一个新的空canvas目标大小canvas
- 将第一个蒙版画到新的 canvas
- 将其 globalCompositeOperation 设置为 'source-in'
- 将第一张图片绘制到新的 canvas
- 将新的canvas画到目标canvas
- 擦除新的 canvas 并重复前面的步骤来合成您的最终图像
举个例子(点击'Run code snippet'):
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let imagesLoaded = 0;
let imageA = document.getElementById("imageA");
let imageB = document.getElementById("imageB");
let width = canvas.width;
let height = canvas.height;
function loaded() {
imagesLoaded++;
if (imagesLoaded == 4) {
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempCanvas.width = width;
tempCanvas.height = height;
tempContext.save();
tempContext.drawImage(document.getElementById("semiCircleA"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageA, 0, 0, width, 160);
ctx.drawImage(tempCanvas, 0, 0, width, height);
tempContext.restore();
tempContext.clearRect(0, 0, width, height);
tempContext.drawImage(document.getElementById("semiCircleB"), 0, 0, width, height);
tempContext.globalCompositeOperation = "source-in";
tempContext.drawImage(imageB, 0, 0, width, height);
ctx.drawImage(tempCanvas, 0, 0, width, height);
}
}
document.getElementById("semiCircleA").onload = loaded;
document.getElementById("semiCircleB").onload = loaded;
imageA.onload = loaded;
imageA.src = "https://picsum.photos/id/237/160/160";
imageB.onload = loaded;
imageB.src = "https://picsum.photos/id/137/160/160";
<h1>Final Canvas</h1>
<canvas id="canvas" width=160 height=160>
</canvas>
<br>
<h1>Sources</h1>
<img id="semiCircleA" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,0 80,160"/>
</svg>'>
<img id="semiCircleB" src='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="160px" height="160px">
<path d="M80,0 A80,80 0 0,1 80,160"/>
</svg>'>
<img id="imageA">
<img id="imageB">
一个canvas可以是一层
像任何元素一样,canvas 很容易创建,可以像图像一样处理,或者如果您熟悉 photoshop,canvas 可以是图层。
创建空白canvas
// Returns the renderable image (canvas)
function CreateImage(width, height) {
return Object.assign(document.createElement("canvas"), {width, height});
}
复制 canvas 或图像之类的对象
// Image can be any image like element including canvas. Returns the renderable image
function CopyImage(img, width = img.width, height = img.height, smooth = true) {
const can = createImage(width, height});
can.ctx = can.getContext("2d");
can.ctx.imageSmoothingEnabled = smooth;
can.ctx.drawImage(img, 0, 0, width, height);
return can;
}
正在加载
切勿在渲染循环中加载图像。图片 onload
事件将不遵循您分配给 src
的顺序。因此 onload
中的图像渲染并不总是按照您希望的顺序进行。
加载所有图像并等待渲染。
加载一组图像的示例。函数 loadImages
returns 承诺将在所有图像加载后解析。
const images = {
maskA: "imageUrl",
maskB: "imageUrl",
imgA: "imageUrl",
imgB: "imageUrl",
};
function loadImages(imgList, data) {
return new Promise((done, loadingError) => {
var count = 0;
const imgs = Object.entries();
for (const [name, src] of imgs) {
imgList[name] = new Image;
imgList[name].src = src;
count ++;
imgList[name].addEventListener("load", () => {
count--;
if (count === 0) { done({imgs: imgList, data}) }
}, {once, true)
);
imgList[name].addEventListener("error", () => {
for (const [name, src] of imgs) { imgList[name] = src }
loadingError(new Error("Could not load all images"));
}, {once, true)
);
}
});
}
渲染
最好创建函数来完成重复的任务。您正在重复的一项任务是遮罩,以下函数使用 canvas 作为目标、图像和遮罩
function maskImage(ctx, img, mask, x = 0, y = 0, w = ctx.canvas.height, h = ctx.canvas.width, clear = true) {
ctx.globalCompositeOperation = "source-over";
clear && ctx.clearRect(0, 0, ctx.canvas.height, ctx.canvas.width);
ctx.drawImage(img, x, y, w, h);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(mask, 0, 0, w, h);
return ctx.canvas; // return the renderable image
}
一旦你设置了一些实用程序来帮助协调加载和渲染,你就可以合成你的最终结果
// assumes ctx is the context to render to
loadImages(images, {ctx}).then(({imgs, {ctx}} => {
const w = ctx.canvas.width, h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const layer = copyImage(ctx.canvas);
ctx.drawImage(maskImage(layer.ctx, imgs.imgA, imgs.maskA), 0, 0, w, h);
ctx.drawImage(maskImage(layer.ctx, imgs.imgB, imgs.maskB), 0, 0, w, h);
// if you no longer need the images always remove them from memory to avoid hogging
// client's resources.
imgs = {}; // de-reference images so that GC can clean up.
}
您现在可以根据需要对任意数量的蒙版图像进行分层。由于为每个子任务创建的函数,因此在本项目和未来的项目中都可以轻松创建更复杂的渲染,而无需编写冗长和重复的代码。