SwingStates proposes an empty canvas to draw interactive graphical scenes to define new widgets. A Canvas
has a rich graphical model and proposes different relations to link graphical shapes and to define the graphical rendering. It offers facilities to program gesture recognition and animations of graphical shapes. Programming interaction with a Canvas
is easily done thanks to state machines and tags. Canvas
class inherits from JPanel
so it can be used like any other Swing widget
The content of a SwingStates' Canvas is described by a display list containing graphical objects. Objects are displayed in the order of display list, i.e. if an object o1
is before an object o2
in the display list, o1
is rendered below o2
.
Each object belonging to the display list is an instance of a subclass of CShape
and has the following properties:
The main functions of a Canvas
are:
To create a graphical object and add it to the display list, use methods new* of Canvas
class:
1 CRectangle rect = canvas.newRectangle(100, 100, 50, 150); 2 CText text = canvas.newText(250, 250, "Hello world", new Font("verdana", Font.PLAIN, 12));
These methods create a graphical object, insert it to the end of canvas' display list and return the created graphical object. For example, ligne 1
is equivalent to:
CRectangle rect = new CRectangle(100, 100, 50, 150); canvas.addShape(rect);
Adding an object to a canvas inserts it at the end of the list, i.e. it is displayed above all the other objects of the canvas. Methods above
, aboveAll
, below
, belowAll
can be used to modify the order of graphical objects in display list.
To put an object o1
above an object o2
:
o1.above(o2);
To put an object o
above all other objects:
o.aboveAll();
Each graphical object has a boolean, drawable, specifying if the object is displayed or not. This is useful when one wants to hide an object temporarily, for instance a menu displayed only when the user invokes it.
Each graphical object o
can be clipped by another object clip
so it is visible only through its clip (i.e. only the intersection of o
with clip
is displayed).
The geometry of a graphical object is defined by:
CPolyline
: an arbitrary graphical shape defined by a path composed of a series of linear or curve segments, the outline can be closed or opened. (CPolyline
is more advanced than Java GeneralPath
, in particular see arcTo method to define arc segments is an arbitrary path)CSegment
: a segment defined by two pointsCRectangle
: a rectangle defined by its bounding box (upper left corner, width and height)CEllipse
: an ellipse, specified by its bounding boxCText
: text, specified by a string and the lower left point of the bounding box of this textCImage
: an image, specified by the name of a file containing the image (JPEG, GIF ou PNG) and the upper left point of this image.CWidget
/ CDynamicWidget
: a Swing widget that can be rendered in a canvas and that is repainted at each user event (CWidget
) or at a given frequency (CDynamicWidget
). It is specified by the underlying Swing widget (class and bounds) and the upper left point of this widget in the canvas.CShape
: any java.awt.Shape
.By default, coordinates of an object are relative to the display surface (the Canvas). The surface's origin is at upper left corner and the surface is pixel-unit. Each graphical object can be transformed. These transformations are defined by composing:
Each transformation is relative to a reference point defined by a ratio r. This ratio, r, is defined relative to the bounding box of the object (before the object being transformed). This reference point is not changed when a transformation is applied to the object. By default, this reference point is set to (1/2, 1/2), i.e. the transformations are relative to the center of the bounding box. The reference point (0, 0) corresponds to the upper left point of the bounding box and th reference point (1, 1) corresponds to the lower right point of the bounding box.
Each transformation have two versions:
translateBy
, rotateBy
, scaleBy
) that adds the transformation to the current transformation of the same type (for example, if an object is rotated by Math.PI/6, calling rotateBy(Math.PI/3) makes this object be rotated by Math.PI/2). translateTo
, rotateTo
, scaleTo
) that sets the current transformation to the specified transformation (for example, if an object is rotated by Math.PI/6, calling rotateTo(Math.PI/3) makes this object be rotated by Math.PI/3).Finally, a graphical object can have a parent, which is another graphical object belonging to the display list. When an object has a parent, its transform is interpreted relatively to the transform of the parent, i.e. affine transform of an object is cumulated with its parent's transform. For example, it is useful to attach all items of a contextual menu to a common parent so all items can be moved just by moving the parent.
A program to test transformationsThe following program illustrates available transformations on graphical objects of the canvas. A control panel allows to modify the current transformations and the reference point for each object. Each object has a ghost on which the transformation is not applied: the effect of each parameter of the transformations is easily visible.
Furthermore, all the objects have a common parent, the red square in the top left corner of the canvas. Drag it with the left mouse button to translate it and drag it with the right button to rotate it. As we explained above, the objects are modified when their parent is modified.
See applet sources.
The graphical attributes of an object define its rendering. they depend on the type of the object:
CShape
, CRectangle
, CEllipse
, CPolyline
, CWidget
and CDynamicWidget
): a fill color, an outline color, an outline style and transparency factors (fill transparency and outline transparency). One can specify if the shape is filled or not and if the shape is outlined or not.CSegment
): a color, a style and a transparency factor.CText
): a font, a font color, an outline color for its bounding box and transparency factors.CImage
): an outline color for its bounding box and transparency factors.
Colors are specified by objects that implement java.awt.Paint
Java interface. It includes simple colors (class java.awt.Color
) but also gradients (class java.awt.GradientPaint
) and textures (class java.awt.TexturePaint
).
The outline style is specified by an object that implements java.awt.Stroke
Java interface. It includes the width of the pen, the shape of the pen and the dotted style (e.g., class java.awt.BasicStroke
).
Fonts for CText
are specified by java.awt.Font
Java class.
Most of SwingStates methods return the object on which they are called so methods calls can be chained through a single instruction. For example:
rect.setFillPaint(Color.green).translateTo(50, 50);
replaces the two following instructions:
rect.setFillPaint(Color.green); rect.translateTo(50, 50);
Tags are a feature to easily manipulate and program interaction with groups of objects (e.g. some shapes can be selected while other not). A graphical object can have several tags and a tag can be added to several graphical objects.
WARNING: Adding a tag is effective only once a graphical object is added to canvas. Calls to method addTag
on an object before adding it to a canvas are ignored.
SwingStates proposes two kinds of tag:
CExtensionalTag
can be added to a graphical object using addTag
method:
CExtensionalTag selected = new CExtensionalTag(canvas) { }; ... CRectangle rect = canvas.newRectangle(100, 100, 50, 150); CText text = canvas.newText(250, 250, "Hello world", new Font("verdana", Font.PLAIN, 12)); rect.addTag(selected); text.addTag(selected);
CIntentionalTag
being registered on a canvas c tags all the shapes of c that satisfy the criterion specified specified criterion
method. The set of tagged shapes will be automatically computed according to this criterion each time this tag is used.
class OutlinedShapes extends CIntentionalTag { public OutlinedShapes(Canvas canvas) { super(canvas); } public boolean criterion(CShape s) { return s.isOutlined(); } } ... CIntentionalTag outlinedShapes = new OutlinedShapes(canvas);
Most of the methods available on a CShape
are also available on a CTag
so all shapes that share a tag t can be modified by modifying t. For example, the following instruction colors and translates all shapes tagged by outlinedShapes
:
outlinedShapes.setFillPaint(Color.LIGHT_GREEN).translateBy(50, 50);
Tags are java.util.Iterator
so they can also be browsed:
tag.reset(); while(tag.hasNext()) tag.nextShape().setTransparencyFill((float)Math.random());
This is convenient for enumerating the shapes that share a tag but SwingStates' programmers must take care of not modifying the collection of shapes tagged by t while iterating on t. Note that methods t.addTo(s)
, t.removeFrom(s)
in class CTag
modify the collection of shapes tagged by t
and that method removeShape(s)
in class Canvas
modify the collection of shapes of all tags that label s
.
Extensional tags can be active: each time an extensional tag is added to (or removed from) a shape, method added
(or removed
) of tag is called so one can override these methods to specify a given behavior. For example, selected graphical objects can be automatically outlined by a stroke of width 2 using this tag:
CExtensionalTag selected = new CExtensionalTag(canvas) { public void added(CShape s) { s.setOutlined(true).setStroke(new BasicStroke(2)); } public void removed(CShape s) { s.setStroke(new BasicStroke(1)); } }; ... CRectangle rect = canvas.newRectangle(100, 100, 50, 150); CText text = canvas.newText(250, 250, "Hello world", new Font("verdana", Font.PLAIN, 12)); rect.addTag(selected); text.addTag(selected);
SwingStates contains a special class of extensional tags, CNamedTag
, that can be referenced using a string. For example, one can directly add a tag using a string and get this tag by querying canvas for tag named by this string:
CRectangle rect = canvas.newRectangle(100, 100, 50, 150); CText text = canvas.newText(250, 250, "Hello world", new Font("verdana", Font.PLAIN, 12)); rect.addTag("selected"); text.addTag("selected"); ... canvas.getTag("selected").setFillPaint(Color.GREEN);
A CStateMachine
allows to handle all mouse and keyboard Java events occuring on a SwingStates' canvas. It inherits from BasicInputStateMachine
and proposes two other versions for each transition (*OnShape and *OnTag). It allows to easily identify three different contexts for input events: (i) on a shape having a given tag, (ii) on a shape or (iii) anywhere.
Picking is automatically done on a SwingStates' canvas so when an event occurs on a graphical object, e.g. a mouse press, *OnShape matching transition can be fired, e.g. PressOnShape(BUTTON1). The following machine differentiates mouse presses on a graphical object (selecting the object) and mouse presses on the background (creating a new object):
CStateMachine sm = new CStateMachine() { public State start = new State() { Transition pressOnShape = new PressOnShape(BUTTON1) { public void action() { getShape().addTag(selected); } }; Transition pressOnBackground = new Press(BUTTON1) { public void action() { canvas.newRectangle(getPoint().getX(), getPoint().getY(), 30, 20); } }; }; }; sm.attachTo(canvas);
Note that a mouse press on a graphical object can trigger a PressOnShape
transition but also a Press
transition. Using these transitions in the order PressOnShape
--> Press
ensures that any mouse press that triggers Press
has not occurred on a shape but using the order Press
--> PressOnShape
makes transition PressOnShape
never triggered since any mouse press always matches the Press
transition.
Each shape can be pickable or not (by using method setPickable
). By default, every shape is pickable so it is taken into account while performing picking and can thus trigger *OnShape transitions. Restricting the set of pickable shapes to only the shapes that are involved in interaction can really improve performances. For example, shapes used as decorations in the background must not be pickable. Consider we want to highlight a labeled box when the cursor is over it, we write the following code:
// Drawing a CText over a CRectangle to build a labeled box CText label = canvas.newText(50, 50, "Label"); CRectangle box = canvas.newRectangle(label.getMinX() - 10, label.getMinY() - 10, 20, 20); label.above(box); CStateMachine sm = new CStateMachine() { Paint initColor; public State out = new State() { Transition enterBox = new EnterOnShape(">> in") { public void action() { initColor = getShape().getFillPaint(); getShape().setFillPaint(Color.YELLOW); } }; }; public State in = new State() { Transition leaveBox = new LeaveOnShape(">> out") { public void action() { getShape().setFillPaint(initColor); } }; }; }; sm.attachTo(canvas);
The result is not the one expected: when the mouse enters the label, the label is highlighted and the background unhighlighted. To solve this problem, the label can be set as non pickable so it will be ignored during the picking process and won't trigger EnterOnShape transition.
// Drawing a CText over a CRectangle to build a labeled box ... label.above(box).setPickable(false);
*OnTag transitions are triggered by events occuring on shapes having a given tag. The tag is specified as an argument of the *OnTag transition. This argument can be the tag itself, the name of the tag (only for CNamedTag
tags) or the class of the tag.
For example, the machine in the following code implements a selection mechanism by pressing on graphical shapes (pressing on a shape selects it and pressing on a selected shape deselects it):
CExtensionalTag selected = new CExtensionalTag(canvas) { public void added(CShape s) { s.setOutlined(true).setStroke(new BasicStroke(2)); } public void removed(CShape s) { s.setStroke(new BasicStroke(1)); } }; ... CRectangle rect = canvas.newRectangle(100, 100, 50, 150); CText text = canvas.newText(250, 250, "Hello world", new Font("verdana", Font.PLAIN, 12)); rect.setOutlined(true); text.setOutlined(false); ... CStateMachine sm = new CStateMachine() { public State start = new State() { Transition pressOnShape = new PressOnTag(selected, BUTTON1) { public void action() { getShape().removeTag(selected); } }; Transition pressOnBackground = new PressOnShape(BUTTON1) { public void action() { getShape().addTag(selected); } }; Transition pressOnBackground = new Press(BUTTON1) { public void action() { canvas.newRectangle(getPoint().getX(), getPoint().getY(), 30, 20); } }; }; }; sm.attachTo(canvas);
The selected
tag makes a selected shape be outlined with a stroke of width 2. However, in this version, a shape which was originally not outlined will stay outlined while it has been deselected. Another solution consists in using one tag per shape and reference the set of tags by the class of these tags in the state machine. Each tag stores the original outline state of its single shape and restores it when it is removed from the shape:
class Selected extends CExtensionalTag { boolean outlined; public Selected(Canvas canvas) { super(canvas); } public void added(CShape s) { // The initial visibility of the shape's outline is stored when the tag is added... outlined = s.getOutlined(); s.setOutlined(true).setStroke(new BasicStroke(2)); } public void removed(CShape s) { // ...so it can be restored when the tag is removed. s.setOutlined(outlined).setStroke(new BasicStroke(1)); } }; ... CRectangle rect = canvas.newRectangle(100, 100, 50, 150); CText text = canvas.newText(250, 250, "Hello world", new Font("verdana", Font.PLAIN, 12)); rect.setOutlined(true); text.setOutlined(false); ... CStateMachine sm = new CStateMachine() { public State start = new State() { // This transition is triggered by any mouse press on a shape having a tag of class Selected Transition pressOnShape = new PressOnTag(Selected.class, BUTTON1) { public void action() { getShape().removeTag(getTag()); } }; Transition pressOnBackground = new PressOnShape(BUTTON1) { public void action() { // Attaching a different instance of Selected tag to each shape getShape().addTag(new Selected()); } }; Transition pressOnBackground = new Press(BUTTON1) { public void action() { canvas.newRectangle(getPoint().getX(), getPoint().getY(), 30, 20); } }; }; }; sm.attachTo(canvas);
CStateMachine
to a shape or a tag
CStateMachine
s can be attached to only given CElement
s (method attachTo(CElement element)
). A CElement
is a shape, a tag or a canvas. Transitions *OnShape
and *OnTag
will be fired only for pickable shapes which are also parts of the attached CElement
s.
For example, the transition t1
will be fired only if a mouse press occurs on a shape that holds the tags movable
and highlitable
while the transition t2
will still be fired by any mouse press event on the canvas:
CStateMachine machine = new CStateMachine() { State start = new State() { Transition t1 = new PressOnShape(BUTTON1, ">> moveShape") { public void action() { highlight(getShape()); ... } }; Transition t2 = new Press(BUTTON1, ">> panView") { ... }; }; State moveShape = new State() { ... }; State panView = new State() { ... }; ... }; ... CTag movable = ...; CTag highlitable = ...; ... machine.attachTo(movable); machine.attachTo(highlitable);
In this section, we build a simple widget in which we can create new shapes, copy, cut and paste shapes using two types of menu: a linear contextual menu invoked by a right click and a marking menu invoked by a left click as illustrated by the applet below. Each menu contains four commands: "new", "cut", "copy" and "paste".
See applet sources: LinearMenu, Menu, MarkingMenu, GraphicalEditor and applet.
Let's start by defining the rendering of a linear menu. A menu item is a rectangle and a text inside of it:
CRectangle bgItem = canvas.newRectangle(0, i*20, 50, 20); bgItem.setFillPaint(Menu.BG_COLOR).setOutlinePaint(Menu.BORDER_COLOR); CText labelItem = (CText) canvas.newText(0, 0, items[i], Menu.FONT); // The text is centered vertically on its background rectangle with a pad x of 3 pixels labelItem.setReferencePoint(0, 0.5).translateTo(3, bgItem.getCenterY());
Because the menu location will vary according to user's mouse presses, we use a parent shape for all graphical objects of a menu that we will translate to move the whole menu.
parent = canvas.newRectangle(-1, -1, 2, 2); // the parent is not displayed, it is simply use to apply a transform to all menu shapes parent.setDrawable(false); parent.addChild(labelItem).addChild(bgItem);
Also, the menu will not always be visible so we attach a tag to each of its graphical elements that we will use to make the menu visible or not.
tagWhole = new CExtensionalTag(canvas) { }; bgItem.addTag(tagWhole); labelItem.addTag(tagWhole); ... // hide the whole menu tagWhole.setDrawable(false);
***
Let's now define interaction with a linear menu. First, we want to define a machine to highlight an item when the mouse cursor passes over it. Highlighting an item means changing the color of its rectangle background so we can add a tag to each background shape that we can use in the state machine for highlighting. To avoid a flickering effect when passes over the text of an item, we set the pickable attribute of text items to false.
bgItem.addTag("menuItem"); labelItem.setPickable(false); ... CStateMachine hilite = new CStateMachine() { public State out = new State() { Transition hiliteItem = new EnterOnTag("menuItem", ">> in") { public void action() { getShape().setFillPaint(Menu.HILITE_COLOR); } }; }; public State in = new State() { Transition unhiliteItem = new LeaveOnTag("menuItem", ">> out") { public void action() { getShape().setFillPaint(Menu.BG_COLOR); } }; }; };
Now, we define interaction for selecting an item in the menu. Because we want to be able to retrieve the text of an item, we use one tag by item that contains the text of this item rather than a simple string tag "menuItem".
public class MenuItem extends CNamedTag { public MenuItem(String nameItem) { super(nameItem); } } bgItem.addTag(tagWhole).addTag(new MenuItem(labelItem.getText())); tagLabels = new CExtensionalTag(canvas) { }; labelItem.addTag(tagLabels); ... interaction = new CStateMachine() { public State menuOff = new State() { Transition invoke = new Press(BUTTON3, ">> menuOn") { public void action() { pInit = getPoint(); showMenu(pInit); } }; }; public State menuOn = new State() { Transition select = new ReleaseOnTag(MenuItem.class, BUTTON3, ">> menuOff") { public void action() { String item = ((MenuItem)getTag()).getName(); System.out.println(item+" selected"); } }; Transition out = new Release(">> menuOff") { }; public void leave() { hideMenu(); } }; }; void showMenu(Point2D pt) { // translate the whole menu to pt parent.translateTo(pt.getX(), pt.getY()); // make the whole menu visible and make it interactive (but not item labels) tagWhole.setDrawable(true).setPickable(true); tagLabels.setPickable(false); // put the menu on top of other shapes, make sure item labels above item backgrounds. tagWhole.aboveAll(); tagLabels.aboveAll(); } void hideMenu() { // make the whole menu visible and make it non interactive // (otherwise its shapes could interfere with other interaction techniques) tagWhole.setDrawable(false).setPickable(false); }
The main difference between the rendering of circular menu and a linear menu is the use of a CPolyLine
to have a background as a pie slice instead of a rectangle and use of a clipping disc shape to obtain an empty zone in the center of the menu.
CPolyLine bgItem = canvas.newPolyLine(0, 0).lineTo(50,0).arcTo(0, -angleStep, 50, 50).close(); (*) CShape clipCircle = (new CEllipse(-50, -50, 100, 100)).getSubtraction(new CEllipse(-30, -30, 60, 60)); bgItem.setClip(clipCircle);
(*) see this page to get further explanations about arcTo
method.
SwingStates implements a mechanism of gesture recognition. Package fr.lri.swingstates.gestures.rubine
implements Rubine's algorithm for gesture recognition [Rubine91] while fr.lri.swingstates.gestures.dollar1
implements algorithm of the $1 recognizer [Wobbrock07]. Both algorithms allow to train a recognizer by drawing series of examples for each class of gestures. This page presents the Training application to create vocabulary of gestures by drawing examples.
Once a classifier is built and saved in a file, e.g. 'vocabulary.cl', it can be loaded in a program using the following instruction:
Classifier classifier = RubineClassifier.newClassifier("vocabulary.cl");to use a classifier implemented with Rubine's algorithm or the following one:
Classifier classifier = Dollar1Classifier.newClassifier("vocabulary.cl");to use a $1 classifier.
The method classify
of a classifier takes a Gesture
as input to output the name of the recognized class. A Gesture
is a simple series of timed points that is built using the method addPoint
. The following applet recognizes four classes of gestures: "new" (stroke a 'N'), a 'V' for a "cut" gesture, a '/\' for a "copy" and a '/' for a "paste" gesture. inputs gesture using a drag interaction. When the user presses the mouse button, he starts to draw a new gesture. Then each mouse drag defines a new point for the current gesture until mouse button stays pressed. Releasing the mouse button ends the current gesture which is sent to the classifier to get the name of the recognized class. To provide feedback to the user, this applet draws the ink trail of the current gesture by using a CPolyLine
on a canvas. The name of the recognized class is printed in the upper left corner. to draw the current gesture to provide feedback. It displays the name of recognized gesture each time the user releases the mouse button.
See applet sources.
SwingStates contains a simple motor of animations for implementing continuous changes for shapes, tags or the whole canvas. Any animation is an Animation
object that has the following attributes:
delay
nbLaps
duration
function
An animation lap lasts duration
milliseconds. During an odd lap, the parameter t
is smoothly changed from 0 to 1 while during an even lap, t
is smoothly changed from 1 to 0 while during an even lap in a lap of duration
. The value of t
is updated every delay
milliseconds and is set according to the pacing function function
(linear function of time or sigmoid function of time). By default, an animation is composed of one lap that lasts 1000 milliseconds and t
is updated every 40 milliseconds according to a linear pacing function.
SwingStates contains some predefined classes for animating style attributes (AnimationFillPaint
, AnimationOutlinePaint
...) and geometrical attributes (AnimationScale
, AnimationTranslate
...). There are two versions for each of the geometrical animations: absolute (e.g. AnimationTranslateTo
) and relative (e.g. AnimationTranslateBy
). Absolute animations are one-lap animations, e.g. AnimationTranslateTo(x,y)
smoothly translates a shape from its current location to (x,y)
, while relative animations has an infinite number of laps, ignores the value of t
and continuously changes a geometrical feature by a delta, e.g. AnimationTranslateTo(dx,dy)
continuously translates a shape by vector (dx,dy)
. Animations for style only exist in absolute version.
For example, the following code progressively fills the background in gray. We first create a pixel unit square and then animate it with an AnimationScaleTo
so it fills the whole canvas. Use sliders to test different values for animation parameters and press left mouse button anywhere in the canvas to start the animation.
// a rectangle in the center of view animatedShape = canvas.newRectangle(100, 150, 1, 1).setOutlined(false); // an animation to magnify the rectangle so it fills the canvas animationDemo = new AnimationScaleTo(canvas.getWidth(), canvas.getHeight()); // associate animation to a shape and start animation animatedShape.animate(animationDemo);
See applet sources.
To create your own animations, you can create a class that inherits from Animation
and override method step(double t)
to implement animation behavior according to t
value. This method is called each time the animation is updated. For example, we want to change the color from gray to yellow when the shape fills the view. We extend AnimationTranslateTo
class and implement step
method to change the filling color:
class ScaleAndColor extends AnimationScaleTo { public ReboundingBall(double sx, double sy) { super(sx, sy); } public void step(double t) { super.step(t); getAnimated().setFillPaint(new Color( (int)(Color.YELLOW.getRed()*t + Color.LIGHT_GRAY.getRed()*(1-t)), (int)(Color.YELLOW.getGreen()*t + Color.LIGHT_GRAY.getGreen()*(1-t)), (int)(Color.YELLOW.getBlue()*t + Color.LIGHT_GRAY.getBlue()*(1-t)))); } }
In the following example, our canvas contains a rebounding ball and we suppose having a ReboundingBall
animation that handles collisions on canvas edges:
// ReboundingBall is a subclass of AnimationTranslateBy that handles collisions on canvas edges AnimationTranslateBy animBall = new ReboundingBall(-10, 10, 400, 300); CEllipse grayBall = canvas.newEllipse(50, 50, 20, 20); grayBall.animate(animBall);
Now, we want the user being able to create balls that vertically rebound. He can click on the canvas to create a new ball. We also want that the new created ball first fills in red and then starts to rebound. So we want rebounding animation starts just after filling animation finishes. One way of doing it is by overriding doStop
method of our filling animation to make the rebounding animation start. Another solution consists in using Animation* transitions in a state machine. Each time an animation starts, stops, is suspended or is resumed, the respective AnimationStarted
, AnimationStopped
, AnimationSuspended
or AnimationResumed
are triggered.
Animation fillRed = new AnimationFillPaint(Color.RED); fillRed.setLapDuration(2000).setNbLaps(1); CStateMachine smBall = new CStateMachine() { ReboundingBall animRebound; public State idle = new State() { Transition newBall = new Press(BUTTON1, ">> prepareBall") { public void action() { CEllipse newBall = newEllipse(getPoint().getX(), getPoint().getY(), 20, 20); fillRed.setAnimatedElement(newBall).start(); } }; Transition endCollide = new AnimationStopped(fillRed, ">> idle") { public void action() { animRebound = new ReboundingBall(0, 10, 400, 300); animRebound.setAnimatedElement(getAnimation().getAnimated()).start(); } }; }; }; smBall.attachTo(canvas);
As graphical objects, animations can be tagged. It is especially useful to manage several animations in parallel. On the previous example, we have used only one animation for filling each new created shape. If we create two shapes in a row, the second created shape will make the animation for the first shape stops: animation will restart on second shape and won't finish its job for the first shape. To solve this problem, we create one animation per shape and tag all these animation using the tag animationsFillRed
. We use this tag in the machine to track each time one of these animations ends to start a rebounding animation for the given shape.
AExtensionalTag animationsFillRed = new AExtensionalTag(); CStateMachine smBall = new CStateMachine() { ReboundingBall animRebound; public State idle = new State() { Transition prepareBall = new Press(BUTTON1) { public void action() { CEllipse newBall = newEllipse(getPoint().getX(), getPoint().getY(), 20, 20); AnimationFillPaint fillRed = new AnimationFillPaint(Color.RED); fillRed.setLapDuration(2000).setNbLaps(1).addTag(animationsFillRed); newBall.animate(fillRed); } }; Transition ballReady = new AnimationStopped(animationsFillRed) { public void action() { animRebound = new ReboundingBall(0, 10, 400, 300); animRebound.setAnimatedElement(getAnimation().getAnimated()).start(); } }; }; }; smBall.attachTo(canvas);
See applet sources.