<!DOCTYPE html>
<html ng-app="app">
<head>
  <meta charset="utf-8" />
  <title>Dynamic Animations</title>
  <link href="style.css" rel="stylesheet" />
</head>
<body>
  <div ng-controller="MainController as vm" class="view">
    <image-board class="board" timeline></image-board>
    <settings-panel></settings-panel>
  </div>

  <!-- vendor -->
  <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/1.14.1/TweenMax.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular-animate.min.js"></script>

  <!-- app -->
  <script src="app.js"></script>
  <script src="animation.js"></script>
  <script src="board.js"></script>
  <script src="point.js"></script>
  <script src="tile.js"></script>
  <script src="timeline.js"></script>

</body>
</html>
html {
  position: relative;
  min-height: 100%;
}

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 12px;
  margin: 0;
}

label {
  display: block;
  font-weight: bold;
  color: #1d1d1d;
  line-height: 1.5;
  margin-bottom: 0;
}

select,
input[type=text],
input[type=number] {
  display: block;
  margin: 0 0 10px 0;
  padding: 4px;
  border-radius: 0;
}

select: disabled {
  color: #777;
  background-color: #aaa;
}

input[type=checkbox] {
  padding: 0;
  margin: 4px 0 0;
}

input[type=checkbox] + label {
  display: inline-block;
  cursor: pointer;
}

input[type=submit] {
  font-size: 13px;
  background-color: #333;
  color: #ccc;
  border: 1px solid #888;
  padding: 8px 18px;
  cursor: pointer;
  margin: 10px 0;
}

input[type=submit]: disabled {
  color: #555;
  cursor: default;
  border-color: #555;
}

h3 {
  margin: 0;
}

.view {
  position: absolute;
  height: 100%;
  width: 100%;
  background: #333;
  overflow: hidden;
}

.settings-panel {
  background: rgba(120, 120, 120, 0.7);
  padding: 6px 12px;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 20;
}

.info {
  color: #1d1d1d;
  border-bottom: 1px solid #888;
  margin-bottom: 8px;
  padding: 4px 0;
  font-size: 13px;
  font-weight: bold;
}

.board {
  height: 500px;
  width: 500px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -250px;
  margin-left: -250px;
}

.point {
  position: absolute;
  min-width: 10px;
  padding: 3px 4px;
  font-size: 12px;
  font-weight: bold;
  line-height: 1;
  color: #222;
  text-align: center;
  white-space: nowrap;
  vertical-align: baseline;
  background-color: blanchedalmond;
  border-radius: 10px;
  z-index: 20;
}

.line {
  position: absolute;
  border-top: 1px solid orange;
  height: 0;
  width: 0;
  z-index: 15;
}

.tile {
  position: absolute;
}

.flip-tile {
  width: 100%;
  height: 100%;
  position: absolute;
}

.tile-back {
  background: url(http://gravatar.com/avatar/5a224f121f96bd037bf6c1c1e2b686fb?s=500) no-repeat;
  position: absolute;
  width: 100%;
  height: 100%;
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  outline: 1px solid transparent;
}

.tile-front {
  width: 100%;
  height: 100%;
  background: green;
  position: absolute;
  border: 1px solid #333;
  outline: 1px solid transparent;
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}

.hit-tile {
  width: 100%;
  height: 100%;
  position: absolute;
  z-index: 1000;
}
Demonstrates how to flip a card in the direction of the mouse and moving an element
along a curve using new animation features in Angular 1.3. Animations are powered 
by the [GreenSock Animation Platform][1].

[GreenSock Forum Post][2]

[1]: http://greensock.com/
[2]: http://greensock.com/forums/topic/10822-easier-animations-in-angular-13/
angular
  .module("app")
  .animation(".tile", introAnimation)
  .animation(".flip-tile", flipAnimation);


/*

!!! If updating to v1.4+, remove className from animate methods

*/

// ========================================================================
//  FLIP ANIMATION
// ========================================================================
function flipAnimation() {

  var zIndex = 30;

  return {

    beforeAnimate: function(element, className, from, to, done) {

      // Boost the z-index to prevent clipping from other
      // tiles and hide the bezier lines
      TweenMax.set(from.parent, {
        zIndex     : zIndex++,
        onComplete : done
      });
    },

    animate: function(element, className, from, to, done) {

      // We are tweening the children instead of the element
      // because of an IE 3D transform problem
      TweenMax.to(from.children, 0.6, {
        ease       : Power1.easeOut,
        force3D    : true,
        rotationX  : to.rotationX || "+=0",
        rotationY  : to.rotationY || "+=0",
        onComplete : done
      });
    }
  };
}

// ========================================================================
//  INTRO ANIMATION
// ========================================================================
function introAnimation() {

  var board    = null;
  var points   = null;
  var timeline = null;

  return {

    beforeEnter: function(element, done, options) {

      if (!timeline || !board || !points) {
        timeline = options.init.timeline;
        points   = options.init.points;
        board    = options.init.board;
      }

      if (options.first) {

        // The first tile will create the initial points that
        // are also used by the bezier-point directive
        for (var i = 1; i < options.points; i++) {

          var angle   = _.random(Math.PI * 2);
          var radiusX = _.random(board.radius, board.radiusX);
          var radiusY = _.random(board.radius, board.radiusY);

          points.push({
            x: board.center + Math.cos(angle) * radiusX,
            y: board.center + Math.sin(angle) * radiusY
          });
        }
      }

      // Add tile's final position to the points in a new array
      options.bezier.values = points.concat(options.bezier.values);

      TweenMax.set(element, {
        autoAlpha  : 0,
        scale      : 0,
        x          : points[0].x,
        y          : points[0].y,
        onComplete : done
      });
    },

    enter: function(element, done, options) {

      var time = _.random(2.25, 2.5);

      timeline.to(element, time, {
        autoAlpha  : 1,
        ease       : Sine.easeInOut,
        force3D    : true,
        scale      : 1,
        bezier     : options.bezier,
        onComplete : done
      }, "enter");

      // The last tile will start the animation
      if (options.last) {
        timeline.play();
        
        // Log out the coordinates to a table
        var values = _.initial(options.bezier.values);
        var table  = _.reduce(values, function(result, coord, i) {
          result[++i] = { x: +coord.x.toFixed(2), y: +coord.y.toFixed(2) };
          return result;
        }, {});
        
        console.log("\nBEZIER POINTS:");
        console.table && console.table(table);
      }
    }
  };
}





angular
  .module("app")
  .directive("imageBoard", imageBoard);

function imageBoard($window) {

  return {
    restrict: "EA",
    link: link,
    template: 
      '<bezier-point ng-repeat="point in points"' +
      '        next="points[$index + 1] || board"' +
      '        show="vm.show"' +
      '        num="{{$index + 1}}"' +
      '        x="{{point.x}}"' +
      '        y="{{point.y}}">' +
      '</bezier-point>'
  };

  function link(scope, element, attrs) {

    $window.onresize = updateBounds;

    scope.board  = {};
    scope.points = [];

    updateBounds();

    function updateBounds() {

      var board  = element[0].getBoundingClientRect();
      var center = board.width / 2;

      _.assign(scope.board, {
        size    : board.width,
        center  : center,
        radius  : Math.sqrt(center * center * 2),
        radiusX : center + board.left,
        radiusY : center + board.top
      });
    }
  }
}
angular
  .module("app")
  .directive("bezierPoint", bezierPoint);

function bezierPoint() {

  return {
    restrict: "EA",
    link: link,
    scope: {
      num  : "@",
      next : "=",
      show : "=",
      x    : "@",
      y    : "@"
    },
    template: 
      '<div ng-show="show.points" class="point">{{num}}</div>' +
      '<div ng-show="show.lines" class="line"></div>'
  };

  function link(scope, element, attrs) {

    var line  = element.find(".line");
    var point = element.find(".point");

    var dx = (scope.next.x || scope.next.center) - scope.x;
    var dy = (scope.next.y || scope.next.center) - scope.y;

    var distance = Math.sqrt(dx * dx + dy * dy);
    var rotation = Math.atan2(dy, dx) * 180 / Math.PI;

    TweenMax.set(point, {
      x: scope.x,
      y: scope.y,
      xPercent: -50,
      yPercent: -50
    });

    TweenMax.set(line, {
      x: scope.x,
      y: scope.y,
      width: distance,
      rotation: rotation,
      transformOrigin: "0 0"
    });
  }
}
<div class="settings-panel">

  <div class="info">
    <h3>Create a bezier curve</h3>
    <br>Flip the tiles in the correct order
    <br>to reveal the picture!
    <br>
    <br>Tiles will flip in the direction 
    <br>of mouse movement.
    <br>
    <br>The coordinates for the points  
    <br>are displayed in the console.
  </div>

  <form name="form" ng-submit="vm.create()">

    <label>
      Grid ({{vm.grid.min}}-{{vm.grid.max}})
      <input type="number" 
             name="grid" 
             ng-model="vm.animation.gridsize" 
             min="{{vm.grid.min}}" 
             max="{{vm.grid.max}}" 
             required />
    </label>

    <label>
      Points ({{vm.animation.type.min}}-{{vm.animation.type.max}})
      <input type="number" 
             name="point" 
             ng-model="vm.animation.points" 
             min="{{vm.animation.type.min}}" 
             max="{{vm.animation.type.max}}" 
             required />
    </label>

    <label>
      Type
      <select ng-model="vm.animation.type" 
              ng-options="type.name for type in vm.types">
      </select>
    </label>

    <label>
      Thru Curviness
      <select ng-model="vm.animation.curviness" 
              ng-options="curve for curve in vm.curves" 
              ng-disabled="vm.animation.type.value !== 'thru'">
      </select>
    </label>

    <div>
      <input id="show-points" type="checkbox" ng-model="vm.show.points" />
      <label for="show-points">Show Points</label>
    </div>

    <div>
      <input id="show-lines" type="checkbox" ng-model="vm.show.lines" />
      <label for="show-lines">Show Lines</label>
    </div>

    <div>
      <input type="submit" value="Create" ng-disabled="form.$invalid" />
    </div>

  </form>
</div>

angular
  .module("app")
  .directive("tile", tile);

function tile($animate) {

  return {
    retstrict: "EA",
    link: link,
    scope: {
      size : "@",
      x    : "@",
      y    : "@"
    },
    template: 
      '<div class="flip-tile">' +
      '   <div class="tile-front"></div>' +
      '   <div class="tile-back"></div>' +
      '</div>' +
      '<div class="hit-tile"' +
      '     ng-mouseenter="setPosition($event.originalEvent)"' +
      '     ng-mousemove="checkHit($event.originalEvent)"' +
      '     ng-mouseleave="clearPosition()">' +
      '</div>'
  };

  function link(scope, element, attrs) {

    var flip  = element.find(".flip-tile");
    var front = element.find(".tile-front");
    var back  = element.find(".tile-back");

    var flipped = false;
    var startX  = null;
    var startY  = null;

    var threshold = 0.7;
    var hitMin    = scope.size * (1 - threshold) / 2;
    var hitMax    = scope.size - hitMin;

    TweenMax.set(element, {
      width  : scope.size,
      height : scope.size
    });

    TweenMax.set(back, {
      rotation  : _.sample([0, 180]),
      rotationY : -180,
      backgroundPosition: -scope.x + "px " + -scope.y + "px"
    });

    TweenMax.set([front, back], {
      backfaceVisibility   : "hidden",
      transformPerspective : 500,
    });

    // GreenSock object to get transformation values
    var transform = front[0]._gsTransform;

    scope.checkHit      = checkHit;
    scope.clearPosition = clearPosition;
    scope.setPosition   = setPosition;
    ////////

    function flipTile(changeX, changeY) {

      var reversed = Math.abs(transform.rotationY % 360) ? -1 : 1;
      var ratio    = Math.abs(changeX / changeY);

      var rotationX = changeY > 0 ? -180 : 180;
      var rotationY = changeX < 0 ? -180 : 180;

      var from = {
        parent   : element,
        children : [front, back]
      };

      var to = ratio < 1
        ? { rotationX: "+=" + rotationX * reversed }
        : { rotationY: "+=" + rotationY };

      $animate.animate(flip, from, to);
    }

    function checkHit($event) {

      if (flipped) { return; }

      var current = getPosition($event);

      // Check if the mouse is within our threshold range
      if (current.x > hitMin && current.x < hitMax &&
          current.y > hitMin && current.y < hitMax) {

        flipped = true;

        current.x -= startX;
        current.y -= startY;

        flipTile(current.x, current.y);
      }
    }

    function getPosition($event) {

      // Firefox doesn't have offset
      var hasOffset = !_.isUndefined($event.offsetX);

      return {
        x: hasOffset ? $event.offsetX : $event.layerX,
        y: hasOffset ? $event.offsetY : $event.layerY,
      };
    }

    function setPosition($event) {

      var current = getPosition($event);

      startX = current.x;
      startY = current.y;
    }

    function clearPosition() {
      flipped = false;
      startX  = null;
      startY  = null;
    }
  }
}


angular
  .module("app")
  .directive("timeline", timeline);

function timeline($animate, $compile, $timeout) {

  return {
    restrict: "A",
    link: link
  };

  function link(scope, element, attrs) {

    // From controller's view model
    var animation = scope.vm.animation;

    // GreenSock timeline
    scope.timeline = new TimelineMax({
      paused  : true,
      onStart : updateScope
    });

    scope.createAnimation  = createAnimation;
    scope.prepareAnimation = prepareAnimation;
    /////////

    function updateScope() {

      // Triggers a digest to display the bezier points and lines
      $timeout(function() {});
    }

    function prepareAnimation() {

      // Clear the timeline, tiles, and points
      scope.timeline.pause(0).clear();
      scope.points.length = 0;
      angular.element(".tile").remove();

      scope.createAnimation();
    }

    function createAnimation() {

      var gridsize  = animation.gridsize;
      var lastIndex = gridsize * gridsize - 1;
      var lastTile  = null;
      var tilesize  = Math.floor(scope.board.size / gridsize);

      for (var row = 0, index = 0; row < gridsize; row++) {
        for (var col = 0; col < gridsize; col++, index++) {

          var x = col * tilesize;
          var y = row * tilesize;

          var bezier = {
            curviness : animation.curviness,
            type      : animation.type.value,
            values    : [{ x: x, y: y }]
          };

          var init = {
            timeline : scope.timeline,
            board    : scope.board,
            points   : scope.points
          };

          var options = {
            bezier : bezier,
            first  : !index,
            last   : (index === lastIndex),
            init   : init,
            points : animation.points,
          };

          var tile = angular
            .element('<tile class="tile"></tile>')
            .attr({ size: tilesize, x: x, y: y });

          // Link the tile template with our scope
          $compile(tile)(scope);

          // Add animations to our paused timeline
          $animate.enter(tile, element, lastTile, options);

          lastTile = tile;
        }
      }
    }
  }
}



angular
  .module("app", ["ngAnimate"])
  .controller("MainController", MainController)
  .directive("settingsPanel", settingsPanel);

// ========================================================================
//  MAIN CONTROLLER
// ========================================================================
function MainController($scope) {

  // View model
  var vm = this;

  vm.create = create;
  vm.curves = [0, 1, 2, 3, 4];
  vm.grid   = { min: 3, max: 10 };
  vm.show   = { points: true, lines: true };

  vm.types = [
    { min: 2, max: 50, name: "Thru",      value: "thru"      },
    { min: 2, max: 50, name: "Soft",      value: "soft"      },
    { min: 3, max: 49, name: "Quadratic", value: "quadratic" },
    { min: 4, max: 49, name: "Cubic",     value: "cubic"     }
  ];

  vm.animation = {
    curviness : vm.curves[1],
    gridsize  : 5,
    points    : 7,
    type      : vm.types[0]
  };

  function create() {

    var points = vm.animation.points;
    var type   = vm.animation.type.value;

    // Adjust for any missing points
    if (type === "cubic") {
      var missingPoints = (points - 4) % 3;
      
      if (points < 4) vm.animation.points = 4;
      else if (missingPoints)
        vm.animation.points += 3 - missingPoints;
    }
    
    if (type === "quadratic") {
      
      if (points < 3) vm.animation.points = 3;
      else if ((points - 3) % 2)
        vm.animation.points++;
    }

    $scope.prepareAnimation();
  }
}

// ========================================================================
//  SETTINGS PANEL
// ========================================================================
function settingsPanel() {

  return {
    restrict: "EA",
    templateUrl: "settings.html"
  };
}