Kawałek Kodu

Dzisiejsze wycinanki nie będą miały wiele wspólnego z wycinankami łowickimi czy kurpiowskimi. Będą nawiązywać do trochę innej tradycji - mianowicie pewnie znanej wielu z Was fascynacji wycinaniem idoli z kolorowych, młodzieżowych czasopism. Dziś już nawet nie trzeba tego robić. W wielu magazynach graficy oferują nam fotosy obiektów westchnień przygotowane już w taki sposób w postaci cyfrowej. Spróbujmy więc odtworzyć ten efekt. 

Mrówcza praca.

Pierwszym krokiem i podstawą naszego efektu będzie skrypt znajdujący krawędzie obiektu. Jednak sam skrypt nie jest przedmiotem dzisiejszego wpisu i skorzystamy z gotowca. Jest nim plugin do biblioteki do wizualizacji danych - D3.js. Tzn. biblioteka tak się nazywa, a sam plugin to /geom/contour.js. Nie musimy go używać wraz ze wspomnianą biblioteką. Będzie czuł się równie dobrze jako samotnik współpracujący z naszym skryptem. Opiera się on na algorytmie maszerujących mrówek (marching ants), który potrawi określić czy dany punkt znajduje się wewnątrz obszaru czy na zewnątrz. Aby ułatwić mu zadanie trzeba zdefiniować czym ma się cechować punkt, aby uznać go za przynależący do jednej z grup. My w przykładzie wykorzystamy obraz w formacie PNG, gdzie przezroczyste piksele będą uznawane za nieprzynależące do obszaru, a nieprzezroczyste jako te z obszaru. Żeby zachęcić coponiektórych do dalszego czytania, zdradzę, że będzie to zdjęcie dziewczyny na przezroczystym tle (sama dziewczyna nie będzie posiadać nic przezroczystego - niestety). Z algorytmem, a właściwie narzędziem wykorzystujący "mrówki" miałeś pewnie styczność w programie graficznym. Magiczna różdżka, to właśnie ta funkcjonalność, która potrafi znaleźć zamknięty obszar o danych cechach.

Funkcja określająca co uznajemy albo nie za punkt w obszarze może wyglądać tak:

const isOpacity = function(x, y) {
  const alpha = pixelData[((y * imageWidth + x) << 2) + 3];
  return (alpha > 0);
}

Dostarczamy ją jako pierwszy parametr dla wspomnianej funkcji w pluginie. x, y to zmienne dostarczane przez plugin, natomiast pixelData, to dane pikseli z CANVAS. Ponieważ jeden piksel reprezentowany jest przez 4 składowe (R, G, B oraz A), to przesuwamy się po tablicy co 4 bajty (<<2, czyli *4). Bajt o indeksie 3 w takiej grupce, to wartość kanału alpha. Z funkcji zwracamy true, jeśli alpha jest większa od 0 (zakładamy, że minimalnie nieprzezroczysty piksel należy do obszaru).

Telimena.

Nałapaliśmy mrówek, więc możemy teraz wypuścić je na naszą dziewczynę i dowiemy się jaki obszar na trawie zajmuje. Zrobimy jej zdjęcie i wytniemy niechlujnie nożyczkami.

Potrzeba nam krótki HTML i trochę więcej JS:

<img src="beach-1853939_girl.png" />

+

/* oprócz poniższego kodu musimy załączyć
   wspomniany plugin
   oraz funkcję isOpacity
*/

/* tworzymy <canvas>
   i pobieramy jego context,
   elementu nie dołączamy
   do drzewa DOM
*/
const canvas = document.createElement('canvas');
const ctx = canvas.getContext("2d");

/* szerokość i wysokość
   obrazka
*/
let imageWidth;
let imageHeight;

/* tablica punktów
   konturu
*/
let points;

/* tablica wartości pikseli
   obrazka
*/
let pixelData;

/* tworzymy obrazek
   wraz i czekamy
   na jego załadowanie
*/

const img = new Image();

img.onload = function() {

/* zapisujemy rozmiary
   obrazka
*/
  imageWidth = this.width;
  imageHeight = this.height;

/* nadajemy takie
   same rozmiary
   <cavas>
*/
  canvas.width = imageWidth;
  canvas.height = imageHeight;

/* teraz możemy spokojnie
   wrysować załadowany obrazek
   do <canvas>
*/
  ctx.drawImage(this, 0, 0);

/* i pobrać jego piksele (RGBA)
*/
  pixelData = ctx.getImageData(0, 0, imageWidth, imageHeight).data;

/* uruchamiamy plugin
*/

  points = geom.contour(isOpacity);

/* mając współrzędnego konturu
   "wycinamy" obrazek z tła
*/
  cutout();
}

/* a obrazek załaduje
   się kiedy podamy
   mu źródło
*/
img.src = document.querySelector('img').src;

/* a tu nasza funkcja
   do wycinanek
*/
function cutout() {

/* będziemy szukać środka
   konturu, więc musimy najpierw
   znaleźć minimum i maksimum
   dla wartości x oraz y
*/
  let maxX = -1;
  let minX = Number.MAX_SAFE_INTEGER;
  let maxY = -1;
  let minY = Number.MAX_SAFE_INTEGER;

/* dla każdego punktu sprawdzamy
   czy jest on mniejszy od aktualnego
   minimum lub większy od aktualnego
   maksimum, jeśli tak, to zapisujemy
   go jako nowe minimum/maksimum
*/
  for (let i = 0; i < points.length; i++) {
    if (points[i][0] > maxX) {
      maxX = points[i][0];
    }
    if (points[i][1] > maxY) {
      maxY = points[i][1];
    }
    if (points[i][0] < minX) {
      minX = points[i][0];
    }
    if (points[i][1] < minY) {
      minY = points[i][1];
    }
  }

/* środek to po prostu połowa
   między maksimum, a minimum
   przesunięta w lewo/dół od minimum
*/
  let middleX = minX + (maxX - minX) / 2;
  let middleY = minY + (maxY - minY) / 2;

/* każdy piksel sprowadzamy do środka
   układu 2D, skalujemy i ponownie
   przesuwamy względem środka
*/
  for (let i = 0; i < points.length; i++) {
    points[i][0] = ((points[i][0] - middleX) * 1.2) + middleX;
    points[i][1] = ((points[i][1] - middleY) * 1.2) + middleY;
  }

/* zaczynamy kreślenie konturu
*/
  ctx.beginPath();
  ctx.moveTo(points[0][0], points[0][1]);

/* ponieważ nie chcemy tak dokładnego
   konturu jak ma obiekt, to używamy
   co 32-gi znaleziony punkt
*/
  for (let i = 1; i < points.length; i += 32) {
    ctx.lineTo(points[i][0], points[i][1]);
  }

/* zamykamy kontur i wypełniamy
   kolorem
*/
  ctx.closePath();
  ctx.fillStyle = "white";
  ctx.fill();

/* na kontur naklejamy
   oryginalny obrazek
*/
  ctx.drawImage(img, 0, 0);

/* podmieniamy dane naszego
   obrazka na wynik skryptu
*/
  document.querySelector('img').src = canvas.toDataURL();
}

Po co nam w ogóle środek obszaru i przesuwanie pikseli względem niego? Po to, aby móc przeskalować cały kontur równomiernie, tzn. oddalić go od obiektu (taki efekt grow dla obszaru selekcji z programu graficznego). Aby to zrobić najpierw trzeba każdy punkt sprowadzić do środka układu współrzędnych 2D (tu jest nim znaleziony środek konturu). Wtedy punkty leżące w ujemnych ćwiartkach jeszcze bardziej staną się ujemne, a w dodatnich dodatnie. Gdybyśmy skalowali surowe współrzędne, kontur powiększyłby się, przesuwając się w prawy dolny róg zdjęcia.

Tak wygląda oryginalny obrazek (przezroczyste tło, więc białe):

A tak wycięty z plakatu obiekt naszych westchnień (czarne tło jest nadane dla BODY):

Kiedy skończysz wycinać swoich idoli ze starych plakatów i gazetek, wpadnij na kolejny wpis. Do przeczytania!

 

Przydatne linki:
Plugin wyszukujący kontur dla biblioteki D3.js.
Nasza jeszcze niewycięta dziewczyna.