Jump to content


Binding tween value to Angular scope variable

Go to solution Solved by OSUblake,

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts



I am trying to build an animation where GSAP will tween an SVG's d value to values that are stored in Angular scope variables. Currently I am using three "buffer" attributes to hold d values waiting to be used. These buffers are in the root controller scope, and are updated / refreshed by a function called in the onComplete of each tween in the timeline.


The svg is inside of a directive, and the bound data can be outputted to the dom inside the directive reflecting the changes made to the scope var. The problem is that the target value of the tween doesn't change, as if GSAP doesn't notice that the scope var has changed. 


How can I get GSAP to notice that the bound var has changed, and tween to the new target value?


I tried to get this going in a code pen, but for some reason the ng-if that the directive waits for was never triggered, so there was a whole bunch of nothing on the screen. Sorry about that, attaching code here :


points.js is a temporary, external .js data source. It looks like this :

var pointsServicePoints = [
13.9649125,104.9441085,......................etc for about 8,000 lines.


var cacheTestApp = angular.module('cacheTestApp',[])
    .controller('CacheTestAppController', ['$scope','CacheService', 'PointConverterService', function($scope, CacheService, PointConverterService){

        var cacheCont = this;

        $scope.dPoints1 = "";
        $scope.dPoints2 = "";
        $scope.dPoints3 = "";

        //Main Holder For X,Y from Cache
        $scope.rawDataArray = [];

        // next offset to use for dPoints - offset by 2 to get to start of next x,y pair.
        var nextDPoint = 2;
        // Points to render for each svg
        var pointsPerSvg = 4000;

            // Get points from service.
            $scope.rawDataArray = CacheService.extractDataPoints();

            // Get points to convert for initial SVGs
            var activeSvg = $scope.rawDataArray.slice(0,pointsPerSvg);
            var firstSvg = $scope.rawDataArray.slice(nextDPoint,nextDPoint + pointsPerSvg);
            var secondSvg = $scope.rawDataArray.slice(nextDPoint,nextDPoint + pointsPerSvg);

            // Convert x,y points to SVGs.
            $scope.dPoints1 = PointConverterService.convertToSvg(activeSvg);
            $scope.dPoints2 = PointConverterService.convertToSvg(firstSvg);
            $scope.dPoints3 = PointConverterService.convertToSvg(secondSvg);


         * Swap points in buffers.
         * @param p
        $scope.advancePoints = function(p){
            var t0 = performance.now();
            var nextSvg = $scope.rawDataArray.slice(nextDPoint,nextDPoint + pointsPerSvg);

                case 1 :
                    $scope.dPoints1 = PointConverterService.convertToSvg(nextSvg);
                case 2 :
                    $scope.dPoints2 = PointConverterService.convertToSvg(nextSvg);
                case 3 :
                    $scope.dPoints3 = PointConverterService.convertToSvg(nextSvg);


            nextDPoint += 2;



cacheTestApp.directive('svgDirective', [function(){

    var linker = function(scope, element, attrs){

        var lead1p1 = document.getElementById('lead1path1');


        var updateMe = function(){
            console.log("Update me triggered!");

        var tl = new TimelineMax({repeat:-1, paused:true});

            tl.to(lead1p1,.1, {attr:{d:scope.points1}, onComplete:scope.advance, onCompleteParams:[{id:1}]})
            .to(lead1p1,.1, {attr:{d:scope.points2}, onComplete:scope.advance, onCompleteParams:[{id:2}]})
            .to(lead1p1,.1, {attr:{d:scope.points3}, onComplete:scope.advance, onCompleteParams:[{id:3}]});

        document.getElementById('tweenStarter').addEventListener('click', function(){tl.play();});
        document.getElementById('tweenStopper').addEventListener('click', function(){tl.pause();});

        var style = window.getComputedStyle(lead1p1);
        var left = style.getPropertyValue('left');

    return {
            points1: '@points1',
            points2: '@points2',
            points3: '@points3',
            advance: '&advance'
        link: linker,
        templateUrl: 'pages/svg-directive.html'



cacheTestApp.service('CacheService', ['$rootScope',function(){

     * Uses points from points js to simulate return to points request.
     * @returns {pointsServicePoints}
    this.extractDataPoints = function(){
        return  pointsServicePoints;

    this.extractHeartbeats = function(){


 * PointConverterService
 * Converts from x,y to svg.
 * Returns path.
cacheTestApp.service('PointConverterService', [function(){

    this.convertToSvg = function(pointArray){

            var size = pointArray.length;
            var last = size - 4;

            var path = "M" + [pointArray[0], pointArray[1]];

            for (var i = 0; i < size - 2; i += 2) {
                var x0 = i ? pointArray[i - 2] : pointArray[0];
                var y0 = i ? pointArray[i - 1] : pointArray[1];
                var x1 = pointArray[i + 0];
                var y1 = pointArray[i + 1];
                var x2 = pointArray[i + 2];
                var y2 = pointArray[i + 3];
                var x3 = i !== last ? pointArray[i + 4] : x2;
                var y3 = i !== last ? pointArray[i + 5] : y2;
                var cp1x = (-x0 + 6 * x1 + x2) / 6;
                var cp1y = (-y0 + 6 * y1 + y2) / 6;
                var cp2x = (x1 + 6 * x2 - x3) / 6;
                var cp2y = (y1 + 6 * y2 - y3) / 6;
                path += "C" + [cp1x, cp1y, cp2x, cp2y, x2, y2];
            return path;


<svg id="ekg_holder">
        <pattern id="basicPattern" x="0" y="0" width="5.5" height="5.5" patternUnits="userSpaceOnUse">
            <rect x='0' y='0' height="5.5" width="5.5" style="fill:rgb(250,250,255);stroke-width:.5;stroke:rgb(0,0,0)"></rect>

    <rect x="0" y="0" width="1100" height="220" fill="url(#basicPattern)"/>
    <rect x='0' y='110' height="1" width="100%"
    <!--<polyline id="EKGpoly"></polyline>-->
    <path id="lead1path1" class="ekgMove"/>
<!--    <path id="lead1path2" class="ekgMove"/>
    <path id="lead1path3" class="ekgMove"/>-->
<a href="#" id="tweenStarter">Start</a>
<a href="#" id="tweenStopper">Stop</a>


<!DOCTYPE html>
<html lang="en-us" data-ng-app="cacheTestApp">
    <title>Cache Test App</title>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-COMPATIBLE" content="IE-Edge">

    <link rel="stylesheet" href="css/bootstrap.min.css"/>
    <link rel="stylesheet" href="css/css.css"/>

    <script src="js/angular.js"></script>
    <script src="js/app.js"></script>
    <script src="js/services.js"></script>
    <script src="js/directives.js"></script>

    <script src="js/greensock/uncompressed/TweenMax.js"></script>

        html, body, input, select, textarea {
            font-size: 1.05em !important;

<body data-ng-controller="CacheTestAppController as cacheCont">

<div class="container">
    <div svg-directive ng-if="dPoints1" points1="{{dPoints1}}" points2="{{dPoints2}}" points3="{{dPoints3}}"


<div class="container">
        <div id="var1holder" class="jumbotron" data-ng-bind="dPoints1"></div>
        <div id="var2holder" class="jumbotron" data-ng-bind="dPoints2"></div>

css :

    fill: none;
    stroke: #ed0002;
    stroke-width: 1;

Thanks in advance for any help, and thanks to @OSUblake for the help on the x,y -> svg conversion!

Link to comment
Share on other sites

  • Solution

GSAP is not Angular, so it doesn't know when something changes. You would need to create new tweens. Not sure if it's the incomplete data, but it doesn't look like an ekg. 



A lot of those points look really close together. If performance becomes an issue, you may want to use this to reduce them.


  • Like 3
Link to comment
Share on other sites

Thanks OSUblake!


I left out the css, which helps a lot in making the data look ekg-ish. 


I am definitely going to checkout the simplfy-js, it could be quite useful as there is a ton of data being shoved into the SVG. 


I'm starting to wonder about the ekg being able to meet the speed requirements I have for this. When I directly bound the svg (outside of a directive) to the scope var then changed the var the line would go screaming by, but I had none of the dom manipulation that I need for users to navigate the data. I'm hoping I can get somewhere near there using this configuration. 


Can I ask - what does this bit of code do?

interval = TweenLite.delayedCall(0.1, function() {

Thanks for your help.

Link to comment
Share on other sites

Oops. I forgot to remove that. I was going to use that as an interval timer to do drive the updates. Here's an example of how it would work.

See the Pen GoyaJO?editors=0010 by osublake (@osublake) on CodePen


There's lots of ways to speed things up. If binding is the bottleneck, you could create a service to do the updates manually. If your data is too big, you might want to implement something that works like a paging system, or splitting your data up into smaller buckets/chunks.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.