stroke/fill 的排序以创建混合重叠

Sequencing of stroke/fill to create a blended overlap

我有大量对象要在 canvas 上绘制。

对象的中心需要与标准 alpha 混合。

后面对象的边框(描边)需要出现以移除任何底层边框,同时保留完整的填充以进行混合。

作为代码片段的示例,尝试了几次失败 - 请注意 'desired' 结果是手动生成的。

解决方案也需要扩展,因为这是针对 requestAnimationFrame 的,将有数千个对象需要迭代,因此执行单独的 beginPath()/stroke() 组合不太可能可行。

var canvas = document.getElementById('canvas');
canvas.width = 600;
canvas.height = 600;
var ctx = canvas.getContext('2d');

//set up control objects
let objects = [{
    x: 20,
    y: 20,
    w: 60,
    h: 30,
    rgba: "rgba(255, 0,0,.5)"
}, {
    x: 40,
    y: 30,
    w: 60,
    h: 30,
    rgba: "rgba(0,255,0,.5)"
}]

//manually produce desired outcome
ctx.beginPath();
for (let i = 0, l = objects.length; i < l; i++) {
    let myObject = objects[i];
    ctx.fillStyle = myObject.rgba;
    ctx.fillRect(myObject.x, myObject.y, myObject.w, myObject.h);
}
ctx.beginPath();

ctx.moveTo(40, 50);
ctx.lineTo(20, 50);
ctx.lineTo(20, 20);
ctx.lineTo(80, 20);
ctx.lineTo(80, 30);
ctx.rect(40, 30, 60, 30);
ctx.stroke();

ctx.font = "15px Arial"
ctx.fillStyle = "black";
ctx.fillText("Desired outcome - (done manually for example)", 120, 50);

//Attempt one: fill on iterate, stroke on end
ctx.beginPath();
for (let i = 0, l = objects.length; i < l; i++) {
    let myObject = objects[i];
    ctx.rect(myObject.x, myObject.y + 70, myObject.w, myObject.h);
    ctx.fillStyle = myObject.rgba;
    ctx.fillRect(myObject.x, myObject.y + 70, myObject.w, myObject.h);
}
ctx.stroke();

ctx.fillStyle = "black";
ctx.fillText("Attempt #1: inner corner of red box fully visible", 120, 120);

//Attempt two: fill and stroke on iterate

for (let i = 0, l = objects.length; i < l; i++) {
    let myObject = objects[i];
    ctx.beginPath();
    ctx.rect(myObject.x, myObject.y + 140, myObject.w, myObject.h);
    ctx.fillStyle = myObject.rgba;
    ctx.fillRect(myObject.x, myObject.y + 140, myObject.w, myObject.h);
    ctx.stroke();
}
ctx.fillStyle = "black";
ctx.fillText("Attempt #2: inner corner of red box partly visible", 120, 170);
ctx.fillText("(This also scales very badly into thousands of strokes)", 120, 190);
<canvas name="canvas" id="canvas" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>

您可以通过两次绘制来实现此目的:

首先你将通过迭代

合成你的笔画

完成后,您的 canvas 将只剩下最后一笔。

现在您必须绘制填充,但由于我们希望笔触位于填充之前,我们必须使用其他合成模式:"destination-over" 并迭代我们的 rects 逆序排列:

(async () => {
var canvas = document.getElementById('canvas');
canvas.width = 600;
canvas.height = 600;

var ctx = canvas.getContext('2d');
ctx.scale(2,2)

//set up control objects
let objects = [{
    x: 20,
    y: 20,
    w: 60,
    h: 30,
    rgba: "rgba(255, 0,0,.5)"
}, {
    x: 40,
    y: 30,
    w: 60,
    h: 30,
    rgba: "rgba(0,255,0,.5)"
},
{
    x: 10,
    y: 5,
    w: 60,
    h: 30,
    rgba: "rgba(0,0,255,.5)"
}]


// first pass, composite the strokes
for (let i = 0, l = objects.length; i < l; i++) {
    let myObject = objects[i];
    ctx.beginPath();
    ctx.rect(myObject.x, myObject.y, myObject.w, myObject.h);
    // erase the previous strokes where our fill will be
    ctx.globalCompositeOperation = "destination-out";
    ctx.fillStyle = "#000"; // must be opaque
    ctx.fill();
    // draw our stroke
    ctx.globalCompositeOperation = "source-over";
    ctx.stroke();
}

await wait(1000);

// second pass, draw the colored fills
// we will draw from behind to keep the stroke at frontmost
// so we need to iterate our objects in reverse order
for (let i = objects.length- 1; i >= 0; i--) {
    let myObject = objects[i];
    // draw behind
    ctx.globalCompositeOperation = "destination-over";
    ctx.fillStyle = myObject.rgba;
    ctx.fillRect(myObject.x, myObject.y, myObject.w, myObject.h);
}

})();
function wait(ms){
  return new Promise(res => setTimeout(res, ms));
}
<canvas name="canvas" id="canvas" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>

当然,这也可以用在动画中:

var canvas = document.getElementById('canvas');
canvas.width = 100;
canvas.height = 100;

var ctx = canvas.getContext('2d');

//set up control objects
let objects = [{
    x: 20,
    y: 20,
    w: 60,
    h: 30,
    rgba: "rgba(255, 0,0,.5)"
}, {
    x: 40,
    y: 30,
    w: 60,
    h: 30,
    rgba: "rgba(0,255,0,.5)"
},
{
    x: 10,
    y: 5,
    w: 60,
    h: 30,
    rgba: "rgba(0,0,255,.5)"
}]
objects.forEach( rect => {
  rect.speedX = Math.random() * 2 - 1;
  rect.speedY = Math.random() * 2 - 1;
});

requestAnimationFrame(anim);
onclick = anim
function anim() {
  update();
  draw();
  requestAnimationFrame( anim );
}
function update() {
  objects.forEach( rect => {
    rect.x = rect.x + rect.speedX;
    rect.y = rect.y + rect.speedY;
    if(
      rect.x + rect.w > canvas.width ||
      rect.x < 0
    ) {
      rect.speedX *= -1;
    }
    if(
      rect.y + rect.h > canvas.height ||
      rect.y < 0
    ) {
      rect.speedY *= -1;
    }
  });
}
function draw() {

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // first pass, composite the strokes
  for (let i = 0, l = objects.length; i < l; i++) {
      let myObject = objects[i];
      ctx.beginPath();
      ctx.rect(myObject.x, myObject.y, myObject.w, myObject.h);
      // erase the previous strokes where our fill will be
      ctx.globalCompositeOperation = "destination-out";
      ctx.fillStyle = "#000"; // must be opaque
      ctx.fill();
      // draw our stroke
      ctx.globalCompositeOperation = "source-over";
      ctx.stroke();
  }

  // second pass, draw the colored fills
  // we will draw from behind to keep the stroke at frontmost
  // so we need to iterate our objects in reverse order
  for (let i = objects.length- 1; i >= 0; i--) {
      let myObject = objects[i];
      // draw behind
      ctx.globalCompositeOperation = "destination-over";
      ctx.fillStyle = myObject.rgba;
      ctx.fillRect(myObject.x, myObject.y, myObject.w, myObject.h);
  }
  ctx.globalCompositeOperation = "source-over";
}
<canvas name="canvas" id="canvas" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>