Canvas

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 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. Te bardziej popularne to pixijsarrow-up-right czy p5.jsarrow-up-right. Ale i wiele popularnych bibliotek - np. charjsarrow-up-right zbudowana jest właśnie w oparciu o canvas.

Jeżeli chodzi o tworzenie gier w canvasie, raczej bym nie pisał ich totalnie od podstaw. Z jednej strony jest to naprawdę porządny trening. I mówię tutaj - porządny!. Praktycznie żadna aplikacja w React, 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.

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 kanalearrow-up-right, 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 Makerarrow-up-right, Constructarrow-up-right czy Phaserarrow-up-right. 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 rozwiązywanie złożonych problemów. Sam bardzo często przeglądam ten blogarrow-up-right, gdzie autor dzieli sie jak tworzyć różne gry w Phaser. Kodu i ciekawych pomysłów 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.ioarrow-up-right, gdzie autorzy co chwila wrzucają wiele fajnych eksperymentów właście w Canvas (1arrow-up-right, 2arrow-up-right, 3arrow-up-right, 4arrow-up-right, 5arrow-up-right, 6arrow-up-right, 7arrow-up-right, 8arrow-up-right i wiele innych) i po prostu analizować. Temat jest bardzo rozległy. Co ciekawe nawet popularny ostatnio three.jsarrow-up-right też działa na canvasie.

Ściągawka z metod

Ściągawkę zawierającą zbiór metod canvas możesz ściągnąć tutajarrow-up-right

Odwołanie się do canvasu

No dobrze. Ale od czegoś trzeba zacząć. Małymi kroczkami, aż staniemy się mistrzami.

Aby zacząć rysowanie po naszym płótnie, 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ł.

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.

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ę

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

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

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

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ę

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ę:

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

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

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

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.

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:

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

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:

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

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.

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ę

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

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

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

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ść pojedynczej linii, a druga odstęp pomiędzy kolejnymi liczbami:

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 generatorzearrow-up-right

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

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.

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

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

Przykład użycia poznanych wiadomości

Wycinanie - clip

Jeżeli kiedykolwiek używałeś maski w svg, maski w dowolnym programie graficznym czy nawet strony https://bennettfeely.com/clippy/arrow-up-right, 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.

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:

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

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.

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)

przesuwa płótno (podobnie do odpowiednika z css)

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

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

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:

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

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

Last updated