100 строк на canvas-е: часть 1

от автора

Предисловием мне хотелось бы поздравить одного хабраюзера с днём рождения. Расти большим, будь умным, и допили уже наконец свой canvas-фреймворк Graphics2D до того состояния, которое считаешь приемлемым.
С днём рождения, я. 😛

Этим летом мне пришла в голову интересная мысль: если бы я писал микробиблиотеку для canvas в 100 строк, что бы я туда уместил?.. Самый развёрнутый ответ можно написать за 1 вечер. А потом пришла и идея этой статьи.

Предлагаю реализовать ООП, события и анимацию на canvas — самые часто нужные (имхо) вещи… и всё это в 100 строк. Часть первая.

Дисклеймер: тут вас (иногда) поджидают совсем ненужные извращения для экономии пары символов кода. Автор (а это я) считает, что в микробиблиотеках так можно, и очень часто делается. Если это не нарушает производительность, конечно.

Рад видеть вас под катом 😉

Начнём с идеи (а первым делом — ООП). 3 главных объекта: пути, изображения, текст. Нет никакой нужды реализовывать, например, прямоугольники и круги в минибиблиотеке: они легко создаются через путь. Как и спрайты — через картинки. И т.п.
Первый аргумент объекта — его содержание.
Второй — стили, которые устанавливаются на canvas перед рисованием.

Я назову это Rat 😛

Rat = function(context){     this.context = context; }; 

Пути

Как-то так будет неплохо:

var path = rat.path([   ['moveTo', 10, 10],   ['lineTo', 100, 100],   ['lineTo', 10, 100],   ['closePath'] ], {   fillStyle: 'red',   strokeStyle: 'green',   lineWidth: 4 }); 

У всех 3 объектов нужно установить свойством контекст, объект для стилей и т.п… Так что:

Rat.init = function(cls, arg){     cls.opt = arg[0];     cls.style = arg[1] || {};     cls.context = arg[2];     cls.draw(arg[2].context); }; 

Вроде бы всё понятно? У каждого объекта есть 3 свойства: opt (1 аргумент), style (2й) и context (контекст), а также функция draw(ctx), рисующая этот объект.

Наш класс:

Rat.Path = function(opt, style, context){     Rat.init(this, arguments); }; 

Да, как ни странно, конструктор — всё.

Самое главное: отрисовка:

Rat.Path.prototype = {     draw: function(ctx){         this.process(function(ctx){             if(this.style.fillStyle)                 ctx.fill();             if(this.style.strokeStyle)                 ctx.stroke();         }, ctx);     },     process: function(callback, ctx){         ctx = ctx || this.context.context;         Rat.style(ctx, this.style);         ctx.beginPath();         this.opt.forEach(function(func){             ctx[func[0]].apply(ctx, func.slice(1));         });         var result = callback.call(this, ctx);         ctx.restore();         return result;     } }; 

Функция process тут вовсе неспроста: она понадобится ещё кое-где:

    isPointIn: function(x,y, ctx){         return this.process(function(ctx){ return ctx.isPointInPath(x, y); }, ctx);     } 

Зачем callback? Хм… Для красоты.

Функция Rat.style, также общая для всех 3 объектов, просто переносит свойства на canvas. Не забываем, что нам также хочется трансформаций:

// не смотрите на меня так, в микробиблиотеках иногда можно так извращаться // иногда Rat.notStyle = "translate0rotate0transform0scale".split(0); Rat.style = function(ctx, style){     ctx.save();     style.origin && ctx.translate.apply(ctx, style.origin);     style.rotate && ctx.rotate(style.rotate);     style.scale && ctx.scale.apply(ctx, style.scale);     style.origin && ctx.translate(-style.origin[0], -style.origin[1]);     style.translate && ctx.translate.apply(ctx, style.translate); // интересно, это лучше до или после origin?     style.transform && ctx.transform.apply(ctx, style.transform);     Object.keys(style).forEach(function(key){         if(!~Rat.notStyle.indexOf(key))             ctx[key] = style[key];     }); }; 

Ай, не бейте, я все объясню. !~Rat.notStyle.indexOf(key) — тоже самое, что и Rat.notStyle.indexOf(key) != -1. Это микробиблиотека всё же.

Ну и, наконец, функция контекста, создающая и возвращающая экземпляр нашего класса:

Rat.prototype = {     path : function(opt, style){ return new Rat.Path(opt, style, this); }, }; 

Всё, можно рисовать пути. Ура!

И, помимо основных стилей, присутствуют, как можно было заметить в Rat.style, трансформации:

var path = rat.path([   ['moveTo', 10, 10],   ['lineTo', 100, 100],   ['lineTo', 10, 100],   ['closePath'] ], {   fillStyle: 'red',   strokeStyle: 'green',   lineWidth: 4,   rotate: 45 / 180 * Math.PI,   origin: [55, 55] }); 

Картинка обрезана, т.к. нарисована в нулевых координатах.

Картинки

Следуя дальше принципу 2 аргументов, мы хотим воот такой вот класс:

var img = new Image(); img.src = "image.jpg"; img.onload = function(){   rat.image(img); } 

Помимо этого, в стилях можно передавать параметры width, height и crop (массив из 4 чисел). Всё так же, как в оригинальной drawImage CanvasRendering2DContext-а.

Снова конструктор класса:

Rat.Image = function(opt, style, context){     Rat.init(this, arguments); }; 

Отрисовка выглядит как-то так:

Rat.Image.prototype.draw = function(ctx){     Rat.style(ctx, this.style);     if(this.style.crop)         ctx.drawImage.apply(ctx, [this.opt, 0, 0].concat(this.style.crop));     else         ctx.drawImage(this.opt, 0, 0, this.style.width || this.opt.width, this.style.height || this.opt.height);     ctx.restore(); }; 

Всё, вроде бы, просто.

И последнее, конечно же:

Rat.prototype = {     ...     image : function(opt, style){ return new Rat.Image(opt, style, this); }, }; 

Ура, и картинки есть.

Текст

3й глобальный объект:

var text = rat.text("Hello, world!", {   fillStyle: 'blue' }); 

Также есть свойство maxWidth.

Конструктор:

Rat.Text = function(){     Rat.init(this, arguments); }; 

Отрисовка очень простая. А решение, как всегда, не очень чистое, зато работающее ).

Rat.Text.prototype.draw = function(ctx){     Rat.style(ctx, this.style);     if(this.style.fillStyle)         ctx.fillText(this.opt, 0, 0, this.style.maxWidth || 999999999999999);     if(this.style.strokeStyle)         ctx.strokeText(this.opt, 0, 0, this.style.maxWidth || 9999999999999999);     ctx.restore(); }; 

А ещё текст на canvas-е можно мерить. Ширину, да. Высота определяется размером шрифта.

Rat.Text.prototype.measure = function(){     var ctx = this.context.context;     Rat.style(ctx, this.style);     var w = ctx.measureText(this.opt).width;     ctx.restore();     return w; }; 

Не забываем:

Rat.prototype = {     ...     image : function(opt, style){ return new Rat.Image(opt, style, this); }, }; 

По мелочи

Иногда нужно простить, забыть, выкинуть всё и начать с чистого листа. Для таких случаев есть функция clear:

Rat.prototype = { ...     clear: function(){         var cnv = this.context.canvas;         this.context.clearRect(0, 0, cnv.width, cnv.height);     } }; 

Для всего остального есть draw, рисующий все объекты из массива:

Rat.prototype = { ...     draw: function(elements){         var ctx = this.context;         elements.forEach(function(element){             element.draw(ctx);         });     } }; 

Примеры:

Ну а теперь… Давайте, например, накодим кнопку на canvas-е (самое простое, что придумалось):

// квадратик var path = rat.path([ 	['moveTo', 10, 10], 	['lineTo', 100, 10], 	['lineTo', 100, 40], 	['lineTo', 10, 40], 	['closePath'] ], { 	fillStyle: '#eee', 	strokeStyle: '#aaa', 	lineWidth: 2 });  // текст var text = rat.text("Hello, world", { 	translate: [55, 28], 	textAlign: 'center', 	fillStyle: 'black' }); 

И пуусть… При наведении мыши она подсвечивается:

var bounds = ctx.canvas.getBoundingClientRect(); var hover = false; ctx.canvas.addEventListener('mousemove', function(e){ 	var x = e.clientX - bounds.left, 		y = e.clientY - bounds.top; 	if(x > 10 && x < 100 && y > 10 && y < 40){ 		if(hover) 			return; 		hover = true; 		path.style.fillStyle = '#ccc'; 		rat.clear(); 		rat.draw([path, text]); 	} 	else if(hover){ 		hover = false; 		path.style.fillStyle = '#eee'; 		rat.clear(); 		rat.draw([path, text]); 	} }); 

А зачем?

Самое интересное, что на базовом canvas можно накодить примерно то же примерно тем же количеством кода.

Скрытый текст

// квадратик var path = { 	fill: '#eee', 	draw: function(){ 		ctx.moveTo(10, 10); 		ctx.lineTo(100, 10); 		ctx.lineTo(100, 40); 		ctx.lineTo(10, 40); 		ctx.closePath();  		ctx.fillStyle = this.fill; 		ctx.strokeStyle = '#aaa'; 		ctx.lineWidth = 2; 		ctx.fill(); 		ctx.stroke(); 	} }; // текст var text = { 	draw: function(){ 		ctx.textAlign = 'center'; 		ctx.fillStyle = 'black'; 		ctx.fillText("Hello, world",  55, 28); 	} }; path.draw(); text.draw();  var bounds = ctx.canvas.getBoundingClientRect(); var hover = false; ctx.canvas.addEventListener('mousemove', function(e){ 	var x = e.clientX - bounds.left, 		y = e.clientY - bounds.top; 	if(x > 10 && x < 100 && y > 10 && y < 40){ 		if(hover) 			return; 		hover = true; 		path.fill = '#ccc'; 		ctx.clearRect(0, 0, 800, 400); 		path.draw(); 		text.draw(); 	} 	else if(hover){ 		hover = false; 		path.fill = '#eee'; 		ctx.clearRect(0, 0, 800, 400); 		path.draw(); 		text.draw(); 	} }); 

Но это стало видно только после того, как 100 строк написаны…
github.com/keyten/Rat.js/blob/master/rat.js

Ну что ж… В следующей части (если хабрахабру будет интересна эта тема) я покажу реализацию обработки мыши, и 3 часть — анимация. Всё снова в 100 строк (посмотрим, получится ли :)).
Пойду праздновать день рождения.

Всем интересного кода!)

ссылка на оригинал статьи http://habrahabr.ru/post/270255/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *