Skip to content

Commit

Permalink
Improved canvas HDPI management (introduced pixelDensityProperty)
Browse files Browse the repository at this point in the history
  • Loading branch information
salmonb committed Jan 24, 2024
1 parent 8b90ba1 commit e508bb7
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 84 deletions.
6 changes: 6 additions & 0 deletions webfx-kit/webfx-kit-gwt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@
<version>0.1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>dev.webfx</groupId>
<artifactId>webfx-kit-util</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>dev.webfx</groupId>
<artifactId>webfx-platform-console</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@
import dev.webfx.kit.launcher.spi.FastPixelReaderWriter;
import dev.webfx.kit.launcher.spi.impl.base.WebFxKitLauncherProviderBase;
import dev.webfx.kit.mapper.WebFxKitMapper;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwt.html.CanvasElementHelper;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwt.html.HtmlNodePeer;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwt.util.DragboardDataTransferHolder;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwt.util.HtmlFonts;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwt.util.HtmlUtil;
import dev.webfx.kit.util.properties.FXProperties;
import dev.webfx.platform.console.Console;
import dev.webfx.platform.util.Strings;
import dev.webfx.platform.util.collection.Collections;
import dev.webfx.platform.util.function.Factory;
import elemental2.dom.*;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
Expand Down Expand Up @@ -148,12 +153,33 @@ public GraphicsContext getGraphicsContext2D(Canvas canvas, boolean willReadFrequ
return WebFxKitMapper.getGraphicsContext2D(canvas, willReadFrequently);
}

private final HTMLCanvasElement canvas = HtmlUtil.createElement("canvas");
// HDPI management

@Override
public DoubleProperty canvasPixelDensityProperty(Canvas canvas) {
String key = "webfx-canvasPixelDensityProperty";
DoubleProperty canvasPixelDensityProperty = (DoubleProperty) canvas.getProperties().get(key);
if (canvasPixelDensityProperty == null) {
canvas.getProperties().put(key, canvasPixelDensityProperty = new SimpleDoubleProperty(getDefaultCanvasPixelDensity()));
// Applying an immediate mapping between the JavaFX and HTML canvas, otherwise the default behaviour of the
// WebFX mapper (which is to postpone and process the mapping in the next animation frame) wouldn't work for
// canvas. The application will indeed probably draw in the canvas just after it is initialized (and sized).
// If we were to wait for the mapper to resize the canvas in the next animation frame, it would be too late.
HTMLCanvasElement canvasElement = (HTMLCanvasElement) ((HtmlNodePeer) canvas.getOrCreateAndBindNodePeer()).getElement();
FXProperties.runNowAndOnPropertiesChange(() ->
CanvasElementHelper.resizeCanvasElement(canvasElement, canvas),
canvas.widthProperty(), canvas.heightProperty(), canvasPixelDensityProperty);

}
return canvasPixelDensityProperty;
}

private final HTMLCanvasElement MEASURE_CANVAS = HtmlUtil.createElement("canvas");


@Override
public Bounds measureText(String text, Font font) {
JavaScriptObject textMetrics = getTextMetrics(canvas, text, HtmlFonts.getHtmlFontDefinition(font));
JavaScriptObject textMetrics = getTextMetrics(MEASURE_CANVAS, text, HtmlFonts.getHtmlFontDefinition(font));
return new BoundingBox(0, 0, getJsonWidth(textMetrics), getJsonHeight(textMetrics));
}

Expand Down Expand Up @@ -191,7 +217,8 @@ public ObservableList<Font> loadingFonts() {
private native JavaScriptObject getTextMetrics(HTMLCanvasElement canvas, String text, String font)/*-{
var context = canvas.getContext("2d");
context.font = font;
return { width: context.measureText(text).width, height: parseFloat(context.font.match(/\d+/)) };
var textMetrics = context.measureText(text);
return { width: textMetrics.width, height: textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent };
}-*/;

private static native double getJsonWidth(JavaScriptObject json)/*-{
Expand Down
6 changes: 6 additions & 0 deletions webfx-kit/webfx-kit-javafxgraphics-peers-gwt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
<version>0.1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>dev.webfx</groupId>
<artifactId>webfx-kit-launcher</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>dev.webfx</groupId>
<artifactId>webfx-kit-util</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package dev.webfx.kit.mapper.peers.javafxgraphics.gwt.html;

import dev.webfx.kit.launcher.WebFxKitLauncher;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwt.shared.HtmlSvgNodePeer;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwt.util.HtmlUtil;
import elemental2.dom.CSSProperties;
import elemental2.dom.CanvasRenderingContext2D;
import elemental2.dom.HTMLCanvasElement;
import elemental2.dom.ImageData;
import javafx.scene.canvas.Canvas;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
Expand Down Expand Up @@ -109,4 +112,50 @@ private static void setImagePeerCanvas(Image image, HTMLCanvasElement canvasElem
image.setPeerCanvasDirty(dirty);
}

// Utility methods to resize a Canvas element

public static void resizeCanvasElement(HTMLCanvasElement canvasElement, Canvas canvas) {
double fxCanvasWidth = canvas.getWidth(), fxCanvasHeight = canvas.getHeight();
// While HTML canvas and JavaFX canvas have an identical size in low-res screens, they differ in HDPI screens
// because JavaFX automatically apply the pixel conversion, while HTML doesn't.
double pixelDensity = WebFxKitLauncher.getCanvasPixelDensity(canvas);
int htmlWidth = (int) (fxCanvasWidth * pixelDensity); // So we apply the density factor to get the hi-res number of pixels.
int htmlHeight = (int) (fxCanvasHeight * pixelDensity);
// Note: the JavaFX canvas size might be 0 initially, but we set a minimal size of 1px for the HTML canvas, the
// reason is that transforms applied on zero-sized canvas are ignored on Chromium browsers (for example applying
// the pixelDensity scale on a zero-sized canvas doesn't change the canvas transform), which would make our
// canvas state snapshot technique below fail.
if (htmlWidth == 0)
htmlWidth = 1;
if (htmlHeight == 0)
htmlHeight = 1;
// It's very important to prevent changing the canvas size when not necessary, because resetting an HTML canvas
// size has these 2 serious consequences (even with identical value):
// 1) the canvas is erased
// 2) the context state is reset (including transforms, such as the initial pixel density on HDPI screens)
boolean htmlSizeHasChanged = canvasElement.width != htmlWidth || canvasElement.height != htmlHeight;
if (htmlSizeHasChanged) {
// Getting the 2D context but only if already created (we don't want to initialize a 2D context if the
// canvas will finally be used for WebGL in the application code - because the context type (2D or WebGL)
// can't be changed once initialized). The reason for getting the context is to eventually create a context
// snapshot (but there is no need if the 2D context was not initialized.
CanvasRenderingContext2D ctx = canvas.theContext == null ? null : Context2DHelper.getCanvasContext2D(canvasElement);
// We don't want to lose the context state when resizing the canvas, so we take a snapshot of it before
// resizing, so we can restore it after that.
Context2DStateSnapshot ctxStateSnapshot = ctx == null ? null : new Context2DStateSnapshot(ctx);
// Now we can change the canvas size, as we are prepared
canvasElement.width = htmlWidth; // => erases canvas & reset context sate
canvasElement.height = htmlHeight; // => erases canvas & reset context sate
// We restore the context state that we have stored in the snapshot (this includes the initial pixelDensity scale)
if (ctxStateSnapshot != null)
ctxStateSnapshot.reapply();
// On HDPI screens, we must also set the CSS size, otherwise the CSS size will be taken from the canvas
// size by default, which is not what we want because the CSS size is expressed in low-res and not in HDPI
// pixels like the canvas size, so this would make the canvas appear much too big on the screen.
if (pixelDensity != 1) { // Scaling down the canvas size with CSS size on HDPI screens
canvasElement.style.width = CSSProperties.WidthUnionType.of(fxCanvasWidth + "px");
canvasElement.style.height = CSSProperties.HeightUnionType.of(fxCanvasHeight + "px");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.webfx.kit.mapper.peers.javafxgraphics.gwt.html;

import dev.webfx.kit.launcher.WebFxKitLauncher;
import dev.webfx.kit.mapper.peers.javafxgraphics.base.CanvasPeerBase;
import dev.webfx.kit.mapper.peers.javafxgraphics.base.CanvasPeerMixin;
import elemental2.dom.HTMLCanvasElement;
Expand All @@ -11,8 +12,6 @@
import javafx.scene.transform.Scale;
import javafx.scene.transform.Transform;

import static dev.webfx.kit.mapper.peers.javafxgraphics.gwt.html.HtmlGraphicsContext.*;

/**
* @author Bruno Salmon
*/
Expand All @@ -36,13 +35,13 @@ private HTMLCanvasElement getCanvasElement() {
@Override
public void updateWidth(Number width) {
// Note: probably already updated by HtmlGraphicsContext
updateCanvasElementWidth(getCanvasElement(), width.doubleValue());
CanvasElementHelper.resizeCanvasElement(getCanvasElement(), getNode());
}

@Override
public void updateHeight(Number height) {
// Note: probably already updated by HtmlGraphicsContext
updateCanvasElementHeight(getCanvasElement(), height.doubleValue());
CanvasElementHelper.resizeCanvasElement(getCanvasElement(), getNode());
}

@Override
Expand All @@ -56,6 +55,7 @@ public WritableImage snapshot(SnapshotParameters params, WritableImage image) {
scaleY = scale.getY();
}
}
N canvas = getNode();
int width, height;
if (image != null) {
width = (int) image.getWidth();
Expand All @@ -70,18 +70,18 @@ public WritableImage snapshot(SnapshotParameters params, WritableImage image) {
// render the new version of this image (= this snapshot).
image.setPeerCanvas(null);
} else {
N canvas = getNode();
width = (int) (canvas.getWidth() * scaleX);
height = (int) (canvas.getHeight() * scaleY);
image = new WritableImage(width , height);
}

HTMLCanvasElement canvasToCopy = getCanvasElement();
double pixelDensity = WebFxKitLauncher.getCanvasPixelDensity(canvas);
// Making a rescaled copy of the canvas if necessary before capturing the image
if (scaleX != DPR || scaleY != DPR) {
if (scaleX != pixelDensity || scaleY != pixelDensity) {
HTMLCanvasElement c = CanvasElementHelper.createCanvasElement(width, height);
// Note: wrong Elemental2 signature. Correct signature = drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
Context2DHelper.getCanvasContext2D(c).drawImage(canvasToCopy, 0, 0, width * DPR, height * DPR, 0, 0, width, height);
Context2DHelper.getCanvasContext2D(c).drawImage(canvasToCopy, 0, 0, width * pixelDensity, height * pixelDensity, 0, 0, width, height);
canvasToCopy = c;
}
ImageData imageData = ImageDataHelper.captureCanvasImageData(canvasToCopy, width, height);
Expand Down
Loading

0 comments on commit e508bb7

Please sign in to comment.