4

I am trying to save fabricjs stickman example by using JSON.stringify and load it back using canvas.loadFromJSON, I even add the properties of circle line1, line2, line3 and line4 explicitly to the JSON using the following way

var jsonSave = JSON.stringify(canvas.toJSON(['line1', 'line2', 'line3', 'line4']));

Although i can see that the saved JSON contains the above mentioned properties, but when I load from JSON and try to move the circle I get an error as Uncaught TypeError: p.line1.set is not a function.

I don't understand why before saving set was a function but after saving and load from JSON its not?

Also is there any other way to make the stickman work without using set method so that it doesn't break after we save it and load it from JSON.

I did look up for similar problems and this link was the most similar problem i could find but the solution provided in this answer, also didn't seem to solve the problem of save and load.

loading stickman from json does not persist line coordinate rotation logic

I have also attached the fiddle link below. To reproduce the scenario

  1. Try to move any circle after fiddle loads.
  2. Click on the save button. (you can go to developers tool and see under the resources tab for the JSON stored in the sessions storage.)
  3. Click on load button.
  4. Try to move any circle and look at console tab in developers tool.

http://jsfiddle.net/hkundnani/h0sf3x5h/5/

Any help would be greatly appreciated.. Thanks..

Community
  • 1
  • 1
harshk17
  • 120
  • 9

2 Answers2

2

When your code creates the circle objects, your code adds extra properties (e.g. line1, line2, line3, line4) to the circle objects. These extra properties are references to existing line objects on the fabric canvas. The issue that you are encountering is that the JSON format has no built-in support for serializing objects by reference. Objects are always serialized by value. As a result, multiple references to the same object will be serialized to the JSON string as multiple objects with the same values. When the JSON string is deserialized, you will have multiple objects with the same values instead of multiple references to the same object. To further compound your problems, FabricJS's loadFromJSON() method with convert the top level objects to FabricJS objects but leave the inner property objects (e.g. circle.line1) as plain objects. These plain objects will not have the FabricJS methods (e.g. no set method).

If you do a Google search of "JSON object references" then you will find articles discussing possible work arounds to the issue of preserving object references in JSON format.

In your case, a possible solution might be to add a custom property (e.g. "$id") to the line objects where the custom property holds an unique identifer for the line object. Save this custom property to the JSON string. After loading the JSON string, you could scan the FabricJS canvas for line objects and create a map from identifiers to line objects. Then you could scan the FabricJS canvas for circle objects and replace their line property values with the line object references from your map. It is not an easy solution but it should work.

Bobby Orndorff
  • 3,265
  • 1
  • 9
  • 7
  • Thanks for answering the question... It gives me some idea, as where the problem can be and what exactly to look for while searching about it... I will look more into JSON object references. Can you provide some example of the solution you suggested... that would be of much help...... – harshk17 Dec 17 '15 at 17:54
2

Possible workaround for fabricjs: http://jsfiddle.net/h0sf3x5h/9/ (snippet does not run, if you know how to solve it, please tell me)

The point is to write custom code in save and load functions to get the lines attached to your circles again.

var canvas = this.__canvas = new fabric.Canvas('c', { selection: false }), saveNow, loadNow;
  fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';


(function() {

  saveNow = (function(){
    var jsonSave = JSON.stringify(canvas.toJSON(['linesID', 'selection', 'id']));
    sessionStorage.canvase = jsonSave;
  });

  function _callBack() {
    var objs = canvas.getObjects();
    var lines = {};
    for(var i = 0; i < objs.length; i++) {
       if (objs[i].type === 'line') {
           lines[objs[i].id] = objs[i];
       }
    }
    for(i = 0; i < objs.length; i++) {
       if (objs[i].type === 'circle') {
           var circle = objs[i];
           if (circle.linesID) {
               circle.line1 = lines[circle.linesID[0]]; 
               circle.line2 = lines[circle.linesID[1]];
               circle.line3 = lines[circle.linesID[2]];
               circle.line4 = lines[circle.linesID[3]];
           }
       }
    }
    canvas.renderAll();
  }

  loadNow = (function(){
    var jsonLoad = sessionStorage.canvase;
    canvas.loadFromJSON(jsonLoad, _callBack);
  });

document.getElementById('save').addEventListener('click', saveNow);
document.getElementById('load').addEventListener('click', loadNow);
  function makeCircle(left, top, line1, line2, line3, line4) {
    var c = new fabric.Circle({
      left: left,
      top: top,
      strokeWidth: 5,
      radius: 12,
      fill: '#fff',
      stroke: '#666'
    });
    c.hasControls = c.hasBorders = false;

    c.line1 = line1;
    c.line2 = line2;
    c.line3 = line3;
    c.line4 = line4;
    c.linesID = [line1 && line1.id, line2 && line2.id, line3 && line3.id, line4 && line4.id];
    return c;
  }

  function makeLine(coords, id) {
    return new fabric.Line(coords, {
      fill: 'red',
      stroke: 'red',
      strokeWidth: 5,
      selectable: false,
      id: id
    });
  }

  var line = makeLine([ 250, 125, 250, 175 ], 'line1'),
      line2 = makeLine([ 250, 175, 250, 250 ], 'line2'),
      line3 = makeLine([ 250, 250, 300, 350], 'line3'),
      line4 = makeLine([ 250, 250, 200, 350], 'line4'),
      line5 = makeLine([ 250, 175, 175, 225 ], 'line5'),
      line6 = makeLine([ 250, 175, 325, 225 ], 'line6');

  canvas.add(line, line2, line3, line4, line5, line6);

  canvas.add(
    makeCircle(line.get('x1'), line.get('y1'), null, line),
    makeCircle(line.get('x2'), line.get('y2'), line, line2, line5, line6),
    makeCircle(line2.get('x2'), line2.get('y2'), line2, line3, line4),
    makeCircle(line3.get('x2'), line3.get('y2'), line3),
    makeCircle(line4.get('x2'), line4.get('y2'), line4),
    makeCircle(line5.get('x2'), line5.get('y2'), line5),
    makeCircle(line6.get('x2'), line6.get('y2'), line6)
  );

  canvas.on('object:moving', function(e) {
    var p = e.target;
    p.line1 && p.line1.set({ 'x2': p.left, 'y2': p.top });
    p.line2 && p.line2.set({ 'x1': p.left, 'y1': p.top });
    p.line3 && p.line3.set({ 'x1': p.left, 'y1': p.top });
    p.line4 && p.line4.set({ 'x1': p.left, 'y1': p.top });
    canvas.renderAll();
  });
})();
<script src="http://fabricjs.com/lib/fabric.js"></script>
<canvas id="c" width="500" height="500"></canvas>

<button id="save">Save </button>
<button id="load">Load </button> 
AndreaBogazzi
  • 14,323
  • 3
  • 38
  • 63
  • I am extremely sorry for replying so late, and thanks for your response, Its perfect and exactly what i needed, except one small bug(though i am not sure if its a bug, please let me know if its a bug). I tried to run the fiddle and figure out what is going wrong. The problem is before we save the line, the coordinates x1, y1, x2, y2 have values like 250, 250, 125, 175 but after we export canvas to json the coordinates change to 0, 0, -25, 25, which results the stickman to break, when we try to drag circle after load. – harshk17 Mar 01 '16 at 03:53
  • i noticed the same problem in another fiddle. i think is a bug, not well known, because line are not so popular. I'm investigating. – AndreaBogazzi Mar 01 '16 at 09:36
  • i need time. i have other bugs before this. i'm sorry. – AndreaBogazzi Mar 03 '16 at 11:21
  • i understand. I tried to search in existing issues to check if any one already faced this bug and i found this issue https://github.com/kangax/fabric.js/pull/1714, which was fixed by you. After going through it i realize that what we facing might not be a bug, because the code is written that way to calculate line coordinates before converting it to object. – harshk17 Mar 05 '16 at 09:04