1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.anim;
15  
16  import demo.view.DemoDefaults;
17  import y.anim.AnimationFactory;
18  import y.anim.AnimationObject;
19  import y.anim.AnimationPlayer;
20  import y.anim.CompositeAnimationObject;
21  import y.base.EdgeCursor;
22  import y.base.Node;
23  import y.base.NodeCursor;
24  import y.geom.Geom;
25  import y.util.DefaultMutableValue2D;
26  import y.util.MutableValue2D;
27  import y.util.Value2DSettable;
28  import y.view.Arrow;
29  import y.view.BezierEdgeRealizer;
30  import y.view.Drawable;
31  import y.view.EdgeLabel;
32  import y.view.EdgeRealizer;
33  import y.view.Graph2D;
34  import y.view.Graph2DView;
35  import y.view.SmartEdgeLabelModel;
36  import y.view.ViewAnimationFactory;
37  
38  import javax.swing.JFrame;
39  import javax.swing.JPanel;
40  import javax.swing.JRootPane;
41  import javax.swing.Timer;
42  import java.awt.BorderLayout;
43  import java.awt.Color;
44  import java.awt.Graphics2D;
45  import java.awt.Rectangle;
46  import java.awt.EventQueue;
47  import java.awt.event.ActionEvent;
48  import java.awt.event.ActionListener;
49  import java.awt.geom.AffineTransform;
50  import java.awt.geom.GeneralPath;
51  
52  
53  /**
54   * Demonstrates how to animate label movement along an edge.
55   */
56  public class LabelAnimationDemo {
57    private static final int PREFERRED_DURATION = 10000;
58  
59    private final Graph2DView view;
60    private EdgeLabel[] labels;
61    private Timer timer;
62  
63    /**
64     * Creates a new LabelAnimationDemo and initializes a Timer that triggers the
65     * animation effects.
66     */
67    public LabelAnimationDemo() {
68      this.view = new Graph2DView();
69      this.view.setFitContentOnResize(true);
70      init();
71    }
72  
73    /**
74     * Initializes a <code>Graph2D</code> to hold edges along whose sides labels
75     * should be moved.
76     * Creates the labels to be moved.
77     */
78    private void init() {
79      final Graph2D graph = view.getGraph2D();
80  
81      // self loop
82      Node node;
83      node = graph.createNode(225, 125);
84      graph.getRealizer(node).setSize(0, 0);
85  
86      EdgeRealizer er;
87      er = graph.getRealizer(graph.createEdge(node, node));
88      er.setLineColor(Color.LIGHT_GRAY);
89      er.clearBends();
90      er.appendBend(225, 25);
91  
92  
93      final BezierEdgeRealizer ber = new BezierEdgeRealizer();
94      ber.appendBend(100, 0);
95      ber.appendBend(200, 400);
96      ber.appendBend(300, 100);
97      ber.setTargetArrow(Arrow.DELTA);
98      ber.setLineColor(Color.LIGHT_GRAY);
99      graph.createEdge(graph.createNode(50, 200), graph.createNode(400, 200),
100         ber.createCopy());
101 
102     ber.clearBends();
103     ber.appendBend(300, 250);
104     ber.appendBend(200, 550);
105     ber.appendBend(100, 250);
106     graph.createEdge(graph.createNode(400, 350), graph.createNode(50, 350),
107         ber.createCopy());
108 
109 
110     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
111       graph.getRealizer(nc.node()).setVisible(false);
112     }
113 
114     final String[] labelTexts = {
115         "Selfloop",
116         "A Label",
117         "Another Label"
118     };
119 
120     labels = new EdgeLabel[labelTexts.length];
121 
122     final EdgeCursor ec = graph.edges();
123     for (int i = 0, n = labelTexts.length; i < n; ++i, ec.next()) {
124       labels[i] = new EdgeLabel(labelTexts[i]);
125       labels[i].setLabelModel(new SmartEdgeLabelModel());
126       labels[i].setPosition(EdgeLabel.ANYWHERE);
127       labels[i].bindRealizer(graph.getRealizer(ec.edge()));
128     }
129 
130     node = graph.createNode(10, 10);
131     graph.getRealizer(node).setSize(0, 0);
132 
133     er = graph.getRealizer(graph.createEdge(node, node));
134     er.setLineColor(Color.LIGHT_GRAY);
135     er.clearBends();
136     er.appendBend(470, 10);
137     er.appendBend(470, 470);
138     er.appendBend(10, 470);
139 
140 
141     timer = new Timer(PREFERRED_DURATION + 500,
142         new ActionListener() {
143           private boolean invert;
144 
145           public void actionPerformed(final ActionEvent e) {
146             play(invert);
147             invert = !invert;
148           }
149         });
150     timer.setInitialDelay(1000);
151     timer.start();
152   }
153 
154   /**
155    * Plays the movement animation.
156    *
157    * @param invert   if <code>true</code> the labels move from right to left;
158    *                 otherwise the labels move from left to right.
159    */
160   private void play(final boolean invert) {
161     final Graph2D graph = view.getGraph2D();
162     EdgeRealizer er;
163 
164     final EdgeCursor ec = graph.edges();
165     int i = 0;
166 
167     final ViewAnimationFactory factory = new ViewAnimationFactory(view);
168 
169     // let's start with a simple variant:
170     // move a label along an edge with the center of the label's
171     // bounding box being centered on the edge path
172 
173     // get the realizer of the edge along which we want to move a label
174     er = graph.getRealizer(ec.edge());
175     ec.next();
176 
177     // create a Drawable representation of the label we want to move.
178     final Drawable selfloopLabel = ViewAnimationFactory.createDrawable(labels[i++]);
179 
180     // create an animation that moves the previously created Drawable along
181     // the chosen edge path
182     final AnimationObject selfloopAnimation =
183         factory.traversePath(er.getPath(), false, selfloopLabel, true, false,
184             PREFERRED_DURATION);
185 
186     // get the realizer of the edge along which we want to move a label
187     er = graph.getRealizer(ec.edge());
188     ec.next();
189 
190     // create a Drawable representation of the label we want to move.
191     final Drawable drawable = ViewAnimationFactory.createDrawable(labels[i++]);
192 
193     // create an animation that moves the previously created Drawable along
194     // the chosen edge path
195     final AnimationObject standard =
196         factory.traversePath(er.getPath(), invert, drawable, true, true,
197             PREFERRED_DURATION);
198 
199     // now we want to create a slightly more complex animation:
200     // we want the animation to start with the label's left end at the
201     // path's start point;
202     // we want the animation to stop, when the label's right end reaches
203     // the path's end point;
204     // and finally, we do not want the label to move on the edge path,
205     // but alongside it
206 
207     // get the realizer that provides the edge path
208     er = graph.getRealizer(ec.edge());
209     ec.next();
210     final GeneralPath path = er.getPath();
211 
212     // create a Drawable representation of the label we want to move.
213     final AnimationDrawable animationDrawable =
214         new AnimationDrawable(ViewAnimationFactory.createDrawable(labels[i]));
215 
216     // create a custom AnimationObject that updates the x offset of the
217     // previously created Drawable to match out start/stop requirements
218     final AnimationObject custom = new AnimationObject() {
219       private final AnimationObject delegate =
220           factory.traversePath(path, invert,
221               animationDrawable.createPositionMutator(),
222               animationDrawable.createDirectionMutator(),
223               PREFERRED_DURATION);
224       private final Value2DSettable internalOffset =
225           animationDrawable.createOffsetMutator();
226 
227       public void initAnimation() {
228         graph.addDrawable(animationDrawable);
229         delegate.initAnimation();
230       }
231 
232       public void calcFrame(final double time) {
233         internalOffset.setX(invert ? time : 1.0 - time);
234         delegate.calcFrame(time);
235       }
236 
237       public void disposeAnimation() {
238         delegate.disposeAnimation();
239         graph.removeDrawable(animationDrawable);
240       }
241 
242       public long preferredDuration() {
243         return delegate.preferredDuration();
244       }
245     };
246 
247 
248     er = graph.getRealizer(ec.edge());
249     final GeneralPath border = er.getPath();
250 
251     final CompositeAnimationObject arrows = AnimationFactory.createConcurrency();
252 
253     final AnimationObject NO_TIME = AnimationFactory.createPause(0);
254     final long pause = PREFERRED_DURATION / 19;
255     final long rest = PREFERRED_DURATION % 19;
256     final long duration = pause * 10 + rest;
257     for (int j = 0; j < 10; ++j) {
258       final Drawable arrowDrawable = new Drawable() {
259         private final Arrow arrow = Arrow.STANDARD;
260 
261         public void paint(final Graphics2D gfx) {
262           final Color oldColor = gfx.getColor();
263           gfx.setColor(Color.GRAY);
264           arrow.paint(gfx, 0, 0, 1, 0);
265           gfx.setColor(oldColor);
266         }
267 
268         public Rectangle getBounds() {
269           return arrow.getShape().getBounds();
270         }
271       };
272 
273       final CompositeAnimationObject sequence = AnimationFactory.createLazySequence();
274       sequence.addAnimation(AnimationFactory.createPause(j * pause));
275       sequence.addAnimation(
276           factory.traversePath(border, false, arrowDrawable, true, false,
277               duration));
278       sequence.addAnimation(NO_TIME);
279       arrows.addAnimation(sequence);
280     }
281 
282     final CompositeAnimationObject concurrency = AnimationFactory.createConcurrency();
283     concurrency.addAnimation(selfloopAnimation);
284     concurrency.addAnimation(standard);
285     concurrency.addAnimation(custom);
286     concurrency.addAnimation(arrows);
287 
288     final AnimationPlayer player = factory.createConfiguredPlayer();
289     player.animate(concurrency);
290   }
291 
292   /**
293    * Creates an application  frame for this demo
294    * and displays it. The given string is the title of
295    * the displayed frame.
296    */
297   private void start(final String title) {
298     final JFrame frame = new JFrame(title);
299 
300     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
301     addContentTo(frame.getRootPane());
302     frame.pack();
303     frame.setLocationRelativeTo(null);
304     frame.setVisible(true);
305   }
306 
307   public final void addContentTo(final JRootPane rootPane) {
308     final JPanel contentPane = new JPanel(new BorderLayout());
309     contentPane.add(view, BorderLayout.CENTER);
310 
311     rootPane.setContentPane(contentPane);
312   }
313 
314   public void dispose() {
315     if (timer != null) {
316       if (timer.isRunning()) {
317         timer.stop();
318       }
319       timer = null;
320     }
321   }
322 
323   public static void main(String[] args) {
324     EventQueue.invokeLater(new Runnable() {
325       public void run() {
326         DemoDefaults.initLnF();
327         (new LabelAnimationDemo()).start("Label Animations");
328       }
329     });
330   }
331 
332 
333   /**
334    * A <code>Drawable</code> implementation that supports movement and
335    * rotation.
336    */
337   private static final class AnimationDrawable implements Drawable {
338     private final Drawable wrappee;
339     private final Rectangle bounds;
340 
341     /** externally specifiable position */
342     private MutableValue2D position;
343 
344     /**
345      * determines the relative offset of the position with regards to the
346      * left side of the drawable's bounding box
347      */
348     private MutableValue2D offset;
349 
350     /** vector governing the rotation */
351     private MutableValue2D direction;
352 
353     AnimationDrawable(final Drawable wrappee) {
354       this.wrappee = wrappee;
355       final Rectangle wrappeeBounds = wrappee.getBounds();
356       this.position = DefaultMutableValue2D.create(wrappeeBounds.getCenterX(),
357           wrappeeBounds.getCenterY());
358       this.offset = DefaultMutableValue2D.create(0, 1);
359       this.direction = DefaultMutableValue2D.create();
360       this.bounds = new Rectangle(wrappeeBounds);
361     }
362 
363     public Rectangle getBounds() {
364       final Rectangle wrappeeBounds = wrappee.getBounds();
365       final double w = Math.ceil(wrappeeBounds.getWidth());
366       final double h = Math.ceil(wrappeeBounds.getHeight());
367 
368       final double x = position.getX();
369       final double y = position.getY();
370 
371       final AffineTransform transform = new AffineTransform();
372       transform.translate(x, y);
373       transform.rotate(radians());
374       transform.translate(-x, -y);
375 
376       Geom.calcTransformedBounds(x - w * offset.getX(),
377           y - h * offset.getY(),
378           w, h, transform, bounds);
379       return bounds;
380     }
381 
382     public void paint(final Graphics2D gfx) {
383       final AffineTransform oldAt = gfx.getTransform();
384       final Rectangle wrappeeBounds = wrappee.getBounds();
385       gfx.translate(position.getX(), position.getY());
386       gfx.rotate(radians());
387       gfx.translate(-wrappeeBounds.x - wrappeeBounds.getWidth() * offset.getX(),
388           -wrappeeBounds.y - wrappeeBounds.getHeight() * offset.getY());
389       wrappee.paint(gfx);
390       gfx.setTransform(oldAt);
391     }
392 
393     Value2DSettable createPositionMutator() {
394       return position;
395     }
396 
397     Value2DSettable createDirectionMutator() {
398       return direction;
399     }
400 
401     Value2DSettable createOffsetMutator() {
402       return offset;
403     }
404 
405     private static final double PI_HALF = Math.PI * 0.5;
406 
407     private double radians() {
408       double radians = Math.atan2(direction.getY(), direction.getX());
409       if (radians < -PI_HALF) {
410         radians += Math.PI;
411       } else if (radians > PI_HALF) {
412         radians -= Math.PI;
413       }
414       return radians;
415     }
416   }
417 }
418