Skip to content

Commit

Permalink
adds retina support
Browse files Browse the repository at this point in the history
  • Loading branch information
aslansky committed Mar 9, 2014
1 parent 6b980be commit 557588d
Show file tree
Hide file tree
Showing 25 changed files with 668 additions and 105 deletions.
12 changes: 10 additions & 2 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var opts = require('nomnom')
.option('base64', {
abbr: 'b',
flag: true,
help: 'instead of creating a sprite, write base64 encoded images to css (css file will be written to <out>)'
help: 'create css with base64 encoded sprite (css file will be written to <out>)'
})
.option('cssPath', {
abbr: 'c',
Expand All @@ -32,14 +32,19 @@ var opts = require('nomnom')
.option('name', {
abbr: 'n',
default: 'sprite.png',
help: 'name of the sprite file'
help: 'name of sprite file'
})
.option('processor', {
abbr: 'p',
choices: ['css', 'less', 'sass', 'scss', 'stylus'],
default: 'css',
help: 'output format of the css. one of css, less, sass, scss or stylus'
})
.option('retina', {
abbr: 'r',
flag: true,
help: 'generate both retina and standard sprites. src images have to be in retina resolution'
})
.option('style', {
abbr: 's',
help: 'file to write css to, if ommited no css is written'
Expand All @@ -58,6 +63,9 @@ var opts = require('nomnom')
default: 'vertical',
help: 'orientation of the sprite image'
})
.option('prefix', {
help: 'prefix for the class name used in css (without .)'
})
.script('css-sprite')
.parse();

Expand Down
17 changes: 12 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
'use strict';

var sprite = require('./lib/css-sprite');
var es = require('event-stream');
var through2 = require('through2');
var vfs = require('vinyl-fs');
var gfs = require('graceful-fs');
var fs = require('graceful-fs');
var mkdirp = require('mkdirp');
var path = require('path');
var replaceExtension = require('./lib/replace-extension');
var _ = require('lodash');
var noop = function () {};

var writeFile = function (file, cb) {
var writeFile = function (file, enc, cb) {
var stream = this;
mkdirp(file.base, function () {
gfs.writeFile(file.path, file.contents, cb);
fs.writeFile(file.path, file.contents, function () {
stream.push(file);
cb();
});
});
};

Expand All @@ -23,6 +28,7 @@ var defaults = {
cssPath: '../images',
processor: 'css',
orientation: 'vertical',
retina: false,
margin: 5
};

Expand All @@ -44,7 +50,8 @@ module.exports = {
}
vfs.src(opts.src)
.pipe(sprite(opts))
.pipe(es.map(writeFile))
.pipe(through2.obj(writeFile))
.on('data', noop)
.on('end', function () {
if (_.isFunction(cb)) {
cb();
Expand Down
181 changes: 124 additions & 57 deletions lib/css-sprite.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

var es = require('event-stream');
var through2 = require('through2');
var Canvas = require('canvas');
var lodash = require('lodash');
var path = require('path');
Expand All @@ -10,93 +10,160 @@ var imageinfo = require('imageinfo');
var replaceExtension = require('./replace-extension');
var Image = Canvas.Image;

// json2css template
json2css.addTemplate('sprite', require(path.join(__dirname, 'templates/sprite.js')));

module.exports = function (opt) {
opt = lodash.extend({}, {name: 'sprite.png', margin: 5, processor: 'css', cssPath: '../images', orientation: 'vertical'}, opt);
opt.styleExtension = (opt.processor === 'stylus') ? 'styl' : opt.processor;
var sprites = [];
var ctxHeight = 0;
var ctxWidth = 0;

function queueImages (file) {
function queue (file, img, cb) {
sprites.push({
'img': img,
'name': replaceExtension(file.relative, '').replace(/\/|\\|\ /g, '-'),
'x': opt.orientation === 'vertical' ? opt.margin : ctxWidth + opt.margin,
'y': opt.orientation === 'vertical' ? ctxHeight + opt.margin: opt.margin,
'width': img.width,
'height': img.height,
'image': path.join(opt.cssPath, opt.name)
});

if (opt.orientation === 'vertical') {
ctxHeight = ctxHeight + img.height + 2 * opt.margin;
if (img.width + 2 * opt.margin > ctxWidth) {
ctxWidth = img.width + 2 * opt.margin;
}
}
else {
ctxWidth = ctxWidth + img.width + 2 * opt.margin;
if (img.height + 2 * opt.margin > ctxHeight) {
ctxHeight = img.height + 2 * opt.margin;
}
}

cb();
}

function queueImages (file, enc, cb) {
if (file.isNull()) {
cb();
return; // ignore
}

if (file.isStream()) {
return this.emit('error', new Error('Streaming not supported'));
cb(new Error('Streaming not supported'));
return; // ignore
}

var queue = function (img) {
sprites.push({
'img': (!opt.base64) ? img : null,
'name': replaceExtension(file.relative, '').replace(/\/|\\/g, '-'),
'x': (!opt.base64) ? (opt.orientation === 'vertical' ? opt.margin : ctxWidth + opt.margin) : 0,
'y': (!opt.base64) ? (opt.orientation === 'vertical' ? ctxHeight + opt.margin: opt.margin) : 0,
'width': img.width,
'height': img.height,
'total_width': img.width,
'total_height': img.height,
'image': (!opt.base64) ? path.join(opt.cssPath, opt.name) : 'data:' + imageinfo(file.contents).mimeType + ';base64,' + file.contents.toString('base64')
});

if (!opt.base64) {
if (opt.orientation === 'vertical') {
ctxHeight = ctxHeight + img.height + 2 * opt.margin;
if (img.width + 2 * opt.margin > ctxWidth) {
ctxWidth = img.width + 2 * opt.margin;
}
}
else {
ctxWidth = ctxWidth + img.width + 2 * opt.margin;
if (img.height + 2 * opt.margin > ctxHeight) {
ctxHeight = img.height + 2 * opt.margin;
}
}
}
};

var img = new Image();
img.src = file.contents;
if (img.complete) {
queue(img);
queue(file, img, cb);
}
else {
img.onload = function () {
queue(img);
queue(file, img, cb);
};
}

}

function endStream () {
if (sprites.length === 0) {
return this.emit('end');
}
function createCanvas () {
var canvas = new Canvas(ctxWidth, ctxHeight);
if (!opt.base64) {
var ctx = canvas.getContext('2d');
lodash.each(sprites, function (sprite) {
sprite.total_width = ctxWidth;
sprite.total_height = ctxHeight;
ctx.drawImage(sprite.img, sprite.x, sprite.y, sprite.width, sprite.height);
var ctx = canvas.getContext('2d');
lodash.each(sprites, function (sprite) {
sprite.total_width = ctxWidth;
sprite.total_height = ctxHeight;
ctx.drawImage(sprite.img, sprite.x, sprite.y, sprite.width, sprite.height);
});
return canvas;
}

function createNonRetinaCanvas (retinaCanvas) {
var canvas = new Canvas(retinaCanvas.width / 2, retinaCanvas.height / 2);
var ctx = canvas.getContext('2d');
ctx.drawImage(retinaCanvas, 0, 0, retinaCanvas.width / 2, retinaCanvas.height / 2);
return canvas;
}

function createStyle (sprite, retinaSprite) {
if (retinaSprite) {
sprites.unshift({
name: retinaSprite.relative,
type: 'retina',
image: (!opt.base64) ? path.join(opt.cssPath, retinaSprite.relative) : 'data:' + imageinfo(retinaSprite.canvas.toBuffer()).mimeType + ';base64,' + retinaSprite.canvas.toBuffer().toString('base64'),
total_width: sprite.canvas.width,
total_height: sprite.canvas.height
});
lodash(sprites).each(function (sprite, i) {
sprites[i].x = sprite.x / 2;
sprites[i].y = sprite.y / 2;
sprites[i].width = sprite.width / 2;
sprites[i].height = sprite.height / 2;
});
}
if (opt.style || opt.base64) {
var css = json2css(sprites, {'format': opt.processor});
this.emit('data', new File({
base: (!opt.base64) ? path.dirname(opt.style) : opt.out,
path: (!opt.base64 || (opt.base64 && opt.style)) ? opt.style : path.join(opt.out, replaceExtension(opt.name, '.' + opt.processor)),
contents: new Buffer(css)
}));

sprites.unshift({
name: sprite.relative,
type: 'sprite',
image: (!opt.base64) ? path.join(opt.cssPath, sprite.relative) : 'data:' + imageinfo(sprite.canvas.toBuffer()).mimeType + ';base64,' + sprite.canvas.toBuffer().toString('base64'),
total_width: sprite.canvas.width,
total_height: sprite.canvas.height
});
return json2css(sprites, {'format': 'sprite', formatOpts: {'cssClass': opt.prefix, 'processor': opt.processor}});
}

function createSprite (cb) {
var sprite, nonRetinaSprite, style;
if (sprites.length === 0) {
cb();
return; // ignore
}
if (!opt.base64) {
this.emit('data', new File({
sprite = {
base: opt.out,
relative: opt.name,
path: path.join(opt.out, opt.name),
canvas: createCanvas()
};

if (opt.retina) {
sprite.path = replaceExtension(sprite.path, '') + '-x2.png';
sprite.relative = replaceExtension(sprite.relative, '') + '-x2.png';
nonRetinaSprite = {
base: opt.out,
relative: opt.name,
path: path.join(opt.out, opt.name),
contents: new Buffer(canvas.toBuffer())
canvas: createNonRetinaCanvas(sprite.canvas)
};
}

if (!opt.base64) {
if (opt.retina) {
this.push(new File({
base: nonRetinaSprite.base,
path: nonRetinaSprite.path,
contents: new Buffer(nonRetinaSprite.canvas.toBuffer())
}));
}
this.push(new File({
base: sprite.base,
path: sprite.path,
contents: new Buffer(sprite.canvas.toBuffer())
}));
}

if (opt.style || opt.base64) {
style = opt.retina ? createStyle(nonRetinaSprite, sprite) : createStyle(sprite);
this.push(new File({
base: !opt.base64 ? path.dirname(opt.style) : opt.out,
path: opt.style ? opt.style : path.join(opt.out, replaceExtension(opt.name, '.' + opt.styleExtension)),
contents: new Buffer(style)
}));
}
this.emit('end');
cb();
}

return es.through(queueImages, endStream);
return through2.obj(queueImages, createSprite);
};
23 changes: 23 additions & 0 deletions lib/templates/css.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{{#sprite}}
{{class}} {
background-image: url('{{{escaped_image}}}');
}

{{/sprite}}
{{#retina}}
@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) {
{{class}} {
background-image: url('{{{escaped_image}}}');
background-size: {{px.total_width}} {{px.total_height}};
}
}

{{/retina}}
{{#items}}
{{class}} {
background-position: {{px.offset_x}} {{px.offset_y}};
width: {{px.width}};
height: {{px.height}};
}

{{/items}}
41 changes: 41 additions & 0 deletions lib/templates/less.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{{#items}}
@{{name}}: {{px.offset_x}}, {{px.offset_y}}, {{px.width}}, {{px.height}};
{{/items}}

.sprite-width(@sprite) {
width: extract(@sprite, 3);
}

.sprite-height(@sprite) {
height: extract(@sprite, 4);
}

.sprite-position(@sprite) {
@sprite-offset-x: extract(@sprite, 1);
@sprite-offset-y: extract(@sprite, 2);
background-position: @sprite-offset-x @sprite-offset-y;
}

.sprite(@sprite) {
.sprite-position(@sprite);
background-repeat: no-repeat;
overflow: hidden;
display: block;
.sprite-width(@sprite);
.sprite-height(@sprite);
}

{{#sprite}}
{{class}} {
background-image: url('{{{escaped_image}}}');
}

{{/sprite}}
{{#retina}}
@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) {
{{class}} {
background-image: url('{{{escaped_image}}}');
background-size: {{px.total_width}} {{px.total_height}};
}
}
{{/retina}}
Loading

0 comments on commit 557588d

Please sign in to comment.