StackOverflow_17343502: Canvas element 'rubbing out' effect with JavaScript (improved version)

Illustration of answer (plus bitmap verification) to: http://stackoverflow.com/questions/17343502/cavas-element-rubbing-out-effect-with-javascript # changelog 2019-06-22 - removed requestAnimationFrame polyfill - code improvements (more realistic effects) - Added debug settings

by Gustavo Carvalho

HTML

<p id="msg">Click and Drag the mouse to clean the window</p>

<div id="container">
  <canvas id="inside" width=271 height=267></canvas>
  <canvas id="the-window" width=271 height=267></canvas>
  <canvas id="dirty" width=271 height=267></canvas>
</div>

<div id="debug-avg-color">
  Average color: <span id="avg-color"></span>
</div>

<button id="btn-reset">
Reset
</button>

<div id="settings">
  <strong>Settings:</strong>
  <input type="checkbox" id="cb-debug" name="cb-debug">
  <label for="cb-debug">Debug</label>
</div>

CSS

body {
  background-color: ivory;
}

#container {
  position: relative;
  width: 271px;
  height: 267px;
  border: 1px solid black;
}

#inside {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
}

#the-window {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 2;
}

#dirty {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 3;
  cursor: pointer;
}

#btn-reset {
  margin-top: 15px;
  width: 271px;
}

#hidden {
  position: absolute;
  left: 280px;
  top: 0;
}

#debug-avg-color {
  display: none;
  position: absolute;
  left: 290px;
  margin-top: 15px;
}

#settings {
  margin-top: 15px;
}

JavaScript

var debug = false; // set to true to show the hidden canvas used for pixel manipulations

var debugAvgColor = document.getElementById("debug-avg-color");
var gameOver = false;

var compositeOperation = "xor"; // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

// canvases container
var container = document.getElementById("container");

// bottom canvas
var bottomCanvas = document.getElementById("inside");
var bottomCtx = bottomCanvas.getContext("2d");

// middle canvas
var middleCanvas = document.getElementById("the-window");
var middleCtx = middleCanvas.getContext("2d");

// top canvas
var topCanvas = document.getElementById("dirty");
var topCtx = topCanvas.getContext("2d");

// hidden canvas (bitmask)
var mask = document.createElement("canvas");
var maskCtx = mask.getContext("2d");
mask.width = topCanvas.width;
mask.height = topCanvas.height;
// fill a black rectangle in the hidden canvas (at the same position of the "glass" and removing the borders)
maskCtx.fillStyle = "rgba(0,0,0,1)";
maskCtx.fillRect(15, 15, mask.width - 30, mask.height - 27);
maskCtx.globalCompositeOperation = compositeOperation;

// load images and draw each one on its respective canvas layer:

var dirty = new Image();
dirty.onload = function() {
  middleCtx.drawImage(this, 0, 0);

  // make the dirty darker with a dark semi-transparent overlay
  middleCtx.fillStyle = "rgba(0, 0, 0, 0.7)";
  middleCtx.fillRect(0, 0, middleCanvas.width, middleCanvas.height);

  middleCtx.globalCompositeOperation = compositeOperation;
}
dirty.src = "https://dl.dropboxusercontent.com/s/q4pqim0v5q0u9pc/before.png";

var windowBars = new Image();
windowBars.onload = function() {
  topCtx.drawImage(this, 0, 0);
}
windowBars.src = "https://dl.dropboxusercontent.com/s/p0pbkwi2kbg2e1m/bars.png";

var wally = new Image();
wally.onload = function() {
  bottomCtx.drawImage(this, 0, 0, this.width, this.height, 0, 0, bottomCanvas.width, bottomCanvas.height);
  //...