Выбрать главу

function relativePos(event, element) {

  var rect = element.getBoundingClientRect();

  return {x: Math.floor(event.clientX - rect.left),

          y: Math.floor(event.clientY - rect.top)};

}

Несколько инструментов рисования должны слушать событие "mousemove", пока кнопка мыши нажата. Функция trackDrag регистрирует и убирает событие для данных ситуаций.

function trackDrag(onMove, onEnd) {

  function end(event) {

    removeEventListener("mousemove", onMove);

    removeEventListener("mouseup", end);

    if (onEnd)

      onEnd(event);

  }

  addEventListener("mousemove", onMove);

  addEventListener("mouseup", end);

}

У неё два аргумента. Один – функция, которая вызывается при каждом событии "mousemove", а другая – функция, которая вызывается при отпускании кнопки. Каждый аргумент может быть не задан.

Инструмент для рисования линий использует две вспомогательные функции для самого рисования.

tools.Line = function(event, cx, onEnd) {

  cx.lineCap = "round";

  var pos = relativePos(event, cx.canvas);

  trackDrag(function(event) {

    cx.beginPath();

    cx.moveTo(pos.x, pos.y);

    pos = relativePos(event, cx.canvas);

    cx.lineTo(pos.x, pos.y);

    cx.stroke();

  }, onEnd);

};

Функция сначала устанавливает свойство контекста lineCap в “round”, из-за чего концы нарисованного пути становятся закруглёнными, а не квадратными, как это происходит по умолчанию. Этот трюк обеспечивает непрерывность линий, когда они нарисованы в несколько приёмов. Если рисовать линии большой ширины, вы увидите разрывы в углах линий, если будете использовать установку lineCap по умолчанию.

Затем, по каждому событию "mousemove", которое случается, пока кнопка нажата, рисуется простая линия между старой и новой позициями мыши, с использованием тех значений параметров strokeStyle и lineWidth, которые заданы в данный момент.

Аргумент onEnd просто передаётся дальше, в trackDrag. При обычном вызове третий аргумент передаваться не будет, и при использовании функции он будет содержать undefined, поэтому в конце перетаскивания ничего не произойдёт. Но он поможет нам организовать ещё один инструмент, ластик erase, используя очень небольшое дополнение к коду.

tools.Erase = function(event, cx) {

  cx.globalCompositeOperation = "destination-out";

  tools.Line(event, cx, function() {

    cx.globalCompositeOperation = "source-over";

  });

};

Свойство globalCompositeOperation влияет на то, как операции рисования на холсте меняют цвет пикселей. По умолчанию, значение свойства "source-over", что означает, что цвет, которым рисуют, накладывается поверх существующего. Если цвет непрозрачный, он просто заменит существующий, но если он частично прозрачный, они будут смешаны.

Инструмент “erase” устанавливает globalCompositeOperation в "destination-out", что имеет эффект ластика, и делает пиксели снова прозрачными.

Вот у нас уже есть два инструмента для рисования. Мы можем рисовать чёрные линии в один пиксель шириной (это задано значениями свойств холста strokeStyle и lineWidth по умолчанию), и стирать их. Работающий, хотя и примитивный, прототип программы.

Цвет и размер кисти

Предполагая, что пользователи захотят рисовать не только чёрным цветом и не только одним размером кисти, добавим элементы управления для этих настроек.

В главе 18 я обсуждал разные варианты полей формы. Среди них не было полей для выбора цвета. По традиции у браузеров нет встроенных полей для выбора цвета, но за последнее время в стандарт включили несколько новых типов полей форм. Один из них — <input type="color">. Среди других — "date", "email", "url" и "number". Пока ещё их поддерживают не все. Для тега <input> тип по умолчанию – “text”, и при использовании нового тега, который ещё не поддерживается браузером, браузеры будут обрабатывать его как текстовое поле. Значит, пользователям с браузерами, которые не поддерживают инструмент для выбора цвета, необходимо будет вписывать название цвета вместо того, чтобы выбирать его через удобный элемент управления.

controls.color = function(cx) {

  var input = elt("input", {type: "color"});

  input.addEventListener("change", function() {

    cx.fillStyle = input.value;

    cx.strokeStyle = input.value;