Generar sprites CSS/LESS automáticamente con Grunt

La utilización de sprites en CSS es una de las técnicas más básicas pero a la vez más necesarias en el desarrollo front-end.

Aunque es posible crear y mantener un sprite de forma manual, te tomará tiempo y esfuerzo que probablemente podría tener un mejor uso (sobre todo cuando tienes que incorporar nuevas imágenes al sprite), por lo tanto qué mejor que automatizar esta tarea con la ayuda de Grunt.

Utilizaremos dos tareas distintas: una para crear un sprite de imágenes en PNG y otra para imágenes vectoriales en SVG, ideales para dispositivos con alta densidad de píxeles.

Sprite de imágenes PNG con spritesmith

En primer lugar, necesitamos añadir el plugin a nuestra configuración de Grunt. Añadimos la dependencia con npm install --save-dev grunt-spritesmith para agregarlo a la definición de package.json

Luego debemos registrar la tarea para Grunt, añadiendo la tarea a tu Gruntfile.js con grunt.loadNpmTasks('grunt-spritesmith');

Finalmente, debes definir las tareas, por ejemplo:

grunt.initConfig({
    sprite: {
        all: {
            // toma todas las imágenes png de esta ruta
            src: 'front/img/sprite/*.png',
            // determina el nombre de archivo del sprite generado
            dest: 'front/img/img-sprite.png',
            // además crearemos este archivo con variables less que
            // usaremos para crear las clases que aplican a cada elemento
            destCss: 'front/less/img-sprite.less',
            cssOpts: {
                functions: false
            }
        }
    }
});

Puedes encontrar la definición de todas las opciones en su repositorio de GitHub.

El plugin detecta automáticamente el tipo de archivo que debe generar a partir de la extensión: CSS, LESS, JSON, SASS, SCSS, Stylus. En este caso particular, con la propiedad cssOpts.functions = false estamos indicando que no genere los mixins necesarios para crear todas las clases, ya que para eso utilizaremos un set de mixins propios.

El archivo LESS generado (sin mixins) es como el siguiente:


@logo-nuevo-color-name: logo-nuevo-color;
@logo-nuevo-color-x: 0px;
@logo-nuevo-color-y: 0px;
@logo-nuevo-color-offset-x: 0px;
@logo-nuevo-color-offset-y: 0px;
@logo-nuevo-color-width: 300px;
@logo-nuevo-color-height: 91px;
@logo-nuevo-color-total-width: 660px;
@logo-nuevo-color-total-height: 182px;
@logo-nuevo-color-image: '../img/img-sprite.png';
@logo-nuevo-color: 0px 0px 0px 0px 300px 91px 660px 182px '../img/img-sprite.png' logo-nuevo-color;
@logo-nuevo-gris-name: logo-nuevo-gris;
@logo-nuevo-gris-x: 0px;
@logo-nuevo-gris-y: 91px;
@logo-nuevo-gris-offset-x: 0px;
@logo-nuevo-gris-offset-y: -91px;
@logo-nuevo-gris-width: 300px;
@logo-nuevo-gris-height: 91px;
@logo-nuevo-gris-total-width: 660px;
@logo-nuevo-gris-total-height: 182px;
@logo-nuevo-gris-image: '../img/img-sprite.png';
@logo-nuevo-gris: 0px 91px 0px -91px 300px 91px 660px 182px '../img/img-sprite.png' logo-nuevo-gris;
@logo-name: logo;
@logo-x: 300px;
@logo-y: 0px;
@logo-offset-x: -300px;
@logo-offset-y: 0px;
@logo-width: 360px;
@logo-height: 88px;
@logo-total-width: 660px;
@logo-total-height: 182px;
@logo-image: '../img/img-sprite.png';
@logo: 300px 0px -300px 0px 360px 88px 660px 182px '../img/img-sprite.png' logo;
@spritesheet-width: 660px;
@spritesheet-height: 182px;
@spritesheet-image: '../img/img-sprite.png';
@spritesheet-sprites: @logo-nuevo-color @logo-nuevo-gris @logo;
@spritesheet: 660px 182px '../img/img-sprite.png' @spritesheet-sprites;

Los nombres de las variables están en relación a los nombres de cada archivo. Como las variables son rutas, puedes modificar su valor al hacer la compilación de LESS a CSS.

Los mixins que incluye de forma predeterminada genera clases que incluyen todas las propiedades para funcionar con el sprite, pero esto hace que tengas un montón de elementos con background-image: url(../img/img-sprite.png); por lo que puedes considerar conveniente redefinir los mixins que vas a utilizar.

En mi caso utilizo lo siguiente:

/*-------------------------------------- Generate Sprites */
.sprite-width(@sprite) {
  width: extract(@sprite, 5);
}
.sprite-height(@sprite) {
  height: extract(@sprite, 6);
}
.sprite-position(@sprite) {
  @sprite-offset-x: extract(@sprite, 3);
  @sprite-offset-y: extract(@sprite, 4);
  background-position: @sprite-offset-x @sprite-offset-y;
}
.sprite-image(@sprite) {
  @sprite-image: extract(@sprite, 9);
  @sprite-image-bare: ~`"@{sprite-image}".slice(1, -1)`;
  background-image: url(@sprite-image-bare);
}
.gen-sprites(@sprites, @i: 1) when (@i <= length(@sprites)) {
  @sprite: extract(@sprites, @i);
  @sprite-name: extract(@sprite, 10);
  .@{sprite-name} {
    .gen-img-sprite(@sprite);
  }
  .gen-sprites(@sprites, @i + 1);
}
.gen-img-sprite(@sprite) {
  .sprite-position(@sprite);
  .sprite-width(@sprite);
  .sprite-height(@sprite);
}

A partir de este punto, quizás quieras también incluir un segundo paso que consista en la optimización del sprite o re-compilar las hojas de estilos a partir de tus LESS… para esto puedes agregar un alias a tu archivo de tareas que especifique las tareas que se deben ejecutar en el orden adecuado, por ejemplo grunt.registerTask('png-sprite', ['sprite', 'less', imagemin']);

Sprite de imágenes SVG con grunt-svg-sprite

Dado que cada vez las pantallas de alta densidad son cada vez más populares (no solamente en productos Apple sino también en múltiples dispositivos móviles y tablets) es recomendable también incluir un sprite de imágenes vectoriales, para lo cual puedes utilizar grunt-svg-sprite.

Igual que en el caso anterior, debemos:

  • Añadir el plugin a nuestras dependencias con npm install --save-dev grunt-svg sprite
  • Cargar la tarea con grunt.loadNpmTasks('grunt-svg-sprite');

Luego toca la definición de la(s) tarea(s).

Este plugin en particular tiene muchas opciones, por lo que dependiendo de qué tanto necesitas que se ajuste a tu estructura de carpetas o qué tanto quieres adaptarte a sus opciones predeterminadas va a ser el rato que vas a necesitar dedicarle a revisar todos los ajustes de configuración.

En lo personal me sirve algo de este estilo:

svg_sprite: {
    dist: {
        // toma todas las imágenes svg
        src: ['*.svg'],
        dest: 'front/',
        // indica un directorio de referencia. es preferible indicarlo ya que
        // genera los nombres de clases considerando la ruta completa
        cwd:  'front/img/svg',
        expand: true,
        options: {
            mode: {
                // tiene varios "modos" de funcionamiento...
                css: {
                    bust: false,
                    // la carpeta destino del archivo .less
                    dest: 'less/',
                    // ... y la ruta destino de la imagen, relativa
                    // a la propiedad "dest" de la tarea ("front/")
                    sprite: '../img/svg-sprite.svg',
                    // crear una clase común que se aplica a todos los
                    // elementos donde se aplica el sprite. permite
                    // reducir propiedades redundantes
                    common: 'svg-sprite',
                    render: {
                        less: {
                            // nombre del archivo less que generará
                            dest: 'svg-sprite.less'
                        }
                    }
                }
            }
        }
    }
}

Es importante mencionar un par de puntos relevantes sobre este plugin:

  1. Al generar el sprite, automáticamente optimiza cada imagen que se incluye; simplifica algunas rutas de modo que el peso del sprite combinado es menor a la suma de cada elemento. Algunos intérpretes más viejos se pueden confundir y mostrar la imagen distorsionada, pero suelen verse correctamente en el navegador.
  2. El plugin puede funcionar con distintos modos que permiten más opciones a cambio de menor compatibilidad con navegadores. El modo básico es css, que supone que cada imagen se usa como fondo de un elemento dado con las clases generadas por el plugin; pero hay otros modos como view que permite mostra solamente una parte del sprite generado, por lo que te puede servir, por ejemplo, para crear un sprite de fondos vectoriales que se repiten.

Puedes encontrar la explicación de cada modo en la documentación del módulo original que utiliza este plugin.