├── .gitignore ├── .travis.yml ├── README.md ├── example ├── images │ ├── global-45c81.png │ ├── global.json │ ├── global │ │ ├── .DS_Store │ │ ├── 100x100.png │ │ ├── 50 100.png │ │ └── 50x50.png │ ├── others-b775d.png │ ├── others.json │ └── others │ │ ├── 100x100.png │ │ ├── 100x50.png │ │ └── 50x50.png ├── simple.coffee ├── sprite.styl └── stylus.coffee ├── index.coffee ├── index.js ├── lib ├── checksum.coffee ├── image.coffee ├── mapper.coffee └── sprite.coffee ├── package.json └── test ├── image.coffee ├── images ├── global-0f2b5.png ├── global.json └── global │ ├── 100x300.png │ └── 200x200.png ├── mapper.coffee └── sprite.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | example/.cache/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: [0.8, 0.9] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/naltatis/node-sprite.png)](http://travis-ci.org/naltatis/node-sprite) 2 | 3 | # A node.js Sprite Library with Stylus and Retina Support 4 | 5 | ## Requirements 6 | `node-sprite` uses **ImageMagick** for its graphics operations. So make sure you have the `convert` and `identify` command available in your envirnoment. 7 | 8 | ## Usage 9 | 10 | There are three exported functions: `sprite`, `sprites` and `stylus`. The following examples show how to use them. 11 | 12 | ### Example Directory Stucture 13 | 14 | ``` 15 | - app.js 16 | - images/ 17 | - global/ 18 | - bar.jpg // 200x100px image 19 | - foo.png // 10x50px image 20 | - animals/ 21 | - cat.gif // 64x64px image 22 | - duck.png // 64x64px image 23 | - mouse.gif // 64x64px image 24 | ``` 25 | 26 | ## Single Sprite 27 | 28 | ```javascript 29 | var sprite = require('node-sprite'); 30 | 31 | sprite.sprite('global', {path: './images'}, function(err, globalSprite) { 32 | console.log(globalSprite.filename()) 33 | console.log('foo', globalSprite.image('foo')); 34 | console.log('bar', globalSprite.image('bar')); 35 | }); 36 | ``` 37 | 38 | This code will generate a sprite image named `./images/global-[checksum].png` and output the following: 39 | 40 | global-45c81.png 41 | foo, {width: 200, height: 100, positionX: 0, positionY: 52} 42 | bar, {width: 64, height: 64, positionX: 0, positionY: 0} 43 | 44 | ## Multiple Sprites 45 | 46 | ```javascript 47 | var sprite = require('node-sprite'); 48 | 49 | sprite.sprites({path: './images'}, function(err, result) { 50 | var globalSprite = result['global']; 51 | var animalsSprite = result['animals']; 52 | console.log(globalSprite.filename()); 53 | console.log(animalsSprite.filename()); 54 | console.log('animals/duck', animalsSprite.image('duck')); 55 | }); 56 | ``` 57 | 58 | This code will generate a sprite image for every subfolder of `./images`. The images are named `./images/[folder]-[checksum].png`. 59 | 60 | global-45c81.png 61 | animals-b775d.png 62 | animals/duck, {width: 10, height: 50, positionX: 0, positionY: 66} 63 | 64 | ## Stylus Integration 65 | 66 | ``` 67 | // screen.styl 68 | #duck 69 | sprite animal duck 70 | #mouse 71 | sprite global mouse false 72 | ``` 73 | 74 | The `sprite` function generates the correct `background` image and position for the specified image. By default it also adds `width` and `height` properties. You can prevent this behaviour by setting the third optional parameter to `false`. 75 | 76 | ```css 77 | /* screen.css */ 78 | #duck { 79 | background: url('./images/animals-b775d.png') 0px -66px; 80 | width: 64px; 81 | height: 64px; 82 | } 83 | #mouse { 84 | background: url('./images/animals-b775d.png') 0px -132px; 85 | } 86 | ``` 87 | 88 | The `sprite.stylus` function behaves similar to `sprite.sprites`, but it returns a helper object, with provides a stylus helper function `helper.fn`. 89 | 90 | ```javascript 91 | var sprite = require('node-sprite'); 92 | var stylus = require('stylus'); 93 | 94 | var str = require("fs").readFileSync("screen.styl") 95 | 96 | sprite.stylus({path: './images'}, function (err, helper) { 97 | stylus(str) 98 | .set('filename', 'screen.styl') 99 | .define('sprite', helper.fn) 100 | .render(function (err, css) { 101 | console.log(css); 102 | }); 103 | }); 104 | ``` 105 | 106 | ## Retina / High Resolution Sprite Support 107 | 108 | node-sprite has a special mode for high resolution sprites. When your sprite folder ends with `@2x` it will be treated differently. 109 | 110 | ### Basic Example 111 | 112 | animals@2x/ 113 | - cat.gif // 128x128px image 114 | - duck.png // 128x128px image 115 | 116 | Although we have 128x128px images. The elements should only have the size of 64x64px and the background has to be scaled down. 117 | 118 | ``` 119 | // screen.styl 120 | #duck 121 | sprite(animal@2x, duck) 122 | background-size sprite-dimensions(animal@2x, name) 123 | ``` 124 | 125 | will be transformed to 126 | 127 | ```css 128 | /* screen.css */ 129 | #duck { 130 | background: url('./images/animals@2x-c575d.png') 0px -66px; 131 | width: 64px; 132 | height: 64px; 133 | background-size: 64px 194px; 134 | } 135 | ``` 136 | 137 | For this to work you have to add the `sprite-dimensions` helper in you stylus configuration: 138 | 139 | `.define('sprite-dimensions', helper.dimensionsFn)` 140 | 141 | ### Retina Mixin 142 | 143 | If you want to have a retina and a non-retina sprite it makes sense to create a mixin like this one: 144 | 145 | ``` 146 | // screen.styl 147 | retina-sprite(folder, name) 148 | sprite(folder, name) 149 | hidpi = s("(min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx)"); 150 | @media hidpi 151 | sprite(folder+"@2x", name, false) 152 | background-size sprite-dimensions(folder+"@2x", name) 153 | 154 | #duck 155 | retina-sprite animals duck 156 | ``` 157 | 158 | This will generate the following css code: 159 | 160 | ```css 161 | #duck { 162 | background: url('./images/animals-b775d.png') 0px -66px; 163 | width: 64px; 164 | height: 64px; 165 | } 166 | @media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) { 167 | #duck { 168 | background: url('./images/animals@2x-c575d.png') 0px -66px; 169 | background-size: 64px 194px; 170 | } 171 | } 172 | ``` 173 | 174 | *Note: All images in the retina folder should have even height and width pixels.* 175 | 176 | ## Options 177 | 178 | All three functions accept an optional options parameter. 179 | 180 | ```javascript 181 | { 182 | path: './images', // sprite directory 183 | padding: 2, // pixels between images 184 | httpPath: './images', // used be the stylus helper 185 | watch: false, // auto update sprite in background 186 | retina: '@2x' // postfix for retina sprite folders 187 | } 188 | ``` 189 | 190 | ## Auto Update on Image Change 191 | 192 | If you pass `watch: true` as an option node-sprite will watch the sprite folders and regenerate the sprite when something changes. 193 | 194 | You can subscribe to the `update` event of the `sprite` or `helper` object to get notified. 195 | 196 | ```javascript 197 | var generateCss = function () {...}; 198 | 199 | sprite.stylus({watch: true}, function (err, helper) { 200 | generateCss(); 201 | helper.on("update", generateCss); 202 | }); 203 | ``` 204 | 205 | ## Structural Sprite Information / JSON 206 | 207 | node-sprite will put a `./images/[folder].json` next to every generated sprite image. This file contains structural information of the generated sprite. This files can be used by other modules or applications. 208 | 209 | They are also usefull if you running your application on a production machine without ImageMagick. In this case node-sprite will fallback to this data. 210 | 211 | ```javascript 212 | { 213 | "name": "animals", 214 | "checksum": "b775d6fa89ad809d7700c32b491c50f0", 215 | "shortsum": "b775d", 216 | "images": [ 217 | { 218 | "name": "cat", 219 | "filename": "cat.gif", 220 | "checksum": "25ce6895f8ed03aa127123430997bbdf", 221 | "width": 64, 222 | "height": 64, 223 | "positionX": 0, 224 | "positionY": 0 225 | }, 226 | ... 227 | ] 228 | } 229 | ``` 230 | 231 | ## Contribute 232 | 233 | Feel free to post issues or pull request. 234 | 235 | You can run the projects tests with the `npm test` command. 236 | 237 | ## License 238 | The MIT License 239 | -------------------------------------------------------------------------------- /example/images/global-45c81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/global-45c81.png -------------------------------------------------------------------------------- /example/images/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "global", 3 | "checksum": "45c816fa89ad809d7700c32b491c50f0", 4 | "shortsum": "45c81", 5 | "images": [ 6 | { 7 | "name": "100x100", 8 | "filename": "100x100.png", 9 | "checksum": "25ce6895f8ed03aa127123430997bbdf", 10 | "width": 100, 11 | "height": 100, 12 | "positionX": 0, 13 | "positionY": 0 14 | }, 15 | { 16 | "name": "50 100", 17 | "filename": "50 100.png", 18 | "checksum": "e5f0b0f339c67bfefce15a1d246d1348", 19 | "width": 50, 20 | "height": 100, 21 | "positionX": 0, 22 | "positionY": 102 23 | }, 24 | { 25 | "name": "50x50", 26 | "filename": "50x50.png", 27 | "checksum": "3626eba4d972899ebdef6094ee256403", 28 | "width": 50, 29 | "height": 50, 30 | "positionX": 0, 31 | "positionY": 204 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /example/images/global/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/global/.DS_Store -------------------------------------------------------------------------------- /example/images/global/100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/global/100x100.png -------------------------------------------------------------------------------- /example/images/global/50 100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/global/50 100.png -------------------------------------------------------------------------------- /example/images/global/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/global/50x50.png -------------------------------------------------------------------------------- /example/images/others-b775d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/others-b775d.png -------------------------------------------------------------------------------- /example/images/others.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "others", 3 | "checksum": "b775d6e3a8405e8ed36c041e1f3725f7", 4 | "shortsum": "b775d", 5 | "images": [ 6 | { 7 | "name": "100x100", 8 | "filename": "100x100.png", 9 | "checksum": "25ce6895f8ed03aa127123430997bbdf", 10 | "width": 100, 11 | "height": 100, 12 | "positionX": 0, 13 | "positionY": 0 14 | }, 15 | { 16 | "name": "100x50", 17 | "filename": "100x50.png", 18 | "checksum": "7b48cd42f3df509bf3e7e6eb0fd16a01", 19 | "width": 100, 20 | "height": 50, 21 | "positionX": 0, 22 | "positionY": 102 23 | }, 24 | { 25 | "name": "50x50", 26 | "filename": "50x50.png", 27 | "checksum": "3626eba4d972899ebdef6094ee256403", 28 | "width": 50, 29 | "height": 50, 30 | "positionX": 0, 31 | "positionY": 154 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /example/images/others/100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/others/100x100.png -------------------------------------------------------------------------------- /example/images/others/100x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/others/100x50.png -------------------------------------------------------------------------------- /example/images/others/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/example/images/others/50x50.png -------------------------------------------------------------------------------- /example/simple.coffee: -------------------------------------------------------------------------------- 1 | sprite = require '../' 2 | 3 | sprite.sprite 'global', {path: './images', padding: 2}, (err, s) -> 4 | console.log "created sprite from #{s.images.length} images" 5 | i = s.image '50 100' 6 | console.log "image '50 100' with dimensions #{i.width}x#{i.height} is at position #{i.positionX},#{i.positionY}" -------------------------------------------------------------------------------- /example/sprite.styl: -------------------------------------------------------------------------------- 1 | #smallSquare 2 | sprite global '50x50' false 3 | sprite global '50 100' -------------------------------------------------------------------------------- /example/stylus.coffee: -------------------------------------------------------------------------------- 1 | stylus = require 'stylus' 2 | node = stylus.nodes 3 | sprite = require('../') 4 | str = require('fs').readFileSync(__dirname + '/sprite.styl', 'utf8') 5 | 6 | ### 7 | stylus(str) 8 | .set('filename', __dirname + '/sprite.styl') 9 | .define('sprite', sprite.stylus()) 10 | .render (err, css) -> 11 | console.log err, css 12 | 13 | ### 14 | 15 | sprite.stylus {path: "./images", watch: true }, (err, helper) -> 16 | output = -> 17 | stylus(str) 18 | .set('filename', __dirname + '/sprite.styl') 19 | .define('sprite', helper.fn) 20 | .render (err, css) -> 21 | console.log err if err 22 | console.log css 23 | output() 24 | helper.on "update", output 25 | console.log "watching for file changes in './images' ..." -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | Sprite = require './lib/sprite' 2 | mapper = require './lib/mapper' 3 | fs = require 'fs' 4 | Seq = require "seq" 5 | { EventEmitter } = require "events" 6 | 7 | createSprite = (name, options = {}, cb = ->) -> 8 | options or= {} 9 | 10 | if typeof options is 'function' 11 | cb = options 12 | options = {} 13 | 14 | padding = options.padding || 2 15 | path = options.path || './images' 16 | 17 | map = new mapper.VerticalMapper padding 18 | sprite = new Sprite name, path, map, options.watch 19 | sprite.load (err) -> 20 | sprite.write (err) -> 21 | cb err, sprite 22 | sprite 23 | 24 | createSprites = (options = {}, cb = ->) -> 25 | if typeof options is 'function' 26 | cb = options 27 | options = {} 28 | 29 | path = options.path || './images' 30 | 31 | Seq() 32 | .seq -> 33 | fs.readdir path, @ 34 | .flatten() 35 | .parFilter (dir) -> 36 | fs.stat "#{path}/#{dir}", (err, stat) => 37 | @ err, stat.isDirectory() 38 | .parMap (dir) -> 39 | createSprite dir, options, @ 40 | .unflatten() 41 | .seq (sprites) -> 42 | result = {} 43 | result[s.name] = s for s in sprites 44 | cb null, result 45 | 46 | stylus = (options = {}, cb = ->) -> 47 | stylus = require 'stylus' 48 | nodes = stylus.nodes 49 | retinaMatcher = new RegExp((options.retina || "2x") + "$") 50 | result = {} 51 | helper = new EventEmitter() 52 | helper.fn = (name, image, dimensions) -> 53 | name = name.string 54 | image = image.string 55 | dimensions = if dimensions then dimensions.val else true 56 | sprite = result[name] 57 | throw new Error("missing sprite #{name}") if not sprite? 58 | item = sprite.image image 59 | throw new Error("missing image #{image} in sprite #{name}") if not item? 60 | 61 | width = item.width 62 | height = item.height 63 | positionX = item.positionX * -1 64 | positionY = item.positionY * -1 65 | 66 | if name.match(retinaMatcher) 67 | width = width / 2 68 | height = height / 2 69 | positionX = positionX / 2 70 | positionY = positionY / 2 71 | 72 | if dimensions 73 | width = new nodes.Property ["width"], "#{width}px" 74 | height = new nodes.Property ["height"], "#{height}px" 75 | @closestBlock.nodes.splice @closestBlock.index+1, 0, width, height 76 | 77 | httpUrl = (options.httpPath || options.path) + "/" + sprite.filename() 78 | 79 | new nodes.Property ["background"], "url('#{httpUrl}') #{positionX}px #{positionY}px" 80 | 81 | helper.dimensionsFn = (name, image) -> 82 | name = name.string 83 | image = image.string 84 | sprite = result[name] 85 | throw new Error("missing sprite #{name}") if not sprite? 86 | item = sprite.image image 87 | throw new Error("missing image #{image} in sprite #{name}") if not item? 88 | 89 | width = sprite._width() 90 | height = sprite._height() 91 | 92 | if name.match(retinaMatcher) 93 | width = width / 2 94 | height = height / 2 95 | 96 | return new nodes.Unit "#{width}px #{height}px" 97 | 98 | createSprites options, (err, sprites) -> 99 | for name, s of sprites 100 | s.on "update", -> 101 | helper.emit "update", name 102 | result = sprites 103 | cb err, helper 104 | helper 105 | 106 | 107 | module.exports = 108 | sprite: createSprite, 109 | sprites: createSprites 110 | stylus: stylus 111 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("coffee-script"); 2 | module.exports = require("./index.coffee"); -------------------------------------------------------------------------------- /lib/checksum.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | fs = require 'fs' 3 | 4 | module.exports = 5 | file: (filename, cb) -> 6 | md5sum = crypto.createHash 'md5' 7 | 8 | s = fs.ReadStream filename 9 | s.on 'data', (d) -> 10 | md5sum.update d 11 | 12 | s.on 'end', -> 13 | d = md5sum.digest 'hex' 14 | cb(null, d) 15 | 16 | array: (array) -> 17 | md5sum = crypto.createHash 'md5' 18 | md5sum.update(entry, 'utf8') for entry in array 19 | md5sum.digest 'hex' 20 | -------------------------------------------------------------------------------- /lib/image.coffee: -------------------------------------------------------------------------------- 1 | im = require "imagemagick" 2 | checksum = require "./checksum" 3 | 4 | class Image 5 | positionX: null 6 | positionY: null 7 | width: null 8 | height: null 9 | checksum: null 10 | constructor: (@filename, @path) -> 11 | @name = @filename.replace /\.(png|gif|jpg|jpeg)$/, '' 12 | readDimensions: (cb) -> 13 | checksum.file @file(), (err, sum) => 14 | im.identify @file(), (err, img) => 15 | unless err 16 | @width = img.width 17 | @height = img.height 18 | @checksum = sum 19 | cb(err) 20 | file: -> 21 | "#{@path}/#{@filename}" 22 | 23 | module.exports = Image -------------------------------------------------------------------------------- /lib/mapper.coffee: -------------------------------------------------------------------------------- 1 | class Mapper 2 | height: 0 3 | width: 0 4 | constructor: (@padding) -> 5 | area: -> @width * @height 6 | 7 | class VerticalMapper extends Mapper 8 | map: (images) -> 9 | @width = @height = 0 10 | 11 | for image in images 12 | image.positionX = 0 13 | image.positionY = @height 14 | @height += image.height + @padding 15 | @width = image.width if image.width > @width 16 | 17 | @height -= @padding 18 | 19 | class HorizontalMapper extends Mapper 20 | map: (images) -> 21 | @width = @height = 0 22 | 23 | for image in images 24 | image.positionX = @width 25 | image.positionY = 0 26 | @width += image.width + @padding 27 | @height = image.height if image.height > @height 28 | 29 | @width -= @padding 30 | 31 | 32 | module.exports.VerticalMapper = VerticalMapper 33 | module.exports.HorizontalMapper = HorizontalMapper -------------------------------------------------------------------------------- /lib/sprite.coffee: -------------------------------------------------------------------------------- 1 | fs = require "fs" 2 | im = require "imagemagick" 3 | watch = require 'watch' 4 | Seq = require "seq" 5 | checksum = require './checksum' 6 | { EventEmitter } = require "events" 7 | 8 | Image = require './image' 9 | 10 | class Sprite extends EventEmitter 11 | images = [] 12 | 13 | constructor: (@name, @path, @mapper, @watch = false) -> 14 | 15 | reload: -> 16 | @_readImages (err, images) => 17 | unless err 18 | @images = images 19 | @mapper.map @images 20 | @write => 21 | @emit "update" 22 | 23 | load: (cb = ->) -> 24 | @_fromJson() 25 | @_readImages (err, images) => 26 | unless err 27 | @images = images 28 | @mapper.map @images 29 | @_watch() 30 | cb err 31 | 32 | url: -> 33 | "#{@path}/#{@filename()}" 34 | 35 | jsonUrl: -> 36 | "#{@path}/#{@name}.json" 37 | 38 | filename: -> 39 | "#{@name}-#{@shortsum()}.png" 40 | 41 | write: (cb) -> 42 | fs.exists @url(), (exists) => 43 | if exists 44 | cb() 45 | else 46 | @_write cb 47 | 48 | _write: (cb) -> 49 | commands = @_emptySprite() 50 | @_addImageData(commands, image) for image in @images 51 | commands.push @url() 52 | im.convert commands, (err) => 53 | @_toJson() 54 | @_cleanup() 55 | cb err 56 | 57 | image: (name) -> 58 | result = @images.filter (i) -> i.name == name 59 | result[0] 60 | 61 | checksum: -> 62 | sums = (img.checksum for img in @images) 63 | checksum.array sums 64 | 65 | shortsum: -> 66 | @checksum()[0...5] 67 | 68 | _watch: -> 69 | return unless @watch 70 | watch.createMonitor "#{@path}/#{@name}/", interval: 500, (m) => 71 | m.on "created", => @reload() 72 | m.on "changed", => @reload() 73 | m.on "removed", => @reload() 74 | 75 | _width: -> 76 | @mapper.width 77 | 78 | _height: -> 79 | @mapper.height 80 | 81 | _emptySprite: -> 82 | ["-size", "#{@_width()}x#{@_height()}", "xc:none"] 83 | 84 | _addImageData: (commands, image) -> 85 | commands.push image.file(), "-geometry", "+#{image.positionX}+#{image.positionY}", "-composite" 86 | 87 | _readImages: (cb) -> 88 | self = @ 89 | limit = 10 90 | Seq() 91 | .seq_ -> 92 | self._getFiles @ 93 | .flatten() 94 | .parMap limit, (filename) -> 95 | self._getImage filename, @ 96 | .unflatten() 97 | .seq (images) => 98 | # workaround for https://github.com/substack/node-seq/issues/22 99 | error = null 100 | for image in images 101 | error = new Error("unable to read all images") unless image? 102 | cb error, images 103 | 104 | _getFiles: (cb) -> 105 | fs.readdir "#{@path}/#{@name}", (err, files) -> 106 | files = files.filter (file) -> file.match /\.(png|gif|jpg|jpeg)$/ 107 | cb err, files 108 | 109 | _cleanup: (cb = ->) -> 110 | self = @ 111 | fs.readdir "#{@path}", (err, files) -> 112 | for file in files 113 | if file.match("^#{self.name}-.*\.png$") and file isnt self.filename() 114 | fs.unlinkSync "#{self.path}/#{file}" 115 | cb() 116 | 117 | _getImage: (filename, cb) -> 118 | image = new Image(filename, "#{@path}/#{@name}") 119 | image.readDimensions (err) -> 120 | # workaround for https://github.com/substack/node-seq/issues/22 121 | if err 122 | cb() 123 | else 124 | cb null, image 125 | 126 | _toJson: -> 127 | info = 128 | name: @name 129 | checksum: @checksum() 130 | shortsum: @shortsum() 131 | images: [] 132 | 133 | for image in @images 134 | imageInfo = 135 | name: image.name 136 | filename: image.filename 137 | checksum: image.checksum 138 | width: image.width 139 | height: image.height 140 | positionX: image.positionX 141 | positionY: image.positionY 142 | info.images.push imageInfo 143 | 144 | info = JSON.stringify(info, null, ' ') 145 | fs.writeFileSync @jsonUrl(), info 146 | 147 | _fromJson: -> 148 | @images = [] 149 | 150 | try 151 | info = fs.readFileSync @jsonUrl(), "UTF-8" 152 | catch error 153 | console.log @jsonUrl() + " not found" 154 | return # no json file 155 | 156 | info = JSON.parse info 157 | for img in info.images 158 | image = new Image(img.filename, "#{@path}/#{@name}") 159 | image.width = img.width 160 | image.height = img.height 161 | image.checksum = img.checksum 162 | image.positionX = img.positionX 163 | image.positionY = img.positionY 164 | @images.push image 165 | 166 | @mapper.map @images 167 | 168 | module.exports = Sprite 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sprite", 3 | "version": "0.1.3", 4 | "author": "naltatis", 5 | "keywords": [ 6 | "css", 7 | "sprite", 8 | "sprites", 9 | "stylus", 10 | "retina", 11 | "images" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/naltatis/node-sprite.git" 16 | }, 17 | "dependencies": { 18 | "coffee-script": "1.3.3", 19 | "underscore": "1.3.1", 20 | "imagemagick": "http://github.com/naltatis/node-imagemagick/tarball/master", 21 | "seq": "0.3.5", 22 | "watch": "0.5.1" 23 | }, 24 | "devDependencies": { 25 | "expresso": "0.9.2", 26 | "stylus": "0.29.0" 27 | }, 28 | "scripts": { 29 | "test": "expresso test/*" 30 | }, 31 | "main": "./index.js", 32 | "engines": { 33 | "node": "> 0.8.x" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/image.coffee: -------------------------------------------------------------------------------- 1 | Image = require '../lib/image' 2 | 3 | path = './test/images/global' 4 | 5 | module.exports = 6 | testImageDimensions: (beforeExit, assert) -> 7 | image = new Image '100x300.png', path 8 | image.readDimensions -> 9 | assert.equal 100, image.width 10 | assert.equal 300, image.height 11 | assert.equal "4a930265ea1cab7bc075cece7aa24e27", image.checksum -------------------------------------------------------------------------------- /test/images/global-0f2b5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/test/images/global-0f2b5.png -------------------------------------------------------------------------------- /test/images/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "global", 3 | "checksum": "0f2b5fd4a7ed57ec667b2350d2dfc831", 4 | "shortsum": "0f2b5", 5 | "images": [ 6 | { 7 | "name": "100x300", 8 | "filename": "100x300.png", 9 | "checksum": "4a930265ea1cab7bc075cece7aa24e27", 10 | "width": 100, 11 | "height": 300, 12 | "positionX": 0, 13 | "positionY": 0 14 | }, 15 | { 16 | "name": "200x200", 17 | "filename": "200x200.png", 18 | "checksum": "dde51aea929d0b9abe9709669ddc1d8d", 19 | "width": 200, 20 | "height": 200, 21 | "positionX": 0, 22 | "positionY": 310 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /test/images/global/100x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/test/images/global/100x300.png -------------------------------------------------------------------------------- /test/images/global/200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naltatis/node-sprite/c09a5026375e1e804c4be44a260fe888586d8d68/test/images/global/200x200.png -------------------------------------------------------------------------------- /test/mapper.coffee: -------------------------------------------------------------------------------- 1 | Sprite = require '../lib/sprite' 2 | VerticalMapper = require('../lib/mapper').VerticalMapper 3 | HorizontalMapper = require('../lib/mapper').HorizontalMapper 4 | 5 | path = './test/images' 6 | 7 | module.exports = 8 | testVerticalMapper: (beforeExit, assert) -> 9 | mapper = new VerticalMapper(10) 10 | sprite = new Sprite 'global', path, mapper 11 | sprite.load -> 12 | images = sprite.images 13 | mapper.map images 14 | 15 | # checking y positions 16 | assert.equal 0, images[0].positionY 17 | assert.equal 310, images[1].positionY 18 | 19 | # checking x positions 20 | assert.equal 0, image.positionX for image in images 21 | 22 | # checking sprite dimensions 23 | assert.equal 200, mapper.width 24 | assert.equal 510, mapper.height 25 | 26 | # checking area 27 | assert.equal 200*510, mapper.area() 28 | 29 | testHorizontalMapper: (beforeExit, assert) -> 30 | mapper = new HorizontalMapper(10) 31 | sprite = new Sprite 'global', path, mapper 32 | sprite.load -> 33 | images = sprite.images 34 | mapper.map images 35 | 36 | # checking y positions 37 | assert.equal 0, images[0].positionX 38 | assert.equal 110, images[1].positionX 39 | 40 | # checking x positions 41 | assert.equal 0, image.positionY for image in images 42 | 43 | # checking sprite dimensions 44 | assert.equal 310, mapper.width 45 | assert.equal 300, mapper.height 46 | 47 | # checking area 48 | assert.equal 310*300, mapper.area() -------------------------------------------------------------------------------- /test/sprite.coffee: -------------------------------------------------------------------------------- 1 | Sprite = require '../lib/sprite' 2 | Image = require '../lib/image' 3 | VerticalMapper = require('../lib/mapper').VerticalMapper 4 | HorizontalMapper = require('../lib/mapper').HorizontalMapper 5 | 6 | path = './test/images' 7 | 8 | mapper = new VerticalMapper(10) 9 | 10 | module.exports = 11 | testSpriteLoading: (beforeExit, assert) -> 12 | sprite = new Sprite 'global', path, mapper 13 | sprite.load -> 14 | assert.equal 2, sprite.images.length 15 | testWritingOutput: (beforeExit, assert) -> 16 | sprite = new Sprite 'global', path, mapper 17 | sprite.load -> 18 | sprite.write -> 19 | assert.ok true 20 | testImageInfo: (beforeExit, assert) -> 21 | sprite = new Sprite 'global', path, mapper 22 | sprite.load -> 23 | assert.equal 100, sprite.image('100x300').width 24 | assert.equal 300, sprite.image('100x300').height 25 | assert.equal 0, sprite.image('200x200').positionX 26 | assert.equal 310, sprite.image('200x200').positionY 27 | assert.isUndefined sprite.image('350x151') 28 | testImageChecksums: (beforeExit, assert) -> 29 | sprite = new Sprite 'global', path, mapper 30 | sprite.load -> 31 | assert.equal 32, sprite.checksum().length --------------------------------------------------------------------------------