Глава 19
Проект: програма за рисуване

Виждам много цветове пред мен. Гледам своето празно платно. След това се опитвам да прилагам цветовете, също както думите оформят поеми, също както нотите оформят музика.”

Joan Miro

Материалът от предишната глава дава всички елементи, които ни трябват за да се изгради просто уеб приложение. В тази глава ние ще направим точно това.

Нашето приложение ще бъде уеб-базирана програма за рисуване, по подобие на Microsoft Paint. Можете да го използвате за да отворите файлове с изображения и освен това да рисувате върху тях с мишката. Ето как ще изглежда:

A simple paint program

Рисуването върху компютър е велико. Не е нужно да се притесняваме за материали, умения или талант. Просто направете цапаница.

Изпълнение

Интерфейса за програмата за рисуване, показва голям <canvas> елемент с голям брой полета на форми под него. Потребителя рисува върху картината, като избира инструмент от <select> полето и след това щракне или плъзне върху платното. Има инструменти за рисуване на лини, за изтриване на части от картината, добавяне на текст и т. н.

Кликването върху платното ще оставим в ръцете на "mousedown" събитието за текущо избрания инструмент, който може да се обработва в зависимост от това, кой начин ще изберем. Рисуващият линия инструмент, например, ще се ослушва за "mousemove" събитие, докато бутона на мишката е свободен и ще чертае линии по пътя на мишката използвайки текущия цвят и размер на четка.

Цвета и размера на четката се избират с допълнителни полета на формуляра. Те са закачени в контекста на fillStyle, strokeStyle и lineWidth, които актуализират платното за рисуване, винаги, когато те се променят.

Можете да заредите изображение в програмата по два начина. Първият използва поле на файл, където потребителя може да избере файл от своята файлова система. Вторият пита за URL и ще донесе изображение от уеб.

Изображенията се запазват по-малко по-нетипичен начин, с линк Save долу в дясно на текущото изображение. Те може да се следват, споделят и запазват. Ще обясня, как става това по-късно.

Изграждане на DOM

Интерфейса на нашата програма е изграден с повече от 30 DOM елемента. Това трябва да се изгради по някакъв начин.

HTML е най-очевидният формат за дефиниране на сложни структури на DOM. Но разделянето на програмата на парчета от HTML и скрипт е затруднено от факта, че много от DOM елементите се нуждаят обработка на събитие или да бъдат манипулирани от скрипта по някакъв друг начин. По този начин, на нашия скрипт ще се налижи да прави много querySelector (или подобни) за да намира и призовава DOM елементите, от които има нужда за да работи.

Би било хубаво, ако структурата на DOM за всяка част от нашия интерфейс е дефиниран в близост до кода на JavaScript, който да го управлява. Поради тази причина, аз избрах да направя всичко по създаването на DOM разклонения вътре в JavaScript. Както видяхме в Глава 13, вградения интерфейс за изграждане на структура на DOM е ужасно многословен. Ако ще правим много голяма структура на DOM се нуждаем от помощна функция.

Тази помощна функция е разширена версия на elt функцията от Глава 13. Тя създава един елемент давайки му име и атрибути и му добавя всички допълнителни аргументи, които той получава, като разклонения на деца, автоматично преобразуване на strings в текст разклонения.

function elt(name, attributes) {
  var node = document.createElement(name);
  if (attributes) {
    for (var attr in attributes)
      if (attributes.hasOwnProperty(attr))
        node.setAttribute(attr, attributes[attr]);
  }
  for (var i = 2; i < arguments.length; i++) {
    var child = arguments[i];
    if (typeof child == "string")
      child = document.createTextNode(child);
    node.appendChild(child);
  }
  return node;
}

Това ни позволява да създаваме елементи лесно, без да прави нашия изходен код дълъг и скучен, като договор на корпорация с краен потребител.

Фондацията

В основата на нашата програма е createPaint функцията, която добавя интерфейс за рисуване на DOM елемент даден, като аргумент. Понеже искаме да изградим нашата програма парче по парче, ние определяме един обект, наречен controls, който ще съдържа функции за инициализиране на различните видове управления под изображението.

var controls = Object.create(null);

function createPaint(parent) {
  var canvas = elt("canvas", {width: 500, height: 300});
  var cx = canvas.getContext("2d");
  var toolbar = elt("div", {class: "toolbar"});
  for (var name in controls)
    toolbar.appendChild(controls[name](cx));

  var panel = elt("div", {class: "picturepanel"}, canvas);
  parent.appendChild(elt("div", null, panel, toolbar));
}

Всяко управление има достъп до контекста на платното за рисуване и чрез този контекст - canvas свойство към самия <canvas> елемент. По-голямата част от програмата живее в този canvas - той съдържа текущата картина, както и избрания цвят (в fillStyle свойство) и размера на четката (в lineWidth свойство).

Ние ще увием платното и управлението в <div> елементи с класове, така че да можем да добавим малко style, като сива рамка около картината.

Избор на инструменти

На първото управление добавяме <select> елемент, който позволява на потребителя да вземе инструмент за рисуване. Както с controls ние ще използваме обект за събиране на различни инструменти, така че няма да ги кодираме на едно място, а ще добавим повечето инструменти по-късно. Този обект свързва имената на инструментите с функция, която трябва да се извика, когато те се избират и платното е кликнато.

var tools = Object.create(null);

controls.tool = function(cx) {
  var select = elt("select");
  for (var name in tools)
    select.appendChild(elt("option", null, name));

  cx.canvas.addEventListener("mousedown", function(event) {
    if (event.which == 1) {
      tools[select.value](event, cx);
      event.preventDefault();
    }
  });

  return elt("span", null, "Tool: ", select);
};

Полето с инструментите е населено с <option> елементи за всички инструменти, които са определени и манипулатора "mousedown" на елемента на платното се грижи за извикване на функцията за текущия инструмент, подавайки и обекта на събитието и контекста на рисуването, като аргументи. Тя също извиква preventDefault, така че задържането на бутона на мишката и влаченето не кара браузъра да избира части от страницата.

Най-основния инструмент е за линия, който позволява на потребителя да чертае линии с мишката. За да накараме линията да свършва на правилното място, ние трябва да сме в състояние да намерим относителните координати на платното, където дадено събитие съответства на мишката. Метода getBoundingClientRect на кратко споменат в Глава 13, може да ни помогне тука. Той ни казва къде е показан елемент спрямо горния ляв ъгъл на екрана. Свойствата clientX и clientY на мишката също са свързани с този ъгъл, така че можем да извадим горния ляв ъгъл на платното от тях за да получим позиция по отношение на този ъгъл.

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", което причинява двата края на очертанията на пътя да са заоблени, а не с квадратна форма по подразбиране. Това е трик за да се уверим, че множеството отделни линии изготвени в отговор на отделни събития, изглеждат, като една съгласувана линия. При по-големи широки линии, вие ще видите пропуски в ъглите, ако използвате обикновени линии по подразбиране.

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

onEnd аргумента на tools.Line просто се подава през trackDrag. Нормалният начин за стартиране на инструмент не изисква подаване на трети аргумент, за това при използването на инструмента за линия, този аргумент ще доведе до undefined и нищо няма да се случи в края на драгването на мишката. Аргумента е там за да ни позволи прилагането на функцията за изтриване на върха на инструмента за линия с много малко допълнителен код.

tools.Erase = function(event, cx) {
  cx.globalCompositeOperation = "destination-out";
  tools.Line(event, cx, function() {
    cx.globalCompositeOperation = "source-over";
  });
};

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

Инструмента за изтриване определя globalCompositeOperation да е "destination-out", което има ефекта за изтриване на пикселите, които се допират, като ги прави прозрачни отново.

Това ни дава два инструмента в нашата програма за рисуване. Можем да чертаем черни линии един пиксел широки (по подразбиране за strokeStyle и lineWidth върху платното) и да ги изтрием после. Това работи, макар и доста ограничено в програмата за рисуване.

Цвят и размер на четката

Ако потребителите искат да направят други цветове освен черно и да използват различни размери на четката, трябва да добавим управления за тези две настройки.

В Глава 18 обсъдихме различни полета на формуляри. Но цветни полета не бяха сред тях. Традиционно браузъра не разполага с вградена поддръжка за цветови колектори, но в последните няколко години редица типове полета на форми са стандартизирани. Един от тях е <input type="color">, други са "date", "email", "url" и "number". Не всички браузъри ги подкрепят все още - по време на това писане, никаква версия на Internet Explorer не поддържа цветни полета. Типа по подразбиране на <input> тага е "text" и когато се използва неподдържан тип, браузъра ще го третира, като текстово поле. Това означава, че потребителите, които работят на Internet Explorer с нашата програма за рисуване, ще трябва да въвеждат името на цвета, който искат, а не да го избират от удобна джаджа.

controls.color = function(cx) {
  var input = elt("input", {type: "color"});
  input.addEventListener("change", function() {
    cx.fillStyle = input.value;
    cx.strokeStyle = input.value;
  });
  return elt("span", null, "Color: ", input);
};

Всеки път, когато стойността на цвета на полето се промени, контекста на рисуване с fillStyle и strokeStyle се актуализира, за да подкрепи новата стойност.

Полето за конфигуриране размера на четката работи по същия начин.

controls.brushSize = function(cx) {
  var select = elt("select");
  var sizes = [1, 2, 3, 5, 8, 12, 25, 35, 50, 75, 100];
  sizes.forEach(function(size) {
    select.appendChild(elt("option", {value: size},
                           size + " pixels"));
  });
  select.addEventListener("change", function() {
    cx.lineWidth = select.value;
  });
  return elt("span", null, "Brush size: ", select);
};

Кодът генерира възможности от масив за размери на четката и отново гарантира, че lineWidth на платното се актуализира, когато е избран размер на четката.

Запазването

За да се обясни изпълнението на линка Save, първо трябва да ви разкажа за URL адреса на данните. URL адрес на данни е URL с данни: като негов протокол. За разлика от обикновените http: и https: URL адреси, URL данните не сочат ресурс, а по-скоро съдържат целия ресурс в тях. Това са URL данни, съдържащи един прост HTML документ:

data:text/html,<h1 style="color:red">Hello!</h1>

URL адресите на данни са полезни за различни задачи, като включително малки изображения директно в style файл. Те също така ни позволяват да се свържем към файловете, които създадохме от страна на клиента в браузъра, без да се налага първо да ги местим на някой сървър.

Елементите на платното имат удобен метод, наречен toDataURL, който ще върне на URL данни, които съдържа картината на платното, като графичен файл. Ние не искаме да актуализираме нашия линк Save всеки път, когато картината се променя, обаче. За големи картини, които включват преместване на доста данни в линка, ще бъде забележимо бавно. Вместо това ще пригодим линка да актуализира своя href атрибут, когато е фокусиран с клавиатурата или мишката преместена върху него.

controls.save = function(cx) {
  var link = elt("a", {href: "/"}, "Save");
  function update() {
    try {
      link.href = cx.canvas.toDataURL();
    } catch (e) {
      if (e instanceof SecurityError)
        link.href = "javascript:alert(" +
          JSON.stringify("Can't save: " + e.toString()) + ")";
      else
        throw e;
    }
  }
  link.addEventListener("mouseover", update);
  link.addEventListener("focus", update);
  return link;
};

По този начин връзката просто си седи тихо там, сочейки нещо погрешно, но когато потребителя подходи към нея, тя магически се актуализира, за да се отбележи в настоящата картина.

Ако заредим голям образ, някои браузъри ще се задавят с гигантски URL данни, който линка произвежда. За малки картини, този подход работи без проблем.

Но тука отново се впускаме в тънкостите на тестовата среда на браузъра. Когато изображението е заредено от URL на друг домейн, ако отговора на сървъра не включва кауза, която да казва на браузъра, че ресурса може да се използва от друг домейн (виж Глава 17), тогава платното ще може да съдържа информация, която потребителя може да вижда, но скрипта да не може.

Ние може да поискаме изображение, което съдържа лична информация (на пример, графика на салдото от потребителска сметка) използвайки потребителската сесия. Ако скриптовете могат да получат информация от това изображение, те биха могли да се ровят в данните на потребителя по нежелателен начин.

За да предотвратим този вид изтичане на информация, браузъра ще отбележи платното, като заразено, когато изображението, което скрипта не може да види се рисува върху него. Пиксел данни, включително URL данни не могат да бъдат извлечени от заразено платно. Можем да пишем върху него, но не можем да го прочетем.

Ето защо се нуждаем от try/catch изявлението в update функцията за линка Save. Когато платното се превърне в заразено, извиквайки toDataURL ще повдигне изключение, като инстанция на SecurityError. Когато това се случи, ние ще създадем връзка към точката на още един вид URL, използвайки javascript: протокол. Тази връзка изпълнява просто скрипта даден след двуточието, която следвайки този линк ще се покаже alert прозорец информирайки потребителя за проблема, когато се натисне.

Зареждане на файлове с изображения

Последните две управления се използват за прехвърляне на изображения от локални файлове и от URL адреси. Ще имаме нужда от следната помощна функция, която се опитва да зареди файл с изображение от URL и да замени съдържанието на платното с него.

function loadImageURL(cx, url) {
  var image = document.createElement("img");
  image.addEventListener("load", function() {
    var color = cx.fillStyle, size = cx.lineWidth;
    cx.canvas.width = image.width;
    cx.canvas.height = image.height;
    cx.drawImage(image, 0, 0);
    cx.fillStyle = color;
    cx.strokeStyle = color;
    cx.lineWidth = size;
  });
  image.src = url;
}

Ние искаме да променим размера на платното, за да отговаря на изображението. По някаква причина, променяната на размера на платното ще предизвика неговия контекст за рисуване да забрави конфигурационните свойства, като fillStyle и lineWidth, така че функцията ще ги запази и възстанови, след като актуализира размера на платното.

Управлението за зареждане на локален файл използва FileReader техниката от Глава 18. Отделно от readAsText метода, който използвахме там, такива четящи обекти също имат метод наречен readAsDataURL, което е точно това, което ни трябва тук. Ние зареждаме файла, който потребителя е избрал като URL данни и го предаваме на loadImageURL да го пусне в платното.

controls.openFile = function(cx) {
  var input = elt("input", {type: "file"});
  input.addEventListener("change", function() {
    if (input.files.length == 0) return;
    var reader = new FileReader();
    reader.addEventListener("load", function() {
      loadImageURL(cx, reader.result);
    });
    reader.readAsDataURL(input.files[0]);
  });
  return elt("div", null, "Open file: ", input);
};

Зареждането на файл от URL дори е по-лесно. Но с текстово поле, то е по-малко ясно, кога потребителя приключва с писането на URL, така че не можем просто да се ослушваме за "change" събития. Вместо това, ще увием полето във форма, която да реагира при подаване на формуляра, когато потребителя натисне Enter или е кликнал върху бутона за зареждане.

controls.openURL = function(cx) {
  var input = elt("input", {type: "text"});
  var form = elt("form", null,
                 "Open URL: ", input,
                 elt("button", {type: "submit"}, "load"));
  form.addEventListener("submit", function(event) {
    event.preventDefault();
    loadImageURL(cx, input.value);
  });
  return form;
};

Сега сме определили всички управления, от които нашата проста програма за рисуване се нуждае, но тя може да използва още няколко инструмента.

Довършване на горе

Можем лесно да добавим инструмент за текст, който използва prompt, за да пита потребителя, какъв string трябва да направи.

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. Веднага след, като функцията установи такава точка, тя връща точката умножена по radius аргумента.

Цикъла е необходим за по-равномерно разпределение на точките. Друг разбираем начин за генериране на случайна точка във вътрешноста на кръг е да се използва случаен ъгъл и разстояние и извикване на Math.sin и Math.cos да създадат съответната точка. Но с този метод, точките са по-склонни да се появяват в близост до центъра на кръга. Има и други начини за това, но те са по-сложни.

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

<link rel="stylesheet" href="css/paint.css">

<body>
  <script>createPaint(document.body);</script>
</body>

Упражнения

Все още има много място за подобрения в тази програма. Нека добавим още няколко функции, като упражнения.

Правоъгълници

Дефинирайте инструмент наречен Rectangle който запълва правоъгълник (виж fillRect метода в Глава 16) с текущия цвят. Правоъгълника трябва да обхваща от точката, където потребителя натисне бутона на мишката до точката, където го освобождава. Имайте в предвид, че последното може да бъде над или в ляво от първото.

След като той работи, вие ще забележите, че до известна степен дразни, като не се вижда правоъгълника, когато влачите мишката за да определите своя размер. Можете ли да измислите начин за показване на модела на правоъгълника по време на драгването без да го рисувате всъщност на платното, докато бутона на мишката се освободи?

Ако нищо не ви идва на ум се върнете на position: absolute стила обсъден в Chapter 13, който може да се използва за наслагване на разклонения върху останалата част от документа. Свойствата pageX и pageY на събитието на мишката могат да бъдат използвани, за позициониране на елемент точно под мишката, чрез определяне на left, top, width и height стилове към правилните пикселни стойности.

<script>
  tools.Rectangle = function(event, cx) {
    // Your code here.
  };
</script>

<link rel="stylesheet" href="css/paint.css">
<body>
  <script>createPaint(document.body);</script>
</body>

Можете да използвате relativePos за да намерите ъгъл, съответстващ на началото на плъзгане на мишката. Измислянето на това, къде свършва влаченето, може да бъде направено с trackDrag или като регистрирате свой собствен манипулатор на събитие.

Когато имате два ъгъла на правоъгълника, трябва по някакъв начин да превърнете това в аргументи, които fillRect очаква: горния ляв ъгъл, ширина и височина на правоъгълника. Math.min може да се използва за да намерите най-левия х-координата и най-гориния у-координата. За да получите ширината и височината, можете да ползвате Math.abs (абсолютната стойност) върху разликата между двете страни.

Показването на правоъгълника по време на плъзгане на мишката изисква подобен набор от числа, но в контекста на цялата страница, а не по отношение на платното. Помислете за написване на функция findRect, която преобразува две точки в обект с top, left, width и height свойства, така че да не е нужно да се пише една и съща логика два пъти.

След това можете да създадете <div> разклонение и настроите style.position: absolute. При определяне на стилове за позициониране, не забравяйте да добавите "px" към номерата. Разклонението трябва да се добавя към документа (може да го добавите с document.body, както и да го отстраните) отново, когато влаченето завърши и действителния правоъгълник се изготви върху платното.

Избор на цвят

Друг инструмент, който се среща често в графичните програми е колектор за цвят, който позволява на потребителя, като кликне върху изображението да опредли цвета под показалеца на мишката. Изградете това.

За тази услуга се нуждаем от начин за достъп до съдържанието на платното. Метода toDataURL повече или по-малко е направил това, но вземането на пиксел-информация от такъв URL с данни е трудно. Вместо това, използвайте метода getImageData в контекста на рисуването, който връща правоъгълно парче с изображение, като обект с width, height, и data свойства. Свойството data притежава масив с числа от 0 до 255, като се използват четири числа за представяне на червено, зелено, синьо и алфа компонента (прозрачност) за всеки пиксел.

Този пример извлича цифрите за един пиксел от платното веднъж, когато платното е празно (всички пиксели са прозрачно черни) и веднъж, когато пикселът е оцветен в червено.

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 показват началните х и у - координати на правоъгълника, който искаме да обработим, последвани от неговата ширина и височина..

Игнорирайте прозрачността по време на това упражнение и вижте само първите три стойности за даден пиксел. Също така не се притеснявайте за актуализирането на цветовете на полето, когато потребителя избира цвят. Просто се уверете, че контекста за рисуване на 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>

Отново ще трябва да ползвате relativePos, за да разберете кой пиксел е кликнат. Функцията pixelAt в примера показва, как да получите стойностите на един пиксел. Поставянето им в един rgb string просто изисква конкатенация на strings.

Уверете се, че проверката за изключение е инстанция на SecurityError, така че да не се справяте случайно с грешен вид изключение.

Наводнение

Това упражнение е за по-напреднали, отколкото предните две и ще изисква да се изработи nontrivial решение на сложен проблем. Уверете се, че имате достатъчно търпение и време, преди да започнете да работите върху него и не се обезсърчавайте от първите неуспехи.

Наводнението е инструмент за запълване на цветове на пиксели под мишката и околните пиксели с един и същи цвят. За целите на това упражнение, обмислете такава група за включване на всички пиксели, които могат да бъдат достигнати от нашия начален пиксел чрез преместване със стъпка един пиксел хоризонтално и вертикално (без диагонал), без да докосваме пиксела, който има различен цвят от началния пиксел.

Следното изображение илюстрира набор от оцветени пиксели, когато инструмента за наводнение се използва с маркирания пиксел.

Flood fill example

Наводнението не изтича през диагоналните пропуски и не докосва пикселите, които не са достъпни, дори да имат същия цвят, като целевия пиксел.

Вие отново ще трябва да разберете цвета на всеки пиксел с getImageData. Може би е добра идея да се извлече цялото изображение на един път и след това да изберете пиксел данни от получения масив. Пикселите са организирани в този масив по начин подобен на елементите на мрежата в Глава 7, по един ред в даден момент с изключение на това, че всеки пиксел е представен от четири стойности. Първата стойност на пиксела е (x, y) в позиция (x + y × width) × 4.

Необходимо е включването на четвърта (алфа) стойност, тъй като искаме да бъдем в състояние да кажем разликата между празни и черни пиксели.

Намирането на всички съседни пиксели със същия цвят изисква от вас “разходка” над повърхността на пикселите, един нагоре, надолу, наляво и надясно, докато новите същия цвят пиксели бъдат намерени. Но вие няма да намерите всички пиксели от една група с първата разходка. По-скоро трябва да направите нещо подобно на връщане назад, извършено с регулярен израз за съвпадение, описано в Глава 9. Когато има повече от една възможна посока, за да продължите, трябва да запишете всички посоки, които не се вземат веднага и да ги прегледате по-късно, когато приключите текущата разходка.

В едно нормално по-големина изображение има много пиксели. За това, вие трябва да се погрижите да се извършва необходимото минимално количество работа или програмата ще отнеме много дълго време да се изпълнява. Например всяка разходка трябва да игнорира пиксела видян от предишните разходки, така че да не се повтаря вече свършената работа.

Аз препоръчвам да извикате fillRect за отделните пиксели, когато един пиксел, който трябва да бъде оцветен е намерен и запазите някаква структура от данни, която ви казва за всички пиксели, които вече са били разгледани.

<script>
  tools["Flood fill"] = function(event, cx) {
    // Your code here.
  };
</script>

<link rel="stylesheet" href="css/paint.css">
<body>
  <script>createPaint(document.body);</script>
</body>

При дадена двойка начални координати и данните за изображението върху цялото платно, този подход трябва да работи с:

  1. Създаване на масив, който да държи информация за вече оцветени координати.

  2. Създаване на масив - работен списък, който да държи координати, които трябва да бъдат разгледани. Поставете начална позиция в него.

  3. Когато работния списък е празен, ние сме готови.

  4. Премахнете една двойка координати от работния списък.

  5. Ако тези координати са вече в нашия масив от цветни пиксели, се върнете към стъпка 3.

  6. Вземете цвета на пиксела в текущите координати и добавете координатите към масива от цветни пиксели.

  7. Добавяне на координатите на всеки съседен пиксел, чийто цвят е същия, като оригиналния цвят на началния пиксел, към работния списък.

  8. Върнете се към стъпка 3.

Работния списък може да бъде просто масив от векторни обекти. Структурата от данни, която проследява цветните пиксели ще бъде консултирана много често. Търсенето през цялото това нещо, всеки път, когато нов пиксел се инспектира ще отнеме много време. Вместо това можете да създадете масив, който съдържа стойност за всеки пиксел, отново с помощта на x + y * width схемата, за асоцииране на позиции с пиксели. Когато проверявате дали един пиксел е вече оцветен, вие директно може да получите достъп до позицията съответстваща на текущия пиксел.

Можете да сравните цветовете, чрез преминаване през съответната част от масива с данни, сравнявайки едно поле в даден момент. Или може да "кондензирате" цвят към единично число или string и да го сравните. Когато правите това, трябва да гарантирате, че всеки цвят произвежда уникална стойност. Например, просто добавяне на цветни компоненти не е безопасно, тъй като различни цветове, могат имат една и съща сума.

Когато се изброяват съседите на дадена точка, трябва да се погрижите за изключването на съседи, които не са от вътрешната страна на платното или вашата програма може да избяга в дадена посока завинаги.