web-dev-qa-db-fra.com

Fonction Annuler-Répéter dans Fabric.js

Existe-t-il un support intégré pour undo/redo dans Fabric.js? Pouvez-vous s'il vous plaît me guider sur la façon dont vous avez utilisé cette annulation et répétez-le dans

19
John

Dans http://jsfiddle.net/SpgGV/9/ , déplacez l'objet et modifiez sa taille. Si l'état de l'objet est modifié et que nous annulons/rétablissons alors, son état précédent sera supprimé lors de la prochaine modification. Il est plus facile d’annuler/de rétablir. Tous les événements de canvas doivent être appelés avant qu'aucun élément ne soit ajouté à canvas. Je n'ai pas ajouté d'événement object:remove ici. Vous pouvez l'ajouter vous-même. Si un élément est supprimé, l'état et la liste doivent être invalides si cet élément est dans ce tableau. La méthode la plus simple consiste à définir state, list = [] et index = 0

Cela effacera l'état de votre file d'attente d'annulation/de rétablissement. Si vous souhaitez conserver tous les états, tels que ajouter/supprimer, ma suggestion est d'ajouter davantage de propriétés à l'élément de votre tableau d'états. Par exemple, state = [{"data":object.originalState, "event": "added"}, ....]. "L'événement" peut être "modifié" ou "ajouté" et défini dans un gestionnaire d'événement correspondant. 

Si vous avez ajouté un objet, définissez state[index].event="added" afin de le vérifier lors de la prochaine utilisation de Annuler. Si c'est "ajouté", alors supprimez-le quand même. Ou bien, lorsque vous utilisez la commande redo, si la cible est "ajoutée", vous l'ajoutez. J'ai récemment été très occupé. J'ajouterai des codes à jsfiddle.net plus tard.

Mise à jour: ajouté setCoords();

var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;

canvas.on("object:added", function (e) {
    var object = e.target;
    console.log('object:modified');

    if (action === true) {
        state = [state[index2]];
        list = [list[index2]];

        action = false;
        console.log(state);
        index = 1;
    }
    object.saveState();

    console.log(object.originalState);
    state[index] = JSON.stringify(object.originalState);
    list[index] = object;
    index++;
    index2 = index - 1;



    refresh = true;
});

canvas.on("object:modified", function (e) {
    var object = e.target;
    console.log('object:modified');

    if (action === true) {
        state = [state[index2]];
        list = [list[index2]];

        action = false;
        console.log(state);
        index = 1;
    }

    object.saveState();

    state[index] = JSON.stringify(object.originalState);
    list[index] = object;
    index++;
    index2 = index - 1;

    console.log(state);
    refresh = true;
});

function undo() {

    if (index <= 0) {
        index = 0;
        return;
    }

    if (refresh === true) {
        index--;
        refresh = false;
    }

    console.log('undo');

    index2 = index - 1;
    current = list[index2];
    current.setOptions(JSON.parse(state[index2]));

    index--;
    current.setCoords();
    canvas.renderAll();
    action = true;
}

function redo() {

    action = true;
    if (index >= state.length - 1) {
        return;
    }

    console.log('redo');

    index2 = index + 1;
    current = list[index2];
    current.setOptions(JSON.parse(state[index2]));

    index++;
    current.setCoords();
    canvas.renderAll();
}

Mise à jour: meilleure solution pour prendre en compte l'algorithme d'historique d'édition. Ici, nous pouvons utiliser Editing.getInst().set(item) où l'élément pourrait être {action, object, state}; Par exemple, {"add", object, "{JSON....}"}.

/**
 * Editing : we will save element states into an queue, and the length of queue 
 * is fixed amount, for example, 0..99, each element will be insert into the top 
 * of queue, queue.Push, and when the queue is full, we will shift the queue, 
 * to remove the oldest element from the queue, queue.shift, and then we will 
 * do Push. 
 * 
 * So the latest state will be at the top of queue, and the oldest one will be 
 * at the bottom of the queue (0), and the top of queue is changed, could be 
 * 1..99.
 * 
 * The initialized action is "set", it will insert item into the top of queue,
 * even if it arrived the length of queue, it will queue.shift, but still do
 * the same thing, and queue only abandon the oldest element this time. When
 * the current is changed and new state is coming, then this time, top will be
 * current + 1.
 *
 * The prev action is to fetch "previous state" of the element, and it will use
 * "current" to do this job, first, we will --current, and then we will return
 * the item of it, because "current" always represent the "current state" of
 * element. When the current is equal 0, that means, we have fetched the last
 * element of the queue, and then it arrived at the bottom of the queue.
 *
 * The next action is to fetch "next state" after current element, and it will
 * use "current++" to do the job, when the current is equal to "top", it means
 * we have fetched the latest element, so we should stop.
 *
 * If the action changed from prev/next to "set", then we should reset top to
 * "current", and abandon all rest after that...
 *
 * Here we should know that, if we keep the reference in the queue, the item
 * in the queue will never be released.
 *
 *
 * @constructor
 */
function Editing() {

    this.queue = [];
    this.length = 4;
    this.bottom = 0;
    this.top = 0;
    this.current = 0;
    this.empty = true;

    // At the Begin of Queue
    this.BOQ = true;

    // At the End of Queue
    this.EOQ = true;

    // 0: set, 1: prev, 2: next
    this._action = 0;
    this._round = 0;
}

Editing.sharedInst = null;
Editing.getInst = function (owner) {

    if (Editing.sharedInst === null) {
        Editing.sharedInst = new Editing(owner);
    }

    return Editing.sharedInst;
};

/**
 * To set the item into the editing queue, and mark the EOQ, BOQ, so we know
 * the current position.
 *
 * @param item
 */
Editing.prototype.set = function (item) {

    console.log("=== Editing.set");

    var result = null;

    if (this._action != 0) {
        this.top = this.current + 1;
    }

    if (this.top >= this.length) {
        result = this.queue.shift();
        this.top = this.length - 1;
    }

    this._action = 0;
    this.queue[this.top] = item;
    this.current = this.top;
    this.top++;

    this.empty = false;
    this.EOQ = true;
    this.BOQ = false;

    console.log("==> INFO : ");
    console.log(item);
    console.log("===========");
    console.log("current: ", 0 + this.current);
    console.log("start: ", 0 + this.bottom);
    console.log("end: ", 0 + this.top);

    return result;

};

/**
 * To fetch the previous item just before current one
 *
 * @returns {item|boolean}
 */
Editing.prototype.prev = function () {

    console.log("=== Editing.prev");

    if (this.empty) {
        return false;
    }

    if (this.BOQ) {
        return false;
    }

    this._action = 1;

    this.current--;

    if (this.current == this.bottom) {
        this.BOQ = true;
    }

    var item = this.queue[this.current];
    this.EOQ = false;

    console.log("==> INFO : ");
    console.log(item);
    console.log("===========");
    console.log("current: ", 0 + this.current);
    console.log("start: ", 0 + this.bottom);
    console.log("end: ", 0 + this.top);

    return item;
};

/**
 * To fetch the next item just after the current one
 *
 * @returns {*|boolean}
 */
Editing.prototype.next = function () {

    console.log("=== Editing.next");

    if (this.empty) {
        return false;
    }

    if (this.EOQ) {
        return false;
    }

    this.current++;

    if (this.current == this.top - 1 && this.top < this.length) {
        this.EOQ = true;
    }

    if (this.current == this.top - 1 && this.top == this.length) {
        this.EOQ = true;
    }

    this._action = 2;

    var item = this.queue[this.current];
    this.BOQ = false;

    console.log("==> INFO : ");
    console.log(item);
    console.log("===========");
    console.log("current: ", 0 + this.current);
    console.log("start: ", 0 + this.bottom);
    console.log("end: ", 0 + this.top);

    return item;
};


/**
 * To empty the editing and reset all state
 */
Editing.prototype.clear = function () {

    this.queue = [];
    this.bottom = 0;
    this.top = 0;
    this.current = 0;
    this.empty = true;
    this.BOQ = true;
    this.EOQ = false;
};
18
Tom

Voici une solution qui a commencé avec ce problème plus simple answer à la même question, Annuler l’historique de restauration pour Canvas FabricJs

Ma réponse va dans le même sens que la réponse de Tom et les autresréponses qui sont des modifications de la réponse de Tom. 

Pour suivre l'état, j'utilise JSON.stringify(canvas) et canvas.loadFromJSON() comme les autres réponses et un événement est enregistré sur le object:modified pour capturer l'état.

Une chose importante est que la dernière canvas.renderAll() devrait être appelée dans un rappel passé au deuxième paramètre de loadFromJSON(), comme ceci

canvas.loadFromJSON(state, function() {
    canvas.renderAll();
}

En effet, l'analyse et le chargement du fichier JSON peuvent prendre quelques millisecondes et vous devez attendre que l'opération soit terminée avant de générer le rendu. Il est également important de désactiver les boutons d'annulation et de rétablissement dès qu'ils sont cliqués et de ne réactiver que lors du même rappel. Quelque chose comme ça

$('#undo').prop('disabled', true);
$('#redo').prop('disabled', true);    
canvas.loadFromJSON(state, function() {
    canvas.renderAll();
    // now turn buttons back on appropriately
    ...
    (see full code below)
}

J'ai une pile d'annulation et de restauration et une pile globale pour le dernier état non modifié. Lorsque certaines modifications se produisent, l'état précédent est inséré dans la pile d'annulation et l'état actuel est à nouveau capturé. 

Lorsque l'utilisateur souhaite annuler, l'état actuel est placé dans la pile de restauration. Ensuite, je supprime la dernière annulation, puis les place dans l'état actuel et la rend sur le canevas.

De même, lorsque l'utilisateur souhaite rétablir, l'état actuel est placé dans la pile d'annulation. Ensuite, je supprime la dernière répétition et les place tous les deux dans l'état actuel et la rend sur le canevas.

Le code

         // Fabric.js Canvas object
        var canvas;
         // current unsaved state
        var state;
         // past states
        var undo = [];
         // reverted states
        var redo = [];

        /**
         * Push the current state into the undo stack and then capture the current state
         */
        function save() {
          // clear the redo stack
          redo = [];
          $('#redo').prop('disabled', true);
          // initial call won't have a state
          if (state) {
            undo.Push(state);
            $('#undo').prop('disabled', false);
          }
          state = JSON.stringify(canvas);
        }

        /**
         * Save the current state in the redo stack, reset to a state in the undo stack, and enable the buttons accordingly.
         * Or, do the opposite (redo vs. undo)
         * @param playStack which stack to get the last state from and to then render the canvas as
         * @param saveStack which stack to Push current state into
         * @param buttonsOn jQuery selector. Enable these buttons.
         * @param buttonsOff jQuery selector. Disable these buttons.
         */
        function replay(playStack, saveStack, buttonsOn, buttonsOff) {
          saveStack.Push(state);
          state = playStack.pop();
          var on = $(buttonsOn);
          var off = $(buttonsOff);
          // turn both buttons off for the moment to prevent rapid clicking
          on.prop('disabled', true);
          off.prop('disabled', true);
          canvas.clear();
          canvas.loadFromJSON(state, function() {
            canvas.renderAll();
            // now turn the buttons back on if applicable
            on.prop('disabled', false);
            if (playStack.length) {
              off.prop('disabled', false);
            }
          });
        }

        $(function() {
          ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
          // Set up the canvas
          canvas = new fabric.Canvas('canvas');
          canvas.setWidth(500);
          canvas.setHeight(500);
          // save initial state
          save();
          // register event listener for user's actions
          canvas.on('object:modified', function() {
            save();
          });
          // draw button
          $('#draw').click(function() {
            var imgObj = new fabric.Circle({
              fill: '#' + Math.floor(Math.random() * 16777215).toString(16),
              radius: Math.random() * 250,
              left: Math.random() * 250,
              top: Math.random() * 250
            });
            canvas.add(imgObj);
            canvas.renderAll();
            save();
          });
          // undo and redo buttons
          $('#undo').click(function() {
            replay(undo, redo, '#redo', this);
          });
          $('#redo').click(function() {
            replay(redo, undo, '#undo', this);
          })
        });
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js" type="text/javascript"></script>
</head>

<body>
  <button id="draw">circle</button>
  <button id="undo" disabled>undo</button>
  <button id="redo" disabled>redo</button>
  <canvas id="canvas" style="border: solid 1px black;"></canvas>
</body>

14
Kirby

J'autorise l'utilisateur à supprimer le dernier chemin ajouté (dans mon application de peinture), cela fonctionne bien pour moi:

var lastItemIndex = (fabricCanvas.getObjects().length - 1);
var item = fabricCanvas.item(lastItemIndex);

if(item.get('type') === 'path') {
  fabricCanvas.remove(item);
  fabricCanvas.renderAll();
}

Mais vous pouvez également supprimer la déclaration SI et laisser les gens supprimer quoi que ce soit.

7
chris.rickard

Je sais qu'il est tard pour répondre à cette question, mais voici ma version de cette implémentation. Peut être utile à quelqu'un.

J'ai implémenté cette fonctionnalité en enregistrant les états de la toile en tant que JSON. Chaque fois qu'un utilisateur ajoute ou modifie un objet dans la Canvas, il enregistre l'état de la toile modifié et le conserve dans une array. Cette array est ensuite manipulée chaque fois que l'utilisateur clique sur le bouton Annuler ou Rétablir.

Jetez un coup d'oeil à ce lien. J'ai également fourni une URL de démonstration fonctionnelle.

https://github.com/abhi06991/Undo-Redo-Fabricjs

HTML:

<canvas id="canvas" width="400" height="400"></canvas> 
<button type="button" id="undo" >Undo</button>
<button type="button" id="redo" disabled>Redo</button>

JS:

var canvasDemo = (function(){
  var _canvasObject = new fabric.Canvas('canvas',{backgroundColor : "#f5deb3"});
    var _config = {
        canvasState             : [],
        currentStateIndex       : -1,
        undoStatus              : false,
        redoStatus              : false,
        undoFinishedStatus      : 1,
        redoFinishedStatus      : 1,
    undoButton              : document.getElementById('undo'),
        redoButton              : document.getElementById('redo'),
    };
    _canvasObject.on(
        'object:modified', function(){
            updateCanvasState();
        }
    );

  _canvasObject.on(
        'object:added', function(){
            updateCanvasState();
        }
    );

  var addObject = function(){
     var rect = new fabric.Rect({
            left   : 100,
            top    : 100,
            fill   : 'red',
            width  : 200,
            height : 200
    });
        _canvasObject.add(rect);
        _canvasObject.setActiveObject(rect);
    _canvasObject.renderAll();
  }

    var updateCanvasState = function() {
        if((_config.undoStatus == false && _config.redoStatus == false)){
            var jsonData        = _canvasObject.toJSON();
            var canvasAsJson        = JSON.stringify(jsonData);
            if(_config.currentStateIndex < _config.canvasState.length-1){
                var indexToBeInserted                  = _config.currentStateIndex+1;
                _config.canvasState[indexToBeInserted] = canvasAsJson;
                var numberOfElementsToRetain           = indexToBeInserted+1;
                _config.canvasState                    = _config.canvasState.splice(0,numberOfElementsToRetain);
            }else{
            _config.canvasState.Push(canvasAsJson);
            }
        _config.currentStateIndex = _config.canvasState.length-1;
      if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
        _config.redoButton.disabled= "disabled";
      }
        }
    }


    var undo = function() {
        if(_config.undoFinishedStatus){
            if(_config.currentStateIndex == -1){
            _config.undoStatus = false;
            }
            else{
            if (_config.canvasState.length >= 1) {
            _config.undoFinishedStatus = 0;
              if(_config.currentStateIndex != 0){
                    _config.undoStatus = true;
                  _canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex-1],function(){
                                var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex-1]);
                            _canvasObject.renderAll();
                        _config.undoStatus = false;
                        _config.currentStateIndex -= 1;
                                _config.undoButton.removeAttribute("disabled");
                                if(_config.currentStateIndex !== _config.canvasState.length-1){
                                    _config.redoButton.removeAttribute('disabled');
                                }
                            _config.undoFinishedStatus = 1;
                });
              }
              else if(_config.currentStateIndex == 0){
                _canvasObject.clear();
                        _config.undoFinishedStatus = 1;
                        _config.undoButton.disabled= "disabled";
                        _config.redoButton.removeAttribute('disabled');
                _config.currentStateIndex -= 1;
              }
            }
            }
        }
    }

    var redo = function() {
        if(_config.redoFinishedStatus){
            if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
                _config.redoButton.disabled= "disabled";
            }else{
            if (_config.canvasState.length > _config.currentStateIndex && _config.canvasState.length != 0){
                    _config.redoFinishedStatus = 0;
                _config.redoStatus = true;
              _canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex+1],function(){
                            var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex+1]);
                        _canvasObject.renderAll();
                        _config.redoStatus = false;
                    _config.currentStateIndex += 1;
                            if(_config.currentStateIndex != -1){
                                _config.undoButton.removeAttribute('disabled');
                            }
                        _config.redoFinishedStatus = 1;
            if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
              _config.redoButton.disabled= "disabled";
            }
              });
            }
            }
        }
    }


    return {
        addObject  : addObject,
        undoButton : _config.undoButton,
        redoButton : _config.redoButton,
        undo       : undo,
        redo       : redo,
  }


  })();



  canvasDemo.undoButton.addEventListener('click',function(){
        canvasDemo.undo();
    });

    canvasDemo.redoButton.addEventListener('click',function(){
        canvasDemo.redo();
    });
  canvasDemo.addObject();
2
Abhinav

Je sais que la réponse est déjà choisie, mais voici ma version, le script est condensé et une réinitialisation à l'état d'origine a également été ajoutée. Après tout événement que vous souhaitez sauvegarder, appelez simplement saveState (); jsFiddle

    canvas = new fabric.Canvas('canvas', {
        selection: false
    });
function saveState(currentAction) {
    currentAction = currentAction || '';
    // if (currentAction !== '' && lastAction !== currentAction) {
        $(".redo").val($(".undo").val());
        $(".undo").val(JSON.stringify(canvas));
        console.log("Saving After " + currentAction);
        lastAction = currentAction;
    // }
    var objects = canvas.getObjects();
    for (i in objects) {
        if (objects.hasOwnProperty(i)) {
            objects[i].setCoords();
        }
    }
}
canvas.on('object:modified', function (e) {
   saveState("modified");
});
// Undo Canvas Change
function undo() {
    canvas.loadFromJSON($(".redo").val(), canvas.renderAll.bind(canvas));
}
// Redo Canvas Change
function redo() {
    canvas.loadFromJSON($(".undo").val(), canvas.renderAll.bind(canvas));
};
$("#reset").click(function () {
    canvas.loadFromJSON($("#original_canvas").val(),canvas.renderAll.bind(canvas));
});

var bgnd = new fabric.Image.fromURL('https://s3-eu-west-1.amazonaws.com/kienzle.dev.cors/img/image2.png', function(oImg){
    oImg.hasBorders = false;
    oImg.hasControls = false;
    // ... Modify other attributes
    canvas.insertAt(oImg,0);
    canvas.setActiveObject(oImg);
    myImg = canvas.getActiveObject();
    saveState("render");
    $("#original_canvas").val(JSON.stringify(canvas.toJSON()));
});

$("#undoButton").click(function () {
    undo();
});
$("#redoButton").click(function () {
    redo();
});
0
mathius1

Vous pouvez utiliser "objet: ajouté" et/ou "objet: supprimé" pour cela - fabricjs.com/events

Vous pouvez suivre ce post: Avons-nous un canevas événement modifié dans Fabric.js?

0