Search code examples
javascriptcanvasfabricjs

How to clear canvas properties and events and add it back in fabricjs?


Context:

I have a use case where I need to get old canvas from the stack and assign it to the currently active canvas in the dom. The problem i am currently facing here is the canvas properties (e.g backgroundColor, height etc) gets replaced and shows up properly but in case of objects within the canvas the properties and events looks like its getting replaced but doesn't show up in the active canvas(DOM).

Note: I do not have option to use fabric's undo/redo here.

I have tried to recreate the issue in the snippet where I am trying to do the following.

Step 1: I am creating the canvas and objects with default properties using addTextBox method, once the default properties are applied I am storing the canvas and textbox object in a variable called as state for backup.

Step 2:

  • on AddHelloText Click(Button) I am pushing state which was recorded in the previous step on to undo stack.

  • After that I am assigning "hello" to textbox and "changing background color to red".

Step 3: on Undo Click(Button) I am getting the canvasOb from undoStack and assigning i to canvas (active canvas).

In this step the when i do undo, the values/events on the canvas are supposed to be of backup canvas which was stored in the undoStack,but instead it has incorrect values. In simple words the canvas background is supposed to be "blue" and textbox object is supposed to have "Default text", even when i click on textbox it is supposed to have background "blue" and text as "Default text"

These are the two issues i am facing currently.

  • Only background gets updated and textobject remains the same.

  • On clicking bounding box (text box), it shows up a different instance of the canvas(the one with the step2 values)

Not sure how exactly i should proceed further.

Any suggestions would be really helpful.

var canvas = new fabric.Canvas("c");
var t1 = null;
var state = {};
const addTextBox=(canvasParam)=>{
     t1 = new fabric.Textbox('Default text', {
              width: 100,
              top: 5,
              left: 5,
              fontSize: 16,
              textAlign: 'center',
               fill: '#ffffff',
     });
     canvas.on('selected', () => {
            canvas.clearContext(canvas.contextTop);
        });
     canvas.setBackgroundColor('blue');
     canvas.add(t1);
  state={id:1,canvasOb:canvas,t1Backup:t1};
}

addTextBox(canvas);

var undoStack =[];

//Update the text to hello and change canvas background to red
  $('#addHello').on('click', function() {
    undoStack.push({state:_.cloneDeep(state)})
    canvas.setBackgroundColor('red');
    t1.text="hello red"
    canvas.renderAll();
  })

 //apply previous canvas stored in the stack
  $('#undo').on('click', function() {
    console.log("TextBox value stored in undo stack: ",undoStack[0].state.canvasOb.getObjects()[0].text);
    console.log("TextBox value stored in undo stack: ",undoStack[0].state.t1Backup.text);
    canvas = undoStack[0].state.canvasOb;
    console.log("Textbox value that should be displayed currently instead of 'hello red':",canvas.getObjects()[0].text);
    canvas.renderAll();
  })
body {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  width: 100vw;
}

#c {
  box-shadow: 0 0 20px 0px #9090908a;
  border-radius: 1rem;
  //padding:.5rem;
}

#app-container{
  height: 30vh;
  width: 30vw;
}
<script src="https://unpkg.com/lodash@4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.1.0/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div >
<html><body><div id="app-container">
<canvas id="c" width="200" height="300"></canvas>
  <button id="addHello" class="button">Add hello</button>
  <button id="undo" class="button">Undo</button>
 </div></body></html>


Solution

  • You are changing the canvas object on the fly even though you are saving its reference inside your state object. So as you update text or change the background color of canvas and etc..., you are in fact changing your saved state, because all you did so far is saving a reference to fabric.js canvas object.

    This article can be interesting: Reference vs Primitive values in Javascript

    The best way to accomplish what you want to achieve (in my opinion) would be to use the fabric.js toJSON method and save the state as JSON, then retrieve it again using loadFromJSON whenever you need it.
    Here's an example:

    var canvas = new fabric.Canvas('c');
    var t1 = null;
    var undoStack = [];
    
    
    // Whenever you make a change and then you want to save it as a state
    const addNewState = () => {
        var canvasObject = canvas.toJSON();
        undoStack.push(JSON.stringify(canvasObject));
    };
    
    const initCanvas = () => {
        t1 = new fabric.Textbox('Default text', {
            width: 100,
            top: 5,
            left: 5,
            fontSize: 16,
            textAlign: 'center',
            fill: '#ffffff',
        });
        canvas.on('selected', () => {
            canvas.clearContext(canvas.contextTop);
        });
        canvas.setBackgroundColor('blue');
        canvas.add(t1);
        addNewState(); // Our first state
    };
    
    // Init canvas with default parameters
    initCanvas(canvas);
    
    const retrieveLastState = () => {
        var latestState = undoStack[undoStack.length - 1]; // Get last state
        var parsedJSON = JSON.parse(latestState);
        canvas.loadFromJSON(parsedJSON);
    };
    
    // Update the text to hello and change canvas background to red
    $('#addHello').on('click', function () {
        canvas.setBackgroundColor('red');
        t1.text = 'hello red';
        canvas.renderAll();
    });
    
    // apply previous canvas stored in the stack
    // Retrieve the very last state
    $('#undo').on('click', function () {
        retrieveLastState();
        canvas.renderAll();
    });
    body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        width: 100vw;
    }
    
    #c {
        box-shadow: 0 0 20px 0px #9090908a;
        border-radius: 1rem;
    }
    
    #app-container {
        height: 30vh;
        width: 30vw;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="app-container">
            <canvas id="c" width="200" height="300"></canvas>
            <button id="addHello" class="button">Add hello</button>
            <button id="undo" class="button">Undo</button>
        </div>
        <script src="https://unpkg.com/lodash@4/lodash.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.1.0/fabric.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    </body>
    
    </html>

    However, the next time you click on #addHello button and then you try to undo it again, it's not going to work. The reason for that is we are trying to access t1 variable which is a fabric.js object on the very first canvas. The solution would be to assign an id to each object you add to the canvas so that you can retrieve them again (for modifying, deleting and etc...). This would make things a lot more complicated so I didn't put that in here.