Создание фреймворка для 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. Более того этот топик — неплох в качестве обучения. Потому попрошу хотя бы в этот раз обойтись без холивара. А то вы уже надоели
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 16

      0
      Можно узнать в каких браузерах все работает?
      Я любитель Оперы, демка не работает. Даже при наведении на фигуры — цвет не меняют.
      Спасибо.
        –7
        В 8ie тоже не работает (даже не отображает сами фигуры)
          +3
          ie до 9 версии не поддерживает canvas вообще. Вам или надо скачать плагин для ie но это все равно, что хром установить ибо все будет на вебките рендериться
          либо реализовывать сходный функционал параллельно на VML
            0
            Простите, в устаревших браузерах — не работает.
            +3
            Для совместимости с Opera нужно использовать не e.layerX, а e.offsetX. Так как первый возвращает undefined
              +2
              Вы правы, спасибо.
              +2
              Пофиксил. Работает во всех современных браузерах.
              0
              Автору спасибо за труд, но думаю стать скорее смутить начинающих. Как вы и заметили в заключении, тут ещё много тонкостей, а небольшое api для отрисовки фигур ничего как таковое не даст. Нужно во первых понимать как работает канва, чтобы грамотно оптимизировать отрисовку, кешировать результаты, сделать чтобы отрисовывались меняющиеся части а не весь холст и тд.
              Так что тут для написание постов непочатый край )).
                0
                Я думаю, что даже если новички не осилят реализацию и детали, то, как минимум, осилят идею, а это — достаточно важно. Желательно понимать, как оно работает.
                –2
                Мне одному уже сразу ясно, кто автор статьи если речь идет о HTML5?
                  +4
                  Нууу… Я иногда вижу статьи о HTML5 другого авторства…
                  Я Вам надоел? =)
                    +3
                    Нет, как раз-таки наоборот.
                  +2
                  > использование map + area
                  — и ещё area медленее работает, чем вычисление расположения мыши в JS (проверено однажды в проекте с пробками на карте). Поэтому приложения, критичные ко времени, лучше сразу писать в подобной оболочке.
                    0
                    Я думаю, у него есть одно классное преимущество. В Андроиде при клике на элемент будет активироваться не всё поле, а только элемент. Но спасибо за комментарий. Я не тестил, но ожидал, что map+area будет быстрее.
                    0
                    Я понимаю, конечно, что это только примитивный пример, но сходу возник вопрос.

                    Зачем в Mouse массив elements?

                    Получается, что каждый новый элемент приходится добавлять в this.elements объекта CF и делать this.mouse.add, чтобы он добавился в this.elements объекта mouse.

                    Не проще ли из Mouse дергать this.canvas.elements, сделав предварительно в инитиалайзере this.canvas = canvas?
                      0
                      Ну это как бы разделение по ответственностям. В угоду ООП, скажем. Mouse ничего не знает про того, кто её вызвал.

                      В LibCanvas это разные массивы. Потому что не все объекты, которые отрисовываются слушают мышь и не все объекты, которые слушают мышь — отрисовываются.

                    Only users with full accounts can post comments. Log in, please.