Algorithm for calculation and distribution of elements on the screen

Asked

Viewed 585 times

3

We are here at the company wanting to create a showcase with the effect of reorganizing style which is used by the stackexchange itself http://stackexchange.com/sites

I’ve been thinking about position calculation algorithms, but I’m not getting anything conclusive on how to rearrange items and calculate voids.

Any logic for this specific purpose?

2 answers

4

Take a look at this plugin http://isotope.metafizzy.co/, may solve your problem.

It has several layout options, but maybe the one Voc6e is interested in is the Masonry. In it the blocks of different size are embedded, as if it were a brick wall (hence the name). A documentation describes like this:

Masonry is the default layout in Isotope. The elements are organized intelligently within a vertical grid. For each element, the script calculates the best position within the grid.

The algorithm that determines the position of the elements is this one:

_masonryLayout : function( $elems ) {
    var instance = this,
        props = instance.masonry;

    $elems.each(function(){
        var $this  = $(this),
            //how many columns does this brick span
            colSpan = Math.ceil( $this.outerWidth(true) / props.columnWidth );
        colSpan = Math.min( colSpan, props.cols );

        if ( colSpan === 1 ) {
            // if brick spans only one column, just like singleMode
            instance._masonryPlaceBrick( $this, props.colYs );
        } else {
            // brick spans more than one column
            // how many different places could this brick fit horizontally
            var groupCount = props.cols + 1 - colSpan,
                groupY = [],
                groupColY,
                i;

            // for each group potential horizontal position
            for ( i=0; i < groupCount; i++ ) {
                // make an array of colY values for that one group
                groupColY = props.colYs.slice( i, i+colSpan );
                // and get the max value of the array
                groupY[i] = Math.max.apply( Math, groupColY );
            }
          instance._masonryPlaceBrick( $this, groupY );
        }
    });
}

From what I could understand what’s going on:

  1. The grid is started with a list of initial positions of each column. Ex: [0, 0, 0, 0]
  2. For each element, the algorithm determines which columns it would fit into, based on width. So, if there are 5 columns, an element with a width of 3 columns, it can enter columns 1 or 2.
  3. In each of the columns possible to fit the element, it is verified which place louder where it is possible to insert it.
  4. The element is inserted in the highest possible spaces, and the first available position of the columns it occupies is updated.

It’s a weird process to explain, but very simple to visualize, if you see the examples on the Isotope website. Or, better yet, if I ride one for you and poke at it.

  • 1

    Hello Leandro, very top this plugin. It will serve as reference also for future works, but I liked masonry more for being free and much simpler

2


I recommend using Masonry for this, it’s very simple: http://masonry.desandro.com/

It does not depend on jQuery, but can be used integrated with it.

Source code:

/*!
 * Masonry v3.1.4
 * Cascading grid layout library
 * http://masonry.desandro.com
 * MIT License
 * by David DeSandro
 */

( function( window ) {

'use strict';

// -------------------------- helpers -------------------------- //

var indexOf = Array.prototype.indexOf ?
  function( items, value ) {
    return items.indexOf( value );
  } :
  function ( items, value ) {
    for ( var i=0, len = items.length; i < len; i++ ) {
      var item = items[i];
      if ( item === value ) {
        return i;
      }
    }
    return -1;
  };

// -------------------------- masonryDefinition -------------------------- //

// used for AMD definition and requires
function masonryDefinition( Outlayer, getSize ) {
  // create an Outlayer layout class
  var Masonry = Outlayer.create('masonry');

  Masonry.prototype._resetLayout = function() {
    this.getSize();
    this._getMeasurement( 'columnWidth', 'outerWidth' );
    this._getMeasurement( 'gutter', 'outerWidth' );
    this.measureColumns();

    // reset column Y
    var i = this.cols;
    this.colYs = [];
    while (i--) {
      this.colYs.push( 0 );
    }

    this.maxY = 0;
  };

  Masonry.prototype.measureColumns = function() {
    this.getContainerWidth();
    // if columnWidth is 0, default to outerWidth of first item
    if ( !this.columnWidth ) {
      var firstItem = this.items[0];
      var firstItemElem = firstItem && firstItem.element;
      // columnWidth fall back to item of first element
      this.columnWidth = firstItemElem && getSize( firstItemElem ).outerWidth ||
        // if first elem has no width, default to size of container
        this.containerWidth;
    }

    this.columnWidth += this.gutter;

    this.cols = Math.floor( ( this.containerWidth + this.gutter ) / this.columnWidth );
    this.cols = Math.max( this.cols, 1 );
  };

  Masonry.prototype.getContainerWidth = function() {
    // container is parent if fit width
    var container = this.options.isFitWidth ? this.element.parentNode : this.element;
    // check that this.size and size are there
    // IE8 triggers resize on body size change, so they might not be
    var size = getSize( container );
    this.containerWidth = size && size.innerWidth;
  };

  Masonry.prototype._getItemLayoutPosition = function( item ) {
    item.getSize();
    // how many columns does this brick span
    var remainder = item.size.outerWidth % this.columnWidth;
    var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil';
    // round if off by 1 pixel, otherwise use ceil
    var colSpan = Math[ mathMethod ]( item.size.outerWidth / this.columnWidth );
    colSpan = Math.min( colSpan, this.cols );

    var colGroup = this._getColGroup( colSpan );
    // get the minimum Y value from the columns
    var minimumY = Math.min.apply( Math, colGroup );
    var shortColIndex = indexOf( colGroup, minimumY );

    // position the brick
    var position = {
      x: this.columnWidth * shortColIndex,
      y: minimumY
    };

    // apply setHeight to necessary columns
    var setHeight = minimumY + item.size.outerHeight;
    var setSpan = this.cols + 1 - colGroup.length;
    for ( var i = 0; i < setSpan; i++ ) {
      this.colYs[ shortColIndex + i ] = setHeight;
    }

    return position;
  };

  /**
   * @param {Number} colSpan - number of columns the element spans
   * @returns {Array} colGroup
   */
  Masonry.prototype._getColGroup = function( colSpan ) {
    if ( colSpan < 2 ) {
      // if brick spans only one column, use all the column Ys
      return this.colYs;
    }

    var colGroup = [];
    // how many different places could this brick fit horizontally
    var groupCount = this.cols + 1 - colSpan;
    // for each group potential horizontal position
    for ( var i = 0; i < groupCount; i++ ) {
      // make an array of colY values for that one group
      var groupColYs = this.colYs.slice( i, i + colSpan );
      // and get the max value of the array
      colGroup[i] = Math.max.apply( Math, groupColYs );
    }
    return colGroup;
  };

  Masonry.prototype._manageStamp = function( stamp ) {
    var stampSize = getSize( stamp );
    var offset = this._getElementOffset( stamp );
    // get the columns that this stamp affects
    var firstX = this.options.isOriginLeft ? offset.left : offset.right;
    var lastX = firstX + stampSize.outerWidth;
    var firstCol = Math.floor( firstX / this.columnWidth );
    firstCol = Math.max( 0, firstCol );
    var lastCol = Math.floor( lastX / this.columnWidth );
    // lastCol should not go over if multiple of columnWidth #425
    lastCol -= lastX % this.columnWidth ? 0 : 1;
    lastCol = Math.min( this.cols - 1, lastCol );
    // set colYs to bottom of the stamp
    var stampMaxY = ( this.options.isOriginTop ? offset.top : offset.bottom ) +
      stampSize.outerHeight;
    for ( var i = firstCol; i <= lastCol; i++ ) {
      this.colYs[i] = Math.max( stampMaxY, this.colYs[i] );
    }
  };

  Masonry.prototype._getContainerSize = function() {
    this.maxY = Math.max.apply( Math, this.colYs );
    var size = {
      height: this.maxY
    };

    if ( this.options.isFitWidth ) {
      size.width = this._getContainerFitWidth();
    }

    return size;
  };

  Masonry.prototype._getContainerFitWidth = function() {
    var unusedCols = 0;
    // count unused columns
    var i = this.cols;
    while ( --i ) {
      if ( this.colYs[i] !== 0 ) {
        break;
      }
      unusedCols++;
    }
    // fit container to columns that have been used
    return ( this.cols - unusedCols ) * this.columnWidth - this.gutter;
  };

  // debounced, layout on resize
  // HEADS UP this overwrites Outlayer.resize
  // Any changes in Outlayer.resize need to be manually added here
  Masonry.prototype.resize = function() {
    // don't trigger if size did not change
    var previousWidth = this.containerWidth;
    this.getContainerWidth();
    if ( previousWidth === this.containerWidth ) {
      return;
    }

    this.layout();
  };

  return Masonry;
}

// -------------------------- transport -------------------------- //

if ( typeof define === 'function' && define.amd ) {
  // AMD
  define( [
      'outlayer/outlayer',
      'get-size/get-size'
    ],
    masonryDefinition );
} else {
  // browser global
  window.Masonry = masonryDefinition(
    window.Outlayer,
    window.getSize
  );
}

})( window );
  • Masonry code is considerably simpler than Isotope code: https://github.com/desandro/masonry/blob/master/masonry.js

  • @J.Bruni yes, I just wanted to give him a second option, I think the Isotope cool, but sometimes you don’t need all his resources.

  • 1

    in case being simpler is a tremendous head start, since he’s interested in algorithm (only if he wants + resources, then yes, study the code of the Isotope... but surely it will be better to master the simple algorithm before leaving for the complex...)

  • @J.Bruni let’s see what Douglas will find, anyway I gave +1 to Leandro because his answer was accurate without doubt.

  • 1

    Even if this link is a good suggestion, this reply will not be valid if one day the link ceases to work. So, because it’s important for the community to have content right here, you’d better respond with more complete answers. Can you elaborate more your answer? A summary of the content of the link would help a lot! Learn more on this item in our Community FAQ: Answers that only contain links are good?

  • 1

    Taking advantage: I find masonry better to study than Isotope (which is based on masonry and came after).

  • 1

    Hello Gabriel, thank you so much for suggestion 0/ Saved my job here and my time too. A great plugin =)

Show 2 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.