Carrousel pas à pas
Bonjour
Je me propose de décomposer au pas à pas la conception et le développement de ce type de carrousel.
• Connaître (un peu) l'objet MovieClip, ses principales propriétés : scaleX, scaleY, et autres x, y, ainsi que les méthodes addChild et addChildAt et l'événement enterFrame (appel récurrent).
• Connaître les événements souris : MouseOver, MouseMove, MouseDown…
Traumatisés des Maths, ne fuyez pas : si vous savez ajouter ou diviser (voire multiplier, soustraire), ça suffira amplement

A la fin du parcours, vous trouverez au chapitre Décliner, un menu de type Dock qui met en œuvre les mêmes principes.
(Les icônes utilisées dans cet exemple proviennent du site www.iconshock.com)
Le plus frappant quand on regarde un carrousel, c'est qu'il tourne.
Oui, bon… Je veux dire que les éléments qui le composent se déplacent sur une trajectoire elliptique. Ce sera donc l'une de nos préoccupations principales, l'autre étant que les objets changent de plan au fur et à mesure de leur course.
Décomposé ça donne trois étapes :
• Disposer des clips à intervalle régulier sur la circonférence d'un cercle
• En gérer la superposition selon leur position
• Les faire tourner
Le reste n'est que détail et syntaxe
Le minimum à comprendre pour s'en sortir avec les ellipses
Attaquons donc les hostilités avec ce fichu cercle à la circonférence duquel il va convenir de répartir les clips.
Pour ce faire nous aurons recours aux fonctions sinus et cosinus. Ne soupirez pas tout de suite, celles et ceux que ces termes rebutent ne rencontreront pas ici d'exposé magistral trigonométriquoïde
Il nous suffira de comprendre les conséquences de l'utilisation des fonctions sin et cos (classe Math) mixturées aux propriétés x/y d'un clip.
Ceux qui veulent comprendre le pourquoi (je ne saurais les en blâmer
), trouveront
ici un tuto qui traite le sujet.
Les familier(e)s de ces notions peuvent se rendre directement à Construire le carrousel fixe
Pour les autres, voici une synthèse orientée vers nos préoccupations :
Math.sin(…) renvoie des valeurs comprises entre -1 et 1, quelque soit le “nombre” qu'on lui passe.
Pareil pour Math.cos(…)
Ces valeurs se combinent de telle sorte que :
Quand on applique au x d'un clip le cosinus d'une valeur, et à son y le sinus de cette même valeur, on place le clip en question quelque part sur un cercle de rayon 1.
Un cercle de rayon 1, c'est pas gros
Pour un cercle de rayon 100, on multiplierait donc par 100.
Un peu d'empirisme ne nuit pas, si ça vous reste obscur, ne vous privez pas de trafiquer le petit swf de démo ci-dessous
(j'ai arrondi les valeurs pour faciliter la lecture)
demo
Construire le carrousel fixe
Allez ! on attaque.
Dans un premier temps on va s'entraîner avec un seul symbole dans la bibliothèque que l'on instanciera autant de fois que nécessaire. Il vous faut donc un clip dans la bibliothèque. Faites simple, un bête rectangle conviendra tout à fait pour nos essais. Ajoutez un champ texte dynamique (ça ne nuira pas à la compréhension de les numéroter au passage). Je suppose que vous le nommez Elt et comme nous devrons l'utiliser via actionScript, cochez la case exporter pour actionScript dans le panneau propriété.
Il va s'agir d'ajouter d'abord une instance de Elt
, puis autant que souhaité, à la circonférence d'un cercle de rayon r.
Ce cercle doit avoir une origine. Nous considérerons que c'est le point de coordonnées 0/0 du clip carrousel qui va accueillir l'ensemble des instances de Elt
.
Pourquoi un clip carrousel ?
Parce que c'est plus pratique de pouvoir isoler le carrousel du reste de l'animation. On fait une boite, comme dit Monsieur Spi Libre à vous, par la suite, de la positionner où ça vous chantera sur la scène…
Il s'agit donc de créer un clip vide (carrousel), d'y ajouter (addChild) une instance de Elt
, et de la positionner quelque part sur un cercle de rayon 100 (par exemple).
On anticipe un zeste et on sort la variable rayon
, et la variable ang
ça nous donne :
var rayon:int=100; // ============== carrousel =========================== // Un clip vide var carrousel:MovieClip=new MovieClip(); // On le positionne carrousel.x=250; carrousel.y=200; // Ajout à la liste d'affichage addChild(carrousel); // Une instance de Elt var elt:Elt= new Elt(); // Une valeur d'angle quelconque var ang:Number=0 // Ajouter elt à carrousel carrousel.addChild(elt); // Positionner l'instance elt.x=Math.cos(ang)*rayon; elt.y=Math.sin(ang)*rayon;
Evidemment, c'est pas top visuel, pour vérifier que notre clip est bien à la circonférence d'un cercle… Ajoutez quelques lignes (que vous pouvez vous dispenser de comprendre) pour dessiner le cercle.
var rayon:int=100; // ============== carrousel =========================== // Un clip vide var carrousel:MovieClip=new MovieClip(); // On le positionne carrousel.x=250; carrousel.y=200; // ========= temporairement pour y voir quelque chose ========== var fond:Shape=new Shape(); fond.graphics.beginFill(0x666666); fond.graphics.lineStyle(1, 0); fond.graphics.drawEllipse(-rayon, -rayon, rayon*2, rayon*2); fond.graphics.endFill(); carrousel.addChild(fond); // ============================================================== // Ajout à la liste d'affichage addChild(carrousel); // Une instance de Elt var elt:Elt= new Elt(); // Une valeur d'angle quelconque var ang:Number=0 // Ajouter elt à carrousel carrousel.addChild(elt); // Positionner l'instance elt.x=Math.cos(ang)*rayon; elt.y=Math.sin(ang)*rayon;
Et on dispose bien le clip où on veut :
ang = 3.14 (presque ∏) le voilà à gauche du cercle
ang= 2*Math.PI, le voilà revenu à droite (2 ∏ –> un tour complet)…
Bien, ça marche…
Disposer les clips en rond
Pour répartir plusieurs clips, il va nous suffire de faire la même chose dans une boucle. La valeur d'angle va augmenter régulièrement de clip en clip. Il s'agit en fait de calculer le pas entre deux clips.
2∏ pour un tour complet, que divise le nombre le clip, le voilà le pas.
Yapuka… Et au point où on en est, on écrit le numéro du clip dans son champ texte (i).
Si la bête vous crie que Contrainte implicite d'une valeur du type int vers un type sans rapport String
, c'est que vous avez passé i, qui est de type int, à la propriété text qui attend du String, et la conversion ne se fait pas automatiquement. Utilisez String(i) pour convertir (puisqu'il faut tout faire !).
var rayonX:int=200; var rayonY:int=100; var nbElt:int=16; //========= Vignettes =================== var i:int; var pasAngulaire:Number=Math.PI*2/nbElt; for (i=0; i<nbElt; i++) { var elt:Elt= new Elt(); // répartition sur circonférence selon angle var ang:Number=i*pasAngulaire; elt.name="elt"+i; elt.ang=ang; elt.x=Math.cos(ang)*rayonX; elt.y=Math.sin(ang)*rayonY; elt.txt.text=String(i); carrousel.addChild(elt); }
Bon, oui j'ai pris un peu d'avance : sur du cercle tout rond, ce n'est pas très satisfaisant. Pour faire de l'ellipse ovoïde il suffit d'appliquer un rayon différent à l'axe des x et des y. (vous l'avez constaté ici)
Déjà, c'est pas mal, si ce n'est que les vignettes se montent dessus. La 15 est au dessus de la 0.
Normal, on les ajoute par addChild, donc les unes sur les autres, la dernière arrivée (15) est par dessus la première (0).
On ne va pas se laisser abattre pour si peu… Ce qui nous arrangerait ce serait que les vignettes de droite s'empilent les unes sur les autres et les vignettes de gauche les unes sous les autres. Pour ça, il faudrait commencer “par le fond”, poser la vignette 0 à la place qu'occupe actuellement la 12, empiler aussi longtemps qu'on est à droite (cos(ang) >0) et ajouter en arrière plan (addChildAt(…,0)) dans le cas contraire.
Poser la première vignette “au fond”, ce n'est jamais que décaler notre point de départ d'un quart de tour (anti horaire). Un demi tour c'est PI, un quart de tour c'est PI/2, un quart de tour anti-horaire c'est -PI/2.
(Veillez à supprimer, la forme grise qui ne nous sert plus à rien, et qui ferait rien qu'à nous énerver)
var rayonX:int=200; var rayonY:int=100; var nbElt:int=16; // ============== carrousel =========================== var carrousel:MovieClip=new MovieClip(); carrousel.x=250; carrousel.y=200; addChild(carrousel); //========= Vignettes =================== var i:int; var pasAngulaire:Number=Math.PI*2/nbElt; // le décalage de départ var decalAngDep=- Math.PI/2; for (i=0; i<nbElt; i++) { var elt:Elt= new Elt(); // répartition sur circonférence selon angle plus décalage var ang:Number=i*pasAngulaire+decalAngDep; elt.name="elt"+i; elt.ang=ang; elt.x=Math.cos(ang)*rayonX; elt.y=Math.sin(ang)*rayonY; if (Math.cos(ang)>0) { // à droite on superpose carrousel.addChild(elt); } else { // à gauche n ajoute en arrière plan carrousel.addChildAt(elt,0); } elt.txt.text=String(i); }
Trop fort ! On l'a
Le plus dur est fait
Faire tourner, c'est tout niais :
Il suffit d'appeler régulièrement une fonction qui replacera chacun des clips en augmentant la valeur passée à sin et cos d'un “cran” à chaque fois. Plus le cran est grand, plus le déplacement est rapide.
On compte, à terme, pouvoir jouer sur la vitesse, on va donc sortir cette valeur sous forme de variable globale et la nommer vitesse
(cacahuèteSalée c'est possible aussi, mais moins parlant).
La valeur passée à sin et cos, je l'appelle angle du clip (permettez moi l'abus de langage). Pour modifier cet angle encore faudra-t-il en disposer, on va donc créer une variable ang
pour chaque clip elt
et la valoriser à l'instanciation :
elt.ang=ang;
Et puis on va en profiter pour donner un nom à chaque instance :
elt.name="elt"+i;
Animer le carrousel
Parti-pris de développement
Une question reste à trancher :
Allons nous ajouter un écouteur enterFrame à chaque clip, ou ajouter un seul écouteur dont la fonction souscrite animera l'ensemble des clips ?
Je préfère un seul écouteur qu'on retire facilement (removeEventListener).
Je n'aime pas l'idée d'une troupée d'appels invoqués sans cesse, même quand le carrousel est à l'arrêt (vitesse=0).
Puisque sur ce coup là c'est moi qui décide (hé hé), en route pour un seul écouteur
On l'ajoute à la scène, on y souscrit la fonction tourne.
stage.addEventListener(Event.ENTER_FRAME,tourne);
La fonction tourne()
Elle attend un paramètre de type Event et voici ce qu'on y fait :
Dans une boucle, de i=0 à i<nbElt
• on “récupère” chaque instance à l'aide de getChildByName
• on augmente son angle de la valeur de vitesse
• on l'utilise avec cos et sin pour déplacer l'instance
… et c'est tout…
A vous de jouer
var rayonX:int=200; var rayonY:int=100; var nbElt:int=16; var vitesse:Number=0.05; // ============== carrousel =========================== var carrousel:MovieClip=new MovieClip(); carrousel.x=250; carrousel.y=250; addChild(carrousel); stage.addEventListener(Event.ENTER_FRAME,Tourne); //========= Vignettes =================== var i:int; var pasAngulaire:Number=Math.PI*2/nbElt; var decalAngDep=- Math.PI/2; for (i=0; i<nbElt; i++) { var elt:Elt= new Elt(); // répartition sur circonférence selon angle var ang:Number=i*pasAngulaire+decalAngDep; elt.name="elt"+i; elt.ang=ang; elt.x=Math.cos(ang)*rayonX; elt.y=Math.sin(ang)*rayonY; if (Math.cos(ang)>0) { carrousel.addChild(elt); } else { carrousel.addChildAt(elt,0); } elt.txt.text=String(i); } // ============== Tourne ============================= function Tourne(e:Event) { var i:int; var elt:MovieClip; for (i=0; i<nbElt; i++) { elt=MovieClip(carrousel.getChildByName("elt"+i)); elt.ang+=vitesse; var ang=elt.ang; // Position x/y elt.x=Math.cos(ang)*rayonX; elt.y=Math.sin(ang)*rayonY; } }
Il ne fallait pas s'attendre à ce que les clips se ré-organisent de la profondeur tous seuls comme des grands… Ça donne un peu le vertige, mais l'objectif premier est atteint : ça tourne
Il faut donc trouver un moyen pour gérer les plans. Un petit dessin au lieu d'un long discours :
Quand un clip prend la position numéroté 0, il doit être basculé en arrière plan.
Quand un clip prend la position numéroté 8, il doit être basculé au premier plan.
Ces positions s'identifient à l'aide des sinus et cosinus.
Quand pour “l'angle” d'un clip, cos = 0 et sin <0 –> arrière plan.
Quand pour “l'angle” d'un clip, cos = 0 et sin >0 –> premier plan.
ce qui se traduit dans la boucle par
//Gestion des plans // arrondir pour choper la bascule var cosArrondi:int=Math.cos(ang)*10; // 0 : tout au fond ou tout devant if (cosArrondi==0) { if (Math.sin(ang)<0) { // arrière plan carrousel.setChildIndex(elt,0); } else { // premier plan carrousel.setChildIndex(elt,nbElt-1); } }
Vitesse variable
Maintenant que ça roule, pardon… tourne on peaufine.
Faire en sorte que la vitesse dépende de la position de la souris sur l'axe x du carrousel c'est tout bête : il suffit surveiller le MouseMove du carrousel et de modifier la variable vitesse
selon la valeur de mouseX.
carrousel.addEventListener(MouseEvent.MOUSE_MOVE,modifVitesse); function modifVitesse(me:MouseEvent) { vitesse=me.currentTarget.mouseX/rayonX/10; }
Monsieur Propre
Si si, depuis tout à l'heure, j'en vois qui froncent le nez : l'arrondi du cosinus… mmff pas très propre tout ça… D'ailleurs il n'y qu'à passer rayonY
à 0 afin que le carrousel soit “de face” et faire tourner tout doucement (maintenant qu'on peut) dans le sens inverse à celui des aiguilles d'une montre pour constater qu'il y a un petit quelque chose furtif qui ne va pas : la vignette du fond chevauche les autres une fraction de seconde
Bon !
Il faut alors avoir recours à une autre stratégie : stocker les vignettes dans un tableau, trier ce tableau selon les sinus de chaque vignette, puis le parcourir en plaçant les vignettes (profondeurs) les une sur les autres, et de fait, respecter l'empilement de la plus loin du regard (sin le plus petit) à la plus proche (sin le plus grand).
Chacun fera selon son goût et ses exigences : si vous n'aplatissez pas le carrousel, ce n'est pas utile.
les tableaux n'étant pas à l'ordre du jour de ce tuto, si vous voulez vous rafraîchir les idées c'est ici
function tourne(e:Event) { var tbVignettes:Array=new Array(); var i:int; var elt:MovieClip; for (i=0; i<nbElt; i++) { elt=MovieClip(carrousel.getChildByName("elt"+i)); elt.ang+=vitesse; var ang:Number=elt.ang; var sinAng:Number=Math.sin(ang); //--> ici Ajouter une propriété sin à l'élément courant elt.sin=sinAng; // Position x/y elt.x=Math.cos(ang)*rayonX; elt.y=sinAng*(rayonY+pivot); //--> ici //Remplir le tableau tbVignettes.push(elt); // Echelle // récupérer une valeur positive de 0 à 2 var s:Number=Math.sin(ang)+1; var coef:Number=s/2*(1-coefPers)+coefPers; elt.scaleX=elt.scaleY=coef; elt.scaleX=elt.scaleY=coef; } //-->ici Gestion des plans : trier selon les sinus de chaque vignette tbVignettes.sortOn("sin",Array.NUMERIC); for (i=0; i<nbElt; i++) { elt=MovieClip(carrousel.getChildByName(tbVignettes[i].name)); carrousel.addChild(elt); } // friction vitesse*=0.99; if (Math.abs(vitesse)<0.001) { stage.removeEventListener(Event.ENTER_FRAME,tourne); } }
Déclencher et arrêter
Déclencher
C'est plus rigolo de déclencher le mouvement quand on survole le carrousel. Il pourrait alors ralentir jusqu'à s'arrêter quand on ne le sollicite plus… Et se relancer à loisir…
Mettre le carrousel en route, c'est un écouteur sur MouseOver qui invoque une fonction lance, laquelle ajoute l'écouteur idoine à stage.
carrousel.addEventListener(MouseEvent.MOUSE_OVER,lance); function lance(me:MouseEvent) { stage.addEventListener(Event.ENTER_FRAME,tourne); }
Et si on veut faire vraiment bien propre, on n'attache l'événement enterFrame, que si ce n'est pas déjà fait.
function lance(me:MouseEvent) { if (! stage.hasEventListener(Event.ENTER_FRAME)) { stage.addEventListener(Event.ENTER_FRAME,tourne); } }
Arrêter
L'arrêter progressivement, c'est - en fin de boucle - réduire la vitesse, et quand elle est suffisamment basse, supprimer l'écouteur de la scène
vitesse*=0.99; if (Math.abs(vitesse)<0.001) { carrousel.removeEventListener(Event.ENTER_FRAME,tourne); }
Effet de perspective
Avoir dessiné un ovale donne une impression de 3D, ce n'est qu'une impression, une triche. Pour tricher un peu plus (mais ça a ses limites) on pourrait appliquer aux clips une échelle proportionnelle à leur position dans le carrousel : plus elles sont “au fond” plus elles sont petites.
La position dans le carrousel est liée au y, donc au sinus. De 1 (tout devant) à -1 (tout au fond) en passant par 0 (au milieu).
Ce serait plus simple d'avoir une valeur qui varie entre deux bornes positives.
Ajoutons 1 à Math.sin(ang), on a :
0 tout au fond, 1 au milieu, 2 devant.
Nous poserons un coef de réduction plancher (var coefPers).
Par exemple : au plus petit, les clips seront à 80% de leur taille d'origine.
Pour un sinus+1 qui vaut de 0 à 2 on veut un coef qui vaut de 0.8 à 1
var s:Number=Math.sin(ang)+1; var coef:Number=s/2*(1-coefPers)+coefPers; elt.scaleX=elt.scaleY=coef;
Il faut intégrer ces quelques lignes aussi bien à la construction du carrousel qu'à la fonction tourne (on pourrait en faire une fonction).
le code tout entier :
var rayonX:int=200; var rayonY:int=100; var nbElt:int=16; var vitesse:Number; var coefPers:Number=0.8; // ============== carrousel =========================== var carrousel:MovieClip=new MovieClip(); carrousel.x=250; carrousel.y=250; addChild(carrousel); carrousel.buttonMode=true; carrousel.addEventListener(MouseEvent.MOUSE_OVER,lance); carrousel.addEventListener(MouseEvent.MOUSE_MOVE,modifVitesse); function lance(me:MouseEvent) { if (! stage.hasEventListener(Event.ENTER_FRAME)) { stage.addEventListener(Event.ENTER_FRAME,tourne); } } function modifVitesse(me:MouseEvent) { vitesse=me.currentTarget.mouseX/rayonX/10; } //========= Vignettes =================== var i:int; var pasAngulaire:Number=Math.PI*2/nbElt; var decalAngDep=- Math.PI/2; for (i=0; i<nbElt; i++) { var elt:Elt= new Elt(); elt.name="elt"+i; // répartition sur circonférence selon angle var ang:Number=i*pasAngulaire+decalAngDep; elt.ang=ang; elt.x=Math.cos(ang)*rayonX; elt.y=Math.sin(ang)*rayonY; // Echelle // récupérer une valeur positive var s:Number=Math.sin(ang)+1; var coef:Number=s/2*(1-coefPers)+coefPers; elt.scaleX=elt.scaleY=coef; // Empilement if (Math.cos(ang)>0) { carrousel.addChild(elt); } else { carrousel.addChildAt(elt,0); } elt.txt.text=String(i); } function tourne(e:Event) { var i:int; var elt:MovieClip; for (i=0; i<nbElt; i++) { elt=MovieClip(carrousel.getChildByName("elt"+i)); elt.ang+=vitesse; var ang=elt.ang; // Position x/y elt.x=Math.cos(ang)*rayonX; elt.y=Math.sin(ang)*rayonY; // Echelle // récupérer une valeur positive de 0 à 2 var s:Number=Math.sin(ang)+1; var coef:Number=s/2*(1-coefPers)+coefPers; elt.scaleX=elt.scaleY=coef; // Gestion des plans var cosArrondi:int=Math.cos(ang)*10; if (cosArrondi==0) { if (Math.sin(ang)<0) { carrousel.setChildIndex(elt,0); } else { carrousel.setChildIndex(elt,nbElt-1); } } } vitesse*=0.99; if (Math.abs(vitesse)<0.005) { stage.removeEventListener(Event.ENTER_FRAME,tourne); } }
Utiliser des clips différents de la bibliothèque
On peut considérer que c'est fini, pour le principe de fonctionnement. La seule chose qui pourrait manquer c'est comment faire en sorte d'attacher non pas de multiples instances d'un même clip, mais bel et bien des clips différents stockés dans la bibliothèque.
La solution passe par la méthode getDefinitionByName. Je raconte ici comment elle fonctionne pour qui la découvre;-)
Il vous faudra donc :
Créer autant de symboles clips que nécessaire dans la bibliothèque,
les nommer avec un suffixe d'incrément (au hasard : Elt0, Elt1, Elt2…),
cocher la case exporter pour actionScript.
… et modifier la première ligne de la boucle de création du carrousel
for (i=0; i<nbElt; i++) { var classeVignette:Class=getDefinitionByName("Elt"+i) as Class; var elt:MovieClip = new classeVignette();
Pour ce parti pris de développement c'est fini.
Une autre voie
D'autres préfèrent attacher un écouteur enterFrame à chaque clip. Monsieur_SPI a choisi d'illustrer le thème de cette façon, voici les sources :
Notons au passage le tutoriel de Monsieur Spi sur la réalisation d'un carrousel avec un moteur 3D: Débuter avec Papervision - Un Carrousel
Lilive - le 22/03/2010
Modifier la perspective
En bonus, voici une toute dernière petite sophistication (surtout pour jouer, parce que je ne suis pas certaine que ce soit très utile): faire en sorte qu'on puisse saisir le carrousel et le faire pivoter.
Cette partie du tutoriel a été mise à jour par mes soins. Excusez les ruptures de style!
Lilive - le 25/05/2010
Modifier rayonY
Dans la pratique il s'agit de modifier la valeur de rayonY
selon la position y de la souris sur le carrousel (mouseY).
• Quand la souris monte il faut diminuer rayonY
.
• Quand la souris descend il faut augmenter rayonY
.
Nous devons donc intercepter le moment où l'on clique sur le carrousel (l'événement MOUSE_DOWN
) afin de commencer à modifier rayonY
en fonction des déplacements de la souris, et jusqu'à ce qu'on relâche la souris.
Intercepter le clic:
carrousel.addEventListener(MouseEvent.MOUSE_DOWN,enfonce);
Au clic, nous devons surveiller les déplacements de la souris, et aussi le moment où elle sera relâchée:
function enfonce(me:MouseEvent) { stage.addEventListener(MouseEvent.MOUSE_MOVE,perspective); stage.addEventListener(MouseEvent.MOUSE_UP,lache); }
Pourquoi mettre les écouteurs sur l'objet stage
et pas sur le carrousel lui-même? Parce-que tant que le bouton de la souris est enfoncé, nous devons surveiller ses déplacements, même si elle n'est plus sur le carrousel.
releaseOutside
pour surveiller le relâchement du bouton. Mais en AS3 il n'existe plus. En écoutant le MOUSE_UP
sur stage
nous sommes sûrs d'attraper l'évènement, où que soit le pointeur de la souris à ce moment.
Nous devons maintenant faire varier rayonY
en fonction des déplacements de la souris. Pour ceci nous allons créer une nouvelle variable precedentMouseY
qui va nous servir à nous souvenir du dernier emplacement qu'à pris la souris. Nous la déclarons au début du code:
var precedentMouseY:int;
Et nous y mettons la valeur de mouseY
au moment du clic:
function enfonce(me:MouseEvent) { stage.addEventListener(MouseEvent.MOUSE_MOVE,perspective); stage.addEventListener(MouseEvent.MOUSE_UP,lache); // On mémorise la position actuelle de la souris precedentMouseY = mouseY; }
Maintenant nous pouvons écrire la fonction perspective
qui écoute les déplacements de la souris et modifie rayonY
:
function perspective(me:MouseEvent) { // Calcul du nombres de points dont s'est déplacée la souris var deplacementSouris:int = mouseY - precedentMouseY; // Modification du rayon en fonction de ce déplacement rayonY += deplacementSouris; // On mémorise de nouveau la position actuelle de la souris precedentMouseY = mouseY; }
Pour savoir comment la souris s'est déplacée sur l'axe y on calcule:
deplacementSouris = carrousel.mouseY - precedentMouseY
Si la souris est descendue de 10 pixels par exemple, mouseY
vaut 10 de plus que precedentMouseY
, et deplacementSouris
vaut donc 10.
Si la souris est montée de 20 pixels, deplacementSouris
vaudra -20.
Il nous suffit donc de modifier rayonY
en y ajoutant la valeur de deplacementSouris
. Quand la souris descendra rayonY
augmentera, et quand la souris montera, rayonY
diminuera, c'est bien ce que nous espérions. A chaque fois que la souris bougera, la fonction perspective
répercutera son déplacement sur le rayonY
du carrousel.
La dernière ligne mémorise de nouveau la dernière position de la souris, pour le prochain appel de perspective
.
Il nous reste à arrêter d'écouter le déplacement de la souris quand on relâche le bouton:
function lache(me:MouseEvent) { stage.removeEventListener(MouseEvent.MOUSE_MOVE,perspective); stage.removeEventListener(MouseEvent.MOUSE_UP,lache); }
A ce stade, c'est fonctionnel, et vous pouvez tester. Mais il est possible de faire varier rayonY
bien au-delà du moment où notre carrousel ressemble à quelque chose. Nous allons donc corriger cela en “bornant” les valeurs possibles:
function perspective(me:MouseEvent) { var deplacementSouris:int = mouseY - precedentMouseY; rayonY += deplacementSouris; if (rayonY > 120) rayonY = 120; // Maximum autorisé = 120 if (rayonY < 0) rayonY = 0; // Minimum autorisé = 0 precedentMouseY = mouseY; }
Mais il reste un vilain bug!
Le voyez-vous?
Le bug
Le bug, c'est que si on essaie de modifier la perspective alors que le carrousel n'est pas en mouvement, on ne voit pas le résultat. C'est normal, puisque la fonction tourne
n'est appelée que lorsque le carrousel tourne, et que c'est elle qui place les vignettes en fonction de rayonY
. Donc si le carrousel ne tourne pas, rayonY
est bien modifié, mais on ne voit pas le changement avant que le carrousel se remette à tourner. On va donc séparer rotation et affichage en 2 fonctions distinctes:
La fonction qui modifie l'angle où sont les vignettes sur le cercle:
function tourne(e:Event) { // On ne retient de notre précédente fonction tourne() que la partie qui modifie l'angle des vignettes: var i:int; var elt:MovieClip; for (i=0; i<nbElt; i++) { elt=MovieClip(carrousel.getChildByName("elt"+i)); elt.ang+=vitesse; } // et à la fin on appelle la fonction de placement place(); }
La fonction qui place les vignettes en fonction de leur angle:
function place() { // C'est notre précédente fonction tourne(), moins la variation d'angle des vignettes var tbVignettes:Array=new Array(); var i:int; var elt:MovieClip; for (i=0; i<nbElt; i++) { elt=MovieClip(carrousel.getChildByName("elt"+i)); var ang:Number=elt.ang; var sinAng:Number=Math.sin(ang); elt.sin=sinAng; elt.x=Math.cos(ang)*rayonX; elt.y=sinAng*(rayonY); tbVignettes.push(elt); var s:Number=Math.sin(ang)+1; var coef:Number=s/2*(1-coefPers)+coefPers; elt.scaleX=elt.scaleY=coef; } tbVignettes.sortOn("sin",Array.NUMERIC); for (i=0; i<nbElt; i++) { elt=MovieClip(carrousel.getChildByName(tbVignettes[i].name)); carrousel.addChild(elt); } vitesse*=0.99; if (Math.abs(vitesse)<0.001) { stage.removeEventListener(Event.ENTER_FRAME,tourne); } }
Et maintenant on peut demander, dans la fonction perspective
, à afficher les vignettes de nouveau dans le cas où le carrousel ne tourne pas:
function perspective(me:MouseEvent) { var deplacementSouris:int = mouseY - precedentMouseY; rayonY += deplacementSouris; precedentMouseY = mouseY; if (rayonY > 120) rayonY = 120; if (rayonY < 0) rayonY = 0; if (!stage.hasEventListener(Event.ENTER_FRAME)) place(); }
Le retour de Monsieur Propre
Maintenant qu'on a une fonction qui place les vignettes, on peut sauter le pas et se débarrasser de la partie placement dans la boucle qui crée les clips. Comme cela il n'y aura plus de code en double, et des changements ultérieurs seront plus faciles à faire, si besoin.
//========= Vignettes =================== var i:int; var pasAngulaire:Number=Math.PI*2/nbElt; var decalAngDep=- Math.PI/2; for (i=0; i<nbElt; i++) { var classeVignette:Class=getDefinitionByName("Elt"+i) as Class; var elt:MovieClip = new classeVignette(); elt.name="elt"+i; // répartition sur circonférence selon angle var ang:Number=i*pasAngulaire+decalAngDep; elt.ang=ang; // Plus besoin ici de calculer x, y, scale, et l'ordre d'affichage. On se contente de les ajouter au carrousel: carrousel.addChild(elt); } // Et on appelle une première fois la fonction de placement: place();
Sources
Charger les images dynamiquement, d'après un fichier XML
Décliner
Ce qu'il faut retenir de ce tuto, c'est que ces fonctions sin et cos de la classe Math sont en fin de compte bien utiles à chaque fois qu'il s'agit de se débrouiller avec, soit des ellipses, soit des valeurs qui doivent varier en “faisant ascenseur”.
Comprendre : je passe de tout petit à tout gros avant de redescendre à tout petit (bon ! oui, ça s'appelle une sinusoïdale… c'est peut-être pas par hasard
)
A titre d'exemple, vous pourrez vous entraîner à faire un menu de type “dock”, car à bien y regarder, l'échelle des boutons va de tout petit (pour les plus éloignés de la souris), à tout gros (pour le bouton survolé).
Le bouton survolé a une distance à la souris de 0 (ou s'approchant), maintenant qu'on sait que cos(0) renvoie 1…
Les sources prennent le parti d'une classe, nommée Dock, avec une fonction statique qui
• renvoie un objet menu, ainsi peut on gérer autant de menus que souhaité sur un même document en toute liberté
• attend un tableau de nom de liaison de symboles et une amplitude (distance à la souris sur laquelle l'effet zoom est appliqué)
public static function creeDock(pTbElt:Array,pAmplitude:int):MovieClip
Voici le code du .fla qui a généré ce .swf de démo
// n'oubliez pas ! :) import Dock; //Un tableau dont les éléments sont des chaines valant le nom de liaison des symboles de bibliothèque var i:int; var tElt:Array=new Array(); for (i=0; i<10; i++) { tElt.push("Elt"+i); } // La fonction creeDock qui attend un tableau, gère une amplitude de 200, // et renvoie un objet MovieClip qui est le menu var m:MovieClip=Dock.creeDock(tElt,200); // Position m.x=50; m.y=100; // Ecouteur m.addEventListener(MouseEvent.CLICK,jeClique); // Ajouter à la liste d'affichage addChild(m); // Une autre avec une amplitude de 400 pour comparer var m2:MovieClip=Dock.creeDock(tElt,400); m2.x=50; m2.y=300; m2.addEventListener(MouseEvent.CLICK,jeClique); addChild(m2); // Notez l'utilisation de target, pour récupérer le bouton cliqué function jeClique(me:MouseEvent) { txtSortie.text=String(me.target); }
Si vous constatez un effet de clignotement quand la souris quitte le menu… C'est le moment ou jamais de relire la différence entre MouseOut et RollOut
Amusez vous bien
