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