【问题标题】:Multiple masked images on top of each other using HTML canvas使用 HTML 画布将多个蒙版图像叠加在一起
【发布时间】:2021-04-01 21:39:05
【问题描述】:

我对画布还很陌生,以前没有使用过它,但我认为它非常适合以下任务。在处理它时我有疑问,我仍然不知道这个任务是否可以使用画布来实现。

Exemplary graphic of the masks and images and the result that I want to achieve (and the actual results that I got).

  • 轮廓只是为了更好地说明图像 尺寸。
  • 掩码是 SVG 图像,之前使用 Promise 预加载 它们被绘制并且每次迭代都会改变。所以在第一 迭代它是图像 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 更改为不同的值 - 但我没有想法。

有什么方法可以实现我想要的输出,如图形中所述,还是我应该放弃画布来完成这项任务?

【问题讨论】:

  • 另一种方法可能是提取 SVG 路径定义并使用它来构造一个 path2d 对象,然后您可以将其标记到画布中。可以通过将图像用作每个路径的填充样式的模式来添加图像。或者,如果您知道 SVG 轮廓始终是半球,只需使用弧线创建路径并使用适当的图像作为图案填充

标签: javascript canvas html5-canvas


【解决方案1】:

不幸的是,您想要实现的目标并不容易 - 至少如果您使用的是被视为图像并直接绘制到画布上的 SVG。

假设我们有以下 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);

我们只会看到一个空的画布。为什么?我们将 globalCompositeOperation 设置为 'source-in' 并且前一个画布和第二个蒙版 (maskB) 没有任何重叠区域。这意味着我们正在有效地擦除画布。

如果我们尝试补偿并保存/恢复上下文或将 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 和图像均已完全加载
  • 创建一个与目标画布大小相同的新空画布
  • 在新画布上绘制第一个蒙版
  • 将其 globalCompositeOperation 设置为“source-in”
  • 在新画布上绘制第一张图片
  • 将新画布绘制到目标画布
  • 擦除新画布并重复前面的步骤以合成最终图像

这是一个示例(只需单击“运行代码 sn-p”):

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">

【讨论】:

  • 我欣赏全面的示例和解释。这最终解决了我的问题:)
【解决方案2】:

画布可以是图层

与任何元素一样,画布很容易创建并且可以像图像一样处理,或者如果您熟悉 Photoshop,画布可以是图层。

创建一个空白画布

// Returns the renderable image (canvas)
function CreateImage(width, height) {
    return Object.assign(document.createElement("canvas"), {width, height});
}

复制画布或图像之类的对象

// 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 函数返回一个承诺,该承诺将在所有图像加载后解决。

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)
            );
        }
    });
}

渲染

最好创建函数来执行重复任务。您正在重复的一项任务是遮罩,以下函数使用画布作为目标、图像和遮罩

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.

}

您现在可以根据需要对任意数量的蒙版图像进行分层。由于为每个子任务创建了函数,因此在本项目和未来项目中,无需编写冗长重复的代码即可轻松创建更复杂的渲染。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-03
    • 1970-01-01
    • 1970-01-01
    • 2015-07-09
    • 1970-01-01
    • 1970-01-01
    • 2013-12-09
    相关资源
    最近更新 更多