Демонстрация
Сейчас мы покажем средства работы с графикой, предоставляемые браузерным окружением.
Полный код примера доступен по ссылке.
Простейший растровый редактор
Мы реализуем растровый редактор: программу, позволяющую редактировать изображение путём изменения цветов его пикселей.
Точка входа: 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);
}
}
}