Ceci est une ancienne révision du document !
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 fois dernière, 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 aovns parlé dasn 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 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. Das notre cas, elles n'ont pas d'effet - une rotation de 0 ne fait pas du tout tourné 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 pour 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 ne nécessite que 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'mage - 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 pouvoir affirmer que que 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 vars 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 aovns 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 <tag> qui fait référence à un document externe. Par souci de simplicité, nous allons conservé 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 est conservé dans le même dossier que le fichier SVG, car je n'aimis que le nom du fichier, pas son chemin relatif ou son URL absolue. Le fichier JS lui-même, « transform.js », contient seulement un seule ligne à cet instant :
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 seul 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 devraient ê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. Remplacez l'appel alert() dans le fichier Javascript par ceci (à droite).
En grande partie, ça ressemble au 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 les 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 il n'est pas disponible dés le chargementd e 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 de code juste à la suite entraîne que la fonction initialisae() est lancée juste apvant que la trame suivante soit dessinée, donnant le temps au navigateur de rendre le contenu de sorte que notre élément <g> existe vraiement dans la structure du document avant que nous l'itilisions. 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 focntion animate(). ••La seule autre propriété que nous créons pour le moment le temps que devrait être mis le carré pour faire une rotation en secondes. ••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 focntion animate(). Nous commençons par faire référence aux propriétés de l'animation que nous avons paramétré avant, et par l'enregistrer sous un nom plus pratique. Ici, nous pouvons utiliser « const » à la place de « let » car la valeur que nous assignons n'est pas modifiés 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 1000 pour convertir le 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 une valeur totale du nombre de dégrés de 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 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 fera heureusement les choses correctement à votre place - mais je préfère un peu explicite sur ce qui arrive. Cela explique la dernière ligne du bloc, où nous utilisons l'opérateur modulo (%) pour obtenir la valeur restante de la division de l'angle total par 360. Celle-ci 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 la nouvelle valeur. La valeur elle-même est une chaîne modèle, délimitée par des apostrophes inversées (`…`). Dans le code, ce n'est pas toujours aussi facile à les distinguer que les apostrophes ordinaires ou les apostrophes doubles ; aussi, assurez-vous de ne pas les négliger. Dans un chaîne modèle, tout le contenu dans 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.
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.
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.
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.
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.
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.
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.
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