SMIL animation on SVG ng-repeat'ed element onl

2019-08-06 21:44发布

问题:

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?

回答1:

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.