Основы 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.
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 28

      +3
      Спасибо за статью. Никак не доходили руки изучить, теперь надеюсь дойдут.
        0
        Behavoiurs так и называется или это опечатка в статье?
          +1
          Опечатка, корректно — Behaviors.
          –1
          Кругом этот GitHub…

          Планируется ли сделать человеческий сайт для человеков а-ля jquery.com, где документация написана человеческим языком и рассчитана на широкий круг пользователей? Может я один такой тугой, но вот лично для меня порог вхождения на сам GitHub высок, потому как вижу я его впервые (ну ладно вру — второй раз в жизни).
            +2
            Планируется.
            Но я вам всё-равно рекомендую изучить GitHub, не пожалеете ;)
              0
              А я за mercurial. Давайте похоливарим (:
                +2
                А что тут холиварить то? Что Git, что Hg — практически идентичны, разница в инфраструктуре и сообществе. Я раньше был на Google Code с Hg, потом перенес все на GitHub. Он и мощнее и практически все JS-прогеры хостятся именно там.
                  +1
                  Да я пошутил, что вы прям. Даже смайлик в конце специально добавил (:
                  Пользуюсь и тем и другим.
                    +1
                    Так я же не сержусь, просто высказал мнение)
            0
            А есть режим автоокругления координат фигур, чтобы при его включении они точно попадали в пиксельный растр?
              0
              У меня есть тестовый point.snapToPixel(). Я его использую, например, тут:
              libcanvas.github.com/shapes/rectangle/creating.html
              libcanvas.github.com/shapes/rectangle/points.html

              Но я пока не определился с корректным интерфейсом и реализацией.
                0
                Да, дело там сложнее, чем кажется на первый взгляд.

                Во-первых, нужен глобальный триггер и некий локальный параметр для отдельного вызова.
                Во-вторых, выбор способа округления — только вниз, только вверх, к ближайшему.
                В-третьих, (в идеале) хочется развить это и сделать ещё выравнивание границ шейпов по аналогии с графическими программами (inside/center/outside). Опять-таки с глобальным параметром.

                Чтобы можно было, скажем, в начале установить .snapToPixel = true, .borderAlignment = 'inside' — и гнать блоки конвейером :)
                  0
                  Угу, я приблизительно так и представлял, но за реализацию пока не брался. Хотя согласен — это очень интересная идея. Кажется, это необходимо добавить к контексту (по аналогии с fillRect и иже с ними):
                  context = canvas.getContext('2d-libcanvas');
                  context.save();
                  context.snapToPixel = true;
                  context.borderAligment = 'inside';
                  context.stroke( shape );
                  context.restore();
                  


                  На самом деле передать параметры везде, куда необходимо — совсем не проблема. Я пока не совсем разобрался с деталями реализации. Необходимо будет написать хорошее приложение, где snapToPixel нужен, чтобы разобраться. Пока такие не встречались.

                  Если бы был желающий это сделать — я бы с удовольствием помог консультациями и кодом.
                    0
                    У меня бродят неопределенные мысли на эту тему, но я пока в JS вообще и Canvas в частности чувствую себя не слишком свободно. Учусь.

                    Кстати, про округления: нужно даже не три, а пять режимов — директивно к каждому из 4-х углов холста и к ближайшему пикселу.

                    Ещё непонятно, как рисовать границу, скажем, при выравнивании 'center' и толщине линии 3px… Когда ровно пополам не делится. Действительно, много тонких моментов.
                      0
                      Ну тогда тренируйтесь, можете очень быстро нагнать свой уровень! Можете обращаться.
                        0
                        Спасибо.
              +1
              Замечательно, спасибо, давно присматриваюсь к вашему фреймворку, но останавливала документация только на русском и зависимость от AtomJs.

              Посмотрел на AtomJs, не совсем понял, он IE не поддерживает (хотя бы 8+)?
                +1
                А в чём недостатки (для вас) только русской документации и уж тем более зависимости от Атома?
                AtomJS и LibCanvas поддерживают IE начиная с девятой версии.
                  +1
                  Для меня проблем с русским нет, да и исходники на гитхабе доступны, спасибо вам. Западные товарищи нервничают, что это «is too complex in maintainable».

                  IE начиная с девятой версии — отлично!

                  Да я понимаю, что мы (программисты) зажрались и требуем документации и тестов от автора.

                  У меня есть некоторое свободное время по вечерам в ближайшее время, я могу попробовать помочь вам с документацией на русском и английском.
                    +1
                    На самом деле я таки да — обязан предоставить документацию и тесты. Просто честно — не успеваю. К примеру, AtomJS у меня покрыт тестами практически полностью (плохо покрыт DOM) — 255 тестов. Есть исходники в репозитории и можно посмотреть у меня на страницах. Доку на LibCanvas решил писать на русском потому что это быстрее, качественнее и всё-равно надо.
                    Однозначно жду коммитов, но, главное, чтобы было кому её поддерживать)
                      +1
                      Постараюсь сделать пул реквест на следующей неделе.

                      Если вам не сложно, заведите пожалуйста теги в репозитории. Это сильно облегчит жизнь при написании документации, плюс очень удобно состовлять чейнжлоги, а так же это заставляет более вдумчиво именовать коммиты :)
                        0
                        Я как-то с Гитом ещё не очень разобрался.

                        Вы предлагаете использовать теги только для версий? Или теги используются для деления на виды коммитов — «документация», «багФикс», «новаяФича»?
                          0
                          Вы предлагаете использовать теги только для версий?


                          Да, теги для версий общепринятый варинат. Тег в общем случае уникально идентифицирует один коммит, т.е. пометить одним тегом два коммита в одной ветке нельзя. Для версий принято считать, что теги в публичном репозитории характеризуют коммит, помеченый тегом, и все нижеидущее коммиты до прошлого тега. Гитхаб использует это соглашение в своем интерфейсе.

                          На некоторых проектах используют текстовые метки в имени коммита для обозначения его типа, к примеру

                          git commit -m "[Bug] Fixed bug with ie6"
                          git commit -m "[Feature] New cool stuff"
                          git commit -m "[Doc] Docs were updated to 1.1.0"
                            0
                            А как использовать эти теги на практике? Как пометить коммит тегом?

                            То есть, если я пометил один коммит версией «1.2.3», то все следующие коммиты, пока не будет ещё одного тега будут версии «1.2.3»?
                              +2
                              Я наверное путано расказал, изивините.

                              К примеру у вас есть три коммита в репозитории: С1, C2 и C3

                              Ветка репозитория это некий стек для коммитов (эту аналогию просто расширить на git stash, git pull --rebase и т.д.).

                              git log и все известные мне утилиты для просмотра дерева коммитов в графическом виде считают, что более новые коммиты отображаютя в самом верху.

                              Допустим, вот вывод git log
                              $ git log

                              * С3
                              * С2
                              * С1


                              Когда мы применяем тег, он применяется к верхнему коммиту (последнему по дате создания):

                              $ git tag v0.1 && git log

                              * С3 <-- v0.1
                              * С2
                              * С1


                              Считается, что все три коммита относятся к версии 0.1

                              Далее, мы делаем еще два коммита:
                              $ git log

                              * С5
                              * С4
                              * С3 <-- v0.1
                              * С2
                              * С1


                              Это «бесхозные коммиты», добавляем новый тег:
                              $ git tag v0.2 && git log

                              * С5 <-- v0.2
                              * С4
                              * С3 <-- v0.1
                              * С2
                              * С1


                              Коммиты 4 и 5 теперь принадлежат к версии 0.2

                              Теперь мы можем работать с тегами, не заморачиваясь на хеши коммитов, к примеру вывести diff между версиями git diff v0.1 v0.2

                              или сделать чейнджлог:
                              git log v0.1..v0.2
                              и т.д.

                              При документировании очень удобно иметь «точку опоры», к примеру знать точно, что документация актуальна на версию XXX, которая помечена тегом. С помощью вышеописанных команд можно быстро посмотреть, что изменилось в коде между версиями и внести изменения в док.

                                0
                                Хм. Спасибо. Имхо, лучше было бы наоборот, но стандарт есть стандарт)
                                Спасибо за объяснение. А вы хорошо разбираетесь в Гите? Можно задавать вопросы в Джаббере?
                                0
                                Если вы имели ввиду, что в репозитории будут болтаться «лишние» коммиты, а версия будет старой, то это не проблема. В мастере так и бывает обычно, а для мажорных, к примеру версий, создаются отдельные ветки.
                                  0
                                  Ага, именно это и имел ввиду.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое