-
-
Notifications
You must be signed in to change notification settings - Fork 314
Description
By default, when I execute the following code:
FlatAnimatedLafChange.showSnapshot();
FlatAnimatedLafChange.hideSnapshotWithAnimation();
I get this very jumpy result on a 1.25/1.5/1.75 scale screen (recorded on 1.5 scale):
https://github.com/user-attachments/assets/5a08dc1b-d8ad-46e4-8770-d06f135e34d5
Then I tried to see if I could find a visually "better" way to do it (this is just a test I've coded up for this example):
public void showSnapshotTest() {
Window[] windows = Window.getWindows();
for (Window window : windows) {
if (!(window instanceof RootPaneContainer) || !window.isShowing()) { continue; }
//find actual screen dimension
GraphicsDevice device = getWindowDevice(window);
double screenSccale = device.getDisplayMode().getWidth() / (double) device.getDefaultConfiguration().getBounds().width;
Dimension windowRealPixelDimension = new Dimension((int) (window.getWidth() * screenSccale), (int) (window.getHeight() * screenSccale));
//create bufferedimage
BufferedImage snapshot = new BufferedImage(windowRealPixelDimension.width, windowRealPixelDimension.height, BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g2 = (Graphics2D) snapshot.getGraphics().create();
//get affine transform for screen
AffineTransform transform = device.getDefaultConfiguration().getDefaultTransform();
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
//set transform to buffered image
g2.transform(transform);
//paint the window on image
window.paint(g2);
//get layered pane
JLayeredPane layeredPane = ((RootPaneContainer) window).getLayeredPane();
//cusrtom snapshot drawing component
JComponent snapshotLayer = new JComponent() {
@Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
//invert screen transform
try { transform.invert(); } catch (Exception e) { }
//find snapshot location on screen
Point posOnScreen = getLocationOnScreen();
//then find the offset of the position in the window coordinate
SwingUtilities.convertPointFromScreen(posOnScreen, window);
//set offset
g2d.translate(-posOnScreen.x, -posOnScreen.y);
//paint
g2d.drawImage(snapshot, transform, null);
}
@Override
public void addNotify() {
super.addNotify();
SwingUtilities.invokeLater(() -> {
//delay then hide the snapshot
try { Thread.sleep(200); } catch (Exception e) { }
layeredPane.remove(this);
window.repaint();
});
}
@Override
public void removeNotify() {
super.removeNotify();
snapshot.flush();
}
};
snapshotLayer.setOpaque(true);
snapshotLayer.setSize(layeredPane.getSize());
layeredPane.add(snapshotLayer, Integer.valueOf(JLayeredPane.DRAG_LAYER + 1));
}
}
public GraphicsDevice getWindowDevice(Window window) {
Rectangle bounds = window.getBounds();
return Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()).stream()
// pick devices where window located
.filter(d -> d.getDefaultConfiguration().getBounds().intersects(bounds))
// sort by biggest intersection square
.sorted((f, s) -> Long.compare(//
square(f.getDefaultConfiguration().getBounds().intersection(bounds)),
square(s.getDefaultConfiguration().getBounds().intersection(bounds))))
// use one with the biggest part of the window
.reduce((f, s) -> s) //
// fallback to default device
.orElse(window.getGraphicsConfiguration().getDevice());
}
private static long square(Rectangle rec) {
return Math.abs(rec.width * rec.height);
}
And with this solution I get this much nicer result (notice the rendering is jumping less and it's more pixel precise):
https://github.com/user-attachments/assets/d3052e4a-4ce8-4002-8c09-d2e129b70383
in the process of testing this custom solution I saved all the snapshots created to file to analize them pixel by pixel. With this approach/rendering tecneaque the snapshots are pixel perfect, but as you can see the text and default rounded border still has issues.
For the text I tried rendering the JLabels with the following hint and it fixed the issue:
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
then for the rounded border I tried replacing the default FlatLaf rounded border with my own version:
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//clip the rendering to paint only what's visible
Shape clip = g2.getClip();
if (c instanceof JComponent jc) {
g2.setClip(jc.getVisibleRect());
} else {
g2.setClip(new Rectangle(x, y, w, h));
}
Area border = new Area(createComponentRectangle(x, y, w, h, arc));
Color background = UIUtils.getParentBackground(c);
if (background != null) {
//the - 1 offset is a fix for a 1 pixel line artifact on top and left while drawing an area shape on a scaled screen
Area outerArea = new Area(new Rectangle(x - 1, y - 1, w + 1, h + 1));
outerArea.subtract(border);
g2.setColor(background);
g2.fill(outerArea);
}
float innerArc = Math.max(0, arc - (outlineWidth * 2));
Area insideArea = new Area(createComponentRectangle(
x + outlineWidth,
y + outlineWidth,
w - (outlineWidth * 2)),
h - (outlineWidth * 2),
innerArc
));
border.subtract(insideArea);
g2.setColor(borderColor);
g2.fill(border);
//don't paint over the component
insideArea.subtract(new Area(new Rectangle(
x + insets.left,
y + insets.top,
(w - (x + insets.left)) - insets.right,
(h - (y + insets.top)) - insets.bottom
)));
if (c instanceof JScrollPane scroll) {
g2.setColor(scroll.getViewport().getView().getBackground());
} else {
g2.setColor(c.getBackground());
}
g2.fill(insideArea);
g2.setClip(clip);
g2.dispose();
}
public static Shape createComponentRectangle(float x, float y, float w, float h, float arc) {
if (arc <= 0) {
return new Rectangle2D.Float(x, y, w, h);
}
if (w == h && arc >= w) {
return new Ellipse2D.Float(x, y, w, h);
}
arc = Math.min(arc, Math.min(w, h));
return new RoundRectangle2D.Float(x, y, w, h, arc, arc);
}
And this is the result (notice the list on the left has the text rendered correctly while the textarea and the checkboxes, which I haven't "fixed", still show the glitch):
https://github.com/user-attachments/assets/4bfd8a9c-36ea-4543-b065-3a7aba2d183e
Notice that this is a complex and work-in-progress app, and I was already using some of my own implementations for the rounded border in the top toolbar and my own version of SVG icons where I've expanded on the functionality to better suit my needs.
The issues I'm talking about are visible everywhere for the text rendering, and in the JSplitPane border where it uses the default border provided by the FlatLaf library. I think the original SVG icons are also a bit jumpy by default but I haven't experimented with them.
I hope this shows how the default rendering of the snapshot is not ideal and needs improvements.
Hopefully a fix will be implemented in a future version and I will be able to finally remove all my temporary "fixes"
I'm currently on windows 11, using java 21 and running last version of FlatLaf