<!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"
};
}