├── lib ├── .tern-port ├── BSTLinearPartition.js └── BreakpointPartition.js ├── bower.json ├── .gitignore ├── jqueryPlugin.js ├── package.json ├── LICENSE ├── index.js ├── dist ├── perfectLayout.min.js ├── jquery.perfectLayout.min.js ├── perfectLayout.js └── jquery.perfectLayout.js └── README.md /lib/.tern-port: -------------------------------------------------------------------------------- 1 | 34063 -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perfect-layout", 3 | "description": "Image layout generator based on linear partitioning", 4 | "main": "dist/perfectLayout.min.js", 5 | "version": "1.0.0", 6 | "authors": [ 7 | "Andrea Moretti " 8 | ], 9 | "moduleType": [ 10 | "es6", 11 | "globals", 12 | "node" 13 | ], 14 | "keywords": [ 15 | "gallery", 16 | "images", 17 | "layout", 18 | "linear", 19 | "partition" 20 | ], 21 | "license": "MIT", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /jqueryPlugin.js: -------------------------------------------------------------------------------- 1 | import perfectLayout from '.'; 2 | 3 | $.fn.perfectLayout = function(photos) { 4 | const node = this; 5 | const scrollBarSize = $('html').hasClass('touch') ? 0 : 15; 6 | const perfectRows = perfectLayout(photos, window.innerWidth - scrollBarSize, $(window).height()); 7 | node.empty(); 8 | 9 | perfectRows.forEach(function (row) { 10 | row.forEach(function (img) { 11 | var imgNode = $('
'); 12 | imgNode.css({ 13 | 'width': img.width + 'px', 14 | 'height': img.height + 'px', 15 | 'background': 'url(' + img.src + ')', 16 | 'background-size': 'cover' 17 | }); 18 | node.append(imgNode); 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perfect-layout", 3 | "version": "1.2.1", 4 | "description": "Image layout generator based on linear partitioning", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "./node_modules/.bin/browserify index.js -t babelify -t uglifyify --standalone perfectLayout -o dist/perfectLayout.min.js", 8 | "build-dev": "./node_modules/.bin/browserify index.js -t babelify --standalone perfectLayout -o dist/perfectLayout.js", 9 | "build-jquery": "npm run build && ./node_modules/.bin/browserify jqueryPlugin.js -t babelify -t uglifyify -o dist/jquery.perfectLayout.min.js", 10 | "build-jquery-dev": "npm run build-dev && ./node_modules/.bin/browserify jqueryPlugin.js -t babelify -o dist/jquery.perfectLayout.js", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "dist": "npm run build && npm run build-jquery && npm run build-dev && npm run build-jquery-dev" 13 | }, 14 | "author": "Andrea Moretti (@axyz) ", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "babelify": "^6.2.0", 18 | "browserify": "^11.0.1", 19 | "uglifyify": "^3.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrea Moretti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/BSTLinearPartition.js: -------------------------------------------------------------------------------- 1 | export default function BSTLinearPartition(seq, k) { 2 | if (seq.length <= 1) return [seq]; 3 | if (k >= seq.length) return seq.map(el => [el]); 4 | 5 | const limit = threshold(seq, k); 6 | let current = 0; 7 | 8 | return seq.reduce((res, el) => { 9 | if (sum(res[current]) + el > limit) current++; 10 | res[current].push(el); 11 | return res; 12 | // waiting for more elegant solutions (Array.fill) to work correctly 13 | }, new Array(k).join().split(',').map(() => [])); 14 | } 15 | 16 | // find the perfect limit that we should not pass when adding elements 17 | // to a single partition. 18 | function threshold(seq, k) { 19 | let bottom = max(seq); 20 | let top = sum(seq); 21 | 22 | while (bottom < top) { 23 | const mid = bottom + ( top - bottom) / 2; 24 | 25 | if (requiredElements(seq, mid) <= k) { 26 | top = mid; 27 | } else { 28 | bottom = mid + 1; 29 | } 30 | } 31 | return bottom; 32 | } 33 | 34 | // find how many elements from [seq] we cann group together stating below 35 | // [limit] by adding their weights 36 | function requiredElements(seq, limit) { 37 | return seq.reduce((res, el) => { 38 | res.tot += el; 39 | if (res.tot > limit) { 40 | res.tot = el; 41 | res.n++; 42 | } 43 | return res; 44 | }, {tot: 0, n: 1}).n; 45 | } 46 | 47 | function sum(arr) { 48 | return arr.reduce((sum, el) => sum + el, 0); 49 | } 50 | 51 | function max(arr) { 52 | return arr.reduce((max, el) => el > max ? el : max, 0); 53 | } 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import BreakpointPartition from './lib/BreakpointPartition.js'; 2 | 3 | export default function perfectLayout(photos, screenWidth, screenHeight, opts) { 4 | opts = opts || {}; 5 | opts.margin = opts.margin || 0; 6 | 7 | const rows = _perfectRowsNumber(photos, screenWidth, screenHeight); 8 | const idealHeight = parseInt(screenHeight / 2, 10); 9 | 10 | if (rows < 1) { 11 | return photos.map(img => { 12 | return { 13 | data: img.data, 14 | src: img.src, 15 | width: parseInt(idealHeight * img.ratio) - (opts.margin * 2), 16 | height: idealHeight 17 | }; 18 | }); 19 | } else { 20 | const weights = photos.map(img => parseInt(img.ratio * 100, 10)); 21 | const partitions = BreakpointPartition(weights, rows); 22 | 23 | let current = 0; 24 | 25 | return partitions.map(row => { 26 | const summedRatios = row.reduce((sum, el, i) => sum + photos[current + i].ratio, 0); 27 | 28 | return row.map(() => { 29 | const img = photos[current++]; 30 | 31 | return { 32 | data: img.data, 33 | src: img.src, 34 | width: parseInt((screenWidth / summedRatios) * img.ratio, 10) - (opts.margin * 2), 35 | height: parseInt(screenWidth / summedRatios, 10) 36 | }; 37 | }); 38 | }); 39 | } 40 | } 41 | 42 | function _perfectRowsNumber(photos, screenWidth, screenHeight) { 43 | const idealHeight = parseInt(screenHeight / 2); 44 | const totalWidth = photos.reduce((sum, img) => sum + img.ratio * idealHeight, 0); 45 | return Math.round(totalWidth / screenWidth); 46 | } 47 | -------------------------------------------------------------------------------- /dist/perfectLayout.min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.perfectLayout = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oa)return r.map(function(r){return{data:r.data,src:r.src,width:parseInt(i*r.ratio)-2*n.margin,height:i}});var o=function(){var e=r.map(function(r){return parseInt(100*r.ratio,10)}),i=_libBreakpointPartitionJs2["default"](e,a),o=0;return{v:i.map(function(e){var a=e.reduce(function(t,e,n){return t+r[o+n].ratio},0);return e.map(function(){var e=r[o++];return{data:e.data,src:e.src,width:parseInt(t/a*e.ratio,10)-2*n.margin,height:parseInt(t/a,10)}})})}}();return"object"==typeof o?o.v:void 0}function _perfectRowsNumber(r,t,e){var n=parseInt(e/2),a=r.reduce(function(r,t){return r+t.ratio*n},0);return Math.round(a/t)}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=perfectLayout;var _libBreakpointPartitionJs=require("./lib/BreakpointPartition.js"),_libBreakpointPartitionJs2=_interopRequireDefault(_libBreakpointPartitionJs);module.exports=exports["default"]; 3 | 4 | },{"./lib/BreakpointPartition.js":2}],2:[function(require,module,exports){ 5 | "use strict";function BreakpointPartition(t,n){if(t.length<=1)return[t];if(n>=t.length)return t.map(function(t){return[t]});var r=findLayoutWidth(t,n),e=0;return t.reduce(function(t,n){return sum(t[e])+n>r&&e++,t[e].push(n),t},new Array(n).join().split(",").map(function(){return[]}))}function findLayoutWidth(t,n){for(var r=sum(t)/n,e=Math.max.apply(null,t),u=Math.max(r,e),o=getLayoutDetails(t,u);o.rowCount>n;)u+=o.nextBreakpoint,o=getLayoutDetails(t,u);return u}function getLayoutDetails(t,n){var r={currentRowWidth:0,rowCount:1,nextBreakpoint:Math.min.apply(null,t)},e=t.reduce(function(t,r){var e=t.currentRowWidth+r,u=void 0;return e>n?(u=e-n,u= imageRatioSequence.length) 10 | return imageRatioSequence.map(item => [item]); 11 | 12 | const layoutWidth = findLayoutWidth(imageRatioSequence, expectedRowCount); 13 | let currentRow = 0; 14 | 15 | return imageRatioSequence.reduce((rows, imageRatio) => { 16 | if (sum(rows[currentRow]) + imageRatio > layoutWidth) currentRow++; 17 | rows[currentRow].push(imageRatio); 18 | return rows; 19 | // waiting for more elegant solutions (Array.fill) to work correctly 20 | }, new Array(expectedRowCount).join().split(',').map(() => [])); 21 | } 22 | 23 | // starting at the ideal width, expand to the next breakpoint until we find 24 | // a width that produces the expected number of rows 25 | function findLayoutWidth(imageRatioSequence, expectedRowCount) { 26 | let idealWidth = sum(imageRatioSequence) / expectedRowCount; 27 | let widestItem = Math.max.apply(null, imageRatioSequence); 28 | let galleryWidth = Math.max(idealWidth, widestItem); 29 | let layout = getLayoutDetails(imageRatioSequence, galleryWidth); 30 | 31 | while (layout.rowCount > expectedRowCount) { 32 | galleryWidth += layout.nextBreakpoint; 33 | 34 | layout = getLayoutDetails(imageRatioSequence, galleryWidth); 35 | } 36 | return galleryWidth; 37 | } 38 | 39 | // find the 40 | function getLayoutDetails(imageRatioSequence, expectedWidth) { 41 | const startingLayout = { 42 | currentRowWidth: 0, 43 | rowCount: 1, 44 | // the largest possible step to the next breakpoint is the smallest image ratio 45 | nextBreakpoint: Math.min.apply(null, imageRatioSequence) 46 | }; 47 | 48 | const finalLayout = imageRatioSequence.reduce((layout, itemWidth) => { 49 | const rowWidth = layout.currentRowWidth + itemWidth; 50 | let currentRowsNextBreakpoint; 51 | 52 | if (rowWidth > expectedWidth) { 53 | currentRowsNextBreakpoint = rowWidth - expectedWidth; 54 | if (currentRowsNextBreakpoint < layout.nextBreakpoint) { 55 | layout.nextBreakpoint = currentRowsNextBreakpoint; 56 | } 57 | layout.rowCount += 1; 58 | layout.currentRowWidth = itemWidth; 59 | } else { 60 | layout.currentRowWidth = rowWidth; 61 | } 62 | 63 | return layout; 64 | }, startingLayout); 65 | return { 66 | rowCount: finalLayout.rowCount, 67 | nextBreakpoint: finalLayout.nextBreakpoint 68 | }; 69 | } 70 | 71 | function sum(arr) { 72 | return arr.reduce((sum, el) => sum + el, 0); 73 | } 74 | -------------------------------------------------------------------------------- /dist/jquery.perfectLayout.min.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oa)return r.map(function(r){return{data:r.data,src:r.src,width:parseInt(i*r.ratio)-2*n.margin,height:i}});var o=function(){var e=r.map(function(r){return parseInt(100*r.ratio,10)}),i=_libBreakpointPartitionJs2["default"](e,a),o=0;return{v:i.map(function(e){var a=e.reduce(function(t,e,n){return t+r[o+n].ratio},0);return e.map(function(){var e=r[o++];return{data:e.data,src:e.src,width:parseInt(t/a*e.ratio,10)-2*n.margin,height:parseInt(t/a,10)}})})}}();return"object"==typeof o?o.v:void 0}function _perfectRowsNumber(r,t,e){var n=parseInt(e/2),a=r.reduce(function(r,t){return r+t.ratio*n},0);return Math.round(a/t)}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=perfectLayout;var _libBreakpointPartitionJs=require("./lib/BreakpointPartition.js"),_libBreakpointPartitionJs2=_interopRequireDefault(_libBreakpointPartitionJs);module.exports=exports["default"]; 3 | 4 | },{"./lib/BreakpointPartition.js":3}],2:[function(require,module,exports){ 5 | "use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}var _=require("."),_2=_interopRequireDefault(_);$.fn.perfectLayout=function(e){var t=this,i=$("html").hasClass("touch")?0:15,r=_2["default"](e,window.innerWidth-i,$(window).height());t.empty(),r.forEach(function(e){e.forEach(function(e){var i=$('
');i.css({width:e.width+"px",height:e.height+"px",background:"url("+e.src+")","background-size":"cover"}),t.append(i)})})}; 6 | 7 | },{".":1}],3:[function(require,module,exports){ 8 | "use strict";function BreakpointPartition(t,n){if(t.length<=1)return[t];if(n>=t.length)return t.map(function(t){return[t]});var r=findLayoutWidth(t,n),e=0;return t.reduce(function(t,n){return sum(t[e])+n>r&&e++,t[e].push(n),t},new Array(n).join().split(",").map(function(){return[]}))}function findLayoutWidth(t,n){for(var r=sum(t)/n,e=Math.max.apply(null,t),u=Math.max(r,e),o=getLayoutDetails(t,u);o.rowCount>n;)u+=o.nextBreakpoint,o=getLayoutDetails(t,u);return u}function getLayoutDetails(t,n){var r={currentRowWidth:0,rowCount:1,nextBreakpoint:Math.min.apply(null,t)},e=t.reduce(function(t,r){var e=t.currentRowWidth+r,u=void 0;return e>n?(u=e-n,u 42 | ``` 43 | then 44 | ``` 45 | var perfectRows = perfectLayout(photos, width, height, { 46 | // default options 47 | margin: 0 48 | }); 49 | ``` 50 | 51 | ### Options 52 | 53 | - margin: [number] 54 | If you are going to use a css margin for your images you need to specify it here 55 | as well, so that the layout will adapt to scale down the images accordingly. 56 | 57 | ## Motivations 58 | 59 | This was inspired by [chromatic.io](http://www.chromatic.io/FQrLQsb) galleries 60 | and I want to credit the [@crispymtn](https://github.com/crispymtn) team for the 61 | original implementation. 62 | 63 | This version aim to be more lightweight using a greedy algorithm instead of the 64 | optimal one and also leave to the user the responsability to choose how to 65 | manipulate the DOM accordingly to the returned array. 66 | 67 | ## Example jQuery plugin 68 | 69 | for convenience a jquery plugin is included for a basic usage. 70 | 71 | assuming that a global `window.photos` array exists as specified above 72 | 73 | ``` 74 | 75 | 76 | 77 | 78 | 79 | 89 | ``` 90 | 91 | *N.B.* Please note that this is only an example on how to use the `perfectLayout` function. 92 | The jQuery plugin is not to be used in production as it do not provide any 93 | crossbrowser optimization, at the time of writing it should however correctly 94 | work on the latest chrome and firefox browsers on linux. 95 | 96 | For custom behaviour give a look at the [jqueryPlugin.js](https://github.com/axyz/perfect-layout/blob/master/jqueryPlugin.js) 97 | and use it as a starting point to generate the desired DOM nodes. 98 | 99 | the data field can be used to populate the images with any needed metadata 100 | you may need and is probably a good idea to provide it from your backend. 101 | 102 | ## Changelog 103 | 104 | ## [v1.2.0] 105 | ### Changed 106 | - using breakPointPartition thanks to @GreenGremlin 107 | 108 | ## [v1.1.1] 109 | ### Changed 110 | - using BST based linear partitioning instead of greedy one 111 | - huge speed improvement 112 | - the resulting set is now optimal 113 | ### Fixed 114 | - the partition will now keep the same order as the input array 115 | - the layout should now be equal to the chromatic.io one in all the cases 116 | 117 | ## [v1.1.0] 118 | ### Added 119 | - margin option 120 | 121 | ## [v1.0.0] 122 | ### Initial Release 123 | -------------------------------------------------------------------------------- /dist/perfectLayout.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.perfectLayout = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= imageRatioSequence.length) return imageRatioSequence.map(function (item) { 90 | return [item]; 91 | }); 92 | 93 | var layoutWidth = findLayoutWidth(imageRatioSequence, expectedRowCount); 94 | var currentRow = 0; 95 | 96 | return imageRatioSequence.reduce(function (rows, imageRatio) { 97 | if (sum(rows[currentRow]) + imageRatio > layoutWidth) currentRow++; 98 | rows[currentRow].push(imageRatio); 99 | return rows; 100 | // waiting for more elegant solutions (Array.fill) to work correctly 101 | }, new Array(expectedRowCount).join().split(',').map(function () { 102 | return []; 103 | })); 104 | } 105 | 106 | // starting at the ideal width, expand to the next breakpoint until we find 107 | // a width that produces the expected number of rows 108 | function findLayoutWidth(imageRatioSequence, expectedRowCount) { 109 | var idealWidth = sum(imageRatioSequence) / expectedRowCount; 110 | var widestItem = Math.max.apply(null, imageRatioSequence); 111 | var galleryWidth = Math.max(idealWidth, widestItem); 112 | var layout = getLayoutDetails(imageRatioSequence, galleryWidth); 113 | 114 | while (layout.rowCount > expectedRowCount) { 115 | galleryWidth += layout.nextBreakpoint; 116 | 117 | layout = getLayoutDetails(imageRatioSequence, galleryWidth); 118 | } 119 | return galleryWidth; 120 | } 121 | 122 | // find the 123 | function getLayoutDetails(imageRatioSequence, expectedWidth) { 124 | var startingLayout = { 125 | currentRowWidth: 0, 126 | rowCount: 1, 127 | // the largest possible step to the next breakpoint is the smallest image ratio 128 | nextBreakpoint: Math.min.apply(null, imageRatioSequence) 129 | }; 130 | 131 | var finalLayout = imageRatioSequence.reduce(function (layout, itemWidth) { 132 | var rowWidth = layout.currentRowWidth + itemWidth; 133 | var currentRowsNextBreakpoint = undefined; 134 | 135 | if (rowWidth > expectedWidth) { 136 | currentRowsNextBreakpoint = rowWidth - expectedWidth; 137 | if (currentRowsNextBreakpoint < layout.nextBreakpoint) { 138 | layout.nextBreakpoint = currentRowsNextBreakpoint; 139 | } 140 | layout.rowCount += 1; 141 | layout.currentRowWidth = itemWidth; 142 | } else { 143 | layout.currentRowWidth = rowWidth; 144 | } 145 | 146 | return layout; 147 | }, startingLayout); 148 | return { 149 | rowCount: finalLayout.rowCount, 150 | nextBreakpoint: finalLayout.nextBreakpoint 151 | }; 152 | } 153 | 154 | function sum(arr) { 155 | return arr.reduce(function (sum, el) { 156 | return sum + el; 157 | }, 0); 158 | } 159 | module.exports = exports['default']; 160 | 161 | },{}]},{},[1])(1) 162 | }); -------------------------------------------------------------------------------- /dist/jquery.perfectLayout.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o'); 91 | imgNode.css({ 92 | 'width': img.width + 'px', 93 | 'height': img.height + 'px', 94 | 'background': 'url(' + img.src + ')', 95 | 'background-size': 'cover' 96 | }); 97 | node.append(imgNode); 98 | }); 99 | }); 100 | }; 101 | 102 | },{".":1}],3:[function(require,module,exports){ 103 | // Rather than blindly perform a binary search from the maximum width. It starts 104 | // from the ideal width (The ideal width being the width if the images fit 105 | // perfectly in the given container.) and expands to the next width that will 106 | // allow an item to move up a row. This algorithm will find the exact width that 107 | // produces the "ideal" layout and should generally find it in two or three 108 | // passes. 109 | 'use strict'; 110 | 111 | Object.defineProperty(exports, '__esModule', { 112 | value: true 113 | }); 114 | exports['default'] = BreakpointPartition; 115 | 116 | function BreakpointPartition(imageRatioSequence, expectedRowCount) { 117 | if (imageRatioSequence.length <= 1) return [imageRatioSequence]; 118 | if (expectedRowCount >= imageRatioSequence.length) return imageRatioSequence.map(function (item) { 119 | return [item]; 120 | }); 121 | 122 | var layoutWidth = findLayoutWidth(imageRatioSequence, expectedRowCount); 123 | var currentRow = 0; 124 | 125 | return imageRatioSequence.reduce(function (rows, imageRatio) { 126 | if (sum(rows[currentRow]) + imageRatio > layoutWidth) currentRow++; 127 | rows[currentRow].push(imageRatio); 128 | return rows; 129 | // waiting for more elegant solutions (Array.fill) to work correctly 130 | }, new Array(expectedRowCount).join().split(',').map(function () { 131 | return []; 132 | })); 133 | } 134 | 135 | // starting at the ideal width, expand to the next breakpoint until we find 136 | // a width that produces the expected number of rows 137 | function findLayoutWidth(imageRatioSequence, expectedRowCount) { 138 | var idealWidth = sum(imageRatioSequence) / expectedRowCount; 139 | var widestItem = Math.max.apply(null, imageRatioSequence); 140 | var galleryWidth = Math.max(idealWidth, widestItem); 141 | var layout = getLayoutDetails(imageRatioSequence, galleryWidth); 142 | 143 | while (layout.rowCount > expectedRowCount) { 144 | galleryWidth += layout.nextBreakpoint; 145 | 146 | layout = getLayoutDetails(imageRatioSequence, galleryWidth); 147 | } 148 | return galleryWidth; 149 | } 150 | 151 | // find the 152 | function getLayoutDetails(imageRatioSequence, expectedWidth) { 153 | var startingLayout = { 154 | currentRowWidth: 0, 155 | rowCount: 1, 156 | // the largest possible step to the next breakpoint is the smallest image ratio 157 | nextBreakpoint: Math.min.apply(null, imageRatioSequence) 158 | }; 159 | 160 | var finalLayout = imageRatioSequence.reduce(function (layout, itemWidth) { 161 | var rowWidth = layout.currentRowWidth + itemWidth; 162 | var currentRowsNextBreakpoint = undefined; 163 | 164 | if (rowWidth > expectedWidth) { 165 | currentRowsNextBreakpoint = rowWidth - expectedWidth; 166 | if (currentRowsNextBreakpoint < layout.nextBreakpoint) { 167 | layout.nextBreakpoint = currentRowsNextBreakpoint; 168 | } 169 | layout.rowCount += 1; 170 | layout.currentRowWidth = itemWidth; 171 | } else { 172 | layout.currentRowWidth = rowWidth; 173 | } 174 | 175 | return layout; 176 | }, startingLayout); 177 | return { 178 | rowCount: finalLayout.rowCount, 179 | nextBreakpoint: finalLayout.nextBreakpoint 180 | }; 181 | } 182 | 183 | function sum(arr) { 184 | return arr.reduce(function (sum, el) { 185 | return sum + el; 186 | }, 0); 187 | } 188 | module.exports = exports['default']; 189 | 190 | },{}]},{},[2]); 191 | --------------------------------------------------------------------------------