Spis treści:

Kategoria:HTMLJavaScriptCanvas


Rysowanie myszką po canvas w HTML5

Sam sobie narysuję - myszką!

To już trzeci wpis z serii canvas + HTML5. Pokazałem już między innymi jak narysować prosty i lekki wykres (Prosty wykres w HTML5) oraz jak obsłużyć zdarzenia kliknięcia (Klikalna plansza w HTML5). Tym razem nie będę ograniczał użytkowników do prostych czynności i pozwolę im malować co tylko zechcą. Będzie ich ograniczała tylko wyobraźnia, chęci i umiejętności. Będzie to taki czarno-biały odpowiednik Painta.

Tworząc tę prostą aplikację w HTML5 i JavaScript pokażę kilka ważnych elementów, których będę używał w kolejnych wpisach. Oprócz sposobu obsługi myszy i rysowania będą to także elementy programowania obiektowego. Przy prostych skryptach nie ma znaczenia, gdzie znajduje sie kod i jak wykonywane są operacje. Gdy programy będą się komplikowały, lepiej trzymać porządek i umieszczać powiązane ze sobą zmienne w jednym miejscu - w obiekcie. Takie pseudoobiektowe struktury znacznie ułatwiają późniejsze czytanie kodu i nanoszenie poprawek. Struktury nazwałem pseudoobiektowymi, bo jakoś ciężko mi przychodzi wrzucenie JavaScript do jednego obiektowego worka z językami Java, C++ czy C#. Takie luźne rozważania będą się co jakiś czas pojawiać, więc nie będę ich teraz przedłużał. Przejdźmy do implementacji.

Strona HTML z kodem JavaScript

Przyjrzyjmy się najpierw przykładowej implementacji internetowego Painta, a następnie przejdźmy do wyjaśnień:

<html>
<head><title>Rysowanie po canvas w HTML5</title></head>
<body>
<h2>Rysowanie myszką po canvas w HTML5</h2>
<canvas style="background:#F0F0F0" id="paint" width="320" height="320" />
<script type="text/javascript">

var cName = "paint";
var canvas = document.getElementById(cName);
var ctx = canvas.getContext("2d");
//Obszar kontrolki będzie wyliczony po załadowaniu strony
var controlBounds;
//Wszystkie zdarzenia będą dla wygody umieszczone wewnątrz obiektu mouse
//W obiekcie mouse będą też wykorzystywane współrzędne X i Y
var mouse = new Object();

//Kontrolka posiada współrzędne X, Y, szerokość i wysokość
function Control(c){
  var e = document.getElementById(c);
  //Pobierz współrzędne elementu canvas
  var windowOffset = (function(){
    var node = e;
    var offset = {X:0, Y:0};
    do {
      offset.X += node.offsetLeft;
      offset.Y += node.offsetTop;
      node = node.offsetParent;
    } while (node);
    return offset;
  })();
  //ustaw pola obiektu
  this.X = windowOffset.X;
  this.Y = windowOffset.Y;
  this.Width = e.width;
  this.Height = e.height;
}
//Uzupełniamy kontrolkę o metodę Contains
//Do sprawdzenia, czy kursor znajduje sie wewnątrz kontrolki
Control.prototype.Contains = function(x,y){
  return x > this.X && y > this.Y &&
    x < this.X + this.Width && y < this.Y + this.Height;
}

//Zdarzenie wciśnięcia przycisku myszy
mouse.Down = function(e){
  ExtendFirefoxEvent(e);
  mouse.X = e.offsetX;
  mouse.Y = e.offsetY;
  canvas.addEventListener("mousemove", mouse.Move, false);
  window.addEventListener("mousemove", mouse.WindowMove, false);
};

//Zdarzenie zwolnienia przycisku myszy,
//Nasłuchiwane na poziomie okna
mouse.Release = function(){
  canvas.removeEventListener("mousemove", mouse.Move, false);
  window.removeEventListener("mousemove", mouse.WindowMove, false);
};

//Zdarzenie poruszania myszą, aktywne tylko wtedy,
//gdy poruszamy się wewnątrz elementu canvas
mouse.Move = function(e){
  ExtendFirefoxEvent(e);
  ctx.moveTo(mouse.X, mouse.Y);
  mouse.X = e.offsetX;
  mouse.Y = e.offsetY;
  ctx.lineTo(mouse.X, mouse.Y);
  ctx.stroke();
};

//Zdarzenie poruszania się po oknie przeglądarki,
//Określamy moment opuszczenia elementu canvas
mouse.WindowMove = function(e){
  if (!controlBounds.Contains(e.pageX, e.pageY))
    mouse.Release();
}

//Zaczynamy zabawę: nasłuchujemy zdarzeń przycisku myszy
canvas.addEventListener("mousedown", mouse.Down, false);
window.addEventListener("mouseup", mouse.Release, false);
//Dopiero po załadowaniu strony elementy będą poukładane
window.addEventListener("load", function(){controlBounds = new Control(cName);}, false);

function ExtendFirefoxEvent(e){
  //Przeglądarka firefox nie posiada atrybutu offsetX
  //Współrzędne pobierane są nieco inaczej
  if (!e.hasOwnProperty("offsetX")){
    e.offsetX = e.layerX - e.currentTarget.offsetLeft;
    e.offsetY = e.layerY - e.currentTarget.offsetTop;
  }
};
</script>
</body>
</html>

Zaprezentowana powyżej strona pozwala uzyskać taki oto efekt. Można śmiało rysować, a jak braknie miejsca, wystarczy odświeżyć stronę. Mógłbym oczywiście dorobić odpowiednią funkcję, ale pozostawiam to do samodzielnego opracowania przez czytelników.

Rys 1. Płótno do rysowania w HTML5 i JavaScript.

Kod wykorzystany do osiągnięcia takiego efektu został pokomentowany, ale wyjaśnijmy sobie krok po kroku poszczególne elementy.

Jak się bawić myszką?

Zabawę z myszką można od razu sprowadzić do odpowiednich zdarzeń. Cała zabawa, przynajmniej w pokazanym przykładzie, rozpoczyna się od wciśnięcia klawisza myszy (zdarzenie mousedown). Uruchamia ono funkcję mouse.Down, która:

  • zapamiętuje współrzędne kliknięcia,
  • zaczyna wyłapywać zdarzenia poruszania myszy dla elementu canvas (mousemove) i przetwarzać je w funkcji mouse.Move,
  • zaczyna wyłapywać zdarzenia poruszania myszy dla całego okna (mousemove) i przetwarzać je w funkcji mouse.WindowMove.

Dlaczego aż dwa zdarzenia? Nie ma takiej potrzeby, ale chciałem pokazać przy okazji jedno z niebezpieczeństw. Otóż istnieje możliwość wyjechania kursorem myszy poza obszar rysowania, gdzie rysowanie nie ma sensu. Łatwo się domyślić, że element canvas nie będzie wtedy otrzymywał żadnych zdarzeń. Akurat w przypadku zdarzenia mousemove i powyższego skryptu nie ma wielkiego ryzyka, o tyle w przypadku przechwytywania zdarzeń naciśnięcia i zwolnienia przycisku myszy można to drugie zdarzenie zgubić. Wystarczy wcisnąć przycisk, wyjechać poza obszar elementu i przycisk zwolnić. W związku z takim ryzykiem zdarzenie zwolnienia przycisku również wyłapywane jest na poziomie okna, a nie na poziomie elementu canvas. Zwolnienie przycisku wywołuje funkcję mouse.Release, która odpina funkcje nasłuchujące zdarzeń. Nie ma potrzeby obciążać przeglądarki. Ta sama funkcja mouse.Release wywoływana jest w jeszcze jednym miejscu - w metodzie nasłuchującej zdarzeń związanych z poruszaniem się myszy dla całego okna. Uznałem, że w przypadku wyjechania kursorem poza obszar elementu canvas rysowanie powinno być przerwane. Takie postępowanie jest też rozsądne w przypadku przesuwania elementów myszką - przesunięcie ich poza obszar doprowadza do tego, że nie da się ich narysować. Nie są widoczne, a zajmują miejsce, pamięć, muszą być w jakiś sposób przetwarzane - o ile oczywiście nie jest to przewidziany sposób na usuwanie elementów. Implementacja prostej funkcji przeciągnij i upuść (drag & drop) nie jest tak odległa jak mogłoby się wydawać. Pokażę ją wkrótce, o ile czas na to pozwoli. Podpowiem, że przyda się funkcja .Contains. Niecierpliwi mogą już przystąpić do implementacji, a cierpliwi lub zagubieni mogą poczekać na jeden z kolejnych wpisów.

Reasumując: rysowanie zaczynamy w momencie naciśnięcia przycisku myszy, a kończymy z chwilą zwolnienia przycisku myszy lub opuszczenia obszaru rysowania. Samo rysowanie to już tylko łączenie punktów reprezentujących kolejne współrzędne kursora myszy.

W pokazanym fragmencie warto zwrócić uwagę na przechowywanie wszystkich funkcji obsługi zdarzeń - umieszczone są wewnątrz obiektu mouse, oraz sposób tworzenia obiektu na bazie funkcji, który przechowuje obszar elementu canvas, tj. współrzędne X i Y oraz wysokość i szerokość. Warto też zwrócić uwagę na sposób dodania metody do obiektu Control. W ten prosty sposób wszystkie obiekty Control, a nie tylko jeden, jak w przypadku mouse, otrzymują dodatkową metodę. Mam już plan na kilka kolejnych wpisów z tej tematyki, więc pokazane tu elementy będą z pewnością wykorzystywane. Będzie więc możliwość utrwalenia sobie tych konstrukcji.

Współrzędne i Firefox

Niezgodności pomiędzy przeglądarkami są tak stare jak same przeglądarki. Przyznam, że specyfikacja nie definiuje atrybutu offsetX, ale jest on wspierany przez prawie wszystkie przeglądarki. Standard definiuje tylko współrzędne względem całej strony oraz współrzędne względem bieżącego widoku. Uznałem, że łatwiej uzupełnić atrybuty offsetX i offsetY w przeglądarkach, które tego atrybutu nie mają, niż korzystać z mało wygodnych atrybutów standardowych. Te atrybuty standardowe sprowadzają bowiem kolejne problemy. Dziękuję przy okazji za komentarze, które zwróciły uwagę na ten problem.

Kategoria:HTMLJavaScriptCanvas

, 2013-12-20

Komentarze:

nie_działa_na_FF (2014-01-25 20:05:33)
Dlaczego skrypt nie działa na FF? Przecież FF obsługuje Canvas.
mara (2014-01-27 14:27:22)
No właśnie, w czym jest problem, że na FF nie działa?
PD (2014-01-27 17:52:22)
Przykład został nieznacznie zmodyfikowany - powinien już działać w przeglądarkach z rodziny Firefox.
mara (2014-01-27 20:02:53)
Tak, już działa :)
Zielny (2016-02-23 09:04:57)
Dlaczego, gdy przyciskiem próbuję wyczyścić pole (clearRect) to faktycznie czyści, ale gdy kliknę w canvas po raz kolejny pojawiają mi się poprzednie linie ?
PD (2016-03-21 17:59:52)
Nie wiem jak dokładnie wygląda przerobiony kod, ale obstawiam brak instrukcji ctx.beginPath() i ctx.closePath(). Pozwalają one na oddzielenie poszczególnych figur od siebie i traktowanie ich niezależnie.
Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?