Canvas
10 June 2011

Основы LibCanvas — теория



Добрый день. Один из самых частых вопросов про LibCanvas сейчас — «С чего начать?». Согласен, порог вхождения в эту библиотеку чуть выше, чем в более простые canvas-библиотеки, потому в этом топике я раскрою основы LibCanvas — базовые понятия и принципы, отрисовка примитивных фигур, события мыши, клавиатуры, анимация, расширенный контекст, поведения. Постараюсь описать всё это с множеством примеров и максимально доступным языком.

Надеюсь, статья даст ответы на вопросы: Что такое LibCanvas? Зачем он нужен и какие его преимущества? С чего начать?

В этой статье будет только теория, а как применить эти знания на практике раскрыто в следующей статье

Общие сведения


LibCanvas — фреймворк для работы с Canvas и сопутствующими технологиями, который может применяться для разработки игр и других интерактивных приложений.

Он построен на базе AtomJS — лёгкого JavaScript фреймворка, чем-то похожего на MooTools и jQuery. Есть вполне неплохая английская документация AtomJS и, если вы использовали MooTools до этого, то вам будет достаточно просто осилить AtomJS.

Последнюю версию LibCanvas можно получить в репозитории, на GitHub также есть ряд актуальных примеров от очень простых до достаточно сложных. Многие принципы можно понять, изучив эти примеры. Есть русская документация, но многие части LibCanvas в ней пока не освещены. Со временем она будет наполнятся и расширяться. Надеюсь, кто-то мне поможет с переводом на английский)

Core


Весь код хранится в пространстве имен LibCanvas. Это хорошо тем, что библиотека не захламляет глобальное пространство имён. Тем не менее, есть и недостаток — достаточно многословный синтаксис в итоге:
var circle = new LibCanvas.Shapes.Circle( 100, 100, 50 );
var circle = new LibCanvas.Shapes.Circle( 222, 222, 22 );


Это можно исправить используя статический метод LibCanvas.extract(). Он глобализует сам LibCanvas, чтобы в своём приложении вы могли использовать короткие имена классов:
LibCanvas.extract();

var circle = new Circle( 100, 100, 50 );
var circle = new Circle( 222, 222, 22 );


Другая альтернатива — использовать алиасы:
var Circle = LibCanvas.Shapes.Circle;

var circle1 = new Circle( 100, 100, 50 );
var circle2 = new Circle( 222, 222, 22 );


LibCanvas.Context2D


Есть встроенный контекст LibCanvas. Его очень просто вызвать:
var context = document.getElementsByTagName('canvas')[0].getContext('2d-libcanvas');
// or:
var context = atom.dom('canvas').first.getContext('2d-libcanvas');


Обратите внимание, что оригинальный '2d' контекст всё так же доступен и не тронут, потому может смело использоваться в ваших приложениях:
var context = atom.dom('canvas').first.getContext('2d');


'2d-libcanvas' контекст обратно совместим с оригинальным контекстом(весь код, написанный для контекста '2d' заработает и в контексте '2d-libcanvas'), но он имеет следующие преимущества:
1. Chainable — все методы можно вызывать цепочкой. Особо популярен такой метод стал с появлением jQuery:
context
	.set({
		  fillStyle: 'black',
		strokeStyle: 'red'
	})
	.  fillRect(20, 20, 40, 40)
	.strokeRect(50, 50, 100, 100);


2. Именованные аргументы — теперь можно передавать не просто набор символов, а хеш:
context.drawImage(img, 10, 15, 40, 45, 20, 25, 50, 55);
// vs
context.drawImage({
	image: img,
	crop : [10, 15, 40, 45],
	draw : [20, 25, 50, 50]
});


3. Фигуры — можно передавать фигуры, а не числа. Это особо удобно, когда у вас есть большое приложение с созданными объектами:
// отрисовка картинки:
	context.drawImage( image, rect.from.x, rect.from.y, rect.width, rect.height );
	// vs
	context.drawImage( image, rect );

// Заливка прямоугольника с сохранением состояния холста:
	context.save();
	context.fillStyle = 'red';
	context.fillRect( rect.from.x, rect.from.y, rect.width, rect.height )
	context.restore();
	// vs:
	context.fill( rect, 'red' );


4. Расширение API — тут целая серия удобств. Во-первых, более удобная работа с путями, текстом, картинками, трансформациями и т.д:
// Изображение с центром в определённой точке, повернутое вокруг оси:
	// original ctx:
	context.save();
	context.translate(this.position.x, this.position.y);
	context.rotate(this.angle);
	context.translate(-this.image.width/2, -this.image.height/2);
	context.drawImage(this.image, 0, 0);
	context.restore();
	
	// vs
	context.drawImage({
		image : this.image,
		center: this.position,
		angle : this.angle
	});

// Текст:
	context.text({
		text: 'Test string \n with line breaks \n is here'
		padding: [ 30, 50 ],
		size: 20,
		align: 'center'
	})

// Крутим холст вокруг оси:
	context.translate( point.x,  point.y);
	context.rotate(angle);
	context.translate(-point.x, -point.y);

	// vs:
	context.rotate( angle, point );

// Рисуем путь
	context.beginPath( );
	context.moveTo( mt.x, mt.y );
	context.lineTo( lt.x, lt.y );
	context.bezierCurveTo( bc1.x, bc1.y, bc2.x, bc2.y, bc.x, bc.y );
	context.quadraticCurveTo( qc1.x, qc1.y, qc.x, qc.y );
	context.closePath();
	
	// vs
	context
		.beginPath( mt )
		.lineTo( lt );
		.curveTo( bc, bc1, bc2 )
		.curveTo( qc, qc1 )
		.closePath();

// Клипаем круг:
	var circle = new Circle( 130, 120, 50 );

	context.beginPath();
	context.arc( circle.center.x, circle.center.y, circle.radius, 0, Math.PI * 2 );
	context.closePath();
	context.clip();
	
	// vs:
	context.clip( circle );
	
// Очищаем весь холст:
	context.clear( 0, 0, canvas.width, canvas.height );
	// vs
	context.clearAll();


И так далее. Думаю, вы и сами видите удобство встроенного контекста.

Объект LibCanvas


При конструировании LibCanvas создаётся объект LibCanvas.Canvas2D. Первым аргументом вы должны передать ссылку на необходимый элемент canvas (css-селектор, dom-объект, etc). Вторым можно передать дополнительные настройки — предельный fps, очистку перед перерисовкой, предзагрузку картинок и другие.
var libcanvas = new LibCanvas('#my-canvas');

libcanvas instanceof LibCanvas; // true
libcanvas instanceof LibCanvas.Canvas2D; // true

// в свойстве можно получить расширенный контекст:
libcanvas.ctx instanceof LibCanvas.Context2D; // true


Каждый кадр состоит из двух этапов. Первый — просчёт данных. Он выполняется каждый раз и отвечает исключительно за математические операции — передвижение объектов, коллизии и т.п. В этом слое не должно быть никакой перерисовки. Второй этап — это рендер. В ней находится часть, которая отвечает за перерисовку содержимого экрана и она будет выполнена только в случае каких-либо изменений на экране. Об этом можно сообщить на этапе просчёта вызовом метода libcanvas.update().

Добавить функцию в этап просчёта можно при помощи метода libcanvas.addFunc(), добавить функцию в этап рендера можно при помощи метода libcanvas.addRender(). Также, на этапе рендера вызываются методы draw переданных объектов. Приблизительно код выглядит так:

libcanvas
	.addFunc(function () {
		scene.recount();
		if (scene.somethingChanged()) {
			libcanvas.update();
		}
	})
	.addRender(function () {
		// будет вызвано только после вызова libcanvas.update();
		scene.drawAll();
	});


Очень много приложений — статичны большую часть времени с перерисовкой только в моменты действия пользователя. Это поможет значительно сократить безсмысленную нагрузку на процессор.

На практике addRender используется редко, т.к. очень удобно отрисовывать объекты методом draw() (об этом ниже).

Всегда перерисовывайте что-то на экране только в случае присутствия изменений. Во многих приложениях такого базового механизма будет недостаточно, но это лучше, чем ничего.

Point


LibCanvas.Point — один из базовых объектов. Он используется очень часто, является составляющей всех фигур и очень удобен для использования вне их. В нём есть методы для определения растояния между двумя точками, углом, умножением точки, а также получением всех соседей.

// Проворачиваем точку A на 60 градусов вокруг точки B:
	var A = new Point(10, 10),
	    B = new Point(20, 20);

	A.rotate( (60).degree(), B );

// считаем сумму значений всех соседей клетки в матрице:
	var sum = 0 +
		matrix[p.y-1][p.x-1] + matrix[p.y-1][p.x] + matrix[p.y-1][p.x+1] +
		matrix[p.y  ][p.x-1] +                      matrix[p.y  ][p.x+1] +
		matrix[p.y+1][p.x-1] + matrix[p.y+1][p.x] + matrix[p.y+1][p.x+1] ;

	// vs
	var sum = point.neighbours.reduce(function(value, p) { return value + matrix[p.y][p.x]; }, 0);


Фигуры


Фигуры содержатся в подпространстве имён LibCanvas.Shapes.* и глобализуются до коротких алиасов. Самые известные фигуры — это Rectangle, Circle, Line. Когда используете LibCanvas — вы должны осознать, что фигуры сами по себе не имеют внешнего вида, они не могут иметь внешнего вида — цвета или тени. За внешний вид отвечает объект, который использует фигуру, например LibCanvas.Ui.Shaper, сами же фигуры содержат в себе только математические операции — как пройти путь, пересечения, находится ли точка внутри фигуры, etc. Они — астральное, но не физическое тело.

Это позволяет отделить поведение от внешнего вида. К примеру, у нас есть доска в арканоиде. На самом деле это картинка, но все действия мы можем производить как с простой фигурой:

var Unit = atom.Class({
	initialize: function (rectangle, image) {
		this.shape = rectangle;
		this.image = image;
	},
	collision: function (anotherUnit) {
		return this.shape.intersect( anotherUnit.shape );
	},
	draw: function () {
		this.libcanvas.ctx.drawImage( this.image, this.shape );
	}
});


Rectangle — самая главная фигура. Она используется не только во время отрисовки прямоугольников и базовых математических операциях, но и во многих методах LibCanvas. Это может быть, например, метод context.drawImage, который принимает аргументами для вырезания и отрисовки прямоугольник или тайловый движок, у которого каждый элемент — это небольшой Rectangle.

Когда какому-нибудь методу требуется Rectangle-like аргумент — он может принять любой аргумент, похожий на прямоугольник. Например:
context.drawImage({
	image: image,
	crop: {
		from: { x: 15, y: 10 },
		size: { width: 50, height: 100 }
	},
	draw: [10,20,100,200]
});


В таком случае crop и draw будут приведены внутри к Rectangle (или к другой необходимой фигуре), но с точки зрения производительности (при многократной перерисовке холста), а также с точки зрения архитектуры приложения — самый выгодный метод — это создание всех объектов во время инициализации приложения. Такое решение было принято специально для того, чтобы поощрять хорошую архитектуру.

var Item = atom.Class({
	initialize: function (image) {
		this.image = image;
		this.cropRect = new Rectangle(15, 10,  50, 100);
		this.drawRect = new Rectangle(10, 20, 100, 200);
	},
	draw: function () {
		context.drawImage({
			image: this.image,
			crop : this.cropRect,
			draw : this.drawRect
		});
	}
});


Аналогичным образом используются и другие фигуры:
// Дуга:
context.arc({
	circle: new Circle( 100, 100, 50 ),
	angle : [ (45).degree(), (135).degree() ]
});

// Отрисовка линии:
context.stroke( new Line([13, 13], [42, 42]), 'red' );


Поведения


Следующая часть — это LibCanvas.Behaviors.*. Каждое из них — это просто примесь, которая добавляет вашему классу определённую функциональность или поведение. К примеру, Animatable добавляет метод animate который позволяет изменять свойства объекта плавно, а Drawable позволяет объектам вашего класса быть добавленными в объект LibCanvas для отрисовки.

Между прочим, именно Drawable и является основой отрисовки в LibCanvas. Смесь Drawable и Shapes.* позволит отрисовать на холст любую фигуру, а добавление других поведений придаст этой фигуре дополнительную функциональность.
var Item = atom.Class({
	Implements: [ Drawable, Draggable ],
	initialize: function (shape) {
		this.shape = shape;
	},
	draw: function () {
		this.libcanvas.ctx.stroke( this.shape, 'red' );
	}
});

libcanvas.addElement(
	new Item( new Rectangle(50, 50, 100, 100) ).draggable()
);


На самом деле — подобный паттерн для отрисовки фигур приходилось создавать достаточно часто, потому уже реализован Ui.Shaper:

libcanvas.createShaper({
	shape : new Rectangle(50, 50, 100, 100),
	stroke: 'red'
}).draggable();


Клава и мышка


Работа с клавиатурой достаточно проста. Достаточно при инициализации приложения вызвать метод libcanvas.listenKeyboard() и вы можете использовать метод libcanvas.getKey( keyName ) при необходимости, чтобы узнать состояние клавиши:

update: function () {
	if( this.libcanvas.getKey('aup') ) {
		this.move();
	}
}


Работу с мышью стоит разобрать. Во-первых, если вы хотите использовать мышь в своём приложении — обязательно вызовите метод libcanvas.listenMouse(). В целях оптимизации события мыши не анализируются до его вызова, ведь есть приложения, которым мышь не нужна. После этого можно легко подписываться на события мыши, добавляя элемент в объект Mouse:
this.libcanvas.mouse.subscribe( element );


Важно, чтобы значением свойства shape элемента была одна из фигур (LibCanvas.Shapes.*), было свойство zIndex и он реализовал класс atom.Class.Events. На практике всё это скрыто за поведениями и когда вы вызываете, например, метод draggable() поведения Draggable объект автоматически подписывается на события мыши. Если же вам надо только слушать события мыши, то достаточно реализовать поведение MouseListener и вызвать метод listenMouse у элемента. Тем не менее, всё-еще остаётся самый главный момент — у элемента дожно быть свойство Shape с какой-либо фигурой внутри. Когда события мыши у вашего объекта слушаются — вы можете подписаться на любое из следующих событий:

/*
	- click
	- mouseover
	- mousemove
	- mouseout
	- mouseup
	- mousedown
	- away:mouseover
	- away:mousemove
	- away:mouseout
	- away:mouseup
	- away:mousedown
*/ // Например:

element
	.listenMouse()
	.addEvent('click', function () {
		alert('element clicked');
	});


Заключение


Я описал здесь основы теоретической части разработки на LibCanvas. В ней не раскрыты многие интересные возможности и принципы, но её цель — объяснить идеологию и показать читателю, с чего начать.

Тема следующей статьи — практическая часть разработки на LibCanvas.

+73
13.8k 187
Comments 28