├── LICENSE.txt ├── balancedgallery.jquery.json ├── demo.html ├── demo_small_box.html ├── README.md ├── jquery.balanced-gallery.min.js └── jquery.balanced-gallery.js /LICENSE.txt: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2013 Ryan Epp 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /balancedgallery.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "balancedgallery", 3 | "version" : "1.0.2", 4 | "title" : "Balanced Gallery", 5 | "author" : { 6 | "name": "Ryan Epp", 7 | "url": "www.ryanepp.com" 8 | }, 9 | "licenses": [ 10 | { 11 | "type": "WTFPL", 12 | "url": "https://raw.github.com/repp/BalancedGallery/master/LICENSE.txt" 13 | } 14 | ], 15 | "dependencies" : { 16 | "jquery": ">=1.5" 17 | }, 18 | "description" : "Balanced Gallery is a jQuery plugin that evenly distributes photos across rows or columns, making the most of the space provided. Photos are scaled based on the size of the 'container' element by default, making Balanced Gallery a great choice for (responsive) websites." 19 | } -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Balanced Gallery Demo 5 | 6 | 7 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /demo_small_box.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Balanced Gallery Demo 5 | 6 | 7 | 15 | 20 | 21 | 22 | 23 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Balanced Gallery 2 | ========= 3 | Balanced Gallery is a jQuery plugin that evenly distributes photos across rows or columns, making the most of the space provided. 4 | Photos are scaled based on the size of the 'container' element by default, making Balanced Gallery a great choice for responsive websites. 5 | 6 | Demos 7 | ------- 8 | [Horizontal Gallery Demo](http://www.ryanepp.com/demos/balanced_gallery/horizontal) 9 | 10 | [Vertical Gallery Demo](http://www.ryanepp.com/demos/balanced_gallery/vertical) 11 | 12 | Quick Start 13 | ---------- 14 | Import JQuery and the Plugin: 15 | ``` html 16 | 17 | 18 | ``` 19 | 20 | Call the plugin on the element containing the gallery's images: 21 | ``` javascript 22 | // wait for the page to load 23 | $(window).load(function() { 24 | $('#myGallery').BalancedGallery({ /* options */ }); 25 | }); 26 | ``` 27 | 28 | Options 29 | ------- 30 | ``` javascript 31 | var defaults = { 32 | autoResize: true, // re-partition and resize the images when the window size changes 33 | background: null, // the css properties of the gallery's containing element 34 | idealHeight: null, // ideal row height, only used for horizontal galleries, defaults to half the containing element's height 35 | idealWidth: null, // ideal column width, only used for vertical galleries, defaults to 1/4 of the containing element's width 36 | maintainOrder: true, // keeps images in their original order, setting to 'false' can create a slightly better balance between rows 37 | orientation: 'horizontal', // 'horizontal' galleries are made of rows and scroll vertically; 'vertical' galleries are made of columns and scroll horizontally 38 | padding: 5, // pixels between images 39 | shuffleUnorderedPartitions: true, // unordered galleries tend to clump larger images at the begining, this solves that issue at a slight performance cost 40 | viewportHeight: null, // the assumed height of the gallery, defaults to the containing element's height 41 | viewportWidth: null // the assumed width of the gallery, defaults to the containing element's width 42 | }; 43 | ``` 44 | 45 | Browser Compatibility 46 | ------------ 47 | Tested and working in: 48 | * Chrome 49 | * Safari 50 | * FireFox 51 | * IE 9+ 52 | * Mobile Safari 53 | * Mobile Chrome 54 | 55 | 56 | Contributing 57 | ------------ 58 | If you'd like to contribute a feature or bugfix, that's awesome. Go for it. As of right now I don't have a specific set of guidelines for contributions but try to follow the plugin's current coding style. 59 | 60 | License 61 | --------- 62 | Copyright (c) 2013 [Ryan Epp](https://twitter.com/ryanEpp) Licensed under the WTFPL license. 63 | 64 | Acknowledgements 65 | ---------------- 66 | Inspired by [crispymtn](http://www.crispymtn.com/stories/the-algorithm-for-a-perfectly-balanced-photo-gallery). 67 | Linear partitioning algorithm ported from [Óscar López](http://stackoverflow.com/questions/7938809/dynamic-programming-linear-partitioning-please-help-grok/7942946#7942946) 68 | -------------------------------------------------------------------------------- /jquery.balanced-gallery.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e,n){"use strict";function i(e,n){I=this,this.element=e,this.elementChildren=t(e).children("*"),this.options=t.extend({},T,n),this.options.autoResize&&(this.unadulteratedHtml=t(this.element).html(),this.unadulteratedCSS=r(),this.unadulteratedOptions=t.extend({},this.options),o()),this.init(),this.createGallery()}function r(){var e=t(I.element);return{width:e[0].style.width,height:e[0].style.height,background:e.css("background"),paddingLeft:e.css("padding-left"),paddingTop:e.css("padding-top"),overflow:e.css("overflow"),fontSize:e.css("font-size")}}function o(){t(e).resize(function(){clearTimeout(G),G=setTimeout(function(){I.recreate()},500)})}function a(){var t,e,n;t=h(),0===t?I.fallbackToStandardSize():(e=f(),n=v(e,t),S(n))}function l(){var t,e,n;t=s(),0===t?I.fallbackToStandardSize():(e=g(),n=v(e,t),z(n),k(n))}function h(){return Math.round(d()/(I.options.viewportWidth-I.options.padding))}function s(){return Math.round(u()/(I.options.viewportHeight-I.options.padding))}function d(){var e=0;return I.elementChildren.each(function(){e+=p(t(this))}),e}function u(){var e=0;return I.elementChildren.each(function(){e+=c(t(this))}),e}function p(t){return W(t)*(I.options.idealHeight+I.options.padding)}function c(t){return 1/W(t)*(I.options.idealWidth+I.options.padding)}function f(){return I.elementChildren.map(function(){var e=parseInt(100*W(t(this)),R);return{element:this,weight:e}})}function g(){return I.elementChildren.map(function(){var e=parseInt(100*(1/W(t(this))),R);return{element:this,weight:e}})}function v(t,e){if(I.options.maintainOrder)return m(t,e);var n=y(t,e);return I.options.shuffleUnorderedPartitions&&(n=H(n)),C(n),n}function m(t,e){var n=t.length;if(0>=e)return[];if(e>=n)return t.map(function(t,e){return[[e]]});var i=w(t,e);n-=1,e-=2;for(var r=[];e>=0;){for(var o=[],a=i[n-1][e]+1;n+1>a;a++)o.push(t[a]);r=[o].concat(r),n=i[n-1][e],e-=1}for(var l=[],h=0;n+1>h;h++)l.push(t[h]);return[l].concat(r)}function w(t,e){for(var n=t.length,i=[],r=0;n>r;r++){for(var o=[],a=0;e>a;a++)o.push(0);i.push(o)}for(var l=[],h=0;n-1>h;h++){for(var s=[],d=0;e-1>d;d++)s.push(0);l.push(s)}for(var u=0;n>u;u++)i[u][0]=t[u].weight+(0!==u?i[u-1][0]:0);for(var p=0;e>p;p++)i[0][p]=t[0].weight;for(var c=function(t,e){return t[0]-e[0]},f=1;n>f;f++)for(var g=1;e>g;g++){for(var v=[],m=0;f>m;m++)v.push([Math.max(i[m][g-1],i[f][0]-i[m][0]),m]);var w=v.sort(c)[0];i[f][g]=w[0],l[f-1][g-1]=w[1]}return l}function y(t,e){for(var n=t.sort(function(t,e){return e.weight-t.weight}),i=new Array(e),r=0;e>r;r++)i[r]=[];for(var o=0;os&&(a=i[h],l=s)}a.push(n[o])}return i}function b(e){var n=0;return t.each(e,function(t,e){n+=e.weight}),n}function H(t){for(var e=0;e'),r.append(i[0])):i=t(I.element),I.container=i[0];for(var o=0;o';i.append(l);for(var h=0;h")}}}function W(t){var e=I.options.padding;return(t.width()+e)/(t.height()+e)}function x(t){for(var e,n,i=t.length;i--;)n=0|Math.random()*i,e=t[i],t[i]=t[n],t[n]=e;return t}var I,L="BalancedGallery",T={autoResize:!0,background:null,idealHeight:null,idealWidth:null,maintainOrder:!0,orientation:"horizontal",padding:5,shuffleUnorderedPartitions:!0,viewportHeight:null,viewportWidth:null},O="ALL_CHILDREN_LOADED",G=null,R=10;t.fn[L]=function(e){return this.each(function(){t.data(this,"plugin_"+L)||t.data(this,"plugin_"+L,new i(this,e))})},i.prototype.recreate=function(){t(this.element).on(O,function(){I.init(),I.createGallery()}),this.reset()},i.prototype.reset=function(){var e=this.elementChildren.length;t(this.element).html(this.unadulteratedHtml),t(this.element).css(this.unadulteratedCSS),this.options=t.extend({},this.unadulteratedOptions),this.elementChildren=t(this.element).children("*");var n=0;this.elementChildren.each(function(){t(this).load(function(){++n===e&&t(I.element).trigger(O)})})},i.prototype.init=function(){null===this.options.viewportWidth&&(this.options.viewportWidth=t(this.element).width()),null===this.options.viewportHeight&&(this.options.viewportHeight=t(this.element).height()),null===this.options.idealWidth&&(this.options.idealWidth=t(this.element).width()/4),null===this.options.idealHeight&&(this.options.idealHeight=t(this.element).height()/2),null!==this.options.background&&t(this.element).css({background:this.options.background}),this.elementChildren.css({display:"inline-block",padding:0,margin:0});var e=this.options.padding+"px";t(this.element).css({fontSize:0,paddingTop:e,paddingLeft:e})},i.prototype.createGallery=function(){var t=this.options.orientation.toLowerCase();if("horizontal"===t)a();else{if("vertical"!==t)throw"BalancedGallery: Invalid Orientation.";l()}},i.prototype.fallbackToStandardSize=function(){var e=this.options.idealHeight;this.elementChildren.each(function(){t(this).height(e),t(this).width(I.idealWidth(t(this)))})}}(jQuery,window,document); -------------------------------------------------------------------------------- /jquery.balanced-gallery.js: -------------------------------------------------------------------------------- 1 | ;(function ( $, window, document, undefined ) { 2 | "use strict"; 3 | 4 | var pluginName = 'BalancedGallery', 5 | balancedGallery, 6 | defaults = { 7 | autoResize: true, 8 | background: null, 9 | idealHeight: null, 10 | idealWidth: null, 11 | maintainOrder: true, 12 | orientation: 'horizontal', 13 | padding: 5, 14 | shuffleUnorderedPartitions: true, 15 | viewportHeight: null, 16 | viewportWidth: null 17 | }, 18 | ALL_CHILDREN_LOADED = 'ALL_CHILDREN_LOADED', 19 | resizeTimeout = null, 20 | RADIX = 10; 21 | 22 | //this wrapper prevents multiple instantiations of the plugin: 23 | $.fn[pluginName] = function ( options ) { 24 | return this.each(function () { 25 | if (!$.data(this, 'plugin_' + pluginName)) { 26 | $.data(this, 'plugin_' + pluginName, new BalancedGallery( this, options )); 27 | } 28 | }); 29 | }; 30 | 31 | function BalancedGallery( element, options ) { 32 | balancedGallery = this; // for contexts when 'this' doesn't refer to the BalancedGallery class. 33 | this.element = element; 34 | this.elementChildren = $(element).children('*'); 35 | this.options = $.extend( {}, defaults, options); // merge arg options and defaults 36 | 37 | if(this.options.autoResize) { 38 | this.unadulteratedHtml = $(this.element).html(); 39 | this.unadulteratedCSS = getUnadulteratedCss(); 40 | this.unadulteratedOptions = $.extend({}, this.options); 41 | setupAutoResize(); 42 | } 43 | 44 | this.init(); 45 | this.createGallery(); 46 | } 47 | 48 | function getUnadulteratedCss() { 49 | var $element = $(balancedGallery.element); 50 | //only the properties modified by the plugin 51 | return { 52 | width: $element[0].style.width, 53 | height: $element[0].style.height, 54 | background: $element.css('background'), 55 | paddingLeft: $element.css('padding-left'), 56 | paddingTop: $element.css('padding-top'), 57 | overflow: $element.css('overflow'), 58 | fontSize: $element.css('font-size') 59 | }; 60 | } 61 | 62 | function setupAutoResize() { 63 | $(window).resize(function() { 64 | clearTimeout(resizeTimeout); 65 | 66 | resizeTimeout = setTimeout(function() { 67 | balancedGallery.recreate(); 68 | }, 500); 69 | }); 70 | } 71 | 72 | BalancedGallery.prototype.recreate = function () { 73 | $(this.element).on(ALL_CHILDREN_LOADED, function() { 74 | balancedGallery.init(); 75 | balancedGallery.createGallery(); 76 | }); 77 | this.reset(); 78 | }; 79 | 80 | BalancedGallery.prototype.reset = function() { 81 | var childCount = this.elementChildren.length; 82 | 83 | $(this.element).html(this.unadulteratedHtml); 84 | $(this.element).css(this.unadulteratedCSS); 85 | this.options = $.extend({}, this.unadulteratedOptions); 86 | this.elementChildren = $(this.element).children('*'); 87 | 88 | var loadedChildren = 0; 89 | this.elementChildren.each(function() { 90 | $(this).load(function() { 91 | if(++loadedChildren === childCount) { 92 | $(balancedGallery.element).trigger(ALL_CHILDREN_LOADED); 93 | } 94 | }); 95 | }); 96 | }; 97 | 98 | BalancedGallery.prototype.init = function () { 99 | if(this.options.viewportWidth === null) { 100 | this.options.viewportWidth = $(this.element).width(); 101 | } 102 | 103 | if(this.options.viewportHeight === null) { 104 | this.options.viewportHeight = $(this.element).height(); 105 | } 106 | 107 | if(this.options.idealWidth === null) { 108 | this.options.idealWidth = $(this.element).width() / 4; 109 | } 110 | 111 | if(this.options.idealHeight === null) { 112 | this.options.idealHeight = $(this.element).height() / 2; 113 | } 114 | 115 | if(this.options.background !== null) { 116 | $(this.element).css({background: this.options.background}); 117 | } 118 | 119 | this.elementChildren.css({display: 'inline-block', padding: 0, margin: 0}); 120 | var padding = this.options.padding + 'px'; 121 | $(this.element).css({fontSize: 0, paddingTop: padding, paddingLeft: padding}); 122 | }; 123 | 124 | BalancedGallery.prototype.createGallery = function() { 125 | var orientation = (this.options.orientation).toLowerCase(); 126 | if(orientation === 'horizontal') { 127 | createHorizontalGallery(); 128 | } else if(orientation === 'vertical') { 129 | createVerticalGallery(); 130 | } else { 131 | throw("BalancedGallery: Invalid Orientation."); 132 | } 133 | }; 134 | 135 | function createHorizontalGallery() { 136 | var rows, weights, partitions; 137 | rows = getRows(); 138 | if(rows === 0) { 139 | balancedGallery.fallbackToStandardSize(); 140 | } else { 141 | weights = getWidthWeights(); 142 | partitions = getPartitions(weights, rows); 143 | resizeHorizontalElements(partitions); 144 | } 145 | } 146 | 147 | function createVerticalGallery() { 148 | var cols, weights, partitions; 149 | cols = getColumns(); 150 | if(cols === 0) { 151 | balancedGallery.fallbackToStandardSize(); 152 | } else { 153 | weights = getHeightWeights(); 154 | partitions = getPartitions(weights, cols); 155 | orientElementsVertically(partitions); 156 | resizeVerticalElements(partitions); 157 | } 158 | } 159 | 160 | function getRows () { 161 | return Math.round( collectiveIdealWidth() / (balancedGallery.options.viewportWidth - balancedGallery.options.padding) ); 162 | } 163 | 164 | function getColumns() { 165 | return Math.round( collectiveIdealHeight() / (balancedGallery.options.viewportHeight - balancedGallery.options.padding) ); 166 | } 167 | 168 | function collectiveIdealWidth() { 169 | var sum = 0; 170 | balancedGallery.elementChildren.each(function () { 171 | sum += idealWidth($(this)); 172 | }); 173 | return sum; 174 | } 175 | 176 | function collectiveIdealHeight() { 177 | var sum = 0; 178 | balancedGallery.elementChildren.each(function () { 179 | sum += idealHeight($(this)); 180 | }); 181 | return sum; 182 | } 183 | 184 | function idealWidth($image) { 185 | return aspectRatio($image) * (balancedGallery.options.idealHeight + balancedGallery.options.padding); 186 | } 187 | 188 | function idealHeight($image) { 189 | return (1/aspectRatio($image)) * (balancedGallery.options.idealWidth + balancedGallery.options.padding); 190 | } 191 | 192 | BalancedGallery.prototype.fallbackToStandardSize = function() { 193 | var idealHeight = this.options.idealHeight; 194 | this.elementChildren.each(function () { 195 | $(this).height( idealHeight ); 196 | $(this).width( balancedGallery.idealWidth($(this)) ); 197 | }); 198 | }; 199 | 200 | function getWidthWeights() { 201 | return balancedGallery.elementChildren.map(function () { 202 | var weight = parseInt( aspectRatio($(this)) * 100, RADIX ); 203 | return {element: this, weight: weight }; 204 | }); 205 | } 206 | 207 | function getHeightWeights() { 208 | return balancedGallery.elementChildren.map(function () { 209 | var weight = parseInt( (1/aspectRatio($(this))) * 100, RADIX ); 210 | return {element: this, weight: weight }; 211 | }); 212 | } 213 | 214 | function getPartitions(weights, sections) { 215 | if(balancedGallery.options.maintainOrder) { 216 | return getOrderedPartition(weights, sections); 217 | } else { 218 | var partitions = getUnorderedPartition(weights, sections); 219 | if(balancedGallery.options.shuffleUnorderedPartitions) { 220 | partitions = shufflePartitions(partitions); 221 | } 222 | reorderElements(partitions); 223 | return partitions; 224 | } 225 | } 226 | 227 | function getOrderedPartition(weights, sections) { 228 | var elementCount = weights.length; 229 | 230 | if(sections <= 0) { 231 | return []; 232 | } 233 | 234 | if(sections >= elementCount) { 235 | return weights.map(function(key, value) { return [([value])]; }); 236 | } 237 | 238 | var solution = createSolutionTable(weights, sections); 239 | elementCount -= 1; 240 | sections -= 2; 241 | var partitions = []; 242 | 243 | while(sections >= 0) { 244 | var results = []; 245 | for(var f = (solution[elementCount-1][sections]+1); f < elementCount+1; f++){ 246 | results.push(weights[f]); 247 | } 248 | partitions = [results].concat(partitions); 249 | 250 | elementCount = solution[elementCount-1][sections]; 251 | sections -= 1; 252 | } 253 | 254 | var results2 = []; 255 | for(var r = 0; r < elementCount+1; r++) { 256 | results2.push(weights[r]); 257 | } 258 | return [results2].concat(partitions); 259 | } 260 | 261 | // Used as part of the ordered partition function: 262 | function createSolutionTable(weights, sections) { 263 | var elementCount = weights.length; 264 | 265 | var table = []; 266 | for (var i = 0; i < elementCount; i++) { 267 | var res = []; 268 | for (var j = 0; j < sections; j++) { 269 | res.push(0); 270 | } 271 | table.push(res); 272 | } 273 | 274 | var solution = []; 275 | for (var k = 0; k < elementCount-1; k++) { 276 | var res2 = []; 277 | for (var l = 0; l < sections-1; l++) { 278 | res2.push(0); 279 | } 280 | solution.push(res2); 281 | } 282 | 283 | for(var m = 0; m < elementCount; m++) { 284 | table[m][0] = weights[m].weight + (m !== 0 ? table[m-1][0] : 0); 285 | } 286 | for(var n = 0; n < sections; n++) { 287 | table[0][n] = weights[0].weight; 288 | } 289 | 290 | var subArraySort = function(a, b){ return a[0]-b[0]; }; 291 | for(var p = 1; p < elementCount; p++) { 292 | for(var q = 1; q < sections; q++) { 293 | var results = []; 294 | for (var r = 0; r < p; r++) { 295 | results.push([ Math.max( table[r][q - 1], table[p][0]-table[r][0] ), r ]); 296 | } 297 | var arr = results.sort(subArraySort)[0]; 298 | table[p][q] = arr[0]; 299 | solution[p-1][q-1] = arr[1]; 300 | } 301 | } 302 | 303 | return solution; 304 | } 305 | 306 | function getUnorderedPartition(weights, sections) { 307 | var sortedWeights = weights.sort(function(a,b){ return b.weight - a.weight; }); 308 | 309 | var partitions = new Array(sections); 310 | for (var i=0; i '); 426 | $element.append($container[0]); 427 | } else { 428 | $container = $(balancedGallery.element); 429 | } 430 | balancedGallery.container = $container[0]; 431 | 432 | 433 | for(var i = 0; i < partitions.length; i++) { 434 | var colName = 'balanced-gallery-col'+i; 435 | var column = ''; 436 | $container.append(column); 437 | for(var j = 0; j < partitions[i].length; j++) { 438 | var child = partitions[i][j].element; 439 | var $col = $($container.find("div#"+colName)); 440 | $col.append(child).append('
'); 441 | } 442 | } 443 | 444 | } 445 | 446 | function aspectRatio($image) { 447 | var padding = balancedGallery.options.padding; 448 | return ($image.width()+padding) / ($image.height()+padding); 449 | } 450 | 451 | function shuffleArray(array) { 452 | var counter = array.length, temp, index; 453 | while (counter--) { 454 | index = (Math.random() * counter) | 0; //not a typo 455 | temp = array[counter]; 456 | array[counter] = array[index]; 457 | array[index] = temp; 458 | } 459 | return array; 460 | } 461 | 462 | })( jQuery, window, document ); --------------------------------------------------------------------------------