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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With