Pull to refresh

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

Reading time 6 min
Views 15K
Предисловием мне хотелось бы поздравить одного хабраюзера с днём рождения. Расти большим, будь умным, и допили уже наконец свой 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 строк (посмотрим, получится ли).
Пойду праздновать день рождения.

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+10
Comments 16
Comments Comments 16

Articles