Предисловием мне хотелось бы поздравить одного хабраюзера с днём рождения. Расти большим, будь умным, и допили уже наконец свой 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/
Добавить комментарий