require=(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i= 1) { // Close out the row with a full width photo this.items.push(itemData); this.completeLayout(rowWidthWithoutSpacing / itemData.aspectRatio, 'justify'); return true; } } } if (newAspectRatio < this.minAspectRatio) { // New aspect ratio is too narrow / scaled row height is too tall. // Accept this item and leave row open for more items. this.items.push(merge(itemData)); return true; } else if (newAspectRatio > this.maxAspectRatio) { // New aspect ratio is too wide / scaled row height will be too short. // Accept item if the resulting aspect ratio is closer to target than it would be without the item. // NOTE: Any row that falls into this block will require cropping/padding on individual items. if (this.items.length === 0) { // When there are no existing items, force acceptance of the new item and complete the layout. // This is the pano special case. this.items.push(merge(itemData)); this.completeLayout(rowWidthWithoutSpacing / newAspectRatio, 'justify'); return true; } // Calculate width/aspect ratio for row before adding new item previousRowWidthWithoutSpacing = this.width - (this.items.length - 1) * this.spacing; previousAspectRatio = this.items.reduce(function (sum, item) { return sum + item.aspectRatio; }, 0); previousTargetAspectRatio = previousRowWidthWithoutSpacing / this.targetRowHeight; if (Math.abs(newAspectRatio - targetAspectRatio) > Math.abs(previousAspectRatio - previousTargetAspectRatio)) { // Row with new item is us farther away from target than row without; complete layout and reject item. this.completeLayout(previousRowWidthWithoutSpacing / previousAspectRatio, 'justify'); return false; } else { // Row with new item is us closer to target than row without; // accept the new item and complete the row layout. this.items.push(merge(itemData)); this.completeLayout(rowWidthWithoutSpacing / newAspectRatio, 'justify'); return true; } } else { // New aspect ratio / scaled row height is within tolerance; // accept the new item and complete the row layout. this.items.push(merge(itemData)); this.completeLayout(rowWidthWithoutSpacing / newAspectRatio, 'justify'); return true; } }, /** * Check if a row has completed its layout. * * @method isLayoutComplete * @return {Boolean} True if complete; false if not. */ isLayoutComplete: function () { return this.height > 0; }, /** * Set row height and compute item geometry from that height. * Will justify items within the row unless instructed not to. * * @method completeLayout * @param newHeight {Number} Set row height to this value. * @param widowLayoutStyle {String} How should widows display? Supported: left | justify | center */ completeLayout: function (newHeight, widowLayoutStyle) { var itemWidthSum = this.left, rowWidthWithoutSpacing = this.width - (this.items.length - 1) * this.spacing, clampedToNativeRatio, clampedHeight, errorWidthPerItem, roundedCumulativeErrors, singleItemGeometry, centerOffset; // Justify unless explicitly specified otherwise. if (typeof widowLayoutStyle === 'undefined' || ['justify', 'center', 'left'].indexOf(widowLayoutStyle) < 0) { widowLayoutStyle = 'left'; } // Clamp row height to edge case minimum/maximum. clampedHeight = Math.max(this.edgeCaseMinRowHeight, Math.min(newHeight, this.edgeCaseMaxRowHeight)); if (newHeight !== clampedHeight) { // If row height was clamped, the resulting row/item aspect ratio will be off, // so force it to fit the width (recalculate aspectRatio to match clamped height). // NOTE: this will result in cropping/padding commensurate to the amount of clamping. this.height = clampedHeight; clampedToNativeRatio = (rowWidthWithoutSpacing / clampedHeight) / (rowWidthWithoutSpacing / newHeight); } else { // If not clamped, leave ratio at 1.0. this.height = newHeight; clampedToNativeRatio = 1.0; } // Compute item geometry based on newHeight. this.items.forEach(function (item) { item.top = this.top; item.width = item.aspectRatio * this.height * clampedToNativeRatio; item.height = this.height; // Left-to-right. // TODO right to left // item.left = this.width - itemWidthSum - item.width; item.left = itemWidthSum; // Increment width. itemWidthSum += item.width + this.spacing; }, this); // If specified, ensure items fill row and distribute error // caused by rounding width and height across all items. if (widowLayoutStyle === 'justify') { itemWidthSum -= (this.spacing + this.left); errorWidthPerItem = (itemWidthSum - this.width) / this.items.length; roundedCumulativeErrors = this.items.map(function (item, i) { return Math.round((i + 1) * errorWidthPerItem); }); if (this.items.length === 1) { // For rows with only one item, adjust item width to fill row. singleItemGeometry = this.items[0]; singleItemGeometry.width -= Math.round(errorWidthPerItem); } else { // For rows with multiple items, adjust item width and shift items to fill the row, // while maintaining equal spacing between items in the row. this.items.forEach(function (item, i) { if (i > 0) { item.left -= roundedCumulativeErrors[i - 1]; item.width -= (roundedCumulativeErrors[i] - roundedCumulativeErrors[i - 1]); } else { item.width -= roundedCumulativeErrors[i]; } }); } } else if (widowLayoutStyle === 'center') { // Center widows centerOffset = (this.width - itemWidthSum) / 2; this.items.forEach(function (item) { item.left += centerOffset + this.spacing; }, this); } }, /** * Force completion of row layout with current items. * * @method forceComplete * @param fitToWidth {Boolean} Stretch current items to fill the row width. * This will likely result in padding. * @param fitToWidth {Number} */ forceComplete: function (fitToWidth, rowHeight) { // TODO Handle fitting to width // var rowWidthWithoutSpacing = this.width - (this.items.length - 1) * this.spacing, // currentAspectRatio = this.items.reduce(function (sum, item) { // return sum + item.aspectRatio; // }, 0); if (typeof rowHeight === 'number') { this.completeLayout(rowHeight, this.widowLayoutStyle); } else { // Complete using target row height. this.completeLayout(this.targetRowHeight, this.widowLayoutStyle); } }, /** * Return layout data for items within row. * Note: returns actual list, not a copy. * * @method getItems * @return Layout data for items within row. */ getItems: function () { return this.items; } }; },{"merge":2}],2:[function(require,module,exports){ /*! * @name JavaScript/NodeJS Merge v1.2.1 * @author yeikos * @repository https://github.com/yeikos/js.merge * Copyright 2014 yeikos - MIT license * https://raw.github.com/yeikos/js.merge/master/LICENSE */ ;(function(isNode) { /** * Merge one or more objects * @param bool? clone * @param mixed,... arguments * @return object */ var Public = function(clone) { return merge(clone === true, false, arguments); }, publicName = 'merge'; /** * Merge two or more objects recursively * @param bool? clone * @param mixed,... arguments * @return object */ Public.recursive = function(clone) { return merge(clone === true, true, arguments); }; /** * Clone the input removing any reference * @param mixed input * @return mixed */ Public.clone = function(input) { var output = input, type = typeOf(input), index, size; if (type === 'array') { output = []; size = input.length; for (index=0;index= layoutConfig.maxNumRows) { currentRow = null; return true; } currentRow = createNewRow(layoutConfig, layoutData); // Item was rejected; add it to its own row if (!itemAdded) { itemAdded = currentRow.addItem(itemData); if (currentRow.isLayoutComplete()) { // If the rejected item fills a row on its own, add the row and start another new one laidOutItems = laidOutItems.concat(addRow(layoutConfig, layoutData, currentRow)); if (layoutData._rows.length >= layoutConfig.maxNumRows) { currentRow = null; return true; } currentRow = createNewRow(layoutConfig, layoutData); } } } }); // Handle any leftover content (orphans) depending on where they lie // in this layout update, and in the total content set. if (currentRow && currentRow.getItems().length && layoutConfig.showWidows) { // Last page of all content or orphan suppression is suppressed; lay out orphans. if (layoutData._rows.length) { // Only Match previous row's height if it exists and it isn't a breakout row if (layoutData._rows[layoutData._rows.length - 1].isBreakoutRow) { nextToLastRowHeight = layoutData._rows[layoutData._rows.length - 1].targetRowHeight; } else { nextToLastRowHeight = layoutData._rows[layoutData._rows.length - 1].height; } currentRow.forceComplete(false, nextToLastRowHeight); } else { // ...else use target height if there is no other row height to reference. currentRow.forceComplete(false); } laidOutItems = laidOutItems.concat(addRow(layoutConfig, layoutData, currentRow)); layoutConfig._widowCount = currentRow.getItems().length; } // We need to clean up the bottom container padding // First remove the height added for box spacing layoutData._containerHeight = layoutData._containerHeight - layoutConfig.boxSpacing.vertical; // Then add our bottom container padding layoutData._containerHeight = layoutData._containerHeight + layoutConfig.containerPadding.bottom; return { containerHeight: layoutData._containerHeight, widowCount: layoutConfig._widowCount, boxes: layoutData._layoutItems }; } /** * Takes in a bunch of box data and config. Returns * geometry to lay them out in a justified view. * * @method covertSizesToAspectRatios * @param sizes {Array} Array of objects with widths and heights * @return {Array} A list of aspect ratios */ module.exports = function (input, config) { var layoutConfig = {}; var layoutData = {}; // Defaults var defaults = { containerWidth: 1060, containerPadding: 10, boxSpacing: 10, targetRowHeight: 320, targetRowHeightTolerance: 0.25, maxNumRows: Number.POSITIVE_INFINITY, forceAspectRatio: false, showWidows: true, fullWidthBreakoutRowCadence: false, widowLayoutStyle: 'left' }; var containerPadding = {}; var boxSpacing = {}; config = config || {}; // Merge defaults and config passed in layoutConfig = merge(defaults, config); // Sort out padding and spacing values containerPadding.top = (!isNaN(parseFloat(layoutConfig.containerPadding.top))) ? layoutConfig.containerPadding.top : layoutConfig.containerPadding; containerPadding.right = (!isNaN(parseFloat(layoutConfig.containerPadding.right))) ? layoutConfig.containerPadding.right : layoutConfig.containerPadding; containerPadding.bottom = (!isNaN(parseFloat(layoutConfig.containerPadding.bottom))) ? layoutConfig.containerPadding.bottom : layoutConfig.containerPadding; containerPadding.left = (!isNaN(parseFloat(layoutConfig.containerPadding.left))) ? layoutConfig.containerPadding.left : layoutConfig.containerPadding; boxSpacing.horizontal = (!isNaN(parseFloat(layoutConfig.boxSpacing.horizontal))) ? layoutConfig.boxSpacing.horizontal : layoutConfig.boxSpacing; boxSpacing.vertical = (!isNaN(parseFloat(layoutConfig.boxSpacing.vertical))) ? layoutConfig.boxSpacing.vertical : layoutConfig.boxSpacing; layoutConfig.containerPadding = containerPadding; layoutConfig.boxSpacing = boxSpacing; // Local layoutData._layoutItems = []; layoutData._awakeItems = []; layoutData._inViewportItems = []; layoutData._leadingOrphans = []; layoutData._trailingOrphans = []; layoutData._containerHeight = layoutConfig.containerPadding.top; layoutData._rows = []; layoutData._orphans = []; layoutConfig._widowCount = 0; // Convert widths and heights to aspect ratios if we need to return computeLayout(layoutConfig, layoutData, input.map(function (item) { if (item.width && item.height) { return { aspectRatio: item.width / item.height }; } else { return { aspectRatio: item }; } })); }; },{"./row":1,"merge":2}]},{},[]);