Canvas grafika

Ostatnia aktualizacja: 04 września 2020

Aby narysować obrazek na płótnie musimy wykonać 2 kroki:

  • Przygotować obrazek dla JavaScript. Już to poznaliśmy w rozdziale o grafikach.
  • Obrazek pod postacią zmiennej rysujemy na płótnie korzystając z metody drawImage()

Podstawowy wariant użycia metody drawImage() ma postać:


ctx.drawImage(img, x, y)
img określa obrazek, który będziemy rysować na płótnie
x, y określają położenie rysowanego obrazka na płótnie

Aby mieć pewność, że rysowany obrazek jest już w pełni załadowany skorzystamy dla niego ze zdarzenia onload.


const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const image = new Image();
image.addEventListener("load", e => {
    ctx.drawImage(image, 0, 0, 262, 256);
});
image.src = "grafika.jpg";

Po wczytaniu grafiki możemy oczywiście dalej działań na naszym canvasie jak to robilismy do tej pory:

W poniższych listingach użyjemy funkcji, którą napisaliśmy w poprzednim rozdziale.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const image = new Image();
image.addEventListener("load", e => {
    ctx.drawImage(image, 0, 0, 262, 256);

    //stawiamy losowo liczby
    for (let i=0; i<100; i++) {
        const x = rand(0, canvas.width);
        const y = rand(0, canvas.height);
        const size = rand(10, 30);
        const color = `hsla(${Math.random()*360}, 80%, 60%, 0.9)`;

        ctx.fillStyle = color;
        ctx.strokeStyle = "rgba(0,0,0,0.7)";

        ctx.font = `bold ${size}px Arial, sans-serif`;
        ctx.fillText(i, x, y);
        ctx.strokeText(i, x, y)
    }
});

image.src = "fantomus.jpg";

Skalowanie grafiki

Kolejna postać drawImage to:


ctx.drawImage(img, x, y, width, height);
img określa obrazek, który będziemy rysować na płótnie
x, y określają położenie rysowanego obrazka na płótnie
width, height rozmiar grafiki na canvasie

Grafika bez przekształceń:

Fantom


const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();

img.addEventListener("load", e => {
    ctx.drawImage(img, 0, 0, 160, 160);
    ctx.drawImage(img, 180, 20, 120, 220);
    ctx.drawImage(img, 320, 10, 100, 100);
    ctx.drawImage(img, 430, 100, 80, 80);
});

img.src = "fantomus.jpg";

Po przekształceniach i narysowaniu na płótnie:

Wycinanie kawałka grafiki

Dla metody drawImage możemy też zastosować dodatkowe atrybuty określające położenie i rozmiar wycinanego kawałka oryginalnej grafiki.


ctx.drawImage(img, cutX, cutY, cutWidth, cutHeight, x, y, width, height);
img grafika do wyświetlenia
cutX, cutY pozycja wycinanego kawałka w oryginalnej grafice
cutWidth, cutHeight rozmiar wycinanego kawałka w oryginalnej grafice
x, y pozycja grafiki na canvasie
width, height rozmiar grafiki na canvasie

Przykładowo mamy grafikę z kostek - każda o rozmiarze 100x100:

Wycinanie grafiki dla canvas

I teraz chcielibyśmy na canvas wrzucić elementy [1,2,5,6] , [3,4,7,8], [9,10,13,14] i [11,12,15,16] ułożone poziomo obok siebie. Dodatkowo chcielibyśmy by wrzucone na canvas kawałki miały rozmiar nie 200x200 a 160x160:


const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d");
const img = new Image();

img.addEventListener("load", e => {
    //[1,2,5,6]
    ctx.drawImage(img, 0, 0, 200, 200,        0, 0, 160, 160);
    //[3,4,7,8]
    ctx.drawImage(img, 200, 0, 200, 200,      165, 0, 160, 160);
    //[9,10,13,14]
    ctx.drawImage(img, 0, 200, 200, 200,      330, 0, 160, 160);
    //[11,12,15,16]
    ctx.drawImage(img, 200, 200, 200, 200,    495, 0, 160, 160);
});
img.src = "./canvas-cut.jpg";

A przypominasz sobie zadanie, jakie wykonywaliśmy przy tablicach wielowymiarowych. Za pomocą powyższych informacji moglibyśmy wreszcie je zrobić tak jak Bozia przykazała.

Do poniższego skryptu użyję tej grafiki.


const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

//by bylo ładniej trochę zmieniłem indeksy w stosunku do kodu z tamtego zadania
const level = [
    [16, 16, 5, 5, 2, 2, 5, 5, 16, 16],
    [16, 5, 5, 5, 2, 2, 5, 5, 5, 16],
    [16, 5, 16, 16, 2, 2, 16, 16, 5, 16],
    [16, 5, 2, 2, 2, 2, 2, 2, 5, 16],
    [2, 2, 2, 2, 16, 16, 2, 2, 2, 2],
    [2, 2, 2, 2, 16, 16, 2, 2, 2, 2],
    [16, 5, 2, 2, 2, 2, 2, 2, 5, 16],
    [16, 5, 16, 16, 2, 2, 16, 16, 5, 16],
    [16, 5, 5, 5, 2, 2, 5, 5, 5, 16],
    [16, 16, 5, 5, 2, 2, 5, 5, 16, 16]
];

//funkcja rysująca
//ctx - canvas
//x, y - pozycja rysowania
//size - rozmiar rysowanego na planszy kafelka
//tileNr - numer kafelka liczony od 1
function paint(ctx, x, y, size, tileNr) {
    const sizeOnImage = 48;
    const cutX = Math.floor((tileNr - 1) % 7) * sizeOnImage;
    const cutY = Math.floor((tileNr - 1) / 7) * sizeOnImage;
    ctx.drawImage(
        img,
        cutX,
        cutY,
        sizeOnImage,
        sizeOnImage,
        size * x,
        size * y,
        size,
        size
    );
}

function drawLevel() {
    for (let y=0; y<level.length; y++) {
        const sub = level[y];

        for (let x=0; x<sub.length; x++) {
            paint(ctx, x, y, 50, sub[x]);
        }
    }
}

const img = new Image();
img.addEventListener("load", e => {
    drawLevel();
});
img.src = "sprite-tiles.png";

Pattern

Podobnie jak przy tle w CSS, także dla canvas możemy użyć powtarzania tła. Służy do tego funkcja createPattern(img, powtarzanie). Funkcja ta przyjmuje parametry:

img wczytana wcześniej grafika
repeat sposób powtarzania. Przyjmuje jedną z wartości: repeat, repeat-x, repeat-y i no-repeat

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const img = new Image();
img.addEventListener("load", e => {
    ctx.fillStyle = ctx.createPattern(img, "repeat");
    ctx.fillRect(40, 20, 150, 120);
    ctx.strokeRect(40, 20, 150, 120);
});
img.src = "./happy.png";

Zauważ, że nasz wzorek zaczyna się krzywo. Podobnie do transformacji aby wzór rozpoczynał się wraz z figurą, musimy posłużyć się funkcją translate:


const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const img = new Image();
img.addEventListener("load", e => {
    ctx.save();
    ctx.translate(40, 20);
    ctx.fillStyle = ctx.createPattern(img, "repeat");
    ctx.fillRect(0, 0, 150, 120); //zmieniamy pozycję rysowanej figury na 0,0
    ctx.strokeRect(0, 0, 150, 120);
    ctx.restore();
});
img.src = "./happy.png";

Podobnie będzie przy pozostałych typach powtórzeń:


ctx.save();
ctx.translate(40, 20);
ctx.fillStyle = ctx.createPattern(img, "repeat");
ctx.fillRect(0, 0, 150, 120);
ctx.strokeRect(0, 0, 150, 120);
ctx.restore();

ctx.save();
ctx.translate(200, 20);
ctx.fillStyle = ctx.createPattern(img, "repeat-x");
ctx.fillRect(0, 0, 150, 120);
ctx.strokeRect(0, 0, 150, 120);
ctx.restore();

ctx.save();
ctx.translate(360, 20);
ctx.fillStyle = ctx.createPattern(img, "repeat-y");
ctx.fillRect(0, 0, 150, 120);
ctx.strokeRect(0, 0, 150, 120);
ctx.restore();

ctx.save();
ctx.translate(520, 20);
ctx.fillStyle = ctx.createPattern(img, "no-repeat");
ctx.fillRect(0, 0, 150, 120);
ctx.strokeRect(0, 0, 150, 120);
ctx.restore();

Manipulacja pikselami

Każda grafika a w tym canvas to uporządkowany zbiór pikseli. Lewy górny róg to początek, a dolny prawy do koniec.

Aby manipulować poszczególnymi pikselami wykorzystamy do tego obiekt typu ImageData. Obiekt taki jest "zapisem grafiki" i zawiera 3 właściwości:

width, height rozmiary grafiki
imageData.data zwraca 1 wymiarową tablicę, której poszczególnymi wartościami są składowe RGBa pikseli czyli
imageData.data = [r,g,b,a, r,g,b,a, r,g,b,a, ....];

Zwracana tablica jest wielkości szerokośćCanvas * wysokośćCanvas * 4 a każda jej komórka zawiera wartość z przedziału 0-255

Element canvas udostępnia nam też metody, dzięki którym możemy obsłużyć piksele:

context.createImageData(width, height)tworzy pusty obiekt typu ImageData o wymiarach podanych w parametrach. Wszystkie piksele zwróconego obiektu są przezroczyste
context.createImageData(innyImageData)zwraca obiekt typu ImageData o wymiarach takich samych jakie ma obiekt przekazany w parametrze. Tylko wymiary są kopiowane. Dane o pikselach nie są kopiowane.
context.getImageData(x, y, width, height)pobiera obiekt typu ImageData, który jest wycinkiem canvasu o wymiarach podanych w atrybutach. Jeżeli x i y nie są podane, wtedy przyjmują wartości 0
context.putImageData(ImageData, x, y)rysuje na canvasie w pozycji x,y piksele z imagedata.

Przykład1 : podmiana kolorów

Mamy prostą grafikę kwiatka. Nie ma ona antyaliasingu (rozmycia pikseli) dla łatwiejszej manipulacji pikselami.

Naszym zadaniem jest pobrać dane tej grafiki, a następnie zamienić liście z koloru żółtego (rgb: 255,245,104) na czerwony (rgb: 255,0,0).

Kwiatek

Jak to zrobić? Musimy narysować na płótnie naszą grafikę - to już znamy z poprzednich przykładów.
Następnie pobierzemy dane z naszego płótna do zmiennej myImgData.
Po pobraniu takich danych wystarczy zrobić po nich pętlę (iterując co 4, bo każdy piksel opisany jest 4 składowymi: red, green, blue i alpha) i w razie czego modyfikować wartości kolorów danego piksela.
Po tej operacji zapisujemy nasz zmodyfikowany obiekt myImgData na płótno.


const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();

img.addEventListener("load", e => {
    //rysujemy kwiatek na płótnie
    ctx.drawImage(img, 0, 0);

    //pobieramy dane z płótna
    const myImgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    //zmieniamy kolor listków kwiatka
    for (let i=0; i<myImgData.data.length; i+=4) {
        if (myImgData.data[i] === 255) {
            myImgData.data[i] = 255;
        }
        if (myImgData.data[i+1] === 245) {
            myImgData.data[i+1] = 0;
        }
        if (myImgData.data[i+2] === 104) {
            myImgData.data[i+2] = 0;
        }
        // i+3 to alpha koloru
    }

    //rysujemy na płótnie nasz zmieniony obraz
    ctx.putImageData(myImgData, 220, 0);
});

img.src = "kwiatek.png";

Przykład2 : odwrócenie kolorów

Kolejny przykład - bardzo podobny do poprzedniego polega na odwróceniu kolorów.


const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();

img.addEventListener("load", e => {
    ctx.drawImage(img, 0, 0);

    //pobieramy dane z płótna
    const myImgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    //odwracam kolory
    for (let i=0; i < myImgData.data.length; i += 4) {
        myImgData.data[i] =   255 - myImgData.data[i]; //czerwony
        myImgData.data[i+1] = 255 - myImgData.data[i+1]; //zielony
        myImgData.data[i+2] = 255 - myImgData.data[i+2]; //niebieski
        // i+3 to alpha koloru
    }

    //rysujemy na płótnie nasz zmieniony obraz
    ctx.putImageData(myImgData, 276, 0);
});

img.src = "fantomus.jpg";

Przykład3 : desaturacja kolorów

Kolejny przykład pokazuje jak zrównać kolory - czyli zrobić grafikę w odcieniach szarości:


const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const fanthomas = new Image();

fanthomas.addEventListener("load", e => {
    ctx.drawImage(fanthomas, 0, 0);

    //pobieramy dane z płótna
    const myImgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    //ujednolicam kolory - wyliczając średnią
    for (let i=0; i < myImgData.data.length; i += 4) {
        const r = myImgData.data[i];
        const g = myImgData.data[i+1];
        const b = myImgData.data[i+2];
        const a = myImgData.data[i+3];
        const average = (r+g+b) / 3;
        myImgData.data[i] = myImgData.data[i+1] = myImgData.data[i+2] = average;
    }

    //rysujemy na płótnie nasz zmieniony obraz
    ctx.putImageData(myImgData, 276, 0);
});

fanthomas.src = "fantomus.jpg";

Ciekawy tutorial o działaniu na pikselach canvasu możesz znaleźć pod adresem: http://net.tutsplus.com/tutorials/javascript-ajax/canvas-from-scratch-pixel-manipulation/

I tak dalej i tak dalej. Jakiś czas temu robiłem mini eksperyment, który polegał na odczycie pikseli z wczytywanych plików. Dzięki temu mogłem te informacje później użyć do generowania dodatkowych rzeczy - np. animacji. Użyłem tam właśnie omawianych technik.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem. Aha - i ta strona korzysta z ciasteczek.

Menu