如何在画布上绘制“剪切”蒙版区域,然后仅在有蒙版区域的地方绘制内容

时间:2018-10-06 20:17:38

标签: javascript html5-canvas

在尝试处理特定的画布绘制案例时,我发现一些奇怪的行为。我可以选择要绘制的区域(“遮罩区域”),定义为任意多边形。然后我想绘制一些图像/形状/等,裁剪后仅绘制到遮罩区域中。

我认为我可以通过以下方式实现这一目标

  • 使用(0, 0, 0, 0)(使用方便的clearRect方法)填充画布
  • (1, 1, 1, 1)填充遮罩区域
  • context的{​​{1}}设置为globalCompositeOperation
  • 绘制内容

(对于熟悉的人,我实际上是在尝试完成与GIMP的“图层蒙版”工具(用全白色填充)类似的东西。)

我知道我可以使用画布裁剪来处理这种特殊情况,但是如果没有画布,我希望使用它的任务将变得更加简单,我认为使用乘法来实现它应该是可能的。无论如何,使用"multiply"来实现类似目标的相反效果(先绘制内容,然后将像素消隐)。

代码如下:

globalCompositeOperation = "destination-out"

这是我的高质量<html> <head><style> html { width: 100%; height: 100%; } body { margin: 0; width: 100%; height: 100%; background: #777; } canvas { border: 1px solid black; margin: 10px auto; display: block; } </style></head> <body><canvas id="can" width="1800" height="900"></canvas></body> <script> const img = new Image; img.onload = () => requestAnimationFrame(drawFrame); img.src = "square.png"; const canvas = document.getElementById("can"); const ctx = canvas.getContext("2d"); let then = 0; function drawFrame (now) { const deltaTime = now - then; then = now; // fill canvas with 0 in all channels ctx.clearRect(0, 0, canvas.width, canvas.height); // draw FPS ctx.fillStyle = "black"; ctx.font = "16px serif"; ctx.fillText(`FPS: ${Math.round(1 / (deltaTime / 1000))}`, 0, 16); // fill mask area with 1 in all channels ctx.fillStyle = "#01010101"; ctx.fillRect(100, 100, 400, 400); // further draws should multiply current canvas values ctx.globalCompositeOperation = "multiply"; // draw image (100*100 resolution image; half the image should be visible) // Each channel of each pixel **should** multiply together // "0"'d regions: 0 in all channels // "1"'d regions: 1 * imgChannelValue = imgChannelValue ctx.drawImage(img, 50, 100); // draw a box, which should have its top-left corner blanked ctx.strokeStyle = "#ff0000"; ctx.strokeRect(300, 300, 500, 300); // (reset compositing) ctx.globalCompositeOperation = "source-over"; requestAnimationFrame(drawFrame); } </script> </html> 测试图片:

image

1 个答案:

答案 0 :(得分:0)

您想要的是合成,所以不要尝试使用混合。

您似乎想要的合成模式是"source-in",它将仅在已绘制目标的位置保留新内容。

因此,您要做的就是在蒙版区域上完全不透明地绘制,然后在绘制图像之前将gCO切换为"source-in"

const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = draw;
img.src = "https://i.stack.imgur.com/TFJu0.png";

function draw() {
  canvas.width = img.width;
  canvas.height = img.height;
  // draw the masking areas with full opacity
  drawShapes();
  // in a timeout just for demo
  setTimeout(()=> {
    ctx.globalCompositeOperation = 'source-in';
    ctx.drawImage(img, 0,0);
    // reset to default
    ctx.globalCompositeOperation = 'source-over';
  }, 1000)
}

function drawShapes() {
  // draw some 'mask' shapes
  ctx.beginPath();
  ctx.moveTo(130,20);
  ctx.arc(100, 20, 30, 0, Math.PI*2);
  ctx.moveTo(10, 45);
  ctx.lineTo(60, 95);
  ctx.lineTo(8, 120);
  ctx.fill();
  ctx.fillRect(40, 0, 20, 20);
}
<canvas id="canvas"></canvas>

但这需要将您的“可见”图形合并为一个图形调用(可以通过首先在屏幕外的画布上进行绘制来实现)。

因此,如果需要,您可以采用另一种方法:首先使用source-over绘制所有“可见”图形,然后使用{{1}作为单个子路径绘制“遮罩区域” }合成模式:

destination-in
const ctx = canvas.getContext("2d");
const urls = ["https://i.stack.imgur.com/TFJu0.png", "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"];

loadImages(urls).then(imgs => {
  // draw 'visibles'
  imgs.forEach((img) => ctx.drawImage(img, 0, 0, canvas.width, canvas.height));
  // Now do the compositing
  ctx.globalCompositeOperation = 'destination-in';
  drawMasks();
  ctx.globalCompositeOperation = 'source-over';

});

function drawMasks() {
  // draw your masks as a single sub-path
  ctx.beginPath();
  ctx.moveTo(130, 30);
  ctx.arc(100, 30, 30, 0, Math.PI * 2);
  ctx.moveTo(30, 45);
  ctx.lineTo(90, 95);
  ctx.lineTo(38, 120);
  ctx.rect(40, 0, 20, 20);
  ctx.fill();
}

function loadImages(urls) {
  return Promise.all(urls.map(u => new Promise((res, rej) => Object.assign(
    (new Image()), {
      src: u,
      onload: e => res(e.target),
      onerror: rej
    }))))
}

再一次,如果您不能将所有“遮盖区域”合并为一个子路径,则可以简单地将它们合并,然后再放在屏幕外的画布上,然后在合成步骤中绘制此屏幕外的画布。