un champs d'étoiles tournant

 

par David Cobac


Pré-requis


Vous avez normalement suivi le code d'un champs d'étoiles !


Objectif : le faire tourner


Pour réaliser notre objectif, nous allons créer une procédure qui prend à intervalle de temps donné tous les objets taggés par "etoile" dans notre canevas et effectue une rotation d'un angle donné par rapport à un point quelconque de l'écran.


Procédure rotation


Les formules de rotation

Soit un point du plan et notre angle, si on considère la point M(x,y) ayant pour image M'(x';y') par la rotation de centre et d'angle alors on a :

ce qui donne :

On passera en arguments de notre procédure l'angle de la rotation et les coordonnées du centre.

Application

Je rappelle que notre canevas se nomme .c

'Récupération des objets etoile et de leurs coordonnées'

Nous allons récupérer toutes les étoiles existantes sur le canevas avec :

 .c find withtag etoile

qui nous fournira la liste des identifiants d'objets taggés par etoile. Pour chacun de ces objets, nous allons récupérer les coordonnées de construction, je rappelle que les étoiles ont été faites avec des oval, nous n'allons donc pas récupérer les coordonnées du centre de l'étoile mais 2 paires de coordonnées (représentant le coin supérieur gauche et le point inférieur droit du rectangle (ici carré cf. construction de nos étoiles) dans lequel est inscrit notre étoile).

 foreach w [.c find withtag etoile] {
     set liste [.c coords $w]
     for {set i 0} {$i<=3} {incr i} {
             set x($i) [lindex $liste $i]
     }
 }

Nous voilà donc avec x(0), x(1), x(2) et x(3) : x(0) et x(2) sont les abscisses et x(1) et x(3) les ordonnées.

La rotation en elle-même

La rotation se fait en deux temps : nous allons tout d'abord transformer ces 4 coordonnées en un couple représentant un unique point : le centre de l'étoile (c'est lui que nous tournerons) puis nous effectuerons sur ce point nos calculs avec les formules précédentes. On notera xx et yy les coordonées du centre de l'étoile et, X et Y les différences qui reviennent dans les deux formules de rotation à appliquer.

 set xx [expr ($x(0)+$x(2))/2];set yy [expr ($x(1)+$x(3))/2]
 set X [expr $xx-$xcentre];set Y [expr $yy-$ycentre]
 set xp [expr $xcentre+$X*$cosinus-$Y*$sinus]
 set yp [expr $ycentre+$X*$sinus+$Y*$cosinus]

xp et yp désignent enfin les nouvelles coordonnées (celles de M' ).

Finalement

Que reste-t-il à faire ? Et bien il faut détruire les anciens objets etoile et dessiner le nouvel oval ayant pour centre le point de coordonnées (xp;yp). Donc pour détruire l'objet (on n'a pas oublié qu'il y a un foreach) :

 .c delete $w

mais hélas nous avons dans cette affaire perdu l'épaisseur de l'étoile, je rappelle que cette épaisseur varie suivant le niveau de réduction de l'étoile considéré, il nous faut donc la récupérer avant de détruire l'objet :

 set epaisseur [expr ($x(0)-$x(2))/2]

En effet, comme notre étoile est dans un carré, je ne calcule que la différence des abscisses (la différence des ordonnées sera identique), de plus on récupére la demi-épaisseur (le rayon plutôt que le diamètre car ce sera plus facile à manipuler par la suite). Et enfin pour dessiner le nouveau, il nous faut faire:

 .c create oval [expr $xp-$epaisseur] [expr $y-$epaisseur] [expr $x+$epaisseur] [expr $y+$epaisseur]\
             -fill white -outline white -tags etoile

Répétition

On insére à la fin de la procédure la répétition régulière à effectuer :

 after 5 "rotation $angle $xcentre $ycentre"

Bien entendu, pour obtenir l'effet désiré, on peut augmenter le rythme de la rotation en diminuant le nombre de ms entre deux rotations ici fixé à 5, mais en dessous de 5 mon ordinateur a du mal à gérer...

La procédure "presque" finale

La procédure ainsi écrite donne :

 proc rotation {angle xcentre ycentre} {
         foreach w [.c find withtag etoile] {
                 set liste [.c coords $w]
                 for {set i 0} {$i<=3} {incr i} {
                         set x($i) [lindex $liste $i]
                 }
                 set xx [expr ($x(0)+$x(2))/2];set yy [expr ($x(1)+$x(3))/2]
                 set X [expr $xx-$xcentre];set Y [expr $yy-$ycentre]
                 set xp [expr $xcentre+$X*cos($angle)-$Y*sin($angle)]
                 set yp [expr $ycentre+$X*sin($angle)+$Y*cos($angle)]
                 set epaisseur [expr ($x(0)-$x(2))/2]
                 .c delete $w
                 .c create oval [expr $xp-$epaisseur] [expr $y-$epaisseur] [expr $x+$epaisseur] [expr $y+$epaisseur]\
                  -fill white -outline white -tags etoile
         }
         after 5 "rotation $angle $xcentre $ycentre"
 }

Il suffira alors de lancer une fois la procédure...et notre champ tournera. La commande :

 rotation 5 [expr $largeur/2] [expr $hauteur/2]

fera l'affaire sachant que largeur et hauteur désignent les dimensions du canevas, ici on tourne donc autour du centre du canevas.


Améliorations


On peut porter deux gros griefs sur le code final (code complet) : Deux instructions de tracé identiques et lourdes dans deux procédures différentes Que de calculs de cosinus et sinus qui mettent à rude épreuve le processeur

Création d'une procédure de tracé

On va tout simplement créer une procédure de tracé de nos étoiles, cette procédure prendra en arguments l'abscisse et l'ordonnée du centre de l'étoile ainsi que son rayon.

 proc jedessineunoval {x y rayon} {
     .c create oval [expr $x-$rayon] [expr $y-$rayon] [expr $x+$rayon] [expr $y+$rayon]\
             -fill white -outline white -tags etoile
 }

On utilisera cette procédure à la fois dans la création de l'étoile et dans la procédure de rotation.

Le calcul : solution proposée par N. Dorlencourt

L'angle restant constant, il est plus performant de calculer nous-même le cosinus et le sinus de cet angle, ainsi le processeur ne recalcule pas systématiquement ces valeurs. Cette méthode impose naturellement de recalculer les lignes trigonométriques dans le code si on veut changer l'angle de rotation. En choisissant par exemple un angle de 5 degrés, on obtient un cosinus d'environ 0,9962 à 10^-4 près et un sinus d'environ 0,0872 à la même précision. La procédure devient :

 proc rotation {xcentre ycentre} {
         foreach w [.c find withtag etoile] {
                 set liste [.c coords $w]
                 for {set i 0} {$i<=3} {incr i} {
                         set x($i) [lindex $liste $i]
                 }
                 set epaisseur [expr ($x(0)-$x(2))/2]
                 set xx [expr ($x(0)+$x(2))/2];set yy [expr ($x(1)+$x(3))/2]
                 set X [expr $xx-$xcentre];set Y [expr $yy-$ycentre]
                 set xp [expr $xcentre+$X*0.9962-$Y*0.0872]
                 set yp [expr $ycentre+$X*0.0872+$Y*0.9962]
                 .c delete $w
                 jedessineunoval $xp $yp $epaisseur
         }
         after 5 "rotation $xcentre $ycentre"
 }

La solution de N. Dorlencourt était plus ambitieuse puisqu'elle proposait tout simplement l'établissement d'une table de cosinus et de sinus, ce qui permet de changer la valeur de l'angle plus facilement sans gros calculs.


Le code complet final


 proc jedessineunoval {x y rayon} {
         .c create oval [expr $x-$rayon] [expr $y-$rayon] [expr $x+$rayon] [expr $y+$rayon]\
          -fill white -outline white -tags etoile
 }
 proc rotation {xcentre ycentre} {
         foreach w [.c find withtag etoile] {
                 set liste [.c coords $w]
                 for {set i 0} {$i<=3} {incr i} {
                         set x($i) [lindex $liste $i]
                 }
                 set epaisseur [expr ($x(0)-$x(2))/2]
                 set xx [expr ($x(0)+$x(2))/2];set yy [expr ($x(1)+$x(3))/2]
                 set X [expr $xx-$xcentre];set Y [expr $yy-$ycentre]
                 set xp [expr $xcentre+$X*0.9962-$Y*0.0872]
                 set yp [expr $ycentre+$X*0.0872+$Y*0.9962]
                 .c delete $w
                 jedessineunoval $xp $yp $epaisseur
         }
         after 5 "rotation $xcentre $ycentre"
 }
 proc main {l L nb epaisseur} {
         set xmil [expr $l/2];set ymil [expr $L/2]
         for {set i 1} {$i<=$nb} {incr i} {
                 set x [expr int(rand()*$l+1)]
                 set y [expr int(rand()*$L+1)]
                 jedessineunoval $x $y $epaisseur
         }
         .c scale etoile $xmil $ymil .95 .95
         .c addtag adetruire enclosed [expr $l/2-2] [expr $L/2-2]\
          [expr $l/2+2] [expr $L/2+2]
         .c delete adetruire
         after 5 "main $l $L $nb $epaisseur"
 }
 wm resizable . false false
 wm geometry .  [winfo screenwidth .]x[winfo screenheight .]
 update
 set largeur [winfo width .]
 set hauteur [winfo height .]
 pack [canvas .c -width $largeur -height $hauteur -background black]
 main $largeur $hauteur 2 4
 rotation [expr $largeur/2] [expr $hauteur/2]

Conclusion


Tcl/Tk n'est pas vraiment prévu pour ce genre de choses ! La lenteur de cet exemple est assez éloquent, mais soyons conscients que c'est aussi lent que simple et que ce petit exercice est loin d'être inintéressant. Notons que l'ajout systématique d'accolades dans toutes les expressions mathématiques par exemple : set xp [expr {$xcentre+$X*0.9962-$Y*0.0872}] au lieu de : set xp [expr $xcentre+$X*0.9962-$Y*0.0872] améliore considérablement les performances. Bien sûr, on modifiera à loisir les différents paramètres notamment le temps de réactualisation, le nombre de points (il vaut mieux d'ailleurs appeler la procédure main avec un nombre minimal de points : 1 !) et la grandeur de la fenêtre (wm geometry . 100 100 a été utilisé pour le débuggage).


AM - J'ai lu l'entier article maintenant, c'est bien fait à mon avis - la structure est bien claire. Mais ça m'a aussi donné d'autres idées d'améloriations : utilisant la commande ".c coords $item $nouveau_coordonnees" on peut changer les coordonnées d'un objet sans devoir le détruire et recréer - c'est plus efficace avec les ordinateurs contemporains des optimisations comme utiliser des tableaux pour cos et sin peut en effet augmenter le temps d'opérations mon ordinateur est sous Windows 98 chez moi avec une granularité d'environ 10 ms, c'est-à-dire, des delays moins que 10 ms avec [after] ne se passent pas.