GrabDuck

Выводим текст на HTML5 Canvas

:

HTML5 Canvas – очень обширная тема со многими “вкусностями”, о многих из которых уже писали и ещё будут писать. Поэтому, я хочу немного рассказать в этой статье, только об одной маленькой, и как на первый взгляд может показаться банальной темой – работа с текстом. Я хочу показать, что с ним почти также можно работать, как с обычным текстом в вебе, т.е. позиционировать, накладывать стили и градиенты, а также писать многострочные предложения легко и без проблем.

Простой текст


Чтобы вывести обычный текст, можно воспользоваться двумя функциями контекста fillText() и strokeText().
Эти функции принимают три обязательных параметра: сам текст и координаты X и Y, т.е. место его расположения, и последний – необязательный: максимальная ширина текста, если вы зададите максимальную ширину текста меньше, чем фактическая ширина, то текст сожмется до указанной вами ширины. Не советую задавать максимальную ширину, так как при сжатии и у него становится плохой вид. Вместо такого сжатия рекомендую переносить текст на новую строку, стандартной функции для этого нет, но я приведу пример, как это легко реализовывается.
Если просто вывести текст при помощи таких функций, то такой текст будет мелкий и невзрачный. Это мы можем исправить задав ему шрифт в переменную контекста font. Шрифт задается, также как и в CSS ([font style][font weight][font size][font face]). Пример:
    ctx.fillStyle = "#00F";
    ctx.strokeStyle = "#F00";
    ctx.font = "italic 30pt Arial";
    ctx.fillText("Fill text", 20, 50);
    ctx.font = 'bold 30px sans-serif';
    ctx.strokeText("Stroke text", 20, 100);

image

Demo

Расположение текста


Есть два стандартных способа позиционирования текста, относительно своего расположения: вертикальное и горизонтальное.
Вертикальное позиционирование задается при помощи textBaseline, в неё устанавливается один из возможных вариантом: top, hanging, middle, alphabetic, ideographic и bottom.
    ctx.textBaseline = "bottom";
    ctx.fillText("bottom", 400, 75);

image

Demo

А горизонтально при помощи textAlign, может быть один из следующих параметров: center, start, end, left, right.

    context.textAlign = "center";
    ctx.textBaseline = "bottom";
    context.fillText("center", 250, 20);

image

Demo

Эти способы далеко не идеальны и при помощи них иногда проблематично спозиционировать текст, так как хочется именно вам. Ниже я опишу способ, как можно использовать своё позиционирование, даже многострочного текста.

Стилизация
Самое простое, как мы можем стилизовать текст, это задать ему цвет. Цвет задается при помощи fillStyle – для задания цвета заливки и strokeStyle – для задания цвета обводки.
Так же как и в CSS3 можно накладывать тени и на текст в канвасе. Это делается при помощи: shadowColor – задание цвета тени, shadowOffsetX и shadowOffsetY – задание отступа и shadowBlur – задание размытия тени.

    ctx.shadowColor = "#F00";
    ctx.shadowOffsetX = 5;
    ctx.shadowOffsetY = 5;
    ctx.shadowBlur = 5;
    ctx.strokeText("Shadow text", 20, 100);

image

Поддерживаются также и градиенты для текста. Сам градиент создается при помощи функции createLinearGradient(). А при помощи функции addColorStop() задаются цвета и позиции их в нём. Текст заливается градиентом также как и сплошным цветом, при помощи fillStyle и strokeStyle.

    var gradient = ctx.createLinearGradient(0, 0, 0, 60);
    gradient.addColorStop(0.0, 'rgba(0, 0, 255, 1)');
    gradient.addColorStop(0.3, 'rgba(128, 0, 255, 0.6)');
    gradient.addColorStop(0.6, 'rgba(0, 0, 255, 0.4)');
    gradient.addColorStop(1.0, 'rgba(0, 255, 0, 0.2)');
    ctx.fillStyle = gradient;

image

Кроме обычной заливки и заливки градиентом можно также залить какой-либо текстурой. Для этого необходимо до начала вывода текста загрузить изображение-текстуру. А затем при помощи createPattern() создать текстуру на его основе. Можно задать, чтобы изображение повторялось либо нет.

    var pattern = ctx.createPattern(img, 'repeat');
    ctx.fillStyle = pattern;

image

Demo

Ширина текста и многострочный текст

Допустим, вам необходимо написать текст на канвасе неограниченной длинны, будь то одно слово, или несколько предложений. Если вы будете его писать как есть, т.е. весь текст вставите в fillText(), то на экране будет видна только та часть, которая по своей ширине не превышает ширину канваса. Т.е. если ширина канваса 400 пикселей, то вы не уведете текст, который выходит за его ширину. Для того, чтобы сделать многострочный текст необходимо исхитрится. Но алгоритм прост. Сначала высчитываем ширину текста, сверяем её с максимальной шириной области, куда мы хотим вывести этот текст. Если ширина текста не превышает ту ширину, то выводим текст, а если превышает, то разбиваем текст самым удобным для вас способом, я предпочитаю разбивать по пробелам.
Для того чтобы высчитать ширину текста можно воспользоваться функцией measureText(), которая отдает в объектной форме ширину текста (к сожалению данная функция не отдает высоту текста, надеюсь эта возможность будет добавлена, но а пока придется высчитывать иными способами, как именно далее в статье).
Пример:

function wrapText(context, text, marginLeft, marginTop, maxWidth, lineHeight)
    {
        var words = text.split(" ");
        var countWords = words.length;
        var line = "";
        for (var n = 0; n < countWords; n++) {
            var testLine = line + words[n] + " ";
            var testWidth = context.measureText(testLine).width;
            if (testWidth > maxWidth) {
                context.fillText(line, marginLeft, marginTop);
                line = words[n] + " ";
                marginTop += lineHeight;
            }
            else {
                line = testLine;
            }
        }
        context.fillText(line, marginLeft, marginTop);
    }
    var canvas = document.getElementById("canvas");
    var context = canvas.getContext("2d");
    var maxWidth = 400; //размер поле, где выводится текст
    var lineHeight = 25;
    /*если мы знаем высоту текста, то мы можем
     предположить, что высота строки должна быть именно такой*/
    var marginLeft = 20;
    var marginTop = 40;
    var text = "Сначала мы разбиваем текст на слова по пробелам, а потом обходим эти слова в цикле, " +
            "объединяя их по одному в строку. Если при последнем объединении ширина этой строки меньше максимальной, " +
            "то продолжаем, а если больше, то выводим строку без последнего слова, а его записываем в новую строку." +
            "И так продолжаем, пока не обработаем весь текст.";
    context.font = "16pt Calibri";
    context.fillStyle = "#000";
    wrapText(context, text, marginLeft, marginTop, maxWidth, lineHeight);

image

Demo

Вроде как всё хорошо, не нужно задавать координаты текста, самому вручную вычислять влезет ли текст по ширине или нет. Почти всё автоматизировано и выглядит неплохо, но остается всё-таки маленький нюанс, высота текста у нас захардкоджена, по этому при смени шрифта или размера придется каждый раз подправлять вручную высоту текста (в примере переменная lineHeight).

Определяем высоту текста

При работе с текстом на канвасе ни функция measureText(), ни одна другая не дают нам определить высоту текста. Всё же можно справиться и с этой бедой, вариант решения этой проблемы, который я хочу предложить вам, не очень красив, но всё же оно работает.
Когда мы задаем стиль текста (размер и стиль шрифта) в font, то мы его задаем в таком же стиле, так как бы мы его задавали в CSS. Шрифты и их размеры, конечно если доступен такой шрифт в канвасе, то также он будет доступен и в обычных стилях.
И так, идея вот в чем, у нас есть размер и тип шрифта в переменной font, мы создаем в DOM новый элемент, туда помещаем текст, и устанавливаем стили из той переменной. Далее при помощи offsetHeight получаем высоту элемента, которая и будет высотой нашего текста. После этого можем благополучно удалить этот элемент из DOM.
Пример реализации этой идеи:

var text = "Этот текст должен быть написан справа в самом низу.";
    var marginLeft = canvas.width - context.measureText(text).width;
    var marginTop = canvas.height - getFontHeight(context.font);
    context.fillText(text, marginLeft, marginTop);
    function getFontHeight(font) {
        var parent = document.createElement("span");
        parent.appendChild(document.createTextNode("height"));
        document.body.appendChild(parent);
        parent.style.cssText = "font: " + font + "; white-space: nowrap; display: inline;";
        var height = parent.offsetHeight;
        document.body.removeChild(parent);
        return height;
    }

Результат:

image

Demo

Буду рад, если кто-то предложит более красивый или правильный вариант получения высоты текста.
Спасибо.