Canvas

Ostatnia aktualizacja: 08 listopada 2019

Wraz z HTML 5 doszło nam kilkanaście nowych elementów. Jednym z nich jest Canvas - czyli po naszemu płótno. Element ten bardzo przypomina obszar roboczy znany praktycznie z każdego programu graficznego. Różnica jest taka, że tam rysujemy za pomocą dostępnych narzędzi (np. piórka), a tutaj - za pomocą JavaScriptu.


<canvas width="400" height="200">
</canvas>

Zanim zaczniemy

Kilka słów na początek. Element taki zachowuje się zupełnie jak img na stronie. Możemy na niego wgrywać za pomocą JavaScriptu obrazki, rysować po nim różne kształty, pisać po nim, odczytywać z niego informacje o pikselach itp. Dodatkowo możemy to wszystko robić dynamicznie, dzięki czemu idealnie nadaje się do tworzenia różnych animacji czy np. gier. A na końcu gdy klikniemy na niego prawym guzikiem, możemy wybrać opcję "Zapisz grafikę jako..." - zupełnie jak przy innych grafikach na stronie.

Wyobraź sobie, że robisz dynamiczną platformówkę. W takiej grze powinno się zawrzeć wiele poruszających się i zależnych od siebie elementów. Gdybyśmy to robili za pomocą CSS i diwów (dać się da), to trzeba by ogarnąć jakoś problem ze skalowaniem (RWD), a i w naszym HTML zapewne byśmy mieli niezły śmietnik, który trzeba by mieć jakoś pod kontrolą. Dzięki canvasowi unikamy takich nieprzyjemnych sytuacji.

Z drugiej strony używanie canvasa nie należy do najprzyjemniejszych rzeczy na świecie. Zrobienie nawet najmniejszej głupotki - ot narysowania kilku kwadratów wiąże się z wyprodukowaniem dość pokaźnych ilości kodu. Do tego w klasycznych warunkach jeżeli chcemy dla elementu wprowadzić interakcję - np. najazd kursorem, przesunięcie itp. - w css użyjemy po prostu :hover, a w js np. mouseenter/mouseleave. W przypadku canvas - jako że to tylko rysowane rzeczy na innym elemencie - trzeba by taką interakcję programowo wyliczać obliczając pozycję kursora, elementu itp. Ogólnie więc czeka nas tutaj dużo zabawy.

Z tego też powodu jeżeli chcemy tutaj tworzyć coś bardziej skomplikowanego niż proste rzeczy, większość ludzi sięga raczej po gotowe biblioteki, które udostępniają nam setki przydatnych funkcjonalności. Jedną z najpopularniejszych jest Raphael/, p5.js czy PaperJs, które dają nam odpowiednią abstrakcję. Ale i wiele popularnych bibliotek - np. charjs zbudowana jest właśnie w oparciu o canvas.

Jeżeli chodzi o tworzenie gier w canvasie, raczej bym nie pisał tego totalnie od podstaw. Z jednej strony jest to naprawdę porządny trening. I mówię tutaj - porządny!. Praktycznie żadna aplikacja w Reakcje czy innym frameworku nie da wam takiego treningu jak napisanie dynamicznej gry w canvasie. To nie zrobienie pętli po tablicy. Mówimy tutaj o wyliczaniu kątów, obliczaniu fizyki itp. Dodatkowo JavaScript nie jest najwydajniejszym językiem na świecie i dość szybko będziemy musieli się zmierzyć nie tylko z problemami takimi jak obrót rysowanego elementu w kierunku kursora ale i coś bardziej zaawansowanego jak buforowanie klatek, optymalizację obiektów czy odpowiednie podejście do wykonywania obliczeń na wielu elementach na planszy.

Twórcy gier, którzy na co dzień działają w Unity, Godot, Phaser i podobnych środowiskach pewnie wiedzą o czym mówię. A i stare chłopy jak ja, które nie jedne dynamicznie twory kreowali we Flashu i ActionScript wiedzą o jakich problemach do rozwiązania mówię. Jest ciężko, ale i jest duża satysfakcja gdy nam się uda. Polecam.

Czy odradzam więc takie zabawy? Oczywiście, że nie. Ba - polecam bardzo. Dużo osób wkraczających we Frontend skupia się tylko na aplikacjach Javascript. A przecież można tutaj robić wiele, wiele innych rzeczy. Przejrzyj sobie filmy na tym kanale, a zobaczysz, ile zabawy może przynieść rozwiązywanie mniej pospolitych problemów.

W przypadku tworzenia gier warto sięgnąć też po gotowe silniki. Najpopularniejszymi są Game Maker, Construct, Phaser. Ten pierwszy daje możliwość tworzenia gier w Canvasie (ale trzeba zapłacić), a te dwa ostanie działają głównie w oparciu o Canvas. Ale znowu - nawet jeżeli użyjemy gotowych rozwiązań, wcale nie oznacza to, że nie czeka nas kodowanie. Sam bardzo często przeglądam ten blog, gdzie autor dzieli sie jak tworzyć różne gry w Phaser. Kodu i kombinowania tam co nie miara...

A co jeżeli nie umiem i nigdy nie będę umiał działać z Canvasem? Nic straconego. Wystarczy regularnie przeglądać strony takie jak np. codepen.io, gdzie autorzy co chwila wrzucają wiele fajnych eksperymentów właście w Canvas (1, 2, 3, 4, 5, 6, 7, 8 i wiele innych) i po prostu analizować. Temat jest bardzo rozległy. Co ciekawe nawet popularny ostatnio tree.js też działa na canvasie.

Ściągawkę zawierającą zbiór metod elementu canvas możesz ściągnąć tutaj

Odwołanie się do canvasu

No dobrze. Ale od czegoś trzeba zacząć. Małe kroczki aż staniemy się mistrzami.

Aby zacząć rysowanie po naszym canvasie, musimy pobrać jego zawartość. Służy do tego funkcja getContext("2d"). Tryb 2d służy do rysowania 2d. Pozostałe możliwości to webgl, webgl2 i bitmaprender, ale tych nie będę tutaj omawiał (niestety nie miałem z nimi styku).


<canvas width="400" height="200" id="canvas">
</canvas>

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

//...rysujemy

Rysowanie ścieżek

Po pobraniu referencji do płótna zaczynamy nim manipulować. Możemy tutaj wgrywać grafiki (tym zajmiemy się później), ale też jak w każdym programie graficznym możemy po nim rysować kształty. Rysowanie takie przypomina rysowanie piórkiem np. w programie Adobe Photoshop czy ilustartorze. Jak to wygląda w praktyce? Wybieramy piórko, rysujemy niewidzialną linię, a następnie otrzymany kształt obrysowujemy lub wypełniamy.

Tworzenie sciezki

Podobnie jak w powyższej animacji rysowanie ścieżek po płótnie będziemy wykonywać w kilku krokach:

  1. rozpoczynamy rysowanie nowej ścieżki za pomocą metody beginPath()
  2. Używamy metod do rysowania ścieżki - np. lineTo(x, y), moveTo(x, y) itp.
  3. obrysowujemy stroke() lub wypełniamy fill() naszą ścieżkę

ctx.beginPath();
ctx.moveTo(20, 20); //stawiamy piórko w punkcie x: 20 y: 20
ctx.lineTo(30, 40); //zaczynamy rysować niewidzialną linię do x : 30, y: 40
ctx.lineTo(35, 10); //kolejna linia
ctx.lineTo(125, 30); //i kolejna
ctx.stroke(); //po zakończeniu rysowania obrysowujemy linię

Jeżeli chcemy zacząć rysować kolejną ścieżkę, poprzednią obrysowujemy/wypełniamy i zaczynamy rysować kolejny path:


ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(125, 30);
ctx.stroke();

ctx.beginPath();
ctx.moveTo(80, 70);
ctx.lineTo(205, 40);
ctx.stroke();

W kolejnych przykładach będziemy generować sporo losowych liczb z zakresu, dlatego napiszmy pomocą funkcję, która nam takie zwróci:


    const rand = function(min, max) {
        return Math.floor(Math.random()*(max-min+1)+min);
    }
    

Spróbujmy użyć powyższych funkcji w praktyce:


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

ctx.beginPath();
ctx.moveTo(0, canvas.height/2); //zaczniemy rysowanie od x: 0, y: polowaWysokosci
const step = 10; //krok co 10
const howMany = canvas.width / step; //ile kroków

for (let i=1; i<=howMany; i++) {
    const y = canvas.height / 2 + rand(-50, 50);
    ctx.lineTo(i * step, y)
}
ctx.stroke();

Jeżeli dodatkowo chcielibyśmy połączyć początkowy i końcowy punkt ścieżki, możemy skorzystać z funkcji closePath(), która zamyka całą ścieżkę


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

ctx.beginPath();
ctx.moveTo(0, canvas.height / 2);
const step = 10; //krok co 10
const howMany = canvas.width / step;

for (let i=1; i<=howMany; i++) {
    const y = canvas.height / 2 + rand(-50, 50);
    ctx.lineTo(i * step, y)
}

//dokańczam rysowanie ścieżki na dole canvasu
ctx.lineTo(canvas.width, canvas.height);
ctx.lineTo(0, canvas.height);
ctx.closePath();

ctx.fill();

Rysowanie prostokątów

W powyższych przykładach rysowaliśmy niewidzialne ścieżki, a następnie albo je obrysowaliśmy, albo wypełnialiśmy kolorem. Jeżeli chcielibyśmy narysować prostokąt, możemy rysować każdy bok jako oddzielną kreskę:


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

ctx.clearRect(0, 0, canvas.width, canvas.height);

const x = rand(10, canvas.width-100);
const y = rand(10, canvas.height-100);
const size = rand(10, 200);

ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + size, y);
ctx.lineTo(x + size, y + size);
ctx.lineTo(x, y + size);
ctx.closePath();
ctx.stroke();

Zamiast rozpoczynać path, rysować ręcznie każdy bok, i zamykać path, możemy też użyć gotowych funkcji, które wykonają powyższe te za nas.

Funkcje te to:

fillRect(x, y, width, height) rysuje wypełniony prostokąt
strokeRect(x, y, width, height) rysuje obrysowany prostokąt
clearRect(x, y, width, height) czyści określony obszar

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

ctx.clearRect(0, 0, canvas.width, canvas.height); //czyścimy canvas

{
    const x = rand(10, canvas.width-100);
    const y = rand(10, canvas.height-100);
    const size = rand(10, 200);
    ctx.fillRect(x, y, size, size); //wypełniony kwadrat
}

{
    const x = rand(10, canvas.width-100);
    const y = rand(10, canvas.height-100);
    const size = rand(10, 200);
    ctx.strokeRect(x, y, size, size); //obrysowany kwadrat
}

Bardzo ważną dla nas będzie funkcja clearRect(), którą bardzo często będziemy wykorzystywać do czyszczenia całego canvasu:


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

ctx.clearRect(0, 0, canvas.width, canvas.height);

Tekst

Aby wypisać tekst, możemy skorzystać z dwóch funkcji:

fillText("tekst", x, y) która wypisuje wypełniony tekst w pozycji x, y
strokeText("tekst", x, y) która wypisuje obrysowany tekst w pozycji x, y

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

ctx.font = "italic bold 50px Arial";
ctx.fillText("Psy i koty są fajne", 10, 60);

ctx.font = "italic bold 30px Arial";
ctx.strokeText("Czasami koty są fajniejsze", 30, 140);

ctx.font = "normal 40px Arial";
ctx.fillText("a czasami psy", 230, 180);

Dodatkowo możemy ustawić wygląd i pozycję pisanego tekstu za pomocą właściwości:

font opis wyglądu czcionki taki sam jaki stosujemy w CSS
textAlign wyrównanie tekstu w poziomie. Możliwe wartości to: start, end, left, right, center. Domyślną wartością jest start.
textBaseline Określa pionowe wyrównanie tekstu względem danego punktu. Możliwe wartości to: top, hanging, middle, alphabetic, ideographic, bottom. Domyślną jest alphabetic.

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

ctx.textAlign = "start";
ctx.textBaseline = "alphabetic";
ctx.fillText("Psy i koty są fajne", canvas.width/2, canvas.height/2);

Jeżeli byśmy chcieli policzyć szerokość ile zajmuje wypisany tekst, możemy skorzystać z funkcji measureText(), która zwraca dane o tekście pisanym z danym formatowaniem:


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

const str = "Psy i koty są fajne"

ctx.font = "italic bold 40px Arial";
ctx.fillText(str, 100, 100);

const textMetrics  = ctx.measureText(str);

ctx.beginPath();
ctx.moveTo(100, 120);
ctx.lineTo(100 + textMetrics.width, 120);
ctx.stroke();

W momencie pisania tego tekstu za pomocą measureText możemy pobrać tylko szerokość danego tekstu, ale w przyszłości będziemy mieli więcej możliwości.

Rysowanie łuków w ścieżkach

Kolejnym kształtem który możemy dołączyć do naszej ścieżki jest łuk:

arc(x, y, radius, startAngle, endAngle, anticlockwise*)

Atrybuty x i y określają miejsce postawienia igły cyrkla. Parametr radius określa promień. Parametry startAngle i endAngle podajemy w radianach i określają one początkowy i końcowy kąt rysowanego łuku. Ostatni parametr określa czy rysować z kierunkiem wskazówek czy w odwrotnym.

Początkowy i końcowy kąt rysowanego łuku podane są w radianach, dlatego napiszmy funkcję, która przeliczy tradycyjne kąty na radiany:


const angleToRadian = function(angle) {
    return Math.PI/180 * angle;
}

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

ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, 100, 0, angleToRadian(range));
ctx.stroke();

Żeby narysować pełne kółko musimy narysować łuk o kącie 360 i go wypełnić:


ctx.arc(100, 100, 70, 0, angleToRadian(360));
ctx.fill();

Podobną w działaniu jest metoda ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise*), która służy do rysowania elips.

Funkcja ta ma podobne parametry do arc(), jedyną różnicą jest to, że podajemy tutaj 2 promienie zamiast pojedynczego a i dochodzi nam dodatkowy parametr rotation, który oznacza obrócenie elipsy.


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

ctx.beginPath();
const range = 200;
ctx.ellipse(canvas.width/2, canvas.height/2, 100, 200, angleToRadian(30), 0, angleToRadian(range));
ctx.stroke();

Poza powyższymi dwoma mamy też funkcję arcTo(x1, y1, x2, y2, radius)), która rysuje łuk połączony z resztą ścieżki:

Kliknij na canvasie w jakieś miejsce by postawić punkt zakrzywienia. Prawym przyciskiem stawiasz drugi punkt. Dodatkowo pod canvasem input:range zmienia range rysowanego łuku.

Rysowanie zakrzywionych ścieżek

Aby narysować zakrzywione linie skorzystamy z funkcji:

quadraticCurveTo(cpx, cpy, x, y) rysuje kwadratową ścieżkę do punktu x, y. Atrybuty cp1x i cp1y określają położenie punktu kontrolnego wyginającego ścieżkę

const canvas = parentCnt.querySelector("canvas");

ctx.beginPath();
ctx.moveTo(10, 10);
ctx.quadraticCurveTo(40, 200, canvas.width-10, canvas.height-10);
ctx.stroke();

Kliknij na canvasie w jakieś miejsce by postawić punkt zakrzywienia:

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) rysuje ścieżkę Beziera do punktu x, y. Atrybuty cp1x, cp1y, cp2x, cp2y określają położenie punktów kontrolnych wyginających ścieżkę.

const canvas = parentCnt.querySelector("canvas");

ctx.beginPath();
ctx.moveTo(10, 10);
const cp1 = {x : 100, y: canvas.height - 30};
const cp2 = {x : canvas.width-60, y: 60};
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, canvas.width-10, canvas.height-10);
ctx.stroke();

Klikaj na canvasie lewym przyciskiem by postawić pierwszy punkt. Prawym by postawić drugi punkt:

Kolory, przezroczystość

Do zmiany koloru obrysu lub wypełnienia służą własności strokeStyle i fillStyle. Aby zmienić kolor, przypisujemy im jego wartość podaną wartością CSS, składową RGB lub nazwą.


ctx.fillStyle = "#6DCF00";
ctx.fillStyle = "rgb(109,207,0)";
ctx.strokeStyle = "green";

Aby ustawić przezroczystość rysowania, możemy wykorzystać dwa sposoby. Pierwszy z nich to użycie właściwości globalAlpha. Wartość tej własności zostanie zastosowana dla wszystkich następnie rysowanych kształtów na canvasie. Drugi sposób to używanie po prostu kolorów z przezroczystością (rgba, hsla):


ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
ctx.strokeRect(10, 10, 120, 120);

ctx.fillStyle = "hsla(120, 80%, 60%, 0.5)";
ctx.fillRect(10, 10, 120, 120);

Wygląd linii

Wygląd linii ustawiamy wykorzystując metody:

  • lineWidth - określa grubość linii

  • lineCap - określa wygląd zakończenia rysowanej linii. Może przyjąć wartość butt, round lub square.

  • lineJoin - określa sposób łączenia 2 linii. Może przyjąć wartość round, bevel lub miter.

  • miterLimit - określa jak daleko kąt połączenia 2 linii metodą miter może wychodzić.

Dodatkowo możemy zdefiniować linię przerywaną za pomocą metody setLineDash(). Jako parametr przyjmuje ona tablicę 2 liczb. Pierwsza określa długość pojedyńczej linii, a druga odstęp pomiędzy kolejnymi liczbami:


ctx.beginPath();
ctx.setLineDash([2, 5]);
ctx.strokeStyle = "red"
ctx.lineWidth = 1;
ctx.moveTo(10, 20);
ctx.lineTo(canvas.width-10, 20);
ctx.stroke();

ctx.beginPath();
ctx.setLineDash([10, 10]);
ctx.strokeStyle = "dodgerblue"
ctx.lineWidth = 3;
ctx.moveTo(10, 50);
ctx.lineTo(canvas.width-10, 50);
ctx.stroke();

ctx.beginPath();
ctx.setLineDash([]);
ctx.strokeStyle = "blue"
ctx.lineWidth = 6;
ctx.moveTo(10, 80);
ctx.lineTo(canvas.width-10, 80);
ctx.stroke();

Gradienty

Gradienty w Canvasie działają nieco inaczej niż te znane z CSS, a bardziej przypominają te używane np. w programie Adobe Ilustrator.
Tworząc gradient musimy określić jego początkową i końcową pozycję. Następnie rysujemy jakiś kształt, który jeżeli zostanie wypełniony, wybierze sobie odpowiedni kawałek z naszego gradientu.

Do tworzenia gradientów korzystamy z metod:

createLinearGradient(x1, y1, x2, y2) tworzy gradient, który możemy wykorzystać do obrysowani albo wypełnienia (za pomocą powyżej opisanych fillStyle i strokeStyle). Gradient liniowy biegnie z punktu x1, y1 do punktu x2, y2
createRadialGradient(x1, y1, r1, x2, y2, r2) ustawia gradient radialny. Atrybuty x1, y1, r1 - określają położenie i promień punktu początkowego okręgu, natomiast x2, y2, r2 - punktu końcowego okręgu
addColorStop(pozycja, kolor) dodaje nowy kolor do gradientu w pozycji z przedziału 0.0 - 1.0. Możesz dodawać takich punktów do woli. Traktuj to jak dodatkowe suwaki np. w tym generatorze

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

//tworze gradient na cala powierzchnie
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, "blue");
gradient.addColorStop(0.2, "dodgerblue");
gradient.addColorStop(0.5, "springgreen");
gradient.addColorStop(1, "orangered");

ctx.strokeStyle = gradient;

for (let i=0; i<50; i++) {
    ctx.beginPath();
    const x = rand(20, canvas.width - 20)
    const y = rand(20, canvas.height - 20)
    const r = rand(10, 100);
    const lineW = rand(1, 10);
    ctx.arc(x, y, r, 0, angleToRadian(360));
    ctx.lineWidth = lineW;
    ctx.stroke();
}

Jeżeli byśmy chcieli, by gradient zaczynał się wraz z figurą, musimy rozpocząć go wraz z pozycją figury.


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

for (let i=1; i<50; i++) {
    const size = rand(20, 80);
    const x = rand(0, canvas.width - size);
    const y = rand(0, canvas.height - size);

    const gradient = ctx.createLinearGradient(x, y, x+size, y+size);
    gradient.addColorStop(0, "gold");
    gradient.addColorStop(1, "red");

    ctx.fillStyle = gradient;

    ctx.fillRect(x, y, size, size);
    ctx.strokeRect(x, y, size, size);
}

Zapisywanie i odczytywanie stanu

Na zakończenie poznamy jeszcze 2 bardzo przydatne metody. Metoda save() służy do zapisania stanu canvasu - w tym właśnie ustawień odnośnie rysowania.
Metoda restore() jak sama nazwa wskazuje służy do odczytania zapisanego wcześniej stanu.
Metoda save i restore działają na zasadzie stosu. Save() odkłada coś na stos, a restore() pobiera ostatni odłożony zapisany stan.


ctx.save();

...jakieś operacje...

ctx.restore();

Wyobraź sobie, że masz w programie graficznym belkę z narzędziami, wyborem kolorów, możliwymi transformacjami, dodatkowymi narzędziami, które mają swoje funkcje, zmieniają położenie czy rotację canvasu itp. Metoda save() będzie służyć tutaj do zapamiętania stanu wybranych w danym momencie opcji. Restore przywróci wcześniej zapisany stan.

W przypadku canvas zapisywane są następujące rzeczy:

  • transformacje
  • Obszar przycinania za pomocą clip()
  • Ustawienia właściwości:
    globalAlpha
    globalCompositeOperation
    strokeStyle
    textAlign, textBaseline
    lineCap, lineJoin, lineWidth, and miterLimit
    fillStyle
    font
    shadowBlur, shadowColor, shadowOffsetX, and shadowOffsetY

Jak to wygląda w praktyce? Przykładowo rysujemy sobie jakiś obrazek. Ustawiamy sobie kilka właściwości rysowania - np. grubość linii czy kolor.
Żeby nie popsuć obecnych ustawień odkładamy jest na stos za pomocą metody save(). Ustawiamy jakieś nowe parametry, zmieniamy kolor.
Po narysowaniu kształtów chcemy wrócić do początkowych ustawień więc odpalamy restore().


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

ctx.save() //zapisuje początkowy styl by nie musieć wszystkiego cofać do stanu początkowego

ctx.beginPath();
ctx.fillStyle = "red";
ctx.lineWidth = 10;
ctx.setLineDash([4,4]);
ctx.strokeStyle = "#333";
ctx.fillRect(100, 100, 200, 200);

ctx.restore(); //wczytuje zapisany stan

ctx.fillText("Kwadrat", 10, 20)

Cień

Aby do rysowanej figury dodać cień, możemy posłużyć się jedną z 4 metod:

shadowOffsetX, shadowOffsetY pozycja cienia w osi x
shadowBlur rozmycie cienia
shadowColor kolor cienia

ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor = "rgba(0,0,0,0.3)"
ctx.shadowBlur = 5;
ctx.fillRect(20, 20, 200, 200);

ctx.shadowOffsetX = -5;
ctx.shadowOffsetY = 5;
ctx.shadowColor = "rgba(0,0,0,0.2)"
ctx.shadowBlur = 3;
ctx.fillStyle = "dodgerblue";
ctx.font = "bold 40px Arial, sans-serif";
ctx.fillText("Testowy", 300, 100);

Przykład użycia poznanych wiadomości


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

const step = 50;
const howMany = canvas.width / step;

for (let i=1; i<howMany; i++) {
    ctx.save();

    //rysuję pionową przerywaną linię
    ctx.beginPath();
    ctx.moveTo(i * step, 0);
    ctx.lineTo(i * step, canvas.height);
    ctx.lineWidth = 0.5;
    ctx.setLineDash([4,4]);
    ctx.strokeStyle = "#ddd";
    ctx.stroke();

    //zaczynam rysować koralik
    ctx.beginPath();

    //ustawiam styl cienia koralika
    ctx.shadowOffsetX = 5;
    ctx.shadowOffsetY = 5;
    ctx.shadowColor = "rgba(0,0,0,0.3)"
    ctx.shadowBlur = 6;

    //pozycja Y koralika
    const y = rand(20, canvas.height-20);

    //rysuję koralik
    ctx.arc(i * step, y, 15, 0, angleToRadian(360));

    //ustawiam gradient dla koralika
    const gx = i*step - 15;
    const gStartY = y - 15;
    const gEndY = y + 15;
    const gradient = ctx.createLinearGradient(gx, gStartY, gx, gEndY);
    gradient.addColorStop(0, "orange");
    gradient.addColorStop(1, "orangered");

    //wypełniam koralik
    ctx.fillStyle = gradient;
    ctx.fill();
    ctx.stroke();

    //cofam ustawiania rysowania do początku
    ctx.restore();

    ctx.font = "bold 55px Arial, sans-serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "medium";
    ctx.fillStyle = "rgba(0,0,0,0.01)"
    ctx.fillText("Koraliki", canvas.width / 2, canvas.height / 2 );
}

Wycinanie - clip

Jeżeli kiedykolwiek używałeś maski w svg, maski w dowolnym programie graficznym czy nawet strony https://bennettfeely.com/clippy/, będziesz wiedział o czym mówimy.

Metoda clip() sprawia, że aktualnie rysowany path staje się właśnie taką maską, która przycina dany obrazek do jej krawędzi.


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

ctx.fillStyle = "black"
ctx.fillRect(0, 0, canvas.width, canvas.height);

//rysuję trójkąt
ctx.beginPath();
ctx.moveTo(canvas.width/2, 20);
ctx.lineTo(canvas.width-20, canvas.height-20);
ctx.lineTo(20, canvas.height-20);
//robię z niego maskę przycinającą
ctx.clip();

//rysuję coś co będzie przycinane przez maskę
//w tym wypadku generuję losowe liczby
for (let i=0; i<400; i++) {
    const x = rand(0, canvas.width);
    const y = rand(0, canvas.height);
    const size = rand(10, 30);
    const color = `hsla(120, 80%, 60%, ${Math.random()})`;

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

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

Gdy stworzymy maskę za pomocą clip() będzie ona oddziaływać na wszystko co po niej stworzymy.

Aby wyłączyć coś z działania tego narzędzia, musimy ponownie posłużyć się zapisem i przywróceniem stanu:


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

ctx.fillStyle = "black"
ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.save();

ctx.beginPath();
ctx.moveTo(canvas.width/2, 20);
ctx.lineTo(canvas.width-20, canvas.height-20);
ctx.lineTo(20, canvas.height-20);
ctx.clip();

for (let i=0; i<400; i++) {
    ...
}

ctx.restore();

ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `bold 150px Arial, sans-serif`;
ctx.fillStyle = "hsla(120, 80%, 60%, 0.2)";
ctx.fillText("MATRIX", canvas.width/2, canvas.height/2);

Bez save i restore:

Z save i restore:

Kompozycja

Canvas udostępnia nam 2 właściwości, które określają sposób rysowania po canvasie.

globalAlpha Ustawia ogólną przezroczystość nowo rysowanych figur. Definiujemy ją z przedziału 0-1. Zamiast tego możemy spokojnie używać kolorów rgba lub hsla
globalCompositeOperation Określa jak mają być nanoszone nowe kształty na płótno - sprawdź najlepiej na poniższym przykładzie

Działanie globalCompositeOperation można przyrównać do operacji boolowskich znanych z programów graficznych (np. pathfinder z AdobeIllustrator lub modyfikator Boolean w Blender3d).

pathfinder

Wartości dla globalCompositeOperation to:

copy Pokazuje rysowaną figurę oraz usuwa wszystko co się z nią zazębiało
destination-atop Rysuje figurę oraz tą część canvasa, która zazębiała się z rysowaną figurą
destination-in Pozostawia na canvasie tą część figur, które zazębiały się z rysowaną figurą
destination-out Pozostawia na canvasie te części figur, które nie zazębiały się z rysowaną figurą.
destination-over Rysuje figurę pod figurami z canvasu
lighter W miejscach zazębiania się sumuje kolory rysowanej figury i canvasu.
source-atop Pokazuje część canvasa która kolidowała z rysowaną figurą nad nią
source-in Rysuje figurę tylko w miejscach gdzie zazębiała się z figurami narysowanymi na canvasie.
source-out Pokazuje rysowaną figurę w miejscach, gdzie canvas był transparenty. W innych miejscach pokazuje przezroczystość.
source-over Domyślna wartość. Pokazuje rysowaną figurę w miejscu rysowania. Źródło zostaje zachowane.

ctx.fillStyle = "black"
ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.clearRect(0, 0, canvas.width, canvas.height);

//4 czarne kwadraty
ctx.fillStyle = "#222"
ctx.fillRect(100, 50, 90, 90);
ctx.fillRect(200, 50, 90, 90);
ctx.fillRect(100, 150, 90, 90);
ctx.fillRect(200, 150, 90, 90);

ctx.globalCompositeOperation = "source-over";

//pomidorowy przyjaciel
ctx.fillStyle = "tomato";
ctx.fillRect(80, 80, 100, 100);

Transformacje

Dla zawartości canvas możemy używać kilku podstawowych funkcji transformacji.

Canvas jako powierzchnia to obszar, na który możemy nałożyć "macierz transformacji", która następnie go zmienia. Co to dla nas oznacza?

Jeżeli przykładowo nałożę na canvas efekt rotate(), rysowane następnie figury będą obrócone - mimo tego, że rysuję je normalnie.

Mamy kilka funkcji, które możemy tutaj wykorzystywać:

setTransform(a, b, c, d, e, f) Ustawia matrix transformacji. Kolejne parametry to:
a - pozioma skala, b - pionowe pochylenie (skew), c - poziome pochylenie, d - pionowa skala, e - poziome przemieszczenie, f - pionowe przemieszczenie
rotate() obraca płótno, co oznacza, że rysowane elementy będą pojawiać się w innej pozycji. Płótno obracane jest względem punkty 0,0
translate(x, y) obraca płótno, co oznacza, że rysowane figury będą pojawiać się obrócone.
scale(x, y) skaluje płótno, co oznacza, że rysowane figury będą pojawiać się przeskalowane. Płótno skalowane jest względem punktu 0,0

//czyszczę transformacje
ctx.setTransform(1, 0, 0, 1, 0, 0);

//obracam canvas
ctx.save();
ctx.rotate(angleToRadian(15));
ctx.strokeRect(20, 20, 200, 200);
ctx.fillRect(230, 20, 200, 200);
ctx.restore();
0 100

Jak widzisz, transformacja rotate jest wykonywana względem punktu 0, 0. Niestety nie mamy tutaj znanej z css właściwości transform-origin, która w css zmienia taki punkt.
Żeby obracać dany element względem konkretnego punktu, musimy skorzystać z dodatkowych obliczeń.

Kroki, które musimy wykonać są następujące:

  1. Przesuwamy cały canvas (a więc i punkt transformacji) za pomocą translate() do punktu, w którym mamy obrócić dany element (czyli środka naszej przyszłej figury)
  2. Obracamy płótno za pomocą rotate()
  3. Rysujemy obiekt tak, by jego środek znajdował się w miejscu obrotu
transformacje animacja

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

const w = 100;
const h = 100;

ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0); //czyszczę transformacje
ctx.beginPath();

//powiedzmy że chce obrócić kwadrat w pozycji 250, 100
ctx.translate(250 + w/2, 100 + h/2); //przesuwamy origin do środka obrotu
ctx.rotate(angleToRadian(range));
ctx.strokeRect(-w/2, -h/2, w, h); //rysuje tak, by środek był w punkcie 0,0
ctx.restore();
0 360

W przypadku tekstów żeby narysować tekst z środkiem w punkcie obrotu możemy posłużyć się właściwościami wyrównującymi:


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

//czyszczę transformacje
ctx.setTransform(1, 0, 0, 1, 0, 0);

ctx.save();
//powiedzmy ze chcemy obracać tekst w punkcie 350, 200
ctx.translate(350, 200);
ctx.rotate(angleToRadian(range));
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `bold 60px Arial, sans-serif`;
ctx.fillText("Hello word", 0, 0)
ctx.restore();
0 360

Podobnie do rotate() będziemy działać w przypadku scale, która także działa względem punktu 0,0:


//skalujemy kwadrat w pozycji 150x50
const w = 100;
const h = 100;
ctx.save(); //zapisuje stan przed transformacjami
ctx.beginPath();
ctx.translate(150 + w/2, 50 + h/2);
ctx.scale(-1, 1);
ctx.strokeRect(-w/2, -h/2, w, h);
ctx.restore();

//obracamy tekst w pozycji 350x200
ctx.save();
ctx.translate(350, 200);
ctx.scale(-1, 1);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `bold 60px Arial, sans-serif`;
ctx.fillText("Hello word", 0, 0);
ctx.restore();
-1 1

Oczywiście skale i obracanie możemy łączyć razem:


ctx.save();
ctx.beginPath();
ctx.translate(150 + w/2, 50 + h/2);
ctx.scale(scale, 1);
ctx.rotate(angleToRadian(rotate));
ctx.strokeRect(-w/2, -h/2, w, h);
ctx.restore();

//obracamy tekst w pozycji 350x200
ctx.save();
ctx.translate(350, 200);
ctx.scale(scale, 1);
ctx.rotate(angleToRadian(rotate));
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `bold 60px Arial, sans-serif`;
ctx.fillText("Hello word", 0, 0);
ctx.restore();
Scale: -1 1
Rotate: 0 360

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