Skip to content

Instantly share code, notes, and snippets.

@Qvatra
Created March 26, 2015 10:50
Show Gist options
  • Save Qvatra/65d481ce0596e6f1b012 to your computer and use it in GitHub Desktop.
Save Qvatra/65d481ce0596e6f1b012 to your computer and use it in GitHub Desktop.
fixed masonry layout for famous-flex LayoutController
/**
* 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;
});
@BodhiHu
Copy link

BodhiHu commented Mar 26, 2015

this is awesome :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment