Implementing Drag and Drop Using Backbone and EaselJS

Emre Guneyler
Share

In this article, we are going to build a simple drag and drop application using EaselJS and Backbone.js. Backbone will give structure to our application by providing models, collections, and views. Easel will make working with the HTML5 canvas element easy. Although we don’t necessarily need Backbone for such a simple application, it is fun to get started with Backbone in this way.

Get Started

First, we create our directory structure as follows:

.
|-- index.html
+-- js
    |-- main.js
    |-- models
    |   +-- stone.js
    +-- views
        +-- view.js

Next, in index.html include the JavaScript files and a canvas element, as shown in the following code sample. Once this is done, we’re ready to manipulate the canvas.

<body>
  <!-- Canvas Element -->
  <canvas id="testcanvas" height="640" width="480"/>

  <script src="/bower_components/jquery/jquery.min.js"></script>
  <!-- underscore is needed by backbone.js -->
  <script src="/bower_components/underscore/underscore-min.js"></script>
  <script src="/bower_components/backbone/backbone.js"></script>
  <script src="/bower_components/easeljs/lib/easeljs-0.7.1.min.js"></script>
  <!-- tweenjs is for some animations -->
  <script src="/bower_components/createjs-tweenjs/lib/tweenjs-0.5.1.min.js"></script>
  <script src="/js/models/stone.js"></script>
  <script src="/js/views/view.js"></script>
  <script src="/js/main.js"></script>
</body>

Backbone Models

By creating a Backbone model, we will have key-value bindings and custom events on that model. This means we can listen to changes for model properties and render our view accordingly. A Backbone collection, is an ordered set of models. You can bind change events to be notified when any model in the collection changes. Next, let’s create a stone model, and a stone collection. The following code belongs in js/models/stone.js.

var Stone = Backbone.Model.extend({

});

var StoneCollection = Backbone.Collection.extend({
  model: Stone
});

Initialize the Backbone View Using EaselJS

Backbone views don’t determine anything about HTML, and can be used with any JavaScript templating library. In our case we aren’t using a templating library. Instead, we manipulate the canvas. You can bind your view’s render() function to the model’s change event so that when the model data changes, the view is automatically updated.

To get started with Easel, we create a stage that wraps the canvas element, and add objects as children. Later, we pass this stage to our backbone view. The code in js/main.js that accomplishes this is shown below.

$(document).ready(function() {
  var stage = new createjs.Stage("testcanvas");
  var view = new CanvasView({stage: stage}).render();
});

We’ve created our CanvasView and called its render() function to render it. We will revisit the implementation of render() shortly. First, let’s see our initialize() function, which is defined in js/views/view.js.

var CanvasView = Backbone.View.extend({
  initialize: function(args) {
    // easeljs stage passed as argument.
    this.stage = args.stage;
    // enableMouseOver is necessary to enable mouseover event http://www.createjs.com/Docs/EaselJS/classes/DisplayObject.html#event_mouseover
    this.stage.enableMouseOver(20);

    // stone collection
    this.collection = new StoneCollection();

    // bounds of pink area and our stones. the pink area is called "rake".
    this.rakeOffsets = {
      x: 10,
      y: 400,
      height: 150,
      width: 300,
      stoneWidth: 50,
      stoneHeight: 50
    };

    // listen to collection's add remove and reset events and call the according function to reflect changes.
    this.listenTo(this.collection, "add", this.renderStone, this);
    this.listenTo(this.collection, "remove", this.renderRake, this);
    this.listenTo(this.collection, "reset", this.renderRake, this);
  },
  //...
});

listenTo() listens for model/collection changes and calls the function passed as the second argument. We pass the context the function is being called in as a third argument. When we add a stone to our collection, an add event will dispatch this.renderStone() and pass the new stone to the function. Similarly, when the collection is reset, a reset event will dispatch this.renderRake(). By implementing these render functions, the view will always be in sync with the collection.

Rendering the View

The render() function, shown below, just calls this.renderRake() and updates the stage.

render: function() {
  this.renderRake();

  // stage.update is needed to render the display to the canvas.
  // if we don't call this nothing will be seen.
  this.stage.update();

  // The Ticker provides a centralized tick at a set interval.
  // we set the fps for a smoother animation.
  createjs.Ticker.addEventListener("tick", this.stage);
  createjs.Ticker.setInterval(25);
  createjs.Ticker.setFPS(60);
},

The renderRake() method, which is also stored in js/views/view.js, is shown below.

renderRake: function() {
  // http://stackoverflow.com/questions/4886632/what-does-var-that-this-mean-in-javascript
  var that = this;

  // create the rake shape
  var rakeShape = new createjs.Shape();

  rakeShape.graphics.beginStroke("#000").beginFill("#daa").drawRect(this.rakeOffsets.x, this.rakeOffsets.y, this.rakeOffsets.width, this.rakeOffsets.height);

  // assign a click handler
  rakeShape.on("click", function(evt) {
    // When rake is clicked a new stone is added to the collection.
    // Note that we add a stone to our collection, and expect view to reflect that.
    that.collection.add(new Stone());
  });

  // add the shape to the stage
  this.stage.addChild(rakeShape);

  // a createjs container to hold all the stones.
  // we hold all the stones in a compound display so we can
  // easily change their z-index inside the container,
  // without messing with other display objects.
  this.stoneContainer = new createjs.Container();
  this.stage.addChild(this.stoneContainer);

  // for each stone in our collection, render it.
  this.collection.each(function(item) {
    this.renderStone(item);
  }, this);
},

renderRake() does two things. First, it renders the rake shape (pink rectangle) on the canvas, and creates a click
handler on it. Second, it traverses the stone collection and calls renderStone() on each item. The click handler adds a new stone to the collection.

Next, let’s look at the renderStone() function.

renderStone: function(model) {
  // var that = this;
  var baseView = this;

  // build the stone shape
  var stoneShape = buildStoneShape();

  // make it draggable
  // the second argument is a callback called on drop
  // we snap the target stone to the rake.
  buildDraggable(stoneShape, function(target, x, y) {
    rakeSnap(target, false);
  });

  // add the stone to the stage and update
  this.stoneContainer.addChild(stoneShape);
  this.stage.update();

  function buildStoneShape() {
    var shape = new createjs.Shape();

    shape.graphics.beginStroke("#000").beginFill("#ddd").drawRect(0, 0, baseView.rakeOffsets.stoneWidth, baseView.rakeOffsets.stoneHeight);
    return shape;
  };
},

We’ve called the buildDraggable() function to make the stone draggable. We will see how to implement that next. But first, let’s review how our backbone view works. The CanvasView listens to the collection’s add event, and when a new stone is added, it calls renderStone(). The render() method renders the rake and calls renderStone() on each stone in the collection. When the rake is clicked, a new stone model is added to the stone collection, and then renderStone() is called on the new stone.

Now, let’s look at the buildDraggable() function that implements the drag and drop functionality:

renderStone: function(model) {
  // ...

  function buildDraggable(s, end) {
    // on mouse over, change the cursor to pointer
    s.on("mouseover", function(evt) {
      evt.target.cursor = "pointer";
    });

    // on mouse down
    s.on("mousedown", function(evt) {
      // move the stone to the top
      baseView.stoneContainer.setChildIndex(evt.target, baseView.stoneContainer.getNumChildren() - 1);

      // save the clicked position
      evt.target.ox = evt.target.x - evt.stageX;
      evt.target.oy = evt.target.y - evt.stageY;

      // update the stage
      baseView.stage.update();
    });

    // on mouse pressed moving (drag)
    s.on("pressmove", function(evt) {
      // set the x and y properties of the stone and update
      evt.target.x = evt.target.ox + evt.stageX;
      evt.target.y = evt.target.oy + evt.stageY;
      baseView.stage.update();
    });

    // on mouse released call the end callback if there is one.
    s.on("pressup", function(evt) {
      if (end) {
        end(evt.target, evt.stageX + evt.target.ox, evt.stageY + evt.target.oy);
      }
    });
  };
  // ...
},

And for the constraint of snapping the stone to the rake, here are the final utility functions we need.

// drag the stone, either by animating or not
function dragStone(s, x, y, animate) {
  if (animate) {
    // Use tween js for animation.
    createjs.Tween.get(s).to({x: x, y: y}, 100, createjs.Ease.linear);
  } else {
    // set x and y attributes without animation
    s.x = x;
    s.y = y;
  }

  // update
  baseView.stage.update();
};

// calculate x position to snap the rake
function snapX(x) {
  if (x &lt; baseView.rakeOffsets.x) {
    x = baseView.rakeOffsets.x;
  } else if (x > baseView.rakeOffsets.x + baseView.rakeOffsets.width - baseView.rakeOffsets.stoneWidth) {
    x = baseView.rakeOffsets.x + baseView.rakeOffsets.width - baseView.rakeOffsets.stoneWidth;
  }

  return x;
};

// calculate y position to snap the rake
function snapY(y) {
  if (y &lt; baseView.rakeOffsets.y) {
    y = baseView.rakeOffsets.y;
  } else if (y > baseView.rakeOffsets.y + baseView.rakeOffsets.height - baseView.rakeOffsets.stoneHeight) {
    y = baseView.rakeOffsets.y + baseView.rakeOffsets.height - baseView.rakeOffsets.stoneHeight;
  }

  return y;
};

// drag stone within the rake bounds. animation is disabled if second argument is given. animation is enabled by default
function rakeSnap(s, animateDisabled) {
  dragStone(s, snapX(s.x), snapY(s.y), !animateDisabled);
};

Conclusion

In conclusion, Backbone is not restricted to DOM manipulation, and can be used anywhere that needs model-view structure. Though it can be used to build single page applications, it is not a complete framework, and we have only seen one side of Backbone in this article. If you like to use Backbone for large scale applications, I suggest using Marionette.js, which handles some primitive problems with Backbone.

The full code for this article can be found on GitHub. A live demo is also available on Heroku. To get started, just click on the pink area to create a draggable stone. The stone will be draggable, and it will be constrained inside the pink area.