June 25th, 2009

Trident part 8 – timeline scenarios

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