将画布的一部分剪切到另一画布上

发布于 2025-01-13 03:36:57 字数 1719 浏览 3 评论 0原文

所以我有这段代码,用于擦除和恢复具有(例如)已删除背景的图像的部分内容。从主画布上擦除很简单,用户可以擦除点之间有一条线的圆形形状。

if(removeMode) {
    ctxs[index].globalCompositeOperation = 'destination-out';
    ctxs[index].beginPath();
    ctxs[index].arc(x, y, radius, 0, 2 * Math.PI);
    ctxs[index].fill();

    ctxs[index].lineWidth = 2 * radius;
    ctxs[index].beginPath();
    ctxs[index].moveTo(old.x, old.y);
    ctxs[index].lineTo(x, y);
    ctxs[index].stroke();
}

问题出在恢复上。目前,我可以使用 getImageData()putImageData() 函数将原始图像的部分内容复制到主画布,但仅限于矩形形状。

ctxs[index].globalCompositeOperation = 'source-out';
ctxs[0].putImageData(ctxs[1].getImageData(x-radius, y-radius, 2*radius, 2*radius), x-radius, y-radius);

理想情况下,我想将原始图像画布的一部分剪辑到主画布上,其形状类似于擦除功能。我已经尝试过 clip() 函数,但老实说我不知道​​如何去做。这是我最初尝试剪辑画布的一部分的内容。

ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, Math.PI * 2);
ctxs[index].fill();

ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();

ctxs[index].clip();

如何将自定义形状从一个画布复制到另一个画布? 提前致谢,

编辑:

我还考虑过使用掩码来创建掩码(例如在 python 中使用 numpy):

Y, X = np.ogrid[:canvas_height, :canvas_width]
# Y, X are matrix values and x, y are coordinates of the cursor within the image
center_dist = np.sqrt((X - x)**2 + (Y-y)**2)

# create mask
mask = center_dist <= radius

# omit everything except circular shape from mask
circular_img = original_img.copy()
circular_img[~mask] = 0 

# combine images
new_img = np.maximum(original_img, new_img)

我现在拥有的示例

So I have this piece of code that I use for erasing and restoring parts of an image with a (for example) removed background. Erasing from the main canvas is simple and the user can erase a circular shape with a line between points.

if(removeMode) {
    ctxs[index].globalCompositeOperation = 'destination-out';
    ctxs[index].beginPath();
    ctxs[index].arc(x, y, radius, 0, 2 * Math.PI);
    ctxs[index].fill();

    ctxs[index].lineWidth = 2 * radius;
    ctxs[index].beginPath();
    ctxs[index].moveTo(old.x, old.y);
    ctxs[index].lineTo(x, y);
    ctxs[index].stroke();
}

The problem is with the restoring. Currently I am able to copy parts of the original image to the main canvas but only in a rectangular shape using the getImageData() and putImageData() functions.

ctxs[index].globalCompositeOperation = 'source-out';
ctxs[0].putImageData(ctxs[1].getImageData(x-radius, y-radius, 2*radius, 2*radius), x-radius, y-radius);

Ideally I would like to clip a part of the original image canvas to the main canvas with a shape similar to the erasing feature. I have tried the clip() function but honestly I am not sure how to go about it. Here is what I initially tried to clip a part of a canvas.

ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, Math.PI * 2);
ctxs[index].fill();

ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();

ctxs[index].clip();

How do I copy a custom shape from a canvas to another canvas?
Thanks in advance,

Edit:

I have also thought of using a mask where I would create the mask as such (example using numpy in python):

Y, X = np.ogrid[:canvas_height, :canvas_width]
# Y, X are matrix values and x, y are coordinates of the cursor within the image
center_dist = np.sqrt((X - x)**2 + (Y-y)**2)

# create mask
mask = center_dist <= radius

# omit everything except circular shape from mask
circular_img = original_img.copy()
circular_img[~mask] = 0 

# combine images
new_img = np.maximum(original_img, new_img)

Example of what I have now

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

傾城如夢未必闌珊 2025-01-20 03:36:58

继续使用复合运算。

“destination-out”确实会删除与新像素重叠的先前像素。

如果您使用反“destination-in”,则仅保留与新像素重叠的先前像素。

因此,您可以完整地存储原始图像,然后根据您想要执行的操作,使用其中一种模式来渲染它。
在这里,因为我们似乎处于类似绘画的配置中,所以我想擦除最终结果并恢复原始画布更有意义。为此,我们需要第三个画布,将其分离,在将“恢复”部分绘制回可见画布之前,我们将在其中单独绘制“恢复”部分:

(async () => {
  // the main, visible canvas
  const canvas = document.querySelector("canvas");
  canvas.width = 500;
  canvas.height = 250;
  const ctx = canvas.getContext("2d");
  // a detached canvas context to do the compositing
  const detached = canvas.cloneNode().getContext("2d");
  // the "source" canvas (here just an ImageBitmap)
  const originalCanvas = await loadImage();
  
  ctx.lineWidth = detached.lineWidth = 8;
  
  // we store every drawing in its own Path2D object
  const paths = [];
  let down = false;
  const checkbox = document.querySelector("input");
  canvas.onmousedown = (evt) => {
    down = true;
    const newPath = new Path2D();
    newPath.isEraser = !checkbox.checked;
    paths.push(newPath);
  };
  canvas.onmouseup = (evt) => { down = false; };
  canvas.onmousemove = (evt) => {
    if (!down) { return; }
    const {x, y} = parseMouseEvent(evt);
    paths[paths.length - 1].lineTo(x, y);
    redraw();
  };
  redraw();
  function redraw() {
    // clear the visible context
    ctx.globalCompositeOperation = "source-over";
    ctx.drawImage(originalCanvas, 0, 0);
    paths.forEach((path) => {
      if (path.isEraser) {
        // erase the current content
        ctx.globalCompositeOperation = "destination-out";
        ctx.stroke(path);
      }
      else {
        // to restore
        // we do the compositing on the detached canvas
        detached.globalCompositeOperation = "source-over";
        detached.drawImage(originalCanvas, 0, 0);
        detached.globalCompositeOperation = "destination-in";
        detached.stroke(path);
        // draw the result on the main context
        ctx.globalCompositeOperation = "source-over";
        ctx.drawImage(detached.canvas, 0, 0);
      }
    });
  }
})().catch(console.error);

async function loadImage() {
  const url = "https://picsum.photos/500/250";
  const req = await fetch(url);
  const blob = req.ok && await req.blob();
  return createImageBitmap(blob);
}
function parseMouseEvent(evt) {
  const rect = evt.target.getBoundingClientRect();
  return {x: evt.clientX - rect.left, y: evt.clientY - rect.top };
}
canvas { border: 1px solid; vertical-align: top }
<label>erase/restore <input type="checkbox"></label>
<canvas></canvas>

请注意,在这里我每次都会创建新路径,但您可以很好地使用相同的路径来擦除和恢复(甚至任何其他图形源)。

Keep using composite operations.

"destination-out" will indeed remove the previous pixels that do overlap with the new ones.

If you use the inverse "destination-in", only the previous pixels that do overlap with the new ones are kept.

So you store your original image intact, and then use one of these modes to render it given the action you want to perform.
Here since it seems we are in a paint-like configuration, I guess it makes more sense to erase the final result and restore the original canvas. For this we need a third canvas, detached where we'll draw the "restoration" part on its own before drawing that back to the visible canvas:

(async () => {
  // the main, visible canvas
  const canvas = document.querySelector("canvas");
  canvas.width = 500;
  canvas.height = 250;
  const ctx = canvas.getContext("2d");
  // a detached canvas context to do the compositing
  const detached = canvas.cloneNode().getContext("2d");
  // the "source" canvas (here just an ImageBitmap)
  const originalCanvas = await loadImage();
  
  ctx.lineWidth = detached.lineWidth = 8;
  
  // we store every drawing in its own Path2D object
  const paths = [];
  let down = false;
  const checkbox = document.querySelector("input");
  canvas.onmousedown = (evt) => {
    down = true;
    const newPath = new Path2D();
    newPath.isEraser = !checkbox.checked;
    paths.push(newPath);
  };
  canvas.onmouseup = (evt) => { down = false; };
  canvas.onmousemove = (evt) => {
    if (!down) { return; }
    const {x, y} = parseMouseEvent(evt);
    paths[paths.length - 1].lineTo(x, y);
    redraw();
  };
  redraw();
  function redraw() {
    // clear the visible context
    ctx.globalCompositeOperation = "source-over";
    ctx.drawImage(originalCanvas, 0, 0);
    paths.forEach((path) => {
      if (path.isEraser) {
        // erase the current content
        ctx.globalCompositeOperation = "destination-out";
        ctx.stroke(path);
      }
      else {
        // to restore
        // we do the compositing on the detached canvas
        detached.globalCompositeOperation = "source-over";
        detached.drawImage(originalCanvas, 0, 0);
        detached.globalCompositeOperation = "destination-in";
        detached.stroke(path);
        // draw the result on the main context
        ctx.globalCompositeOperation = "source-over";
        ctx.drawImage(detached.canvas, 0, 0);
      }
    });
  }
})().catch(console.error);

async function loadImage() {
  const url = "https://picsum.photos/500/250";
  const req = await fetch(url);
  const blob = req.ok && await req.blob();
  return createImageBitmap(blob);
}
function parseMouseEvent(evt) {
  const rect = evt.target.getBoundingClientRect();
  return {x: evt.clientX - rect.left, y: evt.clientY - rect.top };
}
canvas { border: 1px solid; vertical-align: top }
<label>erase/restore <input type="checkbox"></label>
<canvas></canvas>

Note that here I do create new paths every time, but you could very well use the same ones for both erasing and restoring (and even any other graphic source).

尛丟丟 2025-01-20 03:36:57

更简单的解决方案

每个形状都适合一个矩形。

证明 您的画布是一个矩形并且已经包含该形状。

因此,您可以确定包含完整形状的最小可能矩形并将其存储。它必然包含您的形状。重新加载后,您将需要知道副本内形状的边界,因此也需要这些信息。

更难,但更精确的解决方案

您可以创建一个结构并逐点存储内容(但是,这性能不是很好):

const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
let myShape = [];
for (let x = 0; x < canvas.width; x++) {
    for (let y = 0; y < canvas.height; y++) {
        if (inShape(x, y, canvas)) {
            myShape.push({x, y, content: data});
        }
    }
}

上面的代码片段假设您已经正确实现了inShape

均匀形状

如果形状内的所有点都相似,那么您只需要知道形状的边界在哪里。例如,如果您有一个凸多边形,那么您需要知道它的中心在哪里以及边界是什么。如果你有一个实心圆,那么你只需要它的圆心和半径。您需要的几何数据很大程度上取决于您的形状。

Simpler solution

Every shape fits into a rectangle.

Proof Your canvas is a rectangle and already contains the shape.

As a result, you can determine the smallest possible rectangle that contains the full shape and store that. It will necessarily contain your shape. Upon reload you will need to know the shape's boundaries inside the copy though, so that info will also be needed.

Harder, but more precise solution

You can create a structure and store the content, point-by-point (yet, this will be not very performant):

const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
let myShape = [];
for (let x = 0; x < canvas.width; x++) {
    for (let y = 0; y < canvas.height; y++) {
        if (inShape(x, y, canvas)) {
            myShape.push({x, y, content: data});
        }
    }
}

The snippet above assumes that you have properly implemented inShape.

Homogeneous shape

If all the points inside the shape are similar, then you will need to only know where the boundaries of the shape were. If you have a convex polygon, for example, then you will need to know where its center is and what the boundaries are. If you have a filled circle, then you will only need its center and radius. The geometrical data you need largely depend on what shape you have.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文