├── .gitignore ├── Makefile ├── README.md ├── bower.json ├── component.json ├── demo.html ├── demo ├── index.jade └── index.js ├── dist ├── horizontal-grid-packing.css └── horizontal-grid-packing.js ├── lib ├── pack.css └── pack.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store* 32 | ehthumbs.db 33 | Icon? 34 | Thumbs.db 35 | 36 | # Node.js # 37 | ########### 38 | lib-cov 39 | *.seed 40 | *.log 41 | *.csv 42 | *.dat 43 | *.out 44 | *.pid 45 | *.gz 46 | 47 | pids 48 | logs 49 | results 50 | 51 | node_modules 52 | npm-debug.log 53 | 54 | # Git # 55 | ####### 56 | *.orig 57 | *.BASE.* 58 | *.BACKUP.* 59 | *.LOCAL.* 60 | *.REMOTE.* 61 | 62 | # Components # 63 | ############## 64 | 65 | /build 66 | /components -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: component dist 2 | 3 | lint: 4 | @jshint lib 5 | 6 | clean: 7 | @rm -rf components 8 | 9 | component: 10 | @component install --dev 11 | @component build --dev 12 | 13 | dist: 14 | @component build \ 15 | --standalone HorizontalGridPacking \ 16 | --out dist \ 17 | --name horizontal-grid-packing 18 | 19 | demo: npm component 20 | @node demo 21 | 22 | npm: 23 | @npm i 24 | 25 | .PHONY: build lint clean dist component demo npm -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Horizontal grid packing 2 | 3 | Packs grids into rows so that each row expands to the full container width. 4 | Row heights are dynamically adjusted. 5 | Similar to [Masonry](https://github.com/desandro/masonry), but the space taken by all the grids will always be a rectangle without any jagged edges. 6 | By being horizontal, users view the grid linearly (scan left to right, top to bottom) kind of like a comic book. 7 | 8 | Demo: http://jonathanong.github.io/horizontal-grid-packing/ 9 | 10 | See: 11 | 12 | - https://github.com/math-utils/linear-partition 13 | - https://news.ycombinator.com/item?id=6198400 14 | - http://www.crispymtn.com/stories/the-algorithm-for-a-perfectly-balanced-photo-gallery 15 | 16 | ## Install 17 | 18 | If you use [component](https://github.com/component/component), the dependencies are handled for you. 19 | If you use [bower](https://github.com/bower/bower), the dependencies are packaged together. 20 | 21 | ```bash 22 | component install jonathanong/horizontal-grid-packing 23 | bower install horizontal-grid-packing 24 | ``` 25 | 26 | ## API 27 | 28 | ### Layout 29 | 30 | The HTML must strictly be a single container whose children are strictly grid elements. 31 | 32 | ```html 33 |
34 | 35 |
36 | 37 | 38 |
39 | ``` 40 | 41 | This library assumes you know the aspect ratio of each grid element. 42 | Each element should either have a `data-aspect-ratio` attribute (width/height) or both `data-width` and `data-height` attributes. 43 | If you do not know these attributes, use a library such as [imagesloaded](https://github.com/desandro/imagesloaded) to calculate the dimensions before using this library. 44 | This library will not attempt to figure out the dimensions of each grid element. 45 | 46 | ### Pack(container [, options]) 47 | 48 | Returns a new instance of `Pack`. 49 | 50 | ```js 51 | var pack = new HorizontalGridPacking(container, options) 52 | ``` 53 | 54 | `new` is optional. 55 | `container` is the element that contains all the grids. 56 | The `options` are: 57 | 58 | - `height` - Target row height in pixels. 59 | `120` by default. 60 | - `padding` - Padding between each grid in pixels. 61 | `0` by default. 62 | 63 | Each of these options can be changed as an attribute of `pack`: 64 | 65 | ```js 66 | // Change the target height 67 | pack.height = Math.round(window.outerHeight / 5) 68 | // Change the padding 69 | pack.padding = 5 70 | // Recalculate the grid 71 | pack.reload() 72 | ``` 73 | 74 | Other options you may be interested are: 75 | 76 | - `width` - the width of the grid in pixels. 77 | You should change this when `container`'s width changes. 78 | 79 | ### pack.reload() 80 | 81 | Recalculates the grid. 82 | Specifically, you would want to use this when `container` is resized: 83 | 84 | ```js 85 | window.addEventListener('resize', function () { 86 | pack.width = container.clientWidth 87 | pack.height = Math.round(window.outerHeight / Math.PI) 88 | pack.reload() 89 | }) 90 | ``` 91 | 92 | ### pack.destroy() 93 | 94 | Destroys the grid and returns `container` to its original state. 95 | 96 | ### pack.create() 97 | 98 | Creates the grid. 99 | This is called by default. 100 | You should only use this if the grid has been previously destroyed. 101 | 102 | ### pack.append(DocumentFragment || elements) 103 | 104 | Append elements to the current grid. 105 | Could either be a `DocumentFragment` instance whose child nodes are `elements`, 106 | or an array-like variable of grid `elements`. 107 | Appends using `DocumentFragment`, so don't worry about reflows. 108 | 109 | ## Compatibility 110 | 111 | IE9+ (Pull requests welcome for IE8) 112 | 113 | ## License 114 | 115 | The MIT License (MIT) 116 | 117 | Copyright (c) 2013 Jonathan Ong me@jongleberry.com 118 | 119 | Permission is hereby granted, free of charge, to any person obtaining a copy 120 | of this software and associated documentation files (the "Software"), to deal 121 | in the Software without restriction, including without limitation the rights 122 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 123 | copies of the Software, and to permit persons to whom the Software is 124 | furnished to do so, subject to the following conditions: 125 | 126 | The above copyright notice and this permission notice shall be included in 127 | all copies or substantial portions of the Software. 128 | 129 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 130 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 131 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 132 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 133 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 134 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 135 | THE SOFTWARE. 136 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "horizontal-grid-packing", 3 | "description": "Horizontal grid packing", 4 | "version": "0.1.4", 5 | "main": [ 6 | "dist/horizontal-grid-packing.js", 7 | "dist/horizontal-grid-packing.css" 8 | ] 9 | } -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "horizontal-grid-packing", 3 | "description": "Horizontal grid packing", 4 | "version": "0.1.4", 5 | "scripts": [ 6 | "lib/pack.js" 7 | ], 8 | "styles": [ 9 | "lib/pack.css" 10 | ], 11 | "main": "lib/pack.js", 12 | "dependencies": { 13 | "component/classes": "*", 14 | "the-swerve/linear-partitioning": "*" 15 | }, 16 | "development": { 17 | "component/raf": "*" 18 | }, 19 | "license": "MIT", 20 | "repo": "jonathanong/horizontal-grid-packing" 21 | } -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | Horizontal Grid Packing Example
-------------------------------------------------------------------------------- /demo/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(name='viewport', content='width=device-width, initial-scale=1') 5 | title Horizontal Grid Packing Example 6 | style. 7 | html, 8 | body { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | div { 14 | margin: 5px; 15 | } 16 | 17 | img { 18 | transition: all 0.3s linear; 19 | } 20 | link(rel="stylesheet", href="./build/build.css") 21 | script(src='./build/build.js') 22 | body 23 | .horizontal-grid-packing 24 | for image in images 25 | img( 26 | src = 'http://i.imgur.com/' + image.hash + 'l' + image.ext, 27 | data-width = image.width, 28 | data-height = image.height 29 | ) 30 | 31 | script. 32 | var Pack = require('horizontal-grid-packing') 33 | var container = document.querySelector('.horizontal-grid-packing') 34 | var imagesHTML = container.innerHTML 35 | var pack = Pack(container, { 36 | height: height(), 37 | padding: 5 38 | }) 39 | 40 | var raf = require('component-raf') 41 | var pending = false 42 | window.addEventListener('resize', function () { 43 | if (pending) return 44 | pending = true 45 | raf(reload) 46 | }) 47 | 48 | function reload() { 49 | pack.width = container.clientWidth 50 | pack.height = height() 51 | pack.reload() 52 | pending = false 53 | } 54 | 55 | function append() { 56 | var frag = document.createElement('div') 57 | frag.innerHTML = imagesHTML 58 | pack.append(frag.children) 59 | } 60 | 61 | function height() { 62 | return Math.max(Math.round(window.outerHeight / Math.PI), 120) 63 | } -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var request = require('request') 4 | var jade = require('jade') 5 | 6 | var string = fs.readFileSync(path.join(__dirname, 'index.jade'), 'utf8') 7 | var template = jade.compile(string) 8 | 9 | request('http://imgur.com/r/emmawatson.json', function (err, res, body) { 10 | if (err) 11 | throw err 12 | 13 | fs.writeFileSync( 14 | path.join(__dirname, '..', 'demo.html'), 15 | template({ 16 | images: JSON.parse(body).data 17 | }) 18 | ) 19 | 20 | process.exit() 21 | }) -------------------------------------------------------------------------------- /dist/horizontal-grid-packing.css: -------------------------------------------------------------------------------- 1 | .hgp { 2 | position: relative; 3 | visibility: hidden; 4 | overflow: hidden; 5 | } -------------------------------------------------------------------------------- /dist/horizontal-grid-packing.js: -------------------------------------------------------------------------------- 1 | ;(function(){ 2 | 3 | /** 4 | * Require the given path. 5 | * 6 | * @param {String} path 7 | * @return {Object} exports 8 | * @api public 9 | */ 10 | 11 | function require(path, parent, orig) { 12 | var resolved = require.resolve(path); 13 | 14 | // lookup failed 15 | if (null == resolved) { 16 | orig = orig || path; 17 | parent = parent || 'root'; 18 | var err = new Error('Failed to require "' + orig + '" from "' + parent + '"'); 19 | err.path = orig; 20 | err.parent = parent; 21 | err.require = true; 22 | throw err; 23 | } 24 | 25 | var module = require.modules[resolved]; 26 | 27 | // perform real require() 28 | // by invoking the module's 29 | // registered function 30 | if (!module.exports) { 31 | module.exports = {}; 32 | module.client = module.component = true; 33 | module.call(this, module.exports, require.relative(resolved), module); 34 | } 35 | 36 | return module.exports; 37 | } 38 | 39 | /** 40 | * Registered modules. 41 | */ 42 | 43 | require.modules = {}; 44 | 45 | /** 46 | * Registered aliases. 47 | */ 48 | 49 | require.aliases = {}; 50 | 51 | /** 52 | * Resolve `path`. 53 | * 54 | * Lookup: 55 | * 56 | * - PATH/index.js 57 | * - PATH.js 58 | * - PATH 59 | * 60 | * @param {String} path 61 | * @return {String} path or null 62 | * @api private 63 | */ 64 | 65 | require.resolve = function(path) { 66 | if (path.charAt(0) === '/') path = path.slice(1); 67 | 68 | var paths = [ 69 | path, 70 | path + '.js', 71 | path + '.json', 72 | path + '/index.js', 73 | path + '/index.json' 74 | ]; 75 | 76 | for (var i = 0; i < paths.length; i++) { 77 | var path = paths[i]; 78 | if (require.modules.hasOwnProperty(path)) return path; 79 | if (require.aliases.hasOwnProperty(path)) return require.aliases[path]; 80 | } 81 | }; 82 | 83 | /** 84 | * Normalize `path` relative to the current path. 85 | * 86 | * @param {String} curr 87 | * @param {String} path 88 | * @return {String} 89 | * @api private 90 | */ 91 | 92 | require.normalize = function(curr, path) { 93 | var segs = []; 94 | 95 | if ('.' != path.charAt(0)) return path; 96 | 97 | curr = curr.split('/'); 98 | path = path.split('/'); 99 | 100 | for (var i = 0; i < path.length; ++i) { 101 | if ('..' == path[i]) { 102 | curr.pop(); 103 | } else if ('.' != path[i] && '' != path[i]) { 104 | segs.push(path[i]); 105 | } 106 | } 107 | 108 | return curr.concat(segs).join('/'); 109 | }; 110 | 111 | /** 112 | * Register module at `path` with callback `definition`. 113 | * 114 | * @param {String} path 115 | * @param {Function} definition 116 | * @api private 117 | */ 118 | 119 | require.register = function(path, definition) { 120 | require.modules[path] = definition; 121 | }; 122 | 123 | /** 124 | * Alias a module definition. 125 | * 126 | * @param {String} from 127 | * @param {String} to 128 | * @api private 129 | */ 130 | 131 | require.alias = function(from, to) { 132 | if (!require.modules.hasOwnProperty(from)) { 133 | throw new Error('Failed to alias "' + from + '", it does not exist'); 134 | } 135 | require.aliases[to] = from; 136 | }; 137 | 138 | /** 139 | * Return a require function relative to the `parent` path. 140 | * 141 | * @param {String} parent 142 | * @return {Function} 143 | * @api private 144 | */ 145 | 146 | require.relative = function(parent) { 147 | var p = require.normalize(parent, '..'); 148 | 149 | /** 150 | * lastIndexOf helper. 151 | */ 152 | 153 | function lastIndexOf(arr, obj) { 154 | var i = arr.length; 155 | while (i--) { 156 | if (arr[i] === obj) return i; 157 | } 158 | return -1; 159 | } 160 | 161 | /** 162 | * The relative require() itself. 163 | */ 164 | 165 | function localRequire(path) { 166 | var resolved = localRequire.resolve(path); 167 | return require(resolved, parent, path); 168 | } 169 | 170 | /** 171 | * Resolve relative to the parent. 172 | */ 173 | 174 | localRequire.resolve = function(path) { 175 | var c = path.charAt(0); 176 | if ('/' == c) return path.slice(1); 177 | if ('.' == c) return require.normalize(p, path); 178 | 179 | // resolve deps by returning 180 | // the dep in the nearest "deps" 181 | // directory 182 | var segs = parent.split('/'); 183 | var i = lastIndexOf(segs, 'deps') + 1; 184 | if (!i) i = 0; 185 | path = segs.slice(0, i + 1).join('/') + '/deps/' + path; 186 | return path; 187 | }; 188 | 189 | /** 190 | * Check if module is defined at `path`. 191 | */ 192 | 193 | localRequire.exists = function(path) { 194 | return require.modules.hasOwnProperty(localRequire.resolve(path)); 195 | }; 196 | 197 | return localRequire; 198 | }; 199 | require.register("component-indexof/index.js", function(exports, require, module){ 200 | module.exports = function(arr, obj){ 201 | if (arr.indexOf) return arr.indexOf(obj); 202 | for (var i = 0; i < arr.length; ++i) { 203 | if (arr[i] === obj) return i; 204 | } 205 | return -1; 206 | }; 207 | }); 208 | require.register("component-classes/index.js", function(exports, require, module){ 209 | /** 210 | * Module dependencies. 211 | */ 212 | 213 | var index = require('indexof'); 214 | 215 | /** 216 | * Whitespace regexp. 217 | */ 218 | 219 | var re = /\s+/; 220 | 221 | /** 222 | * toString reference. 223 | */ 224 | 225 | var toString = Object.prototype.toString; 226 | 227 | /** 228 | * Wrap `el` in a `ClassList`. 229 | * 230 | * @param {Element} el 231 | * @return {ClassList} 232 | * @api public 233 | */ 234 | 235 | module.exports = function(el){ 236 | return new ClassList(el); 237 | }; 238 | 239 | /** 240 | * Initialize a new ClassList for `el`. 241 | * 242 | * @param {Element} el 243 | * @api private 244 | */ 245 | 246 | function ClassList(el) { 247 | if (!el) throw new Error('A DOM element reference is required'); 248 | this.el = el; 249 | this.list = el.classList; 250 | } 251 | 252 | /** 253 | * Add class `name` if not already present. 254 | * 255 | * @param {String} name 256 | * @return {ClassList} 257 | * @api public 258 | */ 259 | 260 | ClassList.prototype.add = function(name){ 261 | // classList 262 | if (this.list) { 263 | this.list.add(name); 264 | return this; 265 | } 266 | 267 | // fallback 268 | var arr = this.array(); 269 | var i = index(arr, name); 270 | if (!~i) arr.push(name); 271 | this.el.className = arr.join(' '); 272 | return this; 273 | }; 274 | 275 | /** 276 | * Remove class `name` when present, or 277 | * pass a regular expression to remove 278 | * any which match. 279 | * 280 | * @param {String|RegExp} name 281 | * @return {ClassList} 282 | * @api public 283 | */ 284 | 285 | ClassList.prototype.remove = function(name){ 286 | if ('[object RegExp]' == toString.call(name)) { 287 | return this.removeMatching(name); 288 | } 289 | 290 | // classList 291 | if (this.list) { 292 | this.list.remove(name); 293 | return this; 294 | } 295 | 296 | // fallback 297 | var arr = this.array(); 298 | var i = index(arr, name); 299 | if (~i) arr.splice(i, 1); 300 | this.el.className = arr.join(' '); 301 | return this; 302 | }; 303 | 304 | /** 305 | * Remove all classes matching `re`. 306 | * 307 | * @param {RegExp} re 308 | * @return {ClassList} 309 | * @api private 310 | */ 311 | 312 | ClassList.prototype.removeMatching = function(re){ 313 | var arr = this.array(); 314 | for (var i = 0; i < arr.length; i++) { 315 | if (re.test(arr[i])) { 316 | this.remove(arr[i]); 317 | } 318 | } 319 | return this; 320 | }; 321 | 322 | /** 323 | * Toggle class `name`, can force state via `force`. 324 | * 325 | * For browsers that support classList, but do not support `force` yet, 326 | * the mistake will be detected and corrected. 327 | * 328 | * @param {String} name 329 | * @param {Boolean} force 330 | * @return {ClassList} 331 | * @api public 332 | */ 333 | 334 | ClassList.prototype.toggle = function(name, force){ 335 | // classList 336 | if (this.list) { 337 | if ("undefined" !== typeof force) { 338 | if (force !== this.list.toggle(name, force)) { 339 | this.list.toggle(name); // toggle again to correct 340 | } 341 | } else { 342 | this.list.toggle(name); 343 | } 344 | return this; 345 | } 346 | 347 | // fallback 348 | if ("undefined" !== typeof force) { 349 | if (!force) { 350 | this.remove(name); 351 | } else { 352 | this.add(name); 353 | } 354 | } else { 355 | if (this.has(name)) { 356 | this.remove(name); 357 | } else { 358 | this.add(name); 359 | } 360 | } 361 | 362 | return this; 363 | }; 364 | 365 | /** 366 | * Return an array of classes. 367 | * 368 | * @return {Array} 369 | * @api public 370 | */ 371 | 372 | ClassList.prototype.array = function(){ 373 | var str = this.el.className.replace(/^\s+|\s+$/g, ''); 374 | var arr = str.split(re); 375 | if ('' === arr[0]) arr.shift(); 376 | return arr; 377 | }; 378 | 379 | /** 380 | * Check if class `name` is present. 381 | * 382 | * @param {String} name 383 | * @return {ClassList} 384 | * @api public 385 | */ 386 | 387 | ClassList.prototype.has = 388 | ClassList.prototype.contains = function(name){ 389 | return this.list 390 | ? this.list.contains(name) 391 | : !! ~index(this.array(), name); 392 | }; 393 | 394 | }); 395 | require.register("component-is-document-fragment/index.js", function(exports, require, module){ 396 | var toString = Object.prototype.toString; 397 | 398 | module.exports = function isDocumentFragment(element) { 399 | return element 400 | && toString.call(element) === '[object DocumentFragment]' 401 | } 402 | }); 403 | require.register("the-swerve-linear-partitioning/linear-partitioning.js", function(exports, require, module){ 404 | // Explanation: http://www8.cs.umu.se/kurser/TDBAfl/VT06/algorithms/BOOK/BOOK2/NODE45.HTM 405 | 406 | // Partition seq into k buckets 407 | 408 | 409 | var partition = function (seq, k) { 410 | 411 | if (k === 0) return []; 412 | if (k === 1) return [seq]; 413 | 414 | if (k >= seq.length) { 415 | // return the lists of each single element in sequence, plus empty lists for any extra buckets. 416 | var repeated = []; 417 | for (var q = 0; q < k - seq.length; ++q) repeated.push([]); 418 | return seq.map(function(x) { return [x]; }).concat(repeated); 419 | } 420 | 421 | var sequence = seq.slice(0); 422 | var dividers = []; 423 | var sums = prefix_sums(sequence, k); 424 | var conds = boundary_conditions(sequence, k, sums); 425 | 426 | // evaluate main recurrence 427 | for(var i = 2; i <= sequence.length; ++i) { 428 | for(var j = 2; j <= k; ++j) { 429 | 430 | conds[i][j] = undefined; 431 | 432 | for(var x = 1; x < i; ++x) { 433 | 434 | var s = Math.max(conds[x][j-1], sums[i] - sums[x]); 435 | dividers[i] = dividers[i] || []; // Initialize a new row in the dividers matrix (unless it's already initialized). 436 | 437 | // Continue to find the cost of the largest range in the optimal partition. 438 | if (conds[i][j] === undefined || conds[i][j] > s) { 439 | conds[i][j] = s; 440 | dividers[i][j] = x; 441 | } 442 | 443 | } 444 | } 445 | } 446 | 447 | return(reconstruct_partition(sequence, dividers, k)); 448 | }; 449 | 450 | /* Work our way back up through the dividers, referencing each divider that we 451 | * saved given a value for k and a length of seq, using each divider to make 452 | * the partitions. */ 453 | var reconstruct_partition = function(seq, dividers, k) { 454 | var partitions = []; 455 | 456 | while (k > 1) { 457 | if (dividers[seq.length]) { 458 | var divider = dividers[seq.length][k]; 459 | var part = seq.splice(divider); 460 | partitions.unshift(part); 461 | } 462 | --k; 463 | } 464 | 465 | partitions.unshift(seq); 466 | 467 | return partitions; 468 | }; 469 | 470 | /* 471 | Given a list of numbers of length n, loop through it with index 'i' 472 | Make each element the sum of all the numbers from 0...i 473 | For example, given [1,2,3,4,5] 474 | The prefix sums are [1,3,6,10,15] 475 | */ 476 | var prefix_sums = function(seq) { 477 | 478 | var sums = [0]; 479 | 480 | for(var i = 1; i <= seq.length; ++i) { 481 | sums[i] = sums[i - 1] + seq[i - 1]; 482 | } 483 | 484 | return sums; 485 | }; 486 | 487 | /* This matrix holds the maximum sums over all the ranges given the length of 488 | * seq and the number of buckets (k) */ 489 | var boundary_conditions = function(seq, k, sums) { 490 | var conds = []; 491 | 492 | for(var i = 1; i <= seq.length; ++i) { 493 | conds[i] = []; 494 | conds[i][1] = sums[i]; 495 | } 496 | 497 | for(var j = 1; j <= k; ++j) { 498 | conds[1][j] = seq[0]; 499 | } 500 | 501 | return conds; 502 | }; 503 | 504 | module.exports = partition; 505 | 506 | }); 507 | require.register("horizontal-grid-packing/lib/pack.js", function(exports, require, module){ 508 | var isFragment = require('is-document-fragment') 509 | var part = require('linear-partitioning') 510 | var classes = require('classes') 511 | 512 | module.exports = Pack 513 | 514 | function Pack(container, options) { 515 | if (!(this instanceof Pack)) 516 | return new Pack(container, options) 517 | 518 | options = options || {} 519 | 520 | this.container = container 521 | this.isFragment = isFragment(container) 522 | this.classes = !this.isFragment && classes(container) 523 | this.images = slice(container.childNodes).filter(isElement) 524 | this.top = options.top || 0 525 | this.width = options.width || container.clientWidth 526 | this.height = options.height || 120 527 | this.padding = options.padding || 0 528 | 529 | this.create() 530 | } 531 | 532 | Pack.prototype.append = function (images) { 533 | var fragment 534 | 535 | if (isFragment(images)) { 536 | fragment = images 537 | images = slice(fragment.childNodes) 538 | } else { 539 | fragment = document.createDocumentFragment() 540 | images = slice(images) 541 | images.forEach(function (image) { 542 | if (image.parentNode) 543 | image.parentNode.removeChild(image) 544 | 545 | fragment.appendChild(image) 546 | }) 547 | } 548 | 549 | var subpack = new Pack(fragment, { 550 | top: this.totalheight + this.padding, 551 | width: this.width, 552 | height: this.height, 553 | padding: this.padding 554 | }) 555 | 556 | this.totalheight = subpack.totalheight 557 | this.images = this.images.concat(images.filter(isElement)) 558 | this.mirror = this.mirror.concat(subpack.mirror) 559 | 560 | var container = this.container 561 | container.appendChild(fragment) 562 | container.style.height = this.totalheight + 'px' 563 | } 564 | 565 | Pack.prototype.destroy = function () { 566 | this.images.forEach(unsetStyle) 567 | this.mirror = null 568 | 569 | if (this.isFragment) 570 | return 571 | 572 | var style = this.container.style 573 | style.visibility = 574 | style.height = '' 575 | this.classes.remove('hgp') 576 | } 577 | 578 | Pack.prototype.reload = function () { 579 | this.container.style.visibility = 'hidden' 580 | this.create() 581 | } 582 | 583 | Pack.prototype.create = function () { 584 | var index = 0 585 | var ratios = this.calculateAspectRatios() 586 | var container = this.container 587 | var mirrors = this.mirror = [] 588 | 589 | part(ratios, Math.max(Math.min( 590 | Math.floor(ratios.reduce(add, 0) * this.height / this.width), 591 | ratios.length 592 | ), 1)).forEach(function (x) { 593 | index += this.createRow(index, x.length) 594 | }, this) 595 | 596 | var lastmirror = mirrors[mirrors.length - 1] 597 | this.totalheight = lastmirror.top + lastmirror.height 598 | this.images.forEach(positionAbsolute) 599 | 600 | if (this.isFragment) 601 | return 602 | 603 | this.classes.add('hgp') 604 | 605 | var style = container.style 606 | style.height = this.totalheight + 'px' 607 | style.visibility = 'visible' 608 | } 609 | 610 | Pack.prototype.createRow = function (index, count) { 611 | var mirror = this.mirror 612 | var padding = this.padding 613 | var images = this.images.slice(index, index + count) 614 | var height = this.calculateRowHeight(images) 615 | 616 | var row = { 617 | index: index, 618 | count: count, 619 | height: height 620 | } 621 | 622 | var imagemirrors = row.images = [] 623 | var lastrow = mirror[mirror.length - 1] 624 | var top = row.top = lastrow 625 | ? (lastrow.top + lastrow.height + padding) 626 | : (this.top || 0) 627 | 628 | images.forEach(function (image, i) { 629 | var lastimage = i && imagemirrors[i - 1] 630 | var left = lastimage 631 | ? lastimage.right + padding 632 | : 0 633 | var width = Math.round(height * image.aspectRatio) 634 | 635 | var style = image.style 636 | style.left = left + 'px' 637 | style.top = top + 'px' 638 | style.height = height + 'px' 639 | style.width = width + 'px' 640 | 641 | imagemirrors.push({ 642 | left: left, 643 | width: width, 644 | right: left + width, 645 | image: image 646 | }) 647 | }) 648 | 649 | mirror.push(row) 650 | 651 | return count 652 | } 653 | 654 | Pack.prototype.calculateRowHeight = function (images) { 655 | return Math.ceil( 656 | (this.width - (this.padding * (images.length - 1))) / 657 | images.map(getAspectRatio).reduce(add, 0) 658 | ) 659 | } 660 | 661 | Pack.prototype.calculateAspectRatios = function () { 662 | return this.images.map(calculateAspectRatio) 663 | } 664 | 665 | function positionAbsolute(image) { 666 | image.style.position = 'absolute' 667 | } 668 | 669 | function unsetStyle(image) { 670 | var style = image.style 671 | style.width = 672 | style.height = 673 | style.top = 674 | style.left = 675 | style.position = '' 676 | } 677 | 678 | function calculateAspectRatio(image) { 679 | return image.aspectRatio || (image.aspectRatio = 680 | parseFloat(image.getAttribute('data-aspect-ratio')) || 681 | parseInt(image.getAttribute('data-width'), 10) / 682 | parseInt(image.getAttribute('data-height'), 10) 683 | ) 684 | } 685 | 686 | function getAspectRatio(x) { 687 | return x.aspectRatio 688 | } 689 | 690 | function slice(x) { 691 | return [].slice.call(x, 0) 692 | } 693 | 694 | function add(a, b) { 695 | return a + b 696 | } 697 | 698 | function isElement(el) { 699 | return el.nodeType && el.nodeType === Node.ELEMENT_NODE 700 | } 701 | 702 | }); 703 | 704 | 705 | 706 | require.alias("component-classes/index.js", "horizontal-grid-packing/deps/classes/index.js"); 707 | require.alias("component-classes/index.js", "classes/index.js"); 708 | require.alias("component-indexof/index.js", "component-classes/deps/indexof/index.js"); 709 | 710 | require.alias("component-is-document-fragment/index.js", "horizontal-grid-packing/deps/is-document-fragment/index.js"); 711 | require.alias("component-is-document-fragment/index.js", "is-document-fragment/index.js"); 712 | 713 | require.alias("the-swerve-linear-partitioning/linear-partitioning.js", "horizontal-grid-packing/deps/linear-partitioning/linear-partitioning.js"); 714 | require.alias("the-swerve-linear-partitioning/linear-partitioning.js", "horizontal-grid-packing/deps/linear-partitioning/index.js"); 715 | require.alias("the-swerve-linear-partitioning/linear-partitioning.js", "linear-partitioning/index.js"); 716 | require.alias("the-swerve-linear-partitioning/linear-partitioning.js", "the-swerve-linear-partitioning/index.js"); 717 | require.alias("horizontal-grid-packing/lib/pack.js", "horizontal-grid-packing/index.js");if (typeof exports == "object") { 718 | module.exports = require("horizontal-grid-packing"); 719 | } else if (typeof define == "function" && define.amd) { 720 | define(function(){ return require("horizontal-grid-packing"); }); 721 | } else { 722 | this["HorizontalGridPacking"] = require("horizontal-grid-packing"); 723 | }})(); -------------------------------------------------------------------------------- /lib/pack.css: -------------------------------------------------------------------------------- 1 | .hgp { 2 | position: relative; 3 | visibility: hidden; 4 | overflow: hidden; 5 | } -------------------------------------------------------------------------------- /lib/pack.js: -------------------------------------------------------------------------------- 1 | /* jshint browser: true */ 2 | 3 | var part = require('linear-partitioning') 4 | var classes = require('classes') 5 | 6 | module.exports = Pack 7 | 8 | function Pack(container, options) { 9 | if (!(this instanceof Pack)) 10 | return new Pack(container, options) 11 | 12 | options = options || {} 13 | 14 | this.container = container 15 | this.isFragment = container.nodeType === 11 16 | this.classes = !this.isFragment && classes(container) 17 | this.images = slice(container.childNodes).filter(isElement) 18 | this.top = options.top || 0 19 | this.width = options.width || container.clientWidth 20 | this.height = options.height || 120 21 | this.padding = options.padding || 0 22 | 23 | this.create() 24 | } 25 | 26 | Pack.prototype.append = function (images) { 27 | var fragment 28 | 29 | if (images.nodeType === 11) { 30 | fragment = images 31 | images = slice(fragment.childNodes) 32 | } else { 33 | fragment = document.createDocumentFragment() 34 | images = slice(images) 35 | images.forEach(function (image) { 36 | if (image.parentNode) 37 | image.parentNode.removeChild(image) 38 | 39 | fragment.appendChild(image) 40 | }) 41 | } 42 | 43 | var subpack = new Pack(fragment, { 44 | top: this.totalheight + this.padding, 45 | width: this.width, 46 | height: this.height, 47 | padding: this.padding 48 | }) 49 | 50 | this.totalheight = subpack.totalheight 51 | this.images = this.images.concat(images.filter(isElement)) 52 | this.mirror = this.mirror.concat(subpack.mirror) 53 | 54 | var container = this.container 55 | container.appendChild(fragment) 56 | container.style.height = this.totalheight + 'px' 57 | } 58 | 59 | Pack.prototype.destroy = function () { 60 | this.images.forEach(unsetStyle) 61 | this.mirror = null 62 | 63 | if (this.isFragment) return 64 | 65 | var style = this.container.style 66 | style.visibility = 67 | style.height = '' 68 | this.classes.remove('hgp') 69 | } 70 | 71 | Pack.prototype.reload = function () { 72 | this.container.style.visibility = 'hidden' 73 | this.create() 74 | } 75 | 76 | Pack.prototype.create = function () { 77 | var index = 0 78 | var ratios = this.calculateAspectRatios() 79 | var container = this.container 80 | var mirrors = this.mirror = [] 81 | 82 | part(ratios, Math.max(Math.min( 83 | Math.floor(ratios.reduce(add, 0) * this.height / this.width), 84 | ratios.length 85 | ), 1)).forEach(function (x) { 86 | index += this.createRow(index, x.length) 87 | }, this) 88 | 89 | var lastmirror = mirrors[mirrors.length - 1] 90 | this.totalheight = lastmirror.top + lastmirror.height 91 | this.images.forEach(positionAbsolute) 92 | 93 | if (this.isFragment) return 94 | 95 | this.classes.add('hgp') 96 | 97 | var style = container.style 98 | style.height = this.totalheight + 'px' 99 | style.visibility = 'visible' 100 | } 101 | 102 | Pack.prototype.createRow = function (index, count) { 103 | var mirror = this.mirror 104 | var padding = this.padding 105 | var images = this.images.slice(index, index + count) 106 | var height = this.calculateRowHeight(images) 107 | 108 | var row = { 109 | index: index, 110 | count: count, 111 | height: height 112 | } 113 | 114 | var imagemirrors = row.images = [] 115 | var lastrow = mirror[mirror.length - 1] 116 | var top = row.top = lastrow 117 | ? (lastrow.top + lastrow.height + padding) 118 | : (this.top || 0) 119 | 120 | images.forEach(function (image, i) { 121 | var lastimage = i && imagemirrors[i - 1] 122 | var left = lastimage 123 | ? lastimage.right + padding 124 | : 0 125 | var width = Math.round(height * image.aspectRatio) 126 | 127 | var style = image.style 128 | style.left = left + 'px' 129 | style.top = top + 'px' 130 | style.height = height + 'px' 131 | style.width = width + 'px' 132 | 133 | imagemirrors.push({ 134 | left: left, 135 | width: width, 136 | right: left + width, 137 | image: image 138 | }) 139 | }) 140 | 141 | mirror.push(row) 142 | 143 | return count 144 | } 145 | 146 | Pack.prototype.calculateRowHeight = function (images) { 147 | return Math.ceil( 148 | (this.width - (this.padding * (images.length - 1))) / 149 | images.map(getAspectRatio).reduce(add, 0) 150 | ) 151 | } 152 | 153 | Pack.prototype.calculateAspectRatios = function () { 154 | return this.images.map(calculateAspectRatio) 155 | } 156 | 157 | function positionAbsolute(image) { 158 | image.style.position = 'absolute' 159 | } 160 | 161 | function unsetStyle(image) { 162 | var style = image.style 163 | style.width = 164 | style.height = 165 | style.top = 166 | style.left = 167 | style.position = '' 168 | } 169 | 170 | function calculateAspectRatio(image) { 171 | return image.aspectRatio || (image.aspectRatio = 172 | parseFloat(image.getAttribute('data-aspect-ratio')) || 173 | parseInt(image.getAttribute('data-width'), 10) / 174 | parseInt(image.getAttribute('data-height'), 10) 175 | ) 176 | } 177 | 178 | function getAspectRatio(x) { 179 | return x.aspectRatio 180 | } 181 | 182 | function slice(x) { 183 | return [].slice.call(x, 0) 184 | } 185 | 186 | function add(a, b) { 187 | return a + b 188 | } 189 | 190 | function isElement(el) { 191 | return el.nodeType && el.nodeType === Node.ELEMENT_NODE 192 | } 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "horizontal-grid-packing", 3 | "description": "Horizontal grid packing", 4 | "version": "0.1.4", 5 | "author": { 6 | "name": "Jonathan Ong", 7 | "email": "me@jongleberry.com", 8 | "url": "http://jongleberry.com", 9 | "twitter": "https://twitter.com/jongleberry" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/jonathanong/horizontal-grid-packing.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/jonathanong/horizontal-grid-packing/issues", 17 | "email": "me@jongleberry.com" 18 | }, 19 | "license": "MIT", 20 | "devDependencies": { 21 | "jade": "*", 22 | "request": "*" 23 | } 24 | } --------------------------------------------------------------------------------