Spis treści:

Kategoria:HTMLJavaScriptCanvas


Drag & Drop & Canvas - jak poruszyć obiekty

Interaktywność elementów canvas

- Dlaczego ciągle chodzisz w jednych gaciach?
- Bo w dwóch jest mi za gorąco...
Samo rysowanie na powierzchni elementu canvas to mało. Chcemy więcej! Chcemy puruszyć światem... albo chociaż jakimś innym obiektem. Do świata zaraz dojdziemy. Tym razem pokażę, jak narysowane figury można przestawiać korzystając z tego, co już widzieliśmy. To połączenie obsługi myszki (Rysowanie myszką po canvas w HTML5, Klikalna plansza w HTML5) i wykrywania obiektów (zob. Wykrywanie obiektów w elemencie canvas w HTML5) z kilkoma modyfikacjami. Takie rozwiązanie otwiera nam nowe przestrzenie, liczne przestrzenie na jedno skinienie. To, skądinąd bardzo modne słowo, interaktywność. Przejdźmy zatem do konkretów.

Poruszajmy kółeczkiem

Przykład będzie prosty, ale dla osób posiadających odpowiednią wyobraźnię może być jednym z ważniejszych. Na powierzchni elementu canvas narysuję kółeczko, które użytkownik będzie mógł przestawiać. Przesuwanie będzie reprezentowało operację - znów modne i popularne słowo - przeciągnij i upuść (ang. Drag & Drop). Wszystko będzie się odbywało w ramach obszaru rysowania. Przyjrzyjmy się zatem końcowej aplikacji:

Rys 1. Przykład obiektu graficznego, którym możemy poruszać - Drag & Drop.

Zachęcam do zapoznania się z możliwościami aplikacji. Warto także zapoznać się z kilkoma założeniami:

  • Po najechaniu na kółeczko kursor myszy zmienia się, samo kółeczko też zostaje podświetlone.
  • Kółeczko można przestawić wciskając przycisk myszy i przeciągając w wybrane miejsce.
  • Jeżeli myszka wyjedzie poza obszar elementu canvas, operacja przeciągania zostaje przerwana.

Wszystko to zostało zaimplementowane i pokazane poniżej w postaci gotowego kodu.

Kod aplikacji Drag & Drop & Canvas

Przyjrzyjmy się dokładnie zaprezentowanemu listingowi. Wiele konstrukcji powinno być już znanych z wcześniejszych, prostszych przykładów.

<html>
<head><title>Drag & Drop & Canvas - jak poruszyć obiekty</title></head>
<body>
<h2>Drag & Drop w obszarze elementu canvas</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");
//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();
var controlBounds;
var circle;

//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.Color = e.style.backgroundColor;
  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;
}
Control.prototype.Draw = function(){
  ctx.fillStyle = this.Color;
  ctx.fillRect(0,0,this.Width,this.Height);
}

//Sekcja obsługi koła
function Circle(x,y,r){
  this.X = x;
  this.Y = y;
  this.R = r;
  this.IsHovered = false;
}
//Funkcja definiująca relację zawierania
Circle.prototype.Contains = function(x,y){
  this.IsHovered = Math.sqrt(Math.pow(x - this.X, 2) + Math.pow(y - this.Y, 2)) < this.R;
  return this.IsHovered;
}
//Funkcja rysująca obiekt
Circle.prototype.Draw = function(){
  ctx.beginPath();
  //Wskazany obiekt zmienia zabarwienie
  if (this.IsHovered)
    ctx.fillStyle = "rgb(0,0,250)";
  else
    ctx.fillStyle = "rgb(0,0,100)";
  ctx.arc(this.X,this.Y,this.R,0,2*Math.PI);
  ctx.fill();
}

//Zdarzenie wciśnięcia przycisku myszy
mouse.Down = function(e){
  ExtendFirefoxEvent(e);
  mouse.X = e.offsetX;
  mouse.Y = e.offsetY;
  if (circle.Contains(mouse.X, mouse.Y))
    mouse.IsDragging = true;
};

//Zdarzenie zwolnienia przycisku myszy,
//Nasłuchiwane na poziomie okna
mouse.Release = function(e){
  mouse.IsDragging = false;
};

//Zdarzenie poruszania myszą, aktywne tylko wtedy,
//gdy poruszamy się wewnątrz elementu canvas
mouse.Move = function(e){
  ExtendFirefoxEvent(e);
  if (mouse.IsDragging){
    //Oblicz przesunięcie względem poprzedniej pozycji
    dx = e.offsetX - mouse.X;
    dy = e.offsetY - mouse.Y;
    //Przesuń kółeczko
    circle.X = circle.X + dx;
    circle.Y = circle.Y + dy;
    //Zamaluj obszar i narysuj kółeczko w nowym miejscu
    controlBounds.Draw();
    circle.Draw();
  }
  mouse.X = e.offsetX;
  mouse.Y = e.offsetY;
  //Zmień kursor myszy po najechaniu na obiekt
  //Zmiana następuje tylko wtedy, gdy poprzedni stan kursora był inny
  if (circle.Contains(mouse.X, mouse.Y)){
    if (canvas.style.cursor != "pointer"){
      canvas.style.cursor = "pointer";
      circle.Draw();
    }
  }
  else
    if (canvas.style.cursor != "auto"){
      canvas.style.cursor = "auto";
      circle.Draw();
    }
};

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

//Nasłuchujemy kilku zdarzeń myszy
canvas.addEventListener("mousemove", mouse.Move, false);
canvas.addEventListener("mousedown", mouse.Down, false);
window.addEventListener("mouseup", mouse.Release, false);
window.addEventListener("mousemove", mouse.WindowMove, false);
//Dopiero po załadowaniu całej strony elementy będą poukładane
//Tworzymy nasze kółeczko na środku i rysujemy je
window.addEventListener("load", function(){
  controlBounds = new Control(cName);
  circle = new Circle(controlBounds.Width/2, controlBounds.Height/2, 30);
  circle.Draw();
  }, 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>

W kodzie jest trochę komentarzy, więc zrozumienie będzie łatwiejsze. Pomimo tego, jak zwykle, postaram się omówić ważniejsze fragmenty.

Analiza kodu JavaScript

Parę słów wyjaśnienia. Aplikacja korzysta z dwóch kluczowych obiektów, które pozwalają zrealizować nieskomplikowaną logikę. Pierwszy z nich to obiekt kontrolki (Control). Obiekt reprezentuje element canvas udostępniając pobrane wcześniej atrybuty tła (Color), szerokości i wysokości (Width i Height) oraz współrzędne X i Y. Oprócz tego dodałem dwie metody:

  • Contains - określa, czy punkt o wskazanych współrzędnych znajduje się wewnątrz kontrolki.
  • Draw - rysuje zawartość kontrolki. W tym przypadku jest to tylko tło, w bardziej złożonych przykładach moze to być cokolwiek.

Podobne metody ma nasze kółeczko, reprezentowane przez obiekt Circle. Oprócz podstawowych parametrów koła (współrzędne X, Y i promień R), obiekt posiada atrybut IsHovered. To on określa, czy nad wskazanym obiektem znajduje się kursor myszy. Wspomniałem, że kółeczko ma podobne metody do obiektu kontrolki:

  • Contains - określa, czy punkt o wskazanych współrzędnych znajduje się wewnątrz kółeczka.
  • Draw - rysuje kółeczko. Kolor kółeczka zależy od tego, czy znajduje się nad nim kursor myszy (atrybut IsHovered).

Nieco inną funkcję pełni obiekt mouse - jest on tylko po to, aby zgrupować funkcje do obsługi myszki. A trzeba powiedzieć, że przydadzą się nam aż trzy zdarzenia, z czterema metodami obsługi tych zdarzeń. To tam dzieje się cała logika:

  • mouse.Down - Zapamiętujemy współrzędne kursora myszy i sprawdzamy czy kliknięcie pojawiło się w obszarze kółeczka. Jeżeli tak, mamy do czynienia z operacją przeciągania.
  • mouse.Release - Kończymy operację przeciągania, jeżeli oczywiście jakaś była.
  • mouse.Move - Jeżeli jesteśmy w trakcie operacji przeciągania, przestawiamy kółeczko i odrysowujemy obszar elementu canvas. Sprawdzamy też, czy nie znaleźliśmy się nad obszarem lub poza obszarem kółeczka i czy nie trzeba zmienić kursora myszy.
  • mouse.WindowMove - Sprawdzamy, czy nie opuściliśmy obszaru zajmowanego przez element canvas. Jeżeli tak, kończymy operację przeciągania, przywracamy kursor, odrysowujemy obszar i symulujemy zwolnienie przycisku myszy.

Trzeba jeszcze wspomnieć o jednej niedogodności - przeglądarka Firefox nie posiada atrybutów offsetX oraz offsetX. Nie są to atrybuty zdefiniowane w standardzie, ale są wspierane przez większość popularnych przeglądarek - poza Firefoxem. Aby ujednolicić kod, wszystkie funkcje korzystające z potencjalnie nieobecnych atrybutów poprzedzone są wywołaniem metody rozszerzającej obiekt zdarzenia. Po wywołaniu funkcji, nazwanej hucznie ExtendFirefoxEvent, te atrybuty już tam będą.

To w zasadzie wszystko co się tutaj dzieje. Zamieściłem kod całej strony HTML, więc zachęcam do skopiowania go sobie w ustronne miejsce i drobne eksperymenty. Jako zadanie polecam rozszerzenie aplikacji w taki sposób, aby kółeczek było więcej i każde z nich dało się przeciągać. Zapraszam do dzielenia się przemyśleniami i wątpliwościami w komentarzach.

Kategoria:HTMLJavaScriptCanvas

, 2013-12-20

Komentarze:

Krtraw (2014-01-12 22:42:22)
Na iPadzie nie działa
Biały murzyn (2014-05-03 17:35:12)
Czy model się na bieżąco renderuje czy zapisuje gdzieś w cache-u?
PD (2014-05-05 15:40:27)
@Biały murzyn: Wszystko rysuje się na bieżąco. Służą do tego obie funkcje Draw - jedna reprezentuje prostokątny obszar kontrolki canvas i zamazuje poprzedni rysunek, druga, reprezentująca kółko, wykonywana jest zaraz po pierwszej i rysuje obiekt w nowej pozycji.
Dane o rozmiarach i pozycjach są zapisane w zmiennych controlBounds (dla tła) i circle (dla kółka).
Obrazka nie trzeba buforować, bo sam element canvas zapamiętuje stan. Dopóki sami nic nie zmienimy to wszystkim zajmuje się przeglądarka (na przykład podczas przewijania zawartości okna).
Madixs (2015-03-26 10:56:54)
Trzeba jeszcze wspomnieć o jednej niedogodności - przeglądarka Firefox nie posiada atrybutów offsetX oraz offsetX. Nie są to atrybuty zdefiniowane w standardzie, ale są wspierane przez większość popularnych przeglądarek - poza Firefoxem

Nie powinno tam być offsetX oraz offsetY? :) Taka mała literówka w sumie ale razi :)
Dodaj komentarz
Wyślij
Ostatnie komentarze
chcę dodać kolumnę, która będzie połączeniem dwóch innych istniejących już kolumn, jak powinien wyglądać scrypt?
Przydałyby się jeszcze 2 rzeczy do cz. 3 i byłoby superanckie.
1. Na starcie sortuje wg jakiejś kolumny i tam jest już strzałeczka. Widok takiej strzałeczki daje znać użytkownikowi, że taką tabele można sortować, a na razie pojawia się ona tylko po kliknięciu.
2. Uwzględnienie polskich znaków, bo np. przy sortowaniu Nazwisk i Imion jest to bardzo uciążliwe.
Ogólnie bardzo fajnie i prosto.
PS. Jest ten artykuł z jQuery już dostępny.
bardo ciekawe , można dzięki takim wpisom dostrzec wędkę..
Bardzo dziękuję za jasne tłumaczenie z dobrze dobranym przykładem!