Skip to content

Instantly share code, notes, and snippets.

@jewelsea
Created August 18, 2012 17:37
Show Gist options
  • Save jewelsea/3388637 to your computer and use it in GitHub Desktop.
Save jewelsea/3388637 to your computer and use it in GitHub Desktop.
Refactored sample of an animated clock in JavaFX
/** analogue-clock.css (place in the same directory as AnalogueClock.java and ensure build script copies the file to the same location as AnalogueClock.class) */
#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%);
}
.tick {
-fx-stroke: derive(darkgoldenrod, -15%);
-fx-stroke-width: 3;
-fx-stroke-line-cap: round;
}
import javafx.animation.*;
import javafx.beans.property.DoubleProperty;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
import java.util.Calendar;
/**
* Displays an animated AnalogueClock face.
* Time is the system time for the local timezone.
* see: analogue-clock.css for css formatting rules for the clock.
*/
public class AnalogueClock extends Group {
final int HOUR_HAND_LENGTH = 50;
final int MINUTE_HAND_LENGTH = 75;
final int SECOND_HAND_LENGTH = 88;
final int SECOND_HAND_OFFSET = 15;
AnalogueClock(String brandName, double clockRadius) {
setId("analogueClock");
getStylesheets().add(
ResourceResolver.getResourceFor(
getClass(),
"analogue-clock.css"
)
);
// construct the analogueClock pieces.
final Circle face = createClockFace(clockRadius);
final Label brand = createBrand(face, brandName);
final Line hourHand = createHand(
"hourHand",
clockRadius,
0,
percentOf(HOUR_HAND_LENGTH, clockRadius)
);
final Line minuteHand = createHand(
"minuteHand",
clockRadius,
0,
percentOf(MINUTE_HAND_LENGTH, clockRadius)
);
final Line secondHand = createHand(
"secondHand",
clockRadius,
percentOf(SECOND_HAND_OFFSET, clockRadius),
percentOf(SECOND_HAND_LENGTH, clockRadius)
);
// animate the hands with the time.
bindClockHandsToTime(hourHand, minuteHand, secondHand);
getChildren().addAll(
face,
brand,
createTicks(clockRadius),
createSpindle(clockRadius),
hourHand,
minuteHand,
secondHand
);
}
/** @return radial ticks around the clock center to mark time. */
private Group createTicks(double clockRadius) {
final double TICK_START_OFFSET = percentOf(83, clockRadius);
final double TICK_END_OFFSET = percentOf(93, clockRadius);
final Group ticks = new Group();
for (int i = 0; i < 12; i++) {
Line tick = new Line(0, -TICK_START_OFFSET, 0, -TICK_END_OFFSET);
tick.getStyleClass().add("tick");
tick.setLayoutX(clockRadius);
tick.setLayoutY(clockRadius);
tick.getTransforms().add(new Rotate(i * (360 / 12)));
ticks.getChildren().add(tick);
}
return ticks;
}
/** @return a rendered spindle around which the clockwork rotates */
private Circle createSpindle(double clockRadius) {
final Circle spindle = new Circle(clockRadius, clockRadius, 5);
spindle.setId("spindle");
return spindle;
}
private Circle createClockFace(double clockRadius) {
final Circle face = new Circle(clockRadius, clockRadius, clockRadius);
face.setId("face");
return face;
}
private Line createHand(String handId, double clockRadius, double handOffsetLength, double handLength) {
final Line secondHand = new Line(0, handOffsetLength, 0, -handLength);
secondHand.setLayoutX(clockRadius);
secondHand.setLayoutY(clockRadius);
secondHand.setId(handId);
return secondHand;
}
private Label createBrand(Circle face, String brandName) {
final Label brand = new Label(brandName);
brand.setId("brand");
brand.layoutXProperty().bind(face.centerXProperty().subtract(brand.widthProperty().divide(2)));
brand.layoutYProperty().bind(face.centerYProperty().add(face.radiusProperty().divide(2)));
return brand;
}
private void bindClockHandsToTime(final Line hourHand, final Line minuteHand, final Line secondHand) {
// determine initial rotation for the clock hands.
Calendar time = Calendar.getInstance();
final double initialHourhandDegrees = calculateHourHandDegrees(time);
final double initialMinuteHandDegrees = calculateMinuteHandDegrees(time);
final double initialSecondHandDegrees = calculateSecondHandDegrees(time);
// animate the clock movements using timelines.
createRotationTimeline( // the hour hand rotates twice a day.
createRotate(hourHand, initialHourhandDegrees).angleProperty(),
Duration.hours(12),
initialHourhandDegrees
);
createRotationTimeline( // the minute hand rotates once an hour.
createRotate(minuteHand, initialMinuteHandDegrees).angleProperty(),
Duration.minutes(60),
initialMinuteHandDegrees
);
createRotationTimeline( // move second hand rotates once a minute.
createRotate(secondHand, initialSecondHandDegrees).angleProperty(),
Duration.seconds(60),
initialSecondHandDegrees
);
}
private Rotate createRotate(Line hand, double initialHandDegrees) {
final Rotate hourRotate = new Rotate(initialHandDegrees);
hand.getTransforms().add(hourRotate);
return hourRotate;
}
/**
* Performs a 360 degree rotation of the angleProperty once in every duration.
* rotation starts from initialRotation degrees.
*/
private void createRotationTimeline(DoubleProperty angleProperty, Duration duration, double initialRotation) {
Timeline timeline = new Timeline(
new KeyFrame(
duration,
new KeyValue(
angleProperty,
360 + initialRotation,
Interpolator.LINEAR
)
)
);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
private int calculateSecondHandDegrees(Calendar time) {
return time.get(Calendar.SECOND) * (360 / 60);
}
private double calculateMinuteHandDegrees(Calendar time) {
return (time.get(Calendar.MINUTE) + calculateSecondHandDegrees(time) / 360.0) * (360 / 60);
}
private double calculateHourHandDegrees(Calendar time) {
return (time.get(Calendar.HOUR) + calculateMinuteHandDegrees(time) / 360.0) * (360 / 12);
}
private double percentOf(double percent, double clockRadius) {
return percent / 100 * clockRadius;
}
}
/** clockdemo.css (place in the same directory as ClockDemo.java and ensure build script copies the file to the same location as ClockDemo.class) */
#layout {
-fx-padding: 5;
}
#clocks {
-fx-padding: 8;
}
#backdrop {
-fx-background-color: linear-gradient(to bottom, cornsilk, wheat);
-fx-effect: dropshadow(three-pass-box, wheat, 10, 0, 0, 0);
}
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPaneBuilder;
import javafx.scene.layout.VBoxBuilder;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/** Demonstrates drawing a clock in JavaFX */
public class ClockDemo extends Application {
final String BRAND_NAME = "Splotch";
final double CLOCK_RADIUS = 100;
public static void main(String[] args) throws Exception {
launch(args);
}
public void start(final Stage stage) throws Exception {
// create the scene elements.
final Pane backdrop = makeBackdrop("backdrop", stage);
// layout the scene.
final Scene scene = createClockScene(
StackPaneBuilder.create()
.id("layout")
.children(
backdrop,
VBoxBuilder.create()
.id("clocks")
.pickOnBounds(false)
.children(
makeAnalogueClock(stage),
new DigitalClock())
.alignment(Pos.CENTER)
.build()
)
.build()
);
// size the backdrop to the scene.
sizeToScene(backdrop, scene);
// show the scene.
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
stage.show();
}
private AnalogueClock makeAnalogueClock(Stage stage) {
final AnalogueClock analogueClock = new AnalogueClock(BRAND_NAME, CLOCK_RADIUS);
EffectUtilities.addGlowOnHover(analogueClock);
EffectUtilities.fadeOnClick(analogueClock, closeStageEventHandler(stage));
return analogueClock;
}
private void sizeToScene(Pane pane, Scene scene) {
pane.prefWidthProperty().bind(scene.widthProperty());
pane.prefHeightProperty().bind(scene.heightProperty());
}
private Scene createClockScene(Parent layout) {
final Scene scene = new Scene(layout, Color.TRANSPARENT);
scene.getStylesheets().add(
ResourceResolver.getResourceFor(
ClockDemo.class,
"clock-demo.css"
)
);
return scene;
}
private EventHandler<ActionEvent> closeStageEventHandler(final Stage stage) {
return new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent actionEvent) {
stage.close();
}
};
}
private Pane makeBackdrop(String id, Stage stage) {
Pane backdrop = new Pane();
backdrop.setId(id);
EffectUtilities.makeDraggable(stage, backdrop);
return backdrop;
}
}
/** digitalclock.css (place in the same directory as DigitalClock.java and ensure build script copies the file to the same location as DigitalClock.class) */
#digitalClock {
-fx-font-size: 14px;
-fx-font-family: 'Courier New';
}
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Label;
import javafx.util.Duration;
import java.util.Calendar;
/**
* Creates a digital clock display as a simple label.
* Format of the clock display is hh:mm:ss aa, where:
* hh Hour in am/pm (1-12)
* mm Minute in hour
* ss Second in minute
* aa Am/pm marker
* Time is the system time for the local timezone.
* see: digital-clock.css for css formatting rules for the clock.
*/
public class DigitalClock extends Label {
public DigitalClock() {
setId("digitalClock");
getStylesheets().add(
ResourceResolver.getResourceFor(
getClass(),
"digital-clock.css"
)
);
bindToTime();
}
// the digital clock updates once a second.
private void bindToTime() {
Timeline timeline = new Timeline(
new KeyFrame(Duration.seconds(0),
new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent actionEvent) {
Calendar time = Calendar.getInstance();
String hourString = StringUtilities.pad(2, ' ', time.get(Calendar.HOUR) == 0 ? "12" : time.get(Calendar.HOUR) + "");
String minuteString = StringUtilities.pad(2, '0', time.get(Calendar.MINUTE) + "");
String secondString = StringUtilities.pad(2, '0', time.get(Calendar.SECOND) + "");
String ampmString = time.get(Calendar.AM_PM) == Calendar.AM ? "AM" : "PM";
setText(hourString + ":" + minuteString + ":" + secondString + " " + ampmString);
}
}
),
new KeyFrame(Duration.seconds(1))
);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
}
import javafx.animation.FadeTransition;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.effect.Glow;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import javafx.util.Duration;
/** Various utilities for applying different effects to nodes. */
public class EffectUtilities {
/** configures the node to fade when it is clicked on performed the onFinished handler when the fade is complete */
public static void fadeOnClick(final Node node, final EventHandler<ActionEvent> onFinished) {
node.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
node.setMouseTransparent(true);
FadeTransition fade = new FadeTransition(Duration.seconds(1.2), node);
fade.setOnFinished(onFinished);
fade.setFromValue(1);
fade.setToValue(0);
fade.play();
}
});
}
/* adds a glow effect to a node when the mouse is hovered over the node */
public static void addGlowOnHover(final Node node) {
final Glow glow = new Glow();
node.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
node.setEffect(glow);
}
});
node.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
node.setEffect(null);
}
});
}
/** makes a stage draggable using a given node */
public static void makeDraggable(final Stage stage, final Node byNode) {
final Delta dragDelta = new Delta();
byNode.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();
byNode.setCursor(Cursor.MOVE);
}
});
byNode.setOnMouseReleased(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
byNode.setCursor(Cursor.HAND);
}
});
byNode.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
stage.setX(mouseEvent.getScreenX() + dragDelta.x);
stage.setY(mouseEvent.getScreenY() + dragDelta.y);
}
});
byNode.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
byNode.setCursor(Cursor.HAND);
}
}
});
byNode.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
byNode.setCursor(Cursor.DEFAULT);
}
}
});
}
/** records relative x and y co-ordinates. */
private static class Delta {
double x, y;
}
}
import java.net.URL;
public class ResourceResolver {
/**
* Retrieves an absolute resource path from a path relative to the location of the specified class.
* The requested resource must exist otherwise this method will throw an IllegalArgumentException.
* @param clazz a path relative to the location of this class.
* @param path a path relative to the location of this class.
* @return the absolute resource path.
* @throws IllegalArgumentException if a resource at the specified path does not exist.
*/
public static String getResourceFor(Class clazz, String path) {
URL resourceURL = clazz.getResource(path);
if (resourceURL == null) {
throw new IllegalArgumentException("No resource exists at: " + path + " relative to " + clazz.getName());
}
return resourceURL.toExternalForm();
}
}
public class StringUtilities {
/**
* Creates a string left padded to the specified width with the supplied padding character.
* @param fieldWidth the length of the resultant padded string.
* @param padChar a character to use for padding the string.
* @param s the string to be padded.
* @return the padded string.
*/
public static 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();
}
}
@jewelsea
Copy link
Author

jewelsea commented Aug 9, 2013

Thanks for pointing out, the missing hyphens in the filenames JBKMG.
That was a typo, not a clever demo of the ResourceResolver :-)

Unfortunately the github editor is broken for this gist and won't let me rename the files.

If the files are manually renamed as digital-clock.css and clock-demo.css, then everything should work as expected.

@ProfessorX
Copy link

VERY GOOD.

100/100.

THANKS!

@LukeOnuke
Copy link

Hopefully you wont mind me using the make draggable method in a library , couldn't think of a better implementation myself.

@Ygarr
Copy link

Ygarr commented Oct 15, 2022

Now it`s not actual for state-of-the-art JavaFX.

public class StringUtilities {
       ^
ClockDemo.java:8: error: cannot find symbol
import javafx.scene.layout.StackPaneBuilder;
                          ^
  symbol:   class StackPaneBuilder
  location: package javafx.scene.layout
ClockDemo.java:9: error: cannot find symbol
import javafx.scene.layout.VBoxBuilder;
                          ^
  symbol:   class VBoxBuilder
  location: package javafx.scene.layout
ClockDemo.java:29: error: cannot find symbol
      StackPaneBuilder.create()
      ^
  symbol:   variable StackPaneBuilder
  location: class ClockDemo
ClockDemo.java:33: error: cannot find symbol
          VBoxBuilder.create()
          ^
  symbol:   variable VBoxBuilder
  location: class ClockDemo
5 errors

@jewelsea
Copy link
Author

jewelsea commented Oct 16, 2022

@Ygarr Yes, this code is freeware and you can do with it as you like. Also, yes, this gist is quite old. The builder objects were deprecated and removed from JavaFX some time back, so the code no longer compiles. A more modern implementation would replace the builder usage with directly instantiating the related objects and setting the properties on them via setters and make user of lambdas where appropriate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment