javascriptcanvasglobalcompositeoperation

How do I clip / use globalCompositeOperation = "desination-out" to exclude 2 circles from my Canvas?


I am trying to figure out CanvasRenderingContext2D.globalCompositeOperation = "destination-out in order to create a "Fog of War" effect.

Given this example

https://jsfiddle.net/wobzpjLL/

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

// a green background box
ctx.fillStyle = "green";
ctx.fillRect(0,0,300,150); 

// a line
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(300, 150);
ctx.stroke();

// Fog of war
ctx.fillStyle = "black";
ctx.fillRect(0,0,300,150); 

// Here I want two circles that shows the background (green & line)

// text
ctx.fillStyle = "red";
ctx.font = "30px Arial";
ctx.fillText("Hello World",10,50); 

How do I create two circles that excludes the black overlay, so that the user can see the green background and the line, within those two circles, black everywhere else (hence creating a fog of war effect)?


Solution

  • Using masks

    Fog of war using compositing

    The easiest way to achieve the masking is by creating a offscreen canvas and render the mask onto that. Then when animating you render the world and then apply the mask with "destination-in". You can use the canvas CSS background as the fog (as done in demo) or you can render the fog over the top with "destination-over" also shown in demo using the text.

    Layered rendering

    Personally and for performance (if code is needing every break it can get) I would avoid globalCompositeOperation and render the fog over the world by creating the inverse of the mask in the demo. This adds only the one extra render call and no need to change composite operations

    Example using composition

    var c = document.getElementById("myCanvas");
    var ctx = c.getContext("2d");
    
    function createCanvas(){
      var c = document.createElement("canvas");
      c.width = 300;
      c.height = 150;
      c.ctx = c.getContext("2d");
      return c;
    }
    
    // draw game world on offscreen canvas
    function drawBackground(ctx){
      ctx.fillStyle = "green";
      ctx.fillRect(0,0,300,150); 
    
      // a line
      ctx.beginPath();
      ctx.lineWidth = 3;
      ctx.strokeStyle = "#08A";
      ctx.lineTo(0, 0);
      ctx.lineTo(300, 150);
      ctx.stroke();
    }
    
    // create and render world
    var bGround = createCanvas();
    drawBackground(bGround.ctx)
    
    
    // create mask 
    var grad = ctx.createRadialGradient(0,0,60,0,0,0);
    grad.addColorStop(0,"rgba(0,0,0,0.0)");
    grad.addColorStop(0.2,"rgba(0,0,0,1)");
    grad.addColorStop(1,"rgba(0,0,0,1)");
    
    var mask = createCanvas();
    mask.ctx.setTransform(1,0,0,1,75,75);
    mask.ctx.fillStyle = grad;
    mask.ctx.beginPath();
    mask.ctx.arc(0,0,60,0,Math.PI*2);
    mask.ctx.fill();
    mask.ctx.setTransform(1,0,0,1,225,75);
    mask.ctx.beginPath();
    mask.ctx.arc(0,0,60,0,Math.PI*2);
    mask.ctx.fill();
    document.body.appendChild(mask);
    
    // draws the fog
    function drawFog(ctx,x){
      ctx.globalCompositeOperation = "destination-in";
      ctx.drawImage(mask, x, 0);
      ctx.globalCompositeOperation = "source-over";
    }
    
    ctx.fillStyle = "red";
    ctx.font = "30px Arial";
    function loop(time){
      ctx.drawImage(bGround,0,0);
      drawFog(ctx,Math.sin(time / 1000) *20);
      ctx.globalCompositeOperation = "destination-over";
      ctx.fillText("Hello World",10,50); 
      ctx.globalCompositeOperation = "source-over";
      requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
    canvas {
      border:1px solid #d3d3d3;
      
    }
    <canvas id="myCanvas" width="300" height="150" style="background : black;" ></canvas>
    <div>Pre rendered offscreen mask shown below for display only.</div>