Animate Select List Items

Along the bottom of our nodes, we have a list of button icons that are the status of the node. Some of them should be animated, and I’m struggling to find a clean way to get this to work. Sometimes the below code works, sometimes it crashes everything.

Most of the examples I see (ex: here) seem to only animate the models after the diagram has been loaded, not from the get-go. The end goal here is that I would like to pass an object to that list of statuses that has an “animate” boolean on it that tells the graph item that it should be animating/spinning. We can also remove the icon button in the list at different times, in which case the animation would need to stop and the item be removed. I have looked at AnimationTriggers, and those seem to be bound to the graph object itself and not the object data provided (so it doesn’t look like I can optionally animate with those). Any advice here would be greatly appreciated for the proper way of handling something like this.

 public static BottomNodeDecorators(handlers: Map<string, Function>): Panel {
    return GO(Panel, 'Horizontal', new Binding('itemArray', 'bottomOrnaments'), {
      alignment: new Spot(0.5, 0.95, 0, 0),
      itemTemplate: GO(
        Panel,
        'Spot',
        GO('CircularButton', {
          /** Omitted button props*/
        }),
        GO(
          TextBlock,
          {
            textAlign: 'center',
            isActionable: false,
            pickable: false,
            font: Styles.ICON_FONT,
          },
          new Binding('text', 'icon'),
          new Binding('stroke', 'stroke')
        ),
        // the problem is here. I don't like binding to '' which may be the cause of the strange issues
        new Binding('', 'animate', (animated: boolean, graphOrnament: Panel) => {
          const ornament = graphOrnament.data;
          if (animated && !ornament.animation) {
            const animation = new Animation();
            animation.add(graphOrnament, 'angle', graphOrnament.angle, 360); // do a full loop
            animation.easing = Animation.EaseLinear;
            animation.runCount = Infinity;
            animation.duration = 1000; // animate over one second
            animation.start();
            ornament.animation = animation;
            // const clickHandler = handlers.get('updateItemProperty'); // this calls setDataProperty
            // clickHandler!(ornament, 'animation', animation);
          } else if (!animated) {
            if (ornament.animation) {
              ornament.animation.stop();
            }
          }
        })
      ),
    });
  }

** Update: I’ve gotten a bit closer by modifying my code to look like below; however, it does not appear that AnimationTrigger has the same property of runCount available to it to allow it to run infinitely:

  new Binding('angle', 'animate', (_animated: boolean, graphOrnament: Panel) => {
          return graphOrnament.angle + 270; // this shows it rotated, but I need it continual
        }),
  new AnimationTrigger(
    'angle',
    {
      duration: 600,
      easing: Animation.EaseLinear,
    },
    AnimationTrigger.Immediate
  )

Actually, I thought your first attempt was pretty good. Although I’m not sure exactly what your question is. Here’s what I just tried:

<!DOCTYPE html>
<html>
<head>
  <title>Animation Controlled Via Binding</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button id="myTestButton">Toggle Animation Randomly</button>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  $(go.Node, "Spot",
    { locationSpot: go.Spot.Center, locationObjectName: "TEXT" },
    new go.Binding("location", "loc", go.Point.parse),
    $(go.TextBlock,
      { name: "TEXT" },
      new go.Binding("text")),
    $(go.Shape,
      { fill: "white", width: 30, height: 30, alignment: new go.Spot(0.5, 0, 0, -30) },
      new go.Binding("fill", "color"),
      new go.Binding("", "animate", (a, shp) => {
        const node = shp.part;
        let anim = shp._anim;
        if (a) {
          if (!anim) {
            anim = new go.Animation({ duration: 1000, runCount: Infinity, easing: go.Animation.EaseLinear });
            anim.add(shp, "angle", shp.angle, shp.angle+360);
            shp._anim = anim;
          }
          anim.start();
        } else {
          if (anim) anim.stop();
        }
      }))
  );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue", loc: "0 0", animate: true },
  { key: 2, text: "Beta", color: "orange", loc: "100 0", animate: true },
  { key: 3, text: "Gamma", color: "lightgreen", loc: "0 100", animate: false },
  { key: 4, text: "Delta", color: "pink", loc: "100 100" }
]);

document.getElementById("myTestButton").addEventListener("click", e => {
    myDiagram.model.commit(m => {
      m.nodeDataArray.forEach(d => {
        m.set(d, "animate", Math.random() < 0.5);
      });
    });
  });
  </script>
</body>
</html>

Sorry Walter, I should have been a little more clear with the question. With the code I had initially, I had very weird side effect behaviors that made me think creating an animation in a binding like this was incorrect. If I was developing in storybook, it would do things like trigger the storybook clipboard before properly animating the item. In our actual dev environment, everything would just crash. After studying your example a little more, if I save off the animation to the graph object itself instead of the objectdata, everything works as intended. Thanks!

Never store references to Diagram-related objects in the model data – no pointers to instances of Diagram, Layer, GraphObject or Part, Tool, CommandHandler, Layout, or any of the Animation classes. In fact, now that I think about it, really not to any of the GoJS classes, except Point, Size, Rect, Margin, Spot, and even those it would be more space efficient to store the stringified values.

Gotcha, and now I understand why as well.

On a related note, I modified the example you had above, and I noticed that despite adding a margin between the items, when you have more than one moving item next to each other they are bouncing off one another. Any idea why?

<!DOCTYPE html>
<html>
<head>
  <title>Animation Controlled Via Binding</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button id="myTestButton">Toggle Animation Randomly</button>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  $(go.Node, "Spot",
    { locationSpot: go.Spot.Center, locationObjectName: "TEXT" },
    new go.Binding("location", "loc", go.Point.parse),
    $(go.TextBlock,
      { name: "TEXT" },
      new go.Binding("text")),
    $(go.Shape, // body
      { fill: "white", width: 50, height: 50, alignment: new go.Spot(.5, .5, 0, -20) },
      new go.Binding("fill", "color"),
           ),
           $(go.Panel, 'Horizontal', new go.Binding('itemArray', 'bottomOrnaments'), {
      			name: 'BOTTOMORNAMENTS',
          	/* alignment: new go.Spot(0.5, 0.95, 0, 0), */
						itemTemplate: $(
              go.Panel, 'Spot', {margin: 5, background:'pink'},
              $('Button', {width: 15, height: 15}),
              new go.Binding("", "animate", (a, shp) => {
                const node = shp.part;
                let anim = shp._anim;
                if (a) {
                  if (!anim) {
                    anim = new go.Animation({ duration: 1000, runCount: Infinity, easing: go.Animation.EaseLinear });
                    anim.add(shp, "angle", shp.angle, shp.angle+360);
                    shp._anim = anim;
                  }
                  anim.start();
                } else {
                  if (anim) anim.stop();
                }
              })
              )
              
        }
      )
  );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue", loc: "0 0", bottomOrnaments: [{animate: true}, {animate: true}] },
  { key: 2, text: "Beta", color: "orange", loc: "100 0", bottomOrnaments: [{animate: true}] },
  { key: 3, text: "Gamma", color: "lightgreen", loc: "0 100", bottomOrnaments: [{animate: false}]},
  { key: 4, text: "Delta", color: "pink", loc: "100 100" }
]);

document.getElementById("myTestButton").addEventListener("click", e => {
    myDiagram.model.commit(m => {
      m.nodeDataArray.forEach(d => {
        m.set(d, "animate", Math.random() < 0.5);
      });
    });
  });
  </script>
</body>
</html>```

Those are in a “Horizontal” Panel, so by definition they are positioned horizontally next to each other. As rectangular objects rotate, their widths vary.

Well, yes in that example I used squares within the horizontal panels, which are also square by default. We have circular buttons that we have created that extend the button class GoJS provides, but I can see the surrounding panel of the circle shape is still a square. Instead of animating the entire button, just animating the icon seems to solve this. Thanks!