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