В этой статье мы собираемся создать простое приложение перетаскивания с использованием EaselJS и Backbone.js . Backbone придаст структуру нашему приложению, предоставляя модели , коллекции и представления . Easel упростит работу с элементом HTML5 canvas
. Хотя нам не обязательно нужен Backbone для такого простого приложения, интересно начать с Backbone таким образом.
Начать
Сначала мы создаем нашу структуру каталогов следующим образом:
. |-- index.html +-- js |-- main.js |-- models | +-- stone.js +-- views +-- view.js
Далее в index.html
включите файлы JavaScript и элемент canvas
, как показано в следующем примере кода. Как только это будет сделано, мы готовы манипулировать 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, у нас будут привязки значения ключа и пользовательские события в этой модели. Это означает, что мы можем прослушивать изменения свойств модели и соответственно отображать наше представление. Коллекция Backbone, это упорядоченный набор моделей. Вы можете связать события change
чтобы получать уведомления при изменении любой модели в коллекции. Теперь давайте создадим модель камня и коллекцию камней. Следующий код принадлежит js/models/stone.js
.
var Stone = Backbone.Model.extend({ }); var StoneCollection = Backbone.Collection.extend({ model: Stone });
Инициализируйте Магистральное Представление Используя EaselJS
Магистральные представления ничего не определяют о HTML и могут использоваться с любой библиотекой шаблонов JavaScript. В нашем случае мы не используем библиотеку шаблонов. Вместо этого мы манипулируем canvas
. Вы можете привязать функцию render()
вашего представления к событию change
модели, чтобы при изменении данных модели представление автоматически обновлялось.
Чтобы начать работу с Easel, мы создаем сцену, которая оборачивает элемент canvas
, и добавляем объекты как дочерние элементы. Позже мы переходим этот этап к нашему основному представлению. Код в js/main.js
который выполняет это, показан ниже.
$(document).ready(function() { var stage = new createjs.Stage("testcanvas"); var view = new CanvasView({stage: stage}).render(); });
Мы создали наш CanvasView
и вызвали его функцию render()
для его рендеринга. Мы вернемся к реализации render()
ближайшее время. Во-первых, давайте посмотрим на нашу функцию initialize()
, которая определена в 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()
прослушивает изменения модели / коллекции и вызывает функцию, переданную в качестве второго аргумента. Мы передаем контекст, в котором вызывается функция, в качестве третьего аргумента. Когда мы добавляем камень в нашу коллекцию, событие add
отправит this.renderStone()
и передаст новый камень функции. Точно так же, когда коллекция сбрасывается, событие reset
отправляет this.renderRake()
. Благодаря реализации этих функций рендеринга представление всегда будет синхронизировано с коллекцией.
Рендеринг Представления
Функция render()
, показанная ниже, просто вызывает this.renderRake()
и обновляет сцену.
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); },
Метод renderRake()
, который также хранится в js/views/view.js
, показан ниже.
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()
делает две вещи. Во-первых, он рисует на холсте грабли (розовый прямоугольник) и создает click
обработчик на нем. Во-вторых, он пересекает коллекцию камней и вызывает renderStone()
для каждого элемента. Обработчик click
добавляет новый камень в коллекцию.
Далее давайте посмотрим на функцию renderStone()
.
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; }; },
Мы вызвали buildDraggable()
чтобы сделать камень перетаскиваемым. Мы увидим, как реализовать это дальше. Но сначала давайте рассмотрим, как работает наш базовый вид. CanvasView
прослушивает событие add
коллекции, а когда добавляется новый камень, он вызывает renderStone()
. Метод render()
визуализирует грабли и вызывает renderStone()
для каждого камня в коллекции. При нажатии на грабли новая модель камня добавляется в коллекцию камней, а затем вызывается renderStone()
для нового камня.
Теперь давайте посмотрим на buildDraggable()
которая реализует функцию перетаскивания:
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); } }); }; // ... },
И для ограничения привязки камня к граблям, вот последние функции полезности, которые нам нужны.
// 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 sx = x; sy = y; } // update baseView.stage.update(); }; // calculate x position to snap the rake function snapX(x) { if (x < 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 < 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(sx), snapY(sy), !animateDisabled); };
Вывод
В заключение, Backbone не ограничивается манипулированием DOM и может использоваться везде, где требуется структура представления модели. Хотя его можно использовать для создания одностраничных приложений, он не является полной структурой, и мы увидели только одну сторону Backbone в этой статье. Если вам нравится использовать Backbone для крупномасштабных приложений, я предлагаю использовать Marionette.js , который решает некоторые примитивные проблемы с Backbone.
Полный код этой статьи можно найти на GitHub . Живая демоверсия также доступна на Heroku . Чтобы начать, просто нажмите на розовую область, чтобы создать перетаскиваемый камень. Камень будет перетаскиваться, и он будет ограничен внутри розовой области.