Pull to refresh

Создание фреймворка для Canvas: объекты и мышь

Canvas *


Среди вопросов на счёт 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. Более того этот топик — неплох в качестве обучения. Потому попрошу хотя бы в этот раз обойтись без холивара. А то вы уже надоели
Tags:
Hubs:
Total votes 80: ↑73 and ↓7 +66
Views 7.8K
Comments 16
Comments Comments 16

Posts