Outils pour utilisateurs

Outils du site


issue150:inkscape

Ceci est une ancienne révision du document !


Over the past few months we’ve looked at ways to dynamically modify your SVG content when it’s running in a web browser, using JavaScript. By making these changes over a period of time, we can effectively use JS to animate our SVG files. We have looked at animation in this series before: in part 75 we looked at some simple CSS animations, then in parts 76 and 77 we moved on to SMIL animation. At the time I commented that SMIL was something of a dead-end technology, largely due to Microsoft never implementing it in any browser. But times have changed, and Microsoft have effectively given up on developing their own browser engine: shortly Microsoft Edge will begin using the same engine as Chrome, so should gain SMIL support as a side-effect. Whether or not this is enough to turn the tide for SMIL remains to be seen. Personally I think it’s a great technology for animating SVG particularly, but the browser vendors seem to have settled on CSS animations as the way to go – even though that technology still doesn’t cover all the use-cases that SMIL can handle.

Durant ces derniers mois, nous avons regardeé les différentes façons de modifier dynamiquement votre contenu SVG quand il tourne dans un navigateur Web, en utilisant Javascript. En faisant ces modifications sur un certain laps de temps, nous pouvons effectivement utiliser JS pour animer nos fichiers SVG.

Nous avons déjà une animation dans notre série : dans la partir 75, nous avons regardé des animations simples avec CSS, puis dans les parties 76 et 77, nous sommes passés à des animations avec SMIL. À ce moment-là, je'ai fait un commentaire pour indiquer que SMIL était une technologie en fin de vie, en grande partie parce que Microsoft ne l'avait jamais implémentée dnas aucun navigateur. Mais les temps ont changé et microsoft a réellement laissé tomber le développement de son propre moteur de navigation : dans peu de temps, Microsoft Edge commencera à utiliser le même moteur que Chrome, donnant par contre-coup un support à SMIL. Que ça suffise ou pas pour ça inverse les choses pour SMIL, ça reste à voir. Personnellement, je pense que c'est une superbe technologie, en particulier pour l'animation des SVG, mais les fournisseurs de navigateurs semblent considérer que les animations avec CSS sont la voie à suivre - même si cette technologie ne couvre pas tous les cas d'utilisation que SMIL peut traiter.

So we’re slightly stuck in limbo. SMIL offers huge power, but its time may be short. CSS animation is less powerful, but widely supported. However with JavaScript we can sort-of get the best of both worlds: as much power and flexibility as we need, in a way that has excellent cross-browser support. Of course things aren’t all rosy. When using SMIL or CSS animations you essentially take a pretty hands-off approach to things. Your input is a simple instruction: “Animate this object from A to B, over 5 seconds”. You don’t need to work out how the position of the object changes from one frame to the next, or worry about your animation imposing a heavy load on the machine. Instead you simply let the browser handle all the intermediate calculations – and the browser’s code is a lot faster at handling those things than anything you could write for yourself in JavaScript.

Ainsi, nous sommes un peu dans le flou. SMIL offre une puissance énorme, mais son temps est compté. Les animations avec CSS sont moins puissantes, mais largement supportées. Cependant, avec Javascript, nous pouvons avons, en quelque sorte, le meilleur des deux mondes : autant de puissance et de flexibilité que nous voulons, avec le fait qu'il a un excellent support multi-navigateur.

Bien sûr, les choses ne sont aps aussi roses. Quand nous utilisons des animations avec SMIL ou CSS, nous privilégions une approche plutôt désabusée des choses. Votre saisie est une simple instruction : « Animer cet objet de A à B, en 5 secondes ». Vous n'avez pas besoin de dire comment la position de votre objet changera d'une trame à l'autre, ou de vous inquiéter de ce que votre animation impose une forte charge à la machine. Au lieu de ça, vous laisser le navigateur gérer tous les calcules intermédiaires - et le code du navigateur est largement plus rapide pour gérer ces choses que vous pourriez écrire en Javascript.

So JS animation gives you flexibility, but at the expense of performance. Most of the time that won’t matter: the JavaScript engine in a modern browser is heavily optimised, so just moving an element or two around the screen isn’t likely to impose much of a burden. But if you start to animate a large number of objects, especially on a mobile device, you might find that your animations aren’t as smooth as they could be with the other technologies. Enough of the pros and cons, on with the code! Once again we’ll do all this in the browser’s developer tools, so you’ll need a super simple SVG file to start with the code shown below.

L'animation en JS vous donne aussi de la la flexibilité, mais au prix d'une moindre performance. Ça n'a pas d'importance la plupart du temps : le moteur de Javascript dans un navigateur moderne est fortement optimisé ; ainsi, le seul mouvement d'un ou deux éléments sur l'écran n'entraîne pas une grosse charge. Mais si vous commencez à animer un grand nombre d'objetd, particulièrement sur un dispositif mobile, vous pourriez trouver que vos animations ne sont pas aussi lisses qu'elles pourraient l'être avec d'autres technologies.

Arrêtons ces comparaisons, passons au code ! Une fois encore, nous ferons tout cela dans les outils du developpeur du navigateur ; vous aurez besoin aussi d'un fichier SVG super simple pour commncé avec le code présenté ci-dessous.

If you were to load that file into Inkscape, it should look like the image below. The page boundary is a square of 100×100 units, as defined in the viewBox attribute. The square itself is positioned with its top left corner at 10 units down, and 10 units across from the origin (the top-left of the page in SVG). Remember these units are not pixels – the image will actually be scaled to fit the available space in the browser window. By using a 100×100 viewBox it can be convenient to think of the values as percentages, but in reality it’s better to treat them as proportions or ratios, as that mental model works regardless of the viewBox size.

Si vous chargiez ce fichier dans Inkscape, il ressemblerait à l'image du haut de la page suivante. Les limires de la page est un carré de 100×100 unités, comme définit dans l'attribut de viewBox. le carré lui-même est positionné avec son angle du haut à gauche à 10 unités sous, et 10 unités à droite, de l'origine (l'angle en haut à gauche dans SVG). Souvenez-vous que ces unités ne sont pas de spixels - l'image sera en fait redimmensionnée pour s'adapter à laplace disponible dans al fenêtre du anvigateur. En utilisant un viewBox de 100×100, il est pratique de voir ces dimensions comme des pourcentages, mais, en fait, c'est mieux de les traiter comme des proportions ou des rapports, car ce modèle mental fonctionne quelque soit la taille de la viewBox.

We’re going to animate the “x” attribute from its starting value of 10 up to a value of 90. Because the square is 30 units wide, this will leave it hanging off the right-hand side of the screen when the animation finishes. I’ve done this to demonstrate a key difference between animating the content of an SVG file, and animating a <div> or other box in an HTML page: in the latter case the page width will grow and a horizontal scroll bar will appear (unless you specifically prevent that behaviour). With an SVG file, anything outside the viewBox simply isn’t rendered, making it easier to have animations that start or end ‘off-screen’. Think of it a little like a theatre stage, with your props and characters moving to and from the wings.

Nous allons animer l'attribut « x » depui sa valeur de départ de 10 jusqu'à une valeur de 90. Comme le carré fait 30 de coté, il dépassera la marge droite de l'écran à la fin de l'animation. LJe l'ai fait pour vous montrer une différence capilale en tre l'animation du contenu d'un fichier SVG et l'animation d'un<div> ou d'un autre cadre dans une page HTML : dans ce dernier cas, la largeur de la page augmetera et une barre de défilement horizontale apparara (sauf si vous empêchez spécifiquement ce comportement). Avec un fichier SVG, tout ce qui est hors de la viewBox est simplement non rendu, facilitant les animations qui commencent en dehors de l'écran. Pensez à cela comme à une scène de théatre, avec vos accessoires et vos personnages passant de la cour au jardin et vice-versa.

Load that file into your browser then in the developer console (F12) we’ll get a handle to it assigned to a variable for use later on. var s1 = document.querySelector(“#s1”); As a reminder, here’s how we can change the x coordinate using JS. Run it to confirm that the box shifts over a little. s1.setAttribute(“x”, 20); Essentially our animation will consist of running a line like this repeatedly. JavaScript is a pretty poor language for timing-sensitive tasks such as this, but it does offer a few rudimentary functions that will be good enough for our needs. We’ve already seen the most basic of these a few months ago, when we used setTimeout() to turn some traffic lights on and off in sequence. The following (shown above) will shift the square over, after a delay of 2000ms (2 seconds).

Chargez ce fichier dans votre navigateur puis, dans la console du développeur (F12), nous allons lui attacher une manette dans une variable, pour un usage ultérieur.

var s1 = document.querySelector(“#s1”);

Rappel : voici comment la coordonnée x peut être modifiée, en utilisant JS. Lancez-la pour confirmer que la boîte se déplace un peu.

s1.setAttribute(“x”, 20);

En gros, notre animation consistera à lancer une ligne comme celle-là de façon répétitive. Javascrit est un langage adssez pauvre pour les tâches liées au temps telle que celle-ci, mais il offre bien quelques fonctions rudmentaires qui seront suffisantes pour nos besoins. Nous avons déjà vu la plus basiques d'entre elles il y a quelques mois, quand nous avons utilisé setTimeout() pour éclairer et éteindre en séquence des feux tricolores. Le code suivant (ci-dessus) déplacera le carré, après un délai de 2000 ms (2 secondes).

Notice what we did there? Instead of just setting the attribute directly I’ve created a global variable (“currentX”), then added 10 to it before we use the computed result. I also have an “if” statement to set the value back to zero if it gets too big, ensuring the square doesn’t keep moving to infinity. How does this help us to create an animation? Now we can call that function repeatedly to make the square move by 10 units each time. Copy and paste the following lines as a single block, to make sure they all run before the 2s initial delay is up: setTimeout(moveSquare, 2000); setTimeout(moveSquare, 2100); setTimeout(moveSquare, 2200); setTimeout(moveSquare, 2300); setTimeout(moveSquare, 2400); setTimeout(moveSquare, 2500);

Avez-vous noté ce qu'on a fait ? Au lieu de juste paramétrer l'attribut directement, j'ai créé une variable globale (« currentX »),puis lui ai ajouté 10 avant que nous utilisons le résultat calculé. J'ai aussi une déclaration « if » pour ramener la valeur à zéro si elle devient trop grosse, assurant que le carré ne va pas se déplacer jusqu'à l'infini. En quoi tout cela nous aide à créer une animation ? Maintenant, nous appelons la fonction de façon répétitive pour que le carré se déplace de 10 unités à chaque fois. Copiez/collez les lignes suivantes d'un seul bloc, pour être sûr qu'elle sont toutes lancées avant que les 2 secondes de délai intial soit terminé :

setTimeout(moveSquare, 2000); setTimeout(moveSquare, 2100); setTimeout(moveSquare, 2200); setTimeout(moveSquare, 2300); setTimeout(moveSquare, 2400); setTimeout(moveSquare, 2500);

Well I don’t think Pixar has got anything to worry about, but it’s definitely animated. Creating a long list of setTimeout() calls isn’t great though. Fortunately JavaScript has a related function, setInterval(), which does the same thing as an infinite list of setTimeout() calls. It returns a unique number that can be used with the clearInterval() method to stop the process when you’re bored with looking at a jerkily moving square. var i = setInterval(moveSquare, 100); // Some time later… clearInterval(i);

Bon. Je ne pense pas que ça inquiète beaucoupPixar, mais il est vraiment animé. La création d'une ongue liste d'appels setTimeout() n'est vraiment pas grand'chose. Par chance, Javascript a une fonction adaptée, setInterval(), qui fait la même chose qu'une liste infinie d'appels setTimeout(). Elle retourne un seule nombre qui peut être utilisé avec la méthode clearInterval() pour arrêter le traitement quand vous en avez marre de voir bouger ce carré par saccades.

var i = setInterval(moveSquare, 100);

// Un peu plus tard… clearInterval(i);

Now that we don’t have to type a setTimeout() function for every frame of our animation, we can make things move a little more smoothly by reducing the delta, and reducing the time between function calls accordingly: var delta = 1; var i = setInterval(moveSquare, 10); // Some time later… clearInterval(i); Still a bit fast for you? Increase the delay in the setInterval() call. Not fast enough? You can reduce the delay further, but browsers clamp setTimeout() and setInterval() to a lower limit, so it probably won’t have much effect. Instead you can increase the delta value so the square moves two or three units at a time. Or 4.25 if you want – neither JS nor SVG require everything to be integers.

Maintenant que nous n'avons plus à taper la fonction setTimeout() à chaque étape de notre animation, nous pouvons rendre le déplacement plus lisse en diminuant delta et en réduisant en même temps l'intervalle entre les appels de fonction :

var delta = 1; var i = setInterval(moveSquare, 10);

// Un peu plus tard… clearInterval(i);

Est-ce encore un peu rapide pour vous ? Augmentez le délai dans l'appel de setInterval(). Pas encore assez rapide ? Vous pouvez réduire encore le délai, mais les navigateurs bloquent setTimeout() et setInterval() à une limite basse ; aussi, ça n'aura pas beaucoup d'effet. À la place, vous pouvez augmenter la valeur de delta de sorte que le carré se déplace de 2 ou 3 unités à chaque fois. Ou de 4,25, si vous voulez - Ni JS ni SVG n'imposent qu'ils soient des entiers.

For a long time setTimeout() and setInterval() were the only practical way to run some JavaScript at the sort of regular intervals needed for animation. Over the past few years, however, browsers have gained more support for technologies needed to run games – 2D bitmap canvases, 3D graphics and a little thing called requestAnimationFrame(). In case the name didn’t give it away, this function is intended to make animation a little easier. Specifically it does so by calling a function just before the browser is about to display the next frame on screen (typically about 60 times per second). It’s like a setTimeout() where the delay is automatically set to to an optimal value by the browser.

Pendant longtemps, setTimeout() et setInterval() furent la seule façon pratique de lancer du Javascript à des intervalles réguliers nécessaires à une animation. Dans les dernières années, cependant, les navigateurs ont disposé d'un support pour les technologies nécessaires pour faire tourner des jeux - canevas 2D en bitmap, Affichage 3D et une petite chose appelée requestAnimationFrame(). Au cas où le nom ne vous dit rien, cette fonction a tendance à faciliter la réalisation d'une animation. En particlulier, elle le fait en appelant une fonction justa avant que le navigateur se mette à afficher la trame suivante sur l'écran (en général, 60 fois par seconde). Elle ressemble à setTimeout(), avec un délai mis automatiquement à la valeur optimale par le navigateur.

In human terms 1/60 of a second is a pretty short delay, so to test this in the console you will probably want to increate the “delta” variable again, so that it’s more obvious that your square moves. var delta = 10; requestAnimationFrame(moveSquare); Now you might be expecting me to introduce an equivalent animation function to setInterval(), but I’m afraid I’ll have to disappoint you. There’s no function that will repeatedly run a function just before each frame is redrawn. But what we can do is to call requestAnimationFrame() again from inside the animation function.

À l'échelle humaine, 1/60e de seconde est un temps plutôt court ; aussi, pour le tester sur la console vous voudrez peut-être encore augmenter la variable « delta », de sorte que ça paraisse plus évident que le carré bouge.

var delta = 10;

requestAnimationFrame(moveSquare);

Maintenant, vous attendez peut-être de moi que je vous présente une fonction d'animatio équivalente à setInterval(), mais, à mon grand regret, je vais vous décevoir. Il n'y a pas de fonction qui lancer de façon répétée une fonction juste avant que chaque trame soit redessinée. Mais, ce que nous pouvons faire est de rappeler requestAnimationFrame() depuis l'intérieur de al fonction d'animation.

We’ll create a different animation function (shown above) for this next example. It’s similar to the previous one, except that it always moves the square by 1 unit, and stops when it reaches the right-hand side, rather than looping back round. This latter change is mainly so that the code doesn’t keep running indefinitely, otherwise it gets a bit tricky to proceed with the rest of this tutorial. Notice that we call requestAnimationFrame() to start the function running, but we also call this from within the “if” block, firing off another trip to our animation function just before the next frame is rendered.

Nous créerons une fonction d'animation différente (voir ci-dessus) pour ce nouvel exemple. Il est semblable au précédent, sauf que le carré se déplace toujours d'une unité et qu'il s'arrête qnd il atteint le côté droit, plutôt que de tourner en rond pour le retour. Ce dernier changement est fait de telle sorte que les code en continue pas à tourner indéfiniment ; autrement, ce serait un peu compliqué pour réaliser le reste de ce tutoriel. Notez que nous appelons requestAnimationFrame() pour commencer le lancement de la fonction, mais nous l'appelons aussi à l'intérieur du bloc « if », déclenchant un nouveau parcours pour notre fonction d'aniation juste avant que le rendu de la trame suivante ne soit affichée.

Now we’ve got an animation running, but we don’t really have any control over it. The duration of the animation will depend on how frequently requestAnimationFrame() fires in your browser, and all we can do is change “+ 1” to a different value to make the square move more or less on each iteration. But really that’s all we need to be able to do. So long as we know the exact time that the function is called, we can calculate how far into the animation we are, and therefore what position the square should be at. To achieve this, the function that is called by getAnimationFrame() receives a single parameter: a high-resolution timestamp.

Maintenant, notre animation tourne, mais nous n'avons vraiment aucun contrôle sur elle. La durée de l'animation dépendra du nombre de fois où requestAnimationFrame() se déclenchera dans votre navigateur et tout ce que nous pouvons faire est de changer « +1 » pour une autre valeur pour que le carré bouge plus ou moins rapidement à chaque itération. Mais vraiment, c'est tout ce que nous sommes capable de faire. Tant que nous connaissons le moment exact où la fonction est appelée, nous pouvons calculer le temps que prendra l'animation, et par conséquent, où devrait en être le carré. Pour terminer cela, la fonction qui est appelée par getAnimationFrame() reçoit un seul paramètre : une horodatage en haute-résolution.

Let’s work on a practical example: suppose we want our animation to take 10 seconds. At 60 frames per second that’s about 600 movements our square will make, each of them a fraction of a unit. We could divide the total distance by 600 to calculate the exact amount of movement, but that will fail if we run the code on something that refreshes at 30 or 120 frames per second, or if some frames get dropped due to the load on the machine. A better idea is to track how long has passed since the start of the animation, and use that to calculate where the object should be. We can then set the coordinates to the new value, before we fire off another requestAnimationFrame() for the next step in the animation.

Travaillons sur un exemple pratique : supposez que nous voulons que notre animation prenne 10 secondes. À 60 trames par secondes, notre carré effectuera à peu près 600 mouvements, chacun d'eux d'une fraction d'unité. Nous pouvons diviser la distance totale par 600 pour calculer la logueur exacte du mouvement ; mais ce sera faux si nous faisons tourner le code sur quelque chose quei rafraîchit l'écran à 30 ou 120 trames par seconde, ou si quelques trames sont perdues du fait de la charge de la machine. Une meilleure idée serait de suivre le temps écoulé depuis le début de l'animation et de l'utiliser pour calculer où l'objet devrait être. Ensuite, nous pouvons paramétrer les coordonnées à cette nouvelle valeur, avant que nous ne déclenchions un autre requestAnimationFrame() pour le pas suivant de l'animation.

The parameter that gets passed to our function is a value in milliseconds since the document was loaded. We don’t really care about that specific point in time – we need to know how long the animation itself has been running. What we need to do, therefore, is to record the timestamp the first time our function is called. On subsequent calls we can subtract that value from the latest timestamp to work out how far along the animation timeline we have progressed.

Let’s start by initialising a few variables. We’ll create a variable to hold our starting position, setting it to 10. Next we have a “duration” (in milliseconds) to hold the time we want our animation to run for, and “endX” for the X coordinate we want to end up with. Putting these into variables makes it easier to modify the animation to run at a different speed or cover a different distance. Finally we’ll include a “startTime” variable, with an “undefined” value initially, into which we’ll store a copy of the timestamp we receive the first time our animation code is called. var startX = 10 var endX = 90; var currentX = 0; var duration = 10; var startTime = undefined;

Now for our reworked animation function. The main animation code is fairly similar to the previous incarnation, except that we work with the timestamp that is passed to the function in order to calculate the new X value. The first time the function is called we don’t need to update the X coordinate – by definition we’re at the start of the animation – so we just store the current timestamp, then queue up another call before the next frame, then exit. On subsequent calls, the startTime is no longer “undefined” so this part of the function is skipped entirely.

By storing the initial timestamp outside the function, we can calculate how long the animation has been running. Since the two timestamps are in milliseconds, we’ll divide the result by 1000 to convert it to seconds. Since we know the total time for the animation, a quick division will give the proportion of the animation that has passed: we can then multiply that value by the total distance to travel in order to calculate the new X position for that moment in time. Code is shown below.

Running this should produce a smooth animation that takes 10s to complete. Re-run the previous block of “var” lines to reset everything, then the final requestAnimationFrame() call to kick it off again. Try changing the values in the variables to alter the distance the square moves, or the time it takes to perform the animation. In every case you should find that the animation is, if not smooth, at least a lot smoother than you saw with setTimeout() and setInterval().

Creating all these variables outside a function (so-called “global” variables) is generally seen as bad form in the programming world. It also makes it tricky to animate more than one thing, as they’ll all potentially be sharing the same global variables. A better approach is to encapsulate all the variables in a single JavaScript object, then attach that to the SVG element you’re trying to manipulate. Here’s the code above rewritten to work in this way (shown right).

Notice that I’ve removed the “X” from the end of the parameter names, and created a new “attribute” entry with a value of “x”. This starts to make the code more generic: you could change the “x” to a “y” in order to animate movement in the vertical direction, or “r” to animate the radius of a circle. For a truly generic solution you could turn this JS object into an array of objects. The animation code would loop over each entry in the array, allowing you to animate more than one attribute at a time – essential if you want your images to move at an angle, for example. I’ll leave it as a challenge for the reader to implement this.

At the moment our animation is also strictly linear: the attributes are changed at a constant rate over time. More “natural” animation can be achieved with rates that vary – accelerating and decelerating over the course of the movement. The maths to produce such effects, referred to as an “easing function”, is well outside the scope of this series. That alone is a good reason why CSS animation, SMIL, and third-party animation libraries are usually a better option than rolling your own JS animations from scratch. But for simple animations, or just your own education, it’s good to see how the same code you might use to dynamically modify your SVG in discrete steps, can also be put to work to achieve continuous effects.

issue150/inkscape.1572963269.txt.gz · Dernière modification : 2019/11/05 15:14 de d52fr