Essai d’utilisation de la bibliothèque « KineticJS » dans un projet GWT (Google Web Toolkit)

Introduction

Le but de cet article est de tester la bibliothèque "KineticJS". Celle-ci bénéficie d'une meilleure intégration à GWT que "D3.js" car un wrapper "KineticGWT" existe.

Du coup elle devrait la remplacer avantageusement dans un projet de ce type.

Projet de test

Pour générer le projet de test je vais utiliser un archetype Maven pour générer le squelette du projet.

mvn archetype:generate \
   -DarchetypeGroupId=org.codehaus.mojo \
   -DarchetypeArtifactId=gwt-maven-plugin \
   -DarchetypeVersion=2.5.0
  • groupId: conceptforge.kinetics
  • artifactId: TestKinetics
  • version: 1.0-SNAPSHOT
  • package: conceptforge.kinetics
  • module: TestKinetics

A ce stade, le projet est fonctionnel, mais c'est le projet par défaut généré par l'archetype Maven.

Après une bonne cure d'amaigrissement, le projet est devenu un magnifique "Hello World" à partir duquel je vais pouvoir commencer à intégrer "KineticJS".

Voici un aperçu des fichiers qui restent dans le projet :

Le code source complet de ce Hello World est disponible ici : TestKinetics1.tar.gz

Intégration de la librairie KineticsJS

Le portage de cette librairie sous GWT porte (naturellement) le nom de "KineticGWT". Le lien "github" met à disposition de la documentation pour l'intégration de cette librairie dans une application GWT/Maven. Ça tombe bien, c'est exactement le cas de ce programme de test.

Il faut donc :

  • Ajouter les nouvelles dépendances dans "pom.xml"
  • Ajouter le module dans "TestKinetics.gwt.xml"
  • Modifier l'implémentation de "onModuleLoad()"
package conceptforge.kinetics.client;
 
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RootPanel;
import net.edzard.kinetic.*;
 
/**
 * Entry point classes define
 * <code>onModuleLoad()</code>.
 */
public class TestKinetics implements EntryPoint {
 
    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        // Kinetic needs a special div in the DOM
        Element div = DOM.createDiv();
        RootPanel.getBodyElement().appendChild(div);
 
        // Setup stage
        Stage stage = Kinetic.createStage(div, 400, 400);
        Layer layer = Kinetic.createLayer();
        stage.add(layer);
 
        Rectangle c = Kinetic.createRectangle(new Box2d(10, 10, 200, 200));
        layer.add(c);
        stage.draw();
    }
}

Résultat

Site du zéro

Par chance le site du zéro propose un tutoriel sur KineticsJS.

Formes prédéfinies

Voici un exemple librement inspiré de la première partie du tutoriel (formes prédéfinies)

package conceptforge.kinetics.client;
 
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RootPanel;
import net.edzard.kinetic.*;
 
/**
 * Entry point classes define
 * <code>onModuleLoad()</code>.
 */
public class TestKinetics implements EntryPoint {
 
    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        Element div = DOM.createDiv();
        RootPanel.getBodyElement().appendChild(div);
 
        Stage scene = Kinetic.createStage(div, 400, 400);
        Layer calque = Kinetic.createLayer();
        scene.add(calque);
 
        Rectangle r = Kinetic.createRectangle(new Box2d(50, 75, 350, 300));
        r.setFill(Colour.blue);
        r.setStroke(Colour.blue);
        r.setStrokeWidth(0);
 
        Circle c = Kinetic.createCircle(new Vector2d(50, 50), 50);
        c.setFill(Colour.green);
        c.setStroke(Colour.darkgray);        
 
        RegularPolygon h = Kinetic.createRegularPolygon(new Vector2d(200,150), 50, 6);
        h.setRotation(30);
        h.setFill(Colour.red);
 
        calque.add(r);
        calque.add(c);
        calque.add(h);
 
        scene.draw();
    }
}

Résultat

Ombres

En m'inspirant du tutoriel "Opacité et ombre", j'ai ajouté une ombre aux formes de l'exemple précédent :

Pour arriver à ce résultat il suffit de créer l'instance suivante de "Shadow":

Shadow shadow = new Shadow(Colour.black, 12, new Vector2d(8, 8), 0.7);

Ensuite, il faut utiliser la méthode "setShadow" pour passer cette valeur à toutes les formes.

Gestion des événements

En m'inspirant du tutoriel "Interactions", j'ai écrit le code suivant pour observer le contenu de la variable "evt" passée en paramètre dans la gestion d'événements :

r.addEventListener(Event.Type.CLICK, new Node.EventListener() {
 
            public boolean handle(Event evt) {
                try {
                    Window.alert(evt.toString());
                } catch (Exception e) {
                    Window.alert(e.getMessage());
                }
                return true;
            }
        });

En testant, j'ai eu la désagréable surprise de recevoir le message suivant :

Grosse déception

Alors que tous mes tests jusqu'à maintenant me montraient une bonne librairie pour manipuler la balise Canvas et un bon wrapper pour GWT. Je viens de toucher aux limites de cette implémentation.
Lors de la gestion d'événements, la méthode "handle" d'un EventListener contient une variable evt mais celle-ci contient toujours "null".
Après une petite recherche dans le code source, j'ai trouvé (dans l'implémentation de la classe "Node") la méthode suivante qui explique mon problème :

	/**
	 * Add a multi-event listener to the node.
	 * @param eventTypes A number of events to listen to
	 * @param handler The handler.
	 */
	// Seems to be buggy in kineticjs. Always getting mousemove events
	// TODO Maik: It's not buggy, it just fails and then creates too many events due to the failure.
	//             the evt variable is directly from the browser, not from kineticjs, that's why there's
	//             a mismatch between event types
	//             Touch also needs to be handled differently if evt should be used.
	//       quick fix: ignore evt object
	public final native void addEventListener(List eventTypes, EventListener handler) /*-{
		this.on(@net.edzard.kinetic.Node::createEventTypeString(Ljava/util/List;)(eventTypes), function(evt) {
//			if (evt != null) {
//				console.log(evt.type);
//				var javaEvt = @net.edzard.kinetic.Event::new(Lnet/edzard/kinetic/Event$Type;Lnet/edzard/kinetic/Event$Button;II)(
//					@net.edzard.kinetic.Event.Type::valueOf(Ljava/lang/String;)(evt.type.toUpperCase()),
//					@net.edzard.kinetic.Event.Button::fromInteger(I)(evt.button),
//					evt.offsetX,
//					evt.offsetY
//				);
//				javaEvt.@net.edzard.kinetic.Event::setShape(Lnet/edzard/kinetic/Shape;)(evt.shape);
//				var bubble = handler.@net.edzard.kinetic.Node.EventListener::handle(Lnet/edzard/kinetic/Event;)(javaEvt);
//				evt.cancelBubble = !bubble;
//			} else {
				handler.@net.edzard.kinetic.Node.EventListener::handle(Lnet/edzard/kinetic/Event;)(null);
//			}
		});
	}-*/;

En attendant une correction de cette méthode, je propose une solution de contournement (pas très élégante, forcément) :

r.addEventListener(Event.Type.CLICK, new Node.EventListener() {
 
            public boolean handle(Event evt) {
                if (r.getFill().toString().equals(Colour.blue.toString())) {
                    r.setFill(Colour.white);
                } else {
                    r.setFill(Colour.blue);
                }
 
                scene.draw();
                return true;
            }
        });

Je fais un accès direct à l'objet. Ce n'est pas élégant et c'est beaucoup moins souple, mais, au moins, c'est fonctionnel.
En écrivant cette petite routine, j'ai eu une deuxième mauvaise surprise, mais moins grave que la précédente. La comparaison de la couleur ne fonctionne ni avec "==" ni avec "equals".

Conclusion

Tout s'est très bien passé jusqu'à ce que j'essaye de gérer les événements. Mon soupçon est que la librairie "KineticJS" est très fonctionnelle. Ce soupçon n'est pas fondé sur des arguments techniques, mais plutôt construit à partir des différents commentaires que j'ai pu lire à ce sujet sur le Web.

Par contre, le wrapper "KineticGWT" m'a permis de réaliser très facilement les exemples du tutoriel que j'ai utilisé tant que je ne faisais pas de gestion d'événements. Pour l'instant, il n'est qu'en version "0.9.1" c'est donc certainement un bug de jeunesse, mais à ce stade je ne sais pas si j'ose prendre le risque d'intégrer ce wrapper dans un projet réel et contourner tous les petits problèmes résiduels...

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>