javascriptcanvasdrawingdrawing2d

How to scratch reveal an image with Javascript canvas element


I created a canvas element where I can draw paths. There, I draw transparent paths and I "scratch" reveal the foreground. It works on both mobile and desktop.


Solution

  • A working example

    // Animation frame
    window.requestAnimFrame = (function (callback) {
      return window.requestAnimationFrame || 
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimaitonFrame ||
            function (callback) {
              window.setTimeout(callback, 1000/60);
            };
    })();
    
    const body = document.getElementById('body');
    
    // Draw
    const color = "#f70d8a";
    const stroke = 25;
    
    // Canvas
    const canvas = document.getElementById("canvas");
    let canvasWidth = canvas.offsetWidth;
    let canvasHeight = canvas.offsetHeight;
    const context = canvas.getContext("2d");
    context.fillRect(0, 0, canvasWidth, canvasHeight);
    context.strokeStyle = color;
    context.lineWidth = stroke;
    document.getElementById('canvas-background').innerHTML = "Oh<br>yeah";
    
    // Mouse events
    var drawing = false;
    var mousePos = { x:0, y:0 };
    var lastPos = mousePos;
    canvas.addEventListener("mousedown", function (e) {
      drawing = true;
      lastPos = getMousePos(canvas, e);
    }, false);
    canvas.addEventListener("mouseup", function (e) {
      drawing = false;
    }, false);
    canvas.addEventListener("mousemove", function (e) {
      mousePos = getMousePos(canvas, e);
    }, false);
    
    // Touch events
    canvas.addEventListener("touchstart", function (e) {
      mousePos = getTouchPos(canvas, e);
      var touch = e.touches[0];
      var mouseEvent = new MouseEvent("mousedown", {
        clientX: touch.clientX,
        clientY: touch.clientY
      });
      canvas.dispatchEvent(mouseEvent);
    }, false);
    canvas.addEventListener("touchend", function (e) {
      var mouseEvent = new MouseEvent("mouseup", {});
      canvas.dispatchEvent(mouseEvent);
    }, false);
    canvas.addEventListener("touchmove", function (e) {
      var touch = e.touches[0];
      var mouseEvent = new MouseEvent("mousemove", {
        clientX: touch.clientX,
        clientY: touch.clientY
      });
      canvas.dispatchEvent(mouseEvent);
    }, false);
    
    // Prevent default scrolling behavior on Touch events
    document.body.addEventListener("touchstart", function (e) {
      if (e.target == canvas) {
        e.preventDefault();
      }
    }, {passive:false});
    document.body.addEventListener("touchend", function (e) {
      if (e.target == canvas) {
        e.preventDefault();
      }
    }, {passive:false});
    document.body.addEventListener("touchmove", function (e) {
      if (e.target == canvas) {
        e.preventDefault();
      }
    }, {passive:false});
    
    // Mouse position
    function getMousePos(canvasDom, mouseEvent) {
      var rect = canvasDom.getBoundingClientRect();
      return {
        x: mouseEvent.clientX - rect.left,
        y: mouseEvent.clientY - rect.top
      };
    }
     
    // Touch position
    function getTouchPos(canvasDom, touchEvent) {
      var rect = canvasDom.getBoundingClientRect();
      return {
        x: touchEvent.touches[0].clientX - rect.left,
        y: touchEvent.touches[0].clientY - rect.top
      };
    }
    
    // Draw
    function renderCanvas() {
      if (drawing) {
        context.moveTo(lastPos.x, lastPos.y);
        context.lineTo(mousePos.x, mousePos.y);
        context.globalCompositeOperation = "destination-out";
        context.stroke();
        lastPos = mousePos;
      }
    }
    
    // Animation frame
    (function drawLoop () {
      requestAnimFrame(drawLoop);
      renderCanvas();
    })();
    /*---- Global variables ----*/
    
    :root {
      --black: #222;
      --yellow: #fff295;
      --purple: #fa0afa;
      --blue: #0daaf8;
      --green: #0df882;
      --red: #e64c4c;
      --pink: #f70d8a;
      --main-margin-top: 10vh;
      --canvas-width: 320px;
      --canvas-height: 320px;
    }
    
    /*---- Resetter ----*/
    
    * {
        margin: 0;
        border: 0;
        padding: 0;
    }
    
    /*---- Default classes ----*/
    
    body {
        background-color: var(--black);
      color: #fff;
        font-family: Futura, Arial, sans-serif;
        font-size: 1rem;
      line-height:1rem;
    }
    
    main {
      margin: var(--main-margin-top) auto;
      width: var(--canvas-width);
    }
    
    h1 {
      font-size: 2rem;
      font-weight: 400;
    }
    
    /*---- Generic classes ----*/
    
    .center {
      text-align: center;
    }
    
    /*---- Custom classes ----*/
    
    #canvas {
      z-index:9999;
      position:absolute;
      top:calc(var(--main-margin-top) + 2rem);
      left:calc(50vw - var(--canvas-width)/2);
      background-color: transparent;
    }
    
    #canvas-background {
      z-index: 999;
      position: absolute;
      width: inherit;
      line-height: 1em;
      font-size: 5rem;
      padding-top:calc(var(--canvas-height)/2 - 1em);
      text-align: center;
      color:var(--pink);
    }
    <body id="body">
      <main class="center">
        <h1 class="center">Scratch here</h1>
        <canvas id="canvas" width="320px" height="320px">
            </canvas>
        <p id="canvas-background"></p>
      </main>
    </body>