Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
374 views
in Technique[技术] by (71.8m points)

javascript - Using stateProperties and hasStateChanged to implement undo / redo in fabric.js

Edit: The scope of this question has shifted a bit. Please see updates below.

I've been working on an undo/redo of sorts for fabric.js (see fiddle). Though rather naive, it's working for the most part (for adding / removing / moving or resizing individual objects or adding / removing groups of objects), but not for moving or resizing groups of objects. In the fiddle, note console output from onObjectSelected() function.

To see what i mean, draw some objects on the fiddle canvas, and move or resize them individually. Undo / redo works as expected. However, when groups are selected and moved, i can't see how to retrieve the updated positions of the objects within the group, which means the undo / redo can't restore the correct values.

// onObjectSelected function quoted here to satisfy stackoverflow 
// question requirements. See full jsfiddle for context.
for (i in canvas.getObjects()) {
        if (canvas.item(i).active === true ) {
            console.log(canvas.item(i));
            selectedObject.push({
                "itemNum"   : i,
                "svdObject" : fabric.util.object.clone(canvas.item(i))
            });
        }
    }

The reason seems to be that object positions / scales are not updated when moved or resized as a group. I've tried various things, including running setCoords() for all objects, but that has not helped. So i'd appreciate if someone can spot what i'm doing wrong, or has an idea how to work around this.

Edit: To clarify, this is just a normal undo / redo system using two arrays to store properties for objects on the canvas as the user modifies them. When undo or redo is clicked, the properties are applied back to the object on the canvas. The problem i'm having is that when groups of objects are selected and subsequently modified, i don't know how to capture certain properties, such as the positions of the objects in the group, because the properties of the objects within the group are not updated when the objects are modified as a group for some reason.

Update: I've learned that when objects are in user selected groups, the object position is recorded relative to the group it's in, and no longer the canvas, which makes sense. So when dealing with an object in a group, add it's left/top coord to the group left/top coord to calculate the coord of the object relative to the canvas. scaleX/Y should be multiplied by group scaleX/Y.

Update: I've mananaged to get this to do what i want (see new fiddle). I've since learned about fabric's 'stateful' flag, stateProperties array and hasStateChanged method, which would simplify things. The documentation for these is sparse and i've not been successful thus far in using them within this solution. Usage tips or example code using these would be appreciated. Specifically, I would like to be able to control which properties are saved, since i don't need to save all changeable properties.

onObjectSelected looks like this at present:

function onObjectSelected() {

  selectedObject = [];

  for (i in canvas.getObjects()) {

    if (canvas.item(i).active === true) {

      // shft-ctrl-j to see item properties on click (chrome)
      console.log(canvas.item(i));

      // objects can be flipped, but not groups 
      var groupLeft = 0;
      var groupTop = 0;
      var groupScaleX = 1;
      var groupScaleY = 1;
      var groupAngle = 0;


      // if it's a group, add group coords also
      if (typeof canvas.item(i).group != "undefined") {
        var groupLeft = canvas.item(i).group.left;
        var groupTop = canvas.item(i).group.top;
        var groupScaleX = canvas.item(i).group.scaleX;
        var groupScaleY = canvas.item(i).group.scaleY;
        var groupAngle = canvas.item(i).group.angle;
      }

      selectedObject.push({
        "itemNum": i,
        "left": canvas.item(i).left + groupLeft,
        "top": canvas.item(i).top + groupTop,
        "scaleX": canvas.item(i).scaleX * groupScaleX,
        "scaleY": canvas.item(i).scaleY * groupScaleY,
        "angle": canvas.item(i).angle + groupAngle,
        "flipX": canvas.item(i).flipX,
        "flipY": canvas.item(i).flipY
      });
    }

  }

};
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Fabricjs does not support auto-scaling or positioning by default. The only way to support this is to implement your own scaling/positioning mechanisms. I also tried many things but these lines work best at this moment.

So basically, please add these functions in your js:

function prepareRatio(o){
    saveRatio(o);
    o.on('modified', function(e){
        saveRatio(o);
    });
}

function restoreRatio(o){
    o.set({
        left: o._ratio._left * _cv.width,
        top: o._ratio._top * _cv.height,
        width: o._ratio._width * _cv.width,
        height: o._ratio._height * _cv.height
    });

    o.setCoords();
}

function saveRatio(obj){
    obj._ratio = { 
        _left: obj.left / _cv.width,
        _top: obj.top / _cv.height,
        _width: obj.width / _cv.width,
        _height: obj.height / _cv.height
    };
}

I used 2 custom variables here where _cv is the simplified structure that is arranged to store width and height of the target canvas, and, _ratio is for each objects (constant) ratio value.

And use them wisely when you create an object or resize canvas:

fabric.Image.fromURL('xxxx.png', function(img) {

    img.set({
        // TODO: give object width and height, left and top
    });

    // save ratio once you create an object
    prepareRatio(img);

    canvas.add(img);

    // OPTIONAL: add your own codes
});

function redraw() {

    // TODO: resize your canvas as you want

    _cv = {
        width: canvas.getWidth(),
        height: canvas.getHeight()
    };

    canvas.forEachObject(function(obj) {
        // restore ratio every time when you resize your canvas
        restoreRatio(obj);
    });
}

window.addEventListener('resize', redraw, false);

This looks a little bit long but very simple and clear solution in fact. I'm pretty sure this will work immediately in your side. Hope this helps.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...