changing rate of Animation/Transition - animation

I'm trying to do some basic animations, but am failing at the most simple things:
Rectangle rect = new Rectangle(100.0, 10.0);
mainPane.getChildren().add(rect); //so the rectangle is on screen
Animation anim = new Timeline(new KeyFrame(Duration.seconds(30.0),
new KeyValue(rect.widthProperty(), 0.0, Interpolator.LINEAR)));
rect.setOnMouseClicked(e -> {
if (anim.getStatus() == Status.RUNNING) {
anim.pause();
} else {
anim.setRate(Math.random() * 5.0);
anim.play();
System.out.println(anim.getRate());
}
});
The problem I am facing is that when I click the rectangle multiple times, the size will randomly jump around, instead of just changing the speed at which it drops. So for example, I let it run to about 50% size at speed ~2.5 and then stop it. When I start it up again, it will jump to a totally different size, smaller for a lower speed, bigger for a higher speed, so for example to ~20% for ~1.0 speed or ~80% for ~4.5 speed.
At first I thought animation was pre-calculated for the new speed and thus jumped to the position at which it would be, had it been played with the new speed from the beginning for the time that it was already playing before the pause, but it's bigger for a smaller speed, which doesn't really make sense then.
How do I change the speed/rate of an animation without having it jump around?

I think your diagnosis is correct: the current value is interpolated given the current time and current rate. If you decrease the rate without changing the current time, you are then earlier in the animation. Since the animation is shrinking this has the effect of making the rectangle bigger.
The easiest way is probably just to start a new animation each time:
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class VariableRateAnimation extends Application {
private Animation anim ;
#Override
public void start(Stage primaryStage) {
Pane mainPane = new Pane();
Rectangle rect = new Rectangle(100.0, 10.0);
mainPane.getChildren().add(rect); //so the rectangle is on screen
rect.setOnMouseClicked(e -> {
if (anim != null && anim.getStatus() == Status.RUNNING) {
System.out.println("Paused (" + anim.getTotalDuration().subtract(anim.getCurrentTime())+ " remaining)");
anim.pause();
} else {
Duration duration = Duration.seconds(30.0 * rect.getWidth() / (100 * Math.random() * 5.0));
System.out.println("Starting: ("+duration+ " to go)");
double currentWidth = rect.getWidth() ;
if (anim != null) {
anim.stop();
}
anim = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(rect.widthProperty(), currentWidth, Interpolator.LINEAR)),
new KeyFrame(duration, new KeyValue(rect.widthProperty(), 0.0, Interpolator.LINEAR)));
anim.play();
}
});
primaryStage.setScene(new Scene(mainPane, 600, 600));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Related

How to animate several nodes with pause between each one?

I am trying to animate a series of nodes one after the other in a loop. The goal is to have the first node begin its animation, followed by a short pause before the next node begins to animate.
However, when running this within a loop, it executes too fast and all nodes appear to be animating at the same time.
For simplicity, I am using the AnimateFX library to handle the animations, but I assume the functionality needed here would apply in other situations?
How would I add a pause between each of the HBox animations?
import animatefx.animation.Bounce;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class AnimationTest extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
final HBox tiles = new HBox(5);
tiles.setAlignment(Pos.CENTER);
// Create 5 tiles
for (int i = 0; i < 5; i++) {
HBox tile = new HBox();
tile.setPrefSize(50, 50);
tile.setStyle("-fx-border-color: black; -fx-background-color: lightblue");
tiles.getChildren().add(tile);
}
Button button = new Button("Animate");
button.setOnAction(event -> {
// Animate each tile, one at a time
for (Node child : tiles.getChildren()) {
Bounce animation = new Bounce(child);
animation.play();
}
});
root.getChildren().add(tiles);
root.getChildren().add(button);
primaryStage.setWidth(500);
primaryStage.setHeight(200);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
I don't know AnimateFX, but using the standard libraries you can add animations to a SequentialTransition.
For example, to animate each node but starting at a later time, add PauseTransitions of increasing duration and the desired animation to SequentialTransitions, and play the SequentialTransitions.
As I said, I'm not familiar with the library you're using, but I think it would look like this:
Button button = new Button("Animate");
button.setOnAction(event -> {
Duration offset = Duration.millis(500);
Duration start = new Duration();
// Animate each tile, one at a time
for (Node child : tiles.getChildren()) {
Bounce bounce = new Bounce(child);
PauseTransition delay = new PauseTransition(start);
SequentialTransition animation = new SequentialTransition(delay, bounce.getTimeline());
animation.play();
start = start.add(offset);
}
});

Javafx size of a Button after adding it to a Container

In case a button is added dynamically into a layout, the getWidth property adds back 0; However, the preferred size is reachable instantly. I'm assuming it's because the system didn't have a chance to calculate the size of the button ( since it's just added ).
Minimum reproducible example:
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class AgentApp extends Application {
public static void main(String[] args){
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
Stage tagTest = new Stage();
VBox topBox = new VBox();
Parent tagRoot = topBox;
Button btn = new Button("Add a Button dynamically");
btn.setOnAction(event -> {
Button dBtn = new Button("How big is this?!");
dBtn.setOnAction(event1 -> System.out.println("Width is actually: " + dBtn.getWidth())); /* (1) */
topBox.getChildren().add(dBtn);
System.out.println("Width:" + dBtn.getWidth()); /* (2) */
});
topBox.getChildren().add(btn);
Scene tagsScene = new Scene(tagRoot,400,200);
tagTest.setScene(tagsScene);
tagTest.show();
}
}
at (1) the width of the button is printed out correctly, while the width at (2) prints out 0.0. Which is unexpected.
Calling the layout function on the parent of the node ( topbox ) yields no results; Neither inheriting and calling the protected function layoutChildren in a custom container.
But somewhere down the line I assume the size itself must be calculated somewhere, since the size is calculated by the time a dBtn is pressed. How can the size calculation be forced at that point?
UPDATE:
Asking for the width asynchronously returns the correct size:
Button dBtn = new Button("How big is this?!");
dBtn.setOnAction(event1 -> System.out.println("Width is actually: " + dBtn.getWidth()));
topBox.getChildren().add(dBtn);
Platform.runLater(() -> System.out.println("btn width: " + btn.getWidth()));
But since that's an asynchronous call to be run in an unspecified time, the size is still not available instantly.
you have to add the Node into the Scene before calling the getWif

JavaFX draggable background - alternative to using a camera?

I am trying to implement a navigational feature for an editor I am working on, that would allow you to move around in the window by dragging on the background - basically like you can move around in Open Maps.
My current approach is to move a scene-camera around via DragEvent-Listeners on the scene, in which the displayed objects are children to the root Group.
However, I am wondering whether there is another way to implement this that would not require the use of a camera.
The following works as a simple test:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class InfinitePanning extends Application {
#Override
public void start(Stage primaryStage) {
Pane drawingPane = new Pane();
drawingPane.setStyle("-fx-background-color: white;");
Scene scene = new Scene(drawingPane, 800, 800, Color.WHITE);
scene.setOnScroll(e -> {
drawingPane.setTranslateX(drawingPane.getTranslateX() + e.getDeltaX());
drawingPane.setTranslateY(drawingPane.getTranslateY() + e.getDeltaY());
});
scene.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
Point2D center = drawingPane.sceneToLocal(new Point2D(e.getX(), e.getY()));
Circle c = new Circle(center.getX(), center.getY(), 25, Color.CORNFLOWERBLUE);
drawingPane.getChildren().add(c);
}
});
Circle c = new Circle(50, 50, 25, Color.CORNFLOWERBLUE);
drawingPane.getChildren().add(c);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Double-click to add a new circle, scroll with the mouse (scroll button/wheel or scroll gesture on a trackpad) to move around.
There are some subtleties here. The pane is initially sized to fit the scene; as you scroll around the mouse will be outside the bounds of the pane. If you double-click outside the bounds of the pane (so you add a new node whose parameters are outside the bounds), then the pane expands at that point to include the new child.

Increasing animation rate in keyframe method apparently disables the feature

I have searched and found nothing. I am trying to get a simple bouncing ball to speed up on a continuous basis. The code below does not speed the ball up at all (even with a mouse click), however the getRate() property does increase as expected. If I comment out the increaseSpeed() method call in moveBall() the mouse click will speed up the ball.
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
public class MyApp extends Application {
#Override
public void start(Stage primaryStage) {
Bounce bouncePane = new Bounce();
bouncePane.setOnMousePressed(e -> bouncePane.increaseSpeed());
Scene scene = new Scene(bouncePane, 250, 150);
primaryStage.setTitle("Bouncing Ball");
primaryStage.setScene(scene);
primaryStage.show();
bouncePane.requestFocus();
}
}
public class Bounce extends Pane {
public final double rad = 25;
private double x = rad, y = rad;
private double dx = 1, dy = 1;
private Circle ball = new Circle(x, y, rad);
private Timeline anim;
public Bounce() {
ball.setFill(Color.BLUE);
getChildren().add(ball);
anim = new Timeline(
new KeyFrame(Duration.millis(50), e -> moveBall()));
anim.setCycleCount(Timeline.INDEFINITE);
anim.play();
}
public void increaseSpeed() {
anim.setRate(anim.getRate() + 0.5);
System.out.println(anim.getRate());
}
protected void moveBall() {
if (x < rad || x > getWidth() - rad) {
dx *= -1;
}
if (y < rad || y > getHeight() - rad) {
dy *= -1;
}
x += dx;
y += dy;
ball.setCenterX(x);
ball.setCenterY(y);
increaseSpeed();
}
}
Your application actually works (kinda)
If you set the duration of your KeyFrame to Duration.millis(1_000) instead of Duration.millis(50), then you will see the animation speed up (well up to a factor of 1_000/60).
Huh? What's going on
By default JavaFX is capped at 60 frames per second. The key frames in your timeline will not be invoked any more often than that, no matter what you set the playback rate to. Because in your example you set the initial duration of the animation to 50 milliseconds, then increase rate by 50 percent of the original rate in each frame, the animation callback for the key frame quickly (in a fraction of time imperceptible to the human eye) reaches the maximum frame rate of 60 frames per second and once it does that, you can't run it any faster.
What you can do
For this kind of problem, perhaps don't rely on animation playback rate to control the speed of your object. Instead, implement a "game loop" using an AnimationTimer and associate a velocity with your objects and move the object to the required position based upon the loop such as is done in this AnimationTimerTest by JamesD.

How to make a dynamic image at run time?

I'm working on a card game based on the NetBeans platform and I'm struggling to get my head around dynamic images. Why dynamic? Well I want the cards to adjust at run time to changes to the page (i.e. name, text, cost, etc).
My first hack at it was creating a component (JPanel) with labels pre-placed where I loaded the text/image based on the card values. That seems to work fine but then it became troublesome when I thought about some pages having a different look in later editions (meaning not everything would be on the same place).
So I'm trying to get an idea about ways to do this based on some kind of template.
Any idea?
There's a follow-up question at: JList of cards?
Finally I got some time to get back to this and was able to figure out a way using Java 2D tutorial.
The pictures are not near what I will use in my application but serves as proof of concept.
package javaapplication3;
import java.awt.*; import java.awt.font.FontRenderContext; import
java.awt.font.LineBreakMeasurer; import java.awt.font.TextAttribute;
import java.awt.font.TextLayout; import java.awt.image.BufferedImage;
import java.io.File; import java.io.IOException; import
java.net.MalformedURLException; import java.net.URL; import
java.text.AttributedCharacterIterator; import
java.text.AttributedString; import java.util.ArrayList; import
java.util.HashMap; import java.util.logging.Level; import
java.util.logging.Logger; import javax.imageio.ImageIO;
/** * * #author Javier A. Ortiz Bultrón
*/ public class DefaultImageManager {
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
try {
// TODO code application logic here
DefaultImageManager manager = new DefaultImageManager();
URL url = DefaultImageManager.class.getResource("weather-rain.png");
manager.getLayers().add(ImageIO.read(url));
url = DefaultImageManager.class.getResource("weather-sun.png");
manager.getLayers().add(ImageIO.read(url));
manager.addText(new Font("Arial", Font.PLAIN, 10), "Many people believe that Vincent van Gogh painted his best works "
+ "during the two-year period he spent in Provence. Here is where he "
+ "painted The Starry Night--which some consider to be his greatest "
+ "work of all. However, as his artistic brilliance reached new "
+ "heights in Provence, his physical and mental health plummeted. ",
200, 150, new Point(0, 0));
manager.generate();
} catch (MalformedURLException ex) {
Logger.getLogger(DefaultImageManager.class.getName()).log(Level.SEVERE,
null, ex);
} catch (IOException ex) {
Logger.getLogger(DefaultImageManager.class.getName()).log(Level.SEVERE,
null, ex);
}
}
/**
* Layers used to create the final image
*/
private ArrayList layers = new ArrayList();
private ArrayList textLayers = new ArrayList();
/**
* #return the layers
*/
public ArrayList<BufferedImage> getLayers() {
return layers;
}
private Dimension getMaxSize() {
int width = 0, height = 0;
for (BufferedImage img : getLayers()) {
if (img.getWidth() > width) {
width = img.getWidth();
}
if (img.getHeight() > height) {
height = img.getHeight();
}
}
return new Dimension(width, height);
}
public void addText(Font font, String text, int height, int width, Point location) {
BufferedImage textImage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
HashMap<TextAttribute, Object> map =
new HashMap<TextAttribute, Object>();
map.put(TextAttribute.FAMILY, font.getFamily());
map.put(TextAttribute.SIZE, font.getSize());
map.put(TextAttribute.FOREGROUND, Color.BLACK);
AttributedString aString = new AttributedString(text, map);
AttributedCharacterIterator paragraph = aString.getIterator();
// index of the first character in the paragraph.
int paragraphStart = paragraph.getBeginIndex();
// index of the first character after the end of the paragraph.
int paragraphEnd = paragraph.getEndIndex();
Graphics2D graphics = textImage.createGraphics();
FontRenderContext frc = graphics.getFontRenderContext();
// The LineBreakMeasurer used to line-break the paragraph.
LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
// Set break width to width of Component.
float breakWidth = width;
float drawPosY = 0;
// Set position to the index of the first character in the paragraph.
lineMeasurer.setPosition(paragraphStart);
// Get lines until the entire paragraph has been displayed.
while (lineMeasurer.getPosition() < paragraphEnd) {
// Retrieve next layout. A cleverer program would also cache
// these layouts until the component is re-sized.
TextLayout layout = lineMeasurer.nextLayout(breakWidth);
// Compute pen x position. If the paragraph is right-to-left we
// will align the TextLayouts to the right edge of the panel.
// Note: this won't occur for the English text in this sample.
// Note: drawPosX is always where the LEFT of the text is placed.
float drawPosX = layout.isLeftToRight()
? 0 : breakWidth - layout.getAdvance();
// Move y-coordinate by the ascent of the layout.
drawPosY += layout.getAscent();
// Draw the TextLayout at (drawPosX, drawPosY).
layout.draw(graphics, drawPosX, drawPosY);
// Move y-coordinate in preparation for next layout.
drawPosY += layout.getDescent() + layout.getLeading();
}
getTextLayers().add(textImage);
}
public void generate() throws IOException {
Dimension size = getMaxSize();
BufferedImage finalImage = new BufferedImage(size.width, size.height,
BufferedImage.TYPE_INT_ARGB);
for (BufferedImage img : getLayers()) {
finalImage.createGraphics().drawImage(img,
0, 0, size.width, size.height,
0, 0, img.getWidth(null),
img.getHeight(null),
null);
}
for(BufferedImage text: getTextLayers()){
finalImage.createGraphics().drawImage(text,
0, 0, text.getWidth(), text.getHeight(),
0, 0, text.getWidth(null),
text.getHeight(null),
null);
}
File outputfile = new File("saved.png");
ImageIO.write(finalImage, "png", outputfile);
}
/**
* #return the textLayers
*/
public ArrayList<BufferedImage> getTextLayers() {
return textLayers;
}
/**
* #param textLayers the textLayers to set
*/
public void setTextLayers(ArrayList<BufferedImage> textLayers) {
this.textLayers = textLayers;
} }
It still needs some refining specially on the placement of the text but it works. I guess I can implement a xml format to store all this information so is easily configurable. In the example below suns are drawn on top of rain, and the text is on top of all that. For my application each layer will build together the page I want.
Here are the images I used:
And the final result:

Resources