Среди вопросов на счёт 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. Более того этот топик — неплох в качестве обучения. Потому попрошу хотя бы в этот раз обойтись без холивара. А то вы уже надоели