使用canvas,如何将带有插入阴影的孔切割成图像?

Using canvas, how to cut holes with inset shadows into an image?

使用 canvas,我试图将一系列圆孔切割成图像。此外,每个孔都应该有一个嵌入的投影,使图像看起来好像稍微悬停在背景上。

这是我设法做到的:

  1. 使用 globalCompositeOperation = "destination-top"
  2. 在图像中打孔
  3. 从背景形状中切出一个孔,使用“反向缠绕”(先逆时针,然后顺时针)

然而,由于以下两个原因,第二次尝试对于 图像 上的 多个 孔似乎不可行:

这是我目前拥有的:

let cvs = document.querySelector("canvas");
let ctx = cvs.getContext("2d");
let img = new Image();
img.addEventListener("load", function() {
  ctx.drawImage(img, 0, 0, 256, 256);
  ctx.globalCompositeOperation = "destination-out";
  hole(ctx, 64, 64, 32);
  hole(ctx, 192, 64, 32);
  hole(ctx, 64, 192, 32);
  hole(ctx, 192, 192, 32);
});
img.src = "https://placeimg.com/256/256/nature";

function hole(ctx, x, y, r, ccw=false) {
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2, ccw);
    ctx.closePath();
    ctx.fill();
}
<canvas width="256" height="256"></canvas>

现在如何向孔中添加嵌入阴影?

你可以在每个孔后面放一个 div 并在其中使用插入阴影来获得合适的效果(虽然这个片段中的简单阴影让它看起来更像是一个突出的按钮而不是我意识到一个洞)

let cvs = document.querySelector("canvas");
let ctx = cvs.getContext("2d");
let img = new Image();
img.addEventListener("load", function() {
  ctx.drawImage(img, 0, 0, 256, 256);
  ctx.globalCompositeOperation = "destination-out";
  hole(ctx, 64, 64, 32);
  hole(ctx, 192, 64, 32);
  hole(ctx, 64, 192, 32);
  hole(ctx, 192, 192, 32);
});
img.src = "https://placeimg.com/256/256/nature";

function hole(ctx, x, y, r, ccw=false) {
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2, ccw);
    ctx.closePath();
    ctx.fill();
}
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}
.shadow {
  position: absolute;
  box-shadow: 0px 20px 10px gray inset;
  width: 70px;
  height: 70px;
  border-style: solid;
  border-radius: 50%;
  border-color: transparent;
  clear: both;
}
div.shadow:nth-of-type(1) {
  top: 29px;
  left: 29px;
}
div.shadow:nth-of-type(2) {
  top: 157px;
  left: 29px;
}
div.shadow:nth-of-type(3) {
  top: 29px;
  left: 157px;
}
div.shadow:nth-of-type(4) {
  top: 157px;
  left: 157px;
}
<div class="shadow"></div>
<div class="shadow"></div>
<div class="shadow"></div>
<div class="shadow"></div>
<canvas width="256" height="256"></canvas>

我建议您使用第二个 canvas 来帮助创建您想要的效果,以及 shadowOffset 和 shadowColor。

let cvs = document.querySelector("canvas");
let ctx = cvs.getContext("2d");
let img = new Image();

img.addEventListener("load", function() {
  let cvs2 = document.createElement('canvas');
  cvs2.width = 256;
  cvs2.height = 256;
  ctx2 = cvs2.getContext("2d");

  hole(ctx2, 64, 64, 32);
  hole(ctx2, 192, 64, 32);
  hole(ctx2, 64, 192, 32);
  hole(ctx2, 192, 192, 32);

  ctx2.globalCompositeOperation = "source-out";
  ctx2.drawImage(img, 0, 0, 256, 256);

  ctx.shadowOffsetX = 4;
  ctx.shadowOffsetY = 4;
  ctx.shadowBlur = 8;
  ctx.shadowColor = 'black';
  ctx.drawImage(cvs2, 0, 0, 256, 256);
});
img.src = "https://placeimg.com/256/256/nature";

function hole(ctx, x, y, r, ccw = false) {
  ctx.beginPath();
  ctx.arc(x, y, r, 0, Math.PI * 2, ccw);
  ctx.closePath();
  ctx.fill();
}
<canvas width="256" height="256"></canvas>

我刚刚自己找到了一种可能的方法,但我认为它比需要的更复杂:

  1. 像我已经做的那样,将 globalCompositeOperation 设置为 destination-out 来画洞;这会将它们从图像中剪掉,创建显示背景的文字孔
  2. 设置适当的 shadow*lineWidth 值;后者会影响阴影强度,其中较粗的线会产生更强烈的阴影
  3. 使用 stroke(),在 globalCompositeOperation 设置为 destination-over 的情况下,为每个孔绘制一个额外的圆圈,这将在 后面绘制 现有内容;为防止实际笔划可见,请将其 lineWidth 添加到用于孔的半径

let cvs = document.querySelector("canvas");
let ctx = cvs.getContext("2d");
let img = new Image();
img.addEventListener("load", function() {
    ctx.drawImage(img, 0, 0, 256, 256);
    ctx.globalCompositeOperation = "destination-out";
    hole(ctx, 64, 64, 32);
    hole(ctx, 192, 64, 32);
    hole(ctx, 64, 192, 32);
    hole(ctx, 192, 192, 32);
});
img.src = "https://placeimg.com/256/256/nature";

function hole(ctx, x, y, r) {
    let lw = r * 0.2;
    ctx.save();
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.closePath();
    ctx.fill();
    ctx.globalCompositeOperation = "destination-over";
    ctx.lineWidth = lw;
    ctx.strokeStyle = "black";
    ctx.shadowColor = "black";
    ctx.shadowOffsetX = r * 0.1;
    ctx.shadowOffsetY = r * 0.1;
    ctx.shadowBlur = r * 0.2;
    ctx.beginPath();
    ctx.arc(x, y, r + lw, 0, Math.PI * 2);
    ctx.closePath();
    ctx.stroke();
    ctx.restore();
}
<canvas width="256" height="256"></canvas>

您也可以用单个 canvas 来完成,方法是在自身上绘制 canvas,并为阴影设置一个巨大的偏移量。
有了这个巨大的偏移量,我们就可以在其可见矩形之外实际绘制 canvas,但仍保留阴影。

let cvs = document.querySelector("canvas");
let ctx = cvs.getContext("2d");
let img = new Image();
img.addEventListener("load", function() {
  ctx.drawImage(img, 0, 0, 256, 256);
  ctx.globalCompositeOperation = "destination-out";
  hole(ctx, 64, 64, 32);
  hole(ctx, 192, 64, 32);
  hole(ctx, 64, 192, 32);
  hole(ctx, 192, 192, 32);
  ctx.fill(); // single filling is always preferable
  
  // offset by the size of the canvas + the actual offset wanted
  ctx.shadowOffsetX = cvs.width + 4;
  ctx.shadowOffsetY = cvs.height + 4;
  ctx.shadowBlur = 4;
  ctx.shadowColor = "black";
  ctx.globalCompositeOperation = "destination-over";
  // draw at the inverse offset (so the shadow is at the correct position)
  ctx.drawImage( cvs, -cvs.width, -cvs.height );
});
img.src = "https://placeimg.com/256/256/nature";

function hole(ctx, x, y, r, ccw=false) {
    ctx.arc(x, y, r, 0, Math.PI * 2, ccw);
    ctx.closePath();
}
<canvas width="256" height="256"></canvas>