
Среди вопросов на счёт Canvas чаще всего звучали вопросы по внутренностям фреймворков — как понять, что мышка находится над элементом, как это реализовано в фреймворках. В топике мы реализуем подобный Canvas фреймворк на базе AtomJS.
Глобальный интерфейс
Для начала придумаем интерфейс нашего фреймворка. Назовём его Canvas Framework, сокращённо — CF. Это будет глобальная переменная-фабрика для создания инстанса.
Первым аргументом мы будем передавать в неё ссылку на нужный элемент:
var cf = new CF('#my-canvas');
Реализация — простая:
window.CF = atom.Class({ initialize: function (canvas) { this.canvas = atom.dom( canvas ).first; this.ctx = this.canvas.getContext('2d'); } });
Затем мы можем создавать объекты, при помощи этой сущности:
cf.circle([50, 50, 10] , { fill: 'red' , hover : { fill: 'blue' } }); cf.rect ([10, 10, 20, 20], { fill: 'green', hover : { fill: 'blue' } });
Для простоты все объекты у нас будут тягаться и слушать события мыши.
Реализация фигур
Теперь нам необходимо определить базовый класс фигуры.
// Абстрактный класс фигур var Shape = atom.Class({ Implements: [ atom.Class.Events, atom.Class.Options ], cf : null, data : null, hover: false, path: atom.Class.abstractMethod, initialize: function (data, options) { this.data = data; this.setOptions( options ); }, hasPoint: function (x, y) { var ctx = this.cf.emptyCanvas.ctx; this.path( ctx ); return ctx.isPointInPath(x, y); }, draw: function () { var ctx = this.cf.ctx, o = this.options; this.path( ctx ); ctx.save(); ctx.fillStyle = this.hover ? o.hover.fill : o.fill; ctx.fill(); ctx.restore(); } });
Вы видите, что нам понадобится некий emptyCanvas — это будет скрытый Canvas, в который мы будем отрисовывать наши пути, чтобы не нарушать пути «боевого» холста. Обновим конструктор CF:
window.CF = atom.Class({ initialize: function (canvas) { [...] this.emptyCanvas = atom.dom.create( 'canvas', { width: 1, height: 1 }).first; this.emptyCanvas.ctx = this.emptyCanvas.getContext('2d'); } });
Каждая наследующая фигура должна будет только реализовать метод path. Давайте добавим пару фигур — Rectangle и Circle
// circle.data == [x, y, radius] var Circle = atom.Class({ Extends: Shape, path: function (ctx) { ctx.beginPath(); ctx.arc( this.data[0], this.data[1], this.data[2], 0, Math.PI * 2, false ); ctx.closePath(); } }); var Rect = atom.Class({ Extends: Shape, path: function (ctx) { ctx.beginPath(); ctx.rect.apply( ctx, this.data ); ctx.closePath(); } });
Следующее, что нам необходимо сделать — это реализовать Mouse. Мы подпишемся на событие mousemove элемента Canvas и будем запоминать положение курсора. Мышь будет получать элементы Shape, которые будет проверять, менять им hover и вызывать у них евенты mousedown и mouseup. Вы можете видеть, что мы столкнулись с лёгкой некроссбраузерностью — код layerX/Y нету в Опере и там необходимо использовать offsetX/Y. Не критично, но, главное, знать об этом)
var Mouse = atom.Class({ x: 0, y: 0, initialize: function (canvas) { this.elements = []; canvas.bind({ mousemove: this.move.bind(this), mousedown: this.fire.bind(this, 'mousedown'), mouseup: this.fire.bind(this, 'mouseup' ) }); }, add: function (element) { this.elements.push( element ); }, move: function (e) { // Мы воспользуемся layer*, но на практике нужен более надёжный способ if (e.layerX == null) { // opera this.x = e.offsetX; this.y = e.offsetY; } else { // fx, chrome this.x = e.layerX; this.y = e.layerY; } this.elements.forEach(function (el) { el[i].hover = el[i].hasPoint(this.x, this.y) }.bind(this)); }, fire: function (name, e) { this.elements.forEach(function (el) { if (el.hasPoint(this.x, this.y)) { el.fireEvent(name, e); } }.bind(this)); } }); // Добавляем мышь в конструктор: window.CF = atom.Class({ initialize: function (canvas) { [...] this.mouse = new Mouse( this.canvas ); } });
Теперь необходима обновлялка холста.
window.CF = atom.Class({ initialize: function (canvas) { [...] // 25 fps this.update.periodical( 1000/25, this ); }, update: function (shapes) { this.ctx.clearRect(0,0,this.canvas.width, this.canvas.height); this.elements.invoke('draw'); } });
Редактируем наш глобальный объект так, чтобы мы могли создавать элементы:
window.CF = atom.Class({ [...], elements: [], _shape: function (Class, args) { var e = new Class(args[0], args[1]); this.mouse.add( e ); this.elements.push( e ); e.cf = this; return e; }, circle: function (data, options) { return this._shape(Circle, arguments); }, rect: function (data, options) { return this._shape(Rect, arguments); } })
Всё, создаём наше приложение:
var write = function (msg) { atom.dom.create('p').text(msg).appendTo('body'); }; var cf = new CF('canvas'); cf.circle([50, 50, 10] , { fill: 'red' , hover : { fill: 'blue' } }) .addEvent('mousedown', write.bind( window, 'circle mousedown' )); cf.rect ([10, 10, 20, 20], { fill: 'green', hover : { fill: 'blue' } }) .addEvent('mousedown', write.bind( window, 'rect mousedown' ));
Результат
Заключение
На самом деле, за фреймворками заключается намного больше, чем описано в этой статье. К примеру, описанный пример будет рулить только на абсолютно спозиционированных холстах, также там есть куча оптимизаций, нюансов и т.д. Необходимо подстраивать fps под приложение, обновлять холст только при изменениях, обновлять статусы не при движениях мыши, а перед отрисовкой и т.д. Это тяжёлый и кропотливый труд. Лучше воспользоваться чем-то готовым, чем реализовывать их с нуля.
Между прочим, есть альтернативный путь — использование map + area. У него есть свои преимущества, но и недостатки. Это трудности в синхронизации и, главное, невозможность реализации более сложных фигур.
Любители потроллить на тему SVG — удержитесь. Есть причины использовать Canvas. Более того этот топик — неплох в качестве обучения. Потому попрошу хотя бы в этот раз обойтись без холивара. А то вы уже надоели
