Julius Dzidzevičius
Julius Dzidzevičius

Reputation: 11000

HTML5 Canvas - cant apply source-atop on mask

Sorry I am new to Canvas and dont know how to google this out. Problem is that I cant draw on mask if previous layer (night sky) is present.

Here are the two snippets:

const canvas = document.querySelector('#board canvas');
    const ctx = canvas.getContext('2d');
    const { width: w, height: h } = canvas;

    // first layer

    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, w, h);
    ctx.fillStyle = '#555';

    let x, y, radius;
    for (let i = 0; i < 550; i++) {
      x = Math.random() * w;
      y = Math.random() * h;
      radius = Math.random() * 3;
      ctx.beginPath();
      ctx.arc(x, y, radius, 0, Math.PI * 2, false);
      ctx.fill();
    }
    
    // destination

    ctx.font = 'bold 70pt monospace';
    ctx.fillStyle = 'black';
    ctx.fillText('FOO', 10, 60);
    ctx.fillText('BAR', 10, 118);
    ctx.fill();
    
    // source 

    ctx.globalCompositeOperation = 'source-atop';

    for (let i = 0; i < 6; i++) {
      ctx.fillStyle = `hsl(${i * (250 / 6)}, 90%, 55%)`;
      ctx.fillRect(0, i * 20, 200, 20);
    }
<div id="board">
  <canvas width="640" height="480"></canvas>

</div>

EXPECTED RESULT (but with the first layer - night sky):

const canvas = document.querySelector('#board canvas');
    const ctx = canvas.getContext('2d');
    const { width: w, height: h } = canvas;
    
    // destination

    ctx.font = 'bold 70pt monospace';
    ctx.fillStyle = 'black';
    ctx.fillText('FOO', 10, 60);
    ctx.fillText('BAR', 10, 118);
    ctx.fill();
    
    // source 

    ctx.globalCompositeOperation = 'source-atop';

    for (let i = 0; i < 6; i++) {
      ctx.fillStyle = `hsl(${i * (250 / 6)}, 90%, 55%)`;
      ctx.fillRect(0, i * 20, 200, 20);
    }
<div id="board">
  <canvas width="640" height="480"></canvas>
</div>

Upvotes: 0

Views: 453

Answers (1)

Kaiido
Kaiido

Reputation: 137006

  • Compositing will affect the whole context.
  • source-atop mode will draw only where there were existing pixels (i.e only where alpha > 0).
  • When you draw your background, all the pixels of your context have alpha values set to 1.

This means that source-atop will not produce anything on your fully opaque image.

Once you understand these points, it's clear that you need to make your compositing alone.
It could be e.g on a different off-screen canvas that you would then draw back on the main canvas with ctx.drawImage(canvas, x, y).

const canvas = document.querySelector('#board canvas');
const ctx = canvas.getContext('2d');
const {
  width: w,
  height: h
} = canvas;

// background
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = '#555';

let x, y, radius;
for (let i = 0; i < 550; i++) {
  x = Math.random() * w;
  y = Math.random() * h;
  radius = Math.random() * 3;
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2, false);
  ctx.fill();
}


// text compositing on an off-screen context
const ctx2 = Object.assign(document.createElement('canvas'), {
    width: 200,
    height: 120
  }).getContext('2d');
// text
ctx2.font = 'bold 70pt monospace';
ctx2.fillStyle = 'black';
ctx2.fillText('FOO', 10, 60);
ctx2.fillText('BAR', 10, 118);

ctx2.globalCompositeOperation = 'source-atop';
// rainbow
for (let i = 0; i < 6; i++) {
  ctx2.fillStyle = `hsl(${i * (250 / 6)}, 90%, 55%)`;
  ctx2.fillRect(0, i * 20, 200, 20);
}
// now draw our off-screen canvas on the main one
ctx.drawImage(ctx2.canvas, 0, 0);
<div id="board">
  <canvas width="640" height="480"></canvas>

</div>

Or, since this is the only compositing in your composition, you can also do it all on the same, but use an other compositing mode: destination-over.
This mode will draw behind the existing content, this means that you will have to actually draw your background after you made the compositing.

const canvas = document.querySelector('#board canvas');
const ctx = canvas.getContext('2d');
const {
  width: w,
  height: h
} = canvas;
//
// text compositing on a clear context
drawText();
// will draw only where the text has been drawn
ctx.globalCompositeOperation = 'source-atop';
drawRainbow();
// from here we will draw behind
ctx.globalCompositeOperation = 'destination-over';
// so we need to first draw the stars, otherwise they'll be behind
drawStars();
//And finally the sky black background
drawSky();

//... reset
ctx.globalCompositeOperation = 'source-over';

function drawSky() {
  ctx.fillStyle = 'black';
  ctx.fillRect(0, 0, w, h);
}

function drawStars() {
  ctx.fillStyle = '#555';
  let x, y, radius;
  for (let i = 0; i < 550; i++) {
    x = Math.random() * w;
    y = Math.random() * h;
    radius = Math.random() * 3;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2, false);
    ctx.fill();
  }
}

function drawText()  {
  ctx.font = 'bold 70pt monospace';
  ctx.fillStyle = 'black';
  ctx.fillText('FOO', 10, 60);
  ctx.fillText('BAR', 10, 118);
}

function drawRainbow() {
  for (let i = 0; i < 6; i++) {
    ctx.fillStyle = `hsl(${i * (250 / 6)}, 90%, 55%)`;
    ctx.fillRect(0, i * 20, 200, 20);
  }
}
<div id="board">
  <canvas width="640" height="480"></canvas>
</div>

Upvotes: 2

Related Questions