i have a problem with the Timeline in JavaFX : the Timeline is locked at 1fps.
KeyFrames aren't triggerred more than this, even if i've put three keyframes :
60 times per second
120 times per second
1 time per second
They're all triggered at the same time : 1 second
TickSystem class :
package TickSystem;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
public class TickSystem implements EventHandler<ActionEvent> {
private KeyFrame kfU; // update
private KeyFrame kfD; // draw
private KeyFrame kfFPS; // FPS count
public Rectangle r;
public int curFrame = 0;
public int tick = 0;
public final Timeline gameLoop = new Timeline(120);
public final Duration updateTime = Duration.millis((double)1000/60); // 60 times per seconds
public final Duration drawTime = Duration.millis((double)1000/120); // 120 times per seconds
public int fps;
private int lastFrames = 0;
public TickSystem(Rectangle r){
this.r = r;
this.kfU = new KeyFrame(updateTime,"tickKeyUpdate", this::handle);
this.kfD = new KeyFrame(drawTime,"tickKeyDraw", this::handleDraw);
this.kfFPS = new KeyFrame(Duration.seconds(1),"tickKeyFPS", this::handleFPS);
this.gameLoop.setCycleCount(Timeline.INDEFINITE);
this.gameLoop.getKeyFrames().add(this.kfU);
this.gameLoop.getKeyFrames().add(this.kfD);
this.gameLoop.getKeyFrames().add(this.kfFPS);
}
public void start(){
this.gameLoop.play();
}
public void pause(){
this.gameLoop.pause();
}
public void stop(){
this.gameLoop.stop();
}
#Override
public void handle(ActionEvent ae) { // for update
this.tick++;
}
public void handleDraw(ActionEvent ae){ // for draw
this.curFrame++;
this.r.setWidth(curFrame);
}
public void handleFPS(ActionEvent ae) { // for FPS
this.fps = this.curFrame - this.lastFrames;
this.lastFrames = this.curFrame;
System.out.println(this.fps);
}
}
Main class :
package TickSystem;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Data");
primaryStage.setResizable(true);
Group root = new Group();
Scene scene = new Scene(root,400,400);
Rectangle r = new Rectangle(10,10,100,100);
r.setFill(Color.RED);
root.getChildren().add(r);
TickSystem loop = new TickSystem(r);
primaryStage.setScene(scene);
primaryStage.show();
loop.start();
}
}
So, the rectangle must gain 1px on width each time the handleDraw function is called, so 120 times per second.
Actually, he only gain one pixel per second. And i'm at 1fps on the handleFPS function.
This function must print the number of times the handleDraw function has been called each seconds
EDIT :
I've made these three KeyFrames cause i try to make a 2D game.
I've already make a good part of this game on Java with Swing and i need to update infos (like player poisition) 60 times per second but i try to draw informations on screen 120 times per second.
JavaFX sounds better to me for the GUI, so i left java swing behind.
These class are for testing and i'm new on JavaFX. Thanks for your time.
You have three key frames; one at 1/120 second, one at 1/60 second, and one at 1 second. Since the longest duration of any key frame is one second, the duration of one cycle of the timeline is one second.
Therefore, during one cycle of the timeline, the following three things happen:
At 1/120 second, handleDraw() is invoked
At 1/60 second, handle() is invoked
At 1 second, handleFPS() is invoked
So during one cycle of the timeline (1 second), handleDraw() and handle() are invoked once each.
You set the cycle count to INDEFINITE, so once one cycle is completed, it repeats; this happens indefinitely.
One solution is to use a separate timeline for each of the individual tasks. This will not add any appreciable overhead to the application.
(As an aside: there is no point here in implementing EventHandler. You never use an instance of TickSystem as an event handler; you only use the lambda expressions.)
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
public class TickSystem {
private KeyFrame kfU; // update
private KeyFrame kfD; // draw
private KeyFrame kfFPS; // FPS count
public Rectangle r;
public int curFrame = 0;
public int tick = 0;
public final Timeline gameLoop = new Timeline(60);
private final Timeline drawLoop = new Timeline(120);
private final Timeline fpsLoop = new Timeline(1000);
public final Duration updateTime = Duration.millis((double)1000/60); // 60 times per seconds
public final Duration drawTime = Duration.millis((double)1000/120); // 120 times per seconds
public int fps;
private int lastFrames = 0;
public TickSystem(Rectangle r){
this.r = r;
this.kfU = new KeyFrame(updateTime,"tickKeyUpdate", this::handleUpdate);
this.kfD = new KeyFrame(drawTime,"tickKeyDraw", this::handleDraw);
this.kfFPS = new KeyFrame(Duration.seconds(1),"tickKeyFPS", this::handleFPS);
this.gameLoop.setCycleCount(Timeline.INDEFINITE);
this.drawLoop.setCycleCount(Timeline.INDEFINITE);
this.fpsLoop.setCycleCount(Timeline.INDEFINITE);
this.fpsLoop.getKeyFrames().add(this.kfFPS);
this.gameLoop.getKeyFrames().add(this.kfU);
this.drawLoop.getKeyFrames().add(this.kfD);
}
public void start(){
this.gameLoop.play();
this.fpsLoop.play();
this.drawLoop.play();
}
public void pause(){
this.gameLoop.pause();
this.fpsLoop.pause();
this.drawLoop.pause();
}
public void stop(){
this.gameLoop.stop();
this.drawLoop.stop();
this.fpsLoop.stop();
}
public void handleUpdate(ActionEvent ae) { // for update
this.tick++;
}
public void handleDraw(ActionEvent ae){ // for draw
this.curFrame++;
this.r.setWidth(curFrame);
}
public void handleFPS(ActionEvent ae) { // for FPS
this.fps = this.curFrame - this.lastFrames;
this.lastFrames = this.curFrame;
System.out.println(this.fps);
}
}
Or, more succinctly:
public class TickSystem {
private Rectangle r;
private int curFrame = 0;
private int tick = 0;
private final List<Timeline> timelines = new ArrayList<>();
private int fps;
private int lastFrames = 0;
public TickSystem(Rectangle r){
this.r = r;
timelines.add(createTimeline(60, this::handleUpdate));
timelines.add(createTimeline(120, this::handleDraw));
timelines.add(createTimeline(1, this::handleFPS));
}
private Timeline createTimeline(int frequency, EventHandler<ActionEvent> handler) {
Timeline timeline = new Timeline(frequency);
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(1000.0 / frequency), handler));
timeline.setCycleCount(Animation.INDEFINITE);
return timeline;
}
public void start(){
timelines.forEach(Timeline::play);
}
public void pause(){
timelines.forEach(Timeline::pause);
}
public void stop(){
timelines.forEach(Timeline::stop);
}
public void handleUpdate(ActionEvent ae) { // for update
this.tick++;
}
public void handleDraw(ActionEvent ae){ // for draw
this.curFrame++;
this.r.setWidth(curFrame);
}
public void handleFPS(ActionEvent ae) { // for FPS
this.fps = this.curFrame - this.lastFrames;
this.lastFrames = this.curFrame;
System.out.println(this.fps);
}
}
Another solution would be to use an AnimationTimer: see https://stackoverflow.com/a/60685975/2189127
You should also note that your FPS keyframe/timeline is not really measuring frames per second, in the sense of how frequently the scene graph is repainted. It is only measuring how frequently the width of the rectangle is updated (how often the property value is changed).
I'm going to create a moving circle for my later project, and the circle will keep moving, and it interior color will change like color emitting , the changing color will from little circle to larger circle in 5 levels, so how to keep each color change to stay a while and I hope these code present with thread, so I create two thread for the purpose, one control circle moving, another control the circle's interior color emit
here is my code:
import java.awt.*;
import static java.awt.Color.black;
import static java.awt.Color.yellow;
import static java.awt.FlowLayout.RIGHT;
import java.awt.event.*;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import static java.lang.Math.abs;
import java.util.Random;
import javax.swing.*;
import java.util.concurrent.ExecutorService;
class thepane extends JPanel implements Runnable{
public float x,y,r;
public float speedx,speedy;
thepane(float lx,float ly,float lr, float sx,float sy){
loadspeed(sx,sy);
load(lx,ly,lr);
for(int i=0;i<5;i++)
fc[i]=new Color(nd.nextInt(255),nd.nextInt(255),nd.nextInt(255));
}
public void load(float lx,float ly,float lr){
x=lx;y=ly;r=lr;
}
public void loadspeed(float sx,float sy){
speedx=sx;speedy=sy;
}
public void xmoving(){
x+=speedx;
}
public void ymoving(){
y-=speedy;
}
public void touchbond(){
if(x>getWidth()-r||x<0)
speedx*=-1;
if(y>getHeight()-r||y<0)
speedy*=-1;
if(x>getWidth()-r)
x=getWidth()-r;
else if(x<0)
x=0;
if(y>getHeight()-r)
y=getHeight()-r;
else if(y<0)
y=0;
}
Random nd=new Random();
int colorcount=0;
int emitcount=0;
boolean emit=false;
Color[] fc=new Color[5];
Graphics2D comp2D ;
Thread athread;
#Override
public void paintComponent(Graphics comp) {
comp2D = (Graphics2D) comp;
//create rectangle background
comp2D.setColor(Color.BLACK);
comp2D.fillRect(0, 0, getWidth(), getHeight());
//set information text
comp2D.setFont( new Font("Arial", Font.BOLD, 12));
comp2D.setColor(Color.WHITE);
comp2D.drawString("Centre("+(x+r/2)+' '+(y+r/2)+"), xspeed: "+speedx+" yspeed: "+speedy, 10f,10f );
comp2D.drawString("panel width "+getWidth()+" panel height "+getHeight()+" circle radius "
+r, 10f, 22f);
}
//thread run()
#Override
public void run() {
x=100;y=100;
System.out.println("thread in pane start!!!! (current colorcount = "+colorcount+')');
while(true){
circleEmit(fc[colorcount%5]);
repaint();
sleeping(1);
// comp2D=(Graphics2D)this.getGraphics();
// colorEmit(comp2D);
}
}
//wait method
public void waiting(){
try{wait();}
catch(Exception e){}}
public void waiting2D(int time){
try{comp2D.wait(time);}
catch(Exception e){}
}
public void waiting(int time){
try{wait(time);}
catch(Exception e){}
}
//sleep method
public void sleeping(int n){
try{
Thread.sleep(n);
}catch(Exception f){
System.out.print(f);
}
}
Ellipse2D.Float[] e=new Ellipse2D.Float[5];
public void loade(){
float centrex=x+r/2,centrey=y+r/2;
e[0]= new Ellipse2D.Float(centrex-r/10, centrey-r/10, r/5, r/5);
e[1]= new Ellipse2D.Float(centrex-r/5, centrey-r/5, 2*r/5, 2*r/5);
e[2]= new Ellipse2D.Float(centrex-3*r/10, centrey-3*r/10, 3*r/5, 3*r/5);
e[3]= new Ellipse2D.Float(centrex-2*r/5, centrey-2*r/5, 4*r/5, 4*r/5);
e[4]= new Ellipse2D.Float(centrex-r/2, centrey-r/2, r, r);
}
public Color ff;
public synchronized void circleEmit(Color fc){
comp2D=(Graphics2D)this.getGraphics();
loade();
comp2D.setColor(fc);
comp2D.fill(e[emitcount%5]);
waiting(5);
emitcount++;
}
public synchronized void callnotify(){
this.notify();
}
//iterative way to generate color emit
public void colorEmit(Graphics2D comp2D){
//create circle
//set circle property
float centrex=x+r/2,centrey=y+r/2;//so x=centrex-r/2;y=centrey+r/2
Ellipse2D.Float e1 = new Ellipse2D.Float(centrex-r/10, centrey-r/10, r/5, r/5);
Ellipse2D.Float e2 = new Ellipse2D.Float(centrex-r/5, centrey-r/5, 2*r/5, 2*r/5);
Ellipse2D.Float e3 = new Ellipse2D.Float(centrex-3*r/10, centrey-3*r/10, 3*r/5, 3*r/5);
Ellipse2D.Float e4 = new Ellipse2D.Float(centrex-2*r/5, centrey-2*r/5, 4*r/5, 4*r/5);
Ellipse2D.Float e5 = new Ellipse2D.Float(centrex-r/2, centrey-r/2, r, r);
if(colorcount>=4)
emit(comp2D,fc[(colorcount-4)%5],e5);
waiting(1000);
if(colorcount>=3)
emit(comp2D,fc[(colorcount-3)%5],e4);
waiting(1000);
if(colorcount>=2)
emit(comp2D,fc[(colorcount-2)%5],e3);
waiting(1000);
if(colorcount>=1)
emit(comp2D,fc[(colorcount-1)%5],e2);
waiting(1000);
emit(comp2D,fc[colorcount%5],e1);
waiting(1000);
colorcount++;
}
private void emit(Graphics2D comp,Color thecolor,Ellipse2D.Float f){
comp.setColor(thecolor);
comp.fill(f);
}
}
//------------------------------------------------------------------------------------
//main class
public class drawpanel extends Thread implements ActionListener{
JFrame frame=new JFrame();
thepane panel;
JButton FlyingBalls=new JButton("balls"),exit=new JButton("Exit"),stop=new JButton("Stop");
JButton slow=new JButton("slow down"),resume=new JButton("resume");
Float x,y,r;
public void sleeping(int n){
try{
Thread.sleep(n);
}catch(Exception f){
System.out.print(f);
}
}
Thread newthread,pthread;
Thread[] five=new Thread[5];
drawpanel(){
frame.setTitle("FlyingBalls");
frame.setLocation(100, 100);
frame.setLayout(null);
//x,y,r,speedx,speedy
panel=new thepane(nd.nextInt(800),nd.nextInt(500),40,nd.nextFloat()*20+1,nd.nextFloat()*10+1);
panel.setSize(800,500);
frame.setSize(810,580);
frame.add(panel);
FlyingBalls.setSize(80,30);exit.setSize(70,30);stop.setSize(70,30);slow.setSize(140,30);
resume.setSize(100,30);
FlyingBalls.addActionListener(this);
exit.addActionListener(this);
stop.addActionListener(this);slow.addActionListener(this);resume.addActionListener(this);
frame.add(FlyingBalls);frame.add(exit); frame.add(stop);frame.add(slow);frame.add(resume);
FlyingBalls.setLocation(20,500);exit.setLocation(190, 500);stop.setLocation(110,500);
slow.setLocation(270,500);resume.setLocation(420,500);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//control moving ball
newthread=new Thread(this);
//control color change
for(int i=0;i<5;i++){
five[i]=new Thread(panel);
}
// newthread.start();
panel.colorcount++;
five[0].start();
panel.colorcount=2;
// five[1].start();
panel.waiting(5);
}
public static void main(String[] arg){
drawpanel apanel=new drawpanel();
}
int bw=800,bh=500;
void setp(){
x=panel.x;y=panel.y;
}
void touchbond(){
System.out.println("width:"+panel.getWidth()+"Height:"+panel.getHeight());
System.out.println("xposition:"+x+"yposition:"+y);
if(x+r>panel.getWidth()){
panel.speedx*=-1;
x=bw-r;
}
else if(x-r<0){
panel.speedx*=-1;
x=r;
}
if(y-r<0){
panel.speedy*=-1;
y=r;
}
else if(y+r>panel.getHeight()){
panel.speedy*=-1;
y=bh-r;
}
panel.x=x;panel.y=y;
}
int T=10;
Random nd=new Random();
#Override
public void run(){
r=panel.r;
panel.loadspeed(-6.33f,-3.4f);
while(true){
if(stopcount==0){//button control variable
panel.xmoving();panel.ymoving();
panel.touchbond();
sleeping(T);}
panel.loade();
// panel.callnotify();
// panel.colorEmit(panel.comp2D);
panel.repaint();
}
}
#Override
public void start(){
}
int count=0,stopcount=0;
#Override
public void actionPerformed(ActionEvent e) {
if(e.getSource()==exit){
System.exit(0);
}
if(e.getSource()==FlyingBalls){
//panel=new thepane();
}
if(e.getSource()==resume){
stopcount=0;T=10;
panel.emit=false;
}
if(e.getSource()==slow){
if(count%2==0)
T=500;
else
T=10;
count++;
}
if(e.getSource()==stop){
stopcount++;
panel.emit=true;
}
}
}
So, lots of theory to cover.
Firstly...
Animation is not easy, good animation is hard.
Swing is single threaded and is not thread safe
This means that you should not perform any long running or blocking operations within the context of the Event Dispatching Thread.
It also means that you shouldn't modify the UI or anything the UI relies on from outside the context of the Event Dispatching Thread
More threads != more work
More threads doesn't always mean you're going to get more done. In fact, in this scenario, it could really cause a huge number of issues, as you need the ability to reason out the state at a single point in time (when painting)
Animation Theory
Okay, animation is simply the illusion of change, how you accomplish that will come down to the problem you trying to solve.
For me, the best animations are time based animations, not linear.
A linear animation keeps updating from its start state till it reaches its end state, in a constant progression. These don't tend to scale well and can suffer issues on low performant systems.
A time based animation is one where the amount of time is defined and then, based on a anchor time (ie start time) and the state of the animation is updated based on the amount of time which is passed. This is a really simple way to achieve "frame dropping". You'd also be very surprised to find that in general terms, time based animations tend to look better across more platforms.
A time based animation is also more capable of generating "easement" effects, but that's getting way deeper then we need to go right now.
Okay, but what's this got to do with your problem? Well, actually, quite a bit.
The first thing we need is some kind of "main-loop" from which all the animation can be driven. Typically, I'd look to a good animation library, but failing that, a simple Swing Timer will do the basic good really well.
It generates its ticks in the Event Dispatching Thread, which makes it very useful for our needs. See How to Use Swing Timers for more details
So, we start with something like...
private Timer timer;
//...
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
// Update the state
repaint();
}
});
//...
timer.start();
This gives us our "main loop", from which we can update the state as needed and then trigger a repaint of the component.
For the purpose of this demonstration, I'm going to devise a self-contained unit of "animation duration", used to track the amount of time which has passed since the animation was started, this is personal choice, but it would allow me to drive a number of animations and it contains the logic to a single unit of work.
public class AnimationDuration {
private Duration duration;
private Instant startedAt;
public AnimationDuration(Duration duration) {
this.duration = duration;
}
public Duration getDuration() {
return duration;
}
public void start() {
startedAt = Instant.now();
}
public void stop() {
startedAt = null;
}
public boolean isRunning() {
return startedAt != null;
}
public float getProgress() {
Duration runningTime = Duration.between(startedAt, Instant.now());
if (runningTime.compareTo(duration) > 0) {
runningTime = duration;
}
long total = duration.toMillis();
float progress = runningTime.toMillis() / (float) total;
return progress;
}
}
This basically allows to trigger the animation to start running (anchor point in time) and then get the progress of the animation at any point in time. This provides a normalised concept from 0-1, so if we want to make it longer or shorter, all we do is adjust the duration and everything else is taken care of.
For your specific problem, I'd consider some kind of "time line" or "key frames", which defines that certain actions should occur at certain points of time along the time line.
Now, the following is a really simple concept, but it gets the job.
public interface KeyFrame {
public float getProgress();
}
public class TimeLine<K extends KeyFrame> {
private List<K> keyFrames;
public TimeLine() {
keyFrames = new ArrayList<>(25);
}
// Returns the key frames between the current progression
public K getKeyFrameAt(float progress) {
for (int index = 0; index < keyFrames.size(); index++) {
K keyFrame = keyFrames.get(index);
if (progress >= keyFrame.getProgress()) {
if (index + 1 < keyFrames.size()) {
K nextFrame = keyFrames.get(index + 1);
// But only if your between each other
if (progress < nextFrame.getProgress()) {
return keyFrame;
}
} else {
// Nothing after me :D
return keyFrame;
}
}
}
return null;
}
public void add(K keyFrame) {
keyFrames.add(keyFrame);
Collections.sort(keyFrames, new Comparator<KeyFrame>() {
#Override
public int compare(KeyFrame lhs, KeyFrame rhs) {
if (lhs.getProgress() > rhs.getProgress()) {
return 1;
} else if (lhs.getProgress() < rhs.getProgress()) {
return -1;
}
return 0;
}
});
}
}
This allows you to define certain KeyFrames along the timeline, based on a normalised concept of time and then provides the ability to get the KeyFrame based on the current progression through animation.
There are much more complex solutions you might consider, which would generate self contained events based on time progressions automatically, but I prefer been able to driver the animation itself independently, makes these types of things more flexible - add a JSlider and you can manipulate the progression manually ;)
The next thing we need is something to carry the properties for the circle KeyFrame ...
public class CirclePropertiesKeyFrame implements KeyFrame {
private float progress;
private double radius;
private Color color;
public CirclePropertiesKeyFrame(float progress, double radius, Color color) {
this.progress = progress;
this.radius = radius;
this.color = color;
}
#Override
public float getProgress() {
return progress;
}
public Color getColor() {
return color;
}
public double getRadius() {
return radius;
}
#Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; raidus= " + radius + "; color = " + color;
}
}
Now, we need to put it together...
public class TestPane extends JPanel {
private AnimationDuration timelineDuration;
private TimeLine<CirclePropertiesKeyFrame> timeLine;
private Timer timer;
private CirclePropertiesKeyFrame circleProperties;
public TestPane() {
timelineDuration = new AnimationDuration(Duration.ofSeconds(10));
timeLine = new TimeLine<>();
timeLine.add(new CirclePropertiesKeyFrame(0, 5, Color.CYAN));
timeLine.add(new CirclePropertiesKeyFrame(0.2f, 10, Color.BLUE));
timeLine.add(new CirclePropertiesKeyFrame(0.4f, 15, Color.GREEN));
timeLine.add(new CirclePropertiesKeyFrame(0.6f, 20, Color.YELLOW));
timeLine.add(new CirclePropertiesKeyFrame(0.8f, 25, Color.MAGENTA));
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (timelineDuration.isRunning()) {
float progress = timelineDuration.getProgress();
if (progress >= 1.0) {
timelineDuration.stop();
}
CirclePropertiesKeyFrame keyFrame = timeLine.getKeyFrameAt(progress);
circleProperties = keyFrame;
}
repaint();
}
});
}
#Override
public void addNotify() {
super.addNotify();
timelineDuration.start();
timer.start();
}
#Override
public void removeNotify() {
super.removeNotify();
timer.stop();
timelineDuration.stop();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
if (circleProperties != null) {
double radius = circleProperties.radius;
double xPos = (getWidth() / 2) - radius;
double yPos = (getHeight() / 2) - radius;
g2d.setColor(circleProperties.color);
g2d.fill(new Ellipse2D.Double(xPos, yPos, radius * 2, radius * 2));
}
g2d.dispose();
}
}
And then we end up with something like...
Now, this is a 10 second sequence, so every 2 seconds it will update. Try changing the duration of the AnimationDuration and see what happens.
Note This is a non-repeating animation (it doesn't loop). You could make it loop, but the calculation to do so becomes more complicate, as you need to consider by how much you're over the expected Duration and then apply that to the next cycle, so it looks smooth
But what about movement?
Well, actually, pretty much already answered that question. You would also place the movement code inside the Timers ActionListener, right before the repaint request. In fact, I might be tempted to create some kind of class that could take the current KeyFrame information and combine it with the location properties, this would then be used the paintComponent method to draw the circle.
I want to blend the animation states ...
Well, that's a much more difficult question, especially when it comes to colors.
The basic theory is, you need the two key frames which set either side of the current progression. You would then apply a "blending" algorithm to calculate the amount of change to be applied between the two key frames.
Not impossible, just a step more difficult
I use certain events to place small rectangles on an HBox. Their placement is perfect when the window has not been resized, but when you for example go from small to fullscreen, their placement is wrong (of course, because they get a certain value for X at the time of placement - this is measured by getting the width of the HBox at that specific moment).
Question:
How can I make these positions dynamic, so when I resize the window, they stay in proportion?
Pictures:
Code:
#FXML HBox tagLine; // initializes the HBox
...
public void addTag(String sort) {
Rectangle rect = new Rectangle(20, tagLine.getHeight());
double pos = timeSlider.getValue() / 100 * tagLine.getWidth(); // retrieves position at the moment of being called
rect.setTranslateX(pos);
rect.setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
showNotification("Gemarkeerde gebeurtenis: " + sort);
}
});
rect.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
notificationHide.play();
}
});
tagLine.getChildren().add(rect);
}
Few things that you need to take into account while translating a shape with accountable size is that you need to :
Translate the shape from its center
If the translation is dependent on the width of a Node, listen to the changes made to the with of that particular node and make changes to the translate property accordingly
Both of the above points seem to be missing in your implementation. You are never listening to width property of HBox. Neither is your calculation for pos taking the center of Rectangle into account.
Here is an example which try to keep the Rectangle at the center no matter what the size of your HBox is.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
private static final int SIDE = 40;
private static final double DEFAULT_WIDTH = 200;
private static final double DEFAULT_POSITION = 100;
#Override
public void start(Stage primaryStage) {
Rectangle rectangle = new Rectangle(SIDE, SIDE);
HBox root = new HBox(rectangle);
root.setPrefWidth(DEFAULT_WIDTH);
rectangle.translateXProperty().bind(root.widthProperty().multiply(DEFAULT_POSITION/DEFAULT_WIDTH).subtract(SIDE/2));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I have the following JavaFX scene (note the setting of snapToTicks):
package com.example.javafx;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;
public class SliderExample extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage primaryStage) {
Slider slider = new Slider(0.25, 2.0, 1.0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(0.25);
slider.setMinorTickCount(0);
slider.setSnapToTicks(true); // !!!!!!!!!!
Scene scene = new Scene(slider, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
}
which renders a slider like this:
Since snapToTicks is set to true the slider will finally move to the nearest value once the mouse button is released.
How can that final value be retrieved?
I tried
slider.valueProperty().addListener( n -> {
if (!slider.isValueChanging()) {
System.err.println(n);
}
});
which works well except for the minimum and maximum values - if the mouse is already at a position left to the slider or at a position right to the slider, the listener will not be called at all anymore since the final value has already been set.
I have also tried to use the valueChangingProperty:
slider.valueChangingProperty().addListener( (prop, oldVal, newVal) -> {
// NOT the final value when newVal == false!!!!!!!
System.err.println(prop + "/" + oldVal + "/" + newVal);
});
but the problem is that JavaFX will still change the value to the snapped value after that listener has been called with newVal equal to false (which I would even consider a bug, but probably I missed something). So its not possible to access the final, snapped value in that method.
I finally came up with the below solution, based on the proposal from #ItachiUchiha. Essentially, the solution uses both, a valueProperty and a valueChangingProperty listener, and uses some flags to track the current state. At the end, the perform() method is called exactly once when the slider movement is done and the final value is available. This works when the slider is moved either with the mouse or through the keyboard.
A reusable class implemented as subclass of Slider is available at https://github.com/afester/FranzXaver/blob/master/FranzXaver/src/main/java/afester/javafx/components/SnapSlider.java.
package com.example.javafx;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;
public class SliderExample extends Application {
public static void main(String[] args) { launch(args); }
private boolean isFinal = true; // assumption: no dragging - clicked value is the final one.
// variable changes to "false" once dragging starts.
private Double finalValue = null;
#Override
public void start(Stage primaryStage) {
final Slider slider = new Slider(0.25, 2.0, 1.0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(0.25);
slider.setMinorTickCount(0);
slider.setSnapToTicks(true);
slider.valueProperty().addListener(new ChangeListener<Number>() {
final double minCompare = slider.getMin() + Math.ulp(slider.getMin());
final double maxCompare = slider.getMax() - Math.ulp(slider.getMax());
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (isFinal) { // either dragging of knob has stopped or
// no dragging was done at all (direct click or
// keyboard navigation)
perform((Double) newValue);
finalValue = null;
} else { // dragging in progress
double val = (double) newValue;
if (val > maxCompare || val < minCompare) {
isFinal = true; // current value will be treated as final value
// once the valueChangingProperty goes to false
finalValue = (Double) newValue; // remember current value
} else {
isFinal = false; // no final value anymore - slider
finalValue = null; // has been dragged to a position within
// minimum and maximum
}
}
}
});
slider.valueChangingProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (newValue == true) { // dragging of knob started.
isFinal = false; // captured values are not the final ones.
} else { // dragging of knob stopped.
if (isFinal) { // captured value is already the final one
// since it is either the minimum or the maximum value
perform(finalValue);
finalValue = null;
} else {
isFinal = true; // next captured value will be the final one
}
}
}
});
Scene scene = new Scene(slider, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void perform(double value) {
System.err.printf("FINAL: %s\n", value);
}
}
I want to create an application that performs many renderings in a canvas.
The normal JavaFX way blocks the GUI: It is realy hard to press the button in the application code below (run with Java 8).
I searched the web, but JavaFX does not support background rendering: All rendering operation (like strokeLine) are stored in a buffer and are executed in the JavaFX application thread later. So I cannot even use two canvases and exchange then after rendering.
Also the javafx.scene.Node.snapshot(SnapshotParameters, WritableImage) cannot be used to create an image in a background thread, as it needs to run inside the JavaFX application thread and so it will block the GUI also.
Any ideas to have a non blocking GUI with many rendering operations? (I just want to press buttons etc. while the rendering is performed somehow in background or paused regularly)
package canvastest;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
public class DrawLinieTest extends Application
{
int interations = 2;
double lineSpacing = 1;
Random rand = new Random(666);
List<Color> colorList;
final VBox root = new VBox();
Canvas canvas = new Canvas(1200, 800);
Canvas canvas2 = new Canvas(1200, 800);
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<?> drawShapesFuture;
{
colorList = new ArrayList<>(256);
colorList.add(Color.ALICEBLUE);
colorList.add(Color.ANTIQUEWHITE);
colorList.add(Color.AQUA);
colorList.add(Color.AQUAMARINE);
colorList.add(Color.AZURE);
colorList.add(Color.BEIGE);
colorList.add(Color.BISQUE);
colorList.add(Color.BLACK);
colorList.add(Color.BLANCHEDALMOND);
colorList.add(Color.BLUE);
colorList.add(Color.BLUEVIOLET);
colorList.add(Color.BROWN);
colorList.add(Color.BURLYWOOD);
}
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage)
{
primaryStage.setTitle("Drawing Operations Test");
System.out.println("Init...");
// inital draw that creates a big internal operation buffer (GrowableDataBuffer)
drawShapes(canvas.getGraphicsContext2D(), lineSpacing);
drawShapes(canvas2.getGraphicsContext2D(), lineSpacing);
System.out.println("Start testing...");
new CanvasRedrawTask().start();
Button btn = new Button("test " + System.nanoTime());
btn.setOnAction((ActionEvent e) ->
{
btn.setText("test " + System.nanoTime());
});
root.getChildren().add(btn);
root.getChildren().add(canvas);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private void drawShapes(GraphicsContext gc, double f)
{
System.out.println(">>> BEGIN: drawShapes ");
gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
gc.setLineWidth(10);
gc.setLineCap(StrokeLineCap.ROUND);
long time = System.nanoTime();
double w = gc.getCanvas().getWidth() - 80;
double h = gc.getCanvas().getHeight() - 80;
int c = 0;
for (int i = 0; i < interations; i++)
{
for (double x = 0; x < w; x += f)
{
for (double y = 0; y < h; y += f)
{
gc.setStroke(colorList.get(rand.nextInt(colorList.size())));
gc.strokeLine(40 + x, 10 + y, 10 + x, 40 + y);
c++;
}
}
}
System.out.println("<<< END: drawShapes: " + ((System.nanoTime() - time) / 1000 / 1000) + "ms");
}
public synchronized void drawShapesAsyc(final double f)
{
if (drawShapesFuture != null && !drawShapesFuture.isDone())
return;
drawShapesFuture = executorService.submit(() ->
{
drawShapes(canvas2.getGraphicsContext2D(), lineSpacing);
Platform.runLater(() ->
{
root.getChildren().remove(canvas);
Canvas t = canvas;
canvas = canvas2;
canvas2 = t;
root.getChildren().add(canvas);
});
});
}
class CanvasRedrawTask extends AnimationTimer
{
long time = System.nanoTime();
#Override
public void handle(long now)
{
drawShapesAsyc(lineSpacing);
long f = (System.nanoTime() - time) / 1000 / 1000;
System.out.println("Time since last redraw " + f + " ms");
time = System.nanoTime();
}
}
}
EDIT Edited the code to show that a background thread that sends the draw operations and than exchange the canvas does not resolve the problem! Because All rendering operation (like strokeLine) are stored in a buffer and are executed in the JavaFX application thread later.
You are drawing 1.6 million lines per frame. It is simply a lot of lines and takes time to render using the JavaFX rendering pipeline. One possible workaround is not to issue all drawing commands in a single frame, but instead render incrementally, spacing out drawing commands, so that the application remains relatively responsive (e.g. you can close it down or interact with buttons and controls on the app while it is rendering). Obviously, there are some tradeoffs in extra complexity with this approach and the result is not as desirable as simply being able to render extremely large amounts of draw commands within the context of single 60fps frame. So the presented approach is only acceptable for some kinds of applications.
Some ways to perform an incremental render are:
Only issue a max number of calls each frame.
Place the rendering calls into a buffer such as a blocking queue and just drain a max number of calls each frame from the queue.
Here is a sample of the first option.
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.concurrent.*;
import javafx.scene.Scene;
import javafx.scene.canvas.*;
import javafx.scene.control.Button;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.*;
public class DrawLineIncrementalTest extends Application {
private static final int FRAME_CALL_THRESHOLD = 25_000;
private static final int ITERATIONS = 2;
private static final double LINE_SPACING = 1;
private final Random rand = new Random(666);
private List<Color> colorList;
private final WritableImage image = new WritableImage(ShapeService.W, ShapeService.H);
private final Lock lock = new ReentrantLock();
private final Condition rendered = lock.newCondition();
private final ShapeService shapeService = new ShapeService();
public DrawLineIncrementalTest() {
colorList = new ArrayList<>(256);
colorList.add(Color.ALICEBLUE);
colorList.add(Color.ANTIQUEWHITE);
colorList.add(Color.AQUA);
colorList.add(Color.AQUAMARINE);
colorList.add(Color.AZURE);
colorList.add(Color.BEIGE);
colorList.add(Color.BISQUE);
colorList.add(Color.BLACK);
colorList.add(Color.BLANCHEDALMOND);
colorList.add(Color.BLUE);
colorList.add(Color.BLUEVIOLET);
colorList.add(Color.BROWN);
colorList.add(Color.BURLYWOOD);
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Drawing Operations Test");
System.out.println("Start testing...");
new CanvasRedrawHandler().start();
Button btn = new Button("test " + System.nanoTime());
btn.setOnAction(e -> btn.setText("test " + System.nanoTime()));
Scene scene = new Scene(new VBox(btn, new ImageView(image)));
primaryStage.setScene(scene);
primaryStage.show();
}
private class CanvasRedrawHandler extends AnimationTimer {
long time = System.nanoTime();
#Override
public void handle(long now) {
if (!shapeService.isRunning()) {
shapeService.reset();
shapeService.start();
}
if (lock.tryLock()) {
try {
System.out.println("Rendering canvas");
shapeService.canvas.snapshot(null, image);
rendered.signal();
} finally {
lock.unlock();
}
}
long f = (System.nanoTime() - time) / 1000 / 1000;
System.out.println("Time since last redraw " + f + " ms");
time = System.nanoTime();
}
}
private class ShapeService extends Service<Void> {
private Canvas canvas;
private static final int W = 1200, H = 800;
public ShapeService() {
canvas = new Canvas(W, H);
}
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
drawShapes(canvas.getGraphicsContext2D(), LINE_SPACING);
return null;
}
};
}
private void drawShapes(GraphicsContext gc, double f) throws InterruptedException {
lock.lock();
try {
System.out.println(">>> BEGIN: drawShapes ");
gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
gc.setLineWidth(10);
gc.setLineCap(StrokeLineCap.ROUND);
long time = System.nanoTime();
double w = gc.getCanvas().getWidth() - 80;
double h = gc.getCanvas().getHeight() - 80;
int nCalls = 0, nCallsPerFrame = 0;
for (int i = 0; i < ITERATIONS; i++) {
for (double x = 0; x < w; x += f) {
for (double y = 0; y < h; y += f) {
gc.setStroke(colorList.get(rand.nextInt(colorList.size())));
gc.strokeLine(40 + x, 10 + y, 10 + x, 40 + y);
nCalls++;
nCallsPerFrame++;
if (nCallsPerFrame >= FRAME_CALL_THRESHOLD) {
System.out.println(">>> Pausing: drawShapes ");
rendered.await();
nCallsPerFrame = 0;
System.out.println(">>> Continuing: drawShapes ");
}
}
}
}
System.out.println("<<< END: drawShapes: " + ((System.nanoTime() - time) / 1000 / 1000) + "ms for " + nCalls + " ops");
} finally {
lock.unlock();
}
}
}
}
Note that for the sample, it is possible to interact with the scene by clicking the test button while the incremental rendering is in progress. If desired, you could further enhance this to double buffer the snapshot images for the canvas so that the user doesn't see the incremental rendering. Also because the incremental rendering is in a Service, you can use the service facilities to track rendering progress and relay that to the UI via a progress bar or whatever mechanisms you wish.
For the above sample you can play around with the FRAME_CALL_THRESHOLD setting to vary the maximum number of calls which are issued each frame. The current setting of 25,000 calls per frame keeps the UI very responsive. A setting of 2,000,000 would be the same as fully rendering the canvas in a single frame (because you are issuing 1,600,000 calls in the frame) and no incremental rendering will be performed, however the UI will not be responsive while the rendering operations are being completed for that frame.
Side Note
There is something weird here. If you remove all of the concurrency stuff and the double canvases in the code in the original question and just use a single canvas with all logic on the JavaFX application thread, the initial invocation of drawShapes takes 27 seconds, and subsequent invocations take less that a second, but in all cases the application logic is asking the system to perform the same task. I don't know why the initial call is so slow, it seems like a performance issue in the JavaFX canvas implementation to me, perhaps related to inefficient buffer allocation. If that is the case, then perhaps the JavaFX canvas implementation could be tweaked so that a hint for a suggested initial buffer size could be provided, so that it more efficiently allocates space for its internal growable buffer implementation. It might be something worth filing a bug or discussing it on the JavaFX developer mailing list. Also note that the issue of a very slow initial rendering of the canvas is only visible when you issue a very large number (e.g. > 500,000) of rendering calls, so it won't effect all applications.
The issue that is described here has also been discussed on the JavaFX mailing list some months ago in this thread
http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-September/017939.html
The proposed solution is similar to the one given by jewelsea.