├── .gitignore ├── README.md ├── demos ├── demo.css ├── demo.js ├── dev-tiles-sprite.png └── index.html ├── dist ├── tiles.js └── tiles.min.js ├── gruntfile.js ├── package.json └── src ├── Grid.js ├── Template.js ├── Tile.js └── UniformTemplates.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore node modules 2 | node_modules 3 | 4 | # Webstorm 5 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tiles.js 2 | ===== 3 | 4 | ###Goal### 5 | Make it easy to create grid based layouts which can adapt to various screen sizes and changing content. 6 | 7 | ###How### 8 | The Tiles.js library provides a grid control and a simple template language for defining layouts. It uses jQuery to animate tiles when either the content or resolution changes. 9 | 10 | ###Demo### 11 | See the tiles in action on [Pulse for the Web](http://www.pulse.me/)! 12 | 13 | Once you sign-in to Pulse, check out the [Behind the Scenes](http://www.pulse.me/app/dev) page which includes more info about the tiles and a live editor to experiment with templates. 14 | 15 | ###Install### 16 | 17 | npm install tilesjs 18 | 19 | 20 | Or you can download binaries from the dist folder 21 | 22 | Compiled size: 6 KB (just over 2 KB gzipped). 23 | 24 | ###Sample Code### 25 | There are 2 samples in the demos directory. A proper site with documentation and additional samples is coming soon... 26 | 27 | ###Tile### 28 | A tile is a rectangular element that covers one or more cells in a grid. Each tile has a unique identifier and maintains its current position in the grid (top, left, width, height). 29 | 30 | The tile handles several events during its lifecycle: 31 | 32 | * appendTo: tile should be appended to the parent grid element. 33 | * remove: tile should be removed from the parent grid 34 | * resize: tile is resized (or moved) within the parent grid 35 | 36 | ###Template### 37 | A template specifies the layout of variably sized tiles in a grid. We provide a simple JSON based template language for defining templates. A single cell tile should use the period character. Larger tiles may be created using any character that is unused by a adjacent tile. Whitespace is ignored when parsing the rows. 38 | 39 | Examples: 40 | 41 | var simpleTemplate = [ 42 | ' A A . B ', 43 | ' A A . B ', 44 | ' . C C . ', 45 | ]; 46 | 47 | var complexTemplate = [ 48 | ' J J . . E E ', 49 | ' . A A . E E ', 50 | ' B A A F F . ', 51 | ' B . D D . H ', 52 | ' C C D D G H ', 53 | ' C C . . G . ', 54 | ]; 55 | 56 | In addition to creating templates using JSON, you can also programmatically build templates. The library includes a simple UniformTemplate factory which creates 1x1 templates for a given number of columns and tiles. Custom template factories can be created to generate content aware layouts. 57 | 58 | ###Grid### 59 | The grid control renders a set of tiles into a template. The grid was designed to fill available screen area by either scaling the size of a cell or by requesting a new template with a different number of columns. 60 | 61 | It was also designed for a changing set of content. When tiles or template change, the grid will instruct the tiles to either fade in, animate, or fade out to their new location. 62 | 63 | Updates and Redraw are separate processes, so a series of updates may be made to the content followed by a single redraw to trigger the animation when appropriate. During the redraw phase, the grid has a prioritization extensibility point. Custom grid controls can be created to order the content prior to assigning each tile a spot in the grid. 64 | 65 | 66 | ## The MIT License ## 67 | 68 | Copyright (c) 2012 Pixel Lab 69 | 70 | Permission is hereby granted, free of charge, to any person obtaining a copy 71 | of this software and associated documentation files (the "Software"), to deal 72 | in the Software without restriction, including without limitation the rights 73 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 74 | copies of the Software, and to permit persons to whom the Software is 75 | furnished to do so, subject to the following conditions: 76 | 77 | The above copyright notice and this permission notice shall be included in 78 | all copies or substantial portions of the Software. 79 | 80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 81 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 82 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 83 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 84 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 85 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 86 | THE SOFTWARE. -------------------------------------------------------------------------------- /demos/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px; 3 | font-family: Arial; 4 | } 5 | 6 | .grid { 7 | width: 95%; 8 | height: 600px; 9 | position: relative; 10 | overflow-x: hidden; 11 | overflow-y: scroll; 12 | background-color: #ddd; 13 | margin: 10px 0px; 14 | border: 10px solid #ddd; 15 | clear: both; 16 | } 17 | 18 | .grid > div { 19 | background-color: #fff; 20 | position: absolute; 21 | } 22 | 23 | /* slider for sample 1 */ 24 | 25 | .slider { 26 | width: 200px; 27 | display: inline-block; 28 | margin: 0px 10px; 29 | } 30 | 31 | .sliderLabel { 32 | font-size: 18px; 33 | font-weight: bold; 34 | } 35 | 36 | /* template selections for sample #2 */ 37 | 38 | .dev-tile-number, .dev-tile-size { 39 | font-size: 36px; 40 | padding: 10px; 41 | } 42 | 43 | .dev-tiles-templates ul { 44 | margin-bottom: 10px; 45 | } 46 | 47 | .dev-template { 48 | margin-right: 20px; 49 | height: 35px; 50 | display: inline-block; 51 | background: url(dev-tiles-sprite.png) no-repeat; 52 | cursor: pointer; 53 | } 54 | 55 | .dev-template.selected { 56 | background-position-y: -200px; 57 | } 58 | 59 | .dev-l1 { 60 | width: 47px; 61 | } 62 | 63 | .dev-l2 { 64 | width: 47px; 65 | background-position-x: -68px; 66 | } 67 | 68 | .dev-l3 { 69 | width: 39px; 70 | background-position-x: -135px; 71 | } 72 | 73 | .dev-l4 { 74 | width: 39px; 75 | background-position-x: -194px; 76 | } 77 | 78 | .dev-l5 { 79 | width: 47px; 80 | background-position-x: -245px; 81 | margin-right: 0px; 82 | } -------------------------------------------------------------------------------- /demos/demo.js: -------------------------------------------------------------------------------- 1 | // The grid manages tiles using ids, which you can define. For our 2 | // examples we'll just use the tile number as the unique id. 3 | var TILE_IDS = [ 4 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 5 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 6 | ]; 7 | 8 | // SAMPLE #1 9 | $(function() { 10 | 11 | // create the grid and an event which will update the grid 12 | // when either tile count or window size changes 13 | var el = document.getElementById('sample1-grid'), 14 | grid = new Tiles.Grid(el), 15 | updateGrid = function(event, ui) { 16 | 17 | // update the set of tiles and redraw the grid 18 | grid.updateTiles(TILE_IDS.slice(0, ui.value)); 19 | grid.redraw(true /* animate tile movements */); 20 | 21 | // update the tile count label 22 | $('#tileCount').text('(' + ui.value + ')'); 23 | }; 24 | 25 | // use a jQuery slider to update the number of tiles 26 | $('#sample1-tiles') 27 | .slider({ 28 | min: 1, 29 | max: 25, 30 | step: 1, 31 | create: updateGrid, 32 | slide: updateGrid, 33 | change: updateGrid 34 | }) 35 | .slider('value', 8); 36 | 37 | // wait until user finishes resizing the browser 38 | var debouncedResize = debounce(function() { 39 | grid.resize(); 40 | grid.redraw(true); 41 | }, 200); 42 | 43 | // when the window resizes, redraw the grid 44 | $(window).resize(debouncedResize); 45 | }); 46 | 47 | // templates in JSON matching the predefined selections you can 48 | // choose on the demo page 49 | var DemoTemplateRows = [ 50 | [ 51 | " A A B B C C ", 52 | " A A B B C C ", 53 | " . . . . . . ", 54 | " D D E E F F " 55 | ], [ 56 | " A A A A A A ", 57 | " B B C C D D ", 58 | " B B C C D D ", 59 | " B B C C D D " 60 | ], [ 61 | " A A B B . ", 62 | " A A B B . ", 63 | " A A C C . ", 64 | " . . . . ." 65 | ], [ 66 | " A A . . ", 67 | " A A . . ", 68 | " B B . . ", 69 | " C C . ." 70 | ], [ 71 | " A A A B B B ", 72 | " A A A B B B ", 73 | " A A A C C . ", 74 | " . . . . . ." 75 | ] 76 | ]; 77 | 78 | // SAMPLE #2 79 | $(function() { 80 | 81 | var el = document.getElementById('sample2-grid'), 82 | grid = new Tiles.Grid(el); 83 | 84 | // template is selected by user, not generated so just 85 | // return the number of columns in the current template 86 | grid.resizeColumns = function() { 87 | return this.template.numCols; 88 | }; 89 | 90 | // by default, each tile is an empty div, we'll override creation 91 | // to add a tile number to each div 92 | grid.createTile = function(tileId) { 93 | var tile = new Tiles.Tile(tileId); 94 | tile.$el.append('
' + tileId + '
'); 95 | return tile; 96 | }; 97 | 98 | // update the template selection 99 | var $templateButtons = $('#sample2-templates .dev-template').on('click', function(e) { 100 | 101 | // unselect all templates 102 | $templateButtons.removeClass("selected"); 103 | 104 | // select the template we clicked on 105 | $(e.target).addClass("selected"); 106 | 107 | // get the JSON rows for the selection 108 | var index = $(e.target).index(), 109 | rows = DemoTemplateRows[index]; 110 | 111 | // set the new template and resize the grid 112 | grid.template = Tiles.Template.fromJSON(rows); 113 | grid.isDirty = true; 114 | grid.resize(); 115 | 116 | // adjust number of tiles to match selected template 117 | var ids = TILE_IDS.slice(0, grid.template.rects.length); 118 | grid.updateTiles(ids); 119 | grid.redraw(true); 120 | }); 121 | 122 | // make the initial selection 123 | $('#sample2-t1').trigger('click'); 124 | 125 | // wait until users finishes resizing the browser 126 | var debouncedResize = debounce(function() { 127 | grid.resize(); 128 | grid.redraw(true); 129 | }, 200); 130 | 131 | // when the window resizes, redraw the grid 132 | $(window).resize(debouncedResize); 133 | }); 134 | 135 | // SAMPLE #3 136 | $(function() { 137 | 138 | // create a custom Tile which customizes the resize behavior 139 | function CustomTile(tileId, element) { 140 | // initialize base 141 | Tiles.Tile.call(this, tileId, element); 142 | } 143 | 144 | CustomTile.prototype = new Tiles.Tile(); 145 | 146 | CustomTile.prototype.resize = function(cellRect, pixelRect, animate, duration, onComplete) { 147 | 148 | // set the text inside the tile to the dimensions 149 | var cellDimensions = cellRect.width + ' x ' + cellRect.height; 150 | this.$el.find('.dev-tile-size').text(cellDimensions); 151 | 152 | // call the base to perform the resize 153 | Tiles.Tile.prototype.resize.call( 154 | this, cellRect, pixelRect, animate, duration, onComplete); 155 | }; 156 | 157 | 158 | var el = document.getElementById('sample3-grid'), 159 | grid = new Tiles.Grid(el); 160 | 161 | // template is selected by user, not generated so just 162 | // return the number of columns in the current template 163 | grid.resizeColumns = function() { 164 | return this.template.numCols; 165 | }; 166 | 167 | // we'll override creation to use our custom tile 168 | grid.createTile = function(tileId) { 169 | var tile = new CustomTile(tileId); 170 | tile.$el.append('
'); 171 | return tile; 172 | }; 173 | 174 | // update the template selection 175 | var $templateButtons = $('#sample3-templates .dev-template').on('click', function(e) { 176 | 177 | // unselect all templates 178 | $templateButtons.removeClass("selected"); 179 | 180 | // select the template we clicked on 181 | $(e.target).addClass("selected"); 182 | 183 | // get the JSON rows for the selection 184 | var index = $(e.target).index(), 185 | rows = DemoTemplateRows[index]; 186 | 187 | // set the new template and resize the grid 188 | grid.template = Tiles.Template.fromJSON(rows); 189 | grid.isDirty = true; 190 | grid.resize(); 191 | 192 | // adjust number of tiles to match selected template 193 | var ids = TILE_IDS.slice(0, grid.template.rects.length); 194 | grid.updateTiles(ids); 195 | grid.redraw(true); 196 | }); 197 | 198 | // make the initial selection 199 | $('#sample3-t1').trigger('click'); 200 | 201 | // wait until users finishes resizing the browser 202 | var debouncedResize = debounce(function() { 203 | grid.resize(); 204 | grid.redraw(true); 205 | }, 200); 206 | 207 | // when the window resizes, redraw the grid 208 | $(window).resize(debouncedResize); 209 | }); 210 | -------------------------------------------------------------------------------- /demos/dev-tiles-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinkpixellab/tilesjs/5fb74fd35c9aba9ce952d839b7ba3e8cb88779d6/demos/dev-tiles-sprite.png -------------------------------------------------------------------------------- /demos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tiles.js Basic Demos 6 | 7 | 8 | 9 | 10 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 42 |

Sample 1: Resizable grid using 1x1 tiles

43 |

44 | Let's start with a a simple resizable grid of 1x1 tiles. We'll create a Tiles.js grid and 45 | provide a parent div which will hold our tiles. The parent div should use relative positioning 46 | and tile divs should be absolutely positioned. 47 | 48 | When the grid is drawn, the 49 | library looks at the width of the element and determines the number of cell columns 50 | that will fit given a minimum cell width. Once we have a grid of cells, the next 51 | step is to determine how to lay out the tiles on the grid. 52 |

53 |

54 | The simple, default template factory is named UniformTemplates. All of the tiles in the templates 55 | produced by this factory are the same size: 1x1s which each cover a single cell in the grid. 56 |

57 |

58 | Adjust the slider below to change the number of tiles displayed by the grid. Try resizing the 59 | browser window and watch the grid adjust the size and layout of the tiles. View the page source 60 | to see the code. 61 |

62 | 63 | Number of Tiles 64 |
65 | 66 |
67 | 68 |

Sample 2: Variable sized tiles using fixed templates

69 |

70 | The grid uses a pluggable template factory, which makes it possible to generate 71 | templates at run time. The grid will provide the factory with the number of columns 72 | and the target number of tiles. The template factory is expected to return a rectangular 73 | template defined by the number of columns, rows, and the set of rectangles specifying 74 | where each tile goes. 75 |

76 |

77 | The tile count is only a target because it may not be possible for 78 | some template factories to create a rectangular layout for a particular column + tile combination. 79 | If that happens, the factory should add extra tiles so that the template remains rectangular. 80 |

81 |

82 | For this example, we'll bypass the factory by setting the template directly. We've predefined 5 templates using JSON. Select any of the templates below to update the layout of the grid. 83 |

84 | 85 |
86 | 93 |
94 |
95 | 96 |

Sample 3: Updating Tiles During Resize

97 |

98 | In the previous sample we customized our grid by overriding tile creation and adding the tile number to the div. In this example, we'll create a custom Tile that can update content when the tile size changes. 99 |

100 |

101 | The size of a tile may change for several reasons: 102 |

108 |

109 |

110 | The grid determines the location and size of each tile based on the template and current 111 | set of tiles. However, it delegates the element changes by calling Tile.resize(). There are 5 parameters: 112 |

119 |

120 |

121 | In this example, we'll override the resize method and render the cell size for each tile. Try switching between the following fixed templates and observe that the dimensions are updated when tiles are resized. 122 |

123 | 124 |
125 | 132 |
133 |
134 | 135 | 136 | -------------------------------------------------------------------------------- /dist/tiles.js: -------------------------------------------------------------------------------- 1 | /*! Tiles.js v1.0.0 | http://thinkpixellab.com/tilesjs */ 2 | 3 | // single namespace export 4 | var Tiles = {}; 5 | 6 | (function($) { 7 | 8 | var Tile = Tiles.Tile = function(tileId, element) { 9 | 10 | this.id = tileId; 11 | 12 | // position and dimensions of tile inside the parent panel 13 | this.top = 0; 14 | this.left = 0; 15 | this.width = 0; 16 | this.height = 0; 17 | 18 | // cache the tile container element 19 | this.$el = $(element || document.createElement('div')); 20 | }; 21 | 22 | Tile.prototype.appendTo = function($parent, fadeIn, delay, duration) { 23 | this.$el 24 | .hide() 25 | .appendTo($parent); 26 | 27 | if (fadeIn) { 28 | this.$el.delay(delay).fadeIn(duration); 29 | } 30 | else { 31 | this.$el.show(); 32 | } 33 | }; 34 | 35 | Tile.prototype.remove = function(animate, duration) { 36 | if (animate) { 37 | this.$el.fadeOut({ 38 | complete: function() { 39 | $(this).remove(); 40 | } 41 | }); 42 | } 43 | else { 44 | this.$el.remove(); 45 | } 46 | }; 47 | 48 | // updates the tile layout with optional animation 49 | Tile.prototype.resize = function(cellRect, pixelRect, animate, duration, onComplete) { 50 | 51 | // store the list of needed changes 52 | var cssChanges = {}, 53 | changed = false; 54 | 55 | // update position and dimensions 56 | if (this.left !== pixelRect.x) { 57 | cssChanges.left = pixelRect.x; 58 | this.left = pixelRect.x; 59 | changed = true; 60 | } 61 | if (this.top !== pixelRect.y) { 62 | cssChanges.top = pixelRect.y; 63 | this.top = pixelRect.y; 64 | changed = true; 65 | } 66 | if (this.width !== pixelRect.width) { 67 | cssChanges.width = pixelRect.width; 68 | this.width = pixelRect.width; 69 | changed = true; 70 | } 71 | if (this.height !== pixelRect.height) { 72 | cssChanges.height = pixelRect.height; 73 | this.height = pixelRect.height; 74 | changed = true; 75 | } 76 | 77 | // Sometimes animation fails to set the css top and left correctly 78 | // in webkit. We'll validate upon completion of the animation and 79 | // set the properties again if they don't match the expected values. 80 | var tile = this, 81 | validateChangesAndComplete = function() { 82 | var el = tile.$el[0]; 83 | if (tile.left !== el.offsetLeft) { 84 | //console.log ('mismatch left:' + tile.left + ' actual:' + el.offsetLeft + ' id:' + tile.id); 85 | tile.$el.css('left', tile.left); 86 | } 87 | if (tile.top !== el.offsetTop) { 88 | //console.log ('mismatch top:' + tile.top + ' actual:' + el.offsetTop + ' id:' + tile.id); 89 | tile.$el.css('top', tile.top); 90 | } 91 | 92 | if (onComplete) { 93 | onComplete(); 94 | } 95 | }; 96 | 97 | 98 | // make css changes with animation when requested 99 | if (animate && changed) { 100 | 101 | this.$el.animate(cssChanges, { 102 | duration: duration, 103 | easing: 'swing', 104 | complete: validateChangesAndComplete 105 | }); 106 | } 107 | else { 108 | 109 | if (changed) { 110 | this.$el.css(cssChanges); 111 | } 112 | 113 | setTimeout(validateChangesAndComplete, duration); 114 | } 115 | }; 116 | 117 | })(jQuery); 118 | 119 | 120 | /* 121 | A grid template specifies the layout of variably sized tiles. A single 122 | cell tile should use the period character. Larger tiles may be created 123 | using any character that is unused by a adjacent tile. Whitespace is 124 | ignored when parsing the rows. 125 | 126 | Examples: 127 | 128 | var simpleTemplate = [ 129 | ' A A . B ', 130 | ' A A . B ', 131 | ' . C C . ', 132 | ] 133 | 134 | var complexTemplate = [ 135 | ' J J . . E E ', 136 | ' . A A . E E ', 137 | ' B A A F F . ', 138 | ' B . D D . H ', 139 | ' C C D D G H ', 140 | ' C C . . G . ', 141 | ]; 142 | */ 143 | 144 | (function($) { 145 | 146 | // remove whitespace and create 2d array 147 | var parseCells = function(rows) { 148 | var cells = [], 149 | numRows = rows.length, 150 | x, y, row, rowLength, cell; 151 | 152 | // parse each row 153 | for(y = 0; y < numRows; y++) { 154 | 155 | row = rows[y]; 156 | cells[y] = []; 157 | 158 | // parse the cells in a single row 159 | for (x = 0, rowLength = row.length; x < rowLength; x++) { 160 | cell = row[x]; 161 | if (cell !== ' ') { 162 | cells[y].push(cell); 163 | } 164 | } 165 | } 166 | 167 | // TODO: check to make sure the array isn't jagged 168 | 169 | return cells; 170 | }; 171 | 172 | function Rectangle(x, y, width, height) { 173 | this.x = x; 174 | this.y = y; 175 | this.width = width; 176 | this.height = height; 177 | } 178 | 179 | Rectangle.prototype.copy = function() { 180 | return new Rectangle(this.x, this.y, this.width, this.height); 181 | }; 182 | 183 | Tiles.Rectangle = Rectangle; 184 | 185 | // convert a 2d array of cell ids to a list of tile rects 186 | var parseRects = function(cells) { 187 | var rects = [], 188 | numRows = cells.length, 189 | numCols = numRows === 0 ? 0 : cells[0].length, 190 | cell, height, width, x, y, rectX, rectY; 191 | 192 | // make a copy of the cells that we can modify 193 | cells = cells.slice(); 194 | for (y = 0; y < numRows; y++) { 195 | cells[y] = cells[y].slice(); 196 | } 197 | 198 | // iterate through every cell and find rectangles 199 | for (y = 0; y < numRows; y++) { 200 | for(x = 0; x < numCols; x++) { 201 | cell = cells[y][x]; 202 | 203 | // skip cells that are null 204 | if (cell == null) { 205 | continue; 206 | } 207 | 208 | width = 1; 209 | height = 1; 210 | 211 | if (cell !== Tiles.Template.SINGLE_CELL) { 212 | 213 | // find the width by going right until cell id no longer matches 214 | while(width + x < numCols && 215 | cell === cells[y][x + width]) { 216 | width++; 217 | } 218 | 219 | // now find height by going down 220 | while (height + y < numRows && 221 | cell === cells[y + height][x]) { 222 | height++; 223 | } 224 | } 225 | 226 | // null out all cells for the rect 227 | for(rectY = 0; rectY < height; rectY++) { 228 | for(rectX = 0; rectX < width; rectX++) { 229 | cells[y + rectY][x + rectX] = null; 230 | } 231 | } 232 | 233 | // add the rect 234 | rects.push(new Rectangle(x, y, width, height)); 235 | } 236 | } 237 | 238 | return rects; 239 | }; 240 | 241 | Tiles.Template = function(rects, numCols, numRows) { 242 | this.rects = rects; 243 | this.numTiles = this.rects.length; 244 | this.numRows = numRows; 245 | this.numCols = numCols; 246 | }; 247 | 248 | Tiles.Template.prototype.copy = function() { 249 | 250 | var copyRects = [], 251 | len, i; 252 | for (i = 0, len = this.rects.length; i < len; i++) { 253 | copyRects.push(this.rects[i].copy()); 254 | } 255 | 256 | return new Tiles.Template(copyRects, this.numCols, this.numRows); 257 | }; 258 | 259 | // appends another template (assumes both are full rectangular grids) 260 | Tiles.Template.prototype.append = function(other) { 261 | 262 | if (this.numCols !== other.numCols) { 263 | throw 'Appended templates must have the same number of columns'; 264 | } 265 | 266 | // new rects begin after the last current row 267 | var startY = this.numRows, 268 | i, len, rect; 269 | 270 | // copy rects from the other template 271 | for (i = 0, len = other.rects.length; i < len; i++) { 272 | rect = other.rects[i]; 273 | this.rects.push( 274 | new Rectangle(rect.x, startY + rect.y, rect.width, rect.height)); 275 | } 276 | 277 | this.numRows += other.numRows; 278 | this.numTiles += other.numTiles; 279 | }; 280 | 281 | Tiles.Template.fromJSON = function(rows) { 282 | // convert rows to cells and then to rects 283 | var cells = parseCells(rows), 284 | rects = parseRects(cells); 285 | return new Tiles.Template( 286 | rects, 287 | cells.length > 0 ? cells[0].length : 0, 288 | cells.length); 289 | }; 290 | 291 | Tiles.Template.prototype.toJSON = function() { 292 | // for now we'll assume 26 chars is enough (we don't solve graph coloring) 293 | var LABELS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 294 | NUM_LABELS = LABELS.length, 295 | labelIndex = 0, 296 | rows = [], 297 | i, len, rect, x, y, label; 298 | 299 | // fill in single tiles for each cell 300 | for (y = 0; y < this.numRows; y++) { 301 | rows[y] = []; 302 | for (x = 0; x < this.numCols; x++) { 303 | rows[y][x] = Tiles.Template.SINGLE_CELL; 304 | } 305 | } 306 | 307 | // now fill in bigger tiles 308 | for (i = 0, len = this.rects.length; i < len; i++) { 309 | rect = this.rects[i]; 310 | if (rect.width > 1 || rect.height > 1) { 311 | 312 | // mark the tile position with a label 313 | label = LABELS[labelIndex]; 314 | for(y = 0; y < rect.height; y++) { 315 | for(x = 0; x < rect.width; x++) { 316 | rows[rect.y + y][rect.x + x] = label; 317 | } 318 | } 319 | 320 | // advance the label index 321 | labelIndex = (labelIndex + 1) % NUM_LABELS; 322 | } 323 | } 324 | 325 | // turn the rows into strings 326 | for (y = 0; y < this.numRows; y++) { 327 | rows[y] = rows[y].join(''); 328 | } 329 | 330 | return rows; 331 | }; 332 | 333 | // period used to designate a single 1x1 cell tile 334 | Tiles.Template.SINGLE_CELL = '.'; 335 | 336 | })(jQuery); 337 | 338 | 339 | // template provider which returns simple templates with 1x1 tiles 340 | Tiles.UniformTemplates = { 341 | get: function(numCols, targetTiles) { 342 | var numRows = Math.ceil(targetTiles / numCols), 343 | rects = [], 344 | x, y; 345 | 346 | // create the rects for 1x1 tiles 347 | for (y = 0; y < numRows; y++) { 348 | for (x = 0; x < numCols; x++) { 349 | rects.push(new Tiles.Rectangle(x, y, 1, 1)); 350 | } 351 | } 352 | 353 | return new Tiles.Template(rects, numCols, numRows); 354 | } 355 | }; 356 | (function($) { 357 | 358 | var Grid = Tiles.Grid = function(element) { 359 | 360 | this.$el = $(element); 361 | 362 | // animation lasts 500 ms by default 363 | this.animationDuration = 500; 364 | 365 | // the default set of factories used when creating templates 366 | this.templateFactory = Tiles.UniformTemplates; 367 | 368 | // defines the page size for prioritization of positions and tiles 369 | this.priorityPageSize = Number.MAX_VALUE; 370 | 371 | // spacing between tiles 372 | this.cellPadding = 10; 373 | 374 | // min width and height of a cell in the grid 375 | this.cellWidthMin = 150; 376 | this.cellHeightMin = 150; 377 | 378 | // actual width and height of a cell in the grid 379 | this.cellWidth = 0; 380 | this.cellHeight = 0; 381 | 382 | this.cellAspectRatio = 1; 383 | 384 | // number of tile cell columns 385 | this.numCols = 1; 386 | 387 | // cache the current template 388 | this.template = null; 389 | 390 | // flag that tracks whether a redraw is necessary 391 | this.isDirty = true; 392 | 393 | this.tiles = []; 394 | 395 | // keep track of added and removed tiles so we can update tiles 396 | // and the render the grid independently. 397 | this.tilesAdded = []; 398 | this.tilesRemoved = []; 399 | }; 400 | 401 | Grid.prototype.getContentWidth = function() { 402 | // by default, the entire container width is used when drawing tiles 403 | return this.$el.width(); 404 | }; 405 | 406 | // gets the number of columns during a resize 407 | Grid.prototype.resizeColumns = function() { 408 | var panelWidth = this.getContentWidth(); 409 | 410 | // ensure we have at least one column 411 | return Math.max(1, Math.floor((panelWidth + this.cellPadding) / 412 | (this.cellWidthMin + this.cellPadding))); 413 | }; 414 | 415 | // gets the cell size during a grid resize 416 | Grid.prototype.resizeCellWidth = function() { 417 | var panelWidth = this.getContentWidth(); 418 | return Math.ceil((panelWidth + this.cellPadding) / this.numCols) - 419 | this.cellPadding; 420 | }; 421 | 422 | Grid.prototype.resize = function() { 423 | 424 | var newCols = this.resizeColumns(); 425 | if (this.numCols !== newCols && newCols > 0) { 426 | this.numCols = newCols; 427 | this.isDirty = true; 428 | } 429 | 430 | var newCellWidth = this.resizeCellWidth(); 431 | if (this.cellWidth !== newCellWidth && newCellWidth > 0) { 432 | this.cellWidth = newCellWidth; 433 | this.cellHeight = this.cellWidth / this.cellAspectRatio; 434 | this.isDirty = true; 435 | } 436 | }; 437 | 438 | // refresh all tiles based on the current content 439 | Grid.prototype.updateTiles = function(newTileIds) { 440 | 441 | // ensure we dont have duplicate ids 442 | newTileIds = uniques(newTileIds); 443 | 444 | var numTiles = newTileIds.length, 445 | newTiles = [], 446 | i, tile, tileId, index; 447 | 448 | // retain existing tiles and queue remaining tiles for removal 449 | for (i = this.tiles.length - 1; i >= 0; i--) { 450 | tile = this.tiles[i]; 451 | index = $.inArray(tile.id, newTileIds); 452 | if (index < 0) { 453 | this.tilesRemoved.push(tile); 454 | //console.log('Removing tile: ' + tile.id) 455 | } 456 | else { 457 | newTiles[index] = tile; 458 | } 459 | } 460 | 461 | // clear existing tiles 462 | this.tiles = []; 463 | 464 | // make sure we have tiles for new additions 465 | for (i = 0; i < numTiles; i++) { 466 | 467 | tile = newTiles[i]; 468 | if (!tile) { 469 | 470 | tileId = newTileIds[i]; 471 | 472 | // see if grid has a custom tile factory 473 | if (this.createTile) { 474 | 475 | tile = this.createTile(tileId); 476 | 477 | // skip the tile if it couldn't be created 478 | if (!tile) { 479 | //console.log('Tile element could not be created, id: ' + tileId); 480 | continue; 481 | } 482 | 483 | } else { 484 | 485 | tile = new Tiles.Tile(tileId); 486 | } 487 | 488 | // add tiles to queue (will be appended to DOM during redraw) 489 | this.tilesAdded.push(tile); 490 | //console.log('Adding tile: ' + tile.id); 491 | } 492 | 493 | this.tiles.push(tile); 494 | } 495 | }; 496 | 497 | // helper to return unique items 498 | function uniques(items) { 499 | var results = [], 500 | numItems = items ? items.length : 0, 501 | i, item; 502 | 503 | for (i = 0; i < numItems; i++) { 504 | item = items[i]; 505 | if ($.inArray(item, results) === -1) { 506 | results.push(item); 507 | } 508 | } 509 | 510 | return results; 511 | } 512 | 513 | // prepend new tiles 514 | Grid.prototype.insertTiles = function(newTileIds) { 515 | this.addTiles(newTileIds, true); 516 | }; 517 | 518 | // append new tiles 519 | Grid.prototype.addTiles = function(newTileIds, prepend) { 520 | 521 | if (!newTileIds || newTileIds.length === 0) { 522 | return; 523 | } 524 | 525 | var prevTileIds = [], 526 | prevTileCount = this.tiles.length, 527 | i; 528 | 529 | // get the existing tile ids 530 | for (i = 0; i < prevTileCount; i++) { 531 | prevTileIds.push(this.tiles[i].id); 532 | } 533 | 534 | var tileIds = prepend ? newTileIds.concat(prevTileIds) 535 | : prevTileIds.concat(newTileIds); 536 | this.updateTiles(tileIds); 537 | }; 538 | 539 | Grid.prototype.removeTiles = function(removeTileIds) { 540 | 541 | if (!removeTileIds || removeTileIds.length === 0) { 542 | return; 543 | } 544 | 545 | var updateTileIds = [], 546 | i, len, id; 547 | 548 | // get the set of ids which have not been removed 549 | for (i = 0, len = this.tiles.length; i < len; i++) { 550 | id = this.tiles[i].id; 551 | if ($.inArray(id, removeTileIds) === -1) { 552 | updateTileIds.push(id); 553 | } 554 | } 555 | 556 | this.updateTiles(updateTileIds); 557 | }; 558 | 559 | Grid.prototype.createTemplate = function(numCols, targetTiles) { 560 | 561 | // ensure that we have at least one column 562 | numCols = Math.max(1, numCols); 563 | 564 | var template = this.templateFactory.get(numCols, targetTiles); 565 | if (!template) { 566 | 567 | // fallback in case the default factory can't generate a good template 568 | template = Tiles.UniformTemplates.get(numCols, targetTiles); 569 | } 570 | 571 | return template; 572 | }; 573 | 574 | // ensures we have a good template for the specified numbef of tiles 575 | Grid.prototype.ensureTemplate = function(numTiles) { 576 | 577 | // verfiy that the current template is still valid 578 | if (!this.template || this.template.numCols !== this.numCols) { 579 | this.template = this.createTemplate(this.numCols, numTiles); 580 | this.isDirty = true; 581 | } else { 582 | 583 | // append another template if we don't have enough rects 584 | var missingRects = numTiles - this.template.rects.length; 585 | if (missingRects > 0) { 586 | this.template.append( 587 | this.createTemplate(this.numCols, missingRects)); 588 | this.isDirty = true; 589 | } 590 | 591 | } 592 | }; 593 | 594 | // helper that returns true if a tile was in the viewport or will be given 595 | // the new pixel rect coordinates and dimensions 596 | function wasOrWillBeVisible(viewRect, tile, newRect) { 597 | 598 | var viewMaxY = viewRect.y + viewRect.height, 599 | viewMaxX = viewRect.x + viewRect.width; 600 | 601 | // note: y axis is the more common exclusion, so check that first 602 | 603 | // was the tile visible? 604 | if (tile) { 605 | if (!((tile.top > viewMaxY) || (tile.top + tile.height < viewRect.y) || 606 | (tile.left > viewMaxX) || (tile.left + tile.width < viewRect.x))) { 607 | return true; 608 | } 609 | } 610 | 611 | if (newRect) { 612 | // will it be visible? 613 | if (!((newRect.y > viewMaxY) || (newRect.y + newRect.height < viewRect.y) || 614 | (newRect.x > viewMaxX) || (newRect.x + newRect.width < viewRect.x))) { 615 | return true; 616 | } 617 | } 618 | 619 | return false; 620 | } 621 | 622 | Grid.prototype.shouldRedraw = function() { 623 | 624 | // see if we need to calculate the cell size 625 | if (this.cellWidth <= 0) { 626 | this.resize(); 627 | } 628 | 629 | // verify that we have a template 630 | this.ensureTemplate(this.tiles.length); 631 | 632 | // only redraw when necessary 633 | var shouldRedraw = (this.isDirty || 634 | this.tilesAdded.length > 0 || 635 | this.tilesRemoved.length > 0); 636 | 637 | return shouldRedraw; 638 | }; 639 | 640 | // converts cell rectangles to pixel rectangles. allows users 641 | // to override exact placement of the tiles. 642 | Grid.prototype.getPixelRectangle = function(cellRect) { 643 | 644 | var widthPlusPadding = this.cellWidth + this.cellPadding, 645 | heightPlusPadding = this.cellHeight + this.cellPadding; 646 | 647 | return new Tiles.Rectangle( 648 | cellRect.x * widthPlusPadding, 649 | cellRect.y * heightPlusPadding, 650 | (cellRect.width * widthPlusPadding) - this.cellPadding, 651 | (cellRect.height * heightPlusPadding) - this.cellPadding); 652 | }; 653 | 654 | // redraws the grid after tile collection changes 655 | Grid.prototype.redraw = function(animate, onComplete) { 656 | 657 | // see if we should redraw 658 | if (!this.shouldRedraw()) { 659 | if (onComplete) { 660 | onComplete(false); // tell callback that we did not redraw 661 | } 662 | return; 663 | } 664 | 665 | var numTiles = this.tiles.length, 666 | pageSize = this.priorityPageSize, 667 | duration = this.animationDuration, 668 | tileIndex = 0, 669 | appendDelay = 0, 670 | maxAppendDelay = 0, 671 | viewRect = new Tiles.Rectangle( 672 | this.$el.scrollLeft(), 673 | this.$el.scrollTop(), 674 | this.$el.width(), 675 | this.$el.height()), 676 | tile, added, pageRects, pageTiles, i, len, cellRect, pixelRect, 677 | animateTile, priorityRects, priorityTiles; 678 | 679 | 680 | // chunk tile layout by pages which are internally prioritized 681 | for (tileIndex = 0; tileIndex < numTiles; tileIndex += pageSize) { 682 | 683 | // get the next page of rects and tiles 684 | pageRects = this.template.rects.slice(tileIndex, tileIndex + pageSize); 685 | pageTiles = this.tiles.slice(tileIndex, tileIndex + pageSize); 686 | 687 | // create a copy that can be ordered 688 | priorityRects = pageRects.slice(0); 689 | priorityTiles = pageTiles.slice(0); 690 | 691 | // prioritize the page of rects and tiles 692 | if (this.prioritizePage) { 693 | this.prioritizePage(priorityRects, priorityTiles); 694 | } 695 | 696 | // place all the tiles for the current page 697 | for (i = 0, len = priorityTiles.length; i < len; i++) { 698 | tile = priorityTiles[i]; 699 | added = $.inArray(tile, this.tilesAdded) >= 0; 700 | 701 | cellRect = priorityRects[i]; 702 | pixelRect = this.getPixelRectangle(cellRect); 703 | 704 | tile.resize( 705 | cellRect, 706 | pixelRect, 707 | animate && !added && wasOrWillBeVisible(viewRect, tile, pixelRect), 708 | duration); 709 | 710 | if (added) { 711 | 712 | // decide whether to animate (fadeIn) and get the duration 713 | animateTile = animate && wasOrWillBeVisible(viewRect, null, pixelRect); 714 | if (animateTile && this.getAppendDelay) { 715 | appendDelay = this.getAppendDelay( 716 | cellRect, pageRects, priorityRects, 717 | tile, pageTiles, priorityTiles); 718 | maxAppendDelay = Math.max(maxAppendDelay, appendDelay) || 0; 719 | } else { 720 | appendDelay = 0; 721 | } 722 | 723 | tile.appendTo(this.$el, animateTile, appendDelay, duration); 724 | } 725 | } 726 | } 727 | 728 | // fade out all removed tiles 729 | for (i = 0, len = this.tilesRemoved.length; i < len; i++) { 730 | tile = this.tilesRemoved[i]; 731 | animateTile = animate && wasOrWillBeVisible(viewRect, tile, null); 732 | tile.remove(animateTile, duration); 733 | } 734 | 735 | // clear pending queues for add / remove 736 | this.tilesRemoved = []; 737 | this.tilesAdded = []; 738 | this.isDirty = false; 739 | 740 | if (onComplete) { 741 | setTimeout( 742 | function() { onComplete(true); }, 743 | Math.max(maxAppendDelay, duration) + 10 744 | ); 745 | } 746 | }; 747 | 748 | })(jQuery); 749 | -------------------------------------------------------------------------------- /dist/tiles.min.js: -------------------------------------------------------------------------------- 1 | /*! Tiles.js v1.0.0 | http://thinkpixellab.com/tilesjs */ 2 | var Tiles={};!function(a){var b=Tiles.Tile=function(b,c){this.id=b,this.top=0,this.left=0,this.width=0,this.height=0,this.$el=a(c||document.createElement("div"))};b.prototype.appendTo=function(a,b,c,d){this.$el.hide().appendTo(a),b?this.$el.delay(c).fadeIn(d):this.$el.show()},b.prototype.remove=function(b,c){b?this.$el.fadeOut({complete:function(){a(this).remove()}}):this.$el.remove()},b.prototype.resize=function(a,b,c,d,e){var f={},g=!1;this.left!==b.x&&(f.left=b.x,this.left=b.x,g=!0),this.top!==b.y&&(f.top=b.y,this.top=b.y,g=!0),this.width!==b.width&&(f.width=b.width,this.width=b.width,g=!0),this.height!==b.height&&(f.height=b.height,this.height=b.height,g=!0);var h=this,i=function(){var a=h.$el[0];h.left!==a.offsetLeft&&h.$el.css("left",h.left),h.top!==a.offsetTop&&h.$el.css("top",h.top),e&&e()};c&&g?this.$el.animate(f,{duration:d,easing:"swing",complete:i}):(g&&this.$el.css(f),setTimeout(i,d))}}(jQuery),function(a){function b(a,b,c,d){this.x=a,this.y=b,this.width=c,this.height=d}var c=function(a){var b,c,d,e,f,g=[],h=a.length;for(c=0;h>c;c++)for(d=a[c],g[c]=[],b=0,e=d.length;e>b;b++)f=d[b]," "!==f&&g[c].push(f);return g};b.prototype.copy=function(){return new b(this.x,this.y,this.width,this.height)},Tiles.Rectangle=b;var d=function(a){var c,d,e,f,g,h,i,j=[],k=a.length,l=0===k?0:a[0].length;for(a=a.slice(),g=0;k>g;g++)a[g]=a[g].slice();for(g=0;k>g;g++)for(f=0;l>f;f++)if(c=a[g][f],null!=c){if(e=1,d=1,c!==Tiles.Template.SINGLE_CELL){for(;l>e+f&&c===a[g][f+e];)e++;for(;k>d+g&&c===a[g+d][f];)d++}for(i=0;d>i;i++)for(h=0;e>h;h++)a[g+i][f+h]=null;j.push(new b(f,g,e,d))}return j};Tiles.Template=function(a,b,c){this.rects=a,this.numTiles=this.rects.length,this.numRows=c,this.numCols=b},Tiles.Template.prototype.copy=function(){var a,b,c=[];for(b=0,a=this.rects.length;a>b;b++)c.push(this.rects[b].copy());return new Tiles.Template(c,this.numCols,this.numRows)},Tiles.Template.prototype.append=function(a){if(this.numCols!==a.numCols)throw"Appended templates must have the same number of columns";var c,d,e,f=this.numRows;for(c=0,d=a.rects.length;d>c;c++)e=a.rects[c],this.rects.push(new b(e.x,f+e.y,e.width,e.height));this.numRows+=a.numRows,this.numTiles+=a.numTiles},Tiles.Template.fromJSON=function(a){var b=c(a),e=d(b);return new Tiles.Template(e,b.length>0?b[0].length:0,b.length)},Tiles.Template.prototype.toJSON=function(){var a,b,c,d,e,f,g="ABCDEFGHIJKLMNOPQRSTUVWXYZ",h=g.length,i=0,j=[];for(e=0;ea;a++)if(c=this.rects[a],c.width>1||c.height>1){for(f=g[i],e=0;ed;d++)for(c=0;a>c;c++)f.push(new Tiles.Rectangle(c,d,1,1));return new Tiles.Template(f,a,e)}},function(a){function b(b){var c,d,e=[],f=b?b.length:0;for(c=0;f>c;c++)d=b[c],-1===a.inArray(d,e)&&e.push(d);return e}function c(a,b,c){var d=a.y+a.height,e=a.x+a.width;return b&&!(b.top>d||b.top+b.heighte||b.left+b.widthd||c.y+c.heighte||c.x+c.width0&&(this.numCols=a,this.isDirty=!0);var b=this.resizeCellWidth();this.cellWidth!==b&&b>0&&(this.cellWidth=b,this.cellHeight=this.cellWidth/this.cellAspectRatio,this.isDirty=!0)},d.prototype.updateTiles=function(c){c=b(c);var d,e,f,g,h=c.length,i=[];for(d=this.tiles.length-1;d>=0;d--)e=this.tiles[d],g=a.inArray(e.id,c),0>g?this.tilesRemoved.push(e):i[g]=e;for(this.tiles=[],d=0;h>d;d++){if(e=i[d],!e){if(f=c[d],this.createTile){if(e=this.createTile(f),!e)continue}else e=new Tiles.Tile(f);this.tilesAdded.push(e)}this.tiles.push(e)}},d.prototype.insertTiles=function(a){this.addTiles(a,!0)},d.prototype.addTiles=function(a,b){if(a&&0!==a.length){var c,d=[],e=this.tiles.length;for(c=0;e>c;c++)d.push(this.tiles[c].id);var f=b?a.concat(d):d.concat(a);this.updateTiles(f)}},d.prototype.removeTiles=function(b){if(b&&0!==b.length){var c,d,e,f=[];for(c=0,d=this.tiles.length;d>c;c++)e=this.tiles[c].id,-1===a.inArray(e,b)&&f.push(e);this.updateTiles(f)}},d.prototype.createTemplate=function(a,b){a=Math.max(1,a);var c=this.templateFactory.get(a,b);return c||(c=Tiles.UniformTemplates.get(a,b)),c},d.prototype.ensureTemplate=function(a){if(this.template&&this.template.numCols===this.numCols){var b=a-this.template.rects.length;b>0&&(this.template.append(this.createTemplate(this.numCols,b)),this.isDirty=!0)}else this.template=this.createTemplate(this.numCols,a),this.isDirty=!0},d.prototype.shouldRedraw=function(){this.cellWidth<=0&&this.resize(),this.ensureTemplate(this.tiles.length);var a=this.isDirty||this.tilesAdded.length>0||this.tilesRemoved.length>0;return a},d.prototype.getPixelRectangle=function(a){var b=this.cellWidth+this.cellPadding,c=this.cellHeight+this.cellPadding;return new Tiles.Rectangle(a.x*b,a.y*c,a.width*b-this.cellPadding,a.height*c-this.cellPadding)},d.prototype.redraw=function(b,d){if(!this.shouldRedraw())return void(d&&d(!1));var e,f,g,h,i,j,k,l,m,n,o,p=this.tiles.length,q=this.priorityPageSize,r=this.animationDuration,s=0,t=0,u=0,v=new Tiles.Rectangle(this.$el.scrollLeft(),this.$el.scrollTop(),this.$el.width(),this.$el.height());for(s=0;p>s;s+=q)for(g=this.template.rects.slice(s,s+q),h=this.tiles.slice(s,s+q),n=g.slice(0),o=h.slice(0),this.prioritizePage&&this.prioritizePage(n,o),i=0,j=o.length;j>i;i++)e=o[i],f=a.inArray(e,this.tilesAdded)>=0,k=n[i],l=this.getPixelRectangle(k),e.resize(k,l,b&&!f&&c(v,e,l),r),f&&(m=b&&c(v,null,l),m&&this.getAppendDelay?(t=this.getAppendDelay(k,g,n,e,h,o),u=Math.max(u,t)||0):t=0,e.appendTo(this.$el,m,t,r));for(i=0,j=this.tilesRemoved.length;j>i;i++)e=this.tilesRemoved[i],m=b&&c(v,e,null),e.remove(m,r);this.tilesRemoved=[],this.tilesAdded=[],this.isDirty=!1,d&&setTimeout(function(){d(!0)},Math.max(u,r)+10)}}(jQuery); -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | var BANNER_TEMPLATE = '/*! <%= pkg.title %> v<%= pkg.version %> | <%= pkg.homepage %> */\n'; 5 | 6 | // Project configuration. 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | concat: { 10 | options: { 11 | banner: BANNER_TEMPLATE 12 | }, 13 | dist: { 14 | src: [ 15 | 'src/Tile.js', 16 | 'src/Template.js', 17 | 'src/UniformTemplates.js', 18 | 'src/Grid.js' 19 | ], 20 | dest: 'dist/tiles.js' 21 | } 22 | }, 23 | jshint: { 24 | files: [ 'gruntfile.js', 'src/*.js' ], 25 | options: { 26 | curly: true, 27 | eqeqeq: true, 28 | immed: true, 29 | latedef: true, 30 | newcap: true, 31 | noarg: true, 32 | sub: true, 33 | undef: true, 34 | boss: true, 35 | eqnull: true, 36 | browser: true, 37 | globals: { 38 | jQuery: true, 39 | Tiles: true, 40 | console: true 41 | } 42 | } 43 | }, 44 | watch: { 45 | cj: { 46 | files: ['<%= jshint.files %>'], 47 | tasks: ['jshint', 'concat', 'copy', 'uglfiy'] 48 | } 49 | }, 50 | uglify: { 51 | options: { 52 | banner: BANNER_TEMPLATE 53 | }, 54 | dist: { 55 | src: ['<%= concat.dist.dest %>'], 56 | dest: 'dist/tiles.min.js' 57 | } 58 | } 59 | }); 60 | 61 | grunt.loadNpmTasks('grunt-contrib-uglify'); 62 | grunt.loadNpmTasks('grunt-contrib-jshint'); 63 | grunt.loadNpmTasks('grunt-contrib-watch'); 64 | grunt.loadNpmTasks('grunt-contrib-concat'); 65 | grunt.loadNpmTasks('grunt-contrib-copy'); 66 | grunt.loadNpmTasks('grunt-contrib-clean'); 67 | 68 | grunt.registerTask('default', ['jshint', 'concat', 'uglify']); 69 | 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tilesjs", 3 | "title": "Tiles.js", 4 | "version": "1.0.1", 5 | "homepage": "http://thinkpixellab.com/tilesjs", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Pixel Lab", 9 | "url": "http://thinkpixellab.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/thinkpixellab/tilesjs.git" 14 | }, 15 | "dependencies": { 16 | "grunt": "~0.4.5", 17 | "grunt-contrib-uglify": "~0.9.1", 18 | "grunt-contrib-jshint": "~0.11.2", 19 | "grunt-contrib-watch": "~0.6.1", 20 | "grunt-contrib-concat": "~0.5.1", 21 | "grunt-contrib-copy": "~0.8.0", 22 | "grunt-contrib-clean": "~0.6.0" 23 | }, 24 | "devDependencies": {}, 25 | "keywords": [] 26 | } 27 | -------------------------------------------------------------------------------- /src/Grid.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | var Grid = Tiles.Grid = function(element) { 4 | 5 | this.$el = $(element); 6 | 7 | // animation lasts 500 ms by default 8 | this.animationDuration = 500; 9 | 10 | // the default set of factories used when creating templates 11 | this.templateFactory = Tiles.UniformTemplates; 12 | 13 | // defines the page size for prioritization of positions and tiles 14 | this.priorityPageSize = Number.MAX_VALUE; 15 | 16 | // spacing between tiles 17 | this.cellPadding = 10; 18 | 19 | // min width and height of a cell in the grid 20 | this.cellWidthMin = 150; 21 | this.cellHeightMin = 150; 22 | 23 | // actual width and height of a cell in the grid 24 | this.cellWidth = 0; 25 | this.cellHeight = 0; 26 | 27 | this.cellAspectRatio = 1; 28 | 29 | // number of tile cell columns 30 | this.numCols = 1; 31 | 32 | // cache the current template 33 | this.template = null; 34 | 35 | // flag that tracks whether a redraw is necessary 36 | this.isDirty = true; 37 | 38 | this.tiles = []; 39 | 40 | // keep track of added and removed tiles so we can update tiles 41 | // and the render the grid independently. 42 | this.tilesAdded = []; 43 | this.tilesRemoved = []; 44 | }; 45 | 46 | Grid.prototype.getContentWidth = function() { 47 | // by default, the entire container width is used when drawing tiles 48 | return this.$el.width(); 49 | }; 50 | 51 | // gets the number of columns during a resize 52 | Grid.prototype.resizeColumns = function() { 53 | var panelWidth = this.getContentWidth(); 54 | 55 | // ensure we have at least one column 56 | return Math.max(1, Math.floor((panelWidth + this.cellPadding) / 57 | (this.cellWidthMin + this.cellPadding))); 58 | }; 59 | 60 | // gets the cell size during a grid resize 61 | Grid.prototype.resizeCellWidth = function() { 62 | var panelWidth = this.getContentWidth(); 63 | return Math.ceil((panelWidth + this.cellPadding) / this.numCols) - 64 | this.cellPadding; 65 | }; 66 | 67 | Grid.prototype.resize = function() { 68 | 69 | var newCols = this.resizeColumns(); 70 | if (this.numCols !== newCols && newCols > 0) { 71 | this.numCols = newCols; 72 | this.isDirty = true; 73 | } 74 | 75 | var newCellWidth = this.resizeCellWidth(); 76 | if (this.cellWidth !== newCellWidth && newCellWidth > 0) { 77 | this.cellWidth = newCellWidth; 78 | this.cellHeight = this.cellWidth / this.cellAspectRatio; 79 | this.isDirty = true; 80 | } 81 | }; 82 | 83 | // refresh all tiles based on the current content 84 | Grid.prototype.updateTiles = function(newTileIds) { 85 | 86 | // ensure we dont have duplicate ids 87 | newTileIds = uniques(newTileIds); 88 | 89 | var numTiles = newTileIds.length, 90 | newTiles = [], 91 | i, tile, tileId, index; 92 | 93 | // retain existing tiles and queue remaining tiles for removal 94 | for (i = this.tiles.length - 1; i >= 0; i--) { 95 | tile = this.tiles[i]; 96 | index = $.inArray(tile.id, newTileIds); 97 | if (index < 0) { 98 | this.tilesRemoved.push(tile); 99 | //console.log('Removing tile: ' + tile.id) 100 | } 101 | else { 102 | newTiles[index] = tile; 103 | } 104 | } 105 | 106 | // clear existing tiles 107 | this.tiles = []; 108 | 109 | // make sure we have tiles for new additions 110 | for (i = 0; i < numTiles; i++) { 111 | 112 | tile = newTiles[i]; 113 | if (!tile) { 114 | 115 | tileId = newTileIds[i]; 116 | 117 | // see if grid has a custom tile factory 118 | if (this.createTile) { 119 | 120 | tile = this.createTile(tileId); 121 | 122 | // skip the tile if it couldn't be created 123 | if (!tile) { 124 | //console.log('Tile element could not be created, id: ' + tileId); 125 | continue; 126 | } 127 | 128 | } else { 129 | 130 | tile = new Tiles.Tile(tileId); 131 | } 132 | 133 | // add tiles to queue (will be appended to DOM during redraw) 134 | this.tilesAdded.push(tile); 135 | //console.log('Adding tile: ' + tile.id); 136 | } 137 | 138 | this.tiles.push(tile); 139 | } 140 | }; 141 | 142 | // helper to return unique items 143 | function uniques(items) { 144 | var results = [], 145 | numItems = items ? items.length : 0, 146 | i, item; 147 | 148 | for (i = 0; i < numItems; i++) { 149 | item = items[i]; 150 | if ($.inArray(item, results) === -1) { 151 | results.push(item); 152 | } 153 | } 154 | 155 | return results; 156 | } 157 | 158 | // prepend new tiles 159 | Grid.prototype.insertTiles = function(newTileIds) { 160 | this.addTiles(newTileIds, true); 161 | }; 162 | 163 | // append new tiles 164 | Grid.prototype.addTiles = function(newTileIds, prepend) { 165 | 166 | if (!newTileIds || newTileIds.length === 0) { 167 | return; 168 | } 169 | 170 | var prevTileIds = [], 171 | prevTileCount = this.tiles.length, 172 | i; 173 | 174 | // get the existing tile ids 175 | for (i = 0; i < prevTileCount; i++) { 176 | prevTileIds.push(this.tiles[i].id); 177 | } 178 | 179 | var tileIds = prepend ? newTileIds.concat(prevTileIds) 180 | : prevTileIds.concat(newTileIds); 181 | this.updateTiles(tileIds); 182 | }; 183 | 184 | Grid.prototype.removeTiles = function(removeTileIds) { 185 | 186 | if (!removeTileIds || removeTileIds.length === 0) { 187 | return; 188 | } 189 | 190 | var updateTileIds = [], 191 | i, len, id; 192 | 193 | // get the set of ids which have not been removed 194 | for (i = 0, len = this.tiles.length; i < len; i++) { 195 | id = this.tiles[i].id; 196 | if ($.inArray(id, removeTileIds) === -1) { 197 | updateTileIds.push(id); 198 | } 199 | } 200 | 201 | this.updateTiles(updateTileIds); 202 | }; 203 | 204 | Grid.prototype.createTemplate = function(numCols, targetTiles) { 205 | 206 | // ensure that we have at least one column 207 | numCols = Math.max(1, numCols); 208 | 209 | var template = this.templateFactory.get(numCols, targetTiles); 210 | if (!template) { 211 | 212 | // fallback in case the default factory can't generate a good template 213 | template = Tiles.UniformTemplates.get(numCols, targetTiles); 214 | } 215 | 216 | return template; 217 | }; 218 | 219 | // ensures we have a good template for the specified numbef of tiles 220 | Grid.prototype.ensureTemplate = function(numTiles) { 221 | 222 | // verfiy that the current template is still valid 223 | if (!this.template || this.template.numCols !== this.numCols) { 224 | this.template = this.createTemplate(this.numCols, numTiles); 225 | this.isDirty = true; 226 | } else { 227 | 228 | // append another template if we don't have enough rects 229 | var missingRects = numTiles - this.template.rects.length; 230 | if (missingRects > 0) { 231 | this.template.append( 232 | this.createTemplate(this.numCols, missingRects)); 233 | this.isDirty = true; 234 | } 235 | 236 | } 237 | }; 238 | 239 | // helper that returns true if a tile was in the viewport or will be given 240 | // the new pixel rect coordinates and dimensions 241 | function wasOrWillBeVisible(viewRect, tile, newRect) { 242 | 243 | var viewMaxY = viewRect.y + viewRect.height, 244 | viewMaxX = viewRect.x + viewRect.width; 245 | 246 | // note: y axis is the more common exclusion, so check that first 247 | 248 | // was the tile visible? 249 | if (tile) { 250 | if (!((tile.top > viewMaxY) || (tile.top + tile.height < viewRect.y) || 251 | (tile.left > viewMaxX) || (tile.left + tile.width < viewRect.x))) { 252 | return true; 253 | } 254 | } 255 | 256 | if (newRect) { 257 | // will it be visible? 258 | if (!((newRect.y > viewMaxY) || (newRect.y + newRect.height < viewRect.y) || 259 | (newRect.x > viewMaxX) || (newRect.x + newRect.width < viewRect.x))) { 260 | return true; 261 | } 262 | } 263 | 264 | return false; 265 | } 266 | 267 | Grid.prototype.shouldRedraw = function() { 268 | 269 | // see if we need to calculate the cell size 270 | if (this.cellWidth <= 0) { 271 | this.resize(); 272 | } 273 | 274 | // verify that we have a template 275 | this.ensureTemplate(this.tiles.length); 276 | 277 | // only redraw when necessary 278 | var shouldRedraw = (this.isDirty || 279 | this.tilesAdded.length > 0 || 280 | this.tilesRemoved.length > 0); 281 | 282 | return shouldRedraw; 283 | }; 284 | 285 | // converts cell rectangles to pixel rectangles. allows users 286 | // to override exact placement of the tiles. 287 | Grid.prototype.getPixelRectangle = function(cellRect) { 288 | 289 | var widthPlusPadding = this.cellWidth + this.cellPadding, 290 | heightPlusPadding = this.cellHeight + this.cellPadding; 291 | 292 | return new Tiles.Rectangle( 293 | cellRect.x * widthPlusPadding, 294 | cellRect.y * heightPlusPadding, 295 | (cellRect.width * widthPlusPadding) - this.cellPadding, 296 | (cellRect.height * heightPlusPadding) - this.cellPadding); 297 | }; 298 | 299 | // redraws the grid after tile collection changes 300 | Grid.prototype.redraw = function(animate, onComplete) { 301 | 302 | // see if we should redraw 303 | if (!this.shouldRedraw()) { 304 | if (onComplete) { 305 | onComplete(false); // tell callback that we did not redraw 306 | } 307 | return; 308 | } 309 | 310 | var numTiles = this.tiles.length, 311 | pageSize = this.priorityPageSize, 312 | duration = this.animationDuration, 313 | tileIndex = 0, 314 | appendDelay = 0, 315 | maxAppendDelay = 0, 316 | viewRect = new Tiles.Rectangle( 317 | this.$el.scrollLeft(), 318 | this.$el.scrollTop(), 319 | this.$el.width(), 320 | this.$el.height()), 321 | tile, added, pageRects, pageTiles, i, len, cellRect, pixelRect, 322 | animateTile, priorityRects, priorityTiles; 323 | 324 | 325 | // chunk tile layout by pages which are internally prioritized 326 | for (tileIndex = 0; tileIndex < numTiles; tileIndex += pageSize) { 327 | 328 | // get the next page of rects and tiles 329 | pageRects = this.template.rects.slice(tileIndex, tileIndex + pageSize); 330 | pageTiles = this.tiles.slice(tileIndex, tileIndex + pageSize); 331 | 332 | // create a copy that can be ordered 333 | priorityRects = pageRects.slice(0); 334 | priorityTiles = pageTiles.slice(0); 335 | 336 | // prioritize the page of rects and tiles 337 | if (this.prioritizePage) { 338 | this.prioritizePage(priorityRects, priorityTiles); 339 | } 340 | 341 | // place all the tiles for the current page 342 | for (i = 0, len = priorityTiles.length; i < len; i++) { 343 | tile = priorityTiles[i]; 344 | added = $.inArray(tile, this.tilesAdded) >= 0; 345 | 346 | cellRect = priorityRects[i]; 347 | pixelRect = this.getPixelRectangle(cellRect); 348 | 349 | tile.resize( 350 | cellRect, 351 | pixelRect, 352 | animate && !added && wasOrWillBeVisible(viewRect, tile, pixelRect), 353 | duration); 354 | 355 | if (added) { 356 | 357 | // decide whether to animate (fadeIn) and get the duration 358 | animateTile = animate && wasOrWillBeVisible(viewRect, null, pixelRect); 359 | if (animateTile && this.getAppendDelay) { 360 | appendDelay = this.getAppendDelay( 361 | cellRect, pageRects, priorityRects, 362 | tile, pageTiles, priorityTiles); 363 | maxAppendDelay = Math.max(maxAppendDelay, appendDelay) || 0; 364 | } else { 365 | appendDelay = 0; 366 | } 367 | 368 | tile.appendTo(this.$el, animateTile, appendDelay, duration); 369 | } 370 | } 371 | } 372 | 373 | // fade out all removed tiles 374 | for (i = 0, len = this.tilesRemoved.length; i < len; i++) { 375 | tile = this.tilesRemoved[i]; 376 | animateTile = animate && wasOrWillBeVisible(viewRect, tile, null); 377 | tile.remove(animateTile, duration); 378 | } 379 | 380 | // clear pending queues for add / remove 381 | this.tilesRemoved = []; 382 | this.tilesAdded = []; 383 | this.isDirty = false; 384 | 385 | if (onComplete) { 386 | setTimeout( 387 | function() { onComplete(true); }, 388 | Math.max(maxAppendDelay, duration) + 10 389 | ); 390 | } 391 | }; 392 | 393 | })(jQuery); 394 | -------------------------------------------------------------------------------- /src/Template.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | A grid template specifies the layout of variably sized tiles. A single 4 | cell tile should use the period character. Larger tiles may be created 5 | using any character that is unused by a adjacent tile. Whitespace is 6 | ignored when parsing the rows. 7 | 8 | Examples: 9 | 10 | var simpleTemplate = [ 11 | ' A A . B ', 12 | ' A A . B ', 13 | ' . C C . ', 14 | ] 15 | 16 | var complexTemplate = [ 17 | ' J J . . E E ', 18 | ' . A A . E E ', 19 | ' B A A F F . ', 20 | ' B . D D . H ', 21 | ' C C D D G H ', 22 | ' C C . . G . ', 23 | ]; 24 | */ 25 | 26 | (function($) { 27 | 28 | // remove whitespace and create 2d array 29 | var parseCells = function(rows) { 30 | var cells = [], 31 | numRows = rows.length, 32 | x, y, row, rowLength, cell; 33 | 34 | // parse each row 35 | for(y = 0; y < numRows; y++) { 36 | 37 | row = rows[y]; 38 | cells[y] = []; 39 | 40 | // parse the cells in a single row 41 | for (x = 0, rowLength = row.length; x < rowLength; x++) { 42 | cell = row[x]; 43 | if (cell !== ' ') { 44 | cells[y].push(cell); 45 | } 46 | } 47 | } 48 | 49 | // TODO: check to make sure the array isn't jagged 50 | 51 | return cells; 52 | }; 53 | 54 | function Rectangle(x, y, width, height) { 55 | this.x = x; 56 | this.y = y; 57 | this.width = width; 58 | this.height = height; 59 | } 60 | 61 | Rectangle.prototype.copy = function() { 62 | return new Rectangle(this.x, this.y, this.width, this.height); 63 | }; 64 | 65 | Tiles.Rectangle = Rectangle; 66 | 67 | // convert a 2d array of cell ids to a list of tile rects 68 | var parseRects = function(cells) { 69 | var rects = [], 70 | numRows = cells.length, 71 | numCols = numRows === 0 ? 0 : cells[0].length, 72 | cell, height, width, x, y, rectX, rectY; 73 | 74 | // make a copy of the cells that we can modify 75 | cells = cells.slice(); 76 | for (y = 0; y < numRows; y++) { 77 | cells[y] = cells[y].slice(); 78 | } 79 | 80 | // iterate through every cell and find rectangles 81 | for (y = 0; y < numRows; y++) { 82 | for(x = 0; x < numCols; x++) { 83 | cell = cells[y][x]; 84 | 85 | // skip cells that are null 86 | if (cell == null) { 87 | continue; 88 | } 89 | 90 | width = 1; 91 | height = 1; 92 | 93 | if (cell !== Tiles.Template.SINGLE_CELL) { 94 | 95 | // find the width by going right until cell id no longer matches 96 | while(width + x < numCols && 97 | cell === cells[y][x + width]) { 98 | width++; 99 | } 100 | 101 | // now find height by going down 102 | while (height + y < numRows && 103 | cell === cells[y + height][x]) { 104 | height++; 105 | } 106 | } 107 | 108 | // null out all cells for the rect 109 | for(rectY = 0; rectY < height; rectY++) { 110 | for(rectX = 0; rectX < width; rectX++) { 111 | cells[y + rectY][x + rectX] = null; 112 | } 113 | } 114 | 115 | // add the rect 116 | rects.push(new Rectangle(x, y, width, height)); 117 | } 118 | } 119 | 120 | return rects; 121 | }; 122 | 123 | Tiles.Template = function(rects, numCols, numRows) { 124 | this.rects = rects; 125 | this.numTiles = this.rects.length; 126 | this.numRows = numRows; 127 | this.numCols = numCols; 128 | }; 129 | 130 | Tiles.Template.prototype.copy = function() { 131 | 132 | var copyRects = [], 133 | len, i; 134 | for (i = 0, len = this.rects.length; i < len; i++) { 135 | copyRects.push(this.rects[i].copy()); 136 | } 137 | 138 | return new Tiles.Template(copyRects, this.numCols, this.numRows); 139 | }; 140 | 141 | // appends another template (assumes both are full rectangular grids) 142 | Tiles.Template.prototype.append = function(other) { 143 | 144 | if (this.numCols !== other.numCols) { 145 | throw 'Appended templates must have the same number of columns'; 146 | } 147 | 148 | // new rects begin after the last current row 149 | var startY = this.numRows, 150 | i, len, rect; 151 | 152 | // copy rects from the other template 153 | for (i = 0, len = other.rects.length; i < len; i++) { 154 | rect = other.rects[i]; 155 | this.rects.push( 156 | new Rectangle(rect.x, startY + rect.y, rect.width, rect.height)); 157 | } 158 | 159 | this.numRows += other.numRows; 160 | this.numTiles += other.numTiles; 161 | }; 162 | 163 | Tiles.Template.fromJSON = function(rows) { 164 | // convert rows to cells and then to rects 165 | var cells = parseCells(rows), 166 | rects = parseRects(cells); 167 | return new Tiles.Template( 168 | rects, 169 | cells.length > 0 ? cells[0].length : 0, 170 | cells.length); 171 | }; 172 | 173 | Tiles.Template.prototype.toJSON = function() { 174 | // for now we'll assume 26 chars is enough (we don't solve graph coloring) 175 | var LABELS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 176 | NUM_LABELS = LABELS.length, 177 | labelIndex = 0, 178 | rows = [], 179 | i, len, rect, x, y, label; 180 | 181 | // fill in single tiles for each cell 182 | for (y = 0; y < this.numRows; y++) { 183 | rows[y] = []; 184 | for (x = 0; x < this.numCols; x++) { 185 | rows[y][x] = Tiles.Template.SINGLE_CELL; 186 | } 187 | } 188 | 189 | // now fill in bigger tiles 190 | for (i = 0, len = this.rects.length; i < len; i++) { 191 | rect = this.rects[i]; 192 | if (rect.width > 1 || rect.height > 1) { 193 | 194 | // mark the tile position with a label 195 | label = LABELS[labelIndex]; 196 | for(y = 0; y < rect.height; y++) { 197 | for(x = 0; x < rect.width; x++) { 198 | rows[rect.y + y][rect.x + x] = label; 199 | } 200 | } 201 | 202 | // advance the label index 203 | labelIndex = (labelIndex + 1) % NUM_LABELS; 204 | } 205 | } 206 | 207 | // turn the rows into strings 208 | for (y = 0; y < this.numRows; y++) { 209 | rows[y] = rows[y].join(''); 210 | } 211 | 212 | return rows; 213 | }; 214 | 215 | // period used to designate a single 1x1 cell tile 216 | Tiles.Template.SINGLE_CELL = '.'; 217 | 218 | })(jQuery); 219 | -------------------------------------------------------------------------------- /src/Tile.js: -------------------------------------------------------------------------------- 1 | 2 | // single namespace export 3 | var Tiles = {}; 4 | 5 | (function($) { 6 | 7 | var Tile = Tiles.Tile = function(tileId, element) { 8 | 9 | this.id = tileId; 10 | 11 | // position and dimensions of tile inside the parent panel 12 | this.top = 0; 13 | this.left = 0; 14 | this.width = 0; 15 | this.height = 0; 16 | 17 | // cache the tile container element 18 | this.$el = $(element || document.createElement('div')); 19 | }; 20 | 21 | Tile.prototype.appendTo = function($parent, fadeIn, delay, duration) { 22 | this.$el 23 | .hide() 24 | .appendTo($parent); 25 | 26 | if (fadeIn) { 27 | this.$el.delay(delay).fadeIn(duration); 28 | } 29 | else { 30 | this.$el.show(); 31 | } 32 | }; 33 | 34 | Tile.prototype.remove = function(animate, duration) { 35 | if (animate) { 36 | this.$el.fadeOut({ 37 | complete: function() { 38 | $(this).remove(); 39 | } 40 | }); 41 | } 42 | else { 43 | this.$el.remove(); 44 | } 45 | }; 46 | 47 | // updates the tile layout with optional animation 48 | Tile.prototype.resize = function(cellRect, pixelRect, animate, duration, onComplete) { 49 | 50 | // store the list of needed changes 51 | var cssChanges = {}, 52 | changed = false; 53 | 54 | // update position and dimensions 55 | if (this.left !== pixelRect.x) { 56 | cssChanges.left = pixelRect.x; 57 | this.left = pixelRect.x; 58 | changed = true; 59 | } 60 | if (this.top !== pixelRect.y) { 61 | cssChanges.top = pixelRect.y; 62 | this.top = pixelRect.y; 63 | changed = true; 64 | } 65 | if (this.width !== pixelRect.width) { 66 | cssChanges.width = pixelRect.width; 67 | this.width = pixelRect.width; 68 | changed = true; 69 | } 70 | if (this.height !== pixelRect.height) { 71 | cssChanges.height = pixelRect.height; 72 | this.height = pixelRect.height; 73 | changed = true; 74 | } 75 | 76 | // Sometimes animation fails to set the css top and left correctly 77 | // in webkit. We'll validate upon completion of the animation and 78 | // set the properties again if they don't match the expected values. 79 | var tile = this, 80 | validateChangesAndComplete = function() { 81 | var el = tile.$el[0]; 82 | if (tile.left !== el.offsetLeft) { 83 | //console.log ('mismatch left:' + tile.left + ' actual:' + el.offsetLeft + ' id:' + tile.id); 84 | tile.$el.css('left', tile.left); 85 | } 86 | if (tile.top !== el.offsetTop) { 87 | //console.log ('mismatch top:' + tile.top + ' actual:' + el.offsetTop + ' id:' + tile.id); 88 | tile.$el.css('top', tile.top); 89 | } 90 | 91 | if (onComplete) { 92 | onComplete(); 93 | } 94 | }; 95 | 96 | 97 | // make css changes with animation when requested 98 | if (animate && changed) { 99 | 100 | this.$el.animate(cssChanges, { 101 | duration: duration, 102 | easing: 'swing', 103 | complete: validateChangesAndComplete 104 | }); 105 | } 106 | else { 107 | 108 | if (changed) { 109 | this.$el.css(cssChanges); 110 | } 111 | 112 | setTimeout(validateChangesAndComplete, duration); 113 | } 114 | }; 115 | 116 | })(jQuery); 117 | -------------------------------------------------------------------------------- /src/UniformTemplates.js: -------------------------------------------------------------------------------- 1 | 2 | // template provider which returns simple templates with 1x1 tiles 3 | Tiles.UniformTemplates = { 4 | get: function(numCols, targetTiles) { 5 | var numRows = Math.ceil(targetTiles / numCols), 6 | rects = [], 7 | x, y; 8 | 9 | // create the rects for 1x1 tiles 10 | for (y = 0; y < numRows; y++) { 11 | for (x = 0; x < numCols; x++) { 12 | rects.push(new Tiles.Rectangle(x, y, 1, 1)); 13 | } 14 | } 15 | 16 | return new Tiles.Template(rects, numCols, numRows); 17 | } 18 | }; --------------------------------------------------------------------------------