0

I'm making a very simple animation using JavaFX. My goal here is just to have a rectangle move smoothly across the window.

I'm trying to achieve this using AnimationTimer, which seems fit for the task. I've tried different ways of rendering, such as a Rectangle in an AnchorPane, or simply drawing onto a Canvas, but in the end it always boils down to the same thing, with the same results.

I basically store the position of my rectangle, and apply a moving rate to it at each frame.

In fact, when I use a constant moving rate in the handle method of my AnimationTimer, animation is perfectly smooth. However, there are 2 problems with this technique:

  1. Frame rate seems to be platform-dependent, with no easy way to control it. So the animation would render differently on different machines.
  2. Frame rate sometimes varies, for instance when resizing the window, it can sometimes drop by half, or sometimes even double up, which changes the animation speed accordingly.

So I tried to make the animation time-based, by using the argument of AnimationTimer.handle(long now). It solved the inconsistency issues, but the animation is now jittery! A few times per second, the rectangle seems to "jump" a few pixels ahead and then stall for a frame or two to recover it's expected position. It becomes more and more obvious as I increase the speed.

Here's the relevant piece of code (simplified):

AnimationTimer anim = new AnimationTimer() {
  private long lastRun = 0;
  
  @Override
  public void handle(long now) {
    //Ignore first frame as I'm not sure of the timing here
    if (lastRun == 0) {
      lastRun = now;
      return;
    }
    //Now we've got a reference, so let's animate
    double elapsed = (now - lastRun) / 1e9; //Convert to seconds
    //Update position according to speed
    position = position.add(speed.multiply(elapsed)); //Apply speed in pixels/second
    lastRun = now; //Store current time for next loop
    draw();
  }
};

I've tried to log time differences, frame rate and position. Tried a few different fixes, making my code always more complex but with no result whatsoever.

Edit 2022-03-15 following your comments (thanks)

I've tried this on my usual computer (Win 10, Xeon processor, 2 Geforce 1050Ti GPUs), and also on a Microsoft Surface Go 3 tablet under Windows 11. I've tried it using Java 17.0.1 (Temurin) and JavaFX 17.0.1, as well as JDK 8u211 with the same results.

Using JVM argument -Djavafx.animation.pulse=10 has no effect whatsoever other than showing "Setting PULSE_DURATION to 10 hz" in stderr. -Djavafx.animation.framerate=10 doesn't do a thing.

End of edit

I can't figure out what I'm doing wrong here. Can you please help me out ?

Here's my entire code : (Edited on 2022-03-15 to include FPS-meter)

import java.math.BigDecimal;
import java.math.RoundingMode;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;

public class TestFxCanvas2 extends Application {

  // Set your panel size here
  private static final int FRAME_WIDTH = 800;
  private static final int FRAME_HEIGHT = 800;

  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    BorderPane root = new BorderPane();
    MyAnimation2 myAnimation = new MyAnimation2();
    myAnimation.widthProperty().bind(root.widthProperty());
    myAnimation.heightProperty().bind(root.heightProperty());
    root.setCenter(myAnimation);
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.setWidth(FRAME_WIDTH);
    stage.setHeight(FRAME_HEIGHT);

    stage.show();
  }
}

class MyAnimation2 extends Canvas {

  private static final double SPEED = 500; // Speed value to be applied in either direction in px/s
  private static final Point2D RECT_DIMS = new Point2D(50, 50); // rectangle size
  // Canvas painting colors
  private static final Paint BLACK = Paint.valueOf("black");
  private static final Paint RED = Paint.valueOf("red");
  private static final Paint GREEN = Paint.valueOf("ForestGreen");

  // Defines rectangle start position
  private Point2D recPos = new Point2D(0, 300);
  // Stores previous position
  private Point2D oldPos = new Point2D(0, 0);

  // Current speed
  private Point2D speed = new Point2D(SPEED, 0);

  public MyAnimation2() {

    AnimationTimer anim = new AnimationTimer() {
      private long lastRun = 0;

      long[] frameTimes = new long[10];
      long frameCount = 0;

      @Override
      public void handle(long now) {
        // Measure FPS
        BigDecimal fps = null;
        int frameIndex = (int) (frameCount % frameTimes.length);
        frameTimes[frameIndex] = now;
        if (frameCount > frameTimes.length) {
          int prev = (int) ((frameCount + 1) % frameTimes.length);
          long delta = now - frameTimes[prev];
          double fr = 1e9 / (delta / frameTimes.length);
          fps = new BigDecimal(fr).setScale(2, RoundingMode.HALF_UP);
        }
        frameCount++;
        // Skip first frame but record its timing
        if (lastRun == 0) {
          lastRun = now;
          return;
        }
        // Animate
        double elapsed = (now - lastRun) / 1e9;
        // Reverse when hitting borders
        if (hitsBorders())
          speed = speed.multiply(-1.);
        // Update position according to speed
        oldPos = recPos;
        recPos = recPos.add(speed.multiply(elapsed));
        lastRun = now;
        draw(oldPos, recPos, fps);
      }
    };

    // Start
    anim.start();
  }

  private void draw(Point2D oldPos, Point2D recPos, BigDecimal fps) {
    GraphicsContext gfx = this.getGraphicsContext2D();
    // Clear and draw border
    gfx.setStroke(BLACK);
    gfx.setLineWidth(1);
    gfx.clearRect(0, 0, getWidth(), getHeight());
    gfx.strokeRect(0, 0, getWidth(), getHeight());
    // Draw moving shape
    gfx.setFill(RED);
    gfx.fillRect(recPos.getX(), recPos.getY(), RECT_DIMS.getX(), RECT_DIMS.getY());
    // Draw FPS meter
    String fpsText = fps == null ? "FPS" : fps.toString();
    gfx.setTextAlign(TextAlignment.RIGHT);
    gfx.setFill(GREEN);
    gfx.setFont(Font.font(24));
    gfx.setTextBaseline(VPos.TOP);
    gfx.fillText(fpsText, getWidth() - 5, 5);
  }

  private boolean hitsBorders() {
    Rectangle2D frame = new Rectangle2D(0, 0, getWidth(), getHeight());
    Rectangle2D rect = new Rectangle2D(recPos.getX(), recPos.getY(), RECT_DIMS.getX(), RECT_DIMS.getY());
    if (speed.getX() < 0 && rect.getMinX() < frame.getMinX())
      return true;
    else if (speed.getX() > 0 && rect.getMaxX() > frame.getMaxX())
      return true;
    else if (speed.getY() < 0 && rect.getMinY() < frame.getMinY())
      return true;
    else if (speed.getY() > 0 && rect.getMaxY() > frame.getMaxY())
      return true;
    return false;
  }
}

Addition after testing same program in JavaScript

This JavaScript version runs smoothly on my devices

See a video comparing both versions (JavaScript at the top, JavaFX at the bottom) here: https://www.ahpc-services.com/fileshare/JFX_test_pulses/20220315_150431_edit1.mp4

const RED = 'red';
const GREEN = 'ForestGreen';
const BLACK = 'black';

window.addEventListener('DOMContentLoaded', e => {
  const animArea = document.querySelector('#anim-area');
  const ctx = animArea.getContext('2d');
  const cWidth = animArea.clientWidth;
  const cHeight = animArea.clientHeight;
  adjustCanvasSize();
  window.addEventListener('resize', adjustCanvasSize);

  const rect = {
    x: 0,
    y: 50,
    width: 50,
    height: 50
  }
  const speed = {
    x: 500,
    y: 0
  }

  const frameTiming = {
    frameCount: 0,
    frameTimes: Array(10),
    lastRun: 0,
  }

  requestAnimationFrame(animate);

  function animate() {
    const now = Date.now();

    requestAnimationFrame(animate);

    //Count FPS
    let fps;
    const frameIndex = frameTiming.frameCount % frameTiming.frameTimes.length;
    frameTiming.frameTimes[frameIndex] = now;
    if (frameTiming.frameCount > frameTiming.frameTimes.length) {
      const prev = (frameTiming.frameCount + 1) % frameTiming.frameTimes.length;
      const delta = now - frameTiming.frameTimes[prev];
      fps = Math.round(100 * 1000 * frameTiming.frameTimes.length / delta) / 100;
    }
    frameTiming.frameCount++;
    //Ignore first frame
    if (frameTiming.lastRun == 0) {
      frameTiming.lastRun = now;
      return;
    }
    //Animate
    const elapsed = (now - frameTiming.lastRun) / 1e3;
    // Reverse when hitting borders
    if (hitsBorders()) {
      speed.x *= -1;
      speed.y *= -1;
    }
    // Update position according to speed
    const oldRect = Object.assign({}, rect);
    rect.x += speed.x * elapsed;
    rect.y += speed.y * elapsed;
    frameTiming.lastRun = now;
    draw();

    function draw() {
      // Clear and draw border
      ctx.clearRect(0, 0, animArea.width, animArea.height);
      ctx.strokeStyle = BLACK;
      ctx.strokeRect(0, 0, animArea.width, animArea.height);
      // Draw moving shape
      ctx.fillStyle = RED;
      ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
      // Draw FPS meter
      const fpsText = fps == undefined ? "FPS" : `${fps}`;
      ctx.textAlign = 'right';
      ctx.fillStyle = GREEN;
      ctx.font = "24px sans-serif";
      ctx.textBaseline = 'top';
      ctx.fillText(fpsText, animArea.width - 5, 5);
    }

    function hitsBorders() {
      if (speed.x < 0 && rect.x < 0)
        return true;
      else if (speed.x > 0 && rect.x + rect.width > animArea.width)
        return true;
      else if (speed.y < 0 && rect.y < 0)
        return true;
      else if (speed.y > 0 && rect.y + rect.height > animArea.height)
        return true;
      return false;
    }
  }

  function adjustCanvasSize() {
    if (window.innerWidth < cWidth + 30)
      animArea.style.width = (window.innerWidth - 30) + "px";
    else
      animArea.style.width = "";
    if (window.innerHeight < cHeight + 30)
      animArea.style.height = (window.innerHeight - 30) + "px";
    else
      animArea.style.height = "";
    animArea.width = animArea.clientWidth;
    animArea.height = animArea.clientHeight;
  }

});
html,
body {
  margin: 0;
  padding: 0;
}

#anim-area {
  width: 800px;
  height: 800px;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Moving square</title>
</head>

<body>
  <div class="content-wrapper">
    <canvas id="anim-area"></canvas>
  </div>
</body>

</html>
julien.giband
  • 2,467
  • 1
  • 12
  • 19
  • 1
    If all you want to do is animate moving something, use a [TranslateTransition](https://openjfx.io/javadoc/17/javafx.graphics/javafx/animation/TranslateTransition.html). Looking at your code though maybe this isn’t all you want to do (I don’t know what is). – jewelsea Mar 14 '22 at 17:31
  • 1
    This works fine for me. – James_D Mar 14 '22 at 17:41
  • 1
    What is your environment? Hardware, OS, Java and JavaFX versions? – jewelsea Mar 14 '22 at 17:50
  • 1
    Some info on [frame rate cap and pulse logging](https://stackoverflow.com/a/28820370/1155209). – jewelsea Mar 14 '22 at 17:53
  • Thank you guys. All I want to do for now is have smooth animation of a shape using JFX. This is just a preliminary test for a project. @jewelsea Platform : JDK temurin 17.0.1_12 and JavaFX 17.0.1 on Windows 10 19044.1586 / Xeon E3-1271 v3 / 2x GeForce GTX 1050Ti. I'll try and run this on another machine to see what it does – julien.giband Mar 15 '22 at 10:00
  • @jewelsea, to be clear no: my project requires being able to animate more objects in different manners at once. The `TranslateTransition` won't do it for me – julien.giband Mar 15 '22 at 10:04
  • The JavaScript version is irrelevant IMO. What was the result of your analysis of the pulse log? – jewelsea Mar 15 '22 at 16:52
  • @jewelsea I kind of skipped that part :). Trying now, and let you know the results – julien.giband Mar 16 '22 at 08:11
  • @jewelsea TBH, I'm not sure how to read this log. I have some idea, but not sure what is relevant. I've done a few tests changing the value of `javafx.animation.pulse`, which does seem to have some effect on this log's presentation, even if nothing is visible in animation. Log files are there : https://www.ahpc-services.com/fileshare/JFX_test_pulses/ the number before the extension represents the value used for the property. There seems to sometimes be a lot of "Waiting for previous rendering". It's about all I can see for now. – julien.giband Mar 16 '22 at 08:56
  • Some more information looking those files: https://www.ahpc-services.com/fileshare/JFX_test_pulses/loggedframes/ I've recorded a session, logged all the frames' lengths in frames.log, converted this log to Excel, highlighting shorter and longer frames, saved the pulse log into pulse.log. Some chaos can be seen at frames 120 -> 132. If you take a look at the recording frame by frame, you can see that no frame is skipped, but the square moves at different rates between the frames. Pulse log is consistent with frames.log. – julien.giband Mar 16 '22 at 10:29
  • So it looks to me like the timestamp passed to each AnimationTimer.handle loop is wrong, and I still don't know what's wrong with my code – julien.giband Mar 16 '22 at 10:29
  • Easier to edit the question and put extra info in it. – jewelsea Mar 16 '22 at 14:32
  • @jewelsea just figured it out : JFX frames (inconsistent at about 67 FPS average) are out of sync with monitor refresh rate ( about 60Hz). This causes some calculated frames to be dispalyed at intervals different from the ones reported by `AnimationTimer.handle`. If I account for that, then the animation appears smooth, event if the calculated framerate rate goes on a rollercoaster. I'll answer myself. Thanks for asking the right questions and motivating me to put more effort in this ! – julien.giband Mar 16 '22 at 14:37
  • According to this [blog](http://werner.yellowcouch.org/log/javafx-8-command-line-options/), there is a prism.vsync switch that defaults to true. What that effectively means though I am not sure. vsync is a complicated and opaque thing with many inputs such as the monitor. the graphics driver, the OS, the graphics hardware, the JavaFX framework and your code. There is also tech like gsync and freesync. Generally though, it usually just works OK with the defaults. But I guess in your case it didn’t. If you have worked out a solution that works for you that’s great. – jewelsea Mar 16 '22 at 14:48
  • One additional thing you might want to do is create the same animation with a TranslateTransition, as well as your custom AnimationTimer, then run the two side by side to observe any difference. Probably behavior will be identical. – jewelsea Mar 16 '22 at 14:52

1 Answers1

1

I figured this out myself. As it turns out, JavaFX takes no account of the actual refresh rate of the display. It calls AnimationTimer.handle with an average rate of about 67Hz (though varying quite widely), while the typical monitor refreshes at around 60Hz.

This causes some frames to be rendered with a delay (the call being quite offset from the screen display frame), and some frames to be reported with a wide variety of lengths whereas the screen will actually display them at a contant rate, thus the inconsistent movement I observed.

I can compensate for that by measuring the screen's refresh rate, and calculating my rectangle's moving rate based on the next frame to be displayed (I won't know the exact timing, but a constant offset will be OK).

So here are the code parts:

1. Get screen refresh rate

stage.setOnShown(e -> {
  Screen screen = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())
      .get(0);
  GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
  GraphicsDevice d = ge.getScreenDevices()[Screen.getScreens().indexOf(screen)];

  int r = d.getDisplayMode().getRefreshRate();
  System.out.println("Screen refresh rate : " + r);
  //Calculate frame duration in nanoseconds
  this.frameNs = 1_000_000_000L / refreshRate; //Store it as it better suits you
});

Note that this method gives an int for refresh rate, whereas screen refresh rates are often not exactly integers (mine is currently 60.008 Hz). But this seems like a good enough approximation, judging by the results

Also it's relying on awt where I'd rather have a pure JavaFX solution, and I'm assuming that screens are reported in the same order by both systems which is far from guaranteed: So use this with caution in production!

2. Alter the animation loop to account for this refresh rate

AnimationTimer anim = new AnimationTimer() {
  private long lastRun = 0;

  @Override
  public void handle(long now) {
    // Skip first frame but record its timing
    if (lastRun == 0) {
      lastRun = now;
      return;
    }
    // If we had 2 JFX frames for 1 screen frame, save a cycle
    if (now <= lastRun)
      return;
    // Calculate remaining time until next screen frame (next multiple of frameNs)
    long rest = now % frameNs;
    long nextFrame = now;
    if (rest != 0) //Fix timing to next screen frame
      nextFrame += frameNs - rest;
    // Animate
    double elapsed = (nextFrame - lastRun) / 1e9;
    // Reverse when hitting borders
    if (hitsBorders())
      speed = speed.multiply(-1.);
    // Update position according to speed
    oldPos = recPos;
    recPos = recPos.add(speed.multiply(elapsed));
    log.println(String.format("%d\t: %d", frameCount, (now - lastRun) / 1_000_000));
    lastRun = nextFrame;
    draw();
  }
};

And with these alterations, the animation runs smoothly

Full code (improved a bit)

import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;

public class TestFxAnimationCanvas extends Application {

  // Set your panel size here
  private static final int FRAME_WIDTH = 1024;
  private static final int FRAME_HEIGHT = 480;

  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    BorderPane root = new BorderPane();
    SmootherAnimation myAnimation = new SmootherAnimation();
    myAnimation.widthProperty().bind(root.widthProperty());
    myAnimation.heightProperty().bind(root.heightProperty());
    root.setCenter(myAnimation);
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.setWidth(FRAME_WIDTH);
    stage.setHeight(FRAME_HEIGHT);

    // Get screen refresh rate and apply it to animation
    stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e -> {
      Screen screen = Screen.getScreensForRectangle(
          stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()
          ).get(0);
      if (screen == null)
        return;
      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      // /!\ Does ge.getScreenDevices really return same order as Screen.getScreens?
      GraphicsDevice d = ge.getScreenDevices()[Screen.getScreens().indexOf(screen)];
      int r = d.getDisplayMode().getRefreshRate(); // /!\ r is an int whereas screen refresh rate is often not an integer
      myAnimation.setRefreshRate(r);
      //TODO: re-assess when window is moved to other screen
    });

    stage.show();
  }
}

class SmootherAnimation extends Canvas {

  private static final double SPEED = 500; // Speed value to be applied in either direction in px/s
  private static final Point2D RECT_DIMS = new Point2D(50, 50); // rectangle size
  // Canvas painting colors
  private static final Paint BLACK = Paint.valueOf("black");
  private static final Paint RED = Paint.valueOf("red");
  private static final Paint GREEN = Paint.valueOf("ForestGreen");
  private static final Paint BLUE = Paint.valueOf("SteelBlue");

  // Defines rectangle start position, stores current position
  private Point2D recPos = new Point2D(0, 50);
  // Defines initial speed, stores current speed
  private Point2D speed = new Point2D(SPEED, 0);

  //Frame rate measurement
  private long frameCount = 0;
  private BigDecimal fps = null;
  long[] frameTimes = new long[120]; //length defines number of rendered frames to average over
  //Frame duration in nanoseconds according to screen refresh rate
  private long frameNs = 1_000_000_000L / 60; //Default to 60Hz

  public SmootherAnimation() throws IOException {

    AnimationTimer anim = new AnimationTimer() {
      private long previousFrame = 0;

      @Override
      public void handle(long now) {
        // Skip first frame but record its timing
        if (previousFrame == 0) {
          previousFrame = now;
          frameTimes[0] = now;
          frameCount++;
          return;
        }
        
        // If we had 2 JFX frames for 1 screen frame, save a cycle by skipping render
        if (now <= previousFrame)
          return;
        
        // Measure FPS
        int frameIndex = (int) (frameCount % frameTimes.length);
        frameTimes[frameIndex] = now;
        if (frameCount > frameTimes.length) {
          int prev = (int) ((frameCount + 1) % frameTimes.length);
          long delta = now - frameTimes[prev];
          double fr = 1e9 / (delta / frameTimes.length);
          fps = new BigDecimal(fr).setScale(2, RoundingMode.HALF_UP);
        }
        frameCount++;
        
        // Calculate remaining time until next screen frame (next multiple of frameNs)
        long rest = now % frameNs;
        long nextFrame = now;
        if (rest != 0) //Fix timing to next screen frame
          nextFrame += frameNs - rest;
        
        // Animate
        updateWorld(previousFrame, nextFrame);
        previousFrame = nextFrame; //Saving last execution
        draw();
      }
    };

    // Start
    anim.start();
  }

  /**
   * Save frame interval in nanoseconds given passed refresh rate
   * @param refreshRate in Hz
   */
  public void setRefreshRate(int refreshRate) {
    this.frameNs = 1_000_000_000L / refreshRate;
  }
  
  /**
   * Perform animation (calculate object positions)
   * @param previousFrame previous animation frame execution time in ns
   * @param nextFrame next animation frame execution time in ns
   */
  private void updateWorld(long previousFrame, long nextFrame) {
    double elapsed = (nextFrame - previousFrame) / 1e9; //Interval in seconds
    // Reverse when hitting borders
    if ( rectHitsBorders(   recPos.getX(), recPos.getY(),
                            RECT_DIMS.getX(), RECT_DIMS.getY(),
                            speed.getX(), speed.getY()) ) {
      speed = speed.multiply(-1.);
    }
    // Update position according to speed
    recPos = recPos.add(speed.multiply(elapsed));
  }

  /**
   * Draw world onto canvas. Also display calculated frame rate and frame count
   */
  private void draw() {
    GraphicsContext gfx = this.getGraphicsContext2D();
    // Clear and draw border
    gfx.setStroke(BLACK);
    gfx.setLineWidth(1);
    gfx.clearRect(0, 0, getWidth(), getHeight());
    gfx.strokeRect(0, 0, getWidth(), getHeight());
    // Draw moving shape
    gfx.setFill(RED);
    gfx.fillRect(recPos.getX(), recPos.getY(), RECT_DIMS.getX(), RECT_DIMS.getY());
    // Draw FPS meter
    String fpsText = fps == null ? "FPS" : fps.toString();
    gfx.setTextAlign(TextAlignment.RIGHT);
    gfx.setTextBaseline(VPos.TOP);
    gfx.setFill(GREEN);
    gfx.setFont(Font.font(24));
    gfx.fillText(fpsText, getWidth() - 5, 5);
    // Draw frame counter
    gfx.setTextAlign(TextAlignment.LEFT);
    gfx.setFill(BLUE);
    gfx.fillText("" + frameCount, 5, 5);
  }

  /**
   * Tells whether moving rectangle is hitting canvas borders
   * @param x considered rectangle horizontal coordinate (top-left from left)
   * @param y considered rectangle vertical coordinate (top-left from top)
   * @param width considered rectangle width
   * @param height considered rectangle height
   * @param speedX speed component in x direction
   * @param speedY speed component in y direction
   * @return true if a canvas border is crossed in the direction of movement
   */
  private boolean rectHitsBorders(double x, double y, double width, double height, double speedX, double speedY) {
    Rectangle2D frame = new Rectangle2D(0, 0, getWidth(), getHeight());
    Rectangle2D rect = new Rectangle2D(x, y, width, height);
    if (speedX < 0 && rect.getMinX() < frame.getMinX())
      return true;
    else if (speedX > 0 && rect.getMaxX() > frame.getMaxX())
      return true;
    else if (speedY < 0 && rect.getMinY() < frame.getMinY())
      return true;
    else if (speedY > 0 && rect.getMaxY() > frame.getMaxY())
      return true;
    return false;
  }

}
julien.giband
  • 2,467
  • 1
  • 12
  • 19