GoJS Animation

GoJS offers several built-in animations, enabled by default, as well as the ability to create arbitrary animations.

The Diagram.animationManager handles animations within a Diagram. The AnimationManager automatically sets up and dispatches default animations, and has properties to customize and disable them. Custom animations are possible by creating instances of Animation or AnimationTrigger.

This introduction page details the different classes used for GoJS animation.

To see more demonstrations of custom animations, visit the Custom Animations extension sample.

Default Animations

By default, the AnimationManager creates and runs several animations for its Diagram using a single instance of Animation, the AnimationManager.defaultAnimation. These animations occur on various commands, by the Diagram.model setter, and upon layouts. Unlike other Animations, they will be stopped if a new transaction is started during animation.

GoJS will begin an animation automatically for these reasons:

Invoked by CommandHandler:

Invoked by Diagram:

Invoked by AnimationTriggers, if any are declared.

The above quoted names are strings passed to AnimationManager.canStart This method can be overridden to return false if you wish to stop specific automatic animations.

Default Initial Animation

As of GoJS 2.1, the default initial animation fades the diagram upwards into view. Prior versions animated Part locations separately. To control the initial animation behavior, there now exists AnimationManager.initialAnimationStyle, which is set to AnimationStyle.Default by default, but can be set to AnimationStyle.AnimateLocations to use the animation style from GoJS 2.0. You can also set this property to AnimationStyle.None and define your own initial animation using the "InitialAnimationStarting" DiagramEvent.

Here is an example with buttons which set AnimationManager.initialAnimationStyle to the three different values, then reload the Diagram. A fourth button illustrates how one might use the "InitialAnimationStarting" DiagramEvent to make a custom "zoom in" animation.


    diagram.nodeTemplate =
      new go.Node("Auto")
        .add(
          new go.Shape("RoundedRectangle", { strokeWidth: 0, fill: "lightblue" }),
          new go.TextBlock({ margin: 8, font: "bold 14px sans-serif", stroke: '#333' })
            .bind("text")
        );
    diagram.model = new go.GraphLinksModel([{ text: 'Alpha' }, { text: 'Beta' }, { text: 'Delta' }, { text: 'Gamma' }]);

    // only needed for this demonstration, this flag is used to stop
    // the "InitialAnimationStarting" listener when other buttons are pressed
    window.custom = false;

    window.animateDefault = () => {
      window.custom = false;
      diagram.animationManager.initialAnimationStyle = go.AnimationStyle.Default;
      diagram.model = go.Model.fromJson(diagram.model.toJson());
    }
    window.animateLocations = () => {
      window.custom = false;
      diagram.animationManager.initialAnimationStyle = go.AnimationStyle.AnimateLocations;
      diagram.model = go.Model.fromJson(diagram.model.toJson());
    }
    window.animateNone = () => {
      window.custom = false;
      diagram.animationManager.initialAnimationStyle = go.AnimationStyle.None;
      diagram.model = go.Model.fromJson(diagram.model.toJson());
    }
    window.animateCustom = () => {
      window.custom = true;
      diagram.animationManager.initialAnimationStyle = go.AnimationStyle.None;
      // Customer listener zooms-in the Diagram on load:
      diagram.addDiagramListener("InitialAnimationStarting", e => {
        const animation = e.subject.defaultAnimation;
        if (window.custom === false) {
          // a different button was pressed, restore default values on the default animation:
          animation.easing = go.Animation.EaseInOutQuad;
          animation.duration = NaN;
          return;
        }
        animation.easing = go.Animation.EaseOutExpo;
        animation.duration = 1500;
        animation.add(e.diagram, 'scale', 0.1, 1);
        animation.add(e.diagram, 'opacity', 0, 1);
      })
      diagram.model = go.Model.fromJson(diagram.model.toJson());
    }

    

Limitations of Default Animations

The AnimationManager can be turned off by setting AnimationManager.isEnabled to false. Specific default animations can be turned off or modified by overriding AnimationManager.canStart and returning false as desired.

The default animation will be stopped if a new transaction begins during the animation. The same is not true of other Animations, which are not stopped by new transactions, and can continue indefinitely.

Animatable Properties

By default, AnimationTriggers and Animations can animate these properties of GraphObjects:

Additionally Animations (but not AnimationTriggers) can animate these properties of Diagram:

It is possible to animate other properties if they are defined by the programmer -- see the section "Custom Animation Effects" below.

The AnimationTrigger Class

An AnimationTrigger is used to declare GraphObject properties to animate when their value has changed. When a trigger is defined, changes to the target property will animate from the old value to the new value. In templates, triggers are defined in a similar fashion to Bindings:


// In this shape definition, one trigger is defined on a Shape.
// It will cause all changes to Shape.fill to animate
// from their old values to their new values.
new go.Shape("Rectangle", { strokeWidth: 12, stroke: 'black', fill: 'white' })
  .trigger('fill')

Here is an example, with an HTML button that sets the Shape's stroke and fill to new random values:


diagram.nodeTemplate =
  new go.Node()
    .add(
      new go.Shape("Rectangle", { strokeWidth: 12, stroke: 'black', fill: 'white' })
        .trigger('stroke')
        .trigger('fill')
    );

diagram.model = new go.GraphLinksModel([{ key: 'Alpha' }]); // One node

// attach this Diagram to the window to use a button
window.animateTrigger1 = () => {
  diagram.commit(diag => {
    const node = diag.nodes.first();
    node.elt(0).stroke = go.Brush.randomColor();
    node.elt(0).fill = go.Brush.randomColor();
  });
}

AnimationTriggers can invoke an animation immediately, starting a new animation with each property of each GraphObject that has been modified, or they can (much more efficiently) be bundled together into the default animation (AnimationManager.defaultAnimation) and begin at the end of the next transaction. These behaviors can be set with AnimationTrigger.startCondition by the values TriggerStart.Immediate and TriggerStart.Bundled, respectively. The default value, TriggerStart.Default, attempts to infer which is best. It will start immediately if there is no ongoing transaction or if Diagram.skipsUndoManager is true.

AnimationTriggers are only definable on GraphObjects in templates, and cannot be used on RowColumnDefinitions or Diagrams.

The Animation Class

General animation of GraphObject and Diagram properties is possible by creating one or more instances of the Animation class.


const animation = new go.Animation();
// Animate the node's angle from its current value to a random value between 0 and 180 degrees
animation.add(node, "angle", node.angle, Math.random() * 180);
animation.duration = 1000; // Animate over 1 second, instead of the default 600 milliseconds
animation.start(); // starts the animation immediately

Animation.add is used to specify which objects should animate, their properties to animate, and for each property the starting and ending values:


  animation.add(GraphObjectOrDiagram, "EffectName", StartingValue, EndingValue);

Here's the above animation in an example, where each node is animated by an HTML button. Note carefully that each node is added to the same animation. The same effect would be had with one animation per node, but it is always more efficient to group the properties you are animating into a single animation, if possible (for instance, it is possible if they are all going to start at the same time and have the same duration).


// define a simple Node template
diagram.nodeTemplate =
  new go.Node("Spot", { locationSpot: go.Spot.Center })
    .bind("angle")
    .add(
      new go.Shape("Diamond", { strokeWidth: 0, width: 75, height: 75 })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8, font: 'bold 12pt sans-serif' })
        .bind("text")
    );

diagram.model = new go.GraphLinksModel(
  [
    { key: 1, text: "Alpha", color: "lightblue" },
    { key: 2, text: "Beta", color: "orange" },
    { key: 3, text: "Gamma", group: 'G1',  color: "lightgreen" },
    { key: 4, text: "Delta", group: 'G1',  color: "pink", angle: 45 }
  ],
  [
    { from: 1, to: 2 },
    { from: 3, to: 4 }
  ]);

window.animate1 = () => {
  const animation = new go.Animation();
  diagram.nodes.each(node => {
    // Animate the clockwise turning of a node from its current angle to a random value
    animation.add(node, "angle", node.angle, node.angle + Math.random() * 180);
  });
  animation.duration = 1000; // Animate over 1 second, instead of the default 600 milliseconds
  animation.start(); // starts the animation immediately
}

Animating the Diagram is possible by passing it as the object to be animated:


  animation.add(myDiagram, "position", myDiagram.position, myDiagram.position.copy().offset(200, 15));
  ...
  animation.add(myDiagram, "scale", myDiagram.scale, 0.2);

Animations can also be reversed, as is common with animations that are intended to be cosmetic in nature, by setting Animation.reversible to true. This doubles the effective duration of the Animation.

Below are several example Animations, all with Animation.reversible set to true. The first animates Nodes, the other three animate Diagram position and scale.


    // define a simple Node template
    diagram.nodeTemplate =
      new go.Node("Spot", { locationSpot: go.Spot.Center })
        .bind("angle")
          .add(
            new go.Shape("Diamond", { strokeWidth: 0, width: 75, height: 75 })
              .bind("fill", "color"),
            new go.TextBlock({ margin: 8, font: 'bold 12pt sans-serif' })
              .bind("text")
          );

    diagram.model = new go.GraphLinksModel(
    [
      { key: 1, text: "Alpha", color: "lightblue" },
      { key: 2, text: "Beta", color: "orange" },
      { key: 3, text: "Gamma", group: 'G1',  color: "lightgreen" },
      { key: 4, text: "Delta", group: 'G1',  color: "pink" }
    ],
    [
      { from: 1, to: 2 },
      { from: 3, to: 4 }
    ]);

    function makeButtonAnimation(f) {  // return a button event handler to start an animation
      return () => {
        // Stop any currently running animations
        diagram.animationManager.stopAnimation(true);

        const animation = new go.Animation();
        animation.reversible = true; // reverse the animation at the end, doubling its total time
        f(animation);  // initialize the Animation
        animation.start(); // start the animation immediately
      };
    }

    window.animateAngleReverse =
      makeButtonAnimation(animation => {
        diagram.nodes.each(node => {
          // Animate the node's angle from its current value a random value between 0 and 90
          animation.add(node, "angle", node.angle, Math.random() * 90);
        });
      });

    window.animateDiagramPosition =
      makeButtonAnimation(animation => {
        // shift the diagram contents towards the right and then back
        animation.add(diagram, "position", diagram.position, diagram.position.copy().offset(200, 15));
        animation.duration = 700;
      });

    window.animateZoomOut =
      makeButtonAnimation(animation => {
        animation.add(diagram, "scale", diagram.scale, 0.2);
      });

    window.animateZoomIn =
      makeButtonAnimation(animation => {
        animation.add(diagram, "scale", diagram.scale, 4);
      });
  

Without the call to AnimationManager.stopAnimation to protect against rapid button clicks, you would notice that if you clicked Zoom Out, and then during the animation clicked the same button again, the Diagram's scale would not return to its initial value of 1.0. This is because the Animation animates from the current Diagram scale value, to its final value, and back again, but the current value is also what's being changed due to the ongoing animation.

Custom Animation Effects

It is sometimes helpful to add custom ways to modify one or more properties during an animation. You can register new animatable effects with AnimationManager.defineAnimationEffect. The name passed is an arbitrary string, but often reflects a property of a GraphObject class. The body of the function passed determines what property or properties are animated.

Here is an example, creating an "fraction" Animation effect to animate the value of GraphObject.segmentFraction, which will give the appearance of a Link label moving along its path.


// This presumes the object to be animated is a label within a Link
go.AnimationManager.defineAnimationEffect('fraction',
(obj, startValue, endValue, easing, currentTime, duration, animation) => {
  obj.segmentFraction = easing(currentTime, startValue, endValue - startValue, duration);
});

After defining this, we can use it as a property name in an Animation. The following example sets up an indefinite (Animation.runCount = Infinity) and reversible animation, where each link is assigned a random duration to cycle the fill color and segmentFraction of its label. This produces labels that appear to move along their path while pulsating colors. The setting of Animation.reversible causes them to go backwards once finished, to start from their beginning again.


  function animateColorAndFraction() {
    // create one Animation for each link, so that they have independent durations
    myDiagram.links.each(link => {
      const animation = new go.Animation()
      animation.add(link.elt(1), "fill", link.elt(0).fill, go.Brush.randomColor());
      animation.add(link.elt(1), "fraction", 0, 1);
      animation.duration = 1000 + (Math.random()*2000);
      animation.reversible = true; // Re-run backwards
      animation.runCount = Infinity; // Animate forever
      animation.start();
    });
  }

Since Animation.runCount was set to Infinity, this Animation wil run indefinitely.

Animating Deletion

Parts to be deleted can be animated, but since they will no longer exist in the Diagram after removal, a copy must be added to the Animation so that there is an object to animate. This can be done with Animation.addTemporaryPart. The part can then have its deletion animated using Animation.add. This temporary part will be the object that animates, and will automatically appear when animation begins and be removed when animation completes. It is typical for deletion animations to shrink the mock Part, move it off-screen, reduce its opacity to zero, or otherwise show it disappearing in some way.

In this example, each Part being deleted will be scaled to an imperceptible size (by animating scale to 0.01) and spun around (by animating angle), to give the appearance of swirling away. There are other example deletion (and creation) effects in the Custom Animations extension sample.


myDiagram.addDiagramListener('SelectionDeleting', e => {
  // the DiagramEvent.subject is the collection of Parts about to be deleted
  e.subject.each(part => {
    if (!(part instanceof go.Node)) return; // only animate Nodes
    const animation = new go.Animation();
    const deletePart = part.copy();
    animation.add(deletePart, "scale", deletePart.scale, 0.01);
    animation.add(deletePart, "angle", deletePart.angle, 360);
    animation.addTemporaryPart(deletePart, myDiagram);
    animation.start();
  });
});

Animation Examples

To see more examples of custom animations, visit the Custom Animations extension sample. It demonstrates a number of Node creation/deletion animations, linking animations, and more. There are also several samples which contain animation: