Drag & Drop & Canvas - jak poruszyć obiekty
Interaktywność elementów canvas
- Bo w dwóch jest mi za gorąco...
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
Komentarze:
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).
Nie powinno tam być offsetX oraz offsetY? :) Taka mała literówka w sumie ale razi :)