Предисловием мне хотелось бы поздравить одного хабраюзера с днём рождения. Расти большим, будь умным, и допили уже наконец свой canvas-фреймворк Graphics2D до того состояния, которое считаешь приемлемым.
С днём рождения, я. :P
Этим летом мне пришла в голову интересная мысль: если бы я писал микробиблиотеку для canvas в 100 строк, что бы я туда уместил?.. Самый развёрнутый ответ можно написать за 1 вечер. А потом пришла и идея этой статьи.
Предлагаю реализовать ООП, события и анимацию на canvas — самые часто нужные (имхо) вещи… и всё это в 100 строк. Часть первая.
Дисклеймер: тут вас (иногда) поджидают совсем ненужные извращения для экономии пары символов кода. Автор (а это я) считает, что в микробиблиотеках так можно, и очень часто делается. Если это не нарушает производительность, конечно.
Начнём с идеи (а первым делом — ООП). 3 главных объекта: пути, изображения, текст. Нет никакой нужды реализовывать, например, прямоугольники и круги в минибиблиотеке: они легко создаются через путь. Как и спрайты — через картинки. И т.п.
Первый аргумент объекта — его содержание.
Второй — стили, которые устанавливаются на canvas перед рисованием.
Я назову это Rat :P
Как-то так будет неплохо:
У всех 3 объектов нужно установить свойством контекст, объект для стилей и т.п… Так что:
Вроде бы всё понятно? У каждого объекта есть 3 свойства: opt (1 аргумент), style (2й) и context (контекст), а также функция draw(ctx), рисующая этот объект.
Наш класс:
Да, как ни странно, конструктор — всё.
Самое главное: отрисовка:
Функция process тут вовсе неспроста: она понадобится ещё кое-где:
Зачем callback? Хм… Для красоты.
Функция Rat.style, также общая для всех 3 объектов, просто переносит свойства на canvas. Не забываем, что нам также хочется трансформаций:
Ай, не бейте, я все объясню.
Ну и, наконец, функция контекста, создающая и возвращающая экземпляр нашего класса:
Всё, можно рисовать пути. Ура!
И, помимо основных стилей, присутствуют, как можно было заметить в Rat.style, трансформации:
Следуя дальше принципу 2 аргументов, мы хотим воот такой вот класс:
Помимо этого, в стилях можно передавать параметры width, height и crop (массив из 4 чисел). Всё так же, как в оригинальной drawImage CanvasRendering2DContext-а.
Снова конструктор класса:
Отрисовка выглядит как-то так:
Всё, вроде бы, просто.
И последнее, конечно же:
Ура, и картинки есть.
3й глобальный объект:
Конструктор:
Отрисовка очень простая. А решение, как всегда, не очень чистое, зато работающее ).
А ещё текст на canvas-е можно мерить. Ширину, да. Высота определяется размером шрифта.
Не забываем:
Иногда нужно простить, забыть, выкинуть всё и начать с чистого листа. Для таких случаев есть функция clear:
Для всего остального есть draw, рисующий все объекты из массива:
Ну а теперь… Давайте, например, накодим кнопку на canvas-е (самое простое, что придумалось):
И пуусть… При наведении мыши она подсвечивается:
Самое интересное, что на базовом canvas можно накодить примерно то же примерно тем же количеством кода.
Но это стало очевидно только после того, как 100 строк написаны…
github.com/keyten/Rat.js/blob/master/rat.js
Ну что ж… В следующей части (если хабрахабру будет интересна эта тема) я покажу реализацию обработки мыши, и 3 часть — анимация. Всё снова в 100 строк (посмотрим, получится ли).
Пойду праздновать день рождения.
С днём рождения, я. :P
Этим летом мне пришла в голову интересная мысль: если бы я писал микробиблиотеку для canvas в 100 строк, что бы я туда уместил?.. Самый развёрнутый ответ можно написать за 1 вечер. А потом пришла и идея этой статьи.
Предлагаю реализовать ООП, события и анимацию на canvas — самые часто нужные (имхо) вещи… и всё это в 100 строк. Часть первая.
Дисклеймер: тут вас (иногда) поджидают совсем ненужные извращения для экономии пары символов кода. Автор (а это я) считает, что в микробиблиотеках так можно, и очень часто делается. Если это не нарушает производительность, конечно.
Рад видеть вас под катом ;)
Начнём с идеи (а первым делом — ООП). 3 главных объекта: пути, изображения, текст. Нет никакой нужды реализовывать, например, прямоугольники и круги в минибиблиотеке: они легко создаются через путь. Как и спрайты — через картинки. И т.п.
Первый аргумент объекта — его содержание.
Второй — стили, которые устанавливаются на canvas перед рисованием.
Я назову это Rat :P
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 строк (посмотрим, получится ли).
Пойду праздновать день рождения.
Zenitchik
Больше всего понравилось название =('.')=
На втором месте !~Rat.notStyle.indexOf(key)
Сейчас набегут адепты читабельности и заминусят меня. Но, что поделать, я не друг тем людям, для которых смысл этой записи очевиден.
~(-1) === 0, а !0 === true. Без этих знаний можно только скриптики для сайтиков писать, а к толстому клиенту не лучше подходить.
Теперь по существу:
У описанной части есть только одно полезное свойство — графика описывается данными. Данные можно запомнить, а потом отрисовать полностью или частично, в одном canvase или в нескольких. Это открывает возможность разработке в парадигме MVC.
Код смотреть пока не досуг. С удовольствием почитаю следующую статью.
Была бы очень в тему поддержка добавления своих функций отрисовки, доступных по тому же алгоритму.
Keyten
Да, про данные подумал не сразу. Но, в целом, можно и data-ориентированный код на чистом 2dcontext писать, разница есть, но не сильно заметная. Ну, мне так кажется, очень надеюсь, что я ошибаюсь.
В любом случае, именно на этих объектах будут строиться следующие части статьи.
Про добавление своих функций отрисовки чуть-чуть не понял, имеется в виду что-то подобное?:
Zenitchik
Это я сперва не понял идею.
data-ориентированность тут не до конца. Ну, не беда, можно допилить. Жаль, времени пока нет в коде копаться…
Keyten
Предлагайте, если разберусь, напишу, мне это кажется интересным.
Zenitchik
Код посмотрел. Некоторые идеи есть, но кодить пока некогда. А на естественном языке мне мысли о коде выражать труднее, чем на JS.
Будет окно в своих скриптах — предложу свой коммит для Rat.
А пока мне интересно, как Вы думаете реализовать события. Я сам очень мало работал с canvas и рассчитываю с помощью какого-то из Ваших фреймворков ликвидировать безграмотность.
Zenitchik
Пройдёмся по опечаткам (дюже они страшные):
*я не друг тем людям, для которых смысл этой записи не очевиден.
*а к толстому клиенту
нелучше не подходитьВот что бывает, когда в конце рабочего дня комменты пишешь…
Keyten
Ну и ещё недосуг слитно.
На первой опечатке ненадолго завис, но в итоге смысл понял, вторая просто незаметна.
kentilini
Попробуйте нарисовать подряд большую картинку и потом треугольник, и будете приятно удивлены
Keyten
Ну да, картинка же рисуется по событию onload, которое случается после основного потока… Поэтому она окажется нарисована позже (и выше, соответственно). Если вы об этом.
Zenitchik
Можно всю отрисовку сделать асинхронной. Рендерер ждёт, когда прогрузятся все картинки, а потом рисует элементы в порядке следования.
Или, ещё лучше, грузить картинку при создании элемента, а не при отрисовке. Рисовать же несколько раз можно, а грузить достаточно один раз.
Keyten
Ну да: достаточно сделать массив элементов в Rat, и в функциях Rat.prototype.path, image, text, пушать элемент в массив. Можно даже и сам Rat от массива унаследовать. Дальше будет обработка событий, и я, возможно, приду к чему-нибудь подобному.
Такой подход я реализовал в, собственно, Graphics2D ( Github ), а Rat — чисто поиграть-поэкспериментировать, что уложится в 100 строк :).
В G2D всё то же самое реализуется таким кодом:
Всё перерисовывается, обрабатывается и т.п.
kentilini
Мы делали очередь, и ждали пока не будет доступны для отрисовки все ниже стоящие слои. Еще аналог 'z-index'a навешивали, для удобства использования.
Zenitchik
Я бы сделал сервис загрузки картинок, который бы принимал {string}url и возвращал {Image}. Заодно исключив повторную загрузку с одного и того же url.
Zenitchik
UPD: Я бы Rat к массиву не привязывал. Возможно, один и тот же массив придётся отрисовывать в разных canvas, а canvas уже привязан к экземпляру Rat.
Nadoedalo
Мне всегда нравился другой подход:
Как-то оно нагляднее что-ли показывает процесс. Аргументы всё-равно достаточно редко нужны общие для всех рисунков. Хотя, эту обёртку я в основном писал что бы уменьшить количество строк при рисовании фигур, но выглядит достаточно удобно и более гибко.
PS форматирование хромает =(
Keyten
Мне это больше всего напоминает EaselJS:
Может быть, это и удобно. Но… моё чувство эстетики (эстетичности? как правильно?) всеми руками протестует против чеининга свойств контекста:
И при этом в каком-то виде возможна реализация ООП, и соблюдается «чистота» — свойства ранее отрисованных объектов не переползают на позднее рисуемые.