Outils pour utilisateurs

Outils du site


issue152:inkscape

Last time, we looked at the “transform” attribute, particularly with regard to its frequent appearances in Inkscape files. In this instalment, and the next, we’re going to combine this information with some of the JavaScript we’ve covered in previous months to show how you might animate the moving, turning, skewing and scaling of an SVG element. Once again we’ll start off with a very simple SVG file (top right).

La dernière fois, nous avons regardé l'attribut « transform », en particulier par rapport à ses fréquentes apparitions dans les fichiers d'Inkscape. Dans cet article, et dans le prochain, nous allons combiner cette information avec un peu de JavaScript, dont nous avons parlé dans les mois précédents, pour vous montrer comment vous pourriez animer le déplacement, la rotation, la déformation et le changement d'échelle d'un élément SVG.

Une fois encore, nous commencerons avec un fichier SVG très simple (en haut à droite).

As usual, we’ve got a viewbox of 100×100 units, but, this time, I’ve added a background <rect> to make it clearer where the extents of the canvas are when the file is loaded into a web browser. The element we’re going to animate is the red rectangle, which is inside a group (<g>). The group has a transform attribute on it, containing a series of functions that will be applied to its content. In this case, they each have no effect – a rotation of 0 doesn’t turn the shape at all, whilst a scale factor of 1 leaves it at its original size. Strictly speaking, they’re not necessary at all at this point, but they are present to show you the format of the string we’ll be creating with our animation code.

Comme d'habitude, nous trouvons une viewbox de 100×100 unités, mais, cette fois, j'ai ajouté un <rect> en arrière-plan pour clarifier la position des limites du canevas quand le fichier est chargé dans un navigateur Web. L'élément que nous allons animer est un rectangle rouge, qui est à l'intérieur d'un groupe (<g>). Le groupe possède un attribut transform, contenant une série de fonctions qui seront appliquées à son contenu. Dans notre cas, elles n'ont pas d'effet - une rotation de 0 ne fait pas du tout tourner la forme, de même qu'un facteur d'échelle de 1 la laisse à sa taille d'origine. Strictement parlant, elles ne sont pas du tout nécessaires à ce stade, mais elles sont là pour vous montrer le format de la chaîne que nous créerons avec notre code d'animation.

The rectangle itself bears a little explanation. In an SVG document, the y-axis runs down the page, and the x-axis runs from left to right. So the origin – the 0,0 point in the image – is at the top-left of the page. The position of a <rect> is also based on its top-left corner. Therefore, setting the “x” and “y” attributes to 0 would position the rectangle at the top left of the screen. But, for this code, I want to pretend that the “x” and “y” coordinates correspond to the center of the rectangle. This means offsetting its position by half the width to the left, and half the height upwards. Since it’s 20 units on each side, the transform attribute with a translate(-10, -10) does the job nicely. Basically, we’ve moved the rectangle’s reference point from the top-left corner to the center. With that manipulation in place, using 50 for the “x” and “y” coordinates puts it right in the middle of the image.

Le rectangle lui-même nécessite un peu d'explication. Dans un document SVG, l'axe y descend le long de la page et l'axe x va de la gauche vers la droite. Ainsi, l'origine - le point 0,0 de l’image - est en haut à gauche de la page. La position d'un <rect> est aussi basée sur ce coin en haut à gauche. Par conséquent, paramétrer les attributs « x » et « y » à 0 positionnerait le rectangle en haut et à gauche de l'écran. Mais, pour ce code, je veux faire comme si les coordonnées « x » et « y » correspondent au centre du rectangle. Ça signifie qu'il faut décaler sa position de la moitié de la largeur de l'écran vers la gauche, et de la moitié de la hauteur vers le haut. Comme il y a 20 unités de chaque côté, l'attribut transform avec un translate (-10, -10) fait bien l'affaire. En gros, nous avons déplacé le point de référence du rectangle du coin en haut à gauche au milieu. Cette manipulation étant réalisée, en utilisant 50 pour les coordonnées de « x » et « y », il se trouve pile au milieu de l'image.

There’s one other line in the file – a <script> tag that references an external document. For the sake of simplicity, we’re going to keep our code in a separate JavaScript file, avoiding problems with character escaping, and meaning that we don’t need to make any further changes to the SVG file. The only requirement is that the JS file is kept in the same directory as the SVG file, since I’ve used only a filename, not a relative path or absolute URL. The JS file itself, “transform.js”, contains just a single line at this point: alert(“Hello World”); By loading the page in a web browser we can now easily confirm that we have a red square in the middle of a grey square, and that a message pops up from our JavaScript file to indicate that it’s being loaded correctly. Now we can move on to some real code.

Il y a une autre ligne dans le fichier, une balise <script> qui fait référence à un document externe. Par souci de simplicité, nous allons conserver notre code dans un fichier JavaScript séparé, évitant les problèmes avec l'échappement de caractères et signifiant que nous n'aurons pas à faire des changements ultérieurs dans le fichier SVG. La seule exigence est que le fichier JS soit conservé dans le même dossier que le fichier SVG, car je n'ai mis que le nom du fichier, pas son chemin relatif ou son URL absolue. Le fichier JS lui-même, « transform.js », contient seulement une seule ligne pour le moment :

alert(“Hello World”);

En chargeant la page dans un navigateur Web, nous pouvons facilement confirmer que nous avons un rectangle rouge au milieu d'un carré gris, et qu'un message apparaît depuis notre fichier JavaScript pour indiquer qu'il a été chargé correctement. Maintenant, passons à du vrai code.

Based on the approach from part 90 of this series, we’re going to create a single function that updates the transform element for each frame that the browser renders. The function will receive a timestamp, and use that to determine how long the animation has been running, and therefore what values should be put into the transform element’s functions for that particular point in time. To begin with, we’ll just animate one of the properties: rotation. Replace the alert() call in the JavaScript file with this (right). Much of this looks similar to code we’ve seen previously, but there are enough differences to warrant a step-by-step walkthrough.

Basée sur l'approche présentée dans la partie 90 de cette série, nous allons créer une seule fonction qui met à jour l'élément transform à chaque trame que le navigateur affiche. La fonction recevra un horodatage et l'utilisera pour déterminer depuis combien de temps l'animation se déroule et, par conséquent, quelles valeurs doivent être mises dans les fonctions de transformation de l'élément à ce moment particulier.

Pour commencer, nous n'allons animer qu'une seule propriété : la rotation. Dans le fichier JavaScript, remplacez l'appel alert() par ceci (à droite).

En grande partie, ça ressemble à du code que nous avons vu précédemment, mais il y a assez de différences pour nécessiter une analyse point par point.

We start by declaring a global variable called “group”. Previously, we’ve used the “var” keyword to do this, but modern JS has mostly replaced that with “let” (for variables that will change), and “const” (for those that won’t). We’ve used “var” when working in the console as it won’t throw an error if you try to run the same line twice – as “const” would do. But, as we’re creating a separate JS file here, we’ll stick to convention. In this case, the “group” variable will eventually hold a reference to the <g> element, but as that’s not necessarily available as soon as the page loads, we’ll declare it using “let” and update the value later.

Nous commençons par déclarer une variable globale appelée « group ». Précédemment, nous avons utilisé le mot-clé « var » pour faire ça, mais le JS moderne l'a en général remplacé par « let » (pour les variables qui changeront) et « const » (pour celles qui sont fixes). Nous avons utilisé « var » lorsque nous travaillions à la console car il ne renvoie pas d'erreur si on essaie de lancer deux fois la même ligne, ce que ferait « const ». Mais, comme ici nous créons un fichier JS séparé, nous nous tiendrons à la convention. Dans ce cas, la variable « group » fera référence à l'élément <g> , mais puisqu'il n'est pas toujours disponible dés le chargement de la page, nous le déclarerons en utilisant « let » et mettrons la valeur à jour plus tard.

Later doesn’t take long to arrive. The very next line of code causes our initialise() function to run just before the next frame is drawn, giving the browser time to render the content so that our <g> element actually exists in the document structure before we use it. The initialise() function itself does these things: • Set our “group” global variable as a reference to the <g> element. • Set up some JS properties on the element. First is the initial timestamp, which will be used as the basis for calculating the timings in the animate() function. • The only other property we’re creating for now is the time it should take for the square to do one rotation, in seconds. • Finally, we have another call to getAnimationFrame() which will start the actual animation running.

Plus tard n'arrive pas si longtemps après. La ligne suivante de code fait que la fonction initialise() est lancée juste avant que la trame suivante soit dessinée, donnant le temps au navigateur de rendre le contenu de sorte que notre élément <g> existe vraiment dans la structure du document avant que nous l'utilisions. La fonction initialise() elle-même fait ceci : ••Paramétrer notre variable globale « group » comme référence à l'élément <g>. ••paramétrer quelques propriétés JS sur l'élément. L'horodatage initial en premier, qui sera utilisé comme base de calcul des temps dans la fonction animate(). ••La seule autre propriété que nous créons pour le moment est le temps en secondes que devrait mettre le carré pour faire une rotation. ••Enfin, nous avons un autre appel à getAnimationFrame() qui démarrera le véritable lancement de l'animation.

Now we get to the animation code itself, in the form of the animate() function. We begin by getting a reference to the animation properties we set up previously, and storing it with a more convenient name. We can use “const” instead of “let” here as the value we assign doesn’t get changed within this function. The next group of lines just calculates the value, in degrees, that we need to rotate the square by. We get the amount of time the animation has been running for, by subtracting the initial timestamp from the current one, then divide the value by 1000 to convert from milliseconds to seconds. By dividing 360 by the desired rotation time we find the amount of rotation we need to perform every second; multiplying that value by the amount of time we’ve been running for gives a total value for the number of degrees to rotate by.

Maintenant, nous avons le code de l'animation elle-même, sous la forme de la fonction animate(). Nous commençons par faire référence aux propriétés de l'animation que nous avons paramétré avant, en l'enregistrant sous un nom plus pratique. Ici, nous pouvons utiliser « const » à la place de « let » car la valeur que nous assignons n'est pas modifiée dans cette fonction.

Le groupe de lignes suivant calcule simplement la valeur, en degrés, de la rotation que nous devons appliquer au carré. Nous récupérons le durée depuis le lancement de l'animation, en soustrayant l'horodatage initial de l'actuel, puis divisons la valeur par 1 000 pour convertir les millisecondes en secondes. En divisant 360 par le temps de rotation désiré, nous trouvons la quantité de rotation que nous devons réaliser à chaque seconde ; en la multipliant par la durée depuis le début de la rotation, nous obtenons le nombre total de dégrés de la rotation.

After the first rotation has completed, the calculated value will be larger than 360. That’s not actually a problem – the browser will happily do the right thing for you in this case – but I prefer to be a little explicit about what’s happening. That explains the last line of this block, where we use the modulus operator (%) to get the value that remains after dividing the total angle by 360. This has the effect of normalising the rotation angle so it never goes above 360, which can make it easier to see what’s happening if you need to log the value out, or if you view it live in the developer tools.

Une fois le premier tour terminé, la valeur calculée sera plus grande que 360. Ce n'est pas vraiment un problème ; dans ce cas, le navigateur sera heureux de faire les choses correctement à votre place, mais je préfère être un peu explicite sur ce qui se passe. Cela explique la dernière ligne du bloc, où nous utilisons l'opérateur modulo (%) pour obtenir le reste de la division de l'angle total par 360. Cela a un effet de normalisation de l'angle de rotation de façon à ce qu'il ne dépasse jamais 360°, ce qui facilite la vue de ce qui se passe si vous avez besoin d'enregistrer la sortie ou si vous la regardez en direct dans les outils du développeur.

The penultimate line uses setAttribute() to update our transform attribute with a new value. The value itself is a template string, delimited by backticks (`…`). They’re not always as easy to spot in code as the more usual quotes and double-quotes, so make sure you don’t overlook them. Within a template string any content inside a ${} block will be evaluated as JavaScript, and the result will be placed into the string. For our purposes, this means we can use ${angle} to inject the value of the “angle” variable into the string, without having to perform a lot of concatenation. We’re using the three-value form of rotate() in order to provide coordinates for the center of rotation (50, 50) – without it the square will rotate about the top-left corner of the page.

L'avant-dernière ligne utilise setAttribute() pour mettre à jour notre attribut transform avec une nouvelle valeur. La valeur elle-même est une chaîne modèle, délimitée par des apostrophes inversées (`…`). Les distinguer dans le code n'est pas toujours aussi facile que les apostrophes ordinaires ou les apostrophes doubles ; aussi, assurez-vous de ne pas les négliger. Dans une chaîne modèle, tout le contenu d'un bloc ${} sera évalué comme étant du JavaScript, et le résultat sera placé dans la chaîne. Pour nos besoins, ça signifie que nous pouvons utiliser ${angle} pour injecter la valeur de la variable « angle » dans la chaîne, sans avoir à réaliser beaucoup de concaténations. Nous utilisons la forme de rotate() à trois valeurs pour fournir les coordonnées du centre de rotation (50, 50), sans cela, le carré tournera autour du coin en haut à gauche de la page.

The last line simply queues up another call to the animate() function, as we’ve seen previously. Load the SVG file into a web browser and, if everything is correct, you should see the square spinning around in the middle of the page. Press F12 in the browser to open the developer tools, and select the tab labelled “Inspector” (Firefox) or “Elements” (Chrome/Chromium) – it’s usually the leftmost tab on the bar. You should see the structure of your SVG file, with the <g> element visible, and the value of the transform attribute updating as the square rotates. Notice how the first number never goes above 360; try replacing the ${angle} tag with ${totalAngle} and see what difference it makes to the rotation and to the attribute’s value.

La dernière ligne enchaîne simplement sur un autre appel à la fonction animate(), comme nous l'avons vu plus haut. Chargez le fichier SVG dans le navigateur Web et, si tout est correct, vous devriez voir le carré tourner autour du milieu de la page. Appuyez sur F12 dans le navigateur pour ouvrir les outils du développeur et sélectionnez l'onglet intitulé « Inspecteur » (Firefox) ou « Éléments » (Chrome/Chromium), c'est en général l'onglet le plus à gauche dans la barre. Vous devriez voir la structure de votre fichier SVG, avec l'élément <g> visible et la valeur de l'attribut transform mise à jour au fur et à mesure que le carré tourne. Notez que le premier nombre ne dépasse jamais 360 ; essayez de remplacer la balise ${angle} par ${totalAngle} et regardez la différence pour ce qui est de la rotation et la valeur de l'attribut.

Rotation is pretty straightforward because we have to deal with only an ever increasing number. If we exceed a full rotation then we either normalise the number, or let the browser do it for us. The other transform functions are a little more tricky: skewX and skewY expect a value between -90 and +90 (though the extreme ends of the range distort the object so much that they’re not very useful); translate can take any number, but there’s only a limited range that makes sense within the confines of our 100×100 viewbox; scale has a similar practical limit. For all these transform functions, therefore, we want to animate back and forth between two values. This means creating three properties for each thing we want to animate, for the lower limit, upper limit and duration. Here’s how the group.animProperties object might be extended to also include skewX, for example (note the addition of a comma after the rotationDuration property, as this is no longer the last item in the object). Shown top right.

La rotation est simple, car nous n'avons à traiter qu'un nombre qui croît indéfiniment. Si nous dépassons un tour complet, nous devons, soit normaliser le nombre, soit laisser le navigateur le faire pour nous. Les autres fonctions transform sont un peu plus compliquées : skewX et skewY attendent une valeur entre -90 et +90 (bien que les valeurs extrêmes distordent tellement l'objet qu'elles ne sont pas bien utiles) ; translate accepte n'importe quel nombre, mais il n'y a qu'une plage limitée qui a du sens dans un espace de 100×100 ; scale a une limite pratique similaire. Par conséquent, pour toutes ces autres fonctions transform, nous voulons faire une animation qui fait des aller-retour entre deux valeurs. Cela signifie la création de trois propriétés pour chaque chose que nous voulons animer, la limite basse, la limite haute et la durée. Voici comment l'objet group.animProperties pourrait être étendu pour inclure aussi skewX, par exemple (notez l'ajout d'une virgule après la propriété rotationDuration, car elle n'est plus le dernier élément de l'objet). Voir en haut à droite.

To go with the new property, we’ll also need an extra group of lines in the animation function, just after the corresponding lines for rotation, but before the call to setAttribute() (bottom left). We start by assigning props.skewDuration to a local variable, for no other reason than it gets used a lot, so we’ve given it a more convenient name. The second line subtracts the minimum value property from the maximum, to give us the total amount of possible skew. We’ll use this to work out what the current skew amount should be at any given timestamp.

Pour accompagner la nouvelle propriété, nous avons besoin d'un groupe de lignes supplémentaire dans la fonction d'animation, juste après les lignes correspondant à la rotation, mais avant l'appel à setAttribute() (en bas à gauche).

Nous commençons par assigner props.skewDuration à une variable locale, pour la seule raison qu'elle sera beaucoup utilisée ; aussi, nous lui donnons un nom plus pratique. La seconde ligne soustrait la propriété valeur minimum de celle maximum pour obtenir la quantité totale de la déformation possible. Nous l'utiliserons pour déterminer quelle devrait être la déformation actuelle à un moment donné.

The third line calculates the “position” along the animation for the current timestamp. We do this by taking the running time (calculated earlier, in the previous block), dividing it by the duration for this animation, then taking the remainder. This gives us a value that runs from zero to the duration value, then jumps back to zero before ramping up again on each iteration. Rather than running from zero to “duration” it’s more useful if we adjust this value to be a decimal from 0 to 1, which is achieved by dividing by the total duration. If we were to comment out the next few lines and jump to the last one, we would find that the animation cycles repeatedly from the minimum value to the maximum, jumping straight back to the minimum on each iteration. Plotting the values over time results in a “sawtooth” chart.

La troisième ligne calcule la « position » à l'horodatage actuel pendant l'animation. Nous le faisons en prenant le temps du parcours (calculé avant, dans le bloc précédent), en le divisant par la durée de cette animation, puis en prenant le reste. Ça nous donne une valeur qui grimpe de zéro à la valeur de la durée, puis qui redescend à zéro avant de remonter à chaque itération. Plutôt que de varier entre zéro et « duration » (durée), c'est plus utilisable si nous ajustons cette valeur sous forme d'une décimale entre 0 et 1, ce qu'on réalise en la divisant par la durée totale.

Si nous mettions en commentaires les quelques lignes qui suivent et passions à la dernière, nous verrions que l'animation va cycliquement de la valeur minimum au maximum, puis revient immédiatement au minimum à chaque itération. Le tracé des valeurs en fonction du temps donne une courbe en « dents de scie ».

For our animation, however, we want the value to transition linearly both up and down, without the sudden jump between iterations. What we want is a triangle wave: As you can see, on odd numbered iterations we want the animation to proceed as usual, but on the even numbered ones we want the position value to step downwards rather than upwards. In the code above this is done by creating a “skewXDirection” variable which holds the modulus of the current running time when divided by twice the duration. This value will ramp up from zero at the start of an odd iteration, through the duration value at the end of the odd iteration, continuing up to twice the duration value at the end of the subsequent even iteration. We’ve created another sawtooth wave, but this time running from zero to duration×2 over the course of two iterations.

Pour notre animation, cependant, nous voulons une valeur qui varie linéairement vers le haut et vers le bas, sans saut brusque entre les itérations. Nous voulons un tracé triangulaire :

Comme vous le voyez, pour un nombre impair d'itérations, nous voulons que l'animation se déroule comme d'habitude, mais pour un nombre pair d'itérations, nous voulons que la valeur de la position décroisse plutôt que croisse. Dans le code ci-dessus, c'est fait en créant une variable « skewXDirection » qui contient le modulo du temps de l'exécution en cours quand il est divisé par deux fois la durée. Cette valeur montera à partir de zéro au début d'une itération impaire, passant par la valeur de la durée à la fin de l'itération impaire, et continuant jusqu'à deux fois la valeur de la durée à la fin de l'itération paire suivante. Nous avons créé une autre courbe en dents de scie, mais, cette fois, le temps court de zéro à durationx2 au cours de deux itérations.

The “if” statement that follows checks to see if this direction value is greater than the duration: if it is then we must be on an even cycle. In that case the “skewXPosition” variable (which, if you recall, ranges from 0 to 1) is subtracted from 1, so as the animation progresses the final position value first steps upwards, then steps downwards, before the cycle repeats in a triangle wave pattern. The last step in calculating the actual value is to multiply the current position in the cycle by the total range of the animation, then add the minimum value to move the final result into the right range of numbers. Phew! That was a lot to take in for a few lines of code. If you find it easier to follow, try adding some console.log() lines amongst the code so you can see how the values change in the developer tools.

La déclaration « if » qui suit vérifie si cette valeur de direction est plus grande que la durée : si oui, alors nous sommes dans un cycle pair. Dans ce cas, la variable « skewXPosition » (qui, pour mémoire, varie de 0 à 1) est soustraite de 1, de sorte que l'animation progresse vers la position finale, d'abord en croissant, puis en décroissant, avant que le cycle ne se répète sous la forme d'une courbe triangulaire.

La dernière étape du calcul de la valeur réelle est pour multiplier la position actuelle dans le cycle par l'étendue totale de l'animation, puis d'y ajouter la valeur minimum pour replacer le résultat final dans la bonne tranche de nombres.

Ouf ! Ça fait beaucoup à prendre en compte en si peu de lignes de code. Pour vous permettre de suivre plus facilement, essayez d'ajouter quelques lignes console.log() au milieu du code ; ainsi, vous pourrez voir les changements de valeur dans les outils du développeur.

With our final value calculated, the last step is to update the transform attribute to hold both the rotate() and the skewX() functions. Extend the previous template string to this: Loading the file into the browser, you should see the square being skewed as it rotates. But you’ll also notice that our simple, constrained rotation in the middle of the screen has turned into a whirling dervish that swoops out of the bounds of our image before flying back in and then setting off into the distance once more. Next time we’ll discuss why this is happening, and finish this little animation by adding scaling and translation. In the meantime why not test your own understanding of this code by adding the necessary lines to make the shape also skew in the Y direction, at a different frequency to the skewX() effect.

Avec le calcul de notre valeur finale, la dernière étape est de mettre à jour l'attribut transform pour garder en même temps les fonctions rotate() et skewX(). Complétez la chaîne modèle précédente avec ceci :

En chargeant le fichier dans le navigateur, vous devriez voir le carré qui se déforme tout en tournant. Mais vous devriez noter aussi que notre simple rotation contrainte au milieu de l'écran s'est transformée en un derviche tournant qui descend hors des limites de notre image avant de revenir à l'intérieur et ensuite de redisparaître dans le lointain une nouvelle fois. La prochaine fois, nous verrons les raisons de ce comportement, puis finirons cette petite animation en ajoutant le changement d'échelle et la translation. Entre temps, pourquoi ne pas tester votre compréhension de ce code en ajoutant les lignes nécessaires pour que la forme se déforme sur l'axe Y, à une fréquence différente de l'effet skewX().

Traductions du cadre de la page 26 (à mettre en noir): ligne 7 Durée d'une rotation de 360 degrés ligne 9 Début du lancement de l'animation

Traductions du cadre de la page 28 en haut à droite (à mettre en noir): ligne 3 Temps pour un tour complet (360°)

issue152/inkscape.txt · Dernière modification : 2020/01/08 15:36 de auntiee