Created
March 26, 2015 10:50
-
-
Save Qvatra/65d481ce0596e6f1b012 to your computer and use it in GitHub Desktop.
fixed masonry layout for famous-flex LayoutController
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This Source Code is licensed under the MIT license. If a copy of the | |
* MIT-license was not distributed with this file, You can obtain one at: | |
* http://opensource.org/licenses/mit-license.html. | |
* | |
* @author: Oleksandr Zinchenko (Qvatra) | |
* @license MIT | |
*/ | |
/*global console*/ | |
/*eslint no-console: 0*/ | |
/** | |
* Fits (sets max possible size) a collection of renderables with a given aspect ratios to the context size from left to right, and when the right edge is reached, | |
* continues at the next row. | |
* | |
* |options|type|description| | |
* |---|---|---| | |
* |`[cellRatios]`|Array.Number|Array of the dataSource elements aspect ratios| | |
* | |
* Example: | |
* | |
* ```javascript | |
* var FixedMasonryLayout = require('FixedMasonryLayout'); | |
* | |
* var layoutController = new LayoutController({ | |
* layout: FixedMasonryLayout, | |
* layoutOptions: { | |
* cellRatios: [1, 3, 1, 2] | |
* }, | |
* dataSource: [ | |
* new Surface({content: '1', properties:{background:'red'}}), | |
* new Surface({content: '2', properties:{background:'blue'}}), | |
* new Surface({content: '3', properties:{background:'green'}}), | |
* new Surface({content: '4', properties:{background:'yellow'}}) | |
* ] | |
* }); | |
* ``` | |
* @module | |
*/ | |
define(function (require, exports, module) { | |
// Define capabilities of this layout function | |
var capabilities = { | |
sequence: true, | |
scrolling: false | |
}; | |
// data | |
var size; // layout container size | |
var index; // iterator | |
var cellRatios; // integer ratios of elements | |
var gridArea; // summ of all elements areas. used for calculation of the gridSize | |
var grid; // binary grid array. 0 means empty cell - 1 means occupied cell | |
var gridRatio; // aspect ratio of the grid | |
var gridSize; // size of the grid regarding to the gridArea parameter (not pixels) | |
var nodes; // array of all elements (nodes) | |
var node; // current element | |
var set = { // size and position of an element | |
size: [0, 0], | |
translate: [0, 0, 0.01] | |
}; | |
// returns true if element of size=size could be fitted in to the grid at coordinates [x, y] | |
function _canBeFitted(x, y, itemSize) { | |
for (var j = y; j < y + itemSize[1]; j++) { | |
for (var i = x; i < x + itemSize[0]; i++) { | |
if (i > grid.length - 1 || j > grid[0].length - 1 || grid[i][j] == 1) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
// markes occupied grid area with '1' | |
function _reservePlace(x, y, itemSize) { | |
for (var j = y; j < y + itemSize[1]; j++) { | |
for (var i = x; i < x + itemSize[0]; i++) { | |
grid[i][j] = 1; | |
} | |
} | |
} | |
// returns position on the grid if element could be fitted or undefined otherwise | |
function _tryToFit(itemSize) { | |
for (var j = 0; j < grid[0].length; j++) { | |
for (var i = 0; i < grid.length; i++) { | |
if (_canBeFitted(i, j, itemSize)) { | |
_reservePlace(i, j, itemSize); | |
return [i, j]; | |
} | |
} | |
} | |
return undefined; | |
} | |
// calculate recursively next possible position on the grid for the given element | |
function _calculatePosition(size) { | |
var gridPosition = _tryToFit(size); | |
if (!gridPosition) { // make grid bigger (add additional row and col) | |
grid.forEach(function (col) { //adding extra row | |
col.push(0); | |
}); | |
var col = []; | |
grid[0].forEach(function () { //adding extra col | |
col.push(0); | |
}); | |
grid.push(col); | |
return _calculatePosition(size); | |
} | |
return gridPosition; | |
} | |
// calculate canvas size = min size of a rectangle that can fit all elements | |
function _calculateCanvasSize() { | |
var canvasSize = [0, 0]; | |
for (var i = 0; i < nodes.length; i++) { | |
canvasSize[0] = Math.max(nodes[i].gridPosition[0] + nodes[i].aspectRatio, canvasSize[0]); | |
canvasSize[1] = Math.max(nodes[i].gridPosition[1] + 1, canvasSize[1]); | |
} | |
return canvasSize; | |
} | |
// height align calculation | |
function _alignHeight(scale, viewSize, canvasSize) { | |
return (viewSize[1] - canvasSize[1] * scale) / (canvasSize[1] + 1); | |
} | |
// width align calculation | |
function _alignWidth(scale, viewSize, canvasSize) { | |
var gutterInRow = []; | |
var freeSpaceInRow = []; | |
var numCellsInRow = []; | |
var lastCellInRow = []; | |
for (var i = 0; i < canvasSize[1]; i++) { | |
numCellsInRow.push(0); | |
freeSpaceInRow.push(0); | |
gutterInRow.push(0); | |
lastCellInRow.push(null); | |
} | |
for (var i = 0; i < nodes.length; i++) { | |
var row = nodes[i].gridPosition[1]; | |
numCellsInRow[row] += 1; | |
nodes[i]['positionInRow'] = numCellsInRow[row]; | |
lastCellInRow[row] = (lastCellInRow[row] === null || nodes[i].gridPosition[0] > lastCellInRow[row].gridPosition[0]) ? nodes[i] : lastCellInRow[row]; | |
} | |
freeSpaceInRow = lastCellInRow.map(function (cell) { | |
return viewSize[0] - (cell.gridPosition[0] + cell.aspectRatio) * scale; | |
}) | |
for (var i = 0; i < freeSpaceInRow.length; i++) { | |
gutterInRow[i] = Math.floor(freeSpaceInRow[i] / (numCellsInRow[i] + 1)); | |
} | |
return gutterInRow; | |
} | |
// Layout function | |
function FixedMasonryLayout(context, options) { | |
// init | |
size = context.size; | |
cellRatios = options.cellRatios; | |
reflowTransition = options.reflowTransition; | |
// calculate total area of elements | |
gridArea = 0; | |
cellRatios.forEach(function (cellRatio) { | |
gridArea += cellRatio * 1; // we assume that all nodes have height = 1 | |
}) | |
// prepare grid | |
grid = []; | |
gridRatio = size[0] / size[1]; | |
gridSize = [Math.ceil(Math.sqrt(gridRatio * gridArea)), Math.ceil(Math.sqrt(gridArea / gridRatio))]; | |
for (var i = 0; i < gridSize[0]; i++) { | |
grid.push([]); | |
for (var j = 0; j < gridSize[1]; j++) { | |
grid[i].push(0); // init of the grid with '0' | |
} | |
} | |
// 1st loop; calculate positions | |
node = context.next(); | |
nodes = []; | |
index = 0; | |
while (node && (index < cellRatios.length)) { | |
node['aspectRatio'] = cellRatios[index]; | |
node['gridPosition'] = _calculatePosition([cellRatios[index], 1]); | |
nodes.push(node); | |
// Move to next renderable | |
index++; | |
node = context.next(); | |
} | |
var canvasSize = _calculateCanvasSize(); | |
var scale = (size[0] / size[1] > canvasSize[0] / canvasSize[1]) ? size[1] / canvasSize[1] : size[0] / canvasSize[0]; | |
var widthAlign = _alignWidth(scale, size, canvasSize); | |
var heightAlign = _alignHeight(scale, size, canvasSize); | |
// 2nd loop; set node options | |
nodes.forEach(function (node) { | |
set.size = [node.aspectRatio * scale, 1 * scale]; | |
set.translate[0] = node.gridPosition[0] * scale + widthAlign[node.gridPosition[1]] * node.positionInRow; | |
set.translate[1] = node.gridPosition[1] * scale + heightAlign * (node.gridPosition[1] + 1); | |
context.set(node, set); | |
}) | |
} | |
FixedMasonryLayout.Capabilities = capabilities; | |
module.exports = FixedMasonryLayout; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this is awesome :)