easy-sprite
Advanced tools
Comparing version 0.0.1 to 0.0.2
/** | ||
* @fileoverview Exports sprite api。 | ||
* @fileoverview Exports sprite api. | ||
* @author vinnyguitar@126.com | ||
*/ | ||
module.exports = require('./lib/sprite.js').sprite; |
/** | ||
* @fileoverview This file is used for read read sprite comment from css file and change the background image。 | ||
* @fileoverview This file offers method to create sprite image. | ||
* @author vinnyguitar@126.com | ||
*/ | ||
'use strict'; | ||
var css = require('css'), | ||
fs = require('fs'), | ||
url = require('url'), | ||
var fs = require('fs'), | ||
async = require('async'), | ||
lwip = require('lwip'), | ||
path = require('path'), | ||
extend = require('extend'), | ||
generator = require('./generator.js'), | ||
q = require('q'); | ||
var SPRITE_DEF_REG = /@sprite:\s*\[(.+)\]\s*=>\s*(.+\.png)\??(.*)/; | ||
var PARAM_REG = /^(alg|selector)\s*=\s*(.+)/; | ||
var BG_URL_REG = /url\((.+)\)/; | ||
var PNG2X_REG = generator.PNG2X_REG; | ||
var DEFAULT_MARGIN = 10; | ||
var MEDIA2X = 'only screen and (-webkit-min-device-pixel-ratio: 2),' | ||
+'only screen and (min--moz-device-pixel-ratio: 2),' | ||
+'only screen and (-o-min-device-pixel-ratio: 2/1),' | ||
+'only screen and (min-device-pixel-ratio: 2),' | ||
+'only screen and ( min-resolution: 192dpi),' | ||
+'only screen and (min-resolution: 2dppx)'; | ||
var PNG_REG = /\.png$/; | ||
var PNG2X_REG = /(@|-|_)2x\.png$/; | ||
/** | ||
* Collect sprite definition from css rules. | ||
* @param {Array} rules Css rules. | ||
* @return {Array} | ||
* @enum Sprite image package algorithms. | ||
* @type {{BINARY: number, VERTICAL: number, HORIZONTAL: number}} | ||
*/ | ||
function collectSpriteDef(rules) { | ||
var result = []; | ||
rules.forEach(function(rule) { | ||
if(rule.type == 'comment' && SPRITE_DEF_REG.test(rule.comment.replace(/['"]/g, ''))) { | ||
var src = RegExp.$1, | ||
target = RegExp.$2, | ||
params = RegExp.$3; | ||
var item = { | ||
rule: rule, | ||
src: src.replace(/\s/, '').split(',') | ||
}; | ||
if(PNG2X_REG.test(target)) { | ||
item.target = target.replace(PNG2X_REG, '.png'); | ||
item.target2x = target; | ||
var Algorithm = { | ||
BINARY: 1, | ||
VERTICAL: 2, | ||
HORIZONTAL: 3 | ||
}; | ||
/** | ||
* Fix odd pixel to even. | ||
*/ | ||
function fixToEven(pixel) { | ||
return pixel % 2 != 0 ? pixel + 1 : pixel; | ||
}; | ||
/** | ||
* Comparator by width. | ||
*/ | ||
function byWidth(image1, image2) { | ||
return image2.width - image1.width; | ||
}; | ||
/** | ||
* Comparator by height. | ||
*/ | ||
function byHeight(image1, image2) { | ||
return image2.height - image1.height; | ||
}; | ||
/** | ||
* Read png data from files. | ||
* @param {Array} files Files to read. | ||
* @returns {Promise} | ||
*/ | ||
function readPngs(files) { | ||
var promises, | ||
i = 0; | ||
while(i < files.length) {// Read all png files recursively. | ||
var filepath = files[i], | ||
stat = fs.statSync(filepath); | ||
if(stat.isFile(filepath) && PNG_REG.test(filepath)) { | ||
i++; | ||
}else{ | ||
files.splice(i, 1); | ||
if(stat.isDirectory()) { | ||
Array.prototype.push.apply(files, fs.readdirSync(filepath).map(function(name) { | ||
return path.resolve(filepath, name); | ||
})); | ||
} | ||
} | ||
} | ||
promises = files.map(function(filepath) { | ||
var deferred = q.defer(); | ||
lwip.open(filepath, function(err, image) { | ||
if(err) { | ||
deferred.reject('Open ' + filepath + ' error.'); | ||
} else { | ||
item.target = target; | ||
} | ||
if(params) { | ||
var arr = params.split('&'); | ||
arr.forEach(function(param) { | ||
if(PARAM_REG.test(param)) { | ||
item[RegExp.$1] = RegExp.$2.trim(); | ||
} | ||
deferred.resolve({ | ||
file: filepath, | ||
image: image, | ||
width: image.width(), | ||
height: image.height() | ||
}); | ||
if(item.alg == 'b' || item.alg == 'binary') { | ||
item.alg = generator.Algorithm.BINARY; | ||
} else if(item.alg == 'v' || item.alg == 'vertical') { | ||
item.alg = generator.Algorithm.VERTICAL; | ||
} else if(item.alg == 'h' || item.alg == 'horizontal') { | ||
item.alg = generator.Algorithm.HORIZONTAL; | ||
} | ||
} | ||
result.push(item); | ||
} | ||
}); | ||
return deferred.promise; | ||
}); | ||
return result; | ||
return q.all(promises); | ||
}; | ||
/** | ||
* Parse css and generate sprite. | ||
* @param options | ||
* @config src Src css file. | ||
* @config dest Output file of parsed css. | ||
* @config spriteDir Output directory for sprite image. | ||
* @config margin Margin between image. | ||
* @returns {*} | ||
* Arrange images using a binary tree algorithm. | ||
* @param {Array} Images to arrange. | ||
* @param {Number} margin Margin between image. | ||
* @returns {{width: Number, height: Number}} | ||
*/ | ||
function sprite(options, callback) { | ||
var token = css.parse(fs.readFileSync(options.src).toString('utf8')), | ||
rules = token.stylesheet.rules, | ||
srcDir = path.dirname(options.src), | ||
defList = collectSpriteDef(rules), | ||
defMap = {}, | ||
bgImgToSpriteMap = {}, | ||
selectorMap = {}, | ||
bgMap = {}; | ||
if(typeof callback !== 'function') callback = function(){}; | ||
//generate sprite image for all def. | ||
var promises = defList.map(function(def) { | ||
//To absolute path. | ||
def.src = def.src.map(formatPath); | ||
def.target = path.resolve(options.spriteDir, def.target); | ||
if(def.target2x) { | ||
def.target2x = path.resolve(options.spriteDir, def.target2x); | ||
} | ||
defMap[def.target] = def; | ||
function binaryArrange(images, margin) { | ||
var queue = images.slice(0), | ||
width = 0, | ||
margin = margin, | ||
maxWidth = 0, | ||
height = Number.MAX_VALUE; | ||
queue.sort(byWidth); | ||
queue.forEach(function(image) { | ||
image.width = fixToEven(image.width) + margin; | ||
image.height = fixToEven(image.height) + margin; | ||
maxWidth = Math.max(maxWidth, image.width); | ||
}); | ||
return generator.generateSprite( | ||
def, | ||
options.margin || DEFAULT_MARGIN) | ||
.then(function(mapping) { | ||
extend(bgImgToSpriteMap, mapping); | ||
}); | ||
fillRectangle(0, 0, maxWidth * 2, height); | ||
width = 0; | ||
height = 0; | ||
images.forEach(function(image) { | ||
image.width -= margin; | ||
image.height -= margin; | ||
width = Math.max(width, image.x + image.width); | ||
height = Math.max(height, image.y + image.height); | ||
}); | ||
return { | ||
width: width, | ||
height: height | ||
}; | ||
///////////////////////////////// | ||
function fillRectangle(x1, y1, x2, y2) { | ||
var image = getImage(x2 - x1, y2 - y1); | ||
if(!image) return; | ||
image.x = x1; | ||
image.y = y1; | ||
fillRectangle(fixToEven(x1 + image.width), fixToEven(y1), x2, y1 + image.height); | ||
return fillRectangle(fixToEven(x1), fixToEven(y1) + image.height, x2, y2); | ||
}; | ||
return q.all(promises) | ||
.then(function() { | ||
var spriteMap = {}; | ||
//Update background position for the rule that use sprite. | ||
rules.forEach(function(rule) { | ||
if(rule.type == 'rule') { | ||
var sprite, selectors, | ||
dec = getBgDec(rule), | ||
key = dec && path.resolve(srcDir, dec.url); | ||
if(sprite = bgImgToSpriteMap[key]) {//use sprite | ||
spriteMap[sprite.sprite] = sprite; | ||
dec.dec.property = 'background-position'; | ||
if(sprite.sprite2x) { | ||
dec.dec.value = bgPosition(sprite.x / 2, sprite.y / 2); | ||
function getImage(widthLimit, heightLimit) { | ||
for(var i = 0; i < queue.length; i++) { | ||
var image = queue[i]; | ||
if(image.width <= widthLimit && image.height <= heightLimit){ | ||
queue.splice(i, 1); | ||
return image; | ||
} | ||
} | ||
}; | ||
}; | ||
/** | ||
* Arrange images vertically. | ||
* @param {Array} Images to arrange. | ||
* @param {Number} margin Margin between image. | ||
* @returns {{width: Number, height: Number}} | ||
*/ | ||
function verticalArrange(images, margin) { | ||
var width = 0, | ||
height = 0, | ||
queue = images.slice(0); | ||
queue.sort(byWidth); | ||
queue.forEach(function(image) { | ||
image.width = fixToEven(image.width); | ||
image.height = fixToEven(image.height); | ||
image.x = 0; | ||
image.y = height; | ||
width = Math.max(image.width, width); | ||
height = height + image.height + margin; | ||
}); | ||
return { | ||
width: width, | ||
height: height - margin | ||
} | ||
}; | ||
/** | ||
* Arrange images horizontally. | ||
* @param {Array} Images to arrange. | ||
* @param {Number} margin Margin between image. | ||
* @returns {{width: Number, height: Number}} | ||
*/ | ||
function horizontalArrange(images, margin) { | ||
var width = 0, | ||
height = 0, | ||
queue = images.slice(0); | ||
queue.sort(byHeight); | ||
queue.forEach(function(image) { | ||
image.width = fixToEven(image.width); | ||
image.height = fixToEven(image.height); | ||
image.x = width; | ||
image.y = 0; | ||
height = Math.max(image.height, height); | ||
width = width + image.width + margin; | ||
}); | ||
return { | ||
width: width - margin, | ||
height: height | ||
} | ||
}; | ||
/** | ||
* Create a sprite image. | ||
* @param {Object} def Definition of sprite. | ||
* @param {Number} margin Margin between image. | ||
* @return {Promise} | ||
*/ | ||
function create(def, margin) { | ||
return readPngs(def.src) | ||
.then(function(images) { | ||
var size, | ||
deferred = q.defer(); | ||
switch(def.alg) { | ||
case Algorithm.HORIZONTAL: | ||
size = horizontalArrange(images, margin); | ||
break; | ||
case Algorithm.VERTICAL: | ||
size = verticalArrange(images, margin); | ||
break; | ||
default: | ||
size = binaryArrange(images, margin); | ||
break; | ||
} | ||
async.waterfall([ | ||
function(cb) { | ||
lwip.create(size.width, size.height, cb); | ||
}, | ||
function(sprite, cb) { | ||
var result = {}, | ||
batch = sprite.batch(); | ||
images.forEach(function(item) { | ||
batch.paste(item.x, item.y, item.image); | ||
result[item.file] = { | ||
sprite: def.target, | ||
sprite2x: def.target2x, | ||
width: item.width, | ||
height: item.height, | ||
x: item.x, | ||
y: item.y, | ||
spriteWidth: size.width, | ||
spriteHeight: size.height | ||
}; | ||
}); | ||
batch.writeFile(def.target2x || def.target, function(err) { | ||
if(def.target2x) { | ||
sprite.batch() | ||
.scale(0.5, 0.5) | ||
.writeFile(def.target, function(err) { | ||
cb(err, result); | ||
}); | ||
} else { | ||
dec.dec.value = bgPosition(sprite.x, sprite.y); | ||
cb(err, result); | ||
} | ||
if(!defMap[sprite.sprite].selector) {//Not use specific selector invoke background. | ||
if(selectors = bgMap[sprite.sprite]) { | ||
Array.prototype.push.apply(selectors, rule.selectors); | ||
} else { | ||
selectors = rule.selectors.slice(0); | ||
} | ||
bgMap[sprite.sprite] = selectors; | ||
} | ||
} else { | ||
rule.selectors.forEach(function(selector) { | ||
selectorMap[selector] = rule; | ||
}); | ||
} | ||
} | ||
}); | ||
var rule2x = [], | ||
media2x = { | ||
type: 'media', | ||
media: MEDIA2X, | ||
parent: token | ||
}; | ||
defList.forEach(function(def) { | ||
var rule, selectors, | ||
sprite = spriteMap[def.target], | ||
bgSize2x = sprite.spriteWidth / 2 + 'px ' + sprite.spriteHeight / 2 + 'px '; | ||
if(def.target2x) { | ||
var r2x = { | ||
parent: media2x, | ||
type: 'rule', | ||
declarations: [] | ||
}; | ||
r2x.declarations.push({ | ||
parent: r2x, | ||
type: 'declaration', | ||
property: 'background-image', | ||
value: bgUrl(def.target2x) | ||
}); | ||
r2x.declarations.push({ | ||
parent:r2x, | ||
type: 'declaration', | ||
property: 'background-size', | ||
value: bgSize2x | ||
}); | ||
rule2x.push(r2x); | ||
} | ||
if(def.selector && (rule = selectorMap[def.selector])) { | ||
rule.declarations.push({ | ||
parent: rule, | ||
property: 'background', | ||
type: 'declaration', | ||
value: bgUrl(def.target) | ||
}); | ||
r2x && (r2x.selectors = [def.selector]); | ||
rules.splice(rules.indexOf(def.rule), 1); | ||
} else if(selectors = bgMap[def.target]) { | ||
def.rule.type = 'rule'; | ||
def.rule.comment = ''; | ||
def.rule.selectors = selectors; | ||
def.rule.declarations = [{ | ||
parent: def.rule, | ||
type: 'declaration', | ||
property: 'background', | ||
value: bgUrl(def.target) | ||
}]; | ||
r2x && (r2x.selectors = selectors); | ||
], function(err, result) { | ||
if(err) { | ||
deferred.reject(err); | ||
} else { | ||
rules.splice(rules.indexOf(def.rule), 1); | ||
deferred.resolve(result); | ||
} | ||
}); | ||
if(rule2x.length) { | ||
media2x.rules = rule2x; | ||
rules.push(media2x); | ||
} | ||
fs.writeFileSync(options.dest, css.stringify(token)); | ||
callback(); | ||
return deferred.promise; | ||
}, function(err) { | ||
callback(err); | ||
console.error('Some error occur when read png:', err); | ||
}); | ||
///////////////////////// | ||
function formatPath(name) { | ||
return path.resolve(srcDir, name); | ||
}; | ||
function bgPosition(x, y) { | ||
return (x > 0 ? -x + 'px': 0) + ' ' + (y > 0 ? -y + 'px': 0); | ||
}; | ||
function bgUrl(target) { | ||
return 'url('+path.relative(srcDir, target).split(path.sep).join('/')+')'; | ||
}; | ||
function getBgDec(rule) { | ||
for(var dec, i = 0; i < rule.declarations.length; i++) { | ||
dec = rule.declarations[i]; | ||
if(dec.type == 'declaration' | ||
&& (dec.property == 'background' || dec.property=='background-image') | ||
&& BG_URL_REG.test(dec.value)) { | ||
return { | ||
url: RegExp.$1.replace(/['"]/g, ''), | ||
dec: dec | ||
}; | ||
} | ||
} | ||
}; | ||
}; | ||
module.exports.collectSpriteDef = collectSpriteDef; | ||
module.exports.sprite = sprite; | ||
module.exports.Algorithm = Algorithm; | ||
module.exports.PNG2X_REG = PNG2X_REG; | ||
module.exports.fixToEven = fixToEven; | ||
module.exports.readPngs = readPngs; | ||
module.exports.horizontalArrange = horizontalArrange; | ||
module.exports.verticalArrange = verticalArrange; | ||
module.exports.binaryArrange = binaryArrange; | ||
module.exports.create = create; |
{ | ||
"name": "easy-sprite", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "A tool that can create spirte image for css.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -1,5 +0,5 @@ | ||
#easy-sprite | ||
# easy-sprite | ||
easy-sprite是一个简洁的css雪碧图制作工具。它能根据指定规则,将小图片合并为雪碧图。easy-sprite采用css注释指定雪碧图合并规则,能够灵活的满足多种需求。支持自动生成低倍率图片,轻松搞定屏幕适配。 | ||
##安装 | ||
## 安装 | ||
``` | ||
@@ -9,3 +9,3 @@ npm install ease-sprite --save | ||
##使用 | ||
## 使用 | ||
``` | ||
@@ -15,9 +15,11 @@ var sprite = require('ease-sprite'); | ||
``` | ||
##配置说明 | ||
+ **src:**需要处理的css文件。 | ||
+ **dest:**css输出文件名。 | ||
+ **spriteDir:**雪碧图输出目录。 | ||
+ **margin:**雪碧图排列的最小间距离。 | ||
## 配置说明 | ||
+ **src:**需要处理的css文件。 | ||
+ **dest:**css输出文件名。 | ||
+ **spriteDir:**雪碧图输出目录。 | ||
+ **margin:**雪碧图排列的最小间距离,默认10px。 | ||
+ **compress:**输出css文件是否需要压缩,默认false。 | ||
+ **sourcemap:**是否需要输出sourcemap,默认false。 | ||
##雪碧图声明 | ||
## 雪碧图声明 | ||
雪碧图的声明采用css注释。格式为:/\* @sprite:[雪碧图源文件(夹)]=>雪碧图输出文件?selector=背景图调用选择器&alg=图片排列算法 \*/。 | ||
@@ -27,3 +29,3 @@ | ||
##声明示例 | ||
## 声明示例 | ||
@@ -39,3 +41,3 @@ ``` | ||
``` | ||
sprite处理后的结果为: | ||
sprite 处理后的结果为: | ||
@@ -52,6 +54,6 @@ ``` | ||
##关于二倍图 | ||
## 关于二倍图 | ||
为了适配高分屏,通常需要做两套大小的图片。有了sprite工具,则只需要提供一套大图,sprite工具会压缩出一套小图,并通过media query来决定实际用什么图。支持二倍图非常简单,只需将雪碧图输出文件的命名定为@2x.png、-2x.png或_2x.png,工具在识别出这类雪碧图输出后就会自动生成对应的一倍图和media queery等。 | ||
##License | ||
## License | ||
MIT:[http://vinnyguitar.mit-license.org]() |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
15234765
180
54
3
579
1