How to create JavaFX Transition with Equally-Timed Elements? - java

I'm experimenting with JavaFX and animations, especially PathTransition. I'm creating a simple program that makes a ball "bounce," without using the QuadCurveTo class. Here is my code so far:
Ellipse ball = new Ellipse(375, 250, 10, 10);
root.getChildren().add(ball);
Path path = new Path();
path.getElements().add(new MoveTo(375, 500));
int posX = 375;
int posY = 500;
int changeX = 10;
int changeY = 50;
int gravity = 10; // approximate in m/s^2
int sec = 0;
for(; posY<=500; sec++, posX-=changeX, posY-=changeY, changeY-=gravity)
path.getElements().add(new LineTo(posX, posY));
// How do I equally space these elements?
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(sec*1000));
pathTransition.setNode(ball);
pathTransition.setAutoReverse(true);
pathTransition.setCycleCount(Timeline.INDEFINITE);
pathTransition.setInterpolator(Interpolator.LINEAR);
pathTransition.setPath(path);
pathTransition.play();
I have the for loop running through a quadratic sequence, and the ball moves in the correct motion (a curved path).
However, I want it to move slower at the top of the curve (vertex) because it is moving less distance (as changeY, the vertical increment variable, is decreasing as the loop goes on) to simulate a more realistic curve. However, it is traveling in a linear speed throughout the full time.
Is there any way to make each of the elements equally spaced (throughout) time, so that this "bounce" would show correctly? Thanks.

I wouldn't use a timeline or transition at all for this. Use an AnimationTimer and compute the new coordinates based on the elapsed time since the last frame. The AnimationTimer has a handle method which is invoked once per rendering frame, taking a value that represents a timestamp in nanoseconds.
SSCCE (with elasticity added to the physics):
import javafx.animation.AnimationTimer;
import javafx.application.Application;
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 BouncingBall extends Application {
#Override
public void start(Stage primaryStage) {
Circle ball = new Circle(20, 80, 10);
ball.setFill(Color.DARKBLUE);
Pane pane = new Pane(ball);
AnimationTimer timer = new AnimationTimer() {
long lastUpdate = 0 ;
double changeX = 0.1 ;
double changeY = 0 ;
double gravity = 10 ;
double elasticity = 0.95 ;
#Override
public void handle(long now) {
if (lastUpdate == 0) {
lastUpdate = now ;
return ;
}
long elapsedNanos = now - lastUpdate;
double elapsedSeconds = elapsedNanos / 1_000_000_000.0 ;
lastUpdate = now ;
ball.setCenterX(ball.getCenterX() + changeX);
if (ball.getCenterY() + changeY + ball.getRadius() >= pane.getHeight()) {
changeY = - changeY * elasticity;
} else {
changeY = changeY + gravity * elapsedSeconds ;
}
ball.setCenterY(Math.min(ball.getCenterY() + changeY, pane.getHeight() - ball.getRadius()));
}
};
primaryStage.setScene(new Scene(pane, 400, 400));
primaryStage.show();
timer.start();
}
public static void main(String[] args) {
launch(args);
}
}

Related

How do I change the speed of an object moved by keyboard input inside an AnimationTimer?

I have this JavaFX Circle, which moves according to keyboard's arrows.
All the AnimationTimer does is refreshing the circle position every frame.
I found a movement of 0.1 every time a KeyEvent is triggered to be smooth enough for the animation, however it moves really slow. On the other hand if I change the movement to let's say 1.0 or 10.0, it's undoubtedly faster, but also much more choppy (you can clearly see it starts moving by discrete values).
I want to be able to keep the smoothness of translating at most 0.1 per frame, but also be able to change how much space it should move every time a key is triggered.
Below is an mre describing the problem:
public class MainFX extends Application {
private double playerX;
private double playerY;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
AnchorPane pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
playerX = pane.getWidth()/2;
playerY = pane.getHeight()/2;
Circle player = new Circle(playerX,playerY,10);
pane.getChildren().add(player);
scene.addEventHandler(KeyEvent.KEY_PRESSED, this::animate);
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
player.setCenterX(playerX);
player.setCenterY(playerY);
}
};
timer.start();
}
private void animate(KeyEvent key){
if (key.getCode() == KeyCode.UP) {
playerY-=0.1;
}
if (key.getCode() == KeyCode.DOWN) {
playerY+=0.1;
}
if (key.getCode() == KeyCode.RIGHT) {
playerX+=0.1;
}
if (key.getCode() == KeyCode.LEFT) {
playerX-=0.1;
}
}
}
The AnimationTimer's handle() method is invoked on every frame rendering. Assuming the FX Application thread is not overwhelmed with other work, this will occur at approximately 60 frames per second. Updating the view from this method will give a relatively smooth animation.
By contrast, the key event handlers are invoked on every key press (or release, etc.) event. Typically, when a key is held down, the native system will issue repeated key press events at some rate that is system dependent (and usually user-configurable), and typically is much slower that animation frames (usually every half second or so). Changing the position of UI elements from here will result in jerky motion.
Your current code updates the position of the UI element from the playerX and playerY variables in the AnimationTimer: however you only change those variable is the key event handlers. So if the AnimationTimer is running at 60fps, and the key events are occurring every 0.5s (for example), you will "update" the UI elements 30 times with each new value, changing the actual position only two times per second.
A better approach is to use key event handlers merely to maintain the state of variables indicating if each key is pressed or not. In the AnimationTimer, update the UI depending on the state of the keys, and the amount of time elapsed since the last update.
Here is a version of your code using this approach:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class MainFX extends Application {
private boolean leftPressed ;
private boolean rightPressed ;
private boolean upPressed ;
private boolean downPressed ;
private static final double SPEED = 100 ; // pixels/second
private static final double PLAYER_RADIUS = 10 ;
private AnchorPane pane;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
double playerX = pane.getWidth() / 2;
double playerY = pane.getHeight() / 2;
Circle player = new Circle(playerX, playerY, PLAYER_RADIUS);
pane.getChildren().add(player);
scene.addEventHandler(KeyEvent.KEY_PRESSED, this::press);
scene.addEventHandler(KeyEvent.KEY_RELEASED, this::release);
AnimationTimer timer = new AnimationTimer() {
private long lastUpdate = System.nanoTime() ;
#Override
public void handle(long now) {
double elapsedSeconds = (now - lastUpdate) / 1_000_000_000.0 ;
int deltaX = 0 ;
int deltaY = 0 ;
if (leftPressed) deltaX -= 1 ;
if (rightPressed) deltaX += 1 ;
if (upPressed) deltaY -= 1 ;
if (downPressed) deltaY += 1 ;
Point2D translationVector = new Point2D(deltaX, deltaY)
.normalize()
.multiply(SPEED * elapsedSeconds);
player.setCenterX(clampX(player.getCenterX() + translationVector.getX()));
player.setCenterY(clampY(player.getCenterY() + translationVector.getY()));
lastUpdate = now ;
}
};
timer.start();
}
private double clampX(double value) {
return clamp(value, PLAYER_RADIUS, pane.getWidth() - PLAYER_RADIUS);
}
private double clampY(double value) {
return clamp(value, PLAYER_RADIUS, pane.getHeight() - PLAYER_RADIUS);
}
private double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
private void press(KeyEvent event) {
handle(event.getCode(), true);
}
private void release(KeyEvent event) {
handle(event.getCode(), false);
}
private void handle(KeyCode key, boolean press) {
switch(key) {
case UP: upPressed = press ; break ;
case DOWN: downPressed = press ; break ;
case LEFT: leftPressed = press ; break ;
case RIGHT: rightPressed = press ; break ;
default: ;
}
}
}
You aren't using the animation timer properly.
Your key code processing should be used to set a velocity for the object. Set the velocity to 0 when the key is released.
In the animation timer code, change the position based on the elapsed time and the velocity.
The timer will fire events at the frame rate - likely around 60fps. If you want smooth motion you want to adjust the position on every frame. Instead you are using it to set the position to a pre-computed value. It isn't doing anything useful that way. You could just as easily set the position in the key processing code and get the same effect you are getting now.
If you don't want to have the user hold down the keys to move. That is, you want to tap once and have the object move by 10.0. You can set the target position in the key processing code. Jumping the target position by 10 at a time. Then have the animation timer move the current position towards the target position at an appropriate velocity, stopping when the target position is reached.
Maybe something like this:
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
double curX = player.getCenterX();
double curY = player.getCenterY();
double diffX = playerX-curX;
double diffY = playerY-curY;
if (diffX > 1.0) {
curX += 1.0;
else if (diffX < -1.0) {
curX -= 1.0;
} else {
curX = playerX;
}
if (diffY > 1.0) {
curY += 1.0;
else if (diffY < -1.0) {
curY -= 1.0;
} else {
curY = playerY;
}
player.setCenterX(curX);
player.setCenterY(curY);
}
};
That's a primitive example... and note that it will make diagonal movements that go faster than axis-aligned movements. (The velocity vector magnitude for diagonal movements is sqrt(2) in that example instead of 1.)
Basically you want to update the position based on a velocity vector adjusted for the interval between ticks of the animation timer.

How to show images in a large frequency in JavaFX?

My application generates heatmap images as fast as the CPU can (around 30-60 per second) and I want to display them in a single "live heatmap". In AWT/Swing, I could just paint them into a JPanel which worked like a charm.
Recently, I switched to JavaFX and want to achieve the same here; at first, I tried with a Canvas, which was slow but okay-ish but had a severe memory leak problem, causing the application to crash. Now, I tried the ImageView component - which apparently is way too slow as the image gets quite laggy (using ImageView.setImage on every new iteration). As far as I understand, setImage does not guarantee that the image is actually displayed when the function finishes.
I am getting the impression that I am on the wrong track, using those components in a manner they are not made to. How can I display my 30-60 Images per second?
EDIT: A very simple test application. You will need the JHeatChart library.
Note that on a desktop machine, I get around 70-80 FPS and the visualization is okay and fluid, but on a smaller raspberry pi (my target machine), I get around 30 FPS but an extremly stucking visualization.
package sample;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.tc33.jheatchart.HeatChart;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
public class Main extends Application {
ImageView imageView = new ImageView();
final int scale = 15;
#Override
public void start(Stage primaryStage) {
Thread generator = new Thread(() -> {
int col = 0;
LinkedList<Long> fps = new LinkedList<>();
while (true) {
fps.add(System.currentTimeMillis());
double[][] matrix = new double[48][128];
for (int i = 0; i < 48; i++) {
for (int j = 0; j < 128; j++) {
matrix[i][j] = col == j ? Math.random() : 0;
}
}
col = (col + 1) % 128;
HeatChart heatChart = new HeatChart(matrix, 0, 1);
heatChart.setShowXAxisValues(false);
heatChart.setShowYAxisValues(false);
heatChart.setLowValueColour(java.awt.Color.black);
heatChart.setHighValueColour(java.awt.Color.white);
heatChart.setAxisThickness(0);
heatChart.setChartMargin(0);
heatChart.setCellSize(new Dimension(1, 1));
long currentTime = System.currentTimeMillis();
fps.removeIf(elem -> currentTime - elem > 1000);
System.out.println(fps.size());
imageView.setImage(SwingFXUtils.toFXImage((BufferedImage) scale(heatChart.getChartImage(), scale), null));
}
});
VBox box = new VBox();
box.getChildren().add(imageView);
Scene scene = new Scene(box, 1920, 720);
primaryStage.setScene(scene);
primaryStage.show();
generator.start();
}
public static void main(String[] args) {
launch(args);
}
private static Image scale(Image image, int scale) {
BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
AffineTransformOp scaleOp =
new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
return scaleOp.filter((BufferedImage) image, res);
}
}
Your code updates the UI from a background thread, which is definitely not allowed. You need to ensure you update from the FX Application Thread. You also want to try to "throttle" the actual UI updates to occur no more than once per JavaFX frame rendering. The easiest way to do this is with an AnimationTimer, whose handle() method is invoked each time a frame is rendered.
Here's a version of your code which does that:
import java.awt.Dimension;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicReference;
import org.tc33.jheatchart.HeatChart;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
ImageView imageView = new ImageView();
final int scale = 15;
#Override
public void start(Stage primaryStage) {
AtomicReference<BufferedImage> image = new AtomicReference<>();
Thread generator = new Thread(() -> {
int col = 0;
LinkedList<Long> fps = new LinkedList<>();
while (true) {
fps.add(System.currentTimeMillis());
double[][] matrix = new double[48][128];
for (int i = 0; i < 48; i++) {
for (int j = 0; j < 128; j++) {
matrix[i][j] = col == j ? Math.random() : 0;
}
}
col = (col + 1) % 128;
HeatChart heatChart = new HeatChart(matrix, 0, 1);
heatChart.setShowXAxisValues(false);
heatChart.setShowYAxisValues(false);
heatChart.setLowValueColour(java.awt.Color.black);
heatChart.setHighValueColour(java.awt.Color.white);
heatChart.setAxisThickness(0);
heatChart.setChartMargin(0);
heatChart.setCellSize(new Dimension(1, 1));
long currentTime = System.currentTimeMillis();
fps.removeIf(elem -> currentTime - elem > 1000);
System.out.println(fps.size());
image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
}
});
VBox box = new VBox();
box.getChildren().add(imageView);
Scene scene = new Scene(box, 1920, 720);
primaryStage.setScene(scene);
primaryStage.show();
generator.setDaemon(true);
generator.start();
AnimationTimer animation = new AnimationTimer() {
#Override
public void handle(long now) {
BufferedImage img = image.getAndSet(null);
if (img != null) {
imageView.setImage(SwingFXUtils.toFXImage(img, null));
}
}
};
animation.start();
}
public static void main(String[] args) {
launch(args);
}
private static Image scale(Image image, int scale) {
BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
return scaleOp.filter((BufferedImage) image, res);
}
}
Using the AtomicReference to wrap the buffered image ensures that it is safely shared between the two threads.
On my machine, this generates about 130 images per second; note that not all are displayed, as only the latest one is shown each time the JavaFX graphics framework displays a frame (which is typically throttled at 60fps).
If you want to ensure you show all images that are generated, i.e. you throttle the image generation by the JavaFX framerate, then you can use a BlockingQueue to store the images:
// AtomicReference<BufferedImage> image = new AtomicReference<>();
// Size of the queue is a trade-off between memory consumption
// and smoothness (essentially works as a buffer size)
BlockingQueue<BufferedImage> image = new ArrayBlockingQueue<>(5);
// ...
// image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
try {
image.put((BufferedImage) scale(heatChart.getChartImage(), scale));
} catch (InterruptedException exc) {
Thread.currentThread.interrupt();
}
and
#Override
public void handle(long now) {
BufferedImage img = image.poll();
if (img != null) {
imageView.setImage(SwingFXUtils.toFXImage(img, null));
}
}
The code is pretty inefficient, as you generate a new matrix, new HeatChart, etc, on every iteration. This causes many objects to be created on the heap and quickly discarded, which can cause the GC to be run too often, particularly on a small-memory machine. That said, I ran this with the maximum heap size set at 64MB, (-Xmx64m), and it still performed fine. You may be able to optimize the code, but using the AnimationTimer as shown above, generating images more quickly will not cause any additional stress on the JavaFX framework. I would recommend investigating using the mutability of HeatChart (i.e. setZValues()) to avoid creating too many objects, and/or using PixelBuffer to directly write data to the image view (this would need to be done on the FX Application Thread).
Here's a different example, which (almost) completely minimizes object creation, using one off-screen int[] array to compute data, and one on-screen int[] array to display it. There's a little low-level threading details to ensure the on-screen array is only seen in a consistent state. The on-screen array is used to underly a PixelBuffer, which in turn is used for a WritableImage.
This class generates the image data:
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
public class ImageGenerator {
private final int width;
private final int height;
// Keep two copies of the data: one which is not exposed
// that we modify on the fly during computation;
// another which we expose publicly.
// The publicly exposed one can be viewed only in a complete
// state if operations on it are synchronized on this object.
private final int[] privateData ;
private final int[] publicData ;
private final long[] frameTimes ;
private int currentFrameIndex ;
private final AtomicLong averageGenerationTime ;
private final ReentrantLock lock ;
private static final double TWO_PI = 2 * Math.PI;
private static final double PI_BY_TWELVE = Math.PI / 12; // 15 degrees
public ImageGenerator(int width, int height) {
super();
this.width = width;
this.height = height;
privateData = new int[width * height];
publicData = new int[width * height];
lock = new ReentrantLock();
this.frameTimes = new long[100];
this.averageGenerationTime = new AtomicLong();
}
public void generateImage(double angle) {
// compute in private data copy:
int minDim = Math.min(width, height);
int minR2 = minDim * minDim / 4;
for (int x = 0; x < width; x++) {
int xOff = x - width / 2;
int xOff2 = xOff * xOff;
for (int y = 0; y < height; y++) {
int index = x + y * width;
int yOff = y - height / 2;
int yOff2 = yOff * yOff;
int r2 = xOff2 + yOff2;
if (r2 > minR2) {
privateData[index] = 0xffffffff; // white
} else {
double theta = Math.atan2(yOff, xOff);
double delta = Math.abs(theta - angle);
if (delta > TWO_PI - PI_BY_TWELVE) {
delta = TWO_PI - delta;
}
if (delta < PI_BY_TWELVE) {
int green = (int) (255 * (1 - delta / PI_BY_TWELVE));
privateData[index] = (0xff << 24) | (green << 8); // green, fading away from center
} else {
privateData[index] = 0xff << 24; // black
}
}
}
}
// copy computed data to public data copy:
lock.lock();
try {
System.arraycopy(privateData, 0, publicData, 0, privateData.length);
} finally {
lock.unlock();
}
frameTimes[currentFrameIndex] = System.nanoTime() ;
int nextIndex = (currentFrameIndex + 1) % frameTimes.length ;
if (frameTimes[nextIndex] > 0) {
averageGenerationTime.set((frameTimes[currentFrameIndex] - frameTimes[nextIndex]) / frameTimes.length);
}
currentFrameIndex = nextIndex ;
}
public void consumeData(Consumer<int[]> consumer) {
lock.lock();
try {
consumer.accept(publicData);
} finally {
lock.unlock();
}
}
public long getAverageGenerationTime() {
return averageGenerationTime.get() ;
}
}
And here's the UI:
import java.nio.IntBuffer;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AnimationApp extends Application {
private final int size = 400 ;
private IntBuffer buffer ;
#Override
public void start(Stage primaryStage) throws Exception {
// background image data generation:
ImageGenerator generator = new ImageGenerator(size, size);
// Generate new image data as fast as possible:
Thread thread = new Thread(() -> {
while( true ) {
long now = System.currentTimeMillis() ;
double angle = 2 * Math.PI * (now % 10000) / 10000 - Math.PI;
generator.generateImage(angle);
}
});
thread.setDaemon(true);
thread.start();
generator.consumeData(data -> buffer = IntBuffer.wrap(data));
PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
WritableImage image = new WritableImage(pixelBuffer);
BorderPane root = new BorderPane(new ImageView(image));
Label fps = new Label("FPS: ");
root.setTop(fps);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("Give me a ping, Vasili. ");
primaryStage.show();
AnimationTimer animation = new AnimationTimer() {
#Override
public void handle(long now) {
// Update image, ensuring we only see the underlying
// data in a consistent state:
generator.consumeData(data -> {
pixelBuffer.updateBuffer(pb -> null);
});
long aveGenTime = generator.getAverageGenerationTime() ;
if (aveGenTime > 0) {
double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
fps.setText(String.format("FPS: %.2f", aveFPS));
}
}
};
animation.start();
}
public static void main(String[] args) {
Application.launch(args);
}
}
For a version that doesn't rely on the JavaFX 13 PixelBuffer, you can just modify this class to use a PixelWriter (AIUI this won't be quite as efficient, but works just as smoothly in this example):
// generator.consumeData(data -> buffer = IntBuffer.wrap(data));
PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
// PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
// WritableImage image = new WritableImage(pixelBuffer);
WritableImage image = new WritableImage(size, size);
PixelWriter pixelWriter = image.getPixelWriter() ;
and
AnimationTimer animation = new AnimationTimer() {
#Override
public void handle(long now) {
// Update image, ensuring we only see the underlying
// data in a consistent state:
generator.consumeData(data -> {
// pixelBuffer.updateBuffer(pb -> null);
pixelWriter.setPixels(0, 0, size, size, format, data, 0, size);
});
long aveGenTime = generator.getAverageGenerationTime() ;
if (aveGenTime > 0) {
double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
fps.setText(String.format("FPS: %.2f", aveFPS));
}
}
};

bullet trajectory

First of all, I want to shoot a plane with a cannon.
I've setted this Timeline for the trajectory, but I don't see the bullet on my Scene. It's very likely that my trajectory's code isn't correct. I tried to look on the internet about formula for projectile motion, but I understand nothing about physics;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Game_1 extends Application {
private final double gravity = 9.81;
private Timeline timeline;
private ImageView plane;
private Circle circle;
private AnchorPane ap;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Test");
Group group = new Group();
Scene scene = new Scene(group, 600, 350);
scene.setFill(Color.BLACK);
primaryStage.setScene(scene);
primaryStage.show();
}
private void shoot() {
double x = 65.0f;
double y = 408;
double speed = 200;
double t = 2;
double angle = -45;
double dx = Math.cos(angle) * speed;
double dy = Math.sin(angle) * speed;
circle = new Circle(x, y, 5, Color.BLACK);
double x2 = x + dx * t;
double y2 = (Math.tan(angle) * y - (gravity / (2 * Math.pow(speed, 2) * Math.cos(angle))) * Math.pow(x, 2));
timeline = new Timeline();
KeyValue xKV = new KeyValue(circle.centerXProperty(), x2);
KeyValue yKV = new KeyValue(circle.centerYProperty(), y2, new Interpolator() {
#Override
protected double curve(double t) {
return y + dy * t - 0.5 * gravity * t * t;
}
});
KeyFrame xKF = new KeyFrame(Duration.seconds(t), xKV);
KeyFrame yKF = new KeyFrame(Duration.seconds(t), yKV);
timeline.getKeyFrames().addAll(xKF, yKF);
ap.getChildren().add(circle);
timeline.play();
collision();
}
private void collision() {
circle.boundsInParentProperty().addListener((ObservableValue<? extends Bounds> arg0, Bounds oldValue2, Bounds newValue2) -> {
if (circle.getBoundsInParent().intersects(plane.getBoundsInParent())) {
timeline.stop();
ap.getChildren().remove(circle);
}
});
}
}
The curve method should map to the interval [0, 1]. Your method however maps to much higher values. The value val at time t of a animation from t0 to t1 for a interpolator i given start value val0 and end value val1 is calculated as follows:
val = val0 + (val1 - val0) * i.curve((t - t0) / (t1 - t0))
The parameter of the curve method is the relative position in the time interval (0 = start of animation; 1 = end of animation). The result of the method is used to determine how close the value is to the end value (0 = still at the start value; 1 = at the end value).
Therefore you should probably calculate the top point hMax in the cannonball's curve (as described e.g. here on Wikipedia) and use a different interpolator:
Interpolator interpolator = new Interpolator() {
#Override
protected double curve(double t) {
// parabola with zeros at t=0 and t=1 and a maximum of 1 at t=0.5
return 4 * t * (1 - t);
}
};
KeyValue yKV = new KeyValue(circle.centerYProperty(), hMax, interpolator);
Note that upward movement means decreasing the y coordinate for the UI so in this case hMax should be smaller than the y value at the start.
Appart from that your shoot method is never called and some fields are not initialized which would result in a NPE in case it was called. Furthermore if those 2 issues are fixed, a black circle on a black background will be hard to see...
Example
Note that this is not using any physical fromulae and instead just uses some values chosen by me:
#Override
public void start(Stage primaryStage) {
Circle circle = new Circle(10);
circle.setManaged(false);
Pane pane = new Pane(circle);
circle.setCenterX(20);
circle.setCenterY(800);
Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO,
new KeyValue(circle.centerXProperty(), 20),
new KeyValue(circle.centerYProperty(), 800)
), new KeyFrame(Duration.seconds(3),
new KeyValue(circle.centerXProperty(), 380),
new KeyValue(circle.centerYProperty(), 10, new Interpolator() {
#Override
protected double curve(double t) {
// parabola with zeros at t=0 and t=1 and a maximum of 1 at t=0.5
return 4 * t * (1 - t);
}
})
)
);
Scene scene = new Scene(pane, 400, 800);
scene.setOnMouseClicked(evt -> timeline.playFromStart());
primaryStage.setScene(scene);
primaryStage.show();
}
Note that Interpolator.curve is supposed to return 0 for parameter 0 and 1 for parameter 1. Anything else will probably result in jumps, should the property be animated further. Maybe the y-movement in 2 parts would be more appropriate, in case you want to move the ball around after the animation is finished.
I.e.
Interpolator 1: t * (2 - t)
Interpolator 2: t * t
using half the time interval each with end values of the top and start y coordinate of the curve respectively.

Space ship simulator guidance computer targeting with concentric indicator squares

I'm working on a 3D space trading game with some people, and one of the things I've been assigned to do is to make a guidance computer 'tunnel' that the ship travels through, with the tunnel made of squares that the user flies through to their destination, increasing in number as the user gets closer to the destination.
It's only necessary to render the squares for the points ahead of the ship, since that's all that's visible to the user. On their way to a destination, the ship's computer is supposed to put up squares on the HUD that represent fixed points in space between you and the destination, which are small in the distance and get larger as the points approach the craft.
I've had a go at implementing this and can't seem to figure it out, mainly using logarithms (Math.log10(x) and such). I tried to get to get the ship position in 'logarithmic space' to help find out what index to start from when drawing the squares, but then the fact that I only have distance to the destination to work with confuses the matter, especially when you consider that the number of squares has to vary dynamically to make sure they stay fixed at the right locations in space (i.e., the squares are positioned at intervals of 200 or so before being transformed logarithmically).
With regard to this, I had a working implementation with the ship between a start of 0.0d and end of 1.0d, although the implementation wasn't so nice. Anyway, the problem essentially boils down to a 1d nature. Any advice would be appreciated with this issue, including possible workarounds to achieve the same effect or solutions.
(Also, there's a Youtube video showing this effect: http://www.youtube.com/watch?v=79F9Nj7GgfM&t=3m5s)
Cheers,
Chris
Edit: rephrased the entire question.
Edit: new testbed code:
package st;
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.text.DecimalFormat;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class StUI2 extends JFrame {
public static final double DEG_TO_RAD = Math.PI / 180.0d;
public static final DecimalFormat decimalFormat = new DecimalFormat("0.0000");
public static final Font MONO = new Font("Monospaced", Font.PLAIN, 10);
public class StPanel extends Canvas {
protected final Object imgLock = new Object();
protected int lastWidth = 1, lastHeight = 1;
protected boolean first = true;
protected Color bgColour = Color.DARK_GRAY, gridColour = Color.GRAY;
double shipWrap = 700;
double shipFrame = 100;
double shipPos = 0;
long lastUpdateTimeMS = -1;
long currUpdateTimeMS = -1;
public StPanel() {
setFocusable(true);
setMinimumSize(new Dimension(1, 1));
setAlwaysOnTop(true);
}
public void internalPaint(Graphics2D g) {
synchronized (imgLock) {
if (lastUpdateTimeMS < 0) {
lastUpdateTimeMS = System.currentTimeMillis();
}
currUpdateTimeMS = System.currentTimeMillis();
long diffMS = currUpdateTimeMS - lastUpdateTimeMS;
g.setFont(MONO);
shipPos += (60d * ((double)diffMS / 1000));
if (shipPos > shipWrap) {
shipPos = 0d;
}
double shipPosPerc = shipPos / shipWrap;
double distToDest = shipWrap - shipPos;
double compression = 1000d / distToDest;
g.setColor(bgColour);
Dimension d = getSize();
g.fillRect(0, 0, (int)d.getWidth(), (int)d.getHeight());
//int amnt2 = (int)unlog10((1000d / distToDest));
g.setColor(Color.WHITE);
g.drawString("shipPos: " + decimalFormat.format(shipPos), 10, 10);
g.drawString("distToDest: " + decimalFormat.format(distToDest), 10, 20);
g.drawString("shipWrap: " + decimalFormat.format(shipWrap), 150, 10);
int offset = 40;
g.setFont(MONO);
double scalingFactor = 10d;
double dist = 0;
int curri = 0;
int i = 0;
do {
curri = i;
g.setColor(Color.GREEN);
dist = distToDest - getSquareDistance(distToDest, scalingFactor, i);
double sqh = getSquareHeight(dist, 100d * DEG_TO_RAD);
g.drawLine(30 + (int)dist, (offset + 50) - (int)(sqh / 2d), 30 + (int)dist, (offset + 50) + (int)(sqh / 2d));
g.setColor(Color.LIGHT_GRAY);
g.drawString("i: " + i + ", dist: " + decimalFormat.format(dist), 10, 120 + (i * 10));
i++;
} while (dist < distToDest);
g.drawLine(10, 122, 200, 122);
g.drawString("last / i: " + curri + ", dist: " + decimalFormat.format(dist), 10, 122 + (i * 10));
g.setColor(Color.MAGENTA);
g.fillOval(30 + (int)shipPos, offset + 50, 4, 4);
lastUpdateTimeMS = currUpdateTimeMS;
}
}
public double getSquareDistance(double initialDist, double scalingFactor, int num) {
return Math.pow(scalingFactor, num) * num * initialDist;
}
public double getSquareHeight(double distance, double angle) {
return distance / Math.tan(angle);
}
/* (non-Javadoc)
* #see java.awt.Canvas#paint(java.awt.Graphics)
*/
#Override
public void paint(Graphics g) {
internalPaint((Graphics2D)g);
}
public void redraw() {
synchronized (imgLock) {
Dimension d = getSize();
if (d.width == 0) d.width = 1;
if (d.height == 0) d.height = 1;
if (first || d.getWidth() != lastWidth || d.getHeight() != lastHeight) {
first = false;
// remake buf
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
//create an object that represents the device that outputs to screen (video card).
GraphicsDevice gd = ge.getDefaultScreenDevice();
gd.getDefaultConfiguration();
createBufferStrategy(2);
lastWidth = (int)d.getWidth();
lastHeight = (int)d.getHeight();
}
BufferStrategy strategy = getBufferStrategy();
Graphics2D g = (Graphics2D)strategy.getDrawGraphics();
internalPaint(g);
g.dispose();
if (!strategy.contentsLost()) strategy.show();
}
}
}
protected final StPanel canvas;
protected Timer viewTimer = new Timer(1000 / 60, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
canvas.redraw();
}
});
{
viewTimer.setRepeats(true);
viewTimer.setCoalesce(true);
}
/**
* Create the applet.
*/
public StUI2() {
JPanel panel = new JPanel(new BorderLayout());
setContentPane(panel);
panel.add(canvas = new StPanel(), BorderLayout.CENTER);
setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(800, 300);
setTitle("Targetting indicator test #2");
viewTimer.start();
}
public static double unlog10(double x) {
return Math.pow(10d, x);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
StUI2 ui = new StUI2();
}
});
}
}
Assuming you want the squares to be equal height (when you reach them), you can calculate a scaling factor based on the distance to the destination (d) and the required height of the squares upon reaching them (h).
From these two pieces of information you can calculate the inverse tangent (atan) of the angle (alpha) between the line connecting the ship to the destination (horizontal line in your image) and the line connecting the top of the squares with the destination (angled line in your image).
EDIT: corrected formula
Using the angle, you can calculate the height of the square (h') at any given distance from the destination: you know the distance to the destination (d') and the angle (alpha); The height of the square at distance d' is h'=r'*sin(alpha) -- sin(alpha)=cos(alpha)*tan(alpha) and r'=d'/cos(alpha) (the distance between the destination and the top of the square -- the "radius"). Or more easily: h'=d'*tan(alpha).
Note: adopting the algorithm to varying height (when you reach them) squares is relatively simple: when calculating the angle, just assume a (phantom) square of fixed height and scale the squares relatively to that.
If the height of the square at distance d' is calculated for you by your graphic library, all the better, you only need to figure out the distances to place the squares.
What distances to place the squares from the destination?
1) If you want a varying number of squares shown (in front of the ship), but potentially infinite number of squares to consider (based on d), you can chose the distance of the closest square to the destination (d1) and calculate the distances of other squares by the formula s^k*k*d1, where s (scaling factor) is a number > 1 for the k'th square (counting from the destination). You can stop the algorithm when the result is larger than d.
Note that if d is sufficiently large, the squares closest to the distance will block the destination (there are many of them and their heights are small due to the low angle). In this case you can introduce a minimal distance (possibly based on d), below which you do not display the squares -- you will have to experiment with the exact values to see what looks right/acceptable.
2) If you want a fixed amount of squares (sn) showing always, regardless of d, you can calculate the distances of the squares from the destination by the formula d*s^k, where s is a number < 1, k is the index of the square (counting from the ship). The consideration about small squares probably don't apply here unless sn is high.
To fix the updated code, change the relavant part to:
double dist = 0;
double d1 = 10;
int curri = 0;
int i = 1;
int maxSquareHeight = 40;
double angle = Math.atan(maxSquareHeight/distToDest);
while (true)
{
curri = i;
g.setColor(Color.GREEN);
dist = getSquareDistance(d1, scalingFactor, i);
if (dist > distToDest) {
break;
}
double sqh = getSquareHeight(dist, angle);
g.drawLine(30 + (int)(shipWrap - dist), offset+50-(int)(sqh / 2d), 30 + (int)(shipWrap - dist), offset+50+(int)(sqh / 2d));
g.setColor(Color.LIGHT_GRAY);
i++;
}
public double getSquareHeight(double distance, double angle) {
return distance * Math.tan(angle);
}
You should also reduce scalingFactor to the magnitude of ~1.5.
EDIT: If you replace the formula s^k*k*d1 with s^(k-1)*k*d1, then the first square will be exactly at distance d1.
EDIT: fixed square height calculating formula
EDIT: updated code

How do you move an object in a wavy pattern?

I know the following code will move an object in a straight line. How can I get the object to travel in a wavy line? I know that something extra is required for the x variable.
public void draw(Graphics2D g)
{
g.setColor(Color.WHITE);
g.fillOval ((int) (x - r), (int) (y - r), (int)
(2 * r),
(int) (2 * r));
y++;
if (y - r > height)
y = -r;
}
Use the sine or cosine function to calculate y as a function of x.
Multiply the sine or cosine function to increase the amplitude (how high it goes)
y = 100 * sin(x) // will make it have peaks of -100 and 100
Divide the x to increase the period. (distance between peaks)
y = sin(x/2) // will make it take twice the x distance between peaks.
Something like this:
public void draw(Graphics2D g)
{
g.setColor(Color.WHITE);
g.fillOval ((int) (x - r), (int) (y - r), (int)
(2 * r),
(int) (2 * r));
x++; // Left to right movement
// Example, modify the multipliers as necessary
y = 100 * Math.sin(Math.toDegrees(x/4))
}
Including a sin(x) or cos(x) in your function will provide a regular wave pattern, irregular pattern needs a more sophisticated function
I know you already accepted an answer, but here's something to draw additional inspiration from that I whipped up...
package wavy;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Wavy {
public static void main(String[] args) {
final JFrame frame = new JFrame("Wavy!");
final WavyPanel wp = new WavyPanel();
frame.getContentPane().add(wp, BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final Ticker t = new Ticker(wp);
final Repainter r = new Repainter(wp);
frame.pack();
frame.setVisible(true);
final Timer tickTimer = new Timer();
final Timer paintTimer = new Timer();
paintTimer.schedule(r, 1000, 50);
tickTimer.schedule(t, 1000, 10);
}
private static class WavyPanel extends JPanel {
private final Dimension size = new Dimension(640, 480);
private int amplitude = 50;
private int frequency = 5;
private int x = 0;
private double y = size.height / 2;
private int yBase = 0;
WavyPanel() {
super(true);
}
#Override
protected void paintComponent(final Graphics g) {
final Graphics2D g2 = (Graphics2D)g;
g2.setColor(Color.WHITE);
g2.fillRect(0, 0, size.width, size.height);
g2.setColor(Color.BLACK);
g2.fillOval(x, (int)y, 30, 30);
}
#Override
public Dimension getPreferredSize() {
return size;
}
#Override
public Dimension getMinimumSize() {
return size;
}
#Override
public Dimension getMaximumSize() {
return size;
}
public void tick() {
//Move a pixel to the right; loop over to the left when reaching edge
x = (++x) % size.width;
//Length of one full wave = panel width divided by frequency
final int waveLength = size.width / frequency;
//Incrementing yBase; capping off at wavelength
yBase = (++yBase) % waveLength;
//Normalizing to [0..1]
final double normalized = (double)yBase / (double)waveLength;
//Full wave at 2*pi, means...
final double radians = normalized * Math.PI * 2;
//Getting the sine
final double sine = Math.sin(radians);
//Multiplying with amplitude, add to center position and we have our y
y = (int)(sine * amplitude) + size.height/2;
}
}
private static class Ticker extends TimerTask {
private final WavyPanel panel;
Ticker(final WavyPanel panel) {
this.panel = panel;
}
#Override
public void run() {
panel.tick();
}
}
private static class Repainter extends TimerTask {
private final WavyPanel panel;
Repainter(final WavyPanel panel) {
this.panel = panel;
}
#Override
public void run() {
panel.repaint();
}
}
}
This should run at an approximate 20 frames per second. You can increase this by setting the second argument of paintTimer.schedule(r, 1000, 50) lower. The speed of movement can be altered by lowering (speeding up) or increasing (slower) the second argument of tickTimer.schedule(t, 1000, 50).
Changing the amplitude field of WavyPanel will change how high/low the circle moves. Changing the frequency to a higher value will result in shorter waves, while a lower value will produce longer waves.
With some additional work you could add in controls to change the amplitude and frequency on-the-fly. Some additional notes:
You may wish to add some safeguard to the tick() method to make sure that when one invocation is already running, additional ones are skipped until the first one is done. Otherwise the calculations could fail for short tick intervals. A semaphore could be used here.
Since trigonometric calculations aren't exactly the cheapest, you may consider caching some results (e.g. in an array) for re-use if many similar animations are to be played or if there's a lot more drawing going on.
I hope I'm interpreting this right. Could use the sine or cosine of either your x or y coordinate. I'm not at a machine with java so I can't make an example at the moment..
You're right that you need to update both the x and y variables to get a wavy line. Here's the general strategy for a horizontal line that is wavy up and down:
Choose a function f(x) that has the shape you want. This will be used to calculate values for y. (For instance, you can use y = amplitude * Math.sin(frequency * x) to get a regular sine wave of a given amplitude and frequency.)
If necessary, write the code that implements your function.
Set x to some initial value.
In draw, before you paint the oval, calculate y = f(x);. Paint the oval and then increment x. If necessary, reset x so it stays in range.
If you want a vertical line that is wavy left and right, just reverse the roles of x and y in the above. If you want the oval to go in the reverse direction, just decrement instead of increment in step 4.
this sample is for point(Line with one length) on sinus graph and clock using.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class RunSwing extends JPanel {
static int x1 = 500;
static int y1 = 500;
static int x2 = x1;
static int y2 = y1;
final static int vectorLength = 100;
final static int sinx2 = x2;
final static int siny2 = y2;
static double count = 0;
private static RunSwing run = new RunSwing();
final Timer print = new Timer(1000, new ActionListener() {
#Override
public void actionPerformed(final ActionEvent e) {
//increaseSinusGraph();
increaseClockVector();
count+=6; //for clock for 1 second
/*count++;//for sinus*/
if (count % 360 == 0)
System.out.println((count / 360) + " minute passed");
}
});
RunSwing() {
print.start();
}
public static void main(String[] args) {
JFrame frame = new JFrame("amir");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(run);
frame.setSize(1100, 700);
frame.setVisible(true);
}
static void increaseClockVector() {
double cos = Math.cos(Math.toRadians(count));
double sin = Math.sin(Math.toRadians(count));
y2 = siny2 + (int) (vectorLength * sin);
x2 = sinx2 + (int) (vectorLength * cos);
}
static void increaseSinusGraph() {
double sin = Math.sin(Math.toRadians(count));
y2 = siny2 + (int) (vectorLength * sin);
x2++;
}
private void createPoint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.drawLine(x2, y2, x2 + 1, y2 + 1);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(new Color(0, 0, 0));
g.drawLine(x1, y1, x2, y2);//for clock
/*g.drawLine(x2, y2, x2+1, y2+1);//for sinus*/
repaint();
}
}

Categories

Resources