Tutorial. Corazón animado con SVG

21/03/2016

Hoy he decidido hablar un poco de como animar un elemento SVG. Para ello he creado uno con inkscape, tranquilos esta limpiado y optimizado para que ocupe poco. Os dejo un enlace del ejemplo que usaremos:

Si editáis el Heart.svg podréis ver que consta de tres elementos con sus respectivos identificadores: line_path (un path que dibuja el recorrido del pulso), heart_path (otro path que dibuja la forma del corazón) y pulse (un circulo que representa el pulso).

Ahora pasaremos a animarlos para que cobren algo de vida desde la hoja de estilos style.css:

heart_path: Este elemento tendrá dos animaciones, dash (que se ejecutara una sola vez con forwards dibujando el contorno del corazon) y pulse_heart (que aumentara y disminuirá el tamaño simulando el bombeo de sangre). Tranform y perspective me ayudan a centrar el elemento y disminuiré un 15% el tamaño del path con scale:

#heart_path {
    animation: dash 2s ease forwards, pulse_heart 2s ease-out infinite;
    -webkit-animation: dash 2s ease forwards, pulse_heart 2s ease-out infinite;
    transform-origin: 50% 50%;
    -ms-transform-origin: 50% 50%;
    -webkit-transform-origin: 50% 50%;
    perspective: 0px;
    -webkit-perspective: 0px;
    -webkit-transform: scale(0.85, 0.85);
    transform: scale(0.85, 0.85);
}

pulse: En este caso lanzamos la animación heart cada 2 segundos de forma lineal, ningún misterio en ese sentido:

#pulse {
    -webkit-animation: heart 2s infinite linear;
    animation: heart 2s infinite linear;
}

line_path: Parecido al anterior, cada 2.5 segundos se ejecutara la animación dash exclusivamente una vez para dibujar la linea del pulso:

#line_path {
    animation: dash 2.5s ease forwards;
    -webkit-animation: dash 2.5s ease forwards;
}

No he hablado mucho con respecto a los timings de las animaciones (ease, ease-out, linear, cubic...) o las iteraciones de una animación pero os dejo el siguiente enlace por si queréis trastear:

animaciones CSS

Bueno, pasemos ahora a las animaciones. Pulse_heart: esta animación esta asignada al elemento heart_path con un intervalo de 2 segundos, aumentara el corazón a escala 1:1 al 90% (1.8 segundos) y se volverá a reducir cuando alcance el 100% (2 segundos):

@keyframes pulse_heart {
    0% {
        transform: scale(0.85, 0.85);
    }
    90% {
        transform: scale(1, 1);
    }
    100% {
        transform: scale(0.85, 0.85);
    }
}

La animación dash permite dibujar los paths line_path aplicándoles el estilo stroke-dasharray='100' y modificando el valor stroke-dashoffset como se muestra a continuación:

@keyframes dash {
    0% {
        stroke-dashoffset: 95;
    }
    100% {
        stroke-dashoffset: 0;
    }
}

Ahora nos queda el mas complicado, heart. Nuestro circulo pulse recorre 6 tramos así que dividiremos 100% por 6 y en cada tramo aplicaremos los translate que sean pertinentes indicando mediante coordenadas (x,y) hacia donde queremos que se posicione nuestro circulo. Para estas cosas suelo usar inkscape:

@keyframes heart {
    0% {
        transform: translate(0px, 0px);
    }
    16.6666666667% {
        transform: translate(5.687px, 0);
    }
    33.3333333333% {
        transform: translate(8.844px, -7.937px);
    }
    50% {
        transform: translate(12.594px, 3.531px);
    }
    66.6666666667% {
        transform: translate(16.25px, -5.583px);
    }
    83.3333333333% {
        transform: translate(18.719px, 0px);
    }
    100% {
        transform: translate(28.187px, 0px);
    }
}

Finalmente tendríamos nuestras animaciones corriendo pero no cantemos victoria. Las incompatibilidades con los distintos navegadores hacen que no se muestren correctamente en todos los navegadores. Por ejemplo, lo que acabamos de realizar se ve correctamente en Firefox y Chrome pero en IE, Opera y Safari puede que no se vea como lo teníamos planteado. Tendremos que duplicar animaciones y transforms con prefijos (-ms-, -webkit-, -o-, -moz-...).

El problema mas gordo viene con IE, por eso he decidido usar Snap.svg - una libreria JS para animar SVGs -, os muestro como se traduciría nuestro CSS a Snap.svg evitándonos muchos quebraderos de cabeza.

Inicialización de elementos: Aqui pasamos a encapsular los elementos en variables para poder manejarlos fácilmente. Usaremos attr para inicializar strokeDashoffset de cara al dibujado de esos elementos:

/* Seleccionamos el svg a tratar por su identificador svg */
        var svg = Snap("#svg").attr('background', 'red');
        
        /* Asignamos variable a pulse */
        var pulse = svg.select('#pulse');
        
        /* Asignamos variable a heart_path */
        var heart = svg.select('#heart_path').attr({
            strokeDashoffset: 98
        });
        
        /* Asignamos variable a line_path */
        var line = svg.select('#line_path').attr({
            strokeDashoffset: 100
        });

Animación de dibujado: Solo se ejecuta una vez en 2 segundos. Usamos animate para animar el cambio del valor de strokeDashoffset.

heart.animate({
            strokeDashoffset: 0
        }, 2000);
        
        line.animate({
            strokeDashoffset: 0
        }, 2000);
        

Animación de aumento del tamaño (heart): Se ejecuta de forma continuada cada 2 segundos. Usamos una lista HeartAnim con dos variables (animation y dur) y creamos una función animateHeart que ejecute de forma secuencial cada paso de la animación.

/* La s de tranform es de scale */
        var heartAnim = [
            {
                animation: {
                    transform: 's1,1'
                },
                dur: 0
            },

            {
                animation: {
                    transform: 's0.85,0.85'
                },
                dur: 200
            }
            , {
                animation: {
                    transform: 's1,1'
                },
                dur: 1800
            }];

        (function animateHeart(el, i) {

            el.animate(heartAnim[i].animation, heartAnim[i].dur, function () {
                animateHeart(el, ++i in heartAnim ? i : 0);
            })

        })(heart, 0);

Animación movimiento del pulso: Parecido al anterior Se ejecuta de forma continuada cada 2 segundos. Usamos una lista pulseAnim con dos variables (animation y dur) y creamos una función animateCircle que ejecute de forma secuencial cada paso de la animación.

/* La t de tranform es de translate */
        var pulseAnim = [
            {
                animation: {
                    transform: 't0,0'
                },
                dur: 0
        },
            {
                animation: {
                    transform: 't5.687,0'
                },
                dur: 326.1
        },

            {
                animation: {
                    transform: 't8.844,-7.937'
                },
                dur: 326.1
        },
            {
                animation: {
                    transform: 't12.594, 3.531'
                },
                dur: 326.1
        },
            {
                animation: {
                    transform: 't16.25,-5.583'
                },
                dur: 326.1
        },
            {
                animation: {
                    transform: 't18.719,0'
                },
                dur: 326.1
        },
            {
                animation: {
                    transform: 't28.187,0'
                },
                dur: 326.1
        }];

        (function animateCircle(el, i) {
            el.animate(pulseAnim[i].animation, pulseAnim[i].dur, function () {
                animateCircle(el, ++i in pulseAnim ? i : 0);
            })
        })(pulse, 0);

Esto de las animaciones SVG es un coñazo, usar prefijos a saco y aprender a usar Snap.svg para no volveros locos (sobretodo si tenéis pendado usar rotate). Últimamente estoy viendo muchas chapuzas con esto, incluso lo que os he mostrado estara plagado de algún gazapo. Por ultimo, os dejo el enlace de Snap.svg:

Snap.svg