diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78762fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +demo.html +sprite.scss +test.svg diff --git a/demo.tpl b/demo.tpl new file mode 100644 index 0000000..678ce99 --- /dev/null +++ b/demo.tpl @@ -0,0 +1,36 @@ + + + + Gulp Sprite SVG generator + + + + {{#sprites}} +
+ {{/sprites}} + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..f7857f5 --- /dev/null +++ b/index.js @@ -0,0 +1,323 @@ +'use strict'; + +var cheerio = require('cheerio'), + events = require('events'), + fs = require('fs'), + gutil = require('gulp-util'), + mustache = require('mustache'), + packetr = require('./lib/packer.growing'), + path = require('path'), + through2 = require('through2'); + +// Consts +var PLUGIN_NAME = 'gulp-svg-sprite'; + +// 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 +}; + +// Sorting functions from +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('', { xmlMode: true }), + $sprite = $('svg'), + // This data will be passed to our template + data = { + cssPathSvg: options.cssPathSvg, + height: 0, + sprites: [], + units: options.units, + width: 0 + }, + eventEmitter = new events.EventEmitter(), + self, + x = options.x, + y = options.y; + + // 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 = $('') + .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 = $('') + .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 0 ? blocks[0].w+blocks[0].padding : 0; + var h = len > 0 ? blocks[0].h+blocks[0].padding : 0; + + this.root = { x: 0, y: 0, w: w, h: h }; + for (n = 0; n < len ; n++) { + block = blocks[n]; + if (node = this.findNode(this.root, block.w+block.padding, block.h+block.padding)) + block.fit = this.splitNode(node, block.w+block.padding, block.h+block.padding); + else + block.fit = this.growNode(block.w+block.padding, block.h+block.padding); + } + }, + + findNode: function(root, w, h) { + if (root.used) + return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); + else if ((w <= root.w) && (h <= root.h)) + return root; + else + return null; + }, + + splitNode: function(node, w, h) { + node.used = true; + node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h }; + node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h }; + return node; + }, + + growNode: function(w, h) { + var canGrowDown = (w <= this.root.w); + var canGrowRight = (h <= this.root.h); + + var shouldGrowRight = canGrowRight && (this.root.h >= (this.root.w + w)); // attempt to keep square-ish by growing right when height is much greater than width + var shouldGrowDown = canGrowDown && (this.root.w >= (this.root.h + h)); // attempt to keep square-ish by growing down when width is much greater than height + + if (shouldGrowRight) + return this.growRight(w, h); + else if (shouldGrowDown) + return this.growDown(w, h); + else if (canGrowRight) + return this.growRight(w, h); + else if (canGrowDown) + return this.growDown(w, h); + else + return null; // need to ensure sensible root starting size to avoid this happening + }, + + growRight: function(w, h) { + this.root = { + used: true, + x: 0, + y: 0, + w: this.root.w + w, + h: this.root.h, + down: this.root, + right: { x: this.root.w, y: 0, w: w, h: this.root.h } + }; + if (node = this.findNode(this.root, w, h)) + return this.splitNode(node, w, h); + else + return null; + }, + + growDown: function(w, h) { + this.root = { + used: true, + x: 0, + y: 0, + w: this.root.w, + h: this.root.h + h, + down: { x: 0, y: this.root.h, w: this.root.w, h: h }, + right: this.root + }; + if (node = this.findNode(this.root, w, h)) + return this.splitNode(node, w, h); + else + return null; + } + +} + diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ef4dab --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "gulp-svg-sprite", + "description": "Gulp SVG sprite generator", + "repository": "https://github.com/iamdarrenhall/gulp-svg-sprite", + "version": "0.0.1", + "devDependencies": { + "gulp": "^3.8.9" + }, + "dependencies": { + "cheerio": "^0.17.0", + "gulp-util": "^3.0.1", + "mustache": "^0.8.2", + "through2": "^0.6.3" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..aa28aa1 --- /dev/null +++ b/readme.md @@ -0,0 +1 @@ +#gulp-svg-sprite \ No newline at end of file diff --git a/template.tpl b/template.tpl new file mode 100644 index 0000000..71467c0 --- /dev/null +++ b/template.tpl @@ -0,0 +1,23 @@ +// Auto-generated by gulp-svg-sprite + +%sprite:before { + content: ' '; + background-image: url("{{{cssPathSvg}}}"); + background-repeat: no-repeat; + background-size: {{width}}{{units}} {{height}}{{units}}; + display: inline-block; + {{#cssPathNoSvg}} + .no-svg & { + background-image: url("{{{cssPathNoSvg}}}"); + } + {{/cssPathNoSvg}} +} + +{{#sprites}} +.sprite__{{fileName}}:before { + @extend %sprite; + background-position: {{x}}{{units}} {{y}}{{units}}; + width: {{width}}{{units}}; + height: {{height}}{{units}}; +} +{{/sprites}} \ No newline at end of file diff --git a/test/gulpfile.js b/test/gulpfile.js new file mode 100644 index 0000000..3dc4436 --- /dev/null +++ b/test/gulpfile.js @@ -0,0 +1,16 @@ +var gulp = require('gulp'), + svgmin = require('gulp-svgmin'), + svgsprite = require('../index'); + +gulp.task('default', function () { + gulp.src('icons/*.svg') + .pipe(svgsprite({ + cssPathNoSvg: './test.png', + demoDest: './demo.html', + padding: 0, + positioning: 'packed', + units: 'em' + })) + .pipe(svgmin()) + .pipe(gulp.dest('test.svg')); +}) \ No newline at end of file