• Aucun résultat trouvé

Présentation de Polyphony et du prototype de développement

gentleman gentlemwe

Chapitre 3. La boîte à outils Polyphony

3.2 Programmation d'interactions avec Polyphony

3.2.2 Présentation de Polyphony et du prototype de développement

Polyphony est une boîte à outils logicielle expérimentale basée sur ECS, dédiée à la construction d'interfaces graphiques et d'interactions (voir figure 32). Elle se compose d'un noyau implémentant les notions d'Entités, de Composants, de Systèmes, de Descripteurs et de Sélections. Elle fournit de plus des bindings vers l'une ou l'autre de deux bibliothèques de bas-niveau , SDL  [Lan98] et libpointing  [Cas11]. Notre objectif était de pouvoir utiliser le support avancé de la souris de libpointing (souris multiples, remplacement des fonctions de transfert, interaction subpixel  [Rou12]), avec SDL pour l'affichage. Cependant, libpointing empêchait la réception des évènements du clavier par SDL, ce qui nous a obligés à utiliser l'un ou l'autre des bindings. À un niveau supérieur dépendant du premier, Polyphony fournit des Composants, Systèmes, Sélections et fabriques prédéfinis, qui peuvent être utilisés par les applications pour construire rapidement des interfaces.

Langage JavaScript Application noyau ECS binding SDL2 binding libpointing Composants prédéfinis Systèmes prédéfinis Sélections prédéfinies Fabriques prédéfinies

}

Polyphony

Figure 32 : Structure en modules de Polyphony

L'application de dessin vectoriel est un exemple courant et bien approprié pour illustrer la conception de techniques d'interaction  [Gog14, Api04]. Elle combine l'implémentation d'objets graphiques, d'outils (ex. dessin de formes, pipette à couleurs), de commandes (ex. copier/coller, undo), et offre une large palette de tâches possibles avec de nombreuses améliorations et combinaisons entre éléments.

Notre application de base de dessin (voir figure 33) permet ainsi de : dessiner des rectangles et des ovales ;

déplacer, supprimer et changer le type des formes créées ; enregistrer le résultat au format SVG ;

réinitialiser l'espace de travail.

Figure 33 : Espace de travail de notre exemple d'application de dessin vectoriel

3.2.3 Illustration avec le code de l'application

Tous les éléments de l'interface sont des Entités : boutons, formes dessinées, et zone de dessin en arrière plan. Les périphériques d'entrée sont aussi représentés par des Entités, ainsi que la zone d'affichage.

3.2.3.1 Création d'Entités

Les Entités sont essentiellement des identifiants qui peuvent être créés à la volée et sans Composants avec la fonction Entity : let e = Entity(). Dans la pratique, il peut être souhaitable de créer des Entités avec des Composants initiaux. C'est pourquoi la fonction Entity peut recevoir en paramètre optionnel un objet JavaScript pour ajouter ces Composants initiaux. Par exemple, dans notre application de dessin, une Entité pour un simple rectangle sera créée avec :

let e = Entity({

bounds: new Bounds(0, 0, 100, 50),

shape: SHAPE_RECTANGLE, })

Composants bounds, shape, et backgroundColor. Ainsi, l'ajout du Composant approprié à notre Entité la fera apparaître à l'écran :

e.backgroundColor = rgba(0, 0, 255)

L'Entité devient alors visible par le “Système de rendu d'arrière-plan”, qui dessine un rectangle bleu à l'écran. En pratique, Polyphony fournit des fabriques d'Entités qui permettent d'instancier des widgets standards avec des Composants prédéfinis. Ainsi, dans l'application de dessin, les boutons de la barre d'outils ainsi que le canevas sont créés à partir des fabriques Button et Canvas :

let y = 2

let resetButton = Button(new Icon('icons/reset.bmp'), 2, y)

let saveButton = Button(new Icon('icons/save.bmp'), 2, y += 34)

let moveButton = Button(new Icon('icons/move.bmp'), 2, y += 34, {toggleGroup: 1})

let rectButton = Button(new Icon('icons/rect.bmp'), 2, y += 34, {toggleGroup: 1})

let ovalButton = Button(new Icon('icons/oval.bmp'), 2, y += 34, {toggleGroup: 1})

let canvas = Canvas(36, 2, 602, 476)

Chaque fabrique prend en arguments un ensemble de valeurs nécessaires à la construction de l'Entité de base. En dernier argument optionnel, on peut passer un dictionnaire contenant un ensemble de Composants additionnels pour initialiser la nouvelle Entité. Si ce dictionnaire est déjà une Entité, elle est directement complétée plutôt que d'en créer une nouvelle. Par exemple, la fabrique Button est définie par :

function Button(imgOrTxt, x, y, e = {}) {

e.depth = e.depth || 0

e.bounds = e.bounds || new Bounds(x, y, imgOrTxt.w + 8, imgOrTxt.h + 8)

e.shape = e.shape || SHAPE_RECTANGLE

e.backgroundColor = e.backgroundColor || rgba(240, 248, 255)

e[imgOrTxt instanceof Image ? 'image' : 'richText'] = imgOrTxt e.targetable = true

e.actionable = true

return Entity(e) }

Lorsque la fabrique reçoit une Entité à compléter, elle n'ajoute les Composants que s'ils n'ont pas été déjà définis. C'est ce que réalise l'opération e.composant = e.composant || valeur — lorsqu'un Composant n'est pas défini il est évalué à undefined, ce qui est équivalent à false pour les opérations booléennes.

Enfin, dans ECS les Entités sont supprimées manuellement. Comme elles sont accessibles globalement grâce aux Sélections, elles ne peuvent jamais être qualifiées de “hors de portée” pour des mécanismes de portée locale ou de ramasse-miettes. La suppression d'une Entité s'effectue avec e.delete(), ses Composants étant alors automatiquement retirés, ainsi que toutes références pointant vers e dans le système.

3.2.3.2 Création de Composants

Dans Polyphony, les Composants sont les constructeurs d'objets de JavaScript :

function Bounds(x, y, w, h) { this.x = x this.y = y this.w = w this.h = h }

Conformément au modèle ECS, ils ne contiennent pas normalement de code. Une exception à cette règle est la définition d'accesseurs, pour laquelle nous utilisons alors des méthodes d'instance — implémentées par l'héritage prototypal de JavaScript. Un setter, par exemple, se crée avec :

Bounds.prototype = { setX(x) { this.x = x return this } }

Par convention, les setters dans Polyphony renvoient toujours l'objet ciblé (this), afin de pouvoir enchaîner plusieurs appels dans une seule instruction et rendre ainsi le code plus concis  : (e.setX(10).setY(20)).

3.2.3.3 Création de Systèmes

Comme pour les fonctions lambda, qui sont réifiées en tant qu'objets du langage, nous avons implémenté les Systèmes en tant qu'Entités. Leurs dépendances sont donc représentées par des données stockées sous forme de Composants. Les systèmes sont instanciés une seule fois avec la même fonction Entity, qui prend comme paramètres une fonction suivie de ses Composants :

let ResetSystem = Entity(function ResetSystem() {

if (resetButton.tmpAction) {

for (let c of canvas.children)

c.delete()

canvas.children = [] }

...

}, { runOn: POINTER_INPUT, order: 60 })

Dans notre exemple d'application, ce Système simple va vérifier si le bouton resetButton défini plus haut a été activé (cliqué) par la souris. Lorsque c'est le cas, il supprime tous les enfants de l'Entité

de pointage (souris). Le Composant order distingue et ordonne les classes de Systèmes (représentés

en figure 31) : Entrées (0 à 19), Techniques d'interaction (20 à 39), Widgets (40 à 59), Application (60 à

79), et Sorties (80 à 99). ResetSystem fait donc partie des Systèmes d'application. 3.2.3.4 Création de Sélections

Lorsqu'on souhaite itérer sur des groupes d'Entités possédant des Composants en commun, on utilise généralement les Sélections de Polyphony. Par exemple, pour mettre en surbrillance toutes les Entités pouvant être ciblées par la souris :

for (let t of Targetables)

t.border = new Border(1, rgba(0, 255, 0))

Ici, on itère sur la Sélection Targetables, qui contient toutes les Entités avec les Composants bounds, depth et targetable. Pour chacune de ces Entités, on remplace la bordure par une bordure verte épaisse d'un pixel. Une Sélection est définie avec la fonction Selection, qui prend comme paramètre une fonction accept :  Entité → booléen pour filtrer les entités par condition programmable. Un deuxième argument optionnel fournit un critère d'ordre compare

: Entité × Entité → nombre lors de l'itération sur la Sélection. Par exemple, la Sélection Targetables

est définie avec :

let Targetables = Selection(e => 'bounds' in e && 'depth' in e && e.targetable, (a, b) => b.depth - a.depth) // trier par profondeur décroissante

Les Sélections dans Polyphony sont implémentées avec de simples tableaux. Lorsqu'une Entité subit une modification de Composant (ajout, remplacement, ou suppression), elle est ajoutée à une liste interne d'Entités à “soumettre aux Sélections”. À l'issue de l'exécution de tout Système (et avant l'exécution du suivant), chaque Entité de cette liste est soumise à chaque Sélection (elle est passée en argument de la fonction accept) qui l'inclut ou non. Grâce à ce mécanisme, nous ne courons pas le risque de modifier une Sélection alors qu'on itère dessus, ce qui est une source de bugs répandue. Enfin, le tri d'une Sélection est effectué avant d'itérer dessus, à l'aide d'un booléen dirty indiquant si la Sélection a été modifiée et doit être triée à nouveau.