├── .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 | Banner 3 |

4 | 5 | FroXty is JavaFX library which replicates the famous iOS translucent effect with ease. 6 | 7 | ![Demo](https://i.imgur.com/Ri1srhg.gif) 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 | } --------------------------------------------------------------------------------