├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
└── java
│ └── eu
│ └── iamgio
│ └── froxty
│ ├── FrostyBox.java
│ ├── FrostyEffect.java
│ └── SnapshotHelper.java
└── test
├── java
└── eu
│ └── iamgio
│ └── froxty
│ └── FroxtyTest.java
└── resources
├── font
└── Karla-Regular.ttf
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | target/
3 | *.iml
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Giorgio Garofalo
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FroXty is JavaFX library which replicates the famous iOS translucent effect with ease.
6 |
7 | 
8 |
9 | ## Set-up
10 |
11 | FroXty can be imported into your project either by downloading the JAR file (see releases) or via Maven/Gradle through JitPack.
12 |
13 | ### Maven
14 | ```xml
15 |
16 |
17 | jitpack.io
18 | https://jitpack.io
19 |
20 |
21 |
22 | com.github.iAmGio
23 | froxty
24 | 1.4.0
25 |
26 | ```
27 |
28 | ### Gradle
29 | ```gradle
30 | allprojects {
31 | repositories {
32 | ...
33 | maven { url 'https://jitpack.io' }
34 | }
35 | }
36 | dependencies {
37 | implementation 'com.github.iAmGio:froxty:1.4.0'
38 | }
39 | ```
40 |
41 | ## Getting started
42 |
43 | The following piece of code will generate a frosty effect out of any node:
44 | ```java
45 | //...
46 | FrostyEffect effect = new FrostyEffect(opacity, updateTime); // Instantiates the effect. The parameters are optional and default to (0.5, 10)
47 | FrostyBox box = new FrostyBox(effect, node); // Instantiates a container with frosty effect
48 | box.setBorderRadius(borderRadius); // Rounds the borders of the box
49 | root.getChildren().add(box); // Adds the container to the scene
50 | ```
51 |
52 | Then it's possible to style it:
53 | ```css
54 | .frosty-box {
55 | -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, .5), 15, 0, 0, 5);
56 | }
57 |
58 | .frosty-box > * {
59 | -fx-background-color: rgba(255, 255, 255, .4);
60 | -fx-background-radius: 20;
61 | }
62 | ```
63 |
64 | ## Important notes
65 | - Target nodes must not be directly added to the root. Add the Frosty Box, which wraps the target node, instead.
66 | - Applying drop shadows to the target node results in visual errors. Apply effects to the Frosty Box instead.
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | eu.iamgio.froxty
8 | froxty
9 | 1.4.0
10 |
11 |
12 | 1.8
13 | 1.8
14 |
15 |
16 |
17 |
18 |
19 | org.apache.maven.plugins
20 | maven-compiler-plugin
21 | 3.8.1
22 |
23 |
24 | compile
25 | compile
26 |
27 | compile
28 |
29 |
30 |
31 | testCompile
32 | test-compile
33 |
34 | testCompile
35 |
36 |
37 |
38 |
39 | 8
40 | 8
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.openjfx
49 | javafx-controls
50 | 11
51 | provided
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/main/java/eu/iamgio/froxty/FrostyBox.java:
--------------------------------------------------------------------------------
1 | package eu.iamgio.froxty;
2 |
3 | import javafx.animation.PauseTransition;
4 | import javafx.beans.property.SimpleDoubleProperty;
5 | import javafx.beans.property.SimpleObjectProperty;
6 | import javafx.scene.Node;
7 | import javafx.scene.Parent;
8 | import javafx.scene.effect.GaussianBlur;
9 | import javafx.scene.image.Image;
10 | import javafx.scene.image.ImageView;
11 | import javafx.scene.shape.Rectangle;
12 | import javafx.util.Duration;
13 |
14 | /**
15 | * This single-child node contains takes a {@link FrostyEffect} and blurs the content beneath.
16 | * @author Giorgio Garofalo
17 | */
18 | public class FrostyBox extends Parent {
19 |
20 | private final SimpleObjectProperty image = new SimpleObjectProperty<>();
21 | private final SimpleObjectProperty child = new SimpleObjectProperty<>();
22 | private final SimpleDoubleProperty borderRadius = new SimpleDoubleProperty();
23 |
24 | private final SnapshotHelper helper = new SnapshotHelper();
25 | private final GaussianBlur blur;
26 |
27 | /**
28 | * Instantiates a container with frosty backdrop effect.
29 | * @param effect frosty effect instance
30 | * @param child target node
31 | */
32 | public FrostyBox(FrostyEffect effect, Node child) {
33 | this.child.set(child);
34 |
35 | getStyleClass().add("frosty-box");
36 |
37 | // Set-up blurred background
38 | ImageView background = new ImageView();
39 | getChildren().add(background);
40 |
41 | image.addListener((observable, old, img) -> {
42 | if(img != null) {
43 | background.setFitWidth(img.getWidth());
44 | background.setFitHeight(img.getHeight());
45 | background.setImage(img);
46 | updateClip(background, img.getWidth(), img.getHeight());
47 | }
48 | });
49 |
50 | // Bind blur amount to opacityProperty
51 | blur = new GaussianBlur();
52 | blur.radiusProperty().bind(effect.opacityProperty().multiply(100));
53 |
54 | if(child != null) getChildren().add(child);
55 |
56 | initLoop(effect.getUpdateTime());
57 | initChildListener();
58 | }
59 |
60 | /**
61 | * Instantiates a container with frosty backdrop effect and no child.
62 | * @param effect frosty effect instance
63 | */
64 | public FrostyBox(FrostyEffect effect) {
65 | this(effect, null);
66 | }
67 |
68 | /**
69 | * Instantiates a container with default frosty effect.
70 | */
71 | public FrostyBox() {
72 | this(new FrostyEffect(), null);
73 | }
74 |
75 | /**
76 | * @return Target of the effect
77 | */
78 | public Node getChild() {
79 | return child.get();
80 | }
81 |
82 | /**
83 | * Sets the target of the effect.
84 | * @param child target
85 | */
86 | public void setChild(Node child) {
87 | this.child.set(child);
88 | }
89 |
90 | /**
91 | * @return The border radius of this box.
92 | */
93 | public double getBorderRadius() {
94 | return borderRadius.doubleValue();
95 | }
96 |
97 | /**
98 | * @return The border radius of this box.
99 | */
100 | public SimpleDoubleProperty borderRadiusProperty() {
101 | return borderRadius;
102 | }
103 |
104 | /**
105 | * Sets the border radius of this box.
106 | * @param borderRadius new border radius
107 | */
108 | public void setBorderRadius(double borderRadius) {
109 | this.borderRadius.set(borderRadius);
110 | }
111 |
112 | /**
113 | * @return JavaFX blur effect
114 | */
115 | GaussianBlur getBlur() {
116 | return blur;
117 | }
118 |
119 | /**
120 | * Starts the loop that continuously snapshots and blurs the background
121 | * @param updateTime time in millis between tasks
122 | */
123 | private void initLoop(double updateTime) {
124 | // Set-up loop
125 | PauseTransition loop = new PauseTransition(Duration.millis(updateTime));
126 | loop.setOnFinished(e -> {
127 | if(getChild() != null && getScene() != null) {
128 | // Set screenshot as background of the box and blur it
129 | update();
130 | }
131 | loop.playFromStart();
132 | });
133 | loop.playFromStart();
134 |
135 | // Set-up listener to check whether the box is in the scene.
136 | // If not, the loop is paused.
137 | sceneProperty().addListener((observable, oldValue, newValue) -> {
138 | if(newValue == null) {
139 | loop.stop();
140 | } else {
141 | loop.playFromStart();
142 | }
143 | });
144 | }
145 |
146 | /**
147 | * Sets-up a listener for this box's child
148 | */
149 | private void initChildListener() {
150 | // Set-up listener to when child changes
151 | this.child.addListener((observable, oldValue, newValue) -> {
152 | if(newValue == null) {
153 | getChildren().remove(1, 2);
154 | } else if(getChildren().size() < 2) {
155 | getChildren().add(newValue);
156 | } else {
157 | getChildren().set(1, newValue);
158 | }
159 | });
160 | }
161 |
162 | /**
163 | * Updates the clip mask of this box, which allows to round borders
164 | * @param background background view
165 | * @param width mask width
166 | * @param height mask height
167 | */
168 | private void updateClip(Node background, double width, double height) {
169 | Rectangle clip;
170 | if(background.getClip() instanceof Rectangle) {
171 | clip = (Rectangle) background.getClip();
172 | } else {
173 | clip = new Rectangle();
174 | background.setClip(clip);
175 | }
176 | clip.setWidth(width);
177 | clip.setHeight(height);
178 |
179 | double radius = borderRadius.doubleValue();
180 | clip.setArcWidth(radius);
181 | clip.setArcHeight(radius);
182 | }
183 |
184 | private void update() {
185 | try {
186 | image.set(helper.snapshot(this));
187 | } catch(Exception e) {
188 | e.printStackTrace();
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/main/java/eu/iamgio/froxty/FrostyEffect.java:
--------------------------------------------------------------------------------
1 | package eu.iamgio.froxty;
2 |
3 | import javafx.beans.property.SimpleDoubleProperty;
4 |
5 | /**
6 | * This class handles FroXty's frosty/translucent effect
7 | * @author Giorgio Garofalo
8 | */
9 | public class FrostyEffect {
10 |
11 | /**
12 | * Effect opacity
13 | */
14 | private final SimpleDoubleProperty opacity = new SimpleDoubleProperty(0.5);
15 |
16 | /**
17 | * Time (in ms) required to update
18 | */
19 | private int updateTime = 10;
20 |
21 | /**
22 | * Instantiates a new frosty effect with base opacity 0.50
23 | */
24 | public FrostyEffect() {}
25 |
26 | /**
27 | * Instantiates a new frosty effect with base opacity 0.50 and custom update time
28 | * @param updateTime time required to update the effect
29 | */
30 | public FrostyEffect(int updateTime) {
31 | this.updateTime = updateTime;
32 | }
33 |
34 | /**
35 | * Instantiates a new frosty effect
36 | * @param opacity effect opacity
37 | */
38 | public FrostyEffect(double opacity) {
39 | setOpacity(opacity);
40 | }
41 |
42 | /**
43 | * Instantiates a new frosty effect
44 | * @param opacity effect opacity
45 | * @param updateTime time required to update the effect
46 | */
47 | public FrostyEffect(double opacity, int updateTime) {
48 | this(opacity);
49 | this.updateTime = updateTime;
50 | }
51 |
52 | /**
53 | * Opacity of the effect. The more opaque, the more blurry the content looks
54 | */
55 | public SimpleDoubleProperty opacityProperty() {
56 | return opacity;
57 | }
58 |
59 | /**
60 | * @return Opacity of the effect. The more opaque, the more blurry the content looks
61 | */
62 | public double getOpacity() {
63 | return opacity.get();
64 | }
65 |
66 | /**
67 | * Sets the opacity of the effect
68 | * @param opacity new effect opacity
69 | */
70 | public void setOpacity(double opacity) {
71 | this.opacity.set(opacity);
72 | }
73 |
74 | /**
75 | * @return Time (millis) between updates. Default value is 40
76 | */
77 | public int getUpdateTime() {
78 | return updateTime;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/eu/iamgio/froxty/SnapshotHelper.java:
--------------------------------------------------------------------------------
1 | package eu.iamgio.froxty;
2 |
3 | import javafx.geometry.Bounds;
4 | import javafx.scene.Node;
5 | import javafx.scene.Parent;
6 | import javafx.scene.Scene;
7 | import javafx.scene.SnapshotParameters;
8 | import javafx.scene.effect.Effect;
9 | import javafx.scene.image.Image;
10 | import javafx.scene.image.WritableImage;
11 | import javafx.scene.paint.Color;
12 |
13 | /**
14 | * Class that handles image-based operations.
15 | * @author Giorgio Garofalo
16 | */
17 | public class SnapshotHelper {
18 |
19 | private final SnapshotParameters parameters = new SnapshotParameters();
20 |
21 | SnapshotHelper() {
22 | this.parameters.setFill(Color.TRANSPARENT);
23 | }
24 |
25 | /**
26 | * Snapshots the blurred content below a {@link FrostyBox} and crops the result so that it fits the size of the box
27 | * @param box frosty box
28 | * @return cropped and blurred snapshot of the background
29 | */
30 | Image snapshot(FrostyBox box) {
31 | // Temporarily hide this node
32 | box.setVisible(false);
33 |
34 | // Get child position
35 | Node child = box.getChild();
36 | Bounds bounds = child.localToScene(child.getBoundsInLocal());
37 | Scene scene = child.getScene();
38 |
39 | // Temporarily blur the root
40 | Parent root = scene.getRoot();
41 | Effect oldEffect = root.getEffect();
42 | root.setEffect(box.getBlur());
43 |
44 | // Get an image of the root without the target itself
45 | Image backgroundSnapshot = root.snapshot(parameters, null);
46 |
47 | // Get an image of the target
48 | box.setVisible(true);
49 | Image childSnapshot = child.snapshot(parameters, null);
50 |
51 | try {
52 | // Crop the snapshot
53 | return crop(box.getBlur().getRadius(), backgroundSnapshot, scene, bounds);
54 | } catch(IllegalArgumentException e) {
55 | // If either width or height are 0
56 | return null;
57 | } finally {
58 | // Apply the previous effect to the root
59 | root.setEffect(oldEffect);
60 | }
61 | }
62 |
63 | /**
64 | * Crops the snapshot of the root to the size of the box
65 | * @param blurRadius radius of the gaussian blur
66 | * @param source snapshot of the box
67 | * @param scene scene of the box
68 | * @param bounds coordinates and size of the box
69 | * @return cropped image
70 | */
71 | private WritableImage crop(double blurRadius, Image source, Scene scene, Bounds bounds) {
72 | return new WritableImage(source.getPixelReader(),
73 | properValue(bounds.getMinX() + blurRadius, scene.getWidth()),
74 | properValue(bounds.getMinY() + blurRadius, scene.getHeight()),
75 | properValue(bounds.getWidth(), scene.getWidth() - bounds.getMinX()),
76 | properValue(bounds.getHeight(), scene.getHeight() - bounds.getMinY()));
77 | }
78 |
79 | private int properValue(double value, double max) {
80 | if(value < 0) return 0;
81 | if(max < 0) return 0;
82 | // This method is called multiple times, therefore avoiding Math.min calls improves the general performance.
83 | return (int) (value <= max ? value : max);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/test/java/eu/iamgio/froxty/FroxtyTest.java:
--------------------------------------------------------------------------------
1 | package eu.iamgio.froxty;
2 |
3 | import javafx.application.Application;
4 | import javafx.geometry.Pos;
5 | import javafx.scene.Cursor;
6 | import javafx.scene.Scene;
7 | import javafx.scene.control.Label;
8 | import javafx.scene.control.Slider;
9 | import javafx.scene.layout.AnchorPane;
10 | import javafx.scene.layout.Pane;
11 | import javafx.scene.layout.VBox;
12 | import javafx.scene.text.Font;
13 | import javafx.scene.text.TextAlignment;
14 | import javafx.stage.Stage;
15 |
16 | /**
17 | * @author Giorgio Garofalo
18 | */
19 | public class FroxtyTest extends Application {
20 |
21 | public void start(Stage primaryStage) {
22 | AnchorPane root = new AnchorPane();
23 | Scene scene = new Scene(root, 600, 600);
24 | scene.getStylesheets().add("/style.css");
25 | Font.loadFont(getClass().getResourceAsStream("/font/Karla-Regular.ttf"), 16);
26 |
27 | root.getStyleClass().add("root");
28 | root.prefWidthProperty().bind(scene.widthProperty());
29 | root.prefHeightProperty().bind(scene.heightProperty());
30 |
31 | FrostyEffect effect = setupEffect(root);
32 | setupSliders(root, effect);
33 |
34 | primaryStage.setScene(scene);
35 | primaryStage.setTitle("FroXty demo");
36 | primaryStage.show();
37 | }
38 |
39 | private FrostyEffect setupEffect(Pane root) {
40 | Label label = new Label("Welcome to\nFroXty");
41 | label.setTextAlignment(TextAlignment.CENTER);
42 | label.setAlignment(Pos.CENTER);
43 |
44 | Pane target = new Pane(label);
45 | target.getStyleClass().add("container");
46 | target.setCursor(Cursor.MOVE);
47 |
48 | FrostyEffect effect = new FrostyEffect(1);
49 |
50 | target.prefWidthProperty().bind(root.prefWidthProperty().divide(2));
51 | target.prefHeightProperty().bind(root.prefHeightProperty().divide(4));
52 |
53 | label.prefWidthProperty().bind(target.prefWidthProperty());
54 | label.prefHeightProperty().bind(target.prefHeightProperty());
55 |
56 | FrostyBox box = new FrostyBox(effect, target);
57 | box.setBorderRadius(32);
58 | makeBoxDraggable(box);
59 |
60 | box.translateXProperty()
61 | .bind(root.prefWidthProperty().divide(2).subtract(target.prefWidthProperty().divide(2)));
62 | box.translateYProperty()
63 | .bind(root.prefHeightProperty().divide(2).subtract(target.prefHeightProperty().divide(2)));
64 |
65 | root.getChildren().add(box);
66 | return effect;
67 | }
68 |
69 | private void setupSliders(Pane root, FrostyEffect effect) {
70 | Slider opacitySlider = new Slider(0, 1, .5);
71 | effect.opacityProperty().bind(opacitySlider.valueProperty());
72 |
73 | root.getChildren().add(new VBox(opacitySlider));
74 | }
75 |
76 | private void makeBoxDraggable(FrostyBox box) {
77 | class Delta {
78 | public double x, y;
79 | }
80 | Delta delta = new Delta();
81 | box.setOnMousePressed(e -> {
82 | delta.x = box.getLayoutX() - e.getSceneX();
83 | delta.y = box.getLayoutY() - e.getSceneY();
84 | });
85 | box.setOnMouseDragged(e -> {
86 | box.setLayoutX(e.getSceneX() + delta.x);
87 | box.setLayoutY(e.getSceneY() + delta.y);
88 | });
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/test/resources/font/Karla-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iamgio/froxty/768b54fe27f9b6d8b2112bf0c1dad61cabd3fd5b/src/test/resources/font/Karla-Regular.ttf
--------------------------------------------------------------------------------
/src/test/resources/style.css:
--------------------------------------------------------------------------------
1 | .root {
2 | -fx-background-image: url("https://img.gadgethacks.com/img/86/37/63601592852769/0/get-ios-10s-new-wallpaper-any-phone.w1456.jpg");
3 | -fx-background-repeat: stretch;
4 | -fx-background-size: cover;
5 | -fx-background-position: center center;
6 | }
7 |
8 | .text {
9 | -fx-font-smoothing-type: gray;
10 | }
11 |
12 | .frosty-box {
13 | -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, .5), 15, 0, 0, 5);
14 | }
15 |
16 | .frosty-box > * {
17 | -fx-background-color: rgba(255, 255, 255, .4);
18 | -fx-background-radius: 20;
19 | }
20 |
21 | .frosty-box .label {
22 | -fx-font-family: 'Karla';
23 | -fx-font-size: 25;
24 | -fx-text-fill: black;
25 | }
--------------------------------------------------------------------------------