SMIL animation on SVG ng-repeat'ed element only occurs on page load
Asked Answered
Q

1

2

I'm generating some <rect>s with <animate> children using the ng-repeat directive.

On page load the animations are correctly triggered and everything happens as expected. However, when I add a new <rect> the animation does not occur.

The following code snippet demonstrates this behaviour:

function Controller($scope) {
  $scope.rects = [];
  var spacing = 5;
  var width = 10;
  var height = 100;
  var x = 10;
  var y = 10;

  $scope.newRect = function() {
    $scope.rects.push({
      x: x,
      y: y,
      width: width,
      height: height
    });
    x += spacing + width;
  };

  for (var i = 0; i < 5; i++) {
    $scope.newRect();
  }

}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app ng-controller="Controller">
  <svg width="1000" height="200">
    <rect ng-repeat="rect in rects" x="{{rect.x}}" y="{{rect.y}}" height="{{rect.height}}" width="{{rect.width}}">
      <animate attributeName="y" from="{{rect.height}}" to="{{rect.y}}" dur="1s" repeatCount="1" />
      <animate attributeName="height" from="0" to="{{rect.height}}" dur="1s" repeatCount="1" />
    </rect>
  </svg>
  <button ng-click="newRect()">One more</button>
</div>

Upon loading the example, 4 <rect> will appear, animating from the bottom to the top. However, when pressing the "One more" button, the new <rect> will be added without animation (behaviour tested on Firefox 35 and Chrome 38).

How can I trigger the animation for the new <rect>s?

Qualification answered 28/10, 2014 at 17:30 Comment(0)
V
6

The default begin time for an animation element (equivalent to begin="0s") is always measured relative to the SVG load time. This is true even if you create the animation element dynamically after page load.

If you want any other begin time, you will need to either (a) explicitly set a different value for the begin attribute, or (b) use the beginElement() or beginElementAt(offsetTime) methods of the animation element DOM objects. Since you're creating the elements with scripts and you want them to start right after inserting them, the beginElement() method is probably the easiest approach.

Edited to add:

If beginElement isn't working because angular-js doesn't provide you with direct access to the created element, you can make use of the event format for the begin attribute. If the begin attribute contains the name of a DOM event (or a semi-colon separated list of times and event names), the animation will begin when that event occurs. By default, the event will be listened for on the element that the animation will affect—the rectangle in your case. (You can tie the animation to a different element using the elementID.eventName format).

The trick to get the animation to start as soon as it is added is to link it to one of the rarely used DOM-mutation events, specifically DOMNodeInserted. That way, when you add the animation node as a child of the <rect>, or when you add the <rect> to the SVG, the event and then the animation will be triggered immediately.

If you want a delay between inserting the element and triggering the animation, use the eventName + timeOffset format.

Here is the proof of concept with vanilla JS (as a fiddle).

And here is your modified snippet; the JS code is the same, only the angular template has changed:

function Controller($scope) {
  $scope.rects = [];
  var spacing = 5;
  var width = 10;
  var height = 100;
  var x = 10;
  var y = 10;

  $scope.newRect = function() {
    $scope.rects.push({
      x: x,
      y: y,
      width: width,
      height: height
    });
    x += spacing + width;
  };

  for (var i = 0; i < 5; i++) {
    $scope.newRect();
  }

}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app ng-controller="Controller">
  <svg width="1000" height="120">
    <rect ng-repeat="rect in rects" x="{{rect.x}}" y="{{rect.y}}" 
          height="{{rect.height}}" width="{{rect.width}}">
      <animate attributeName="y" from="{{rect.height}}" to="{{rect.y}}" 
               dur="1s" repeatCount="1" begin="DOMNodeInserted"/>
      <animate attributeName="height" from="0" to="{{rect.height}}" 
               dur="1s" repeatCount="1" begin="DOMNodeInserted"/>
    </rect>
  </svg>
  <button ng-click="newRect()">One more</button>
</div>

I'm not sure whether you intentionally want to have the bars start 10px offset from the bottom line. If that was accidental you can fix it by setting the from value of the first animation to rect.height + rect.y.

I've tested the fiddle in the latest Chrome and Firefox. If you need to support older browsers, especially if you're supporting older IE with a SMIL polyfill, you'll want to test to make sure the DOM mutation events are being thrown correctly.

Vieira answered 28/10, 2014 at 22:19 Comment(4)
I did try the beginElement() approach. However, since I need Javascript access to the <animate> element before triggering it, I needed to wrap the call in a setTimeout(fn, 0). In some cases this creates a flicker where the user can see the full <rect> before the animation is triggered (and now I wanted to write a paragraph and unwittingly submitted the reply. Lesson learnt!). I'll try to see if I can make something with the (a) approach, though, thanks.Qualification
@Qualification See edits if you haven't figured it out for yourself yet.Vieira
I did try that solution but unknowingly wrote this.DOMNodeInserted instead of DOMNodeInserted. Thank you for the edit, it was enormously helpful. As for the other points: the 10px offset was, indeed, unintentional, I corrected it in my code, but deemed it not important enough for the question at hand. For the second point: no, older IEs are not a problem at this juncture, fortunately.Qualification
Yes, the SMIL syntax can be a little misleading -- it looks a lot like Javascript, but not quite! There are also some old tutorials out there which use non-standard syntax that was supported in the old Adobe plugin but aren't part of the standard.Vieira

© 2022 - 2024 — McMap. All rights reserved.