December 15, 2015

Pure css3 responsive masonry layout for items with fixed width.

Recently I had a task of creating Masonry style layout for items with fixed width but not fixed height. This suppose to be used within Polymer element to layout sub elements in masonry style. I didn't want to introduce any external library to do that so I decided to go with pure css. In Polymer my custom css styles would be scoped for my host element only and not visible for 'outside world'. Apparently it's not hard to do with just several lines of css:

the codepen example is here;

.masonry {
   -moz-column-gap: 1.5em;
   -webkit-column-gap: 1.5em;
   column-gap: 1.5em;
   font-size: .85em;
   width: 95%;
   margin: 3em auto;
}

.item {
   display: inline-block;
   width: 200px;
   background: silver;
   padding: 1em;
   margin: 0 0 1.5em;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
   -webkit-box-sizing: border-box;
}

@media only screen and (min-width: 400px) { .masonry { -moz-column-count: 2; -webkit-column-count: 2; column-count: 2; }}

@media only screen and (min-width: 700px) { .masonry { -moz-column-count: 3; -webkit-column-count: 3; column-count: 3; }}

@media only screen and (min-width: 900px) { .masonry { -moz-column-count: 4; -webkit-column-count: 4; column-count: 4; }}

@media only screen and (min-width: 1100px) { .masonry { -moz-column-count: 5; -webkit-column-count: 5; column-count: 5; }}
and the usage is:

<class=masonry parent>
<class=item child>
<content/>
      ... 
</class=item child>
</class=masonry parent>

August 21, 2015

How to make website look like an app

Things that we want to achieve:
1) make customizable shortcut to a device’s desktop.
2) hide browser navigation bar

Lets start with creating a shortcut. for safari:

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" sizes="72x72" href="72.png">
<link rel="apple-touch-icon" sizes="114x114" href="114.png">
<link rel="apple-touch-icon" href="57.png">
<link rel="apple-touch-startup-image" href="splash.png">
for chrome:

<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" sizes="72x72" href="72.png">
<link rel="icon" sizes="114x114" href="114.png">
<link rel="icon" sizes="192x192" href="192.png">
<link rel="icon" sizes="57x57" href="57.png">
for IE10:

<meta name="name" content="" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="msapplication-square70x70logo" content="72.png" />
<meta name="msapplication-square150x150logo" content="150.png" />
<meta name="msapplication-wide310x150logo" content="310x150.png" />
<meta name="msapplication-square310x310logo" content="310.png" />
if you need to support other browsers then it might be a good idea to use manup.js polifill library. In this case you have to create manifest file:

{
  "name": "app name",
  "short_name": "app",
  "icons": [{
        "src": "64.png",
        "sizes": "64x64" 
    }, {
        "src": "128.png",
        "sizes": "128x128"    
    }],
  "start_url": "index.html",
  "display": "fullscreen",
  "orientation": "landscape"
}
Next I will explain how to solve safari full screen mode problem You have created a shortcut for your website and opened it. You can see safari in full screen mode BUT as soon as you navigate using href links safari returns to default mode with a navigation bar. My first clue was to use target=”_parent” in all links which should force new pages open in the current frame but for some reason that doesn’t work for safari. solution I did find in here. Actually only thing you should do to fix that is to add some javascript code to your page:

(function(document,navigator,standalone) {   
   if ((standalone in navigator) && navigator[standalone]) {      
      var curnode, location=document.location, stop=/^(a|html)$/i;
         document.addEventListener('click', function(e) {
            curnode=e.target;
            while (!(stop).test(curnode.nodeName)) {
               curnode=curnode.parentNode;
            }            
            if('href' in curnode && ( curnode.href.indexOf('http') || ~curnode.href.indexOf(location.host) ) ) {
               e.preventDefault();
               location.href = curnode.href;
            }
      },false);
   }
})(document,window.navigator,'standalone');

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

February 4, 2015

How to avoid blurring of the text in Famo.us during transition in Zdirection

Famo.us is a new JavaScript framework that includes an open source 3D layout engine fully integrated with a 3D physics animation engine that can render to DOM, Canvas, or WebGL.
Famo.us is a great framework to create really cool UI’s with 2d/3d animations of any kind. However I’ve noticed some bugs (which is ok as the framework is new) and some limitations due to the fact that some browsers could not correctly apply 3dmatrices. One of the hugest limitation on my opinion is blurring of the text during z-translate of the elements. This means that we simply can not use perspective transitions if our elements contain text.
I was trying to find workaround and as a result came up with the following solution:

1) do actual transition in z-direction
2)  scale projection of the surface back to its original size
3) resize our element to fit needed projection size
4) resize font

here is the codepen link

define(function (require, exports, module) {
    var Engine = require('famous/core/Engine');
    var Surface = require('famous/core/Surface');
    var Transform = require('famous/core/Transform');
    var StateModifier = require('famous/modifiers/StateModifier');
    var Transitionable = require('famous/transitions/Transitionable');

    var perspective = 1000;
    var fontValue = 100;      //initially font-size is 100%
    var surfSize = [100, 100];

    var mainContext = Engine.createContext();
    mainContext.setPerspective(perspective);
    var transitionable = new Transitionable(0);

    var mySurface = new Surface({
        size: surfSize,
        properties: {
            backgroundColor: 'red',
            textAlign: 'center',
            color: 'white',
            fontSize: fontValue + '%',
            lineHeight: surfSize[1] + 'px'
        },
        content: 'Click Me'
    });

    var transitionModifier = new StateModifier({
        origin: [.5, .5],
        align: [.5, .5],
        transform: Transform.translate(0, 0, 0.01)
    });

    mainContext.add(transitionModifier).add(mySurface);

    function translateZ(dist, transition) {
        transitionable.reset(0);
        transitionable.set(dist, transition);

        function prerender() {
            var currentDist = transitionable.get();
            //perspective formula: dist = perspective(1 - 1/scaleFactor)
            var currentScale = 1 / (1 - currentDist / perspective);
            var currentSize = [surfSize[0] * currentScale, surfSize[1] * currentScale];
            var currentFontValue = fontValue * currentScale;  

            //f.e: bring closer => make projection scaleFactor times bigger
            var transitionTransform = Transform.translate(0, 0, currentDist);
            //scaling back to avoid text blurring            
            var scaleTransform = Transform.scale(1 / currentScale, 1 / currentScale, 1);
            transitionModifier.setTransform(Transform.multiply(transitionTransform, scaleTransform));

            mySurface.setSize(currentSize); //resize to get correct projection size                                                     
            mySurface.setOptions({
                properties: {
                    fontSize: currentFontValue + '%', //resizing font;                                                  
                    lineHeight: currentSize[1] + 'px' //align text;                                                  
                }
            })

            if (currentDist === dist) {
                Engine.removeListener('prerender', prerender);
            }
        }

        Engine.on('prerender', prerender);
    }

    Engine.on('click', function () {
        translateZ(750, { curve: 'easeOutBounce', duration: 2000 });
    });
});
This approach could be extended to use ContainerSurfaces as a containers where you will set fontSize in %. ContainerSurface will propagate this property to its children.

January 27, 2015

Building an Ionic hybrid mobile app with TypeScript

Presentation based on experiences of building a hybrid mobile app using the Ionic framework, Cordova, AngularJS and TypeScript.

The app we have built is for Android and iOS.