Skip to content

UI scaling graphic glitches noticeable with Snapshots #1039

@Frank-99

Description

@Frank-99

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions