UNPKG

coffee-sprites

Version:

CoffeeScript/JavaScript stylesheet-based sprite image engine.

537 lines (498 loc) 16.1 kB
// Generated by CoffeeScript 1.4.0 var CoffeeSprites, Image, Sprite, async, fs, gd, instance, log, path, spawn, sprite_count; gd = require('node-gd'); async = require('async2'); fs = require('fs'); path = require('path'); instance = undefined; spawn = require('child_process').spawn; log = function(i, m) { var l; l = instance.o.logger; (l.length === 2 && l(i, m)) || l(m); }; CoffeeSprites = (function() { function CoffeeSprites(o) { instance = this; o = o || {}; o.image_path = o.image_path || ''; o.sprite_path = o.sprite_path || ''; o.sprite_url = o.sprite_url || ''; o.logger = o.logger || console.log; o.manifest_file = o.manifest_file || path.join(o.sprite_path, 'sprite-manifest.json'); this.o = o; this.reset(); } CoffeeSprites.prototype.reset = function() { this.sprites = {}; return this._read_manifest = false; }; CoffeeSprites.prototype.read_manifest = function() { var abspath, data, file, i, name, sprite, _ref, _ref1; if (!this._read_manifest) { this._read_manifest = true; if (fs.existsSync(this.o.manifest_file)) { data = (JSON.parse(fs.readFileSync(this.o.manifest_file))) || {}; _ref = data.sprites; for (name in _ref) { sprite = _ref[name]; this.sprites[name] = new Sprite(name, sprite.options); _ref1 = sprite.images; for (i in _ref1) { file = _ref1[i]; abspath = path.join(this.o.image_path, sprite.options.path || '', file + '.png'); if (fs.existsSync(abspath)) { this.sprites[name].add(file); } } } } } }; CoffeeSprites.prototype.write_manifest = function() { var count, data, file, name; data = { sprites: {} }; count = 0; for (name in this.sprites) { data.sprites[name] = { options: this.sprites[name].o, images: [] }; for (file in this.sprites[name].images) { data.sprites[name].images.push(file); count++; } } if (count) { fs.writeFileSync(this.o.manifest_file, JSON.stringify(data, null, 2)); log('success', "wrote " + (path.relative(process.cwd(), this.o.manifest_file))); } }; CoffeeSprites.prototype.extend = function(engine) { var g, generate_placeholder, _this = this; g = engine.o.globals; generate_placeholder = function(key, name, png) { _this.read_manifest(); if (typeof png !== 'undefined') { _this.sprites[name].add(png); } return "SPRITE_" + key + "_PLACEHOLDER(" + name + ", " + (png || '') + ")"; }; g.sprite_map = function(name, options) { var k, sprite; _this.read_manifest(); if (_this.sprites[name]) { for (k in options) { _this.sprites[name].o[k] = options[k]; } } else { sprite = new Sprite(name, options); _this.sprites[name] = sprite; } return name; }; g.sprite = function(sprite, png) { return generate_placeholder('URL_AND_IMAGE_POSITION', sprite, png); }; g.sprite_url = function(sprite, png) { return generate_placeholder('URL', sprite, png); }; g.sprite_position = function(sprite, png) { return generate_placeholder('POSITION', sprite, png); }; g.sprite_width = function(sprite, png) { return generate_placeholder('WIDTH', sprite, png); }; g.sprite_height = function(sprite, png) { return generate_placeholder('HEIGHT', sprite, png); }; engine.on.end = function(css, cb) { var flow, name, sprite, _fn, _ref; flow = new async(); _ref = _this.sprites; _fn = function(sprite) { return flow.series(function() { return sprite.render(this); }); }; for (name in _ref) { sprite = _ref[name]; _fn(sprite); } flow["finally"](function(err, changes) { if (err) { return cb(err); } css = css.replace(/SPRITE_(.+?)_PLACEHOLDER\((.+?), (.*?)\)/g, function(match, key, name, png) { var image; sprite = _this.sprites[name]; image = sprite.images[png]; switch (key) { case 'POSITION': return image.coords(); case 'URL': return sprite.digest_url(image.tileset()); case 'URL_AND_IMAGE_POSITION': return "url(" + (sprite.digest_url(image.tileset())) + ") " + (image.coords()); case 'WIDTH': return image.px(image.w); case 'HEIGHT': return image.px(image.h); } }); _this.write_manifest(); instance.reset(); cb(null, css); }); }; }; return CoffeeSprites; })(); Image = (function() { function Image(sprite, file) { this.sprite = sprite; this.file = file; this.absfile = path.join(instance.o.image_path, this.file + '.png'); this.src = undefined; this.x = 0; this.y = 0; this.w = 0; this.h = 0; } Image.prototype.toString = function() { return "Image#file=" + this.file + ",x=" + this.x + ",y=" + this.y + ",width=" + this.w + ",height=" + this.h; }; Image.prototype.read = function(cb) { var _this = this; return gd.openPng(this.absfile, function(err, src) { if (err) { return cb(err); } _this.src = src; _this.w = src.width; _this.h = src.height; return cb(null, _this); }); }; Image.prototype.basename = function() { return path.basename(this.absfile, '.png'); }; Image.prototype.repeat = function() { var repeat; switch (repeat = this.sprite.o[this.basename() + '-repeat'] || 'no-repeat') { case 'no-repeat': case 'repeat-x': case 'repeat-y': break; default: throw err("WARN: " + repeat + " is an invalid repeat value"); } return repeat; }; Image.prototype.tileset = function() { var repeat; if ((repeat = this.repeat()) === 'no-repeat' && this.sprite.o.layout === 'smart') { return 'smart'; } else { return repeat; } }; Image.prototype.px = function(i) { if (i === 0) { return 0; } else { return i + 'px'; } }; Image.prototype.coords = function() { return this.px(this.x * -1) + ' ' + this.px(this.y * -1); }; return Image; })(); sprite_count = 0; Sprite = (function() { function Sprite(name, o) { if (typeof name !== 'string') { o = name; name = ''; } this.name = name || 'sprite-' + (++sprite_count); o = o || {}; o.layout = o.layout || 'smart'; this.images = {}; this.tilesets = {}; this.tileset_types = ['smart', 'no-repeat', 'repeat-x', 'repeat-y']; this.digest = ''; this.o = o; return; } Sprite.prototype.add = function(file) { var k; if (typeof this.images[file] === 'undefined') { this.images[file] = new Image(this, path.join(this.o.path || '', file)); k = this.images[file].tileset(); if (typeof this.tilesets[k] === 'undefined') { this.tilesets[k] = { images: [], digest: '', digest_file: '', src: undefined, w: 0, h: 0 }; } this.tilesets[k].images.push(this.images[file]); } return this.images[file]; }; Sprite.prototype.render = function(cb) { var count, k, position_and_pack, read, render_to_disk, sprite, _this = this; sprite = this; count = 0; for (k in sprite.images) { count++; } if (count < 1) { return cb(null, "sprite map \"" + sprite.name + "\" has no images."); } read = function() { var flow, image, tileset, type, _fn, _ref, _ref1; flow = async["new"](); _ref = sprite.tilesets; for (type in _ref) { tileset = _ref[type]; _ref1 = tileset.images; _fn = function(image) { return flow.series(function() { return image.read(this); }); }; for (k in _ref1) { image = _ref1[k]; _fn(image); } } return flow["finally"](function(err) { if (err) { return cb(err); } return position_and_pack(); }); }; position_and_pack = function() { var GrowingPacker, changes, image, packer, sort, tileset, type, _ref, _ref1, _ref2, _ref3, _ref4; sprite.o.spacing = sprite.o.spacing || 0; changes = true; _ref = sprite.tilesets; for (type in _ref) { tileset = _ref[type]; if (type === 'smart') { _ref1 = tileset.images; for (k in _ref1) { image = _ref1[k]; image._w = image.w; image.w += sprite.o.spacing; image._h = image.h; image.h += sprite.o.spacing; } 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); }, maxside: function(a, b) { var c, diff, n; c = ["max", "min", "h", "w"]; n = 0; while (n < c.length) { diff = sort[c[n]](a, b); if (diff !== 0) { return diff; } n++; } return 0; } }; tileset.images.sort(sort.maxside); GrowingPacker = require('../vendor/packer.growing.js'); packer = new GrowingPacker(); packer.fit(tileset.images); tileset.w = packer.root.w; tileset.h = packer.root.h; _ref2 = tileset.images; for (k in _ref2) { image = _ref2[k]; if (!image.fit) { continue; } image.x = image.fit.x; image.y = image.fit.y; image.w = image._w; image.h = image._h; } } else if (type === 'repeat-y') { _ref3 = tileset.images; for (k in _ref3) { image = _ref3[k]; tileset.h = Math.max(tileset.h, image.h); image.x = tileset.w; tileset.w += image.w + sprite.o.spacing; } } else if (type === 'repeat-x' || type === 'no-repeat') { _ref4 = tileset.images; for (k in _ref4) { image = _ref4[k]; tileset.w = Math.max(tileset.w, image.w); image.y = tileset.h; tileset.h += image.h + sprite.o.spacing; } } tileset.digest = sprite.calc_digest(type); changes &= !fs.existsSync(tileset.digest_file = sprite.digest_file(type)); } if (!changes) { return cb(null, "no changes in sprite(s)."); } return render_to_disk(); }; render_to_disk = function() { var flow, tileset, type, _ref; flow = new async; _ref = sprite.tileset_types; for (k in _ref) { type = _ref[k]; if (tileset = sprite.tilesets[type]) { (function(type, tileset) { return flow.serial(function() { var file, files, image, next, outfile, pattern, transparency, x, y, _i, _j, _k, _len, _ref1, _ref2, _ref3, _ref4, _ref5, _this = this; next = this; tileset.src = gd.createTrueColor(tileset.w, tileset.h); transparency = tileset.src.colorAllocateAlpha(0, 0, 0, 127); tileset.src.fill(0, 0, transparency); tileset.src.colorTransparent(transparency); tileset.src.alphaBlending(0); tileset.src.saveAlpha(1); count = 0; _ref1 = tileset.images; for (k in _ref1) { image = _ref1[k]; count++; switch (type) { case 'no-repeat': case 'smart': image.src.copy(tileset.src, image.x, image.y, 0, 0, image.w, image.h); break; case 'repeat-x': for (x = _i = 0, _ref2 = tileset.w, _ref3 = image.w; 0 <= _ref2 ? _i < _ref2 : _i > _ref2; x = _i += _ref3) { image.src.copy(tileset.src, x, image.y, 0, 0, image.w, image.h); } break; case 'repeat-y': for (y = _j = 0, _ref4 = tileset.h, _ref5 = image.h; 0 <= _ref4 ? _j < _ref4 : _j > _ref4; y = _j += _ref5) { image.src.copy(tileset.src, image.x, y, 0, 0, image.w, image.h); } } } pattern = tileset.digest_file.replace(/-[\w\d+]+\.png$/, '-*.png'); files = require('glob').sync(pattern); for (_k = 0, _len = files.length; _k < _len; _k++) { file = files[_k]; fs.unlinkSync(file); } outfile = instance.o.pngcrush ? tileset.digest_file + '.tmp' : tileset.digest_file; outfile = tileset.digest_file + (instance.o.pngcrush ? '.tmp' : ''); log('pending', "writing " + count + " images to " + (path.relative(process.cwd(), tileset.digest_file)) + "..."); return tileset.src.savePng(outfile, 0, function() { var p, stdout; if (instance.o.pngcrush) { log('pending', "pngcrush " + (path.relative(process.cwd(), tileset.digest_file)) + "..."); p = spawn(instance.o.pngcrush, ['-rem', 'alla', '-reduce', '-brute', outfile, tileset.digest_file]); stdout = ''; p.stdout.on('data', function(data) { return stdout = data; }); p.stderr.on('data', function(err) { if (err) { return next(err); } }); return p.on('exit', function(code) { if (fs.existsSync(outfile)) { fs.unlinkSync(outfile); } if (code !== 0) { return next("pngcrush exited with code " + code + ". " + stdout); } return next(null, true); }); } else { return next(null, true); } }); }); })(type, tileset); } } flow["finally"](function(err) { if (err) { return cb(err); } return cb(null); }); }; read(); }; Sprite.prototype.calc_digest = function(type) { var b, image, k, _ref; b = { o: [], i: [] }; for (k in this.o) { b.o.push(k + ':' + this.o[k]); } _ref = this.tilesets[type].images; for (k in _ref) { image = _ref[k]; b.i.push(image.basename()); } b = b.o.sort().join('|') + '|' + b.i.sort().join('|'); return require('crypto').createHash('md5').update(b).digest('hex').substr(-10); }; Sprite.prototype.suffix = function(s) { return { 'smart': '', 'no-repeat': '', 'repeat-x': '-x', 'repeat-y': '-y' }[s]; }; Sprite.prototype.digest_file = function(type) { return path.join(instance.o.sprite_path, "" + this.name + (this.suffix(type)) + "-" + this.tilesets[type].digest + ".png"); }; Sprite.prototype.digest_url = function(type) { return path.join(instance.o.sprite_url, "" + this.name + (this.suffix(type)) + "-" + this.tilesets[type].digest + ".png"); }; return Sprite; })(); module.exports = function(options) { new CoffeeSprites(options); return function(engine) { instance.extend(engine); return instance; }; };