I made a little JavaFX app generating longshadows. At this point I struggle with the rendering (see picture).
The missing line on the rectangle's corner seems hard to fix. Changing the loop, which applies the manipulation, will mess up other shapes' shadow (e.g. circle).
The glitch at 'a' is related to the Bresenham algorithm, I guess.(?)
Additional info:
Changing the image resolution makes no difference: Gitches keep showing.
Question:
How to get it fixed? Does the SDK provide something helpful? Do I have to rewrite the code?
Code
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import javax.imageio.ImageIO;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
public class Main extends Application {
private PrintWriter writer;
private String colorObjFilter = "0x009688ff";
private static final String IMG_PATH = "img/ls-test-1k.png";
private static final int LONGSHADOW_LENGTH = 100;
private static final String
ANSI_GREEN = "\u001B[32m",
ANSI_RESET = "\u001B[0m";
#Override
public void start(Stage stage) throws Exception {
writer = new PrintWriter("out.txt", "UTF-8");
InputStream is = new FileInputStream(new File(IMG_PATH));
ImageView imageView = new ImageView(new Image(is));
Image image = imageView.getImage();
StackPane root = new StackPane();
Scene scene = new Scene(root, image.getWidth(), image.getHeight(), Paint.valueOf
("#EEEEEE"));
scene.addEventFilter(KeyEvent.KEY_PRESSED, evt -> {
if (evt.getCode().equals(KeyCode.ESCAPE)) {
stage.close();
}
});
final Canvas canvas = new Canvas(image.getWidth(), image.getHeight());
canvas.setOnMouseClicked((MouseEvent e) -> {
Color color = image.getPixelReader().getColor((int) e.getX(), (int) e.getY());
System.out.println(ANSI_GREEN + " -> " + color.toString() + ANSI_RESET);
colorObjFilter = color.toString();
try {
processImage(root, canvas, image);
} catch (IOException e1) {
e1.printStackTrace();
}
});
root.getChildren().addAll(imageView, canvas);
stage.setScene(scene);
stage.show();
}
private void processImage(StackPane root, Canvas canvas, Image image) throws IOException {
long delta = System.currentTimeMillis();
int width = (int) image.getWidth();
int height = (int) image.getHeight();
GraphicsContext gc = canvas.getGraphicsContext2D();
System.out.println("width: " + width + "\theight: " + height);
BufferedImage bufferedImage = ImageIO.read(new File(IMG_PATH));
// keep threshold small to get clean paths to draw
edgeDetection(gc, image, 0.00000001d);
writer.close();
Label label = new Label();
root.setAlignment(Pos.BOTTOM_LEFT);
root.setOnMouseMoved(event -> label.setText(event.getX() + "|" + event.getY()
+ "|" + bufferedImage.getRGB((int) event.getX(), (int) event.getY())));
root.getChildren().addAll(label);
System.out.println("took: " + (System.currentTimeMillis() - delta) + " ms");
}
public void edgeDetection(GraphicsContext gc, Image image, double threshold) {
Color topPxl, lowerPxl;
double topIntensity, lowerIntensity;
PixelWriter pw = gc.getPixelWriter();
for (int y = 0; y < image.getHeight() - 1; y++) {
for (int x = 1; x < image.getWidth(); x++) {
topPxl = image.getPixelReader().getColor(x, y);
lowerPxl = image.getPixelReader().getColor(x - 1, y + 1);
topIntensity = (topPxl.getRed() + topPxl.getGreen() + topPxl.getBlue()) / 3;
lowerIntensity = (lowerPxl.getRed() + lowerPxl.getGreen() + lowerPxl.getBlue()) / 3;
if (Math.abs(topIntensity - lowerIntensity) > threshold) {
int y2 = y;
for (int x2 = x; x2 < x + LONGSHADOW_LENGTH; x2++) {
y2++;
try {
Color color = image.getPixelReader().getColor(x2, y2);
// colorObjFilter protects the purple letter being manipulated
if (!color.toString().toLowerCase()
.contains(colorObjFilter.toLowerCase())) {
pw.setColor(x2, y2, Color.color(.7f, .7f, .7f, .9f));
}
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}
}
}
}
public static void main(String[] args) {
launch(args);
}
}
I have no idea why your original program has some rendering artifacts.
Here is an alternate solution, which is extremely brute force, as it just generates a shadow image that it renders over and over again at different offsets to end up with a long shadow. A couple of methods of generating the shadowImage are demonstrated, one is a ColorAdjust effect on the original image, the other is generation of a shadow image using a PixelWriter.
Your original solution of using a PixelWriter for everything with an appropriate algorithm for shadow generation is more elegant (if you can get it to work ;-).
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class ShadowSpray extends Application {
private static final double W = 400;
private static final double H = 400;
private static final int SHADOW_LENGTH = 100;
private static final double IMG_X = 20;
private static final double IMG_Y = 20;
private static final int FONT_SIZE = 200;
private static final double SHADOW_SLOPE_FACTOR = 1.5;
Color SHADOW_COLOR = Color.GRAY.brighter();
#Override
public void start(Stage stage) {
Image image = getImage();
Canvas canvas = new Canvas(W, H);
GraphicsContext gc = canvas.getGraphicsContext2D();
// drawWithShadowUsingStencil(gc, image, IMG_X, IMG_Y, SHADOW_LENGTH, SHADOW_COLOR);
drawWithShadowUsingColorAdjust(gc, image, IMG_X, IMG_Y, SHADOW_LENGTH);
stage.setScene(new Scene(new Group(canvas)));
stage.show();
}
private void drawWithShadowUsingColorAdjust(GraphicsContext gc, Image image, double x, double y, int shadowLength) {
// here the color adjust for the shadow is based upon the intensity of the input image color
// which is a weird way to calculate a shadow color, but does come out nicely
// because it appropriately handles antialiased input images.
ColorAdjust monochrome = new ColorAdjust();
monochrome.setBrightness(+0.5);
monochrome.setSaturation(-1.0);
gc.setEffect(monochrome);
for (int offset = shadowLength; offset > 0; --offset) {
gc.drawImage(image, x + offset, y + offset / SHADOW_SLOPE_FACTOR);
}
gc.setEffect(null);
gc.drawImage(image, x, y);
}
private void drawWithShadowUsingStencil(GraphicsContext gc, Image image, double x, double y, int shadowLength, Color shadowColor) {
Image shadow = createShadowImage(image, shadowColor);
for (int offset = shadowLength; offset > 0; --offset) {
gc.drawImage(shadow, x + offset, y + offset / SHADOW_SLOPE_FACTOR);
}
gc.drawImage(image, x, y);
}
private Image createShadowImage(Image image, Color shadowColor) {
WritableImage shadow = new WritableImage(image.getPixelReader(), (int) image.getWidth(), (int) image.getHeight());
PixelReader reader = shadow.getPixelReader();
PixelWriter writer = shadow.getPixelWriter();
for (int ix = 0; ix < image.getWidth(); ix++) {
for (int iy = 0; iy < image.getHeight(); iy++) {
int argb = reader.getArgb(ix, iy);
int a = (argb >> 24) & 0xFF;
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = argb & 0xFF;
// because we use a binary choice, we lose anti-alising info in the shadow so it looks a bit jaggy.
Color fill = (r > 0 || g > 0 || b > 0) ? shadowColor : Color.TRANSPARENT;
writer.setColor(ix, iy, fill);
}
}
return shadow;
}
private Image getImage() {
Label label = new Label("a");
label.setStyle("-fx-text-fill: forestgreen; -fx-background-color: transparent; -fx-font-size: " + FONT_SIZE + "px;");
Scene scene = new Scene(label, Color.TRANSPARENT);
SnapshotParameters snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
return label.snapshot(snapshotParameters, null);
}
public static void main(String[] args) {
launch();
}
}
Inbuilt, JavaFX has a DropShadow effect, which is almost what you want, especially when you set the spread to 1 and the radius to 0, however, it just generates a single offset shadow image rather than a long shadow effect.
With some alternate text and a shorter "long shadow":
Related
I have this code where I have random rectangles generating over the entire canvas. I also have 1 rectangle at the center.
I need to get the 1 rectangle at the center to move around when the user hovers the mouse over the canvas.
Here is my code
package sample;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
private static final int DRAW_WIDTH = 800;
private static final int DRAW_HEIGHT = 500;
private Animation myAnimation;
private Canvas canvas;
private GraphicsContext gtx;
#Override
public void start(Stage stage) throws Exception
{
stage.setTitle("tRIPPy BoXXX");
canvas = new Canvas(DRAW_WIDTH, DRAW_HEIGHT);
gtx = canvas.getGraphicsContext2D();
gtx.setLineWidth(3);
gtx.setFill(Color.BLACK);
gtx.fillRect(0, 0, DRAW_WIDTH, DRAW_HEIGHT);
VBox vBox = new VBox();
vBox.getChildren().addAll(canvas);
Scene scene = new Scene(vBox, DRAW_WIDTH, DRAW_HEIGHT);
stage.setScene(scene);
stage.show();
myAnimation = new Animation();
myAnimation.start();
}
class Animation extends AnimationTimer
{
#Override
public void handle(long now)
{
Random rand = new Random();
double red = rand.nextDouble();
double green = rand.nextDouble();
double blue = rand.nextDouble();
Color boxColor = Color.color(red,green,blue);
gtx.setStroke(boxColor);
....
This is the box that I want to have moving around with the users mouse. I have tried some things on my own, however I can't get the rect to stay the way it is in the code.
.....
int rectX = 800 / 2;
int rectY = 500 / 2;
for (int side = 10; side <= 100; side += 10) {
gtx.strokeRect(rectX - side / 2, rectY - side / 2, side, side);
}
int centerX = (int)(Math.random()*800);
int centerY = (int)(Math.random()*800);
for (int side = (int)(Math.random()*100); side <= Math.random()* 100; side += Math.random()*100)
{
gtx.strokeRect(centerX - side / 2, centerY - side / 2, side, side);
}
}
}
public static void main(String[] args)
{
launch(args);
}
If you plan to move some graphics arround, why do you then start with a canvas? On a canvas you cannot move anything arround unless you constantly want to redraw everything again and again. Putting your rectangles into the scene graph is much better suited for this task.
I have been searching for quite a bit on a way to make static visual representations of Java, but im not certain that what i found to be the most correct way to accomplish what im looking for.
I looking for a way to simply produce an image to represent the state of a board in Java. For example, lets say i have an implementation of Checkers in Java, what im looking for is a way to, after each individual move, represent the game board.
I have looked into JavaFX and Swing, but judging from what i found out they are more suited for actual dynamic GUI's, not image processing( tho i can be wrong).
What options are there really? Or should i use any external apps/software for this?
You could try something like this (a JavaFX solution) which will render images of a checkerboard to png files. Not sure if that is what you are looking for as your question was a bit unclear to me, but perhaps it is helpful for you.
import javafx.application.*;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.*;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
public class Checkout extends Application {
private static final double SQUARE_SIZE = 20;
private static final double PIECE_RADIUS = SQUARE_SIZE / 2.5;
private static final Color WHITE = Color.BEIGE;
private static final Color BLACK = Color.ROSYBROWN;
// setupindex, player, position (1-32 index into top to bottom black squares on the board).
private static final int[][][] setups =
{
{
{31, 27, 19},
{17, 12, 5}
},
{
{30, 26, 18},
{19, 11, 10},
},
{
{31, 24, 19},
{18, 11, 10}
}
};
private static final String SNAPSHOT_FILE_PREFIX = "snapshot-";
private static final String SNAPSHOT_FILE_SUFFIX = ".png";
private WritableImage snapshotFxImage =
new WritableImage((int) (SQUARE_SIZE * 8), (int) (SQUARE_SIZE * 8));
private BufferedImage snapshotBufferedImage =
new BufferedImage((int) (SQUARE_SIZE * 8), (int) (SQUARE_SIZE * 8), BufferedImage.TYPE_INT_ARGB);
private static final String USER_HOME =
System.getProperties().getProperty("user.home");
#Override
public void start(Stage stage) throws IOException {
Group layout = new Group();
renderBoard(layout);
Scene scene = new Scene(layout);
int i = 0;
for (int[][] setup : setups) {
List<Node> pieces = renderSetup(setup);
layout.getChildren().addAll(pieces);
snapshot(scene, i);
layout.getChildren().removeAll(pieces);
i++;
}
Platform.setImplicitExit(false);
Platform.exit();
}
private void renderBoard(Group layout) {
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
Rectangle square = new Rectangle(
c * SQUARE_SIZE,
r * SQUARE_SIZE,
SQUARE_SIZE,
SQUARE_SIZE
);
Color fill =
(((c + r) % 2) == 0)
? Color.WHITE
: Color.BLACK;
square.setFill(fill);
layout.getChildren().add(square);
}
}
}
private List<Node> renderSetup(int[][] setup) {
List<Node> pieces = new ArrayList<>();
for (int player = 0; player < 2; player++) {
for (int pieceIndex = 0; pieceIndex < setup[player].length; pieceIndex++) {
Color fill =
player == 0
? WHITE
: BLACK;
Circle piece = new Circle(PIECE_RADIUS, fill);
pieces.add(piece);
movePieceTo(piece, setup[player][pieceIndex]);
}
}
return pieces;
}
private void movePieceTo(Circle circle, int p) {
int r = (p - 1) / 4;
int c = ((p - 1) % 4) * 2 + (((r % 2) == 0) ? 1 : 0);
circle.setCenterX(c * SQUARE_SIZE + SQUARE_SIZE / 2);
circle.setCenterY(r * SQUARE_SIZE + SQUARE_SIZE / 2);
}
private void snapshot(Scene scene, int snapshotIdx) throws IOException {
Path path = Paths.get(
USER_HOME,
SNAPSHOT_FILE_PREFIX + snapshotIdx + SNAPSHOT_FILE_SUFFIX
);
scene.snapshot(snapshotFxImage);
SwingFXUtils.fromFXImage(snapshotFxImage, snapshotBufferedImage);
ImageIO.write(snapshotBufferedImage, "png", path.toFile());
System.out.println("Saved: " + path.toString());
}
public static void main(String[] args) {
launch(args);
}
}
You can use the Graphics class to draw to an in memory image. Then you can save that to a file or display it some way. Heres an example of how you might get started doing that:
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class SO45990731 {
public static void main(String [] args) throws IOException {
BufferedImage image = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
// TODO draw your checker board
g.drawOval(100, 100, 300, 300);
// cleanup and write
g.dispose();
ImageIO.write(image, "png", new File("board.png"));
}
}
I'm currently developing a game in Java, and I've been trying to figure out how to draw a shape (e.g. a circle) to the canvas, on top of a different shape (e.g. a square), but to only draw the parts of the circle which are intersecting the square, similar to a clipping mask between layers in Photoshop.
I've tried using GraphicsContext.clearRect() to clear the areas where the bottom shape is not, but that removes the background.
The code below produces this result:
However, this is the result I desire:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class CircleWithinSquareTest extends Application {
#Override
public void start(Stage stage) throws Exception {
int width = 200;
int height = 200;
Canvas canvas = new Canvas(width, height);
GraphicsContext gc = canvas.getGraphicsContext2D();
AnimationTimer timer = new AnimationTimer() {
final int bgCellSize = 8;
final int x = 100;
final int y = 100;
double angle = 0;
#Override
public void handle(long now) {
/* Draw checkered background */
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, width, height);
gc.setFill(Color.LIGHTGRAY);
boolean odd = false;
for (int y = 0; y < height; y += bgCellSize) {
odd = !odd;
for (int x = odd ? 0 : bgCellSize; x < width; x += bgCellSize * 2) {
gc.fillRect(x, y, bgCellSize, bgCellSize);
}
}
/* Draw square */
gc.setFill(Color.BLUE);
gc.fillRect(x, y, 50, 50);
/* Draw circle */
gc.save();
angle += 5;
if (angle >= 360) {
angle = 0;
}
Rotate r = new Rotate(angle, x, y);
gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy());
gc.setFill(Color.RED);
gc.fillOval(x, y, 30, 30);
gc.restore();
}
};
timer.start();
Group root = new Group(canvas);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
}
You can use clipping, add next code before setTransform:
gc.beginPath();
gc.rect(x, y, 50, 50);
gc.closePath();
gc.clip();
I want to get the current position (x,y) of a Circle (javafx.scene.shape.Circle) i am moving via a PathTransition, while the transition is running/happening.
So i need some kind of task, that checks the position of the circle every 50 milliseconds (for example).
I also tried this solution Current circle position of javafx transition which was suggested on Stack Overflow, but i didn't seem to work for me.
Circle projectile = new Circle(Playground.PROJECTILE_SIZE, Playground.PROJECTILE_COLOR);
root.getChildren().add(projectile);
double duration = distance / Playground.PROJECTILE_SPEED;
double xOff = (0.5-Math.random())*Playground.WEAPON_OFFSET;
double yOff = (0.5-Math.random())*Playground.WEAPON_OFFSET;
Line shotLine = new Line(player.getCurrentX(), player.getCurrentY(), aimLine.getEndX() + xOff, aimLine.getEndY() + yOff);
shotLine.setEndX(shotLine.getEndX() + (Math.random()*Playground.WEAPON_OFFSET));
PathTransition pt = new PathTransition(Duration.seconds(duration), shotLine, projectile);
// Linear movement for linear speed
pt.setInterpolator(Interpolator.LINEAR);
pt.setOnFinished(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
// Remove bullet after hit/expiration
projectile.setVisible(false);
root.getChildren().remove(projectile);
}
});
projectile.translateXProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
double x = collider.getTranslateX() - projectile.getTranslateX();
double y = collider.getTranslateY() - projectile.getTranslateY();
double distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
System.out.println("Distance: "+ distance);
if (distance < 50) {
System.out.println("hit");
}
}
});
pt.play();
A PathTransition will move a node by manipulating its translateX and translateY properties. (A TranslateTransition works the same way.)
It's hard to answer your question definitively as your code is so incomplete, but if the projectile and collider have the same parent in the scene graph, converting the initial coordinates of the projectile and collider by calling localToParent will give the coordinates in the parent, including the translation. So you can observe the translateX and translateY properties and use that conversion to check for a collision. If they have different parents, you can do the same with localToScene instead and just convert both to coordinates relative to the scene.
Here's a quick SSCCE. Use the left and right arrows to aim, space to shoot:
import javafx.animation.Animation;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ShootingGame extends Application {
#Override
public void start(Stage primaryStage) {
final double width = 400 ;
final double height = 400 ;
final double targetRadius = 25 ;
final double projectileRadius = 5 ;
final double weaponLength = 25 ;
final double weaponX = width / 2 ;
final double weaponStartY = height ;
final double weaponEndY = height - weaponLength ;
final double targetStartX = targetRadius ;
final double targetY = targetRadius * 2 ;;
Pane root = new Pane();
Circle target = new Circle(targetStartX, targetY, targetRadius, Color.BLUE);
TranslateTransition targetMotion = new TranslateTransition(Duration.seconds(2), target);
targetMotion.setByX(350);
targetMotion.setAutoReverse(true);
targetMotion.setCycleCount(Animation.INDEFINITE);
targetMotion.play();
Line weapon = new Line(weaponX, weaponStartY, weaponX, weaponEndY);
weapon.setStrokeWidth(5);
Rotate weaponRotation = new Rotate(0, weaponX, weaponStartY);
weapon.getTransforms().add(weaponRotation);
Scene scene = new Scene(root, width, height);
scene.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.LEFT) {
weaponRotation.setAngle(Math.max(-45, weaponRotation.getAngle() - 2));
}
if (e.getCode() == KeyCode.RIGHT) {
weaponRotation.setAngle(Math.min(45, weaponRotation.getAngle() + 2));
}
if (e.getCode() == KeyCode.SPACE) {
Point2D weaponEnd = weapon.localToParent(weaponX, weaponEndY);
Circle projectile = new Circle(weaponEnd.getX(), weaponEnd.getY(), projectileRadius);
TranslateTransition shot = new TranslateTransition(Duration.seconds(1), projectile);
shot.setByX(Math.tan(Math.toRadians(weaponRotation.getAngle())) * height);
shot.setByY(-height);
shot.setOnFinished(event -> root.getChildren().remove(projectile));
BooleanBinding hit = Bindings.createBooleanBinding(() -> {
Point2D targetLocation = target.localToParent(targetStartX, targetY);
Point2D projectileLocation = projectile.localToParent(weaponEnd);
return (targetLocation.distance(projectileLocation) < targetRadius + projectileRadius) ;
}, projectile.translateXProperty(), projectile.translateYProperty());
hit.addListener((obs, wasHit, isNowHit) -> {
if (isNowHit) {
System.out.println("Hit");
root.getChildren().remove(projectile);
root.getChildren().remove(target);
targetMotion.stop();
shot.stop();
}
});
root.getChildren().add(projectile);
shot.play();
}
});
root.getChildren().addAll(target, weapon);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I recently wanted to create an animated background in JavaFX, similar to the Swing example seen here. I used a Canvas on which to draw, as shown in Working with the Canvas API, and an AnimationTimer for the drawing loop, as shown in Animation Basics. Unfortunately, I'm not sure how to resize the Canvas automatically as the enclosing Stage is resized. What is a good approach?
A similar question is examined in How to make canvas Resizable in javaFX?, but the accepted answer there lacks the binding illustrated in the accepted answer here.
In the example below, the static nested class CanvasPane wraps an instance of Canvas in a Pane and overrides layoutChildren() to make the canvas dimensions match the enclosing Pane. Note that Canvas returns false from isResizable(), so "the parent cannot resize it during layout," and Pane "does not perform layout beyond resizing resizable children to their preferred sizes." The width and height used to construct the canvas become its initial size. A similar approach is used in the Ensemble particle simulation, FireworksApp, to scale a background image while retaining its aspect ratio.
As an aside, note the difference from using fully saturated colors compared to the original. These related examples illustrate placing controls atop the animated background.
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/31761362/230513
* #see https://stackoverflow.com/a/8616169/230513
*/
public class Baubles extends Application {
private static final int MAX = 64;
private static final double WIDTH = 640;
private static final double HEIGHT = 480;
private static final Random RND = new Random();
private final Queue<Bauble> queue = new LinkedList<>();
private Canvas canvas;
#Override
public void start(Stage stage) {
CanvasPane canvasPane = new CanvasPane(WIDTH, HEIGHT);
canvas = canvasPane.getCanvas();
BorderPane root = new BorderPane(canvasPane);
CheckBox cb = new CheckBox("Animate");
cb.setSelected(true);
root.setBottom(cb);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
for (int i = 0; i < MAX; i++) {
queue.add(randomBauble());
}
AnimationTimer loop = new AnimationTimer() {
#Override
public void handle(long now) {
GraphicsContext g = canvas.getGraphicsContext2D();
g.setFill(Color.BLACK);
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
for (Bauble b : queue) {
g.setFill(b.c);
g.fillOval(b.x, b.y, b.d, b.d);
}
queue.add(randomBauble());
queue.remove();
}
};
loop.start();
cb.selectedProperty().addListener((Observable o) -> {
if (cb.isSelected()) {
loop.start();
} else {
loop.stop();
}
});
}
private static class Bauble {
private final double x, y, d;
private final Color c;
public Bauble(double x, double y, double r, Color c) {
this.x = x - r;
this.y = y - r;
this.d = 2 * r;
this.c = c;
}
}
private Bauble randomBauble() {
double x = RND.nextDouble() * canvas.getWidth();
double y = RND.nextDouble() * canvas.getHeight();
double r = RND.nextDouble() * MAX + MAX / 2;
Color c = Color.hsb(RND.nextDouble() * 360, 1, 1, 0.75);
return new Bauble(x, y, r, c);
}
private static class CanvasPane extends Pane {
private final Canvas canvas;
public CanvasPane(double width, double height) {
canvas = new Canvas(width, height);
getChildren().add(canvas);
}
public Canvas getCanvas() {
return canvas;
}
#Override
protected void layoutChildren() {
super.layoutChildren();
final double x = snappedLeftInset();
final double y = snappedTopInset();
// Java 9 - snapSize is deprecated, use snapSizeX() and snapSizeY() accordingly
final double w = snapSize(getWidth()) - x - snappedRightInset();
final double h = snapSize(getHeight()) - y - snappedBottomInset();
canvas.setLayoutX(x);
canvas.setLayoutY(y);
canvas.setWidth(w);
canvas.setHeight(h);
}
}
public static void main(String[] args) {
launch(args);
}
}
I combined both prior solutions ( #trashgod and #clataq's ) by putting the canvas in a Pane and binding it to it:
private static class CanvasPane extends Pane {
final Canvas canvas;
CanvasPane(double width, double height) {
setWidth(width);
setHeight(height);
canvas = new Canvas(width, height);
getChildren().add(canvas);
canvas.widthProperty().bind(this.widthProperty());
canvas.heightProperty().bind(this.heightProperty());
}
}
Couldn't you do this with a Binding as well? The following seems to produce the same results without having to add the derived class.
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* #see http://stackoverflow.com/a/31761362/230513
* #see http://stackoverflow.com/a/8616169/230513
*/
public class Baubles extends Application {
private static final int MAX = 64;
private static final double WIDTH = 640;
private static final double HEIGHT = 480;
private static final Random RND = new Random();
private final Queue<Bauble> queue = new LinkedList<>();
private Canvas canvas;
#Override
public void start(Stage stage) {
canvas = new Canvas(WIDTH, HEIGHT);
BorderPane root = new BorderPane(canvas);
CheckBox cb = new CheckBox("Animate");
cb.setSelected(true);
root.setBottom(cb);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// Create bindings for resizing.
DoubleBinding heightBinding = root.heightProperty()
.subtract(root.bottomProperty().getValue().getBoundsInParent().getHeight());
canvas.widthProperty().bind(root.widthProperty());
canvas.heightProperty().bind(heightBinding);
for (int i = 0; i < MAX; i++) {
queue.add(randomBauble());
}
AnimationTimer loop = new AnimationTimer() {
#Override
public void handle(long now) {
GraphicsContext g = canvas.getGraphicsContext2D();
g.setFill(Color.BLACK);
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
for (Bauble b : queue) {
g.setFill(b.c);
g.fillOval(b.x, b.y, b.d, b.d);
}
queue.add(randomBauble());
queue.remove();
}
};
loop.start();
cb.selectedProperty().addListener((Observable o) -> {
if (cb.isSelected()) {
loop.start();
} else {
loop.stop();
}
});
}
private static class Bauble {
private final double x, y, d;
private final Color c;
public Bauble(double x, double y, double r, Color c) {
this.x = x - r;
this.y = y - r;
this.d = 2 * r;
this.c = c;
}
}
private Bauble randomBauble() {
double x = RND.nextDouble() * canvas.getWidth();
double y = RND.nextDouble() * canvas.getHeight();
double r = RND.nextDouble() * MAX + MAX / 2;
Color c = Color.hsb(RND.nextDouble() * 360, 1, 1, 0.75);
return new Bauble(x, y, r, c);
}
public static void main(String[] args) {
launch(args);
}
}