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"
}
}