Добрый день. Один из самых частых вопросов про 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.