Repository: axyz/perfect-layout Branch: master Commit: 398db7e01e71 Files: 12 Total size: 24.2 KB Directory structure: gitextract_q68n26jo/ ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── dist/ │ ├── jquery.perfectLayout.js │ └── perfectLayout.js ├── index.js ├── jqueryPlugin.js ├── lib/ │ ├── .tern-port │ ├── BSTLinearPartition.js │ └── BreakpointPartition.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Andrea Moretti Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Perfect Layout [Medium Article](https://medium.com/@axyz/in-search-of-the-perfect-image-gallery-34f46f7615a1) [DEMO](http://codepen.io/axyz/full/VLJrKr/) given an array of images in the form ``` { data: WATHEVER_YOU_WANT, src: "path/to/image.jpg", ratio: 1.5 } ``` returns an array of images in the form ``` { data: WATHEVER_YOU_WANT, src: "path/to/image.jpg", width: WIDTH, height: HEIGHT } ``` where WIDTH and HEIGHT are the dimensions that image must have to fit the layout. ## Usage on node ``` $ npm install --save perfect-layout ``` and ``` var perfectLayout = require('perfect-layout') ``` while on the browser you can just ``` ``` then ``` var perfectRows = perfectLayout(photos, width, height, { // default options margin: 0 }); ``` ### Options - margin: [number] If you are going to use a css margin for your images you need to specify it here as well, so that the layout will adapt to scale down the images accordingly. ## Motivations This was inspired by [chromatic.io](http://www.chromatic.io/FQrLQsb) galleries and I want to credit the [@crispymtn](https://github.com/crispymtn) team for the original implementation. This version aim to be more lightweight using a greedy algorithm instead of the optimal one and also leave to the user the responsability to choose how to manipulate the DOM accordingly to the returned array. ## Example jQuery plugin for convenience a jquery plugin is included for a basic usage. assuming that a global `window.photos` array exists as specified above ``` ``` *N.B.* Please note that this is only an example on how to use the `perfectLayout` function. The jQuery plugin is not to be used in production as it do not provide any crossbrowser optimization, at the time of writing it should however correctly work on the latest chrome and firefox browsers on linux. For custom behaviour give a look at the [jqueryPlugin.js](https://github.com/axyz/perfect-layout/blob/master/jqueryPlugin.js) and use it as a starting point to generate the desired DOM nodes. the data field can be used to populate the images with any needed metadata you may need and is probably a good idea to provide it from your backend. ## Changelog ## [v1.2.0] ### Changed - using breakPointPartition thanks to @GreenGremlin ## [v1.1.1] ### Changed - using BST based linear partitioning instead of greedy one - huge speed improvement - the resulting set is now optimal ### Fixed - the partition will now keep the same order as the input array - the layout should now be equal to the chromatic.io one in all the cases ## [v1.1.0] ### Added - margin option ## [v1.0.0] ### Initial Release ================================================ FILE: bower.json ================================================ { "name": "perfect-layout", "description": "Image layout generator based on linear partitioning", "main": "dist/perfectLayout.min.js", "version": "1.0.0", "authors": [ "Andrea Moretti " ], "moduleType": [ "es6", "globals", "node" ], "keywords": [ "gallery", "images", "layout", "linear", "partition" ], "license": "MIT", "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests" ] } ================================================ FILE: dist/jquery.perfectLayout.js ================================================ (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'); imgNode.css({ 'width': img.width + 'px', 'height': img.height + 'px', 'background': 'url(' + img.src + ')', 'background-size': 'cover' }); node.append(imgNode); }); }); }; },{".":1}],3:[function(require,module,exports){ // Rather than blindly perform a binary search from the maximum width. It starts // from the ideal width (The ideal width being the width if the images fit // perfectly in the given container.) and expands to the next width that will // allow an item to move up a row. This algorithm will find the exact width that // produces the "ideal" layout and should generally find it in two or three // passes. 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports['default'] = BreakpointPartition; function BreakpointPartition(imageRatioSequence, expectedRowCount) { if (imageRatioSequence.length <= 1) return [imageRatioSequence]; if (expectedRowCount >= imageRatioSequence.length) return imageRatioSequence.map(function (item) { return [item]; }); var layoutWidth = findLayoutWidth(imageRatioSequence, expectedRowCount); var currentRow = 0; return imageRatioSequence.reduce(function (rows, imageRatio) { if (sum(rows[currentRow]) + imageRatio > layoutWidth) currentRow++; rows[currentRow].push(imageRatio); return rows; // waiting for more elegant solutions (Array.fill) to work correctly }, new Array(expectedRowCount).join().split(',').map(function () { return []; })); } // starting at the ideal width, expand to the next breakpoint until we find // a width that produces the expected number of rows function findLayoutWidth(imageRatioSequence, expectedRowCount) { var idealWidth = sum(imageRatioSequence) / expectedRowCount; var widestItem = Math.max.apply(null, imageRatioSequence); var galleryWidth = Math.max(idealWidth, widestItem); var layout = getLayoutDetails(imageRatioSequence, galleryWidth); while (layout.rowCount > expectedRowCount) { galleryWidth += layout.nextBreakpoint; layout = getLayoutDetails(imageRatioSequence, galleryWidth); } return galleryWidth; } // find the function getLayoutDetails(imageRatioSequence, expectedWidth) { var startingLayout = { currentRowWidth: 0, rowCount: 1, // the largest possible step to the next breakpoint is the smallest image ratio nextBreakpoint: Math.min.apply(null, imageRatioSequence) }; var finalLayout = imageRatioSequence.reduce(function (layout, itemWidth) { var rowWidth = layout.currentRowWidth + itemWidth; var currentRowsNextBreakpoint = undefined; if (rowWidth > expectedWidth) { currentRowsNextBreakpoint = rowWidth - expectedWidth; if (currentRowsNextBreakpoint < layout.nextBreakpoint) { layout.nextBreakpoint = currentRowsNextBreakpoint; } layout.rowCount += 1; layout.currentRowWidth = itemWidth; } else { layout.currentRowWidth = rowWidth; } return layout; }, startingLayout); return { rowCount: finalLayout.rowCount, nextBreakpoint: finalLayout.nextBreakpoint }; } function sum(arr) { return arr.reduce(function (sum, el) { return sum + el; }, 0); } module.exports = exports['default']; },{}]},{},[2]); ================================================ FILE: dist/perfectLayout.js ================================================ (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) { return [item]; }); var layoutWidth = findLayoutWidth(imageRatioSequence, expectedRowCount); var currentRow = 0; return imageRatioSequence.reduce(function (rows, imageRatio) { if (sum(rows[currentRow]) + imageRatio > layoutWidth) currentRow++; rows[currentRow].push(imageRatio); return rows; // waiting for more elegant solutions (Array.fill) to work correctly }, new Array(expectedRowCount).join().split(',').map(function () { return []; })); } // starting at the ideal width, expand to the next breakpoint until we find // a width that produces the expected number of rows function findLayoutWidth(imageRatioSequence, expectedRowCount) { var idealWidth = sum(imageRatioSequence) / expectedRowCount; var widestItem = Math.max.apply(null, imageRatioSequence); var galleryWidth = Math.max(idealWidth, widestItem); var layout = getLayoutDetails(imageRatioSequence, galleryWidth); while (layout.rowCount > expectedRowCount) { galleryWidth += layout.nextBreakpoint; layout = getLayoutDetails(imageRatioSequence, galleryWidth); } return galleryWidth; } // find the function getLayoutDetails(imageRatioSequence, expectedWidth) { var startingLayout = { currentRowWidth: 0, rowCount: 1, // the largest possible step to the next breakpoint is the smallest image ratio nextBreakpoint: Math.min.apply(null, imageRatioSequence) }; var finalLayout = imageRatioSequence.reduce(function (layout, itemWidth) { var rowWidth = layout.currentRowWidth + itemWidth; var currentRowsNextBreakpoint = undefined; if (rowWidth > expectedWidth) { currentRowsNextBreakpoint = rowWidth - expectedWidth; if (currentRowsNextBreakpoint < layout.nextBreakpoint) { layout.nextBreakpoint = currentRowsNextBreakpoint; } layout.rowCount += 1; layout.currentRowWidth = itemWidth; } else { layout.currentRowWidth = rowWidth; } return layout; }, startingLayout); return { rowCount: finalLayout.rowCount, nextBreakpoint: finalLayout.nextBreakpoint }; } function sum(arr) { return arr.reduce(function (sum, el) { return sum + el; }, 0); } module.exports = exports['default']; },{}]},{},[1])(1) }); ================================================ FILE: index.js ================================================ import BreakpointPartition from './lib/BreakpointPartition.js'; export default function perfectLayout(photos, screenWidth, screenHeight, opts) { opts = opts || {}; opts.margin = opts.margin || 0; const rows = _perfectRowsNumber(photos, screenWidth, screenHeight); const idealHeight = parseInt(screenHeight / 2, 10); if (rows < 1) { return photos.map(img => { return { data: img.data, src: img.src, width: parseInt(idealHeight * img.ratio) - (opts.margin * 2), height: idealHeight }; }); } else { const weights = photos.map(img => parseInt(img.ratio * 100, 10)); const partitions = BreakpointPartition(weights, rows); let current = 0; return partitions.map(row => { const summedRatios = row.reduce((sum, el, i) => sum + photos[current + i].ratio, 0); return row.map(() => { const img = photos[current++]; return { data: img.data, src: img.src, width: parseInt((screenWidth / summedRatios) * img.ratio, 10) - (opts.margin * 2), height: parseInt(screenWidth / summedRatios, 10) }; }); }); } } function _perfectRowsNumber(photos, screenWidth, screenHeight) { const idealHeight = parseInt(screenHeight / 2); const totalWidth = photos.reduce((sum, img) => sum + img.ratio * idealHeight, 0); return Math.round(totalWidth / screenWidth); } ================================================ FILE: jqueryPlugin.js ================================================ import perfectLayout from '.'; $.fn.perfectLayout = function(photos) { const node = this; const scrollBarSize = $('html').hasClass('touch') ? 0 : 15; const perfectRows = perfectLayout(photos, window.innerWidth - scrollBarSize, $(window).height()); node.empty(); perfectRows.forEach(function (row) { row.forEach(function (img) { var imgNode = $('
'); imgNode.css({ 'width': img.width + 'px', 'height': img.height + 'px', 'background': 'url(' + img.src + ')', 'background-size': 'cover' }); node.append(imgNode); }); }); }; ================================================ FILE: lib/.tern-port ================================================ 34063 ================================================ FILE: lib/BSTLinearPartition.js ================================================ export default function BSTLinearPartition(seq, k) { if (seq.length <= 1) return [seq]; if (k >= seq.length) return seq.map(el => [el]); const limit = threshold(seq, k); let current = 0; return seq.reduce((res, el) => { if (sum(res[current]) + el > limit) current++; res[current].push(el); return res; // waiting for more elegant solutions (Array.fill) to work correctly }, new Array(k).join().split(',').map(() => [])); } // find the perfect limit that we should not pass when adding elements // to a single partition. function threshold(seq, k) { let bottom = max(seq); let top = sum(seq); while (bottom < top) { const mid = bottom + ( top - bottom) / 2; if (requiredElements(seq, mid) <= k) { top = mid; } else { bottom = mid + 1; } } return bottom; } // find how many elements from [seq] we cann group together stating below // [limit] by adding their weights function requiredElements(seq, limit) { return seq.reduce((res, el) => { res.tot += el; if (res.tot > limit) { res.tot = el; res.n++; } return res; }, {tot: 0, n: 1}).n; } function sum(arr) { return arr.reduce((sum, el) => sum + el, 0); } function max(arr) { return arr.reduce((max, el) => el > max ? el : max, 0); } ================================================ FILE: lib/BreakpointPartition.js ================================================ // Rather than blindly perform a binary search from the maximum width. It starts // from the ideal width (The ideal width being the width if the images fit // perfectly in the given container.) and expands to the next width that will // allow an item to move up a row. This algorithm will find the exact width that // produces the "ideal" layout and should generally find it in two or three // passes. export default function BreakpointPartition(imageRatioSequence, expectedRowCount) { if (imageRatioSequence.length <= 1) return [imageRatioSequence]; if (expectedRowCount >= imageRatioSequence.length) return imageRatioSequence.map(item => [item]); const layoutWidth = findLayoutWidth(imageRatioSequence, expectedRowCount); let currentRow = 0; return imageRatioSequence.reduce((rows, imageRatio) => { if (sum(rows[currentRow]) + imageRatio > layoutWidth) currentRow++; rows[currentRow].push(imageRatio); return rows; // waiting for more elegant solutions (Array.fill) to work correctly }, new Array(expectedRowCount).join().split(',').map(() => [])); } // starting at the ideal width, expand to the next breakpoint until we find // a width that produces the expected number of rows function findLayoutWidth(imageRatioSequence, expectedRowCount) { let idealWidth = sum(imageRatioSequence) / expectedRowCount; let widestItem = Math.max.apply(null, imageRatioSequence); let galleryWidth = Math.max(idealWidth, widestItem); let layout = getLayoutDetails(imageRatioSequence, galleryWidth); while (layout.rowCount > expectedRowCount) { galleryWidth += layout.nextBreakpoint; layout = getLayoutDetails(imageRatioSequence, galleryWidth); } return galleryWidth; } // find the function getLayoutDetails(imageRatioSequence, expectedWidth) { const startingLayout = { currentRowWidth: 0, rowCount: 1, // the largest possible step to the next breakpoint is the smallest image ratio nextBreakpoint: Math.min.apply(null, imageRatioSequence) }; const finalLayout = imageRatioSequence.reduce((layout, itemWidth) => { const rowWidth = layout.currentRowWidth + itemWidth; let currentRowsNextBreakpoint; if (rowWidth > expectedWidth) { currentRowsNextBreakpoint = rowWidth - expectedWidth; if (currentRowsNextBreakpoint < layout.nextBreakpoint) { layout.nextBreakpoint = currentRowsNextBreakpoint; } layout.rowCount += 1; layout.currentRowWidth = itemWidth; } else { layout.currentRowWidth = rowWidth; } return layout; }, startingLayout); return { rowCount: finalLayout.rowCount, nextBreakpoint: finalLayout.nextBreakpoint }; } function sum(arr) { return arr.reduce((sum, el) => sum + el, 0); } ================================================ FILE: package.json ================================================ { "name": "perfect-layout", "version": "1.2.1", "description": "Image layout generator based on linear partitioning", "main": "index.js", "scripts": { "build": "./node_modules/.bin/browserify index.js -t babelify -t uglifyify --standalone perfectLayout -o dist/perfectLayout.min.js", "build-dev": "./node_modules/.bin/browserify index.js -t babelify --standalone perfectLayout -o dist/perfectLayout.js", "build-jquery": "npm run build && ./node_modules/.bin/browserify jqueryPlugin.js -t babelify -t uglifyify -o dist/jquery.perfectLayout.min.js", "build-jquery-dev": "npm run build-dev && ./node_modules/.bin/browserify jqueryPlugin.js -t babelify -o dist/jquery.perfectLayout.js", "test": "echo \"Error: no test specified\" && exit 1", "dist": "npm run build && npm run build-jquery && npm run build-dev && npm run build-jquery-dev" }, "author": "Andrea Moretti (@axyz) ", "license": "MIT", "devDependencies": { "babelify": "^6.2.0", "browserify": "^11.0.1", "uglifyify": "^3.0.1" } }