forked from frontendbeast/gulp-svg-spritesheet
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Darren Hall
committed
Nov 3, 2014
0 parents
commit d116776
Showing
8 changed files
with
592 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
demo.html | ||
sprite.scss | ||
test.svg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>Gulp Sprite SVG generator</title> | ||
<style> | ||
.sprite { | ||
font-size: 1em; | ||
} | ||
.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 { | ||
background-position: {{x}}{{units}} {{y}}{{units}}; | ||
width: {{w}}{{units}}; | ||
height: {{h}}{{units}}; | ||
} | ||
{{/sprites}} | ||
</style> | ||
</head> | ||
<body> | ||
{{#sprites}} | ||
<div class="sprite sprite__{{fileName}}"></div> | ||
{{/sprites}} | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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('<?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" />', { 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 = $('<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(" "); | ||
|
||
// Set sprite data to be used by the positioning function | ||
var sprite = { | ||
fileName: filename, | ||
file: $file.contents(), | ||
h: parseFloat(coords[3]), | ||
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(coords[2]) | ||
}; | ||
|
||
// 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); | ||
|
||
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); | ||
} | ||
|
||
return through2.obj(processSVG, processSprites); | ||
}; | ||
|
||
module.exports = spriteSVG; |
Oops, something went wrong.