March 30, 2015

Carousel (Slide Show) module for famo.us

Slide Show module is written in javaScript using famo.us framework.
The module is a modification of 'sliderShow' module for famo.us.
demo of non-modified version (Note: This demo works on old version of famo.us)

List of changes:
1) fixed bug with using old version of famo.us (otherwise doesn't work at all).
2) fixed bug: instead of Modifier use StateModifier.
3) changed CSS.
4) changed constructor feed parameters.
5) adjustable nav. bubble radius.
6) solved positioning bug for bubbles.
7) fixed translation bug: using translate(x, y,0.01) instead of translate(x,y,0).
8) adjustable size of the sliderShow container.


var slideshow = new Slidershow({
   width: window.innerWidth,
   height: 500,
   transition: {duration: 600, curve: 'easeInOut' },
   navDotRadius: window.innerWidth/100,
   navDotGap: window.innerWidth / 75,
   sliders: renderablesToShow
});

GISTS (modified version):
slidershow,
css

define(function (require, exports, module) {
 'use strict';

 var Surface = require('famous/core/Surface');
 var StateModifier = require('famous/modifiers/stateModifier');
 var Transform = require('famous/core/Transform');
 var EventHandler = require('famous/core/EventHandler');
 var OptionsManager = require('famous/core/OptionsManager');
 var View = require('famous/core/View');
 var ContainerSurface = require('famous/surfaces/ContainerSurface');
 var Utility = require('famous/utilities/Utility');

 function Slidershow(options) {
  this.options = Object.create(Slidershow.DEFAULT_OPTIONS);
  this._optionsManager = new OptionsManager(this.options);

  if (options) this.setOptions(options);

  this.total = this.options.sliders.length;
  this.page = 1;
  this._sliders = [];
  this._rwdDelay = undefined;
  this._currentSlideId = 0;
  this._navigationSurf = [];

  this._eventOutput = new EventHandler();
  EventHandler.setOutputHandler(this, this._eventOutput);

  _createSlider.call(this);
 }

 Slidershow.DEFAULT_OPTIONS = {
  width: window.innerWidth,
  height: 500,
  transition: {
   duration: 600,
   curve: 'easeInOut'
  },
  navDotRadius: window.innerWidth/100,
  navDotGap: window.innerWidth / 75
 };

 function _createSlider() {
  this.container = new ContainerSurface({
   size: [(this.options.width === window.innerWidth ? undefined : this.options.width), this.options.height],
   properties: {
    overflow: 'hidden',
    backgroundColor: 'rgb(33, 33, 33)'
   }
  });

  this.sliderContainer = new ContainerSurface({
   size: [(this.options.width === window.innerWidth ? undefined : this.options.width), this.options.height],
   properties: {
    overflow: 'hidden'
   }
  });

  // Create slide items
  _createSlideItem.call(this);

  // Navigation
  if (this.total > 1) _createNavigation.call(this);

  this.container.add(this.sliderContainer);

  if (this.options.width === window.innerWidth) {
   window.addEventListener('resize', function () {
    this.options.width = window.innerWidth;
   }.bind(this), false);
  }
 }

 function _createNavButton(direction) {     // Prev, Next button
  var container, surf, mod;

  container = new ContainerSurface({
   size: [true, undefined],
   properties: {
    background: 'rgba(33,33,33,0.4)',
    padding: '2em'
   }
  });
  surf = new Surface({
   size: [true, true],
   content: '',
   classes: ['projects-assets-nav-button']
  });
  mod = new StateModifier({
   align: [0.5, 0.5],
   origin: [0.5, 0.5]
  });
  container.add(mod).add(surf);

  mod = new StateModifier({
   align: [(direction === 'prev' ? 0 : 1), 0.5],
   origin: [(direction === 'prev' ? 0 : 1), 0.5]
  });

  container.on('click', function (e) {
   this.slidingPage(direction);
  }.bind(this));

  this.container.add(mod).add(container);
 }

 function _createNavBubble(bubble, view) {
  var surf, mod;

  surf = new Surface({
   classes: ['projects-assets-bubble-button'],
   properties: {
    borderWidth: this.options.navDotRadius + 'px',
    borderRadius: bubble === 0 ? 2 * this.options.navDotRadius + 'px' : this.options.navDotRadius + 'px',
    padding: bubble === 0 ? this.options.navDotRadius + 'px' : '0px'
   }
  });

  var dist = (bubble != 0) ? bubble * (this.options.navDotRadius * 2 + this.options.navDotGap) - this.options.navDotRadius : -2 * this.options.navDotRadius;

  mod = new StateModifier({
   size: (bubble === 0 ? [this.options.navDotRadius * 3, this.options.navDotRadius * 3] : [this.options.navDotRadius, this.options.navDotRadius]),
   transform: Transform.translate(dist, 0, 0.01),
   origin: [0, 0.5]
  });
  view._add(mod).add(surf);

  surf.on('click', function (e) {
   this.jumpToSlide(bubble);
  }.bind(this));

  this._navigationSurf.push({ view: view, surf: surf });
 }

 function _createNavigation() {
  var surf, mod, container, view, i;

  _createNavButton.call(this, 'prev');
  _createNavButton.call(this, 'next');

  container = new ContainerSurface({
   size: [(this.total - 1) * (this.options.navDotGap + 2 * this.options.navDotRadius), this.options.navDotRadius]
  });

  view = new View();
  for (i = 0; i < this.total; i++) {
   _createNavBubble.call(this, i, view);
  }
  container.add(view);

  mod = new StateModifier({
   origin: [0.5, 0.5],
   align: [0.5, 0.9]
  });

  this.container.add(mod).add(container);
 }

 function _createSlideItem() {
  for (var i = (this.total - 1) ; i >= 0; i--) {

   var container = new ContainerSurface({ size: [this.options.width, this.options.height] });

   if (typeof this.options.sliders[i] === 'object') container.add(this.options.sliders[i]);

   var view = new View({});

   var dx; // calculate nitial x position of the slide
   if (i === 0) { //first item
    dx = 0;
   } else {
    if (i === 1) { //next item
     dx = this.options.width;
    } else if (i === this.total - 1) { // prev item
     dx = -this.options.width;
    } else {
     dx = -10 * this.options.width; //all other items
    }
   }

   view.add(new StateModifier({
    transform: Transform.translate(dx, 0, 0.01)
   })).add(container);

   this._sliders.push(view);
   this.sliderContainer.add(view);
  }
 }

 function _resetNavigation(slideId) {
  var activated = {
   borderRadius: 2 * this.options.navDotRadius + 'px',
   padding: this.options.navDotRadius + 'px'
  }
  var deactivated = {
   borderRadius: this.options.navDotRadius + 'px',
   padding: '0px'
  }
  var nav = this._navigationSurf[this._currentSlideId];
  nav.surf.setOptions({ properties: deactivated });
  nav.view._node._child[this._currentSlideId].get().setSize([this.options.navDotRadius, this.options.navDotRadius]);
  var dist = this._currentSlideId * (this.options.navDotRadius * 2 + this.options.navDotGap) - this.options.navDotRadius;
  nav.view._node._child[this._currentSlideId].get().setTransform(Transform.translate(dist, 0, 0.01));
  nav.view._node._child[this._currentSlideId].get().setOrigin([0, 0.5]);

  nav = this._navigationSurf[slideId];
  nav.surf.setOptions({ properties: activated });
  nav.view._node._child[slideId].get().setSize([this.options.navDotRadius * 3, this.options.navDotRadius * 3]);
  var dist = slideId * (this.options.navDotRadius * 2 + this.options.navDotGap) - 2 * this.options.navDotRadius;
  nav.view._node._child[slideId].get().setTransform(Transform.translate(dist, 0, 0.01));
  nav.view._node._child[slideId].get().setOrigin([0, 0.5]);

  this._currentSlideId = slideId;
 }

 Slidershow.prototype.getTotal = function getTotal() {
  return this.total;
 };

 Slidershow.prototype.getPage = function getPage() {
  return this.page;
 };

 Slidershow.prototype.jumpToSlide = function jumpToSlide(slide, direction /* only for prev, next */) {
  var current = this.total - this.page >= this.total ?
      this.total - 1 : this.total - this.page <= 0 ?
      0 : this.total - this.page,
   next = current - 1 < 0 ? this.total - 1 : current - 1,
   prev = current + 1 >= this.total ? 0 : current + 1,
   feature = this.total - (slide + 1);

  if (current === feature) return true;
  var direction = direction || 'feature';

  _resetNavigation.call(this, slide);

  if (Math.abs(current - feature) !== (this.total - 1)) {
   this._sliders[next]._node.get().setTransform(
     Transform.translate(this.options.width * -10, 0, 0.01),
     undefined
   );
   this._sliders[prev]._node.get().setTransform(
     Transform.translate(this.options.width * -10, 0, 0.01),
     undefined
   );
  }

  this._sliders[feature]._node.get().setTransform(
    Transform.translate(this.options.width * (current > feature ? 1 : -1), 0, 0.01)
  );
  this._sliders[feature]._node.get().setTransform(
    Transform.translate(0, 0, 0.01),
    this.options.transition
  );
  this._sliders[current]._node.get().setTransform(
    Transform.translate(this.options.width * (current > feature ? -1 : 1), 0, 0.01),
    this.options.transition
  );

  if (Math.abs(current - feature) !== (this.total - 1)) {
   next = feature - 1 < 0 ? this.total - 1 : feature - 1;
   prev = feature + 1 >= this.total ? 0 : feature + 1;

   if (current > feature) {
    this._sliders[next]._node.get().setTransform(
      Transform.translate(this.options.width, 0, 0.01),
      undefined
    );
   } else {
    this._sliders[prev]._node.get().setTransform(
      Transform.translate(this.options.width * -1, 0, 0.01),
      undefined
    );
   }
  }

  this.page = slide + 1;
  this.page = (this.page > this.total) ? 1 : (this.page < 1) ? this.total : this.page;

  this._eventOutput.emit('pageChange', { page: this.page, direction: direction });
 };

 Slidershow.prototype.slidingPage = function slidingPage(direction) {
  if (direction === 'next') {
   this.jumpToSlide((this.page >= this.total) ? 0 : this.page, direction);
  } else if (direction === 'prev') {
   this.jumpToSlide((this.page <= 1) ? this.total - 1 : this.page - 2, direction);
  }
 };

 Slidershow.prototype.setOptions = function setOptions(options) {
  if (options.sliders === undefined || options.sliders.length === 0) {
   options.sliders = [];
  }
  if (options.width === undefined || typeof options.width !== 'number') {
   options.width = window.innerWidth;
  }
  if (options.height === undefined || typeof options.height !== 'number') {
   options.height = 500;
  }
  if (options.transition === undefined || typeof options.transition !== 'object') {
   options.transition = {
    duration: 600,
    curve: 'easeInOut'
   };
  }

  this._optionsManager.setOptions(options);
 };

 Slidershow.prototype.render = function render() {
  if (this.total === 0) return null;

  return [
    {
     target: this.container.render()
    }
  ];
 };

 module.exports = Slidershow;
});
CSS:

.projects-assets-nav-button {
 text-align: center;
 color: rgba(255,255,255,0.8);
 cursor: pointer;
 font-size: 4em;
 line-height: 1em;
}

.projects-assets-nav-button:hover {
 color: rgb(252, 236, 60);
}
 
.projects-assets-bubble-button {
 border-color: rgba(252, 236, 60, 0.8);
 cursor: pointer;
 box-shadow: 10px 6px 35px 0px rgb(33, 33, 33);
 border-style: solid;
}

.projects-assets-bubble-button:hover {
 border-color: rgb(252, 236, 60);
}

March 26, 2015

JavaScript Fixed Masonry responsive layout.

The idea of this responsive layout is to present all surfaces on a custom viewport preserving the aspect ratios and keeping all surfaces as big as possible.

The codePen of the working responsive layout example is here.

This layout function work with famo.us framework and  famous-flex lib.
Gist of the layout function is here.

/**
 * 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;
});