Skip to content
This repository has been archived by the owner on Nov 26, 2022. It is now read-only.

Latest commit

 

History

History
320 lines (241 loc) · 17.3 KB

README.md

File metadata and controls

320 lines (241 loc) · 17.3 KB

Designing an AngularJS Animation DSL

Summary

The goal is the development of a next-generation Animation layer for AngularJS with functionality and power to easily develop complex, rich user experiences. A new Animation Timeline API and a easy-to-use DSL will be derived based on experiments and explorations of real-world animation design samples; samples with UX as those as demonstrated in Material Design and the Polymer Topeka Quiz application.

The following three (3) Animation libraries will be considered:

For these experiments, several real-world UX applications were selected from Material Design: dsl_ideas

revealexplode

Each application will be implemented with the three (3) Animation libraries show above. These implementations will be used to identify core animation APIs and features. And that API will, in turn, be used to derive a XML-based Animation DSL.

Background

Meaningful transitions establish visual continuity with and during transitions between two visual states. Carefully choreographed motion design can effectively guide the user’s attention and focus through multiple steps of a process or procedure, avoid confusion when layouts change or elements are rearranged, and improve the overall beauty of the experience

Highlighted in Material Design is a user experience (UX) that is achieved using the 'Reveal' pattern. Reveal animations provide users visual continuity when you show or hide a group of UI elements... Reveal animations often involve complex choreographies with hierarchical timing; where elements are removed, added, or shared between the animation states.

The Koda application developed below uses the Explode-Reveal effect and provides a UX composed of complex motions.

Using Greensock (GSAP) Timelines

Leveraging the power of the Greensock Animation Platform (GSAP) and the timeline features provided in TimelineLite, we can implement choreographed animations in JavaScript and in our Timeline DSL.

DSL: is an acronym for 'domain specific language'. In our case the DSL is intended to be used in HTML markup as a designer-like facade to Timeline features; features that are themselves layered on top of AngularJS ngAnimate features.

From Javascript to DSL

Consider the Koda JavaScript use of the Greensock TimeLine API:

var zoom = new TimelineLite({paused:true}),
    unzoom = new TimelineLite({paused:true});

// Do zoom to show Kodaline details...

zoom.timeScale(1)
    .set($("#mask"),                               { zIndex:90, className:""})
    .set($("#details"),                            { height:162, opacity:0, width:162 } )
    .set($("#details"),                            { className:"" })
    .to( $("#details"),                       0.2, { opacity:1} )
    .to( $("#details"),                       0.3, { left:0, height:210, width:323 } )
    .addLabel("fullWdith")
    .to( $("#mask"),                          0.5, { opacity:0.80 },        "fullWidth-=0.3" )
    .to( $("#details"),                       0.3, { top:18, height:512 },  "fullWidth-=0.05" )
    .addLabel("slideIn")
    .set($("#details > #green"),                   { zIndex:92, opacity:1.0, top:21, className:"" })
    .to( $("#details > #green"),              0.2, { top:0 },                "slideIn" )
    .to( $("#details > #tile"),               0.6, { height:131 },           "fullWdith")
    .to( $("#details > #info"),               0.5, { height:56 },            "fullWdith+=0.2")
    .to( $("#details > #title > div.content"),0.8, { opacity:1 },            "fullWdith+=0.3")
    .to( $("#details > #pause"),              0.4, { opacity:1, scale:1.0 }, "fullWidth+=0.4")
    .to( $("#details > #info > div.content"), 1.0, { opacity:1 },            "fullWdith+=0.6");

We can this express this same transition as an HTML-based DSL:

<!-- AngularJS Koda SPA  -->

<body ng-app="kodaline" ng-controller="TimelineController" >

 <!-- Animation DSL -->
 <timeline state="zoom" time-scale="1" resolve="preloadImages(source)" >
    <timeline>
      <step target="#mask"                           style="z-index:90;" />
      <step target="#mask"                           style="opacity:0.8;" position="300" duration="0.5"/>
    </timeline>
    <timeline>
      <step target="#details"                        style="opacity:1; bounds:{{source.from}};"/>
      <step target="#details"                        style="left:0; height:210; width:323;" duration="0.3"  />
      <step mark-position="fullWidth"/>
      <step target="#details"                        style="top:18; height:512" duration="300" position="fullWidth-=0.3"/>
      <step mark-position="slideIn"/>
      <step target="#details > #green"               style="z-index:92; opacity:1; top:21;" />
      <step target="#details > #green"               style="top:0;" />
      <step target="#details > #title"               style="height:131;"  duration="200" position="fullWidth" />
      <step target="#details > #info"                style="height:56;"   duration="0.6" position="fullWidth+=0.2" />
      <step target="#details > #title > div.content" style="opacity:1.0;" duration="500" position="fullWidth+=0.3" />
      <step target="#details > #pause"               style="opacity:0.8;" duration="800" position="fullWidth+=0.4" />
      <step target="#details > #info > div.content"  style="opacity:0;"   duration="0.4" position="fullWidth+=0.6" />
    </timeline>
 </timeline>

 <!-- UI View Elements --> 

 <div id="stage">

    <!-- Tile Grid View -->
    <div id="status" class="status"></div>
    <div id="header"></div>

    <div class="tile1" ng-click="showDetails(0)" ></div>
    <div class="tile2" ng-click="showDetails(1)" ></div>
    <div class="tile3" ng-click="showDetails(2)" ></div>
    <div class="tile4" ng-click="showDetails(3)" ></div>

    <div id="other"></div>
    <div id="footer"></div>

    <!-- Tile Grid Mask -->
    <div id="mask" class="hidden"></div>

    <!-- Tile Details View -->
    <div id="green_status" class="hidden"></div>
    <div id="details" class="hidden">

This DSL is much more expressive and intuitive. For web designers tasked with animation and choreographic considerations (instead of Javascript implementataions), the DSL is especially suitable as the transition definitions are embedded within the HTML markup in close proximity to the DOM elements that will animated.

Instead of the typical separation of HTML layouts from animation logic (and element manipulation) in JavaScript, we can express both the UI and the UX transitions within the same UI layers of the client.

Note: some of the parameters (eg. Line #69) support AngularJS interpolation symbols and data-binding. This is powerful feature over the javascript-approach... timelines will be automatically updated when scope variables are modified. This, in turn, means that the timeline can be applied to 1..n targets. In the case of Koda, the timeline is applied to any of the gridlist tiles.


Animation States

Leveraging the concept of associating views and view layouts with states of the application, transitions of states can be defined that animate elements and their properties. Changes between states can trigger animations...

Such solutions can be seen in the:

With the features provided by gsTimeline library, animation states can be consider as specific groupings of transitions.

The Koda #4 sample uses Animation states to define a zoom state that shows the tile details zoomed full screen. Simply set the $scope.state = 'zoom' to trigger the animations associated with that state.

$timeline( "zoom", {
    onUpdate          : makeNotify("zoom", "updating..."),
    onComplete        : makeNotify("zoom", "complete.")
});

// Perform animation via state change
$scope.state        = "zoom";
$scope.selectedTile = selectedTile;

The Koda Application

Koda with GreenSock GSAP

Use Greensock's (GSAP) TimelineMax within a Gridlist application and demonstrate the use of animation timelines to build complex transitions.

Samples

Here are some quick links to source or demos for the experiments:

Description HTML Javascript Live Demos
Use ngAnimate with TweenMax block_1.html block_1.js Plunkr #1
Use custom Animation services layer block_2.html block_2.js CodePen #2
Use gsTimeline $timeline() with DSL block_3.html block_3.js CodePen #3

Block Animations

Description HTML Javascript Live Demos
jQuery app with click animation koda_1.html koda_1.js CodePen #1
AngularJS app with Timeline slider controls koda_2.html koda_2.js CodePen #2
AngularJS app with DSL koda_3.html koda_3.js
AngularJS app with DSL & States koda_4.html koda_4.js CodePen #4
DSL for Explode-Reveal reveal_1.html reveal_1.js CodePen #5
dsl_codepen_2

Functional Considerations

While exploring the animation API requirements, the implemenation should also consider other functional requirements:

Koda #1

  • Load images in background so zoom works quickly
  • Use promises to delay transitions until the images are ready
  • Dynamically modify timescale so unzoom is faster
  • Use of global keypress to unzoom/reverse the timeline
  • Use data model to dynamically define tile zoom from/to transitions

Koda #2 Only

  • Plugin use of Timeline Slider controls; independent of TimelineController
    • Sync Slider to transition timeline
    • Use slider to manually sequence through transition frames

Koda #3 Only

  • Use of AngularJS-GSAP $timeline service to parse animation DSL (in HTML) and build animations with databindings to scope and data models. Uses programmatic approach to trigger animations:
$timeline( "zoom", {

  onComplete        : makeNotify("zoom"),
  onReverseComplete : makeNotify("unzoom"),
  onUpdate          : makeNotify("zoom", "update")

}).then( function(animation){
    animation.restart();
});

Koda #4 Only

  • Use of AngularJS-GSAP $timeline service (again) but with feature support for Animation States. Now uses state name changes to trigger animations:
$timeline( "zoom", {
    onUpdate          : makeNotify("zoom", "updating..."),
    onComplete        : makeNotify("zoom", "complete.")
});

// Perform animation via state change
$scope.state        = "zoom";
$scope.selectedTile = selectedTile;

Tips to Run Locally

Open Terminal console in the project directory.

bower update
http-server -d ./

Open Browser and navigate to URL http://localhost:8080/


Debugging

By default the $timeline() service and process outputs to the console:

>> TimelineBuilder::makeTimeline() invoked by $timeline('0')
TimelineStates::watchState( state = 'zoom' )
loading $( #backgroundLoader ).src = http://solutionoptimist-bucket.s3.amazonaws.com/kodaline/album_kodaline.png
 $('#backgroundLoader').loaded() 
loading $( #backgroundLoader ).src = http://solutionoptimist-bucket.s3.amazonaws.com/kodaline/album_moby_v2.png
 $('#backgroundLoader').loaded() 
>> TimelineBuilder::makeTimeline() invoked by $timeline('zoom')
timeline.set( '#mask', 0.001,  {"zIndex":"-10","className":""}, '' )
timeline.set( '#details', 0.001,  {"zIndex":"-11","className":""}, '' )
timeline.set( '#green_status', 0.001,  {"zIndex":"-13","className":""}, '' )
timeline.set( '#other', 0.001,  {"top":"481","left":"88","opacity":"1.0"}, '' )
timeline.set( '#mask', 0.001,  {"zIndex":"90"}, '' )
timeline.set( '#details', 0.01,  {"zIndex":"92","opacity":"0.01","top":"-1"}, '' )
timeline.set( '#details', 0.3,  {"opacity":"1.0"}, '' )
addLabel( 'fullThumb' )
timeline.set( '#other', 0.2,  {"top":"532","left":"324"}, '' )
timeline.set( '#other', 0.1,  {"opacity":"0"}, 'fullThumb+=0.1' )
timeline.set( '#details', 0.5,  {"delay":"0.3","left":"0","width":"329"}, '' )
addLabel( 'fullWidth' )
timeline.set( '#mask', 0.5,  {"opacity":"0.80"}, 'fullWidth-=0.3' )
timeline.set( '#details', 0.3,  {"opacity":"1","top":"18","height":"512"}, 'fullWidth+=0.1' )
addLabel( 'slideIn' )
timeline.set( '#green_status', 0.001,  {"zIndex":"91","opacity":"1","top":"21"}, 'slideIn' )
timeline.set( '#green_status', 0.2,  {"top":"1"}, 'slideIn' )
timeline.set( '#details > #title', 0.6,  {"height":"131"}, 'fullWidth' )
timeline.set( '#details > #info', 0.5,  {"height":"56"}, 'fullWidth+=0.2' )
timeline.set( '#details > #title > div.content', 0.8,  {"opacity":"1.0"}, 'fullWidth+=0.3' )
timeline.set( '#details > #info > div.content', 0.4,  {"opacity":"1"}, 'fullWidth+=0.6' )
timeline.set( '#details > #pause', 0.4,  {"opacity":"1","scale":"1.0"}, 'fullWidth+=0.4' )
loading $( #backgroundLoader ).src = http://solutionoptimist-bucket.s3.amazonaws.com/kodaline/album_supermodel.png
 $('#backgroundLoader').loaded() 
loading $( #backgroundLoader ).src = http://solutionoptimist-bucket.s3.amazonaws.com/kodaline/album_goulding.png
 $('#backgroundLoader').loaded() 
loading $( #backgroundLoader ).src = http://solutionoptimist-bucket.s3.amazonaws.com/kodaline/album_goyte.png
 $('#backgroundLoader').loaded() 
loading $( #backgroundLoader ).src = http://solutionoptimist-bucket.s3.amazonaws.com/kodaline/album_pharrell.png
 $('#backgroundLoader').loaded() 
updating $(#details > img).src = 'http://solutionoptimist-bucket.s3.amazonaws.com/kodaline/album_kodaline.png'
>> TimelineStates::triggerTimeline( state = 'zoom' )
tl('zoom') updating...
>> TimelineBuilder::makeTimeline() invoked by $timeline('zoom')
timeline.set( '#mask', 0.001,  {"zIndex":"-10","className":""}, '' )
timeline.set( '#details', 0.001,  {"zIndex":"-11","className":""}, '' )
timeline.set( '#green_status', 0.001,  {"zIndex":"-13","className":""}, '' )
timeline.set( '#other', 0.001,  {"top":"481","left":"88","opacity":"1.0"}, '' )
timeline.set( '#mask', 0.001,  {"zIndex":"90"}, '' )
timeline.set( '#details', 0.01,  {"zIndex":"92","opacity":"0.01","left":"0","top":"73","width":"162","height":"164"}, '' )
timeline.set( '#details', 0.3,  {"opacity":"1.0"}, '' )
addLabel( 'fullThumb' )
timeline.set( '#other', 0.2,  {"top":"532","left":"324"}, '' )
timeline.set( '#other', 0.1,  {"opacity":"0"}, 'fullThumb+=0.1' )
timeline.set( '#details', 0.5,  {"delay":"0.3","left":"0","height":"216","width":"329"}, '' )
addLabel( 'fullWidth' )
timeline.set( '#mask', 0.5,  {"opacity":"0.80"}, 'fullWidth-=0.3' )
timeline.set( '#details', 0.3,  {"opacity":"1","top":"18","height":"512"}, 'fullWidth+=0.1' )
addLabel( 'slideIn' )
timeline.set( '#green_status', 0.001,  {"zIndex":"91","opacity":"1","top":"21"}, 'slideIn' )
timeline.set( '#green_status', 0.2,  {"top":"1"}, 'slideIn' )
timeline.set( '#details > #title', 0.6,  {"height":"131"}, 'fullWidth' )
timeline.set( '#details > #info', 0.5,  {"height":"56"}, 'fullWidth+=0.2' )
timeline.set( '#details > #title > div.content', 0.8,  {"opacity":"1.0"}, 'fullWidth+=0.3' )
timeline.set( '#details > #info > div.content', 0.4,  {"opacity":"1"}, 'fullWidth+=0.6' )
timeline.set( '#details > #pause', 0.4,  {"opacity":"1","scale":"1.0"}, 'fullWidth+=0.4' )
tl('zoom') updating...
tl('zoom') complete.