Proportionnalité pratique en Actionscript: Plus loin que la Règle de Trois
Introduction
En programmant, on rencontre souvent la situation suivante (faites glisser les curseurs de l'animation ci-dessous):
Si vous ne l'avez pas reconnue, laissez-moi le temps de vous la décrire avant de vous montrer des exemples plus convaincants:
- Une variable numérique A peut prendre des valeurs comprises entre les valeurs A1 et A2. Dans l'exemple ci-dessus A1 vaut 20 et A2 150.
- Une autre variable numérique B peut prendre des valeurs comprises entre les valeurs B1 et B2. Dans l'exemple B1 vaut 400 et B2 800.
- A et B sont liées dans leurs variations, c'est-à-dire prennent en même temps leurs valeurs extrêmes, et parcourent “en parallèle”, ou “à la même vitesse” leur plage de valeurs possibles.
- Quand une des deux variables change, l'autre variable doit changer aussi en accord avec la relation ci-dessus. Vous pouvez modifier avec la souris la valeur de A ou de B, l'autre variable s'ajustera automatiquement.
En pratique, on sera le plus souvent dans la situation de connaître les valeurs de A1, A2, B1, B2, de connaître la valeur de A et de chercher à calculer celle de B. Cela ne vous parle pas encore? Alors autres exemples:
Trois cas de figure
Il y a des chances que vous ayez déjà été confronté à quelque chose de similaire. Si parfois vous cafouillez un peu à trouver le petit calcul nécessaire pour passer de A à B, ce tutoriel est pour vous.
Une première formule
Pour commencer nos explications, oublions nos variables A et B et occupons-nous d'une variable X qui peut prendre une valeur entre 0 et MAX.
Exemple: MAX = 200, X = 100
on peut dire que X est à “la moitié” du chemin à faire entre 0 et MAX
On voit que si on calcule X / MAX on obtient 0.5
0.5 s'écrit d'ailleurs aussi 1/2
1/2 qui se lit “un demi”, est qui veut bien dire “la moitié” (et qui distinctement prononcé à haute voix à la terrasse d'un café aura certainement un résultat).
Autre exemple: MAX = 200, X = 50
Cette fois x est “au quart” du chemin à faire entre 0 et MAX
Si on calcule X / MAX on obtient 0.25
0.25 c'est aussi 1/4, qui se lit “un quart”, c'est bien ça.
Ce nombre qu'on calcule, X / MAX, c'est “le rapport” auquel est placé X entre 0 et MAX.
Ce rapport, nommons-le “ratio”, se calcule ainsi:
ratio = X / MAX
Comme X est compris entre 0 et MAX, ratio est toujours compris entre 0 et 1
Plus ratio est petit, plus X est proche de 0
Plus ratio est grand, plus X est proche de MAX
Si ratio est 0 alors X = 0
Si ratio est 1 alors X = MAX
Inversement, si on a les valeurs MAX et ratio et qu'on cherche à calculer X, il suffit de faire:
X = MAX * ratio
Exemple: MAX = 200 et ratio = 0.5
X = 200 * 0.5 = 100
Exemple: MAX = 200 et ratio = 0.25
X = 200 * 0.25 = 50
Voilà, nous avons ce qu'il nous faut pour réaliser la barre de progression du premier exemple:
Application pratique de la première formule
La façon de s'y prendre
Prenons comme variable X le nombre d'octets chargés. Pour être plus explicite nous allons l'appeler bytesLoaded plutôt que X.
Ici, la valeur maximale MAX est 2134. Posons la variable bytesTotal = 2134
Nous pouvons calculer ratio:
ratio = bytesLoaded / bytesTotal;
On veut maintenant que la barre de progression se remplisse en même temps.
Disons que la barre est un rectangle de 260 pixels, comme dans l'exemple ci-dessus. Cette fois, prenons comme variable X la largeur du rectangle de remplissage, et appelons-la barWidth. Sa valeur MAX est donc 260. Plutôt que MAX, appelons-la barMaxWidth.
On veut:
Que quand le nombre d'octets bytesLoaded est à 0, la barre soit vide (que barWidth vaille 0).
Que quand le nombre d'octets bytesLoaded est égal à bytesTotal, la barre soit pleine (que barWidth vaille barMaxWidth).
Que quand le nombre d'octets bytesLoaded est à la moitié, la barre soit pleine à moitié.
C'est-à-dire que bytesLoaded et barWidth ont le même ratio.
Comme nous avons déjà calculé ratio à partir de bytesLoaded et bytesTotal, nous pouvons calculer barWidth en faisant:
barWidth = barMaxWidth * ratio;
C'est aussi simple que ça.
La réalisation
Voici le tout mis en application pour réaliser la barre de préchargement d'une animation faite dans Flash Professional CS4. Ce code fonctionne si on le met sur la première image du scénario, et si on place sur la scène un symbole de clip nommé bar qui représente le remplissage de la barre à son maximum:
// On stoppe la lecture de l'animation jusqu'à ce que le chargement soit terminé stop(); // On mémorise la largeur maximale que peut prendre la barre de progression var barMaxWidth:int = bar.width; // Et on la met à 0 puisque le chargement commence à peine bar.width = 0; // On met en place les écouteurs qui vont permettre de suivre le chargement loaderInfo.addEventListener(ProgressEvent.PROGRESS, progressHandler); loaderInfo.addEventListener(Event.COMPLETE, completeHandler); // Au fur et à mesure que le chargement avance function progressHandler(pEvt:ProgressEvent):void { // On calcule le ratio à partir des octets var ratio:Number = pEvt.bytesLoaded / pEvt.bytesTotal; // On calcule la largeur de la barre à partir du ratio bar.width = barMaxWidth * ratio; } // A la fin du chargement function completeHandler( event:Event ) { // On se débarrasse des écouteurs loaderInfo.removeEventListener(ProgressEvent.PROGRESS, progressHandler) ; loaderInfo.removeEventListener(Event.COMPLETE, completeHandler) ; // Et on passe à la suite de l'animation gotoAndPlay(2); }
Pensez à utiliser la fonction “Simuler le téléchargement” pour admirer le résultat.
Et la règle de trois dans tout ça?
Certains d'entre vous se sont peut-être dit qu'on aurait pu se passer de ce raisonnement avec le ratio, et appliquer une simple règle de trois. C'est absolument vrai, puisque nos 2 variables prennent en même temps la valeur 0 et que nous sommes donc avec deux valeurs proportionnelles. Le résultat serait le même (heureusement), puisque le calcul
ratio = bytesLoaded / bytesTotal; barWidth = barMaxWidth * ratio;
peut se réduire en une ligne:
barWidth = barMaxWidth * bytesLoaded / bytesTotal;
Et c'est cette formule que nous aurions directement trouvée en appliquant la règle de trois. L'avantage de passer par le calcul d'un ratio, outre le fait d'amener une autre vision (donc compréhension) de la chose, est que nous pourrons nous débrouiller si les 2 variables ne prennent pas en même temps la valeur 0. C'est ce qui se passe dans les 2 autres exemples d'animations donnés au début, et que nous allons maintenant traiter.
Formule générale
Dans cet autre exemple on ne peut plus appliquer notre première formule, car les variables A et B ne sont pas à 0 en même temps. Elle ne sont pas proportionnelles, au sens strict. Si nous essayons de faire comme auparavant:
ratio = A / A2 B = B2 * ratio
Nous aurons ceci:
Quand le carré est à gauche, la transparence B n'est pas de 0.2 comme nous le voulions, mais de 0.12. D'ailleurs cela aurait été étonnant que ça marche, puisque notre calcul ne fait intervenir ni la valeur de A1, ni celle de B1.
De la variable au ratio
N'abandonnons pas notre ratio pour autant. L'idée est bonne: un nombre entre 0 et 1 qui nous indique à quel rapport se situe la variable A entre ses 2 extrêmes A1=30 et A2=260.
Pour nous ramener à une situation connue, nous allons poser
X = A - A1
Quand A prend sa valeur minimale A1=30, X vaut 30-30 c'est-à-dire 0.
Quand A augmente, X augmente.
Quand A approche de sa valeur maximale A2=260, X approche de sa valeur maximale MAX.
MAX = A2 - A1 = 260 - 30 = 230
Nous sommes revenu à la situation d'une variable X naviguant entre 0 et MAX, et nous pouvons calculer le ratio:
ratio = X / MAX
C'est-à-dire, en remplaçant X et MAX par leur formule:
ratio = (A - A1) / (A2 - A1)
Nous obtenons bien un ratio entre 0 et 1, tout va bien.
Du ratio à la variable
Maintenant nous allons devoir calculer l'opacité du carré à partir du ratio. Nous voulons que cette opacité B varie entre B1=0.2 et B2=1
Nous pouvons voir B comme la somme de 0.2 et d'une valeur X qui varierait entre 0 et 0.8. Cette valeur MAX=0.8 s'obtient par la formule:
MAX = B2 - B1 MAX = 1 - 0.2 = 0.8
Nous pouvons calculer X d'après le ratio trouvé:
X = ratio * MAX X = ratio * (B2 - B1) X = ratio * 0.8
Puis pour calculer l'opacité B nous n'avons plus qu'à rajouter B1:
B = X + B1
Sur une seule ligne cela donne:
X = ratio * (B2 - B1) B = X + B1 Soit B = ratio * (B2 - B1) + B1
Synthèse: D'une variable à l'autre
Pour calculer la valeur de B à partir de celle de A nous devons donc faire:
ratio = (A - A1) / (A2 - A1) B = ratio * (B2 - B1) + B1
C'est toujours la même logique. On trouve à quel rapport se situe la 1ère variable dans son intervalle de valeurs possibles, puis on affecte à la 2eme variable la valeur située au même rapport dans son propre intervalle.
Nous pouvons si nous voulons créer une fois pour toute la fonction qui fera le travail:
function mapValue(a:Number, a1:Number, a2:Number, b1:Number, b2:Number):Number { var ratio:Number = (a - a1) / (a2 - a1); return ratio * (b2 - b1) + b1; }
Et en voici une variante plus compacte qui fait l'économie de la création de la variable temporaire ratio:
function mapValue(a:Number, a1:Number, a2:Number, b1:Number, b2:Number):Number { return (a - a1) / (a2 - a1) * (b2 - b1) + b1; }
Exemple
Voici le code de l'animation d'exemple. square est l'occurrence du symbole de clip qui contient le dessin du carré:
// Valeurs extrêmes de l'abscisse du carré var xMin:int = 30; var xMax:int = 260; // Valeurs extrêmes de l'opacité var alphaMin:Number = 0.2; var alphaMax:Number = 1; // Rectangle dans lequel on autorise le carré à évoluer var bounds:Rectangle = new Rectangle(xMin, 30, xMax - xMin, 140) // On appelle une 1ere fois la fonction qui va définir l'opacité du carré d'après sa position adjustAlpha(); // Et on écoute le clic sur le carré square.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler); // Quand le bouton de la souris est enfoncé sur le carré function mouseDownHandler(e:MouseEvent):void { // On démarre le drag dans le rectangle de déplacements autorisés square.startDrag(false, bounds); // On écoute le déplacement de la souris et le relâchement du bouton stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler); stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); } // Quand le bouton est relâché function mouseUpHandler(e:MouseEvent):void { // On arrête le déplacement square.stopDrag(); // On enlève les écouteurs du déplacement et du relâchement stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler); stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); } // Quand la souris se déplace pendant le drag function mouseMoveHandler(e:MouseEvent):void { // On demande à définir l'opacité du carré d'après sa position adjustAlpha(); } // Calcule l'opacité du carré d'après sa position function adjustAlpha():void { var ratio:Number = (square.x - xMin) / (xMax - xMin); square.alpha = ratio * (alphaMax - alphaMin) + alphaMin; }
carre-mobile.fla
Bonus: Une classe toute faite
Suivant les idées de Dldler et de goabonga, voici une classe permettant de passer de A à B de plusieurs façons (voir les exemples fournis).
Miracles divers
Ce qui est beau avec les maths, c'est qu'il arrive que le résultat de nos recherches dépasse nos espérances. Mais si mais si, vous allez voir.
Nous avons raisonné à partir de valeurs d'exemples comme A1=30, A2=260, B1=0.2 et B2=1.
Cela nous aidait à y voir plus clair. Et nous en avons déduit une formule:
ratio = (A - A1) / (A2 - A1) B = ratio * (B2 - B1) + B1
Le raisonnement s'appuyait sur des notions intuitives, valides dans la mesure où nous avions des valeurs positives, et que A1 était inférieur à A2, ainsi que B1 inférieur à B2. Relisez les parties précédentes en prenant des exemples où ces valeurs peuvent être négatives, ou bien rangées dans l'autre sens, et vous verrez que la justesse du propos se fait largement moins évidente.
Le miracle, c'est que les formules que nous avons trouvées ainsi restent valides quelques soient les valeurs de A1, A2, B1 et B2. Dans tous les cas, si nous appliquons ces formules, le résultat sera correct, et les deux variables A et B prendrons en même temps leurs valeurs A1 et B1, ainsi que leurs valeurs A2 et B2.
Le miracle de l'inversion des valeurs
Essayez. Reprenons le code ci-dessus:
var xMin:int = 30; var xMax:int = 260; var alphaMin:Number = 0.2; var alphaMax:Number = 1; function adjustAlpha():void { var ratio:Number = (square.x - xMin) / (xMax - xMin); square.alpha = ratio * (alphaMax - alphaMin) + alphaMin; }
Et changeons le nom des variables:
var x1:int = 30; var x2:int = 260; var alpha1:Number = 0.2; var alpha2:Number = 1; function adjustAlpha():void { var ratio:Number = (square.x - x1) / (x2 - x1); square.alpha = ratio * (alpha2 - alpha1) + alpha1; }
Vous pouvez donner toutes les valeurs que vous voulez aux extrêmes, cela fonctionnera encore: quand le carré sera en x1, son opacité sera alpha1, quand il sera en x2 son opacité sera alpha2. Par exemple:
var x1:int = 30; var x2:int = 260; var alpha1:Number = 1; var alpha2:Number = 0.2;
Ou ce qui reviendra au même:
var x1:int = 260; var x2:int = 30; var alpha1:Number = 0.2; var alpha2:Number = 1;
Le troisième exemple donné tout en haut du tutoriel utilise lui aussi des valeurs inversées.
Le prodige de l'inversion des signes
Voici maintenant une variante qui utilise des valeurs négatives:
La rotation du carré se fait d'après l'abscisse du pointeur de la souris:
// Valeurs entre lesquelles on prend l'abscisse du pointeur en compte var x1:int = 80; var x2:int = 210; // Valeurs extrêmes de la rotation var alpha1:Number = 30; var alpha2:Number = - 30; // On appelle une 1ere fois la fonction qui va définir la rotation du carré d'après la position de la souris adjustRotation(); // Et on écoute le déplacement du pointeur stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); // Quand la souris se déplace function mouseMoveHandler(e:MouseEvent):void { // On demande à définir la rotation du carré d'après la position du pointeur adjustRotation(); } // Calcule la rotation du carré d'après la position du pointeur function adjustRotation():void { // On calcule la position du pointeur bornée entre x1 et x2 var x:Number = mouseX; if (x < x1) x = x1; if (x > x2) x = x2; // Puis on calcule la rotation var ratio:Number = (x - x1) / (x2 - x1); square.rotation = ratio * (alpha2 - alpha1) + alpha1; }
carre-rotation.fla
La magie de l'extension à l'au-delà
Un dernier rebondissement pour en finir: Les formules restent également valables au-delà des intervalles de valeurs définis par A1 et A2, et B1 et B2.
C'est-à-dire que A1, A2, B1, B2 ne sont pas obligatoirement des valeurs extrêmes pour les variables A et B. A et B peuvent en fait varier de moins l'infini à plus l'infini. Enfin, au bout d'un moment Flash protestera et vous parlera peut-être de dépassement de valeur, mais vous avez de la marge.
A quoi cela peut-il servir? Nous allons bien trouver un petit exemple. Hum…
J'appelle pad le rectangle gris “Survolez-moi”.
On sait que quand la souris est à gauche du pad, l'image doit aligner son bord gauche sur celui de la scène. Donc:
// Valeur 1 pour la variable A (mouseX) var xp1:Number = pad.x; // Valeur 1 pour la variable B (image.x) var xi1:Number = 0;
On sait que quand la souris est à droite du pad, l'image doit aligner son bord droit sur celui de la scène. Donc:
// Valeur 2 pour la variable A (mouseX) var xp2:Number = pad.x + pad.width; // Valeur 2 pour la variable B (image.x) var xi2:Number = - image.width + stage.stageWidth;
Cette fois nous ne nous contentons pas de faire varier image.x en fonction de mouseX, mais nous voulons également faire varier image.y en fonction de mouseY. Au final nous aurons donc:
// Bornes du pad var xp1:Number = pad.x; var xp2:Number = pad.x + pad.width; var yp1:Number = pad.y; var yp2:Number = pad.y + pad.height; // Valeurs de calage de l'abscisse de l'image var xi1:Number = 0; var xi2:Number = - image.width + stage.stageWidth; // Valeurs de calage de l'ordonnée de la photo var yi1:Number = 0; var yi2:Number = - image.height + stage.stageHeight;
Dans la fonction de placement, nous calculons la position de l'image en fonction des coordonnées du pointeur de la souris:
// On positionne l'image d'après la position de la souris function mouseMoveHandler(e:Event):void { var ratio:Number; // En abscisse ratio = (mouseX - xp1) / (xp2 - xp1); image.x = ratio * (xi2 - xi1) + xi1; // En ordonnée ratio = (mouseY - yp1) / (yp2 - yp1); image.y = ratio * (yi2 - yi1) + yi1; }
L'image est donc entièrement survolée quand on explore le pad avec le pointeur de la souris. Mais comme les coordonnées du pointeur peuvent prendre des valeurs qui vont au-delà de celles des limites du pad, le mouvement de l'image continue également, et elle sort de la scène, la laissant dégagée. De cette façon les autres éléments interactifs de l'animation sont bien visibles quand l'utilisateur en approche le pointeur.
Le code de ce .fla varie légèrement de celui donné ci-dessus afin que le déplacement de l'image soit bien fluide, mais ceci est une autre histoire.
Conclusion
Nous avons maintenant une méthode sûre qui permet de faire évoluer proportionnellement deux variables, à partir du moment où nous connaissons deux valeurs possibles pour chaque variable. Ceci suffira à résoudre bon nombre de situations type qu'on rencontre en programmant pour Flash. Enfin… au niveau de la conception mathématique en tout cas.
N'hésitez surtout pas à venir laisser commentaires et questions au sujet de ce tutoriel sur le forum. Le lien pour accéder à la discussion idoine est juste en dessous. Vous pouvez aussi y partager vos propres exemples mettant en jeu la proportionnalité. Et si vous avez trouvé cette page utile, un simple merci fait toujours très plaisir, rassure sur le fait qu'on n'a pas écrit pour rien, et donne envie de persévérer.
Lilive.
