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.