Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Carrousel pas à pas

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Compatible Flash CS3. Cliquer pour en savoir plus sur les compatibilités.Par nataly (Nataly), le 14 mars 2010
  • Révision : lilive - 25/05/2010

Bonjour :)

Je me propose de décomposer au pas à pas la conception et le développement de ce type de carrousel.

Pour suivre ce tuto (d'un point de vue AS3), il vous faudra :
• 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 8-)

A la fin du parcours, vous trouverez au chapitre Décliner, un menu de type Dock qui met en œuvre les mêmes principes.

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

(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… :mrgreen: 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.

:arrow: Ceux qui veulent comprendre le pourquoi (je ne saurais les en blâmer ;-)), trouveront ici un tuto qui traite le sujet.
:arrow: 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 :D
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

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.


Si vous voulez creuser le principe, je vous invite à consulter ce tuto, certes en AS2 mais ça change peu de choses quant aux mises en en application.


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;

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

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)

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.


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);
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Trop fort ! On l'a :D


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;
	}
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

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 :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

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);
	}
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.


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.

:idea: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.

Ceux qui viennent d'AS2 auraient eu le réflexe d'utiliser le 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;
}
C'est mieux.
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

Edit par Lilive:

Thade a proposé des variantes du carrousel ici et ici, qui permettent notamment de charger des images externes pour les vignettes. Les images à charger sont listées dans un fichier XML.
Merci Thade :D !

Et dans la foulée Nataly a créé une deuxième page pour expliquer tout cela:

Pour combiner carrousel, fichier XML, et gestion du clic sur les vignettes, c'est page suivante.



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… :mrgreen: 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… :idea:

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

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

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 8-)

Voilà les sources .as, ocazou ;)
dock.as

Amusez vous bien :)