44import io .fair_acc .chartfx .ui .css .TextStyle ;
55import io .fair_acc .chartfx .ui .css .TextStyle .TextBounds ;
66import io .fair_acc .chartfx .utils .PropUtil ;
7- import javafx .beans .binding .Bindings ;
8- import javafx .beans .binding .BooleanBinding ;
97import javafx .beans .property .BooleanProperty ;
108import javafx .beans .property .LongProperty ;
119import javafx .beans .property .SimpleBooleanProperty ;
1210import javafx .beans .property .SimpleLongProperty ;
13- import javafx .css .StyleOrigin ;
1411import javafx .geometry .VPos ;
1512import javafx .scene .Scene ;
1613import javafx .scene .SnapshotParameters ;
1714import javafx .scene .canvas .Canvas ;
1815import javafx .scene .canvas .GraphicsContext ;
19- import javafx .scene .image .Image ;
2016import javafx .scene .image .ImageView ;
17+ import javafx .scene .image .WritableImage ;
2118import javafx .scene .layout .VBox ;
2219import javafx .scene .paint .Color ;
2320import javafx .scene .text .TextAlignment ;
@@ -36,7 +33,10 @@ public class GlyphAtlas {
3633
3734 public GlyphAtlas (TextStyle style ) {
3835 this .style = style ;
39- PropUtil .initAndRunOnChange (() -> needsScaling .set (style .getRotate () % 90 != 0 ), style .rotateProperty ());
36+
37+ // TODO: do more tests to see whether it's actually worth scaling up for rotations
38+ // PropUtil.initAndRunOnChange(() -> needsScaling.set(style.getRotate() % 90 != 0), style.rotateProperty());
39+
4040 var listener = StyleUtil .incrementOnChange (invalidCounter );
4141 listener .accept (needsScaling );
4242 listener .accept (style .fontProperty ());
@@ -124,9 +124,12 @@ protected void bake() {
124124 double currentX = minPadding ;
125125 double maxHeight = 0 ;
126126
127- // A 1 to 1 pixel mapping is best, but we need more
128- // resolution for subpixel operations
129- final double scale = (screenScale == 1 && style .getRotate () == 0 ) ? 1 : 2 * screenScale ;
127+ // A 1 to 1 pixel mapping is best, but higher resolution may help with rotations and subpixel ops
128+ final double canvasScale = needsScaling .get () ? 2 : 1 ;
129+ final double xRenderScale = Screen .getPrimary ().getOutputScaleX (); // logical to screen coords
130+ final double yRenderScale = Screen .getPrimary ().getOutputScaleY ();
131+ final double xImageScale = xRenderScale * canvasScale ; // logical to image coords
132+ final double yImageScale = yRenderScale * canvasScale ;
130133
131134 // Determine char sizes
132135 for (char c = MIN_CHAR ; c <= MAX_CHAR ; c ++) {
@@ -155,11 +158,13 @@ protected void bake() {
155158 double height = padToAlignment (2 * minPadding + maxHeight );
156159
157160 // Create atlas
158- var canvas = new Canvas (scale * currentX , scale * height );
161+ var canvasWidth = canvasScale * currentX ;
162+ var canvasHeight = canvasScale * height ;
163+ var canvas = new Canvas (canvasWidth , canvasHeight );
159164 var gc = canvas .getGraphicsContext2D ();
160165 style .copyStyleTo (gc );
161166 gc .setTextAlign (TextAlignment .RIGHT );
162- gc .scale (scale , scale ); // better quality than scaling the snapshot
167+ gc .scale (canvasScale , canvasScale ); // requests a larger font -> better quality than scaling bitmap
163168 gc .setTextAlign (TextAlignment .LEFT );
164169 gc .setTextBaseline (VPos .TOP );
165170
@@ -170,25 +175,41 @@ protected void bake() {
170175 // Note: might look odd, but enable at will
171176 gc .strokeText (String .valueOf (c ), coords .atlasX , coords .atlasY );
172177 }
173- coords .atlasX = Math .floor (coords .atlasX * scale );
174- coords .atlasY = Math .floor (coords .atlasY * scale );
175- coords .atlasW = coords .atlasW * scale ;
176- coords .atlasH = coords .atlasH * scale ;
178+ coords .atlasX = Math .floor (coords .atlasX * xImageScale );
179+ coords .atlasY = Math .floor (coords .atlasY * yImageScale );
180+ coords .atlasW = coords .atlasW * xImageScale ;
181+ coords .atlasH = coords .atlasH * yImageScale ;
177182 }
178183
184+ // Snapshots default to rendering logical pixels into physical pixels,
185+ // so we need to scale the input to actually get a copy of screen pixels.
186+
179187 SnapshotParameters params = new SnapshotParameters ();
188+ params .setTransform (Transform .scale (xRenderScale , yRenderScale ));
180189 params .setFill (Color .TRANSPARENT );
190+ params .setDepthBuffer (false );
191+
192+ // Do the snapshot
193+ atlas = new WritableImage ((int ) Math .ceil (canvasWidth * xRenderScale ), (int ) Math .ceil (canvasHeight * yRenderScale ));
181194 atlas = canvas .snapshot (params , null );
182195
183196 if (showDebug ) {
197+ System .out .println ("GlyphAtlas: " +
198+ canvas .getWidth () + "x" + canvas .getHeight () + " -> "
199+ + atlas .getWidth () + "x" + atlas .getHeight ());
184200
185201 var showCanvas = new Stage ();
186202 showCanvas .setScene (new Scene (new VBox (canvas )));
187203 showCanvas .setTitle (canvas .getWidth () + "x" + canvas .getHeight ());
188204 showCanvas .show ();
189205
206+ // The image looks blurry when double-scaled-up, so we need to counter one scaling
190207 var showImage = new Stage ();
191- showImage .setScene (new Scene (new VBox (new ImageView (atlas ))));
208+ var view = new ImageView (atlas );
209+ view .setFitHeight (canvas .getHeight ());
210+ view .setFitWidth (canvas .getWidth ());
211+ var root = new VBox (view );
212+ showImage .setScene (new Scene (root ));
192213 showImage .setTitle (atlas .getWidth () + "x" + atlas .getHeight ());
193214 showImage .show ();
194215
@@ -206,12 +227,11 @@ protected double padToAlignment(double currentX) {
206227 protected final TextStyle style ;
207228 protected boolean valid = false ;
208229
209- private Image atlas ;
230+ private WritableImage atlas ;
210231 private final GlyphRegion [] glyphMap = new GlyphRegion [128 ];
211232 private final TextBounds bounds = new TextBounds ();
212233 private static final int MIN_CHAR = 32 ;
213234 private static final int MAX_CHAR = 126 ;
214- private final double screenScale = Screen .getPrimary ().getOutputScaleX ();
215235 private final double minPadding = 8 ;
216236 private static final double alignment = 8 ;
217237 private final BooleanProperty needsScaling = new SimpleBooleanProperty (this , "needsScaling" , false );
0 commit comments