Studium przypadku: Poznawanie świata kanw HTML5

Derek Detweiler
Derek Detweiler

Wstęp

Ostatniej wiosny (2010) zainteresowałem się szybko rosnącym wsparciem dla HTML5 i podobnych technologii. W tamtym czasie z przyjaciółką rzucaliśmy sobie wyzwanie w dwutygodniowych konkursach na tworzenie gier, aby doskonalić nasze umiejętności w zakresie programowania i tworzenia gier oraz wcielać w życie swoje pomysły na gry. Zacząłem więc dodawać elementy HTML5 do swoich zgłoszeń konkursowych, aby lepiej zrozumieć, jak one działają i robić rzeczy, które na podstawie wcześniejszych specyfikacji HTML były prawie niemożliwe.

Spośród wielu nowych funkcji HTML5 rosnąca obsługa tagu Canvas dały mi świetną okazję do implementowania interaktywnej sztuki za pomocą JavaScriptu, co skłoniło mnie do wdrożenia gry logicznej o nazwie Entanglement. Udało mi się już utworzyć prototyp z tyłu Osadników z katańskich kafelków, więc wymagamy wykonania 3 elementów niezbędnego do stworzenia sześciokątnego kafelka do zabawy w internecie: rysowania sześciokąta, rysowania ścieżek i obracania kafelka. Poniżej szczegółowo opisujemy, jak udało mi się osiągnąć każdy z nich w obecnej formie.

Rysowanie sześciokąta

W oryginalnej wersji gry Entanglement, używając kilku metod rysowania na płótnie, udało mi się narysować sześciokąt, ale obecna wersja gry rysuje tekstury przycięte z arkusza sprite, używając drawImage().

Arkusz sprite z kafelkami
Arkusz sprite

Obrazy zostały połączone w jeden plik, więc jest to tylko jedno żądanie do serwera, a nie w tym przypadku – 10. Aby narysować wybrany sześciokąt do płótna, musimy najpierw zebrać razem nasze narzędzia: płótno, kontekst i obraz.

Aby utworzyć odbitkę na płótnie, potrzebujemy tylko tagu canvas w dokumencie HTML, jak w tym przykładzie:

<canvas id="myCanvas"></canvas>

Podaję mu identyfikator, który pozwoli nam pobrać go do naszego skryptu:

var cvs = document.getElementById('myCanvas');

Po drugie, potrzebujemy kontekstu 2D dla obszaru roboczego, aby zacząć rysować:

var ctx = cvs.getContext('2d');

Na koniec potrzebujemy obrazu. Jeśli plik o nazwie „tiles.png” znajduje się w tym samym folderze co nasza strona internetowa, możemy go pobrać w ten sposób:

var img = new Image();
img.src = 'tiles.png';

Teraz, gdy mamy już trzy komponenty, możemy użyć funkcji ctx.drawImage(), aby narysować pojedynczy sześciokąt z arkusza sprite do obszaru roboczego:

ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

W tym przypadku korzystamy z czwartego sześciokąta od lewej w górnym rzędzie. Narysujemy go też w obszarze roboczym w lewym górnym rogu, zachowując ten sam rozmiar co oryginał. Przy założeniu, że sześciokąty mają 400 pikseli szerokości i 346 pikseli wysokości, w sumie będzie to wyglądać mniej więcej tak:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';
var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

Udało nam się skopiować część obrazu do obszaru roboczego. W efekcie:

Płytki sześciokątne
Kafelki w kształcie sześciokąta

Rysowanie ścieżek

Po narysowaniu sześciokąta na płótno narysujemy kilka linii. Najpierw przyjrzymy się geometrii płytki sześciokątnej. Chcemy dodać po 2 linie na bok, każdy z końcówką 1/4 od końca wzdłuż każdej krawędzi i 1/2 krawędzi od siebie, na przykład:

Punkty końcowe linii na kafelku sześciokątnym
Punkty końcowe linii na kafelku sześciokątnym

Musimy też mieć ładną krzywą. Korzystając z metody prób i błędów, odkryłem, że jeśli utworzę prostą prostopadłą do krawędzi w każdym punkcie końcowym, przecięcie każdej pary punktów końcowych wokół danego kąta sześciokąta tworzy ładny punkt kontrolny beziera dla danych punktów końcowych:

Punkty kontrolne na kafelku sześciokątnym
Punkty kontrolne na kafelku sześciokątnym

Teraz mapujemy zarówno punkty końcowe, jak i punkty kontrolne na płaszczyznę kartezjańską, odpowiadającą obrazowi na płótnie. Teraz możemy wrócić do kodu. Dla uproszczenia zaczniemy od jednego wiersza. Zaczniemy od narysowania ścieżki od lewego górnego rogu do prawego dolnego punktu końcowego. Poprzedni obraz sześciokątny o wymiarach 400 × 346 oznacza, że najwyższy punkt końcowy ma 150 pikseli w poprzek i 0 pikseli w dół, czyli 150 x 0. Jej punkt kontrolny będzie mieć postać (150, 86). Punkt końcowy na dolnej krawędzi to (250, 346) z punktem kontrolnym (250, 260):

Współrzędne pierwszej krzywej Béziera
Współrzędne pierwszej krzywej Béziera

Mając podane współrzędne, możemy zacząć rysować. Zaczniemy od nowa z funkcją ctx.beginPath(), a następnie przejdziemy do pierwszego punktu końcowego przy użyciu polecenia:

ctx.moveTo(pointX1,pointY1);

Możemy wtedy narysować samą linię za pomocą funkcji ctx.bezierCurveTo() w następujący sposób:

ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);

Linia ma ładne obramowanie, więc za każdym razem będziemy kreślić tę ścieżkę dwukrotnie, używając innej szerokości i koloru. Kolor jest ustawiany za pomocą właściwości ctx.slideStyle, a szerokość – za pomocą parametru ctx.lineWidth. W sumie pierwszy wiersz będzie wyglądał tak:

var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.beginPath();
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

Mamy teraz sześciokątny kafelek z pierwszą linią wijącą się przez:

Pojedyncza linia na kafelku sześciokątnym
Pojedyncza linia na kafelku sześciokątnym

Gdy wpiszesz współrzędne pozostałych 10 punktów końcowych oraz odpowiadające im punkty kontrolne krzywej Beziera, możemy powtórzyć powyższe kroki i utworzyć kafelek podobny do tego:

Ukończono sześciokątny kafelek.
Ukończony kafelek sześciokątny

Obracanie obszaru roboczego

Kiedy mamy już kafelek, chcemy móc go obracać, aby wybrać w grze różne ścieżki. Aby to zrobić, używając kanwy, używamy ctx.translate() i ctx.rotate(). Chcemy, aby kafelek obracał się wokół środka, więc najpierw przesuń punkt odniesienia obszaru roboczego na środek sześciokątnego kafelka. W tym celu stosujemy:

ctx.translate(originX, originY);

Gdzie originX to połowa szerokości kafelka sześciokątnego, a originY to połowa wysokości, co daje:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);

Teraz możemy obrócić kafelek z naszym nowym punktem środkowym. Sześciokąt ma sześć ścian, więc chcemy go obrócić przez pomnożenie wyniku Math.PI przez 3. Zachowamy prostotę i zrobimy jedno obrót w prawo za pomocą:

ctx.rotate(Math.PI / 3);

Ponieważ jednak nasz sześciokąt i linie korzystają ze starych współrzędnych (0,0) jako punktu początkowego, gdy skończymy obracać, będziemy chcieli je przetłumaczyć przed rysowaniem. W sumie mamy teraz:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

Jeśli umieścisz powyższe tłumaczenie i obrót przed naszym kodem renderowania, obrócony kafelek będzie renderowany:

Obrócony sześciokątny kafelek
Obrócony sześciokątny kafelek

Podsumowanie

Powyżej wymieniłem kilka funkcji dostępnych w HTML5 tagu canvas, w tym renderowanie obrazów, rysowanie krzywych Béziera i obracanie obszaru roboczego. Korzystanie z tagu canvas HTML5 i dostępnych w nim narzędzi do rysowania w języku JavaScript na potrzeby gry Entanglement okazało się przyjemnym doświadczeniem. Nie mogę się doczekać kolejnych nowych aplikacji i gier, które inni będą tworzyć z użyciem tej otwartej i nowej technologii.

Dokumentacja kodu

Wszystkie podane wyżej przykłady kodu zebraliśmy poniżej, aby pełniej pełnić funkcję informacyjną:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

ctx.beginPath();
var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 250;
pointY1 = 0;
controlX1 = 250;
controlY1 = 86;
controlX2 = 150;
controlY2 = 86;
pointX2 = 75;
pointY2 = 43;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 150;
pointY1 = 346;
controlX1 = 150;
controlY1 = 260;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 43;
controlX1 = 250;
controlY1 = 86;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 130;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 25;
pointY1 = 130;
controlX1 = 100;
controlY1 = 173;
controlX2 = 100;
controlY2 = 173;
pointX2 = 25;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 303;
controlX1 = 250;
controlY1 = 260;
controlX2 = 150;
controlY2 = 260;
pointX2 = 75;
pointY2 = 303;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();