Марейн Хавербеке - Выразительный JavaScript
event.preventDefault();
loadImageURL(cx, form.querySelector("input").value);
});
return form;
};
Теперь мы определили все элементы управления, требующиеся нашей программе, но нужно добавить ещё несколько инструментов.
Закругляемся
Очень просто можно добавить инструмент для вывода текста, который выводит запрос пользователю, куда он должен ввести текст.
tools.Text = function(event, cx) {
var text = prompt("Text:", "");
if (text) {
var pos = relativePos(event, cx.canvas);
cx.font = Math.max(7, cx.lineWidth) + "px sans-serif";
cx.fillText(text, pos.x, pos.y);
}
};
Можно было бы добавить полей для размера текста и шрифта, но для простоты мы всегда используем шрифт sans-serif и размер шрифта, как у текущей кисти. Минимальный размер – 7 пикселей, потому что меньше текст будет нечитаемый.
Ещё один необходимый инструмент для каляк-маляк – “аэрозоль”. Она рисует случайные точки под кистью, пока нажата кнопка мыши, создавая более или менее густые точки в зависимости от скорости движения курсора.
tools.Spray = function(event, cx) {
var radius = cx.lineWidth / 2;
var area = radius * radius * Math.PI;
var dotsPerTick = Math.ceil(area / 30);
var currentPos = relativePos(event, cx.canvas);
var spray = setInterval(function() {
for (var i = 0; i < dotsPerTick; i++) {
var offset = randomPointInRadius(radius);
cx.fillRect(currentPos.x + offset.x,
currentPos.y + offset.y, 1, 1);
}
}, 25);
trackDrag(function(event) {
currentPos = relativePos(event, cx.canvas);
}, function() {
clearInterval(spray);
});
};
Аэрозоль использует setInterval для выплёвывания цветных точек каждые 25 миллисекунд, пока нажата кнопка мыши. Функция trackDrag используется для того, чтобы currentPos указывала на текущее положение курсора, и для выключения интервала при отпускании кнопки.
Чтобы посчитать, сколько точек нужно нарисовать каждый раз по окончанию интервала, функция подсчитывает размер области текущей кисти и делит его на 30. Для поиска случайного положения под кистью используется функция randomPointInRadius.
function randomPointInRadius(radius) {
for (;;) {
var x = Math.random() * 2 - 1;
var y = Math.random() * 2 - 1;
if (x * x + y * y <= 1)
return {x: x * radius, y: y * radius};
}
}
Эта функция создаёт точки в квадрате между (-1,-1) и (1,1). Используя теорему Пифагора, она проверяет, лежит ли созданная точка внутри круга с радиусом 1. Когда такая точка находится, она возвращает её координаты, умноженные на радиус.
Цикл нужен для равномерного распределения точек. Проще было бы создавать точки в круге, взяв случайный угол и радиус и вызвав Math.sin и Math.cos для создания точки. Но тогда точки с большей вероятностью появлялись бы ближе к центру круга. Это ограничение можно обойти, но результат будет сложнее, чем предыдущий цикл.
Теперь наша программа для рисования готова. Запустите код и попробуйте.
<link rel="stylesheet" href="css/paint.css">
<body>
<script>createPaint(document.body);</script>
</body>
Упражнения
В программе ещё очень много чего можно улучшить. Давайте добавим ей возможностей.
Прямоугольники
Определите инструмент Rectangle, заполняющий прямоугольник (см. метод fillRect из главы 16) текущим цветом. Прямоугольник должен появляться из той точки, где пользователь нажал кнопку мыши, и до той точки, где он отпустил кнопку. Заметьте, что последнее действие может произойти левее или выше первого.
Когда это заработает, вы заметите, что изображение прямоугольника дрожит и его плохо видно. Можете ли вы придумать способ показа прямоугольника во время движения мыши, но чтобы он не выводился на холст, пока кнопка не отпущена?
Если не придумаете, вспомните о стиле position: absolute, который мы обсуждали в главе 13, который можно использовать, чтобы выводить узел поверх остального документа. Свойства pageX и pageY событий мыши можно использовать для точного расположения элемента под мышью, записывая нужные значения в стили left, top, width и height.
<script>
tools.Rectangle = function(event, cx) {
// Ваш код
};
</script>
<link rel="stylesheet" href="css/paint.css">
<body>
<script>createPaint(document.body);</script>
</body>
Выбор цвета
Ещё один часто встречающийся инструмент – выбор цвета, который позволяет щелчком мыши на картинке выбрать цвет, который находится под курсором. Сделайте такой инструмент.
Для его изготовления понадобится доступ к содержимому холста. Метод toDataURL примерно это и делал, но получить информацию о пикселе из URL с данными сложно. Вместо этого мы возьмём метод контекста getImageData, возвращающий прямоугольный кусок картинки в виде объекта со свойствами width, height и data. В свойстве data содержится массив чисел от 0 до 255, и для каждого пикселя хранится четыре номера — red, green, blue и alpha (прозрачность).
Этот пример получает числа из одного пикселя холста, один раз, когда тот пуст (все пиксели – прозрачные чёрные), и один раз, когда пиксель окрашен в красный цвет.
function pixelAt(cx, x, y) {
var data = cx.getImageData(x, y, 1, 1);
console.log(data.data);
}
var canvas = document.createElement("canvas");
var cx = canvas.getContext("2d");
pixelAt(cx, 10, 10);
// → [0, 0, 0, 0]
cx.fillStyle = "red";
cx.fillRect(10, 10, 1, 1);
pixelAt(cx, 10, 10);
// → [255, 0, 0, 255]
Аргументы getImageData показывают начальные координаты прямоугольника x и y, которые нам надо получить, за которыми идут ширина и высота.
Игнорируйте прозрачность в этом упражнении, работайте только с первыми тремя цифрами для заданного пикселя. Также не волнуйтесь по поводу обновления поля color при выборе цвета. Просто убедитесь, что fillStyle и strokeStyle контекста установлены в цвет, оказавшийся под курсором.
Помните, что эти свойства принимают любой цвет, который понимает CSS, включая запись вида rgb(R, G, B), которую вы видели в главе 15.
Метод getImageData имеет те же ограничения, что и toDataURL – он выдаст ошибку, когда на холсте содержатся пиксели картинки, скачанной с другого домена. Используйте запись try/catch для сообщения об этих ошибках через окно alert.
<script>
tools["Pick color"] = function(event, cx) {
// Your code here.
};
</script>
<link rel="stylesheet" href="css/paint.css">
<body>
<script>createPaint(document.body);</script>
</body>
Заливка
Это упражнение более сложное, чем предыдущие, и оно потребует разработки нетривиального решения хитрой задачи. Убедитесь, что у вас есть свободное время и терпение перед началом работы, и не отчаивайтесь, если сразу у вас что-то не будет получаться.
Инструмент заливки окрашивает пиксель под курсором мыши и под целой группой пикселей вокруг него, имеющих тот же цвет. Для целей нашего упражнения мы будем считать, что эта группа включает все пиксели, до которых можно добраться от начального, двигаясь по одному пикселю по горизонтали и вертикали (но не по диагонали), не прикасаясь к пикселям, чей цвет отличается от исходного.
Следующая картинка демонстрирует набор пикселей, окрашиваемых, когда инструмент заливки применяется к помеченному пикселю:
Заливка не протекает через диагональные разрывы и не касается пикселей, которых нельзя достичь, даже если они того же цвета, что и исходный.
Вам вновь понадобится getImageData для выяснения цвета пикселя. Скорее всего, удобнее будет получить всю картинку за раз, а потом уже получать данные по пикселям из получившегося массива. Пиксели в массиве организованы схожим образом с решёткой из главы 7, по рядам, только каждый пиксель описывается четырьмя значениями. Первое значение для пикселя с координатами (x,y) находится на позиции (x + y × width) × 4.
Включайте в рассмотрение четвёртое число (альфа), потому что нам нужно будет различать чёрные и пустые (прозрачные) пиксели.
Поиск соседних пикселей того же цвета требует пройти по поверхности пикселей вверх, вниз, влево и вправо, пока там находятся пиксели того же цвета. За первый проход всю группу пикселей найти не получится. Вместо этого нужно будет сделать что-то похожее на отслеживание в регулярных выражениях, описанное в главе 9. Когда у вас есть больше одного возможного направления, нужно сохранить все те, по которым вы прямо сейчас не идёте, и просмотреть их позже, по окончанию текущего шага.