Trident part 8 – timeline scenarios

June 25th, 2009 | 7 Comments »

Over the course of this week i’m talking about different concepts in the Trident animation library for Java applications. Part eight talks about timeline scenarios that allow creating parallel, sequential, staged or arbitary execution graphs of timelines, runnables, Swing workers and custom application actors.

Timeline scenarios

Timeline scenario allows combining multiple timeline scenario actors in a parallel, sequential or custom order.

There are three core types of timeline scenario actors:

Additional types of timeline scenario actors can be added in the application code by implementing the TimelineScenario.TimelineScenarioActor interface.

To create a custom timeline scenario, use the following APIs of the TimelineScenario class:

  • public void addScenarioActor(TimelineScenarioActor actor) adds the specified actor
  • public void addDependency(TimelineScenarioActor actor, TimelineScenarioActor... waitFor) specifies the dependencies between the actors

Timeline scenario kinds

There are three built-in timeline scenario kinds that address the most common dependencies between the actors:

  • Timeline.Parallel runs all the actors in a parallel fashion
  • Timeline.Sequence runs the actors one after another in the order they have been added
  • Timeline.RendezvousSequence allows simple branch-and-wait ordering. The rendezvous scenario has a stage-like approach. All actors belonging to the same stage run in parallel, while actors in stage N+1 wait for all actors in stage N to be finished. The RendezvousSequence.rendezvous() marks the end of one stage and the beginning of another.

Simple Swing timeline scenario

The following example shows a Swing application with a simple timeline scenario that launches five parallel timelines. It shows the code behind this video, where every volley is a separate timeline, and all currently playing volleys are part of the same timeline scenario.

In the code, there are three “hierarchy” levels of fireworks:

  • The entire fireworks display – this is a timeline scenario that consists of five volley explosions.
  • The volley explosion implemented in VolleyExplosion class – this is a collection of single explosions that have the same color and originate from the same explosion center point.
  • The single explosion implemented in SingleExplosion class – this is a fading circle that represents a single “leaf” part of the volley explosion.

The code behind the single explosion is quite simple:

   public class SingleExplosion {
      float x;
 
      float y;
 
      float radius;
 
      float opacity;
 
      Color color;
 
      public SingleExplosion(Color color, float x, float y, float radius) {
         this.color = color;
         this.x = x;
         this.y = y;
         this.radius = radius;
         this.opacity = 1.0f;
      }
 
      public void setX(float x) {
         this.x = x;
      }
 
      public void setY(float y) {
         this.y = y;
      }
 
      public void setRadius(float radius) {
         this.radius = radius;
      }
 
      public void setOpacity(float opacity) {
         this.opacity = opacity;
      }
 
      public void paint(Graphics g) {
         Graphics2D g2d = (Graphics2D) g.create();
         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
               RenderingHints.VALUE_ANTIALIAS_ON);
         g2d.setComposite(AlphaComposite.SrcOver.derive(this.opacity));
         g2d.setColor(this.color);
         g2d.fill(new Ellipse2D.Float(this.x - this.radius, this.y
               - this.radius, 2 * radius, 2 * radius));
         g2d.dispose();
      }
   }

It has four fields that specify its location, size, opacity and color. Each field except the color has a public setter that is used in the timeline created in the parent volley explosion. Finally, it has a custom painting implementation that paints the graphical representation of the single volley.

The volley explosion is implemented by the following class:

   public class VolleyExplosion {
      private int x;
 
      private int y;
 
      private Color color;
 
      private Set circles;
 
      public VolleyExplosion(int x, int y, Color color) {
         this.x = x;
         this.y = y;
         this.color = color;
         this.circles = new HashSet();
      }
 
      public TimelineScenario getExplosionScenario() {
         TimelineScenario scenario = new TimelineScenario.Parallel();
 
         int duration = 1000 + (int) (1000 * Math.random());
         for (int i = 0; i < 18; i++) {
            float dist = (float) (50 + 10 * Math.random());
            float radius = (float) (2 + 2 * Math.random());
            for (float delta = 0.6f; delta <= 1.0f; delta += 0.2f) {
               float circleRadius = radius * delta;
 
               double degrees = 20.0 * (i + Math.random());
               float radians = (float) (2.0 * Math.PI * degrees / 360.0);
 
               float initDist = delta * dist / 10.0f;
               float finalDist = delta * dist;
               float initX = (float) (this.x + initDist
                     * Math.cos(radians));
               float initY = (float) (this.y + initDist
                     * Math.sin(radians));
               float finalX = (float) (this.x + finalDist
                     * Math.cos(radians));
               float finalY = (float) (this.y + finalDist
                     * Math.sin(radians));
 
               SingleExplosion circle = new SingleExplosion(this.color,
                     initX, initY, circleRadius);
               Timeline timeline = new Timeline(circle);
               timeline.addPropertyToInterpolate("x", initX, finalX);
               timeline.addPropertyToInterpolate("y", initY, finalY);
               timeline.addPropertyToInterpolate("opacity", 1.0f, 0.0f);
               timeline.setDuration(duration - 200
                     + (int) (400 * Math.random()));
               timeline.setEase(new Spline(0.4f));
 
               synchronized (this.circles) {
                  circles.add(circle);
               }
               scenario.addScenarioActor(timeline);
            }
         }
 
         return scenario;
      }
 
      public void paint(Graphics g) {
         synchronized (this.circles) {
            for (SingleExplosion circle : this.circles) {
               circle.paint(g);
            }
         }
      }
   }

The timeline scenario that implements this volley explosion:

  • Each single explosion is implemented as a separate timeline.
  • Scenario has random duration
  • Single explosions are created at almost evenly distributed angles (every 20 degrees) and at almost evenly distributed distance from the center (three for each angle).
  • The scenario has 54 different timelines, one for each single explosion.

Now we get to the main application class. It implements the following functionality:

  • Playing five explosion volleys (five timeline scenarios).
  • Waiting for all five to be done.
  • Playing another five – repeating the previous two steps.
  • Listening to the mouse events, suspending the currently playing scenarios on mouse press, and resuming them on mouse release.

The code starts by declaring the relevant data structures:

public class Fireworks extends JFrame {
   private Set volleys;

   private Map volleyScenarios;

   private JPanel mainPanel;

Here is the constructor of this class:

   public Fireworks() {
      this.mainPanel = new JPanel() {
         @Override
         protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            synchronized (volleys) {
               for (VolleyExplosion exp : volleys)
                  exp.paint(g);
            }
         }
      };
      this.mainPanel.setBackground(Color.black);
      this.mainPanel.setPreferredSize(new Dimension(480, 320));

      Timeline repaint = new SwingRepaintTimeline(this);
      repaint.playLoop(RepeatBehavior.LOOP);

      this.volleys = new HashSet();
      this.volleyScenarios = new HashMap();

      this.mainPanel.addMouseListener(new MouseAdapter() {
         @Override
         public void mousePressed(MouseEvent e) {
            synchronized (volleys) {
               for (TimelineScenario scenario : volleyScenarios.values())
                  scenario.suspend();
            }
         }

         @Override
         public void mouseReleased(MouseEvent e) {
            synchronized (volleys) {
               for (TimelineScenario scenario : volleyScenarios.values())
                  scenario.resume();
            }
         }
      });

      new Thread() {
         @Override
         public void run() {
            while (true) {
               if ((mainPanel.getWidth() > 0)
                     && (mainPanel.getHeight() > 0)) {
                  addExplosions(5);
               }
            }
         }
      }.start();

      this.add(mainPanel);
      this.pack();
      this.setLocationRelativeTo(null);
      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   }

We have:

  • A JPanel that paints all currently playing volley explosions on black background.
  • A looping timeline that repaints the contents of this application.
  • The data structures tracking the currently playing explosions.
  • The mouse listener that suspends the currently playing scenarios on mouse press and resumes them on mouse release.
  • A thread that adds five explosions in an infinite loop (see the explanation of addExplosions below)

Here is the method that makes sure that the volley explosions are run in batches of 5, even when they have different durations:

   private void addExplosions(int count) {
      final CountDownLatch latch = new CountDownLatch(count);

      for (int i = 0; i < count; i++) {
         int r = (int) (255 * Math.random());
         int g = (int) (100 + 155 * Math.random());
         int b = (int) (50 + 205 * Math.random());
         Color color = new Color(r, g, b);

         int x = 60 + (int) ((mainPanel.getWidth() - 120) * Math.random());
         int y = 60 + (int) ((mainPanel.getHeight() - 120) * Math.random());
         final VolleyExplosion exp = new VolleyExplosion(x, y, color);
         synchronized (volleys) {
            volleys.add(exp);
            TimelineScenario scenario = exp.getExplosionScenario();
            scenario.addCallback(new TimelineScenarioCallback() {
               @Override
               public void onTimelineScenarioDone() {
                  synchronized (volleys) {
                     volleys.remove(exp);
                     volleyScenarios.remove(exp);
                     latch.countDown();
                  }
               }
            });
            volleyScenarios.put(exp, scenario);
            scenario.play();
         }
      }

      try {
         latch.await();
      } catch (Exception exc) {
      }
   }

Here, we have:

  • A CountDownLatch that will be used to wait until all timeline scenarios that run the volley explosions are done
  • A random color and a random center location for each one of the volley explosions
  • A callback that notifies the count down latch when the timeline scenario is done
  • Waiting on the count down latch – until all timeline scenarios are done

And finally, the main method to launch the fireworks:

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            new Fireworks().setVisible(true);
         }
      });
   }

Click below for the WebStart demo


Related posts:

  1. Trident part 3 – timeline life cycle Over the course of the next few days i’m going to talk about different concepts...
  2. Trident part 4 – duration fraction and timeline position Over the course of the next few days i’m going to talk about different concepts...
  3. Animation blueprints for Swing – complex transition scenarios After adding such animation effects as fading, translucency, load progress, asynchronous load of images and...
  4. Animation blueprints for SWT – complex transition scenarios After adding such animation effects as fading, load progress, asynchronous load of images and smooth...


7 Comments on “Trident part 8 – timeline scenarios”

  1. 1 pavan said at 8:15 am on June 25th, 2009:

    Hi Krill,

    The webstart seems to be a problem

    Regards,
    Pavan

  2. 2 Tbee said at 9:09 am on June 25th, 2009:

    One suggestion, Kirill.

    I understand that coding timelines like this the correct way to implement an animation library. But after some experience in flash, I found the autonomous animation often more practical.

    With autonomous I mean that every animated object has internal timelines for each animatable property. So for example a rectange can have “animX(int)”. Calling that will start a timeline which does the animation with predefined settings, and when done cleans up the timeline. animX() is the animated version of setX().

    The advantage is, that if you have some kind of layout logic, and it tells a rectangle it manages to animX to a certain location, and while that animation is running, another event occurs changing the destination. You then do not end up with the timeline dance: having to stop all the old ones and starting new ones. So there is quite a administration going on. By encapsulating that in the animated object, that administration is implicit. The object will start a new timeline itself, starting from the position the old animation left it in.

    Basically this a decorator pattern; you decorate an object with encapsulated animated properties.

    Example:

    o.animX(100);

    when the object is at X=53 an event triggers:

    o.animX(30)

  3. 3 Kirill Grouchnikov said at 12:16 pm on June 25th, 2009:

    Pavan,

    Would you mind providing a little bit more information on the problems you’re experiencing with the WebStart link?

    Thanks
    Kirill

  4. 4 Kirill Grouchnikov said at 12:21 pm on June 25th, 2009:

    Tom,

    This is an interesting approach. Of course, it requires changing the implementation of the specific class (or supporting this on the UI toolkit level). One can think about exploring a base abstract class that would expose this functionality – internally using Trident to manage the timelines. A side effect would be to require the application classes to extend that base class.

    Thanks
    Kirill

  5. 5 pavan said at 12:53 pm on June 25th, 2009:

    Krill,
    The webstart link is working fine now.

    Thanks,
    Pavan

  6. 6 Tbee said at 1:07 pm on June 25th, 2009:

    Yes. Or maybe you could bolt in onto a layoutmanager, say “AnimatedLayout”. Suppose it would extend absolute layout and have a constraints implementation that uses Trident internally.

    lLayout.getConstraints(lComponent).animX(100);

    This would initially only make sense for x,y,w,h… But that is what is animated most.

    But how about:

    lLayout.getConstraints(lComponent).animForeground(Color.BLACK);

    A bit off maybe, but since the LM already handles x,y,w,h, why not put in the rest? Basically the layout manager does the layout and manages the timelines around its components.

    Would that make sense?

  7. 7 Kirill Grouchnikov said at 2:03 pm on June 25th, 2009:

    Tom,

    I don’t have a good answer for you right now, but this is something that i will keep in mind for future directions of Trident. Animating coordinates can certainly be within the realm of the layout manager, but beyond that it doesn’t sound right – that the layout manager will be responsible for animating the foreground color of the controls.

    Thanks
    Kirill