100 строк на canvas-е: часть 1
Предисловием мне хотелось бы поздравить одного хабраюзера с днём рождения. Расти большим, будь умным, и допили уже наконец свой canvas-фреймворк Graphics2D до того состояния, которое считаешь приемлемым.
С днём рождения, я. :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 строк (посмотрим, получится ли :)).
Пойду праздновать день рождения.