Digital Trail

Digital Trail est une petite application graphique Windows Forms dont le but est extrêmement simple : représenter le déplacement de la souris sous la forme d’un chemin s’atténuant avec le temps. Elle permet notamment de retrouver quelques éléments de base indispensables à une application graphique de ce type : Utilisation du double buffer, des pinceaux, des couleurs, gestion de la souris et des timers pour l’animation.

Avec les outils graphiques disponibles dans le .NET Framework, il est possible à partir d’un concept très simple et avec un peu d’imagination, d’obtenir des résultats assez sympathiques.

 

Digital Trail

Digital Trail

 

Téléchargement : Exécutable v. 1.0, Fichiers source VS 2008

 

Présentation du projet

 

Concernant ce programme, il s’agissait à l’origine de tester quelques fonctions du .NET Framework à l’aide d’une petite application graphique très simple et agréable à utiliser. En général, ce type d’application fini toujours par contenir des exemples d’algorithmes ou de codes susceptibles d’être réutilisées plus tard à d’autres occasions. L’avantage aussi, c’est que ce genre de programme nécessite peu de temps de développement pour arriver à un premier résultat visible et intéressant (et plus attrayant qu’une application console). Pour résumer, ce sont des petits programmes qui ne prennent pas la tête et qui me réconcilient souvent avec la programmation lorsque je sature un peu ! Enfin, c’était surtout valable pour le Framework 2.0 et la librairie graphique System.Drawing. Pour WPF (Windows Presentation Foundation) et le XAML, c’est un peu particulier en raison du rendu vectoriel et des objets de base plus complexes à utiliser, mais c’est sans doute une question d’habitude. J’essaierai de faire une comparaison entre ces deux librairies graphiques une fois que j’aurai épuisé mon stock d’applications Windows Forms.

Mais revenons à notre sujet : L’idée de départ ici était de représenter le déplacement de la souris par un tracé s’atténuant avec le temps, sur le même principe que la traînée (trail en anglais) laissée par un avion ou une comète par exemple.
Plutôt que d’afficher un simple trait, j’ai essayé de proposer quelque chose d’un peu plus élaboré en utilisant plusieurs méthodes disponibles dans le .NET Framework.

Le déplacement de la souris permet donc de dessiner un tracé, dont chaque élément correspond à un carré (une cellule) plus ou moins grande, et dont la couleur s’estompe avec le temps. Différentes options sont disponibles : deux glissières pour définir la taille des cellules et la vitesse à laquelle leur couleur s’estompe, et deux cases à cocher pour les autres options graphiques. Un effet visuel supplémentaire (un effet de « brillance ») est associé à l’utilisation des boutons de la souris.

 

Fonctionnement

 

Gestion du tracé

Il y a plusieurs façons de gérer l’effet de traînée du tracé. Évidemment, dans tous les cas il faut avant tout trouver une solution efficace pour stocker les différents points du chemin suivi par la souris. On peut notamment choisir d’utiliser une liste ou un tableau pour mémoriser les données nécessaires. Ces deux solutions ont leurs avantages et leurs inconvénients suivant l’effet visuel que l’on souhaite obtenir au final.

Utilisation d’un tableau à deux dimensions

C’est la solution la plus évidente : utiliser un tableau (Array), ou plutôt une matrice à deux dimensions (x et y) pour y stocker l’état de chaque point de la zone d’affichage. Idéalement on pourrait utiliser un tableau d’octets pour y indiquer l’état visuel du point à afficher (0 -> masqué, 255 -> visible). Cette solution est performante tant que la taille des éléments du tracé à afficher est assez importante car la matrice contiendra peu de points dans ce cas. Mais plus la grille est fine, plus le rendu sera coûteux en mémoire et en temps de traitement : l’ensemble des cellules du tableau devant être parcourues pour chaque image calculée. De plus, ce n’est pas particulièrement optimisé de vérifier la valeur de plusieurs milliers de cases si le tracé ne dépasse pas une cinquantaine de points de longueur. Dans le cas le plus extrême (taille de la cellule = 1 pixel), passer directement par un objet Bitmap serait alors plus intéressant, mais obligerait à utiliser du code non managé pour avoir des performances correctes et à faire du traitement d’image, ce qui n’est pas vraiment le but recherché ici.

Utilisation d’une liste

Une autre solution consiste donc à ne conserver que les informations utiles, à savoir l’état et les coordonnées des n points formant la traînée, ce que l’on peut stocker dans un tableau avec une seule dimension. Mais étant donné que la longueur de la traînée n’est pas constante (elle peut varier suivant le déplacement de la souris et les différentes options paramétrables), on regardera plutôt du côté des collections.
En théorie, on aurait pu utiliser une collection du type Queue (file d’attente) puisque dans notre cas, l’élément le plus ancien est le premier à disparaître : chaque point de la traînée s’affaiblit lentement puis fini par ne plus être visible. Cependant on a besoin ici de modifier tous les éléments régulièrement (affaiblissement de la couleur à chaque image affichée) avant qu’ils ne puissent être retirés, et ce type de collection n’est pas vraiment prévu pour ce mode de fonctionnement. J’ai donc préféré utiliser une simple collection List. A noter que, lors de l’événement Paint, les éléments ne sont pas parcourus à l’aide d’une boucle foreach mais avec une traditionnelle boucle for, tout simplement pour pouvoir retirer au fur et à mesure les éléments devenus invisibles. Utiliser un Remove dans la boucle foreach aurait modifié la collection parcourue et donc déclenché une exception.

 

Animation et Timers

Pour cette application il n’y a pas vraiment de soucis de performances, je me suis donc contenté d’un simple Timer situé dans System.Windows.Forms. Ce type de minuterie n’est normalement pas idéal pour gérer une vraie animation car sa précision est limitée à 55 millisecondes, soit environ 18 images par seconde. Cependant ce n’est pas vraiment important ici et cela permet de simplifier le code. Pour rappel, ce timer est spécialement prévu pour les applications Windows.Forms monothread et déclenche un événement à un intervalle défini. Dans notre cas, chaque déclenchement met à jour l’état des points du tracé (en affaiblissant la couleur des points et en nettoyant les points de la liste devenus invisibles), puis force le rafraîchissement graphique de la fenêtre à l’aide la méthode Invalidate. A noter que pour une application WPF, on pourra remplacer ce composant par un DispatcherTimer, situé dans le nouvel espace de nom System.Windows.Threading (sans oublier toutefois que WPF propose d’autres moyens pour gérer les animations).

Le .NET Framework propose aussi d’autres types de minuteries :

  • Une classe Timer dans l’espace de nom System.Timers : Considérée comme une minuterie serveur, son fonctionnement est basé aussi sur les événements mais pour un contexte multithread. Celle-ci est plus précise que la minuterie Windows.Forms.
  • Une classe Timer dans l’espace de nom System.Threading : Il s’agit d’une minuterie très simple et performante prévue pour les applications multithread, mais son utilisation est un peu moins pratique que les autres minuteries. De plus son fonctionnement peut poser quelques soucis avec les applications Windows Forms.

MSDN propose une page résumant ces trois types de minuterie et leur utilisation : Introduction aux composants Timer serveur.

 

Rendu graphique

 

Passons maintenant à l’aspect le plus intéressant du programme : le rendu graphique et le dessin des différents éléments.

 

Double buffer

Le double buffer est indispensable pour que l’animation s’affiche sans scintillements. Pour ceux qui ne connaissent pas ce principe, il faut juste savoir que les objets graphiques sont en fait dessinés en arrière plan dans une mémoire tampon (buffer). Une fois le rendu terminé, cette mémoire est ensuite copiée à l’écran en une seule opération. De cette façon, on ne voit plus la zone de dessin s’effacer complètement à chaque image calculée, ce qui élimine le scintillement.

L’activation du double buffer n’était pas très intuitive avec le Framework 1.1 (il fallait créer une classe dérivée de Control pour avoir accès aux méthodes nécessaires, ou bien utiliser un objet Bitmap intermédiaire). Heureusement, les choses se sont bien arrangées avec la version 2.0 puisqu’il suffit d’activer la propriété correspondante d’une Windows.Form.

Article MSDN sur la gestion des double buffers
Et en particulier celui-ci : Graphiques mis deux fois en mémoire tampon.

 

Atténuation du tracé

Pas grand chose de spécial ici. La disparition progressive des éléments du tracé est simplement réalisée en diminuant la composante alpha (transparence) de leur couleur jusqu’à ce qu’elle soit égale à zéro (couleur totalement transparente) : A chaque appel du timer de rafraîchissement (tmrRefresh), une certaine valeur est retirée à chaque élément du tracé. Cette valeur se paramètre à l’aide de la glissière « speed » : plus la valeur est faible (vers la gauche de la glissière), plus il faudra de déclenchements du timer, donc de temps, pour mettre l’alpha à 0 et rendre l’élément invisible.

 

Mode Flat (plat) ou Dégradé

Il est possible de choisir entre deux rendus différents pour les éléments du tracé : le mode « flat » (plat, sans relief) et « dégradé » (mode par défaut). Dans les deux cas, chaque élément est représenté par un carré, dessiné au moyen de la méthode FillRectangle de l’objet Graphics.
Pour le mode « flat », il s’agit de remplir ces carrés de la façon la plus simple possible, c’est-à-dire avec une couleur uniforme. Dans ce cas, c’est le pinceau SolidBrush qui est utilisé.
Pour le mode par défaut « dégradé », les carrés sont remplis avec un dégradé linéaire vertical composé de la couleur du tracé (partie supérieure du carré) et de la couleur transparente (partie inférieure). Le pinceau utilisé ici est un GradientBrush.
Voici le détail de la déclaration des pinceaux dans l’application. Cet extrait est disponible dans le fichier Render.cs :

// Pinceau solide
using (SolidBrush b = new SolidBrush(color))
{
    g.FillRectangle(b, rect);
}

// Pinceau dégradé
using (LinearGradientBrush b = new LinearGradientBrush(rect.Location,
    new Point(rect.Left, rect.Top + rect.Height),
mainColor, backColor))
{
    g.FillRectangle(b, rect);
}

 

Les pinceaux dégradés sont très utiles pour rehausser facilement un objet graphique en deux dimensions. Il est aussi possible de créer des effets beaucoup plus complexes à l’aide de rampes de dégradés, qui permettent de contrôler beaucoup plus finement les couleurs et le fondu réalisé.

 

Animation de la teinte

La méthode la plus « simple » pour modifier la couleur d’un objet est de faire varier sa teinte. La teinte est une des trois composantes de l’espace HSL (Hue, Saturation, Lightness, ou encore Teinte, Saturation, Luminosité en français). Grâce à cet espace de couleurs, il suffit de faire varier un seul paramètre, la teinte, pour obtenir une variation de la couleur sans altérer la luminosité et la saturation initiales. En définissant une variable dont la valeur évolue de 0 à 360 (intervalle habituel pour cette composante) avec un pas constant, on obtient une transition fluide entre chaque nuance de couleur. Le même effet serait bien plus difficile à obtenir avec l’espace RVB. Cependant, le .NET Framework ne fournit aucune méthode de base pour travailler avec cet espace, mis à part la méthode GetHue de la structure Color, qui permet uniquement l’accès en lecture aux composantes HSL de la couleur. J’ai donc utilisé mes propres classes pour réaliser les conversions, à l’aide des formules que l’on peut récupérer sur Internet. Cette longue recherche était d’ailleurs à l’origine d’un article sur le sujet, que je vous invite à découvrir : System.Drawing.Color et l’espace de couleurs TSL (HSL).

Animation de la teinte

Exemple de tracé utilisant l'animation de la teinte

 

Brillance

Pour une application associant le déplacement de la souris à un effet visuel, il aurait été dommage de passer totalement à côté de la gestion des boutons de la souris. J’ai donc ajouté un dernier effet lié à la pression d’un de ces boutons, disponible avec l’événement MouseDown.

Le choix du type d’effet n’a pas été facile : je voulais quelque chose d’intéressant à regarder qui me permettrait de mettre en valeur le tracé obtenu avec le déplacement de la souris, tout en restant simple à coder. J’ai donc choisi de simuler un éclat autour de la traînée laissée par le curseur, un chemin brillant dont l’effet disparaît dès que le bouton est relâché (événement MouseUp). Cet effet est obtenu assez simplement en dessinant des carrés supplémentaires transparents et de taille supérieure autour de chaque élément (cellule) du tracé. En se superposant, ils donnent un rendu assez intéressant et très proche du résultat souhaité. L’effet est accentué en utilisant temporairement la couleur blanche pour l’ensemble des éléments du tracé.

Pour ne pas trop s’embêter avec le calcul des coordonnées des nouveaux carrés, il existe une méthode prévue pour ce genre de transformation dans la structure Rectangle : Inflate. Celle-ci permet de recalculer les 4 paramètres d’un rectangle (coordonnées du coin supérieur gauche avec Top et Left, et la taille avec Height et Width) en modifiant la taille de ce dernier, tout en le maintenant centré par rapport à ses dimensions précédentes. En gros, ça permet de dessiner des carrés plus grands centrés sur le carré d’origine en spécifiant simplement le facteur d’agrandissement. Voici le résultat du dessin de chacun de ces carrés sur un élément du tracé :

Détails de l'effet de brillance

La couleur utilisée pour dessiner les 5 carrés supplémentaires est quasiment la même pour chacun d’entre eux. L’effet de dégradé dans les nuances de gris est en fait obtenu grâce à la superposition de couleurs semi transparentes. L’astuce est liée au comportement de la couche alpha (transparence) dans le .NET Framework et à son mode de fusion (alpha blending en anglais). Ici les carrés sont combinés un peu comme des feuille de papier transparent que l’on empilerait sur plusieurs couches : à chaque nouvelle feuille, la couleur deviendrait plus prononcée et la transparence diminuerait aux endroits où les couchent se superposent. Il existe de nombreux modes de fusion des couleurs, mais malheureusement le .NET Framework n’en propose qu’un seul. Cela peut être particulièrement gênant dans certains cas, notamment lorsque l’on souhaite réellement reproduire un effet de fusion de sources lumineuses. Mais ici ce n’est pas vraiment un problème. A noter que l’effet a toutefois été accentué pour chaque élément en y ajoutant un léger multiplicateur, de façon a donner à chaque carré un alpha un peu plus faible au fur et à mesure que sa taille augmente. Le dégradé est ainsi un peu plus prononcé.

Voici le résultat final lorsqu’on applique cet effet visuel à chaque élément du tracé :

Rendu final de l'effet de brillance

Rendu final de l'effet de brillance

 

Attention toutefois : le code n’est pas vraiment optimisé au niveau de la vitesse d’exécution, ce qui a un impact assez important sur le nombre d’images par secondes calculées. On pourrait aussi envisager d’associer de nouveaux effets à d’autres évènements de la souris, comme l’utilisation de la molette, ou encore distinguer les effets et animations suivant le type de bouton pressé (droite, gauche, milieu).

 

  1. Pas encore de commentaire
  1. Pas encore de trackbacks