JavaFX Image (PNG) transparency crispness being lost when rendering - java
I am loading and displaying a transparent PNG on a gradient background with the following result:
If I open the same file in Paint.NET, add the background it looks like that:
Somehow, JavaFX makes the image loose crispness, I fear this may be an issue for ALL images in my app and its just that its most visible in this particular case.
Here is extracted code showing how I load this particular image:
ImageView imgDownload = new ImageView(this.getClass().getResource("/img/docstore/document_downloaded_btn.png").toExternalForm());
imgDownload.setFitWidth(59);
imgDownload.setFitHeight(32);
GridPane.setHalignment(imgDownload, HPos.CENTER);
grid_item.add(imgDownload, 3, 0);
For reference, here is a link to the original image.
I'm looking for an answer highlighting a possible reason this is happening
Update
JavaFX images in Java 8 are now rendered crisply in all cases.
The original issue described in the question has been addressed.
The bug Dreen filed regarding this behaviour, RT-28765 Incorrect subpixel placement for an Image, was closed as a duplicate of RT-19282 [StackPane] unwanted blur with ImageView. RT-19282 was closed as fixed for Java 8.
I tested Java 8 build 108 on Windows 7. The sample application in this answer now displays correctly (no fuzzy image sometimes offset by half a pixel in the x or y direction).
This is an excellent question and very curious behaviour.
I put together a sample program which offers a possible explanation and a workaround.
The output of this program after running it and dragging the border around a little to resize it, is as below. The fuzzy cloud on the left is a standard ImageView placed in a GridPane. The crisp cloud on the right is an ImageView wrapped in my workaround fix class (CenteredRegion), placed in the same GridPane.
While the above image is displayed, the output of the program is:
Layout SnapToPixel: true
...
fuzzy: New Bounds: BoundingBox [minX:14.5, minY:12.5, minZ:0.0, width:59.0, height:32.0, depth:0.0, maxX:73.5, maxY:44.5, maxZ:0.0]
fuzzy: xDisplacement: 0.5, yDisplacement: 0.5
crisp: New Bounds: BoundingBox [minX:84.0, minY:13.0, minZ:0.0, width:59.0, height:32.0, depth:0.0, maxX:143.0, maxY:45.0, maxZ:0.0]
crisp: xDisplacement: 0.0, yDisplacement: 0.0
As you can see, the fuzzy image has not been pixel aligned and is offset by half a pixel in the x and y direction, causing it to look fuzzy. This is despite the grid region having it's snapToPixel setting as true.
As the stage containing the layout is dynamically resized, the fuzzy image will alternate between being pixel aligned and not being pixel aligned (depending on the oddness or evenness of the width and height of the scene). This creates an an annoying shimmering effect as you resize the stage borders, due to the fuzzy image continuously alternating between being fuzzy and clear.
The behaviour of the fuzzy image would seem to be a bug when snapToPixel is set to true on the parent container, so I'd advise filing a bug against the JavaFX runtime project and linking back to this stack overflow question in the bug and placing a link to the created bug in a comment.
The crisp version remains crisp because it is housed in a custom region implementation which ensures pixel alignment.
Test system was win7 + jdk8b77 + ATI HD4600 graphics card.
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.value.*;
import javafx.geometry.*;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.stage.Stage;
public class TransparentPngSample extends Application {
public static final String IMAGE_LOC = "http://i.imgur.com/byY8whh.png";
#Override public void start(Stage stage) {
Pane layout = createSceneContent();
stage.setScene(new Scene(layout));
stage.show();
System.out.println("Layout SnapToPixel: " + layout.snapToPixelProperty().get());
}
private Pane createSceneContent() {
final Image cloudImage = new Image(IMAGE_LOC);
final ImageView fuzzyCloud = new ImageView(cloudImage);
final CenteredRegion crispCloud = new CenteredRegion(new ImageView(cloudImage));
GridPane layout = new GridPane();
layout.setHgap(10);
layout.setVgap(10);
layout.addRow(0, fuzzyCloud, crispCloud);
layout.setAlignment(Pos.CENTER);
layout.setStyle("-fx-padding: 10px; -fx-background-color: slategrey;");
fuzzyCloud.boundsInParentProperty().addListener(new BoundsReporter("fuzzy"));
crispCloud.boundsInParentProperty().addListener(new BoundsReporter("crisp"));
return layout;
}
class CenteredRegion extends Region {
private Node content;
CenteredRegion(Node content) {
this.content = content;
getChildren().add(content);
}
#Override protected void layoutChildren() {
content.relocate(
Math.round(getWidth() / 2 - content.prefWidth(USE_PREF_SIZE) / 2),
Math.round(getHeight() / 2 - content.prefHeight(USE_PREF_SIZE) / 2)
);
System.out.println("crisp content relocated to: " +
getLayoutX() + "," + getLayoutY()
);
}
public Node getContent() {
return content;
}
}
class BoundsReporter implements ChangeListener<Bounds> {
final String logPrefix;
BoundsReporter(String logPrefix) {
this.logPrefix = logPrefix;
}
#Override public void changed(ObservableValue<? extends Bounds> ov, Bounds oldBounds, Bounds newBounds) {
System.out.println(logPrefix + ": " +
"New Bounds: " + newBounds
);
double xDisplacement = newBounds.getMinX() - Math.floor(newBounds.getMinX());
double yDisplacement = newBounds.getMinY() - Math.floor(newBounds.getMinY());
System.out.println(logPrefix + ": " +
"xDisplacement: " + xDisplacement + ", " +
"yDisplacement: " + yDisplacement
);
}
}
public static void main(String[] args) { launch(args); }
}
Related
JavaFX Text - How to define the size in pixels, align it correctly and select a suitable font with the same letter width?
import javafx.geometry.VPos; import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.scene.text.TextBoundsType; import javafx.stage.Stage; public class TextSize extends javafx.application.Application { #Override public void start(Stage stage) { var rectangle = new Rectangle(600, 100, Color.YELLOWGREEN); var text = new Text(0, 50, "EXAMPLING"); text.setFont(Font.font("Monospaced", 100)); text.setBoundsType(TextBoundsType.VISUAL); // Removes text padding, but not the one on the left. text.setTextAlignment(TextAlignment.CENTER); // Doesn't align text horizontally. text.setTextOrigin(VPos.CENTER); // Aligns text vertically. stage.setScene(new Scene(new Pane(rectangle, text))); stage.show(); } public static void main(String[] args) { launch(args); } } I need to edit the text. I primarily want to set the correct Text size. Specifically, the distance between the Text and each edge of the Rectangle should be at least 5 pixels. I could achieve this by defining the size of the Text in pixels. In this case, the text height should be a maximum of 90px and the text length should be a maximum of 590px. However, the font size is not defined in pixels. Even if I enter it using fxml, it has no effect. How do I define it, please? I want to place the text in the center of the Rectangle. Therefore, I need the position of the Text to define its center. var text = new Text(300, 50, "EXAMPLING"); But, as I said in the comments in the code, I couldn't align the text horizontally and remove the padding on the left (As can be seen from the picture). You will probably say that I should use StackPane. But, this is just an example, so I want to know if it can be achieved without it, please? This Font has all the letters the same width, but I don't like it very much. Do you know of a better Font (Arial style), which also has the same width letters? Please help. Thank you UPDATE: I solved the size(1.) and font(3.) of the text using this code. var rectangle = new Rectangle(600, 100, Color.YELLOWGREEN); var text = new Text(0, rectangle.getHeight() * 0.5, "EXAMPLING"); text.setFont(Font.loadFont(getClass().getResourceAsStream("GT Pressura Mono Regular Regular.ttf"), rectangle.getHeight() * 1.3)); text.setBoundsType(TextBoundsType.VISUAL); var max_width = rectangle.getWidth() - 7; for (var i = text.getFont().getSize() - 0.5; text.getLayoutBounds().getWidth() >= max_width; i -= 0.5) { text.setFont(new Font(text.getFont().getName(), i)); } text.setTextAlignment(TextAlignment.CENTER); text.setTextOrigin(VPos.CENTER); stage.setScene(new Scene(new Pane(rectangle, text))); stage.show(); However, I still don't know how to solve the positioning(2.).
Rectangle size binding to Scene size - JavaFX
I'm trying to write a simple JavaFX application that contains a graphical representation of an NxM matrix (default 100 x 100). I would like to bind the matrix size to the Scene size, so when I resize the app window, the matrix follows it and keeps the aspect ratio. The binding works seamlessly for small matrices (for example 10x10), but when the matrix gets bigger (for example 50x50) and rectangles get smaller so they can fit my screen, the binding process becomes discontinuous. The matrix sometimes even gets bigger than window size (can't see all cells/rectangles) and matrix size seems like it's switching between integer values. What I did is created a StackPane as a root Node inside the Scene and a GridPane as a child Node to the StackPane. I filled the GridPane with an NxM Rectangle matrix. The code follows: public class GridPaneExample extends Application { public static void main(String[] args) { launch(args); } #Override public void start(Stage primaryStage) throws Exception { int n = 100, m = 100; // matrix size double rectw = 5, recth = 5; // size of each rectangle StackPane root = new StackPane(); Scene scene = new Scene(root, (m + 2) * rectw, (n + 2) * recth, Color.DARKOLIVEGREEN); // creating a scene with // a frame around matrix primaryStage.setScene(scene); primaryStage.sizeToScene(); GridPane gp = new GridPane(); root.getChildren().add(gp); Rectangle[][] rects = new Rectangle[n][m]; for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) { rects[i][j] = new Rectangle(); rects[i][j].setWidth(rectw); rects[i][j].setHeight(recth); rects[i][j].setFill(Color.ANTIQUEWHITE); rects[i][j].setStrokeType(StrokeType.INSIDE); rects[i][j].setStrokeWidth(0.2); rects[i][j].setStroke(Color.GREY); rects[i][j].widthProperty().bind(gp.widthProperty().divide(m)); rects[i][j].heightProperty().bind(gp.heightProperty().divide(n)); GridPane.setRowIndex(rects[i][j], i); GridPane.setColumnIndex(rects[i][j], j); gp.getChildren().add(rects[i][j]); } rects[0][0].setFill(Color.RED); rects[0][m - 1].setFill(Color.RED); rects[n - 1][0].setFill(Color.RED); rects[n - 1][m - 1].setFill(Color.RED); StackPane.setAlignment(gp, Pos.TOP_CENTER); gp.minHeightProperty().bind(scene.heightProperty().subtract(2 * recth)); gp.minWidthProperty().bind(scene.widthProperty().subtract(2 * rectw)); gp.maxHeightProperty().bind(scene.heightProperty().subtract(2 * recth)); gp.maxWidthProperty().bind(scene.widthProperty().subtract(2 * rectw)); root.setLayoutY(recth); primaryStage.show(); primaryStage.setMinHeight(primaryStage.getHeight()); primaryStage.setMinWidth(primaryStage.getWidth()); } } Here are some screenshots of the program showing different sized matrices. A 10 x 10 matrix app when started (rectangle size is 20x20): https://i.imgur.com/I2vMLSD.png A 10 x 10 matrix app when stretched: https://i.imgur.com/9rR3e0g.png A 100 x 100 matrix app when started (rectangle size is 5x5): https://i.imgur.com/1KCKg6W.png A 100 x 100 matrix app when stretched a bit, see how the matrix gets out of bounds: https://i.imgur.com/pG8DxXA.png So, is there a way to make this binding experience smooth and consistent, maybe by using something else other than Rectangles? Edit: A weird thing happens. When I remove the rectangle binding lines from code and launch the app with 100x100 matrix, the matrix looks as it should. The program prints out the size of the GridPane and red rectangles. They are 500x500 and 5x5 respectively, as expected. However, when I include rectangle binding (as shown in the code), the matrix gets out of bounds, as in third photo attached, but the program still prints out the same dimensions of GridPane and rectangles.
As commented earlier, this problem is fixable by calling Node#setSnapToPixel(false) on the GridPane. Explanation This problem occurs when Nodes become so small that a single pixel becomes too big for accurate display. JavaFX uses a 'pixel snapping' feature by default to ensure a 'clear/clean' look. This would be mainly vissible with borders getting 'fussy/blurry' when not used. In the backend JavaFX has defined the methods snapSpace, snapSize and snapPosition, which correspond respectively to Math.round, Math.ceil and Math.round. This means that if a Node has a width of 2.5 with a 1 pixel border, it should have the size of (1 + 2.5 + 1) = 4.5, wich ceiled wil result in 5.0. If you have 100 elements getting a 0.5 width bonus it will result in 50 pixels overflow! Setting 'snapToPixel' to false means it wil ignore the mechanism above, and use mixed colors per pixel. If you would look closely this 'blurr' effect is noticable on borders. But it is worth the tradeoff for the perfect layout IMO.
Here is a sample app I wrote a long time ago which does a similar kind of thing. It uses resizable nodes within a GridPane with a listener on the layoutBoundsProperty to pick the size of the resizable nodes. Not sure if it is really what you are interested in (the approach you have in your question, with some fixes or tweaks, is probably fine for what you wish to accomplish), but it might be worth taking a look at for an alternate approach if needed. Relevant code is for the swatch node in the ColorChooser constructor from the sample code below (sorry it's a bit long, it was written for a different purpose but seems close enough that it might be worth posting in its entirety): import javafx.application.Application; import javafx.beans.property.*; import javafx.event.*; import javafx.geometry.*; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.stage.StageStyle; /** * Sample application for using the color chooser */ public class ColorChooserSample extends Application { public void start(final Stage stage) throws Exception { // initialize the stage. stage.setTitle("Color Chooser"); stage.initStyle(StageStyle.UTILITY); // create a new color chooser sized to the stage. final String[][] smallPalette = { {"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"}, {"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"}, {"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"}, {"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"}, {"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"}, {"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"}, }; final ColorChooser colorChooser = new ColorChooser(smallPalette); // to use the full web palette, just use the default constructor. // final ColorChooser colorChooser = new ColorChooser(); final Scene scene = new Scene(colorChooser, 600, 500); // show the stage. stage.setScene(scene); stage.show(); // monitor the color chooser's chosen color and respond to it. colorChooser.chosenColorProperty().addListener((observableValue, oldColor, newColor) -> System.out.println("Chose: " + colorChooser.getChosenColorName() + " " + colorChooser.getChosenColor()) ); } public static void main(String[] args) { launch(args); } } /** * A Color Chooser Component - allows the user to select a color from a palette. */ class ColorChooser extends VBox { private final double GOLDEN_RATIO = 1.618; private final double MIN_TILE_SIZE = 5; private final double nColumns; private final double nRows; /** * The color the user has selected or the default initial color (the first color in the palette) */ private final ReadOnlyObjectWrapper<Color> chosenColor = new ReadOnlyObjectWrapper<Color>(); public Color getChosenColor() { return chosenColor.get(); } public ReadOnlyObjectProperty<Color> chosenColorProperty() { return chosenColor.getReadOnlyProperty(); } /** * Friendly name for the chosen color */ private final ReadOnlyObjectWrapper<String> chosenColorName = new ReadOnlyObjectWrapper<String>(); public String getChosenColorName() { return chosenColorName.get(); } /** * Preferred size for a web palette tile */ private DoubleProperty prefTileSize = new SimpleDoubleProperty(MIN_TILE_SIZE); /** * A palette of colors from http://docs.oracle.com/javafx/2.0/api/javafx/scene/doc-files/cssref.html#typecolor */ private static final String[][] webPalette = { {"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"}, {"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"}, {"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"}, {"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"}, {"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"}, {"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"}, {"darkgray", "#a9a9a9"}, {"darkgreen", "#006400"}, {"darkgrey", "#a9a9a9"}, {"darkkhaki", "#bdb76b"}, {"darkmagenta", "#8b008b"}, {"darkolivegreen", "#556b2f"}, {"darkorange", "#ff8c00"}, {"darkorchid", "#9932cc"}, {"darkred", "#8b0000"}, {"darksalmon", "#e9967a"}, {"darkseagreen", "#8fbc8f"}, {"darkslateblue", "#483d8b"}, {"darkslategray", "#2f4f4f"}, {"darkslategrey", "#2f4f4f"}, {"darkturquoise", "#00ced1"}, {"darkviolet", "#9400d3"}, {"deeppink", "#ff1493"}, {"deepskyblue", "#00bfff"}, {"dimgray", "#696969"}, {"dimgrey", "#696969"}, {"dodgerblue", "#1e90ff"}, {"firebrick", "#b22222"}, {"floralwhite", "#fffaf0"}, {"forestgreen", "#228b22"}, {"fuchsia", "#ff00ff"}, {"gainsboro", "#dcdcdc"}, {"ghostwhite", "#f8f8ff"}, {"gold", "#ffd700"}, {"goldenrod", "#daa520"}, {"gray", "#808080"}, {"green", "#008000"}, {"greenyellow", "#adff2f"}, {"grey", "#808080"}, {"honeydew", "#f0fff0"}, {"hotpink", "#ff69b4"}, {"indianred", "#cd5c5c"}, {"indigo", "#4b0082"}, {"ivory", "#fffff0"}, {"khaki", "#f0e68c"}, {"lavender", "#e6e6fa"}, {"lavenderblush", "#fff0f5"}, {"lawngreen", "#7cfc00"}, {"lemonchiffon", "#fffacd"}, {"lightblue", "#add8e6"}, {"lightcoral", "#f08080"}, {"lightcyan", "#e0ffff"}, {"lightgoldenrodyellow", "#fafad2"}, {"lightgray", "#d3d3d3"}, {"lightgreen", "#90ee90"}, {"lightgrey", "#d3d3d3"}, {"lightpink", "#ffb6c1"}, {"lightsalmon", "#ffa07a"}, {"lightseagreen", "#20b2aa"}, {"lightskyblue", "#87cefa"}, {"lightslategray", "#778899"}, {"lightslategrey", "#778899"}, {"lightsteelblue", "#b0c4de"}, {"lightyellow", "#ffffe0"}, {"lime", "#00ff00"}, {"limegreen", "#32cd32"}, {"linen", "#faf0e6"}, {"magenta", "#ff00ff"}, {"maroon", "#800000"}, {"mediumaquamarine", "#66cdaa"}, {"mediumblue", "#0000cd"}, {"mediumorchid", "#ba55d3"}, {"mediumpurple", "#9370db"}, {"mediumseagreen", "#3cb371"}, {"mediumslateblue", "#7b68ee"}, {"mediumspringgreen", "#00fa9a"}, {"mediumturquoise", "#48d1cc"}, {"mediumvioletred", "#c71585"}, {"midnightblue", "#191970"}, {"mintcream", "#f5fffa"}, {"mistyrose", "#ffe4e1"}, {"moccasin", "#ffe4b5"}, {"navajowhite", "#ffdead"}, {"navy", "#000080"}, {"oldlace", "#fdf5e6"}, {"olive", "#808000"}, {"olivedrab", "#6b8e23"}, {"orange", "#ffa500"}, {"orangered", "#ff4500"}, {"orchid", "#da70d6"}, {"palegoldenrod", "#eee8aa"}, {"palegreen", "#98fb98"}, {"paleturquoise", "#afeeee"}, {"palevioletred", "#db7093"}, {"papayawhip", "#ffefd5"}, {"peachpuff", "#ffdab9"}, {"peru", "#cd853f"}, {"pink", "#ffc0cb"}, {"plum", "#dda0dd"}, {"powderblue", "#b0e0e6"}, {"purple", "#800080"}, {"red", "#ff0000"}, {"rosybrown", "#bc8f8f"}, {"royalblue", "#4169e1"}, {"saddlebrown", "#8b4513"}, {"salmon", "#fa8072"}, {"sandybrown", "#f4a460"}, {"seagreen", "#2e8b57"}, {"seashell", "#fff5ee"}, {"sienna", "#a0522d"}, {"silver", "#c0c0c0"}, {"skyblue", "#87ceeb"}, {"slateblue", "#6a5acd"}, {"slategray", "#708090"}, {"slategrey", "#708090"}, {"snow", "#fffafa"}, {"springgreen", "#00ff7f"}, {"steelblue", "#4682b4"}, {"tan", "#d2b48c"}, {"teal", "#008080"}, {"thistle", "#d8bfd8"}, {"tomato", "#ff6347"}, {"turquoise", "#40e0d0"}, {"violet", "#ee82ee"}, {"wheat", "#f5deb3"}, {"white", "#ffffff"}, {"whitesmoke", "#f5f5f5"}, {"yellow", "#ffff00"}, {"yellowgreen", "#9acd32"} }; public ColorChooser() { this(webPalette); } public ColorChooser(String[][] colors) { super(); // create a pane for showing info on the chosen color. final HBox colorInfo = new HBox(); final Label selectedColorName = new Label(); HBox.setMargin(selectedColorName, new Insets(2, 0, 2, 10)); colorInfo.getChildren().addAll(selectedColorName); chosenColorName.addListener((observableValue, oldName, newName) -> { if (newName != null) { colorInfo.setStyle("-fx-background-color: " + newName + ";"); selectedColorName.setText(newName); chosenColor.set(Color.web(newName)); } }); // create a color swatch. final GridPane swatch = new GridPane(); swatch.setSnapToPixel(false); // calculate the number of columns and rows based on the number of colors and a golden ratio for layout. nColumns = Math.floor(Math.sqrt(colors.length) * 2 / GOLDEN_RATIO); nRows = Math.ceil(colors.length / nColumns); // create a bunch of button controls for color selection. int i = 0; for (String[] namedColor : colors) { final String colorName = namedColor[0]; final String colorHex = namedColor[1]; // create a button for choosing a color. final Button colorChoice = new Button(); colorChoice.setUserData(colorName); // position the button in the grid. GridPane.setRowIndex(colorChoice, i / (int) nColumns); GridPane.setColumnIndex(colorChoice, i % (int) nColumns); colorChoice.setMinSize(MIN_TILE_SIZE, MIN_TILE_SIZE); colorChoice.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); // add a mouseover tooltip to display more info on the colour being examined. // todo it would be nice to be able to have the tooltip appear immediately on mouseover, but there is no easy way to do this, (file jira feature request?) final Tooltip tooltip = new Tooltip(colorName); tooltip.setStyle("-fx-font-size: 14"); tooltip.setContentDisplay(ContentDisplay.BOTTOM); final Rectangle graphic = new Rectangle(30, 30, Color.web(colorHex)); graphic.widthProperty().bind(prefTileSize.multiply(1.5)); graphic.heightProperty().bind(prefTileSize.multiply(1.5)); tooltip.setGraphic(graphic); colorChoice.setTooltip(tooltip); // color the button appropriately and change it's hover functionality (doing some of this in a css sheet would be better). final String backgroundStyle = "-fx-background-color: " + colorHex + "; -fx-background-insets: 0; -fx-background-radius: 0;"; colorChoice.setStyle(backgroundStyle); colorChoice.setOnMouseEntered(mouseEvent -> { final String borderStyle = "-fx-border-color: ladder(" + colorHex + ", whitesmoke 49%, darkslategrey 50%); -fx-border-width: 2;"; colorChoice.setStyle(backgroundStyle + borderStyle); }); colorChoice.setOnMouseExited(mouseEvent -> { final String borderStyle = "-fx-border-width: 0; -fx-border-insets: 2;"; colorChoice.setStyle(backgroundStyle + borderStyle); }); // choose the color when the button is clicked. colorChoice.setOnAction(new EventHandler<ActionEvent>() { #Override public void handle(ActionEvent actionEvent) { chosenColorName.set((String) colorChoice.getUserData()); } }); // add the color choice to the swatch selection. swatch.getChildren().add(colorChoice); i++; } // select the first color in the chooser. ((Button) swatch.getChildren().get(0)).fire(); // layout the color picker. getChildren().addAll(swatch, colorInfo); VBox.setVgrow(swatch, Priority.ALWAYS); setStyle("-fx-background-color: black; -fx-font-size: 16;"); swatch.layoutBoundsProperty().addListener((observableValue, oldBounds, newBounds) -> { prefTileSize.set(Math.max(MIN_TILE_SIZE, Math.min(newBounds.getWidth() / nColumns, newBounds.getHeight() / nRows))); for (Node child : swatch.getChildrenUnmodifiable()) { Control tile = (Control) child; final double margin = prefTileSize.get() / 10; tile.setPrefSize(prefTileSize.get() - 2 * margin, prefTileSize.get() - 2 * margin); GridPane.setMargin(child, new Insets(margin)); } }); } }
Can we create partially colored text in Processing?
I want to learn to create partially colored text/String in processing(java) like below example. It can be 10% white 90% black. or any ratio for any two color. I would like to learn how to do it in processing.
Option 1: In Processing Render your text into a PGraphics object and then iterate over the pixels[] of the object to change the colour of a proportion of the text. In the example below, the colour ratio will not be correct unless you create a PGraphics object of the exact dimensions of the text - I'm not sure whether there is a programmatic way to determine the required dimensions. (As a result, I'm using a ratio of 0.6 to recolour the top ~50% of the text in the example). Furthermore, noSmooth() has been called on the object as anti-aliasing will create pixels that aren't quite the original text colour, which makes replacing them more complicated than simply checking for equality (==). import processing.core.PApplet; import processing.core.PGraphics; public class Text extends PApplet { PGraphics text; public static void main(String[] args) { PApplet.main(Text.class); } #Override public void settings() { size(400, 400); } #Override public void setup() { makeText(); } #Override public void draw() { background(55); image(text, 100, 100); } public void makeText() { final int orange = color(255,165,0); final int yellow = color(255,255,0); final float ratio = 0.6f; text = createGraphics(150, 60); text.noSmooth(); text.beginDraw(); text.fill(orange); text.textSize(60); text.textAlign(LEFT, TOP); text.loadPixels(); text.text("TEXT", 0, 0); text.endDraw(); for (int pixel = 0; pixel < (text.pixels.length * ratio); pixel++) { if (text.pixels[pixel] == orange) { text.pixels[pixel] = yellow; } } } } Result: Option 2: With JavaFX This is a hacky method, but it gives better results since the text is anti-aliased. This method requires that your sketch be in FX2D rendering mode, which can be prescribed in the size() call. Expose the stackPane belonging to the PApplet (this is where the JavaFX text object will be added): Canvas canvas = (Canvas) ((PSurfaceFX) getSurface()).getNative(); StackPane p = (StackPane) canvas.getParent(); Create a JavaFX text object. I am using a CSS style (a linear gradient with immediate cutoff) for the partially-coloured effect. javafx.scene.text.Text t = new javafx.scene.text.Text("TEXT"); t.setCache(true); t.setFont(Font.font(null, FontWeight.NORMAL, 60)); t.setStyle("-fx-fill:linear-gradient( from 100.0% 100.0% to 100.0% 0.0%, rgb(255, 165, 0) 0.5," + "rgb(255, 255, 0)" +" 0.5);"); Finally, add the text to the PApplet's stage stackpane (which was exposed earlier): p.getChildren().add(t); Result (note the anti-aliasing): Also note that you'lll need to use t.setVisible() to toggle visibility since this text element is entirely separate from Processing's drawing canvas.
Texturing an Imported Triangle Mesh Javafx
I created a design in blender exported to STL and used the StlModelImporterJFX to import it into my JavaFX program and run it. Everything runs fine, the application works, there is just one thing missing...texture, so basically, I want to take my imported mesh and create an image as seen below for a smaller design. Is there any program or algorithm that I can use to create an image such as that below that I can later edit manually and use as a texture for the entire Triangle Mesh? Also on a side note is it possible to edit this image live in the program and swap out colours while running? Sorry if this is poorly worded, if you want any clarification, I can provide it.
When you import a 3D model with a third-party 3D importer you have less control of the resulting TriangleMesh. If you want to provide texture features to your model you'll have to edit the exported file and add the texture coordinates, which is not the best approach. But if you could generate the mesh from scratch, you could easily apply textures over it. This question shows how you can define the texture coordinates and uses the same net image you have to provide the texture of an icosahedron. Based on the answer on that question, the texture can be defined without an actual image, just with a palette of colors. And you can easily change those on runtime, i.e. when you click on one face you can change the color on that face. The Fxyz library makes use of a TexturedMesh, designed to easily apply textures to 3D shapes. You can find many primitives there, like the icosahedron. This question shows the result of different texture modes over an icosahedron. This short snippet shows how you can apply a texture over faces, and change it on runtime: private int numColors = 10; #Override public void start(Stage primaryStage) { PerspectiveCamera camera = new PerspectiveCamera(true); camera.setTranslateZ(-5); IcosahedronMesh icoFaces = new IcosahedronMesh(100, 0); icoFaces.setTextureModeFaces(numColors); icoFaces.getTransforms().addAll(new Rotate(20, Rotate.X_AXIS), new Rotate(-10, Rotate.Y_AXIS)); final Group group = new Group(icoFaces); Scene scene = new Scene(group, 600, 400, true, SceneAntialiasing.BALANCED); scene.setCamera(camera); primaryStage.setScene(scene); primaryStage.setTitle(("Icosahedron - FXyz3D")); primaryStage.show(); icoFaces.setOnMouseClicked(e -> { ObservableFaceArray faces = ((TriangleMesh) icoFaces.getMesh()).getFaces(); int selectedFace = e.getPickResult().getIntersectedFace(); int colorId = faces.get(6 * selectedFace + 1); int newColorId = colorId + 1 >= numColors ? 0 : colorId + 1; faces.set(6 * selectedFace + 1, newColorId); faces.set(6 * selectedFace + 3, newColorId); faces.set(6 * selectedFace + 5, newColorId); }); } Running the application: And after clicking in the frontal green face:
JavaFx 8 - Scaling / zooming ScrollPane relative to mouse position
I need to zoom in / out on a scroll pane, relative to the mouse position. I currently achieve the zooming functionality by wrapping my content in a Group, and scaling the group itself. I create a new Scale object with a custom pivot. (Pivot is set to the mouse position) This works perfectly for where the Group's initial scale is 1.0, however scaling afterwards does not scale in the correct direction - I believe this is because the relative mouse position changes when the Group has been scaled. My code: #Override public void initialize(URL location, ResourceBundle resources) { Delta initial_mouse_pos = new Delta(); anchorpane.setOnScrollStarted(event -> { initial_mouse_pos.x = event.getX(); initial_mouse_pos.y = event.getY(); }); anchorpane.setOnScroll(event -> { double zoom_fac = 1.05; double delta_y = event.getDeltaY(); if(delta_y < 0) { zoom_fac = 2.0 - zoom_fac; } Scale newScale = new Scale(); newScale.setPivotX(initial_mouse_pos.x); newScale.setPivotY(initial_mouse_pos.y); newScale.setX( content_group.getScaleX() * zoom_fac ); newScale.setY( content_group.getScaleY() * zoom_fac ); content_group.getTransforms().add(newScale); event.consume(); }); } private class Delta { double x, y; } How do I get the correct mouse position at different levels of scaling? Is there a completely different way to zooming the ScrollPane that is easier?
This is a scalable, pannable JavaFX ScrollPane : import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; public class ZoomableScrollPane extends ScrollPane { private double scaleValue = 0.7; private double zoomIntensity = 0.02; private Node target; private Node zoomNode; public ZoomableScrollPane(Node target) { super(); this.target = target; this.zoomNode = new Group(target); setContent(outerNode(zoomNode)); setPannable(true); setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); setFitToHeight(true); //center setFitToWidth(true); //center updateScale(); } private Node outerNode(Node node) { Node outerNode = centeredNode(node); outerNode.setOnScroll(e -> { e.consume(); onScroll(e.getTextDeltaY(), new Point2D(e.getX(), e.getY())); }); return outerNode; } private Node centeredNode(Node node) { VBox vBox = new VBox(node); vBox.setAlignment(Pos.CENTER); return vBox; } private void updateScale() { target.setScaleX(scaleValue); target.setScaleY(scaleValue); } private void onScroll(double wheelDelta, Point2D mousePoint) { double zoomFactor = Math.exp(wheelDelta * zoomIntensity); Bounds innerBounds = zoomNode.getLayoutBounds(); Bounds viewportBounds = getViewportBounds(); // calculate pixel offsets from [0, 1] range double valX = this.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth()); double valY = this.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight()); scaleValue = scaleValue * zoomFactor; updateScale(); this.layout(); // refresh ScrollPane scroll positions & target bounds // convert target coordinates to zoomTarget coordinates Point2D posInZoomTarget = target.parentToLocal(zoomNode.parentToLocal(mousePoint)); // calculate adjustment of scroll position (pixels) Point2D adjustment = target.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1)); // convert back to [0, 1] range // (too large/small values are automatically corrected by ScrollPane) Bounds updatedInnerBounds = zoomNode.getBoundsInLocal(); this.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth())); this.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight())); } }
Did you try to remove the setOnScrollStarted-event and move its content to the setOnScroll-event? Doing so reduces the need of your extra Delta-class and the computations of your mouse-positions are always on par with the current zoom factor. I implemented the same thing and it works the way you are describing it. Somehow like this: #Override public void initialize(URL location, ResourceBundle resources) { anchorpane.setOnScroll(event -> { double zoom_fac = 1.05; if(delta_y < 0) { zoom_fac = 2.0 - zoom_fac; } Scale newScale = new Scale(); newScale.setPivotX(event.getX); newScale.setPivotY(event.getY); newScale.setX( content_group.getScaleX() * zoom_fac ); newScale.setY( content_group.getScaleY() * zoom_fac ); content_group.getTransforms().add(newScale); event.consume(); }); }
I believe this is a duplicate of this question which involves the same concepts at work. If you don't really care if it zooms relative to your mouse and just prefer it zoom in the center look at this question. If you need any more help comment below.
Assuming you want to have the following zoom behavior: When the mouse wheel is pushed forward/backward the object under the cursor will be scaled up/down and the area under the cursor is now centered within the zooming area. Eg. pushing the wheel forward while pointing at a place left from the center of the zooming area results in a 'up-scale and move right' action. The scaling thing is as simple as you have already done so far. The tricky part is the move action. There are some problem you have to consider within your calculations: You have to calculate the difference from the center and the position of the cursor. You can calculate this value by subtracting the center point (cp) from the mouse position (mp). If your zoom level is 1 and you point 50px left from the center you want to move your object 50px to the right, because 1px of your screen corresponds to one 1px of your object (picture). But if you doubled the size of your object, than 2 screen pixel are equal to on object pixel. You have to consider this when moving the object, because the translating part is always done before the scaling part. In other words you are moving your object in original size and the scaling is only the second independent step. How is the scaling done? In JavaFX you simply set some scale-properties and JavaFX does the rest for you. But what does it exactly? The important thing to know is, where the fixed point is while zooming the object. If you scale an object there will be one point which stays fixed at its position while all other points are moving towards this point or moving way from it. As the documentation of JavaFX says the center of the zoomed object will be the fixed point. Defines the factor by which coordinates are scaled about the center of the object along the X axis of this Node. That means you have to ensure that your visual center point is equal to the one JavaFX uses while scaling you object. You can achieve this if you wrap your object within a container. Now zoom the container instead of the object and position the object within the container to fit your needs. I hope this helps. If you need more help please offer a short working example project.