Canvas grafika
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:
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ń:
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:

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).

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.