从 JavaFX 中擦除抗锯齿形状 Canvas
Erasing Antialiased Shapes from a JavaFX Canvas
我继承了一个模拟程序来扩展新功能。最初是使用 AWT 图形库作为 Applet 编写的。在添加新功能之前,我想使程序适应桌面并使用 JavaFX 而不是 AWT。
模拟每秒数十次绘制数百或数千个对象,然后擦除它们并在新位置重新绘制它们,有效地为它们设置动画。我正在为 UI 的那部分使用 Canvas 对象。擦除是通过用背景颜色重新绘制对象来完成的。我所看到的是擦除对象是不完整的。然而,一种“光环”被抛在了后面。
下面的程序说明了这个问题。单击“绘制”按钮会导致它使用前景色在 Canvas 上绘制几百个圆圈。绘制后,再次单击该按钮将通过在背景颜色中重新绘制圆圈来擦除圆圈。 draw/erase 的多个循环将构建可见的“幽灵”图像背景。
package com.clartaq.antialiasingghosts;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
static final int NUM_CIRCLES = 500;
static final int CIRCLE_DIAMETER = 10;
static final double PANEL_WIDTH = 75.0;
static final double PANEL_HEIGHT = 40.0;
static final Color FG_COLOR = Color.rgb(10, 0, 200);
static final Color BG_COLOR = Color.rgb(255, 255, 255);
static final double BUTTON_WIDTH = 50.0;
GraphicsContext gc;
Random rand = new Random();
double[] px = new double[NUM_CIRCLES];
double[] py = new double[NUM_CIRCLES];
void randomizeParticlePositions() {
for (int i = 0; i < NUM_CIRCLES; i++) {
px[i] = rand.nextDouble() * PANEL_WIDTH;
py[i] = rand.nextDouble() * PANEL_HEIGHT;
}
}
void drawCircles(Color color) {
gc.setFill(color);
for (int i = 0; i < NUM_CIRCLES; i++) {
var screenX = px[i] * CIRCLE_DIAMETER;
var screenY = py[i] * CIRCLE_DIAMETER;
gc.fillOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
}
@Override
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
stage.setTitle("AntiAliasingGhosts -- erasing objects leaves ghosts in JavaFX");
Label versionLabel = new Label("JavaFX " + javafxVersion
+ ", running on Java " + javaVersion + ".");
double canvasWidth = (PANEL_WIDTH * CIRCLE_DIAMETER);
double canvasHeight = (PANEL_HEIGHT * CIRCLE_DIAMETER);
Canvas canvasRef = new Canvas(canvasWidth, canvasHeight);
gc = canvasRef.getGraphicsContext2D();
Button deBtn = new Button("Draw");
deBtn.setPrefWidth(BUTTON_WIDTH);
deBtn.setOnAction(e -> {
String txt = deBtn.getText();
switch (txt) {
case "Draw" -> {
randomizeParticlePositions();
drawCircles(FG_COLOR);
deBtn.setText("Erase");
}
case "Erase" -> {
drawCircles(BG_COLOR);
deBtn.setText("Draw");
}
default -> Platform.exit();
}
});
Button exBtn = new Button("Exit");
exBtn.setPrefWidth(BUTTON_WIDTH);
exBtn.setOnAction(e -> Platform.exit());
TilePane tp = new TilePane();
tp.setAlignment(Pos.CENTER);
tp.setHgap(10);
tp.getChildren().addAll(deBtn, exBtn);
VBox root = new VBox();
root.setPadding(new Insets(7));
root.setSpacing(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(versionLabel, canvasRef, tp);
StackPane sp = new StackPane(root);
BackgroundFill bf = new BackgroundFill(BG_COLOR, CornerRadii.EMPTY, Insets.EMPTY);
Background bg = new Background(bf);
sp.setBackground(bg);
Scene scene = new Scene(sp, 640.0, 480.0);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
我在擦除的时候把圆圈的直径扩大2个像素,就能得到很好的擦除效果。当然,这也会影响附近的形状。
另外,用fillRect方法擦除整个Canvas似乎是合理的,但这意味着如果要重新绘制任何东西都必须重新绘制。我想可以通过擦除和重新绘制 Canvas 的一小部分来优化重新绘制,但如果没有必要,我不想这样做。
程序显示的放大部分显示它确实是抗锯齿效果。用SceneAntialiasing.DISABLED参数构造Scene好像没有什么效果。
尝试按照 this question 中的建议关闭图像平滑没有帮助。
是否可以通过在背景颜色中重新绘制来擦除在 Canvas 上绘制的单个形状?
我正在使用 Java 17.0.1、JavaFX 17.0.1 和 5K Mac 显示器(如果相关)。
为方便起见,请注意GraphicsContext
中fillOval
和strokeOval()
的区别。您可以根据合适的布尔值有条件地擦除 drawCircles()
中的轮廓:
if (stroke) {
gc.setStroke(BG_COLOR);
gc.strokeOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
尝试一些有代表性的形状,例如fillRect
,验证想要的结果。
在我看来,更好的选择是采用 erase -> render 策略。 and here may help you establish whether the approach is scalable to your use-case. See also this related examination 重采样工件的完整示例。
权宜之计,亲测:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
static final int NUM_CIRCLES = 500;
static final int CIRCLE_DIAMETER = 10;
static final double PANEL_WIDTH = 75.0;
static final double PANEL_HEIGHT = 40.0;
static final Color FG_COLOR = Color.rgb(10, 0, 200);
static final Color BG_COLOR = Color.rgb(255, 255, 255);
static final double BUTTON_WIDTH = 50.0;
GraphicsContext gc;
Random rand = new Random();
private boolean stroke;
double[] px = new double[NUM_CIRCLES];
double[] py = new double[NUM_CIRCLES];
void randomizeParticlePositions() {
for (int i = 0; i < NUM_CIRCLES; i++) {
px[i] = rand.nextDouble() * PANEL_WIDTH;
py[i] = rand.nextDouble() * PANEL_HEIGHT;
}
}
void drawCircles(Color color) {
gc.setFill(color);
for (int i = 0; i < NUM_CIRCLES; i++) {
var screenX = px[i] * CIRCLE_DIAMETER;
var screenY = py[i] * CIRCLE_DIAMETER;
gc.fillOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
if (stroke) {
gc.setStroke(BG_COLOR);
gc.strokeOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
}
}
@Override
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
stage.setTitle("AntiAliasingGhosts -- erasing objects leaves ghosts in JavaFX");
Label versionLabel = new Label("JavaFX " + javafxVersion
+ ", running on Java " + javaVersion + ".");
double canvasWidth = (PANEL_WIDTH * CIRCLE_DIAMETER);
double canvasHeight = (PANEL_HEIGHT * CIRCLE_DIAMETER);
Canvas canvasRef = new Canvas(canvasWidth, canvasHeight);
gc = canvasRef.getGraphicsContext2D();
Button deBtn = new Button("Draw");
deBtn.setPrefWidth(BUTTON_WIDTH);
deBtn.setOnAction(e -> {
String txt = deBtn.getText();
switch (txt) {
case "Draw" -> {
randomizeParticlePositions();
drawCircles(FG_COLOR);
deBtn.setText("Erase");
stroke = true;
}
case "Erase" -> {
drawCircles(BG_COLOR);
deBtn.setText("Draw");
stroke = false;
}
default ->
Platform.exit();
}
});
Button exBtn = new Button("Exit");
exBtn.setPrefWidth(BUTTON_WIDTH);
exBtn.setOnAction(e -> Platform.exit());
TilePane tp = new TilePane();
tp.setAlignment(Pos.CENTER);
tp.setHgap(10);
tp.getChildren().addAll(deBtn, exBtn);
VBox root = new VBox();
root.setPadding(new Insets(7));
root.setSpacing(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(versionLabel, canvasRef, tp);
StackPane sp = new StackPane(root);
BackgroundFill bf = new BackgroundFill(BG_COLOR, CornerRadii.EMPTY, Insets.EMPTY);
Background bg = new Background(bf);
sp.setBackground(bg);
Scene scene = new Scene(sp, 640.0, 480.0);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
我继承了一个模拟程序来扩展新功能。最初是使用 AWT 图形库作为 Applet 编写的。在添加新功能之前,我想使程序适应桌面并使用 JavaFX 而不是 AWT。
模拟每秒数十次绘制数百或数千个对象,然后擦除它们并在新位置重新绘制它们,有效地为它们设置动画。我正在为 UI 的那部分使用 Canvas 对象。擦除是通过用背景颜色重新绘制对象来完成的。我所看到的是擦除对象是不完整的。然而,一种“光环”被抛在了后面。
下面的程序说明了这个问题。单击“绘制”按钮会导致它使用前景色在 Canvas 上绘制几百个圆圈。绘制后,再次单击该按钮将通过在背景颜色中重新绘制圆圈来擦除圆圈。 draw/erase 的多个循环将构建可见的“幽灵”图像背景。
package com.clartaq.antialiasingghosts;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
static final int NUM_CIRCLES = 500;
static final int CIRCLE_DIAMETER = 10;
static final double PANEL_WIDTH = 75.0;
static final double PANEL_HEIGHT = 40.0;
static final Color FG_COLOR = Color.rgb(10, 0, 200);
static final Color BG_COLOR = Color.rgb(255, 255, 255);
static final double BUTTON_WIDTH = 50.0;
GraphicsContext gc;
Random rand = new Random();
double[] px = new double[NUM_CIRCLES];
double[] py = new double[NUM_CIRCLES];
void randomizeParticlePositions() {
for (int i = 0; i < NUM_CIRCLES; i++) {
px[i] = rand.nextDouble() * PANEL_WIDTH;
py[i] = rand.nextDouble() * PANEL_HEIGHT;
}
}
void drawCircles(Color color) {
gc.setFill(color);
for (int i = 0; i < NUM_CIRCLES; i++) {
var screenX = px[i] * CIRCLE_DIAMETER;
var screenY = py[i] * CIRCLE_DIAMETER;
gc.fillOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
}
@Override
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
stage.setTitle("AntiAliasingGhosts -- erasing objects leaves ghosts in JavaFX");
Label versionLabel = new Label("JavaFX " + javafxVersion
+ ", running on Java " + javaVersion + ".");
double canvasWidth = (PANEL_WIDTH * CIRCLE_DIAMETER);
double canvasHeight = (PANEL_HEIGHT * CIRCLE_DIAMETER);
Canvas canvasRef = new Canvas(canvasWidth, canvasHeight);
gc = canvasRef.getGraphicsContext2D();
Button deBtn = new Button("Draw");
deBtn.setPrefWidth(BUTTON_WIDTH);
deBtn.setOnAction(e -> {
String txt = deBtn.getText();
switch (txt) {
case "Draw" -> {
randomizeParticlePositions();
drawCircles(FG_COLOR);
deBtn.setText("Erase");
}
case "Erase" -> {
drawCircles(BG_COLOR);
deBtn.setText("Draw");
}
default -> Platform.exit();
}
});
Button exBtn = new Button("Exit");
exBtn.setPrefWidth(BUTTON_WIDTH);
exBtn.setOnAction(e -> Platform.exit());
TilePane tp = new TilePane();
tp.setAlignment(Pos.CENTER);
tp.setHgap(10);
tp.getChildren().addAll(deBtn, exBtn);
VBox root = new VBox();
root.setPadding(new Insets(7));
root.setSpacing(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(versionLabel, canvasRef, tp);
StackPane sp = new StackPane(root);
BackgroundFill bf = new BackgroundFill(BG_COLOR, CornerRadii.EMPTY, Insets.EMPTY);
Background bg = new Background(bf);
sp.setBackground(bg);
Scene scene = new Scene(sp, 640.0, 480.0);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
我在擦除的时候把圆圈的直径扩大2个像素,就能得到很好的擦除效果。当然,这也会影响附近的形状。
另外,用fillRect方法擦除整个Canvas似乎是合理的,但这意味着如果要重新绘制任何东西都必须重新绘制。我想可以通过擦除和重新绘制 Canvas 的一小部分来优化重新绘制,但如果没有必要,我不想这样做。
程序显示的放大部分显示它确实是抗锯齿效果。用SceneAntialiasing.DISABLED参数构造Scene好像没有什么效果。
尝试按照 this question 中的建议关闭图像平滑没有帮助。
是否可以通过在背景颜色中重新绘制来擦除在 Canvas 上绘制的单个形状?
我正在使用 Java 17.0.1、JavaFX 17.0.1 和 5K Mac 显示器(如果相关)。
为方便起见,请注意GraphicsContext
中fillOval
和strokeOval()
的区别。您可以根据合适的布尔值有条件地擦除 drawCircles()
中的轮廓:
if (stroke) {
gc.setStroke(BG_COLOR);
gc.strokeOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
尝试一些有代表性的形状,例如fillRect
,验证想要的结果。
在我看来,更好的选择是采用 erase -> render 策略。
权宜之计,亲测:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
static final int NUM_CIRCLES = 500;
static final int CIRCLE_DIAMETER = 10;
static final double PANEL_WIDTH = 75.0;
static final double PANEL_HEIGHT = 40.0;
static final Color FG_COLOR = Color.rgb(10, 0, 200);
static final Color BG_COLOR = Color.rgb(255, 255, 255);
static final double BUTTON_WIDTH = 50.0;
GraphicsContext gc;
Random rand = new Random();
private boolean stroke;
double[] px = new double[NUM_CIRCLES];
double[] py = new double[NUM_CIRCLES];
void randomizeParticlePositions() {
for (int i = 0; i < NUM_CIRCLES; i++) {
px[i] = rand.nextDouble() * PANEL_WIDTH;
py[i] = rand.nextDouble() * PANEL_HEIGHT;
}
}
void drawCircles(Color color) {
gc.setFill(color);
for (int i = 0; i < NUM_CIRCLES; i++) {
var screenX = px[i] * CIRCLE_DIAMETER;
var screenY = py[i] * CIRCLE_DIAMETER;
gc.fillOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
if (stroke) {
gc.setStroke(BG_COLOR);
gc.strokeOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
}
}
@Override
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
stage.setTitle("AntiAliasingGhosts -- erasing objects leaves ghosts in JavaFX");
Label versionLabel = new Label("JavaFX " + javafxVersion
+ ", running on Java " + javaVersion + ".");
double canvasWidth = (PANEL_WIDTH * CIRCLE_DIAMETER);
double canvasHeight = (PANEL_HEIGHT * CIRCLE_DIAMETER);
Canvas canvasRef = new Canvas(canvasWidth, canvasHeight);
gc = canvasRef.getGraphicsContext2D();
Button deBtn = new Button("Draw");
deBtn.setPrefWidth(BUTTON_WIDTH);
deBtn.setOnAction(e -> {
String txt = deBtn.getText();
switch (txt) {
case "Draw" -> {
randomizeParticlePositions();
drawCircles(FG_COLOR);
deBtn.setText("Erase");
stroke = true;
}
case "Erase" -> {
drawCircles(BG_COLOR);
deBtn.setText("Draw");
stroke = false;
}
default ->
Platform.exit();
}
});
Button exBtn = new Button("Exit");
exBtn.setPrefWidth(BUTTON_WIDTH);
exBtn.setOnAction(e -> Platform.exit());
TilePane tp = new TilePane();
tp.setAlignment(Pos.CENTER);
tp.setHgap(10);
tp.getChildren().addAll(deBtn, exBtn);
VBox root = new VBox();
root.setPadding(new Insets(7));
root.setSpacing(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(versionLabel, canvasRef, tp);
StackPane sp = new StackPane(root);
BackgroundFill bf = new BackgroundFill(BG_COLOR, CornerRadii.EMPTY, Insets.EMPTY);
Background bg = new Background(bf);
sp.setBackground(bg);
Scene scene = new Scene(sp, 640.0, 480.0);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}