Search code examples
javascripthtmlcsscanvashtml5-canvas

How to not erase background in canvas JS?


So I made this canvas on which you can paint on. The problem is that when you erase your drawings it will also erase the background.

    // SETTING ALL VARIABLES

        var isMouseDown=false;
        var canvas = document.createElement('canvas');
        var body = document.getElementsByTagName("body")[0];
        var ctx = canvas.getContext('2d');
        var linesArray = [];
        currentSize = 5;
        var currentColor = "rgb(200,20,100)";
        var currentBg = "white";
let newImage = new Image();
  newImage.src = 'https://www.arnoldvanhooft.nl/wp-content/uploads/2019/06/ja-knop.png'
        // INITIAL LAUNCH
newImage.onload = () => {
  ctx.drawImage(newImage, 0, 0, 500, 500);
  }
        createCanvas();

        // BUTTON EVENT HANDLERS

        document.getElementById('canvasUpdate').addEventListener('click', function() {
            createCanvas();
            redraw();
        });
        document.getElementById('colorpicker').addEventListener('change', function() {
            currentColor = this.value;
        });
        document.getElementById('bgcolorpicker').addEventListener('change', function() {
            ctx.fillStyle = this.value;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            redraw();
            currentBg = ctx.fillStyle;
        });
        document.getElementById('controlSize').addEventListener('change', function() {
            currentSize = this.value;
            document.getElementById("showSize").innerHTML = this.value;
        });
        document.getElementById('saveToImage').addEventListener('click', function() {
            downloadCanvas(this, 'canvas', 'masterpiece.png');
        }, false);
        document.getElementById('eraser').addEventListener('click', eraser);
        document.getElementById('clear').addEventListener('click', createCanvas);
        document.getElementById('save').addEventListener('click', save);
        document.getElementById('load').addEventListener('click', load);
        document.getElementById('clearCache').addEventListener('click', function() {
            localStorage.removeItem("savedCanvas");
            linesArray = [];
            console.log("Cache cleared!");
        });

        // REDRAW 

        function redraw() {
                for (var i = 1; i < linesArray.length; i++) {
                    ctx.beginPath();
                    ctx.moveTo(linesArray[i-1].x, linesArray[i-1].y);
                    ctx.lineWidth  = linesArray[i].size;
                    ctx.lineCap = "round";
                    ctx.strokeStyle = linesArray[i].color;
                    ctx.lineTo(linesArray[i].x, linesArray[i].y);
                    ctx.stroke();
                }
        }

        // DRAWING EVENT HANDLERS

        canvas.addEventListener('mousedown', function() {mousedown(canvas, event);});
        canvas.addEventListener('mousemove',function() {mousemove(canvas, event);});
        canvas.addEventListener('mouseup',mouseup);

        // CREATE CANVAS

        function createCanvas() {
            canvas.id = "canvas";
            canvas.width = parseInt(document.getElementById("sizeX").value);
            canvas.height = parseInt(document.getElementById("sizeY").value);
            canvas.style.zIndex = 8;
            canvas.style.position = "absolute";
            canvas.style.border = "1px solid";
            ctx.fillStyle = currentBg;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            body.appendChild(canvas);
        }

        // DOWNLOAD CANVAS

        function downloadCanvas(link, canvas, filename) {
            link.href = document.getElementById(canvas).toDataURL();
            link.download = filename;
        }

        // SAVE FUNCTION

        function save() {
            localStorage.removeItem("savedCanvas");
            localStorage.setItem("savedCanvas", JSON.stringify(linesArray));
            console.log("Saved canvas!");
        }

        // LOAD FUNCTION

        function load() {
            if (localStorage.getItem("savedCanvas") != null) {
                linesArray = JSON.parse(localStorage.savedCanvas);
                var lines = JSON.parse(localStorage.getItem("savedCanvas"));
                for (var i = 1; i < lines.length; i++) {
                    ctx.beginPath();
                    ctx.moveTo(linesArray[i-1].x, linesArray[i-1].y);
                    ctx.lineWidth  = linesArray[i].size;
                    ctx.lineCap = "round";
                    ctx.strokeStyle = linesArray[i].color;
                    ctx.lineTo(linesArray[i].x, linesArray[i].y);
                    ctx.stroke();
                }
                console.log("Canvas loaded.");
            }
            else {
                console.log("No canvas in memory!");
            }
        }

        // ERASER HANDLING

        function eraser() {
            currentSize = 50;
            currentColor = ctx.fillStyle
        }

        // GET MOUSE POSITION

        function getMousePos(canvas, evt) {
            var rect = canvas.getBoundingClientRect();
            return {
                x: evt.clientX - rect.left,
                y: evt.clientY - rect.top
            };
        }

        // ON MOUSE DOWN

        function mousedown(canvas, evt) {
            var mousePos = getMousePos(canvas, evt);
            isMouseDown=true
            var currentPosition = getMousePos(canvas, evt);
            ctx.moveTo(currentPosition.x, currentPosition.y)
            ctx.beginPath();
            ctx.lineWidth  = currentSize;
            ctx.lineCap = "round";
            ctx.strokeStyle = currentColor;

        }

        // ON MOUSE MOVE

        function mousemove(canvas, evt) {

            if(isMouseDown){
                var currentPosition = getMousePos(canvas, evt);
                ctx.lineTo(currentPosition.x, currentPosition.y)
                ctx.stroke();
                store(currentPosition.x, currentPosition.y, currentSize, currentColor);
            }
        }

        // STORE DATA

        function store(x, y, s, c) {
            var line = {
                "x": x,
                "y": y,
                "size": s,
                "color": c
            }
            linesArray.push(line);
        }

        // ON MOUSE UP

        function mouseup() {
            isMouseDown=false
            store()
        }
.colorButtons {
            display: block;
            margin: 20px 0;
        }

        canvas {
            cursor: crosshair;
        }

        div#sidebar {
            position: absolute;
            left: 0;
            width: 150px;
            padding: 20px 20px;
            top: 0;
        }

        canvas#canvas {
            left: 150px;
            top: 45px;
        }

        .btn {
            margin-bottom: 10px;
            width: 100%;
        }
        input {
            width: 100%;
            margin-bottom: 10px;
        }

        .input-group {
            margin-bottom: 10px;
        }

        .toolsButtons .btn {
            width: 48%;
        }

        .sizeButtons .btn {
            width: 48%;
        }

        .colorpicker {
            background: transparent;
            height: 40px;
        }
<!-- using Bootstrap CSS because lazy to write 3 classes --> 

<body>
    <div id="sidebar">
        <div class="colorButtons">
            <h3>Colour</h3>
            <input type="color" id="colorpicker" value="#c81464" class="colorpicker">
        </div>
        <div class="colorButtons">
            <h3>Bg Color</h3>
            <input type="color" value="#ffffff" id="bgcolorpicker" class="colorpicker">
        </div>

        <div class="toolsButtons">
            <h3>Tools</h3>
            <button id="eraser" class="btn btn-default">eraser</span></button>
            <button id="clear" class="btn btn-danger"> <span class="glyphicon glyphicon-repeat" aria-hidden="true"></span></button>
        </div>

        <div class="buttonSize">
            <h3>Size (<span id="showSize">5</span>)</h3>
            <input type="range" min="1" max="50" value="5" step="1" id="controlSize">
        </div>

        <div class="canvasSize">
            <h3>Canvas</h3>
            <div class="input-group">
                <span class="input-group-addon">X</span>
                <input type="number" id="sizeX" class="form-control" placeholder="sizeX" value="800" class="size">
            </div>
            <div class="input-group">
                <span class="input-group-addon">Y</span>
                <input type="number" id="sizeY" class="form-control" placeholder="sizeY" value="800" class="size">
            </div>
            <input type="button" class="updateSize btn btn-success" value="Update" id="canvasUpdate">
        </div>
        <div class="Storage">
            <h3>Storage</h3>
            <input type="button" value="Save" class="btn btn-warning" id="save">
            <input type="button" value="Load" class="btn btn-warning" id="load">
            <input type="button" value="Clear" class="btn btn-warning" id="clearCache">
        </div>
        <div class="extra">
            <h3>Extra</h3>
            <a id="saveToImage" class="btn btn-warning">Download</a>
        </div>
        
    </div>
  </body>

I have tried by adding the photo in a different way but that way it wouldn't be saved the right way. I also have tried changing layers with CSS and index but that also didn't work


Solution

  • Using layers

    A canvas drawing app can use many canvases to define layers. Layers can include things like backgrounds, drawing layers, composite layers (multiply, screen, etc) and much more. Much the same as layers are used in apps like photoshop.

    A bonus when using layers is that the immediate drawing state can be displayed without affecting the existing layers, as you can draw the pen on the output layer when the mouse button is not down. (see example)

    To get the most from canvas layers you should become familiar with the many ctx.globalCompositeOperation modes.

    The example uses the following ctx.globalCompositeOperation modes

    • "copy" copies pixels from source to destination including transparent pixels.
    • "source-over" (used in example draw mode) The default drawing mode. Copies pixels ignoring transparent pixels and blending semi transparent pixels.
    • "destination-out" (used in example erase mode) Removes pixels from the destination canvas where you draw opaque pixels, and partially removes pixels where you draw semi transparent pixels.

    Performance

    Even lowend devices can handle many canvas layers easily as long as you ensure that the canvas resolution does not exceed the device display size by many factors as performance is regulated by the availability of GPU RAM

    You may be tempted to have the DOM handle the layer composition. It turns out that using the CanvasRenderingContext2D API to do layering is more efficient than letting the DOM handle it

    Example

    Below is a very basic drawing example. It uses 2 canvas layers, one for the background, and one for the drawing layer.

    The background is loaded and then drawn to scale on the bg canvas.

    When the mouse button is down the update function draws or erases to/from the drawing layer.

    A 3rd canvas is used to show the result. This canvas is added to the DOM and the update function renders the layers to it as needed.

    To save the result of the layers you can download the content of the 3rd canvas, or create a new canvas (if the display canvas size does not match the drawing size), draw the layers to it, and download its content.

    Useage: Use mouse (left click) to draw / erase on drawing layer. Use button to toggle drawing mode (Draw / Erase)

    ;(()=>{
        setTimeout(start, 0);
        var ctx1, ctx2, ctx3;
        const SIZE = 180;
        const PEN_SIZE = 30;
        function start() {
           const button = tag("button", {textContent: "Draw", title: "Toggle erase / draw mode", className: "floatBtn"});
           const canProps = {width: SIZE, height: SIZE};
           ctx1 = tag("canvas", canProps).getContext("2d");  // BG layer
           ctx2 = tag("canvas", canProps).getContext("2d");  // drawing layer
           ctx3 = tag("canvas", canProps).getContext("2d");  // display canvas context
           ctx2.lineWidth = ctx3.lineWidth = PEN_SIZE;
           ctx2.lineCap = ctx3.lineCap = "round";
           ctx2.lineJoin = ctx3.lineJoin = "round";
           ctx2.strokeStyle = ctx3.strokeStyle = "BLUE";
           append(BODY, ctx3.canvas, button);
    
           // Load BG image and draw on bg canvas when loaded. Note bg is
           // scaled to fit 180 by 180 canvas
           const bgImg = new Image;
           bgImg.src = "https://i.sstatic.net/C7qq2.png?s=256&g=1";
           listener(bgImg, "load", () => (ctx1.drawImage(bgImg, 0, 0, 180, 180), mouse.update = true), {once: true});
           listener(button, "click", () => {
              mouse.draw = !mouse.draw;   // Toggle drawing mode
              button.textContent = mouse.draw ? "Draw" : "Erase"; 
           });
           mouse.update = true;
           update();
        }
        
        function update() {
            requestAnimationFrame(update)
            if (!mouse.update) { return }
            ctx3.globalCompositeOperation = "copy";  // to draw bg image 
            ctx3.drawImage(ctx1.canvas, 0 , 0);
            if (mouse.lastX !== undefined) {  // Avoid line from zero when mouse first over body
                ctx3.globalCompositeOperation = "source-over";  // to draw drawing layer
                if (mouse.button) {           // draw on drawing layer if mouse down
                    ctx2.globalCompositeOperation = mouse.draw ? "source-over" : "destination-out";
                    ctx2.beginPath();
                    ctx2.lineTo(mouse.lastX, mouse.lastY);
                    ctx2.lineTo(mouse.x, mouse.y + 0.01); // Small 100th px offset
                                                          // ensures line is drawn
    
                    ctx2.stroke();
                }
    
                ctx3.drawImage(ctx2.canvas, 0 , 0);
                if (!mouse.button) { 
                    ctx3.strokeStyle = mouse.draw ? "BLUE" : "RED";
                    ctx3.beginPath();
                    ctx3.lineTo(mouse.lastX, mouse.lastY);
                    ctx3.lineTo(mouse.x, mouse.y + 0.01);
                    ctx3.stroke();
                }
                mouse.lastX = mouse.x;
                mouse.lastY = mouse.y;
            }
            mouse.update = false;
        }
        
        
        const TAU      = Math.PI * 2;
        const DOC      = document, BODY = DOC.body, assign = Object.assign;
        const isArr    = Array.isArray;
        const tag      = (tag, props = {}) => assign(DOC.createElement(tag), props);
        const append   = (el, ...sibs) => sibs.reduce((p, sib) => ((isArr(sib) ? append(p, ...sib) : p.appendChild(sib)), p), el);
        const listener = (qe, name, call, opt = {}) => (qe.addEventListener(name, call, opt), qe);
        const mouse  = {x: 0, y: 0, button: false, lastX: undefined, lastY: undefined, draw: true, update: true}
        function mouseEvents(e) {
          mouse.update = true;
          mouse.x = e.pageX;
          mouse.y = e.pageY;
          if (mouse.lastX === undefined) {
              mouse.lastX = mouse.x;
              mouse.lastY = mouse.y;  
          }
          mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
        }
        ["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
    
    
    })();
    canvas { position: absolute; top: 0px; left: 0px; cursor: crosshair}
    .floatBtn {  position : absolute; top: 0px; left: 180px; cursor: pointer}