Skip to content

Commit ba36898

Browse files
committed
fixed glyph atlas snapshot scaling for high dpi screens
1 parent c80f50c commit ba36898

1 file changed

Lines changed: 37 additions & 17 deletions

File tree

chartfx-chart/src/main/java/io/fair_acc/chartfx/axes/spi/GlyphAtlas.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,17 @@
44
import io.fair_acc.chartfx.ui.css.TextStyle;
55
import io.fair_acc.chartfx.ui.css.TextStyle.TextBounds;
66
import io.fair_acc.chartfx.utils.PropUtil;
7-
import javafx.beans.binding.Bindings;
8-
import javafx.beans.binding.BooleanBinding;
97
import javafx.beans.property.BooleanProperty;
108
import javafx.beans.property.LongProperty;
119
import javafx.beans.property.SimpleBooleanProperty;
1210
import javafx.beans.property.SimpleLongProperty;
13-
import javafx.css.StyleOrigin;
1411
import javafx.geometry.VPos;
1512
import javafx.scene.Scene;
1613
import javafx.scene.SnapshotParameters;
1714
import javafx.scene.canvas.Canvas;
1815
import javafx.scene.canvas.GraphicsContext;
19-
import javafx.scene.image.Image;
2016
import javafx.scene.image.ImageView;
17+
import javafx.scene.image.WritableImage;
2118
import javafx.scene.layout.VBox;
2219
import javafx.scene.paint.Color;
2320
import 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

Comments
 (0)