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