svg : la librairie Snap

On a vu dans les quelques articles précédents l’essentiel pour créer et manipuler des images SVG sans toutefois aborder l’animation qui est une partie importante que je réserve pour la fin. Dans le présent article je vais aborder la présentation de la librairie Snap.svg. J’en ai déjà un peu parlé dans le premier article.

C’est certainement la librairie la plus utiliser pour le SVG. Elle a été écrite par l’auteur de Raphaël. Cette dernière est excellente mais comme elle prend en compte les anciens navigateurs elle ne peut pas proposer les fonctionnalités les plus récentes comme les groupements, les dégradés, les masques… En plus on peut très bien agir sur un fichier SVG existant qui n’a pas été créé par Snap, ce qui ouvre de belles possibilités comme créer son SVG avec Inkscape et ensuite créer l’animation avec Snap.

On dessine

Avec Snap on peut dessiner en utilisant l’API. Mais pour commencer il faut charger la librairie, par exemple avec le CDN :

<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js"></script>

On dispose alors de toutes les commandes !

La première chose à faire est de créer un SVG sur la page s’il n’en existe pas déjà un :

var s = Snap(800, 600)

Si on a déjà un SVG il suffit de le référencer :

var s = Snap("#monsvg")

Ensuite on peut utiliser toute l’API

Par exemple tracer un cercle (les paramètres sont x, y et r, référence) :

s.circle(200, 200, 100)

On a évidemment les valeurs par défaut pour le trait, le remplissage… On peut modifier tous les attributs ainsi :

var cercle = s.circle(200, 200, 100);
cercle.attr({
    fill: 'red',
    stroke: 'blue',
    strokeWidth: 10
})

On peut aussi chaîner les commandes (comme avec JQuery) :

s.circle(200, 200, 100).attr({
    fill: 'red',
    stroke: 'blue',
    strokeWidth: 10
})

On peut de la même manière dessiner un rectangle (paramètres : x, y, width, height, [rx], [ry], référence) :

s.rect(200, 200, 200, 100).attr({
    fill: 'yellow',
    stroke: 'magenta',
    strokeWidth: 10
})

On peut ainsi tracer lignes, ellipse, polygones, polylignes… Tout ce qu’on a vu dans les articles précédents.

Les formes sont tracées dans l’ordre :

var ellipse = s.ellipse(200, 200, 100, 150).attr({
    fill: 'DodgerBlue',
    stroke: 'MediumSeaGreen',
    strokeOpacity: .2,
    strokeWidth: 30
})
s.line(50, 50, 300, 300).attr({
    stroke: 'DarkGreen',
    strokeWidth: 40
})

On peut changer ce comportement avec before et after :

var ligne = s.line(50, 50, 300, 300).attr({
    stroke: 'DarkGreen',
    strokeWidth: 40
}).after(ellipse)

Groupes et masques

Groupes

Une possibilité intéressante consiste à grouper les formes (référence) pour les gérer de façon globale, par exemple pour affecter des attributs, ce qui existe de base pour le SVG :

var ellipse = s.ellipse(200, 200, 100, 150).attr({
    fill: 'DodgerBlue',
    stroke: 'MediumSeaGreen',
    strokeOpacity: .2
})
var polyligne = s.polyline(50, 50, 200, 100, 100, 300, 400, 200).attr({
    fill: 'none',
    stroke: 'DarkOrange',
    strokeOpacity: .7
})
var groupe = s.group(ellipse, polyligne)
groupe.attr({
    strokeWidth: 50
})

On peut simplifier la syntaxe en remplaçant group par g.

Masques

On a déjà vu les masques avec le SVG. Snap permet facilement de les utiliser (référence), on va par exemple masquer la polyligne avec l’ellipse :

var ellipse = s.ellipse(200, 200, 100, 150).attr({
    fill: 'DodgerBlue',
    stroke: 'MediumSeaGreen',
    strokeOpacity: .2
})
var polyligne = s.polyline(50, 50, 200, 100, 100, 300, 400, 200).attr({
    fill: 'none',
    stroke: 'DarkOrange',
    strokeWidth: 40,
    strokeOpacity: .7,
    mask: ellipse
})

Transformations

Voyons maintenant les transformations. Par exemple une rotation :

var ellipse = s.ellipse(200, 200, 100, 150).attr({
    fill: 'Navy'
}).transform('r45')

On utilise transform (référence) en transmettant un paramètre qui est soit une chaîne de caractères comme ci-dessus, soit un objet.

Pour une translation on utilise t en précisant x et y :

transform('r45 t200,100')

Pour la dimension évidemment c’est s (scale) :

transform('s2')

Là on double toutes les dimensions.

Animation

Voyons à présent la partie la plus intéressante : l’animation. On a la commande animate (référence) :

var ellipse = s.ellipse(200, 200, 100, 150).attr({
    fill: 'Navy'
})
ellipse.animate({ry:100}, 1000)

Au départ l’ellipse a son rayon y à 200 et on le fait passer progressivement à 100, et au final on a donc un cercle :

La syntaxe complète est :

Element.animate(attrs, duration, [easing], [callback])

On peut enchaîner des animations en utilisant le callback :

ellipse.animate({ry:100}, 1000, function(){
  ellipse.animate({ry: 150}, 1000)
})

Pour faire une translation il suffit de jouer sur x et/ou y :

var rectangle = s.rect(50, 50, 200, 100).attr({
  fill: 'Green'
})
rectangle.animate({x:300}, 1000)

On peut accomplir toutes les transformations, par exemple une rotation, et ajouter un effet :

var rectangle = s.rect(50, 50, 200, 100).attr({
  fill: 'Green'
})
rectangle.animate({transform: 'r90'}, 1000, mina.bounce)

J’ai utilisé le paramètre pour le easing, c’est à dire l’effet sur l’animation. On a du choix :

var rectangle1 = s.rect(20, 10, 100, 50).attr({fill: 'Green'})
var rectangle2 = s.rect(20, 60, 100, 50).attr({fill: 'Blue'})
var rectangle3 = s.rect(20, 110, 100, 50).attr({fill: 'Orange'})
var rectangle4 = s.rect(20, 160, 100, 50).attr({fill: 'DarkBlue'})
var rectangle5 = s.rect(20, 210, 100, 50).attr({fill: 'DarkSalmon'})
var rectangle6 = s.rect(20, 260, 100, 50).attr({fill: 'Maroon'})
var rectangle7 = s.rect(20, 310, 100, 50).attr({fill: 'Red'})
rectangle1.animate({transform: 't300'}, 1000, mina.easeout)
rectangle2.animate({transform: 't300'}, 1000, mina.easein)
rectangle3.animate({transform: 't300'}, 1000, mina.easeinout)
rectangle4.animate({transform: 't300'}, 1000, mina.backin)
rectangle5.animate({transform: 't300'}, 1000, mina.backout)
rectangle6.animate({transform: 't300'}, 1000, mina.elastic)
rectangle7.animate({transform: 't300'}, 1000, mina.bounce)

Un exemple

Pour faire un peu le point de cette librairie prenons un exemple :

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8" />
    <title>Animation avec Snap</title>
    <style>
    body {
        background-color: lavender;
    }
    .svg-wrapper {
        position: relative;
        width: 100%;
        height: 1px;
        box-sizing: content-box;
        margin: 0;
        padding: 0;
        padding-bottom: 100%;
        padding-bottom: calc(100% - 1px); 
    }
    .svg-wrapper > svg { 
        display: block;
        position: absolute; 
        height: 100%;
        width: 100%;
        left: 0;
        top: 0;
    }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js"></script>
</head>
<body>
    <div class="svg-wrapper">
        <svg viewBox="0,0 400,400"></svg>
    </div>
</body>
<script>

    // Initialisations
    let s = Snap('svg')
    let f = s.filter(Snap.filter.shadow(2, 2, 1, 'black', .5))  
    let boutonbase = s.rect(10, 10, 40, 40, 10, 10).attr({filter: f, fill: 'yellow'})
    let boutons = []
    let textes = []
    let boutonOuvert = null
    const colors = ['yellow', 'lightgreen', 'lightblue']
    const texts = ['Texte 1', 'Texte 2', 'Texte 3']

    // Création des boutons et textes
    boutons.push(boutonbase)
    for(let i = 0; i < 3; ++i) {
        let y = 44 * i
        if(i) boutons.push(boutonbase.clone().transform('t0,' + y ).attr({ fill: colors[i]}))
        textes.push(s.text(-100, -100, texts[i]).attr({ 
            fontSize: 20,
            fontFamily: 'Arial, Helvetica, sans-serif',
            opacity: 0
        }))        
    }

    // Mise en place des événements
    for(let bouton of boutons) {
        bouton.mouseover( function() {
            action(this)
        })       
    }

    let action = (bouton) => {
        if(bouton != boutonOuvert) {
            bouton.stop().animate({ width: 100 }, 1500, mina.bounce, 
                () => {
                    let box = bouton.getBBox()
                    let x = box.x + 10
                    let y = box.y + 28
                    let boutonTemp = boutonOuvert 
                    boutonOuvert = bouton
                    textes[boutons.indexOf(bouton)].attr({x: x, y: y}).stop().animate({opacity: 1}, 400)
                    if(boutonTemp) {
                        let textActual = textes[boutons.indexOf(boutonTemp)]  
                        textActual.animate({opacity: 0}, 300, () => textActual.attr({x: -100, y: -100}))
                        boutonTemp.stop().animate({ width: 40 }, 1500, mina.bounce)
                    }               
                }
            )
        }
    }

</script>
</html>

Un fonctionnement simple : lorsqu’on survole un bouton il s’élargit puis un texte apparaît dessus. Lorsqu’on fait la même chose sur un autre bouton l’effet est le même mais le précédent se réduit avec disparition du texte.

Le HTML est très simple et se contente de déclarer un SVG avec une viewBox :

<div class="svg-wrapper">
    <svg viewBox="0,0 400,400"></svg>
</div>

Le CSS ajouté est destiné essentiellement à étirer le SVG sur tout l’écran. Il y aurait bien d’autres façons de le faire mais ce n’est pas le sujet.

Voyons le JavaScript (j’ai adopté des fonctionnalités ES6 prises en compte par tous les navigateurs récents). On commence par initialiser Snap avec le SVG existant :

let s = Snap('svg')

Ensuite on crée un filtre pour l’ombre des boutons :

let f = s.filter(Snap.filter.shadow(2, 2, 1, 'black', .5))

La méthode shadow de Snap est vraiment pratique !

Ensuite on crée le premier bouton qui va servir de base pour les autres (on lui applique le filtre pour l’ombre) :

let boutonbase = s.rect(10, 10, 40, 40, 10, 10).attr({filter: f, fill: 'yellow'})

Ensuite on a quelques initialisations :

let boutons = []
let textes = []
let boutonOuvert = null
const colors = ['yellow', 'lightgreen', 'lightblue']
const texts = ['Texte 1', 'Texte 2', 'Texte 3']

On crée alors les boutons et les textes :

// Création des boutons et textes
boutons.push(boutonbase)
for(let i = 0; i < 3; ++i) {
    let y = 44 * i
    if(i) boutons.push(boutonbase.clone().transform('t0,' + y ).attr({ fill: colors[i]}))
    textes.push(s.text(-100, -100, texts[i]).attr({ 
        fontSize: 20,
        fontFamily: 'Arial, Helvetica, sans-serif',
        opacity: 0
    }))        
}

Remarquez la méthode clone qui permet de cloner un élément. Les texte sont placé hors viewBox et sont transparents.

Ensuite on met en place les événement de survol de souris pour les boutons :

// Mise en place des événements
for(let bouton of boutons) {
    bouton.mouseover( function() {
        action(this)
    })       
}

Enfin on code la partie animation :

let action = (bouton) => {
    if(bouton != boutonOuvert) {
        bouton.stop().animate({ width: 100 }, 1500, mina.bounce, 
            () => {
                let box = bouton.getBBox()
                let x = box.x + 10
                let y = box.y + 28
                let boutonTemp = boutonOuvert 
                boutonOuvert = bouton
                textes[boutons.indexOf(bouton)].attr({x: x, y: y}).stop().animate({opacity: 1}, 400)
                if(boutonTemp) {
                    let textActual = textes[boutons.indexOf(boutonTemp)]  
                    textActual.animate({opacity: 0}, 300, () => textActual.attr({x: -100, y: -100}))
                    boutonTemp.stop().animate({ width: 40 }, 1500, mina.bounce)
                }               
            }
        )
    }
}

Pour placer le texte sur le bouton on utilise la méthode getBBox qui donne les références de la boîte encadrante (bounding box). Le reste ne présente pas de particularité. Remarquez la méthode stop qui permet d’arrêter une éventuelle animation en cours avant d’en lancer une autre.

Conclusion

On a vu que Snap est performant et pratique. Je déplore toutefois l’absence d’un véritable tutoriel sur le site de la librairie. On a juste les références et il faut un peut galérer pour arriver à trouver les fonctionnalités…

On peut aussi regretter l’absence de ligne de temps pour organiser les animations.

Mais au final c’est une librairie parfaite pour des créations même élaborées à condition de vraiment s’investir dans la logique les appellations en place qui ne sont pas forcément intuitives.

Une alternative intéressante est svg.js qui est assez bien documentée et plus légère.

Laisser un commentaire