Created
May 11, 2012 08:52
-
-
Save jewelsea/2658491 to your computer and use it in GitHub Desktop.
Sample of an animated clock in JavaFX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** clock.css (place input same directory as Clock.java and ensure build script copies the file to the same location as Clock.class) */ | |
.root { | |
-fx-background-color: linear-gradient(to bottom, cornsilk, wheat); | |
-fx-padding: 12; | |
-fx-background-insets: 5; | |
-fx-effect: dropshadow(three-pass-box, wheat, 10, 0, 0, 0); | |
} | |
#face { | |
-fx-fill: radial-gradient(radius 180%, burlywood, derive(burlywood, -30%), derive(burlywood, 30%)); | |
-fx-stroke: derive(burlywood, -45%); | |
-fx-stroke-width: 5; | |
-fx-effect: dropshadow(three-pass-box, grey, 10, 0, 4, 4); | |
} | |
#brand { | |
-fx-font-size: 14px; | |
} | |
#hourHand { | |
-fx-stroke: darkslategray; | |
-fx-stroke-width: 4; | |
-fx-stroke-line-cap: round; | |
} | |
#minuteHand { | |
-fx-stroke: derive(darkslategray, -5%); | |
-fx-stroke-width: 3; | |
-fx-stroke-line-cap: round; | |
} | |
#secondHand { | |
-fx-stroke: derive(firebrick, -15%); | |
-fx-stroke-width: 2; | |
-fx-stroke-line-cap: round; | |
} | |
#spindle { | |
-fx-fill: derive(darkslategray, +5%); | |
} | |
#digitalClock { | |
-fx-font-size: 14px; | |
-fx-font-family: 'Courier New'; | |
} | |
.tick { | |
-fx-stroke: derive(darkgoldenrod, -15%); | |
-fx-stroke-width: 3; | |
-fx-stroke-line-cap: round; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javafx.animation.*; | |
import javafx.application.Application; | |
import javafx.event.*; | |
import javafx.geometry.Pos; | |
import javafx.scene.*; | |
import javafx.scene.control.Label; | |
import javafx.scene.effect.Glow; | |
import javafx.scene.input.MouseEvent; | |
import javafx.scene.layout.VBox; | |
import javafx.scene.paint.Color; | |
import javafx.scene.shape.*; | |
import javafx.scene.transform.Rotate; | |
import javafx.stage.*; | |
import javafx.util.Duration; | |
import java.util.*; | |
/** Note that this clock does not keep perfect time, but is close. | |
It's main purpose is to demonstrate various features of JavaFX. */ | |
public class Clock extends Application { | |
public static void main(String[] args) throws Exception { launch(args); } | |
public void start(final Stage stage) throws Exception { | |
// construct the analogueClock pieces. | |
final Circle face = new Circle(100, 100, 100); | |
face.setId("face"); | |
final Label brand = new Label("Splotch"); | |
brand.setId("brand"); | |
brand.layoutXProperty().bind(face.centerXProperty().subtract(brand.widthProperty().divide(2))); | |
brand.layoutYProperty().bind(face.centerYProperty().add(face.radiusProperty().divide(2))); | |
final Line hourHand = new Line(0, 0, 0, -50); | |
hourHand.setTranslateX(100); hourHand.setTranslateY(100); | |
hourHand.setId("hourHand"); | |
final Line minuteHand = new Line(0, 0, 0, -75); | |
minuteHand.setTranslateX(100); minuteHand.setTranslateY(100); | |
minuteHand.setId("minuteHand"); | |
final Line secondHand = new Line(0, 15, 0, -88); | |
secondHand.setTranslateX(100); secondHand.setTranslateY(100); | |
secondHand.setId("secondHand"); | |
final Circle spindle = new Circle(100, 100, 5); | |
spindle.setId("spindle"); | |
Group ticks = new Group(); | |
for (int i = 0; i < 12; i++) { | |
Line tick = new Line(0, -83, 0, -93); | |
tick.setTranslateX(100); tick.setTranslateY(100); | |
tick.getStyleClass().add("tick"); | |
tick.getTransforms().add(new Rotate(i * (360 / 12))); | |
ticks.getChildren().add(tick); | |
} | |
final Group analogueClock = new Group(face, brand, ticks, spindle, hourHand, minuteHand, secondHand); | |
// construct the digitalClock pieces. | |
final Label digitalClock = new Label(); | |
digitalClock.setId("digitalClock"); | |
// determine the starting time. | |
Calendar calendar = GregorianCalendar.getInstance(); | |
final double seedSecondDegrees = calendar.get(Calendar.SECOND) * (360 / 60); | |
final double seedMinuteDegrees = (calendar.get(Calendar.MINUTE) + seedSecondDegrees / 360.0) * (360 / 60); | |
final double seedHourDegrees = (calendar.get(Calendar.HOUR) + seedMinuteDegrees / 360.0) * (360 / 12) ; | |
// define rotations to map the analogueClock to the current time. | |
final Rotate hourRotate = new Rotate(seedHourDegrees); | |
final Rotate minuteRotate = new Rotate(seedMinuteDegrees); | |
final Rotate secondRotate = new Rotate(seedSecondDegrees); | |
hourHand.getTransforms().add(hourRotate); | |
minuteHand.getTransforms().add(minuteRotate); | |
secondHand.getTransforms().add(secondRotate); | |
// the hour hand rotates twice a day. | |
final Timeline hourTime = new Timeline( | |
new KeyFrame( | |
Duration.hours(12), | |
new KeyValue( | |
hourRotate.angleProperty(), | |
360 + seedHourDegrees, | |
Interpolator.LINEAR | |
) | |
) | |
); | |
// the minute hand rotates once an hour. | |
final Timeline minuteTime = new Timeline( | |
new KeyFrame( | |
Duration.minutes(60), | |
new KeyValue( | |
minuteRotate.angleProperty(), | |
360 + seedMinuteDegrees, | |
Interpolator.LINEAR | |
) | |
) | |
); | |
// move second hand rotates once a minute. | |
final Timeline secondTime = new Timeline( | |
new KeyFrame( | |
Duration.seconds(60), | |
new KeyValue( | |
secondRotate.angleProperty(), | |
360 + seedSecondDegrees, | |
Interpolator.LINEAR | |
) | |
) | |
); | |
// the digital clock updates once a second. | |
final Timeline digitalTime = new Timeline( | |
new KeyFrame(Duration.seconds(0), | |
new EventHandler<ActionEvent>() { | |
@Override public void handle(ActionEvent actionEvent) { | |
Calendar calendar = GregorianCalendar.getInstance(); | |
String hourString = pad(2, '0', calendar.get(Calendar.HOUR) == 0 ? "12" : calendar.get(Calendar.HOUR) + ""); | |
String minuteString = pad(2, '0', calendar.get(Calendar.MINUTE) + ""); | |
String secondString = pad(2, '0', calendar.get(Calendar.SECOND) + ""); | |
String ampmString = calendar.get(Calendar.AM_PM) == Calendar.AM ? "AM" : "PM"; | |
digitalClock.setText(hourString + ":" + minuteString + ":" + secondString + " " + ampmString); | |
} | |
} | |
), | |
new KeyFrame(Duration.seconds(1)) | |
); | |
// time never ends. | |
hourTime.setCycleCount(Animation.INDEFINITE); | |
minuteTime.setCycleCount(Animation.INDEFINITE); | |
secondTime.setCycleCount(Animation.INDEFINITE); | |
digitalTime.setCycleCount(Animation.INDEFINITE); | |
// start the analogueClock. | |
digitalTime.play(); | |
secondTime.play(); | |
minuteTime.play(); | |
hourTime.play(); | |
stage.initStyle(StageStyle.TRANSPARENT); | |
// add a glow effect whenever the mouse is positioned over the clock. | |
final Glow glow = new Glow(); | |
analogueClock.setOnMouseEntered(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
analogueClock.setEffect(glow); | |
} | |
}); | |
analogueClock.setOnMouseExited(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
analogueClock.setEffect(null); | |
} | |
}); | |
// fade out the scene and shut it down when the mouse is clicked on the clock. | |
analogueClock.setOnMouseClicked(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
analogueClock.setMouseTransparent(true); | |
FadeTransition fade = new FadeTransition(Duration.seconds(1.2), analogueClock); | |
fade.setOnFinished(new EventHandler<ActionEvent>() { | |
@Override public void handle(ActionEvent actionEvent) { | |
stage.close(); | |
} | |
}); | |
fade.setFromValue(1); | |
fade.setToValue(0); | |
fade.play(); | |
} | |
}); | |
// layout the scene. | |
final VBox layout = new VBox(); | |
layout.getChildren().addAll(analogueClock, digitalClock); | |
layout.setAlignment(Pos.CENTER); | |
final Scene scene = new Scene(layout, Color.TRANSPARENT); | |
scene.getStylesheets().add(getResource("clock.css")); | |
stage.setScene(scene); | |
// allow the clock background to be used to drag the clock around. | |
final Delta dragDelta = new Delta(); | |
layout.setOnMousePressed(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
// record a delta distance for the drag and drop operation. | |
dragDelta.x = stage.getX() - mouseEvent.getScreenX(); | |
dragDelta.y = stage.getY() - mouseEvent.getScreenY(); | |
scene.setCursor(Cursor.MOVE); | |
} | |
}); | |
layout.setOnMouseReleased(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
scene.setCursor(Cursor.HAND); | |
} | |
}); | |
layout.setOnMouseDragged(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
stage.setX(mouseEvent.getScreenX() + dragDelta.x); | |
stage.setY(mouseEvent.getScreenY() + dragDelta.y); | |
} | |
}); | |
layout.setOnMouseEntered(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
if (!mouseEvent.isPrimaryButtonDown()) { | |
scene.setCursor(Cursor.HAND); | |
} | |
} | |
}); | |
layout.setOnMouseExited(new EventHandler<MouseEvent>() { | |
@Override public void handle(MouseEvent mouseEvent) { | |
if (!mouseEvent.isPrimaryButtonDown()) { | |
scene.setCursor(Cursor.DEFAULT); | |
} | |
} | |
}); | |
// show the scene. | |
stage.show(); | |
} | |
private String pad(int fieldWidth, char padChar, String s) { | |
StringBuilder sb = new StringBuilder(); | |
for (int i = s.length(); i < fieldWidth; i++) { | |
sb.append(padChar); | |
} | |
sb.append(s); | |
return sb.toString(); | |
} | |
static String getResource(String path) { | |
return Clock.class.getResource(path).toExternalForm(); | |
} | |
// records relative x and y co-ordinates. | |
class Delta { double x, y; } | |
} |
That is a nice feature set @antic-ml. The refactored code, may have been a better starting point for the fork as that code is better organized.
Thanks for your comment on the feature set.
I understand your desire to refactor the code in response to criticism of
your coding style, but the new version is long-winded in comparison to the
short and concise version you originally uploaded. The purpose of your
original gist was to demonstrate various features of JavaFX, which it did
very well. Perhaps your critic neglected to take this fact into account?
Also, because the code was all in one file, it was extremely easy to hack
and play around with. The new version adds classes and a number of levels
of indirection which makes it a little harder to grok. (In particular, I
have always disliked the builder pattern because I find it hard to read.)
The refactored version is very nicely written, but I prefer your original
version for its consciseness.
Thanks for producing the original gist. I learned how to do a number of
things from it.
Yours,
Mario Gianota
…On Thu, 12 Dec 2019 at 07:27, jewelsea ***@***.***> wrote:
That is a nice feature set @antic-ml <https://github.com/antic-ml>. The refactored
code <https://gist.github.com/jewelsea/3388637>, may have been a better
starting point for the fork as that code is better organized.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<https://gist.github.com/2658491?email_source=notifications&email_token=AM4TNWFBAXGSKRXELHZKLCDQYHRWPA5CNFSM4JPUTOHKYY3PNVWWK3TUL52HS4DFVNDWS43UINXW23LFNZ2KUY3PNVWWK3TUL5UWJTQAF5YR6#gistcomment-3109151>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AM4TNWFX5SN3IY4FFJYWRSTQYHRWPANCNFSM4JPUTOHA>
.
This is amazing though, good Job
Super!!! Thank you!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I forked this Gist and added the following features to the clock:
It is available here: JavaFXClock