UNPKG

@mariusgundersen/gulp-svg-spritesheet

Version:
334 lines (273 loc) 11.7 kB
'use strict'; var cheerio = require('cheerio'), events = require('events'), fs = require('fs'), gutil = require('gulp-util'), mkdirp = require('mkdirp'), mustache = require('mustache'), packetr = require('./lib/packer.growing'), path = require('path'), through2 = require('through2'); // Consts var PLUGIN_NAME = 'gulp-svg-spritesheet'; // Options var defaults = { cssPathNoSvg: '', // Leave blank if you dont want to specify a fallback cssPathSvg: './test.svg', // CSS path to generated SVG demoDest: '', // Leave blank if you don't want a demo file demoSrc: '../demo.tpl', // The souce or the demo template padding: 0, // Add some padding between sprites pixelBase: 16, // Used to calculate em/rem values positioning: 'vertical', // vertical, horizontal, diagonal or packed templateSrc: '../template.tpl', // The source of the CSS template templateDest: './sprite.scss', units: 'px', // px, em or rem x: 0, // Starting X position y: 0, // Starting Y position templateData: {} }; // Sorting functions from Jake Gordon's bin packing algorithm demo // https://github.com/jakesgordon/bin-packing var sort = { w : function (a,b) { return b.w - a.w; }, h : function (a,b) { return b.h - a.h; }, max : function (a,b) { return Math.max(b.w, b.h) - Math.max(a.w, a.h); }, min : function (a,b) { return Math.min(b.w, b.h) - Math.min(a.w, a.h); }, height : function (a,b) { return sort.msort(a, b, ['h', 'w']); }, width : function (a,b) { return sort.msort(a, b, ['w', 'h']); }, maxside : function (a,b) { return sort.msort(a, b, ['max', 'min', 'h', 'w']); }, msort: function(a, b, criteria) { var diff, n; for (n = 0 ; n < criteria.length ; n++) { diff = sort[criteria[n]](a,b); if (diff !== 0) return diff; } return 0; } }; // This is where the magic happens var spriteSVG = function(options) { options = options || {}; // Extend our defaults with any passed options for (var key in defaults) { options[key] = options[key] || defaults[key]; } // Create one SVG to rule them all, our sprite sheet var $ = cheerio.load('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>', { xmlMode: true }), $sprite = $('svg'), // This data will be passed to our template data = options.templateData, eventEmitter = new events.EventEmitter(), self, x = options.x, y = options.y; // Copy values from the options into the templateData data.cssPathSvg = options.cssPathSvg; data.height = 0; data.sprites = []; data.units = options.units; data.width = 0; // When a template file is loaded, render it eventEmitter.on("loadedTemplate", renderTemplate); // Generate relative em/rem untis from pixels function pxToRelative(value) { return value / options.pixelBase; } // Load a template file and then render it function loadTemplate(src, dest) { fs.readFile(src, function(err, contents) { if(err) { new gutil.PluginError(PLUGIN_NAME, err); } var file = { contents: contents.toString(), data: data, dest: dest }; eventEmitter.emit("loadedTemplate", file); }); } // Position sprites using Jake Gordon's bin packing algorithm // https://github.com/jakesgordon/bin-packing function packSprites(cb) { var packer = new GrowingPacker(); // Get coordinates of sprites packer.fit(data.sprites); // For each sprite for (var i in data.sprites) { var sprite = data.sprites[i], // Create, initialise and populate an SVG $svg = $('<svg/>') .attr({ 'height': sprite.h, 'viewBox': sprite.viewBox, 'width': sprite.w, 'x': Math.ceil(sprite.fit.x)+options.padding, 'y': Math.ceil(sprite.fit.y)+options.padding }) .append(sprite.file); // Check and set parent SVG width if(sprite.fit.x+sprite.w+options.padding>data.width) { data.width = Math.ceil(sprite.fit.x+sprite.w+options.padding); } // Check and set sprite sheet height if(sprite.fit.y+sprite.h+options.padding>data.height) { data.height = Math.ceil(sprite.fit.y+sprite.h+options.padding); } // Round up coordinates and add padding sprite.h = Math.ceil(sprite.h); sprite.w = Math.ceil(sprite.w); sprite.x = -Math.abs(Math.ceil(sprite.fit.x))-options.padding; sprite.y = -Math.abs(Math.ceil(sprite.fit.y))-options.padding; // Convert to relative units if required if(options.units!=='px') { sprite.h = pxToRelative(sprite.h); sprite.w = pxToRelative(sprite.w); sprite.x = pxToRelative(sprite.x); sprite.y = pxToRelative(sprite.y); } // Add the SVG to the sprite sheet $sprite.append($svg); } // Save the sprite sheet saveSpriteSheet(cb); } function positionSprites(cb) { // For each sprite for (var i in data.sprites) { var sprite = data.sprites[i]; // Add padding sprite.x = x+options.padding; sprite.y = y+options.padding; // Create, initialise and populate an SVG var $svg = $('<svg/>') .attr({ 'height': sprite.h, 'viewBox': sprite.viewBox, 'width': sprite.w, 'x': Math.ceil(sprite.x), 'y': Math.ceil(sprite.y) }) .append(sprite.file); // Round up coordinates sprite.h = Math.ceil(sprite.h); sprite.w = Math.ceil(sprite.w); sprite.x = -Math.abs(Math.ceil(sprite.x)); sprite.y = -Math.abs(Math.ceil(sprite.y)); // Increment x/y coordinates and set sprite sheet height/width if(options.positioning==='horizontal' || options.positioning==='diagonal') { x+=sprite.w+options.padding; data.width+=sprite.w+options.padding; if(options.positioning!=='diagonal' && data.height<sprite.h+options.padding) { data.height = sprite.h+options.padding; } } if(options.positioning==='vertical' || options.positioning==='diagonal') { y+=sprite.h+options.padding; data.height+=sprite.h+options.padding; if(options.positioning!=='diagonal' && data.width<sprite.w+options.padding) { data.width = sprite.w+options.padding; } } // Convert to relative units if required if(options.units!=='px') { sprite.h = pxToRelative(sprite.h); sprite.w = pxToRelative(sprite.w); sprite.x = pxToRelative(sprite.x); sprite.y = pxToRelative(sprite.y); } // Add the SVG to the sprite sheet $sprite.append($svg); } // Save the sprite sheet saveSpriteSheet(cb); } function processSVG(file, encoding, cb) { // Ignore empty files if (file.isNull()) { return; } // We don't do streaming if (file.isStream()) { return cb(new gutil.PluginError(PLUGIN_NAME, 'Streams are not supported')); } // We're using the filename as the CSS class name var filename = path.basename(file.relative, path.extname(file.relative)), // Load the file contents $file = cheerio.load(file.contents.toString('utf8'), {xmlMode: true})('svg'), viewBox = $file.attr('viewBox'), coords = viewBox.split(" "), width = $file.attr('width') || coords[2], height = $file.attr('height') || coords[3]; // Set sprite data to be used by the positioning function var sprite = { fileName: filename, file: $file.contents(), h: parseFloat(height), padding: options.padding, // Round up coordinates to avoid chopping off edges viewBox: Math.ceil(coords[0])+" "+Math.ceil(coords[1])+" "+Math.ceil(coords[2])+" "+Math.ceil(coords[3]), w: parseFloat(width) }; // Add the sprite to our array data.sprites.push(sprite); // Move on to processSprites() cb(); } function processSprites(cb) { // Save this for referencing in positioning functions self = this; // Sort the sprites so the biggest are first to avoid this issue: // https://github.com/jakesgordon/bin-packing/blob/master/js/packer.growing.js#L10 data.sprites.sort(sort.maxside); // Lay out the sprites if(options.positioning==='packed') { packSprites(cb); } else { positionSprites(cb); } } // Render our template and then save the file function renderTemplate(file) { var compiled = mustache.render(file.contents, file.data); mkdirp(path.dirname(file.dest), function(){ fs.writeFile(file.dest, compiled); }); } // Final processing of sprite sheet then we return file to gulp pipe function saveSpriteSheet(cb) { // Add padding to even edges up data.height+=options.padding; data.width+=options.padding; // If there is a non-svg fallback send the path to the template if(options.cssPathNoSvg) { data.cssPathNoSvg = options.cssPathNoSvg; } // Set the sprite sheet width, height and viewbox $sprite.attr({ 'height': data.height, 'viewBox': '0 0 '+data.width+' '+data.height, 'width': data.width }); // Convert to relative units if required if(options.units!=='px') { data.height = pxToRelative(data.height); data.width = pxToRelative(data.width); } // Save our CSS template file loadTemplate(options.templateSrc, options.templateDest); // If a demo file is required, save that too if(options.demoDest) { loadTemplate(options.demoSrc, options.demoDest); } // Create a file to pipe back to gulp var file = new gutil.File({path: './', contents: new Buffer($.xml())}); // Pipe it baby! self.push(file); // Aaand we're done cb(); } return through2.obj(processSVG, processSprites); }; module.exports = spriteSVG;