Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial import

  • Loading branch information...
commit 3c51eecaf878a198258a03a93ad7664fa7088f1c 0 parents
andris9 authored
16 LICENSE
@@ -0,0 +1,16 @@
+Copyright (c) 2011 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
60 README.md
@@ -0,0 +1,60 @@
+Stylus-Sprite
+=============
+
+**Stylus-Sprite** is an extension for [Stylus](https://github.com/LearnBoost/stylus) which makes sprite images from Stylus tags.
+Actually it takes a image file (currently only PNG's are supported), places it to a sprite image and replaces the original
+pointer in the CSS file with position coordinates according to the sprite image.
+
+Installation
+------------
+
+ npm install stylus-sprite
+
+Usage
+-----
+
+Consider the following Stylus CSS
+
+ .block-elm
+ background: url(sprite.png) no-repeat sprite("star.png");
+ width: 25px;
+ height: 25px;
+
+After running Stylus-Sprite the resulting CSS would be something like
+
+ .block_elm{
+ background: url(sprite.png) no-repeat -25px -78px;
+ width: 25px;
+ height: 25px;
+ }
+
+And the image *sprite.png* would have *star.png* placed on position 25x78 px.
+
+See test folder for complete example.
+
+JavaScript API
+--------------
+
+Creating the sprite consists of two phases - preparation and rendering.
+
+ var stylus = require("stylus"),
+ StylusSprite = require("stylus-sprite")
+ sprite = new StylusSprite({output_file:"sprite.png"});
+
+ var css = "body.....";
+
+ stylus(css).
+ set('filename', 'test.css').
+ define('sprite', function(filename, option_val){
+ // preparation phase
+ return sprite.spritefunc(filename, option_val);
+ }).
+ render(function(err, css){
+ if (err) throw err;
+
+ // rendering phase
+ sprite.build(css, function(err, css){
+ if (err) throw err;
+ console.log(css);
+ });
+ });
30 package.json
@@ -0,0 +1,30 @@
+{
+ "name": "stylus-sprite",
+ "description": "Generate sprite images with Stylus",
+ "version": "0.1.0",
+ "author" : "Andris Reinman",
+ "maintainers":[
+ {
+ "name":"andris",
+ "email":"andris@node.ee"
+ }
+ ],
+ "homepage": "http://github.com/andris9/stylus-sprite",
+ "repository" : {
+ "type" : "git",
+ "url" : "http://github.com/andris9/stylus-sprite.git"
+ },
+ "main" : "./stylus-sprite",
+ "licenses" : [
+ {
+ "type": "MIT",
+ "url": "http://github.com/andris9/stylus-sprite/blob/master/LICENSE"
+ }
+ ],
+ "dependencies": {
+ "stylus":"*",
+ "node-gd":"*"
+ },
+ "engine": [ "node >=0.3.0" ],
+ "keywords": ["CSS", "sprite", "sprites", "images"]
+}
448 stylus-sprite.js
@@ -0,0 +1,448 @@
+var stylus=require("stylus"),
+ gdlib = require("node-gd"),
+ pathlib = require('path');
+
+module.exports = Sprite;
+
+/**
+ * Sprite
+ *
+ * Updates Stylus CSS and generates a sprite image
+ *
+ * -Sprite-------------------------------
+ * | |
+ * | X--Block------------------------- |
+ * | | | |
+ * | | -Img---------- -Img---------- | |
+ * | | | -Imgfile-- | | -Imgfile-- | | |
+ * | | | | | | | | | | | |
+ * | | | ---------- | | ---------- | | |
+ * | | -------------- -------------- | |
+ * | --------------------------------- |
+ * --------------------------------------
+ *
+ * "X" marks the position for CSS in pixel values
+ *
+ **/
+function Sprite(options){
+
+ options = options || {};
+
+ this.images = [];
+ this.processedImages = [];
+ this._img_id = 0;
+ this.canvasWidth = 0;
+ this.canvasHeight = 0;
+ this.padding = 10;
+
+ this.image_root = options.image_root || "";
+ this.output_file = options.output_file || "sprite.png";
+}
+
+
+/**
+ * Sprite#keys -> Object
+ *
+ * Valid key names and values
+ **/
+Sprite.prototype.keys = {
+ "width":{
+ type:"number"
+ },
+ "height":{
+ type:"number"
+ },
+ "align":{
+ type:"predefined",
+ values: ["block","left","center","right"]
+ },
+ "valign":{
+ type:"predefined",
+ values: ["block","bottom","middle","top"]
+ },
+ "resize":{
+ type: "boolean"
+ },
+ "repeat":{
+ type:"predefined",
+ values: ["no","x","y"]
+ },
+ "limit-repeat-x":{
+ type:"number"
+ },
+ "limit-repeat-y":{
+ type:"number"
+ }
+};
+
+
+/**
+ * Sprite#defaults -> Object
+ *
+ * Default values to be used with image blocks
+ **/
+Sprite.prototype.defaults = {
+ width: 0,
+ height: 0,
+ align: "block",
+ valign: "block",
+ resize: false,
+ repeat: "no",
+ "limit-repeat-y": 300,
+ "limit-repeat-x": 0
+};
+
+
+/**
+ * Sprite#getDefaults() -> Object
+ *
+ * Generates a copy of Sprite#defaults
+ **/
+Sprite.prototype.getDefaults = function(){
+ var defaults = {},
+ keys = Object.keys(this.defaults);
+ for(var i = 0, len = keys.length; i < len; i++){
+ defaults[keys[i]] = this.defaults[keys[i]];
+ }
+ return defaults;
+};
+
+/**
+ * Sprite#validate(key, value) -> String|Number
+ * - key (String): key name
+ * - value (String): value for the key
+ *
+ * Checks if the key is allowed and that the value is formatted accordingly.
+ * Returns processed value (eg. string "245" converted to number 245 etc.)
+ **/
+Sprite.prototype.validate = function(key, value){
+
+ if(!this.keys[key]){
+ throw new Error("Invalid key '"+key+"'");
+ }
+
+ switch(this.keys[key].type){
+ case "number":
+ value = Number(value);
+ if(isNaN(value)){
+ throw new Error("Invalid number value '"+key+"' for "+key);
+ }
+ break;
+ case "predefined":
+ if(this.keys[key].values.indexOf(value)<0){
+ throw new Error("Unknown value '"+value+"' for "+key+", allowed: "+this.keys[key].values);
+ }
+ break;
+ case "boolean":
+ value = value=="false" || value=="0" || !value?false:true;
+ break;
+ }
+ return value;
+};
+
+/**
+ * Sprite#spritefunc(filename, option_val) -> String
+ * - filename (Object): filename for the image
+ * - options (Object): options for the image
+ *
+ * This function is run by Stylus. option_val is parsed and a options object
+ * is generated from it.
+ *
+ * key1: value1; key2: value2; ...
+ *
+ * When encountering unknown key or the value is not suitable, an error is thrown.
+ * Sprite image positions are replaced with placeholders in the form of
+ *
+ * SPRITE_PLACEHOLDER(IMG_ID)
+ *
+ * When the actual sprite is generated then these placeholders will be replaced
+ * with actual positions of the image in the sprite file
+ **/
+
+Sprite.prototype.spritefunc = function(filename, options){
+
+ // setup default values
+ var imgdata = this.getDefaults();
+ imgdata.filename = filename.val;
+
+ // parse option string, split parts by ";"
+ (options && options.val || "").split(";").forEach((function(opts){
+
+ // split on ":", find key and value
+ var parts = opts.split(":"),
+ key = parts[0] && parts.shift().trim().toLowerCase(),
+ value = parts.length && parts.join(":").trim();
+
+ // skip empty parts
+ if(!key)return;
+
+ // validate key and its value
+ imgdata[key] = this.validate(key, value);
+
+ }).bind(this));
+
+ // generate a "hash" for indexing the value by joining sorted keypairs
+ // -> a_key=value;b_key=value;c_key=value
+ var keys = Object.keys(imgdata), hash = [];
+ keys.sort();
+ for(var i=0; i<keys.length; i++){
+ hash.push(keys[i]+"="+imgdata[keys[i]]);
+ }
+ imgdata.hash = hash.join(";");
+
+ // check if the imgdata object is already processed (check if hash exists)
+ if(!this.images.filter(function(elm){
+ if(elm.hash == imgdata.hash){
+ imgdata = elm; // use cached value
+ return true;
+ }
+ return false;
+ }).length){
+ // not yet, generate ID value
+ this._img_id++;
+ imgdata._img_id = this._img_id;
+ this.images.push(imgdata);
+ imgdata.lineno = filename.lineno;
+ }
+ return "SPRITE_PLACEHOLDER("+imgdata._img_id+")";
+}
+
+
+/**
+ * Sprite#build(css, callback, err) -> undefined
+ * - css (String): Styuls generated CSS text
+ * - callback (Function): callback to be run after images are processed
+ * - err (Object): error object
+ *
+ * Queue manager for image processing. If there's any images left to process,
+ * run the processor with callback set as self. If all images are processed,
+ * run the sprite generator
+ **/
+Sprite.prototype.build = function(css, callback, err){
+
+ if(err){
+ callback(err);
+ }
+
+ if(this.images.length){
+ // process all images, one by one
+ this.processImage(this.images.shift(), this.build.bind(this, css, callback));
+ }else{
+ // if there's no images left, generate sprite
+ this.makeMap(css, callback);
+ }
+}
+
+
+/**
+ * Sprite#processImage(imgdata, callback) -> undefined
+ * - imgdata (Object): hash containing metadata for the image
+ * - callback (Function): callback function to be run
+ *
+ * Processes individual image file, calculates width and height for the
+ * resulting image block etc. This is asynchronous function - when it finishes
+ * then it runs the callback function which in turn might run this function again
+ * but for another image
+ **/
+Sprite.prototype.processImage = function(imgdata, callback){
+ console.log("processing "+imgdata.filename +" ("+imgdata._img_id+")...");
+
+ // Open Image
+ gdlib.openPng(pathlib.join(this.image_root, imgdata.filename), (function(err, img, path){
+
+ if(err){
+ if(err.message){
+ err.message+="; CSS line nr #"+imgdata.lineno;
+ }
+ return callback(err);
+ }
+
+ // Find actual width of the image
+ imgdata.imageWidth = img.width;
+ if(!imgdata.width){
+ imgdata.width = img.width;
+ }
+
+ // Find actual height of the image
+ imgdata.imageHeight = img.height;
+ if(!imgdata.height){
+ imgdata.height = img.height;
+ }
+
+ // Calculate block size for the image
+ // Use pixel values, or 100% for X axis where needed (repeat:x)
+ imgdata.blockWidth = (imgdata.repeat=="x" && imgdata['limit-repeat-x'] || "100%") || (imgdata.align!="block" && "100%") || imgdata.width;
+ imgdata.blockHeight = (imgdata.repeat=="y" && imgdata['limit-repeat-y']) || imgdata.height;
+
+ // Block height can't be lower than image height
+ if(imgdata.blockHeight<imgdata.height){
+ imgdata.blockHeight=imgdata.height;
+ }
+
+ // Calculate canvas width
+ if(typeof imgdata.blockWidth=="number" && imgdata.blockWidth>this.canvasWidth){
+ this.canvasWidth = imgdata.blockWidth;
+ }
+ // Images with 100% block width need canvas width to be at least their image width
+ if(imgdata.width>this.canvasWidth){
+ this.canvasWidth = imgdata.width;
+ }
+
+ // Calculate maximum canvas height
+ this.canvasHeight += imgdata.blockHeight+this.padding;
+
+ // Keep the image object for later use
+ imgdata.image = img;
+
+ // Move image data from this.images -> this.processedImages
+ this.processedImages.push(imgdata);
+
+ // return
+ process.nextTick(callback);
+ }).bind(this));
+
+}
+
+/**
+ * Sprite#makeMap(css, callback) -> undefined
+ * - css (String): Styulus generated CSS
+ * - callback (Function): callback to be run when the image is completed
+ *
+ *
+ **/
+Sprite.prototype.makeMap = function(css, callback){
+
+ var currentImageData,
+ blockImage,
+ spriteImage = gdlib.create(this.canvasWidth, this.canvasHeight),
+
+ posX, posY,
+ curX=0, curY=0,
+ startX=0, startY=0,
+ remainder;
+
+ for(var i=0, len = this.processedImages.length; i<len; i++){
+
+ // create image element in correct dimensions
+ currentImageData = this.processedImages[i];
+ if(currentImageData.blockWidth=="100%"){
+ currentImageData.blockWidth = this.canvasWidth;
+ }
+
+ posX = 0;
+ posY = 0;
+
+ if(currentImageData.width>currentImageData.imageWidth){
+ posX = Math.round(currentImageData.width/2-currentImageData.imageWidth/2);
+ }
+
+ // Vertical align for positioning image in image element
+ if(currentImageData.height>currentImageData.imageHeight){
+ switch(currentImageData.valign){
+ case "top":
+ posY = 0;
+ break;
+ case "bottom":
+ posY = currentImageData.height-currentImageData.imageHeight;
+ break;
+ case "middle":
+ default:
+ posY = Math.round(currentImageData.height/2 - currentImageData.imageHeight/2);
+ }
+ }
+
+ // Generate block from image element
+ blockImage = gdlib.create(currentImageData.width, currentImageData.height);
+ if(currentImageData.resize){
+ // resize image to dimensions
+ currentImageData.image.copyResampled(blockImage,
+ 0, // dstX
+ 0, // dstY
+ 0, // srcX
+ 0, // srcY
+ currentImageData.width, // dstWidth
+ currentImageData.height, // dstHeight
+ currentImageData.imageWidth, // srcWidth
+ currentImageData.imageHeight // srcHeight
+ );
+ }else{
+ // copy and place in actual dimension (if fits)
+ currentImageData.image.copyResampled(blockImage,
+ posX, // dstX
+ posY, // dstY
+ 0, // srcX
+ 0, // srcY
+ currentImageData.imageWidth, // dstWidth
+ currentImageData.imageHeight, // dstHeight
+ currentImageData.imageWidth, // srcWidth
+ currentImageData.imageHeight // srcHeight
+ );
+ }
+
+ // Horizontal align for positioning image element in block
+ switch(currentImageData.align){
+ case "center":
+ curX = Math.round(this.canvasWidth/2 - currentImageData.width/2);
+ break;
+ case "right":
+ curX = this.canvasWidth - currentImageData.width;
+ break;
+ case "left":
+ default:
+ curX = 0;
+ }
+
+ startX = curX;
+ startY = curY;
+
+ // REPEAT:NO
+ // copy block to sprite (position curX,curY)
+ if(currentImageData.repeat=="no"){
+ blockImage.copyResampled(spriteImage, curX, curY, 0, 0, currentImageData.width, currentImageData.height, currentImageData.width, currentImageData.height);
+ curY += currentImageData.height + this.padding;
+ }
+
+ // REPEAT:X
+ // copy and replicate block to sprite horizontally
+ if(currentImageData.repeat=="x"){
+ curX = 0;
+ startX = 0;
+ while(curX<currentImageData.blockWidth){
+ remainder = curX + currentImageData.width<currentImageData.blockWidth?currentImageData.width:currentImageData.blockWidth-curX;
+ blockImage.copyResampled(spriteImage, curX, curY, 0, 0, remainder, currentImageData.height, remainder, currentImageData.height);
+ curX += currentImageData.width;
+ }
+ curY += currentImageData.height + this.padding;
+ }
+
+ // REPEAT:Y
+ // copy and replicate block to sprite horizontally
+ if(currentImageData.repeat=="y"){
+ while(curY < startY + currentImageData.blockHeight){
+ remainder = curY + currentImageData.height<startY + currentImageData.blockHeight?currentImageData.height:startY + currentImageData.blockHeight-curY;
+ blockImage.copyResampled(spriteImage, curX, curY, 0, 0, currentImageData.width, remainder, currentImageData.width, remainder);
+ curY += remainder;
+ }
+ curY += this.padding;
+ }
+
+ // Replace placeholders from CSS with real positions
+ var re = new RegExp("SPRITE_PLACEHOLDER\\("+currentImageData._img_id+"\\)","g"),
+ cssPlacementX = currentImageData.align=="right"?"100%":"-"+startX+"px",
+ cssPlacementY = "-"+startY+"px";
+
+
+ css = css.replace(re, cssPlacementX+" "+cssPlacementY);
+
+ }
+
+ // Save to file
+ spriteImage.savePng(this.output_file, 0, function(){
+ console.log("CSS processed")
+ callback(null, css);
+ });
+
+}
+
+
+
BIN  test/images/star.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 test/out/sprite.css
@@ -0,0 +1,72 @@
+.normal_block {
+ background-position: -0px -0px;
+ background-image: url("sprite.png");
+ background-repeat: no-repeat;
+ width: 46px;
+ height: 49px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+.resized_block {
+ background-position: -0px -59px;
+ background-image: url("sprite.png");
+ background-repeat: no-repeat;
+ width: 300px;
+ height: 30px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+.repeat-x_block {
+ background-position: -0px -99px;
+ background-image: url("sprite.png");
+ background-repeat: repeat-x;
+ height: 49px;
+ margin: 10px;
+ border-top: 1px solid #333;
+ border-bottom: 1px solid #333;
+}
+.repeat-y_block {
+ background-position: -0px -158px;
+ background-image: url("sprite.png");
+ background-repeat: repeat-y;
+ width: 46px;
+ height: 300px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+.aligned_block {
+ background-position: 100% -468px;
+ background-image: url("sprite.png");
+ background-repeat: no-repeat;
+ width: 300px;
+ height: 300px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+.leftalign_block {
+ background-position: -0px -778px;
+ background-image: url("sprite.png");
+ background-repeat: no-repeat;
+ width: 300px;
+ height: 49px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+.centeralign_block {
+ background-position: -0px -837px;
+ background-image: url("sprite.png");
+ background-repeat: no-repeat;
+ width: 300px;
+ height: 49px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+.middlealign_block {
+ background-position: -0px -896px;
+ background-image: url("sprite.png");
+ background-repeat: no-repeat;
+ height: 300px;
+ width: 46px;
+ margin: 10px;
+ border: 1px solid #333;
+}
20 test/out/sprite.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Sprite test</title>
+ <meta charset="utf-8"/>
+ <link href="sprite.css" type="text/css" rel="stylesheet"/>
+ </head>
+ <body>
+
+ <div class="normal_block"></div>
+ <div class="resized_block"></div>
+ <div class="repeat-x_block"></div>
+ <div class="repeat-y_block"></div>
+ <div class="aligned_block"></div>
+ <div class="leftalign_block"></div>
+ <div class="centeralign_block"></div>
+ <div class="middlealign_block"></div>
+
+ </body>
+</html>
BIN  test/out/sprite.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 test/test.css
@@ -0,0 +1,89 @@
+
+
+.normal_block{
+ background-position: sprite("star.png"); /* no params*/
+ background-image: url(sprite.png);
+ background-repeat: no-repeat;
+
+ width: 46px;
+ height: 49px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+
+.resized_block{
+ background-position: sprite("star.png","width:300; height:30; resize: true");
+ background-image: url(sprite.png);
+ background-repeat: no-repeat;
+
+ width: 300px;
+ height: 30px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+
+.repeat-x_block{
+ background-position: sprite("star.png","repeat:x");
+ background-image: url(sprite.png);
+ background-repeat: repeat-x;
+
+ height: 49px;
+ margin: 10px;
+ border-top: 1px solid #333;
+ border-bottom: 1px solid #333;
+}
+
+.repeat-y_block{
+ background-position: sprite("star.png","repeat:y; limit-repeat-y: 300");
+ background-image: url(sprite.png);
+ background-repeat: repeat-y;
+
+ width: 46px;
+ height: 300px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+
+.aligned_block{
+ background-position: sprite("star.png","height:300; valign:bottom; align:right");
+ background-image: url(sprite.png);
+ background-repeat: no-repeat;
+
+ width: 300px;
+ height: 300px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+
+.leftalign_block{
+ background-position: sprite("star.png","align:left");
+ background-image: url(sprite.png);
+ background-repeat: no-repeat;
+
+ width: 300px;
+ height: 49px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+
+.centeralign_block{
+ background-position: sprite("star.png","align:center; width: 300");
+ background-image: url(sprite.png);
+ background-repeat: no-repeat;
+
+ width: 300px;
+ height: 49px;
+ margin: 10px;
+ border: 1px solid #333;
+}
+
+.middlealign_block{
+ background-position: sprite("star.png","valign:middle;height:300");
+ background-image: url(sprite.png);
+ background-repeat: no-repeat;
+
+ height: 300px;
+ width: 46px;
+ margin: 10px;
+ border: 1px solid #333;
+}
24 test/test.js
@@ -0,0 +1,24 @@
+var stylus = require("stylus"),
+ StylusSprite = require("../stylus-sprite"),
+ fs = require("fs");
+
+var sprite = new StylusSprite({
+ image_root:"./images", // will be appended to the image paths from css
+ output_file:"out/sprite.png" // output image
+});
+
+
+stylus(fs.readFileSync("test.css").toString("utf-8")).
+ set('filename', 'test.css').
+ define('sprite', function(filename, option_val){
+ return sprite.spritefunc(filename, option_val);
+ }).
+ render(function(err, css){
+ if (err) throw err;
+ sprite.build(css, function(err, css){
+ if (err) throw err;
+
+ fs.writeFileSync("out/sprite.css", css);
+ console.log("CSS written to sprite.css");
+ });
+ });
Please sign in to comment.
Something went wrong with that request. Please try again.