Демонстрация

Сейчас мы покажем средства работы с графикой, предоставляемые браузерным окружением.

Полный код примера доступен по ссылке.

Простейший растровый редактор

Мы реализуем растровый редактор: программу, позволяющую редактировать изображение путём изменения цветов его пикселей.

Точка входа: HTML-страница

Основной режим работы браузера: просмотр файлов, написанных на языке HTML. Эти файлы описывают общую структуру «документа» (этим словом называется то, что браузер в итоге рисует).

Также в HTML-файле могут содержаться части, написанные на языке CSS (задают по большей части внешний вид тех или иных компонент документа), и части, написанные на языке Javascript (описывают поведение документа в ответ на действия пользователя или же иные «события»).

Нам сейчас (и далее) не будут требоваться никакие особенности HTML и CSS, поэтому ограничимся самым минимумом:

<!doctype html>

<meta charset="utf8">

<title>Микропэйнт</title>

<script src="main.js"></script>

<canvas id="CANVAS"></canvas>

Этот текст нужно сохранить в какой-нибудь текстовый файл. Желательно, чтобы название файла оканчивалось на .html, а текст кодировался при помощи utf8.

Первая строчка задаёт версию языка HTML.

Вторая — указывает кодировку файла.

Третья — название, которое браузер печатает на соответствующей вкладке. Можно её и не писать.

Четвёртая — указывает, что нужно запустить программу, которая содержится в файле main.js. Хотя HTML позволяет писать Javascript-код между открывающим «тегом» <script> и закрывающим </script>, лучше всё же этот код писать в отдельном файле. И Вам будет проще, и текстовому редактору, которым Вы пользуетесь.

Пятая строчка описывает единственную компоненту документа — элемент canvas. Этот элемент предоставляет различные способы рисовать на нём изображение при помощи программы. «Атрибут» id="CANVAS" задаёт идентификатор элемента: именно по этому идентификатору мы получим доступ к элементу внутри программы.

Событие load

Вообще говоря, Javascript обычно исполняется в ответ на те или иные события. А именно, считав программу из файла, браузер сперва выполняет весь код, находящийся вне определения функций, а затем переходит в режим ожидания.

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

Одним из возможных событий является load. Оно происходит в тот момент, когда браузер заканчивает формирование документа. После этого события из документа можно получить доступ к любому элементу, описанному в HTML.

Функцию-обработчик этого события можно задавать путём изменения глобальной переменной onload (есть также метод .addEventListener, при помощи которого можно задать несколько обработчиков одному и тому же событию, но его использование чревато трудноуловимыми ошибками и, по-хорошему, требует организации системы регистрации обработчиков событий).

Начнём с простого:

onload = () => {
    const canvas = document.getElementById("CANVAS");
    const ctx = canvas.getContext("2d"); 

    ctx.fillStyle = 'red';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}

Первая строчка функции получает элемент с идентификатором CANVAS.

Вторая — получает от этого элемента «контекст» 2d. Под термином «контекст» понимается набор функций для работы с холстом и некоторое внутреннее состояние, обеспечивающее взаимное влияние этих функций друг на друга.

Кроме контекста 2d холст предоставляет ещё контексты webgl и webgl2, которые используются для управления GPU в соответствии со стандартом OpenGL ES.

Описание контекста 2d можно посмотреть, например, на MDN.

Далее демонстрируется метод .fillRect, рисующий сплошной прямоугольник. Управляется поведение этого метода полем .fillStyle, которое позволяет задать цвет прямоугольника. Цвет можно задавать одним из следующих способов:

  • названием
  • в 16-ричной системе счисления по маске #RGB (например, #a0e)
  • в 16-ричной системе счисления по маске #RGBA (например, #a0ef)
  • в 16-ричной системе счисления по маске #RRGGBB (например, #aa00ee)
  • в 16-ричной системе счисления по маске #RRGGBBAA (например, #aa00eeff)
  • в десятичной системе счисления rgb(170,0,238) или rgba(170,0,238,1.0)
  • ещё несколькими способами

Отметим, что альфа-канал используется для линейной интерполяции между старым и новым цветами по формуле (для каждой из RGB-компонент)

\[\alpha x + (1-\alpha) X\]

где \(x\) — старое значение компоненты, а \(X\) — новое. Это — самый частый, но далеко не единственный способ использования альфа-канала.

Также отметим, что ctx.canvas.width и ctx.canvas.height можно было бы заменить на просто canvas.width и canvas.height, но более длинным кодом мы продемонстрировали, что холст тоже является частью контекста (и его не обязательно передавать в другие функции, которые рисуют что-то, для чего требуется знать параметры холста).

Реакция на мышь

В конце обработчика допишем пару вспомогательных функций и ещё один обработчик.

    const drawAt = (x, y) => {
        ctx.fillStyle = 'white';
        ctx.fillRect(x - 5, y - 5, 11, 11);
    };

    const getMouseCoords = (data) => {
        const { x, y } = canvas.getBoundingClientRect();

        const curX = data.clientX - x;
        const curY = data.clientY - y;

        return [curX, curY];
    };
    
    canvas.onmousedown = (data) => {
        if (data.button != 0) { return; }

        const [x, y] = getMouseCoords(data);
        drawAt(x, y);
    };

Первая из функций ставит белый квадратик с центром в заданной точке.

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

Теперь мы можем ставить на холст отдельные квадратики.

Рисование линий

Наконец, модифицируем код так, чтобы можно было рисовать непрерывные линии. Первый шаг: добавление переменной, хранящей состояние кнопки мыши (нажата/отжата), и переменных, хранящих предыдущие координаты мыши:

    let buttonPressed = false;
    let prevX = null;
    let prevY = null;

    onmousemove = (data) => {
        if (!buttonPressed) { return; }

        const [curX, curY] = getMouseCoords(data);

        /* рисуем линию между prevX,prevY и curX,curY */

        prevX = curX;
        prevY = curY;
    };

    canvas.onmousedown = (data) => {
        if (data.button != 0) { return; }
        buttonPressed = true;

        const [x, y] = getMouseCoords(data);
        drawAt(x, y);
    };

    onmouseup = (data) => {
        if (data.button != 0) { return; }
        buttonPressed = false;
        prevX = null;
        prevY = null;
    };

Предыдущий canvas.onmousedown нужно заменить на вышеприведённый: в нём есть одна дополнительная строчка buttonPressed = true;.

Отметим, что onmouseup у нас глобальный, а не от холста: мы можем вывести курсор за предел холста и там отпустить кнопку.

Рисование линии можно реализовать, например, одной из модификаций алгоритма Брезенхема:

        if (prevX === null) {
            drawAt(curX, curY);
        } else {
            const dx = curX - prevX;
            const dy = curY - prevY;

            if (dx === 0 && dy === 0) { 
                drawAt(curX, curY);
            } else if (Math.abs(dy) > Math.abs(dx)) {
                for (let i = 0; i <= Math.abs(dy); i++) {
                    const rx = prevX + dx * i / Math.abs(dy); 
                    const ry = prevY + dy * i / Math.abs(dy); 
                    drawAt(rx, ry);
                }
            } else if (Math.abs(dx) >= Math.abs(dy)) {
                for (let i = 0; i <= Math.abs(dx); i++) {
                    const rx = prevX + dx * i / Math.abs(dx); 
                    const ry = prevY + dy * i / Math.abs(dx); 
                    drawAt(rx, ry);
                }
            }
        }