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

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

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

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

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


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

Я назову это Rat :P

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

Пути


Как-то так будет неплохо:
9b8a9658fc6d7199be19c9dcd2337a75.png

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, трансформации:
cc86bdf18c959c0025b7a8a5460315d3.png

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'
});


171a7baaf5200844c1667fcbe04a7a71.png

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

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]);
        }
});


x4Qc2DY.png

А зачем?


Самое интересное, что на базовом 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 строк (посмотрим, получится ли :)).
Пойду праздновать день рождения.

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

© Habrahabr.ru