How to animate a spinning top? - java

It's Hanukkah and I'm trying to animate a spinning top (dreidel):
I can get it to spin on its own axis. Here is my code:
import static javafx.scene.paint.Color.*;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Point3D;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DreidelAnim extends Application {
private double bodyBase = 30;
private double bodyHeight = bodyBase * 3 / 2;
private double baseRadius = bodyBase / 2;
#Override
public void start(Stage stage) throws Exception {
DoubleProperty spinAngle = new SimpleDoubleProperty();
Rotate spin = new Rotate(0, Rotate.Z_AXIS);
spin.angleProperty().bind(spinAngle);
Timeline spinAnim = new Timeline(new KeyFrame(Duration.seconds(2), new KeyValue(spinAngle, 360)));
spinAnim.setCycleCount(Timeline.INDEFINITE);
spinAnim.play();
Group dreidel = createDreidel();
Translate zTrans = new Translate(0, 0, -(bodyHeight/2 + baseRadius));
dreidel.getTransforms().addAll(spin, zTrans);
Scene scene = new Scene(dreidel, 200, 200, true, SceneAntialiasing.BALANCED);
scene.setFill(SKYBLUE);
scene.setCamera(createCamera());
stage.setScene(scene);
stage.show();
}
private Group createDreidel() {
double handleHeight = bodyBase * 3/4;
Cylinder handle = new Cylinder(bodyBase / 6, handleHeight);
handle.setTranslateZ(-(bodyHeight + handleHeight) / 2);
handle.setRotationAxis(Rotate.X_AXIS);
handle.setRotate(90);
handle.setMaterial(new PhongMaterial(RED));
Box body = new Box(bodyBase, bodyBase, bodyHeight);
body.setMaterial(new PhongMaterial(BLUE));
Sphere base = new Sphere(baseRadius);
base.setTranslateZ(bodyHeight / 2);
base.setMaterial(new PhongMaterial(GREEN));
return new Group(handle, body, base);
}
private Camera createCamera() {
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(1000);
int xy = 150;
Translate trans = new Translate(-xy, xy, -120);
Rotate rotXY = new Rotate(70, new Point3D(1, 1, 0));
Rotate rotZ = new Rotate(45, new Point3D(0, 0, 1));
camera.getTransforms().addAll(trans, rotXY, rotZ);
return camera;
}
public static void main(String[] args) {
launch();
}
}
I created a simple model, spinning it around its axis, and translated it so that its tip is on (0, 0, 0). Here is the result:
How can I achieve something similar to the picture on the top where it's also spinning around a rotating axis?

The rotation of the axis around which the object spins is called a Precession. The spinning top motion requires 2 rotations:
Rotation of the object around its internal axis (parallel to the red handle).
Rotation one of the internal axis around a static axis (z in this case).
On the face of it, you'd need 2 Animation instances. However, both rotations are actually the same. The pivot point for both is (0, 0, 0) (after the zTrans) and they are both around the z axis, only one of them is tilted at an angle.
Here is the modified code:
import static javafx.scene.paint.Color.*;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Point3D;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class FinalDreidelSpin extends Application {
private double bodyBase = 30;
private double bodyHeight = bodyBase * 3 / 2;
private double baseRadius = bodyBase / 2;
#Override
public void start(Stage stage) throws Exception {
double tiltAngle = 40;
DoubleProperty spinAngle = new SimpleDoubleProperty();
Rotate spin = new Rotate(0, Rotate.Z_AXIS);
Rotate tilt = new Rotate(tiltAngle, Rotate.X_AXIS);
spin.angleProperty().bind(spinAngle);
Timeline spinAnim = new Timeline();
spinAnim.getKeyFrames().add(new KeyFrame(Duration.seconds(2), new KeyValue(spinAngle, 360)));
spinAnim.setCycleCount(Timeline.INDEFINITE);
spinAnim.play();
Group dreidel = createDreidel();
Translate zTrans = new Translate(0, 0, -(bodyHeight/2 + baseRadius));
dreidel.getTransforms().addAll(spin, tilt, spin, zTrans);
Scene scene = new Scene(new Group(dreidel, createAxes()), 200, 200, true, SceneAntialiasing.BALANCED);
scene.setFill(SKYBLUE);
scene.setCamera(createCamera());
stage.setScene(scene);
stage.show();
}
private Group createDreidel() {
double handleHeight = bodyBase * 3/4;
Cylinder handle = new Cylinder(bodyBase / 6, handleHeight);
handle.setTranslateZ(-(bodyHeight + handleHeight) / 2);
handle.setRotationAxis(Rotate.X_AXIS);
handle.setRotate(90);
handle.setMaterial(new PhongMaterial(RED));
Box body = new Box(bodyBase, bodyBase, bodyHeight);
body.setMaterial(new PhongMaterial(BLUE));
Sphere base = new Sphere(baseRadius);
base.setTranslateZ(bodyHeight / 2);
base.setMaterial(new PhongMaterial(GREEN));
return new Group(handle, body, base);
}
private Camera createCamera() {
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(1000);
int xy = 150;
Translate trans = new Translate(-xy, xy, -100);
Rotate rotXY = new Rotate(70, new Point3D(1, 1, 0));
Rotate rotZ = new Rotate(45, new Point3D(0, 0, 1));
camera.getTransforms().addAll(trans, rotXY, rotZ);
return camera;
}
private Group createAxes() {
int axisWidth = 1;
int axisLength = 400;
Cylinder xAxis = new Cylinder(axisWidth, axisLength);
xAxis.setMaterial(new PhongMaterial(CYAN));
Cylinder yAxis = new Cylinder(axisWidth, axisLength);
yAxis.setRotationAxis(Rotate.Z_AXIS);
yAxis.setRotate(90);
yAxis.setMaterial(new PhongMaterial(MAGENTA));
Cylinder zAxis = new Cylinder(axisWidth, axisLength);
zAxis.setRotationAxis(Rotate.X_AXIS);
zAxis.setRotate(90);
zAxis.setMaterial(new PhongMaterial(YELLOW));
return new Group(xAxis, yAxis, zAxis);
}
public static void main(String[] args) {
launch();
}
}
Where I added axes representations for viewing convenience. Note that the getTransforms() list does not require its objects to be unique (unlike getChildren()), which allows us to reuse the same animation. The order of the animations is also important as noted below.
The tilting is a simple rotation around the x or y axis.
If we tilt and then spin, getTransforms().addAll(tilt, spin, zTrans), we would get the internal rotation (listed 1 above), only tilted:
If we spin and then tilt, getTransforms().addAll(spin, tilt, zTrans), we would get the precession (listed 2 above):
Combining the 2 as in the complete code would give the desired result:

This is another possible answer, very much based in #user1803551 approach, but using a 3D mesh that can use a texture image, and a different precession period.
This is how it looks like:
In order to apply a texture, I'll use the net concept for the body of the dreidel, and this image:
that is based on this image.
Finally I'll add a regular cylinder for the handle.
I won't go into details on how to create the TriangleMesh for the body, but we define 9 vertices (3D coordinates), 16 texture coordinates (2D), and 14 triangle faces including the vertex indices and the texture indices. The cube is defined by its side width, and the pyramid by its height. The net dimensions are L = 4 * width, H = 2 * width + height.
For instance, face 0 has vertices 0 - 2 - 1, and texture indices 8 - 3 - 7, where vertex 0 has coordinates {width / 2, width / 2, width / 2}, and texture index 8 has coordinates {width, 2 * width}, which are normalized between [0, 1]: {width / L, 2 * width / H}.
In this case, and for the sake of the sample, the values are hardcoded:
float width = 375f;
float height = 351f;
This is the 3D shape class:
class DreidelMesh extends Group {
float width = 375f;
float height = 351f;
public DreidelMesh(){
MeshView bodyMesh = new MeshView(createBodyMesh());
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(new Image(getClass().getResourceAsStream("3dreidel3d.png")));
bodyMesh.setMaterial(material);
Cylinder handle = new Cylinder(45, 260);
handle.setTranslateY(-(handle.getHeight() + width) / 2);
material = new PhongMaterial(Color.web("#daaf6d"));
handle.setMaterial(material);
getTransforms().add(new Rotate(90, Rotate.X_AXIS));
getChildren().addAll(bodyMesh, handle);
}
private TriangleMesh createBodyMesh() {
TriangleMesh m = new TriangleMesh();
float L = 4f * width;
float H = 2f * width + height;
float w2 = width / 2f;
// POINTS
m.getPoints().addAll(
w2, w2, w2,
w2, w2, -w2,
w2, -w2, w2,
w2, -w2, -w2,
-w2, w2, w2,
-w2, w2, -w2,
-w2, -w2, w2,
-w2, -w2, -w2,
0f, w2 + height, 0f
);
// TEXTURES
m.getTexCoords().addAll(
width / L, 0f,
2f * width/ L, 0f,
0f, width / H,
width / L, width / H,
2f * width/ L, width / H,
3f * width/ L, width / H,
1f, width / H,
0f, 2f * width / H,
width / L, 2f * width / H,
2f * width/ L, 2f * width / H,
3f * width/ L, 2f * width / H,
1f, 2f * width / H,
width / 2f / L, 1f,
3f * width / 2f / L, 1f,
5f * width / 2f / L, 1f,
7f * width / 2f / L, 1f
);
// FACES
m.getFaces().addAll(
0, 8, 2, 3, 1, 7,
2, 3, 3, 2, 1, 7,
4, 9, 5, 10, 6, 4,
6, 4, 5, 10, 7, 5,
0, 8, 1, 7, 8, 12,
4, 9, 0, 8, 8, 13,
5, 10, 4, 9, 8, 14,
1, 11, 5, 10, 8, 15,
2, 3, 6, 4, 3, 0,
3, 0, 6, 4, 7, 1,
0, 8, 4, 9, 2, 3,
2, 3, 4, 9, 6, 4,
1, 11, 3, 6, 5, 10,
5, 10, 3, 6, 7, 5
);
return m;
}
}
Finally, this shape is added to the scene, and I'll provide two animations (instead of one), one for the spin, and one slower for the precession:
#Override
public void start(Stage stage) {
double tiltAngle = 15;
DoubleProperty spinAngle = new SimpleDoubleProperty();
DoubleProperty precessionAngle = new SimpleDoubleProperty();
Rotate spin = new Rotate(0, Rotate.Z_AXIS);
Rotate precession = new Rotate(0, Rotate.Z_AXIS);
Rotate tilt = new Rotate(tiltAngle, Rotate.X_AXIS);
spin.angleProperty().bind(spinAngle);
precession.angleProperty().bind(precessionAngle);
Timeline spinAnim = new Timeline();
spinAnim.getKeyFrames().add(new KeyFrame(Duration.seconds(1.5), new KeyValue(spinAngle, 360)));
spinAnim.setCycleCount(Timeline.INDEFINITE);
spinAnim.play();
Timeline precessionAnim = new Timeline();
precessionAnim.getKeyFrames().add(new KeyFrame(Duration.seconds(4), new KeyValue(precessionAngle, 360)));
precessionAnim.setCycleCount(Timeline.INDEFINITE);
precessionAnim.play();
Group dreidel = new Group(new DreidelMesh());
Translate zTrans = new Translate(0, 0, - dreidel.getBoundsInLocal().getMaxZ());
dreidel.getTransforms().addAll(precession, tilt, spin, zTrans);
Scene scene = new Scene(new Group(dreidel), 300, 300, true, SceneAntialiasing.BALANCED);
scene.setFill(SKYBLUE);
scene.setCamera(createCamera());
stage.setScene(scene);
stage.setTitle("JavaFX 3D - Dreidel");
stage.show();
}
Running the application will show the animation displayed above.

Related

Rotate a group of rectangles around their common center

I have a few rectangles that I assign rotation one by one ((javafx.scene.shape.Rectangle) shape).rotateProperty().bind(rotate);
For example, 45 degrees
My rectangle rotates around its center. Please tell me how to make the enemy a few right-angles around their common center.
To get something like this
this.rotate.addListener((obs, old, fresh) -> {
for (VObject vObject : children ) {
vObject.rotate.set(this.rotate.get());
}
});
This is how I add rotation. How can I specify the angle
Update: I used the advice below and now I set the rotation individually for each rectangle. (The selection is still a bit wrong)
this.rotate.addListener((obs, old, fresh) -> {
Rotate groupRotate = new Rotate(rotate.get(),
this.x.getValue().doubleValue() + this.width.getValue().doubleValue() / 2 ,
this.y.getValue().doubleValue() + this.height.getValue().doubleValue() / 2);
for (VObject vObject : children ) {
vObject.getShape().getTransforms().clear();
vObject.getShape().getTransforms().add(groupRotate);
}
});
But now the axis also rotates depending on the rotation.
Can I set the rotation to the rectangles without turning the coordinate axis?
If you put all rectangles into a common Group, you can rotate them at once:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class RotateAllApplication extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
// the common group
Group group = new Group();
group.getChildren().addAll(new Rectangle(10, 10, 80, 40), //
new Rectangle(110, 10, 80, 40), //
new Rectangle(10, 110, 80, 40), //
new Rectangle(110, 110, 80, 40));
// rotate the group instead of each rectangle
group.setRotate(45.0);
root.setCenter(group);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
Update: If you don't want to create a parent Group object, you can apply the same rotation transformation to each child instead. While Node#setRotate(double) always rotates around the center of the node, adding a transformation to Node#getTransforms() is more general and not restricted to simple rotations.
The following statement will apply a rotation around the point (100.0/100.0) of the parent coordinate system to all children in the list:
childrenList.forEach(child -> child.getTransforms().add(Transform.rotate(45.0, 100.0, 100.0)));
Use a Rotate transform and specify the appropriate pivot point:
#Override
public void start(Stage primaryStage) throws IOException {
Pane pane = new Pane();
pane.setPrefSize(600, 600);
Rectangle[] rects = new Rectangle[4];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
Rectangle rect = new Rectangle(100 + i * 200, 100 + j * 100, 150, 50);
rects[i * 2 + j] = rect;
pane.getChildren().add(rect);
}
}
Slider slider = new Slider(0, 360, 0);
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
Rotate rotate = new Rotate();
// find pivot point
for (Rectangle rect : rects) {
double val = rect.getX();
if (minX > val) {
minX = val;
}
val += rect.getWidth();
if (maxX < val) {
maxX = val;
}
val = rect.getY();
if (minY > val) {
minY = val;
}
val += rect.getHeight();
if (maxY < val) {
maxY = val;
}
rect.getTransforms().add(rotate);
}
rotate.setPivotX(0.5 * (maxX + minX));
rotate.setPivotY(0.5 * (maxY + minY));
rotate.angleProperty().bind(slider.valueProperty());
Scene scene = new Scene(new VBox(10, pane, slider));
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.show();
}
If you're planing to apply multiple transformations, you may need to adjust the code for finding the pivot point to use transforms for calculating the bounds...

TriangleMesh - Backside faces are visible

Good Day! I have the following issue. The graphic model is not displayed correctly: some backside faces of the model that should be hidden by the frontside remain visible. Here are some exmples to clarify: (isometry)
(issue)
This issue comes out especially notable when applying light and material. So the the question is how this can be solved for JavaFX?
UPD:
public class VertexTest extends Application {
PerspectiveCamera camera;
Cam cam = new Cam();
double mouseOldX, mouseOldY, mousePosX, mousePosY, mouseDeltaX, mouseDeltaY;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
TriangleMesh mesh = new Shape3DRectangle(100, 100, 100);
MeshView view = new MeshView(mesh);
view.setDrawMode(DrawMode.LINE);
view.setMaterial(new PhongMaterial(Color.RED));
cam.getChildren().add(view);
Scene scene = new Scene(cam, 1000, 1000, true);
addEvents(view, scene);
camera = new PerspectiveCamera();
camera.setTranslateX(-500);
camera.setTranslateY(-500);
camera.setTranslateZ(1000);
scene.setCamera(camera);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addEvents(MeshView view, Scene s) {
s.setOnMouseDragged(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getX();
mousePosY = me.getY();
mouseDeltaX = mousePosX - mouseOldX;
mouseDeltaY = mousePosY - mouseOldY;
cam.ry.setAngle(cam.ry.getAngle() - mouseDeltaX);
cam.rx.setAngle(cam.rx.getAngle() + mouseDeltaY);
}
});
}
class Cam extends Group {
Translate t = new Translate();
Translate p = new Translate();
Translate ip = new Translate();
Rotate rx = new Rotate();
{
rx.setAxis(Rotate.X_AXIS);
}
Rotate ry = new Rotate();
{
ry.setAxis(Rotate.Y_AXIS);
}
Rotate rz = new Rotate();
{
rz.setAxis(Rotate.Z_AXIS);
}
Scale s = new Scale();
public Cam() {
super();
getTransforms().addAll(t, p, rx, rz, ry, s, ip);
}
}
public class Shape3DRectangle extends TriangleMesh {
public Shape3DRectangle(float Width, float Height, float deep) {
this.getPoints().setAll(-Width / 2, Height / 2, deep / 2, // idx p0
Width / 2, Height / 2, deep / 2, // idx p1
-Width / 2, -Height / 2, deep / 2, // idx p2
Width / 2, -Height / 2, deep / 2, // idx p3
-Width / 2, Height / 2, -deep / 2, // idx p4
Width / 2, Height / 2, -deep / 2, // idx p5
-Width / 2, -Height / 2, -deep / 2, // idx p6
Width, -Height / 2, -deep / 2 // idx p7
);
this.getTexCoords().addAll(0.0f, 0.0f);
this.getFaces().addAll(5, 0, 4, 0, 0, 0 // P5,T1 ,P4,T0 ,P0,T3
, 5, 0, 0, 0, 1, 0 // P5,T1 ,P0,T3 ,P1,T4
, 0, 0, 4, 0, 6, 0 // P0,T3 ,P4,T2 ,P6,T7
, 0, 0, 6, 0, 2, 0 // P0,T3 ,P6,T7 ,P2,T8
, 1, 0, 0, 0, 2, 0 // P1,T4 ,P0,T3 ,P2,T8
, 1, 0, 2, 0, 3, 0 // P1,T4 ,P2,T8 ,P3,T9
, 5, 0, 1, 0, 3, 0 // P5,T5 ,P1,T4 ,P3,T9
, 5, 0, 3, 0, 7, 0 // P5,T5 ,P3,T9 ,P7,T10
, 4, 0, 5, 0, 7, 0 // P4,T6 ,P5,T5 ,P7,T10
, 4, 0, 7, 0, 6, 0 // P4,T6 ,P7,T10 ,P6,T11
, 3, 0, 2, 0, 6, 0 // P3,T9 ,P2,T8 ,P6,T12
, 3, 0, 6, 0, 7, 0 // P3,T9 ,P6,T12 ,P7,T13
);
}
}
}
I've been playing around with your sample, and I think I've found out the reason of your issues.
First, I checked the winding of the faces. All of them are counter-clockwise, so all their normals go outwards, as they should be.
Then I modified other vertices instead of the last one. In some cases there were no issues, in others, the issue was still there.
Basically, the issue happens when there are "concave" surfaces, meaning that two faces have normals that will cross. And it doesn't happen when all the surfaces are "convex", meaning that their normals point outwards and won't cross.
This is a clear image of both type of meshes taken from here:
Back to your sample, you are defining a concave mesh:
But if instead of modifying vertex #7, we make the #5 larger, we have a convex mesh, with no rendering issues:
Obviously, while this fix the rendering problem, it changes your initial shape.
If you want to keep your initial geometry, the other possible solution is changing the faces, so you don't have any concave areas.
Let's have a look at the faces 5-1-3 and 5-3-7, and let's say we want to move now the vertex #1.
If we keep your triangles, face 5-1-3 and 5-3-7 will define a concave surface to be render (their normals will cross), while if we change those triangles to 5-1-7 and 1-3-7, then the surface will be convex (their normals won't cross):
Back to your initial shape, this change in those two faces will solve the rendering issues.
While the vertices are the same, the geometry is a little bit difference. So it requires some refinement (more elements). Adding those elements should be done keeping in mind this convex concept. The problem is not trivial, though, as you can see here.
Nice analysis by Jose but it looks to me as if the OP has just forgotten to divide the Width by 2 in this line of his code.
Width, -Height / 2, -deep / 2 // idx p7
should be
Width / 2, -Height / 2, -deep / 2 // idx p7
The class is called Shape3DRectangle but with this mistake
the geometry is not rectangular anymore.
You can set the cullFaceProperty for every Shape3D. I guess that is what you need but I am not sure whether I understood your question precisely.
Shape3D#cullFaceProperty

Timeline for parabolic trajectory in JavaFX

I'm sorry, but I continue not understanding. My problem is I know nothing about physics but my teacher assigned to me this project.
private void shoot() {
Group group = new Group();
double angle = cannon.getRotate();
double speed = slider.getValue();
double x = cannon.getLayoutX();
double y = cannon.getLayoutY();
double v0X = Math.cos(angle)*speed;
double voY = Math.sin(angle)*speed;
Circle c = new Circle(x, y, 8, Color.BLACK);
/*t is the time, but I don't know its
value or has it the same value of the KeyFrame duration? */
double x2 = x + voX*t;
double y2 = y + v0Y * t - 0.5 * gravity * t * t;
Line l = new Line(x, y, x2, y2);
l.setStroke(Color.BLACK);
group.getChildren().addAll(c, l);
final Timeline timeline = new Timeline();
KeyValue xKV = new KeyValue(c.centerXProperty(), x2);
KeyValue yKV = new KeyValue(c.centerYProperty(), y2 , new Interpolator() {
#Override
//Maybe I need I splite, not a curve (?)
protected double curve(double t) {
//thisshould be trajectory's formula
return Math.tan(angle) * x*-(gravity/(2*speed*Math.cos(angle)))*x*x;
}
});
KeyFrame xKF = new KeyFrame(Duration.millis(2000), xKV);
KeyFrame yKF = new KeyFrame(Duration.millis(2000), yKV);
timeline.getKeyFrames().addAll(xKF, yKF);
timeline.play();
}
I'm at a standstill. Please, help meeee
In a KeyValue, the first parameter should be a WritableValue, e.g. circle.centerXProperty(), which represents the initial coordinate, say x. The second parameter should be a type compatible value, in this case the x coordinate toward which the projectile should move. As the timeline plays, the WritableValue will be updated accordingly. Add a second KeyValue to drive the y coordinate.
In the first example seen here, three instances of KeyValue move a figure from it's initial position to its destination position, which is size units away along each coordinate axis. In this related example, a figure moves form point p1 to p2.
In the example below, a Circle moves parallel to the x axis between 100 and 500. At the same time, that same Circle moves parallel to the y axis between 300 and 100 following the curve() defined by the parabola y = –4(x – ½)2 + 1, which has vertex (½, 1) and x intercepts at 0 and 1. This implementation of curve() models a parabolic path on a unit square, as required by the curve() API. You can change the angle of elevation by changing the ratio of height to width in the keys frames, e.g.
KeyValue xKV = new KeyValue(c.centerXProperty(), 200);
KeyValue yKV = new KeyValue(c.centerYProperty(), 0, new Interpolator() {…});
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
* #see https://stackoverflow.com/a/38031826/230513
*/
public class Test extends Application {
#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();
Circle c = new Circle(100, 300, 16, Color.AQUA);
Line l = new Line(100, 300, 500, 300);
l.setStroke(Color.AQUA);
group.getChildren().addAll(c, l);
final Timeline timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setAutoReverse(false);
KeyValue xKV = new KeyValue(c.centerXProperty(), 500);
KeyValue yKV = new KeyValue(c.centerYProperty(), 100, new Interpolator() {
#Override
protected double curve(double t) {
return -4 * (t - .5) * (t - .5) + 1;
}
});
KeyFrame xKF = new KeyFrame(Duration.millis(2000), xKV);
KeyFrame yKF = new KeyFrame(Duration.millis(2000), yKV);
timeline.getKeyFrames().addAll(xKF, yKF);
timeline.play();
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX Line segment width

I have some code which responds to key presses, and draws points and line segments correspondingly. However, the width of the line segments seems to alternate, even though I don't touch the line stroke width in the code. I'm wondering why this is happening.
Here is the code:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.scene.shape.Line;
public class DrawLineSegments extends Application {
static double x = 0.0;
static double y = 0.0;
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
Pane p = new Pane();
Rectangle border = new Rectangle(0, 0, 300, 100);
border.setFill(Color.TRANSPARENT);
Rectangle initialPoint = new Rectangle(border.getWidth() / 2, border.getHeight() / 2, 2, 2);
initialPoint.setFill(Color.BLACK);
x = border.getWidth() / 2;
y = border.getHeight() / 2;
p.getChildren().addAll(border, initialPoint);
p.setOnKeyPressed(e -> {
switch (e.getCode()) {
case UP :
Line upLine = new Line(x + 1, y + 1, x + 1, y + 1 - 7.5);
y = y - 7.5;
upLine.setStroke(Color.BLUEVIOLET);
upLine.setFill(Color.SANDYBROWN);
p.getChildren().add(upLine);
p.getChildren().add(new Rectangle(x, y, 2, 2));
break;
case DOWN :
Line downLine = new Line(x + 1, y + 1, x + 1, y + 1 + 7.5);
y = y + 7.5;
downLine.setStroke(Color.BLUEVIOLET);
downLine.setFill(Color.SANDYBROWN);
p.getChildren().add(downLine);
p.getChildren().add(new Rectangle(x, y, 2, 2));
break;
case LEFT :
Line leftLine = new Line(x - 7.5, y + 1, x + 1, y + 1);
x = x - 7.5;
leftLine.setStroke(Color.BLUEVIOLET);
leftLine.setFill(Color.SANDYBROWN);
p.getChildren().add(leftLine);
p.getChildren().add(new Rectangle(x, y, 2, 2));
break;
case RIGHT :
Line rightLine = new Line(x + 7.5, y + 1, x + 1, y + 1);
x = x + 7.5;
rightLine.setStroke(Color.BLUEVIOLET);
rightLine.setFill(Color.SANDYBROWN);
p.getChildren().add(rightLine);
p.getChildren().add(new Rectangle(x, y, 2, 2));
break;
default:
break;
}
});
Scene scene = new Scene(p);
primaryStage.setScene(scene);
primaryStage.setTitle("Draw Line Segments");
primaryStage.show();
p.requestFocus();
}
}
And an image that perhaps better explains what I'm asking:
You may want to read up on the Coordinate System of a Node
Coordinate System
The Node class defines a traditional computer graphics "local"
coordinate system in which the x axis increases to the right and the y
axis increases downwards. The concrete node classes for shapes provide
variables for defining the geometry and location of the shape within
this local coordinate space. For example, Rectangle provides x, y,
width, height variables while Circle provides centerX, centerY, and
radius.
At the device pixel level, integer coordinates map onto the corners
and cracks between the pixels and the centers of the pixels appear at
the midpoints between integer pixel locations. Because all coordinate
values are specified with floating point numbers, coordinates can
precisely point to these corners (when the floating point values have
exact integer values) or to any location on the pixel. For example, a
coordinate of (0.5, 0.5) would point to the center of the upper left
pixel on the Stage. Similarly, a rectangle at (0, 0) with dimensions
of 10 by 10 would span from the upper left corner of the upper left
pixel on the Stage to the lower right corner of the 10th pixel on the
10th scanline. The pixel center of the last pixel inside that
rectangle would be at the coordinates (9.5, 9.5).
Regarding your problem: Don't use 7.5. Instead use 7 or 8.

Impact of TYPE_INT_ARGB_PRE

I've been having some issues with a ConvolveOp that can be fixed by setting the imageType of the BufferedImage I'm working with to TYPE_INT_ARGB_PRE (see related SO answer here).
Unfortunately I don't fully understand all the implications of selecting this different imageType and I can't seem to find a good reference either, so let me try here:
Which drawing operations are affected by changing the imageType of a BufferedImage from TYPE_INT_ARGB to TYPE_INT_ARGB_PRE? Is it just BufferedImageOps? Or does it affect any of the draw commands on the image's Graphics object or the way the image is rendered if it is drawn onto a different Graphics object?
This basically depends on whether the painting algorithms take into account the information of whether the image is using premultiplied alpha or not.
As already pointed out in the comment: The results will in most cases be the same - at least for the basic drawing operations: Whether you are painting a "non-premultiplied" image into a premultiplied one, or vice versa, will not affect the result, because the differences are handled internally.
A special case are the BufferedImageOps. The JavaDoc comments explicitly say how the alpha channel is treated, and passing in the wrong kind of image can lead to the undesirable results described in the question that you linked to.
It's hard to pin down "the" reason why they decided to implement the BufferedImageOp this way. But a (somewhat vague) statement here is: When operating on (and combining) the pixels of a single source, and these pixels have different alpha values, the treatment of the alpha channel may become fiddly. It's simply not always perfectly obvious what should happen with the alpha channel.
For example, imagine a stack of thee pixels (here, in ARGB, with floating point values):
[1.00, 1.00, 0.00, 0.00] // 100% red, 100% alpha
[0.00, 0.00, 0.00, 0.00] // black, 0% alpha
[0.00, 0.00, 0.00, 0.00] // black, 0% alpha
Now, you want to do a convolution on these pixels (as in the question that you linked to). The kernel could then be
[0.33...]
[0.33...],
[0.33...]
which means that the center pixel of the result should just be the "average" of all pixels (ignoring the borders - roughly as with ConvolveOp#EDGE_ZERO_FILL).
The convolution would then treat all channels equally. For a non-premultiplied image, this would mean that the resulting pixel is a dark red with low opacity:
[0.33, 0.33, 0.00, 0.00]
For the premultiplied image, the components are assumed to be multipled with their alpha value. In this case, the resulting pixel would be fully red, with the same opacity:
[0.33, 1.00, 0.00, 0.00]
Doing the maths behind this is tedious. And in fact, too tedious for me to do it manually - so here is an example:
and the corresponding code
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.Locale;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class PremultipliedAlphaTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(new PremultipliedAlphaTestPanel());
f.setSize(550,500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class PremultipliedAlphaTestPanel extends JPanel
{
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
BufferedImage imageS = createImage(BufferedImage.TYPE_INT_ARGB);
BufferedImage imageP = createImage(BufferedImage.TYPE_INT_ARGB_PRE);
Kernel kernel = new Kernel(1, 3,
new float[]{ 1.0f / 3.0f, 1.0f / 3.0f, 1.0f / 3.0f });
ConvolveOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_ZERO_FILL, null);
BufferedImage resultS = op.filter(imageS, null);
BufferedImage resultP = op.filter(imageP, null);
g.setColor(Color.BLACK);
g.setFont(new Font("Monospaced", Font.PLAIN, 12));
g.drawString("Straight:", 10, 40);
print(g, 2, 1, imageS.getRGB(0, 0));
print(g, 2, 2, imageS.getRGB(0, 1));
print(g, 2, 3, imageS.getRGB(0, 2));
print(g, 7, 2, resultS.getRGB(0, 1));
g.drawString("Premultiplied:", 10, 240);
print(g, 2, 5, imageP.getRGB(0, 0));
print(g, 2, 6, imageP.getRGB(0, 1));
print(g, 2, 7, imageP.getRGB(0, 2));
print(g, 7, 6, resultP.getRGB(0, 1));
g.scale(50, 50);
g.drawImage(imageS, 1, 1, null);
g.drawImage(resultS, 6, 1, null);
g.drawImage(imageP, 1, 5, null);
g.drawImage(resultP, 6, 5, null);
}
private static void print(Graphics2D g, int px, int py, int argb)
{
g.drawString(stringFor(argb), px*50+5, py*50+25);
}
private static String stringFor(int argb)
{
int a = (argb >> 24) & 0xFF;
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = (argb ) & 0xFF;
float fa = a / 255.0f;
float fr = r / 255.0f;
float fg = g / 255.0f;
float fb = b / 255.0f;
return String.format(Locale.ENGLISH,
"%4.2f %4.2f %4.2f %4.2f", fa, fr, fg, fb);
}
private static BufferedImage createImage(int type)
{
BufferedImage b = new BufferedImage(1,3, type);
Graphics2D g = b.createGraphics();
g.setColor(new Color(1.0f,0.0f,0.0f,1.0f));
g.fillRect(0, 0, 1, 1);
g.setColor(new Color(0.0f,0.0f,0.0f,0.0f));
g.fillRect(0, 1, 1, 1);
g.setColor(new Color(0.0f,0.0f,0.0f,0.0f));
g.fillRect(0, 2, 1, 1);
g.dispose();
return b;
}
}

Categories