From c8fb521987e0744c4d0e225bec6408455f8d5d81 Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:40:12 +0930 Subject: [PATCH 1/2] Sharpen lobby sleeve icon with high-quality downscale The lobby panel drew the full 360x500 sleeve source straight into a 58x80 label, a ~6x single-pass reduction that looked aliased and rough. Route the three icon-set sites through a helper that pre-scales once via the cached area-averaging resize() at 2x the label size, leaving HiDPI headroom. The scaled image is cached per sleeve, so the heavy downscale runs once rather than on every repaint. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/java/forge/screens/home/PlayerPanel.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java b/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java index bb18e423f10..f0009549ce3 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java @@ -228,7 +228,7 @@ void update() { avatarLabel.repaintSelf(); sleeveLabel.setEnabled(mayEdit); - sleeveLabel.setIcon(FSkin.getSleeves().get(type == LobbySlotType.OPEN ? -1 : sleeveIndex)); + setSleeveIcon(FSkin.getSleeves().get(type == LobbySlotType.OPEN ? -1 : sleeveIndex)); sleeveLabel.repaintSelf(); txtPlayerName.setEnabled(mayEdit); @@ -699,7 +699,7 @@ private void createSleeve() { final String[] currentPrefs = FModel.getPreferences().getPref(FPref.UI_SLEEVES).split(","); if (index < currentPrefs.length) { sleeveIndex = Integer.parseInt(currentPrefs[index]); - sleeveLabel.setIcon(FSkin.getSleeves().get(sleeveIndex)); + setSleeveIcon(FSkin.getSleeves().get(sleeveIndex)); } else { setRandomSleeve(false); } @@ -805,11 +805,15 @@ public int getSleeveIndex() { } public void setSleeveIndex(final int sleeveIndex0) { sleeveIndex = sleeveIndex0; - final SkinImage icon = FSkin.getSleeves().get(sleeveIndex); - sleeveLabel.setIcon(icon); + setSleeveIcon(FSkin.getSleeves().get(sleeveIndex)); sleeveLabel.repaintSelf(); } + // Scaling the full-size source straight to this small label looks rough; pre-scale once at 2x for HiDPI headroom + private void setSleeveIcon(final SkinImage icon) { + sleeveLabel.setIcon(icon == null ? null : icon.resize(58 * 2, 80 * 2)); + } + public int getTeam() { return teamComboBox.getSelectedIndex(); } From ed8911d42c336ef090112c5e55d4e126380a3b68 Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:49:51 +0930 Subject: [PATCH 2/2] Sharpen large downscaled background icons in FLabel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FLabel's background-icon paint path is the correct place for this fix — it's where the scaling happens — but it was never adapted for large icons: it draws the full-resolution source straight to the label in a single bicubic pass, which aliases when the source is far larger than the draw size (e.g. a 360x500 card-back sleeve in a ~58x80 lobby label). The previous fix sidestepped that at the call site by pre-scaling the sleeve before handing it to the label; drop that bespoke workaround and fix the path itself. When a background icon's source exceeds 3x its draw size, pre-scale it once with area-averaging (cached, grow-only so resizing can't re-trigger it) and let the existing bicubic finish. The sleeves are currently the only icons this affects — every other background icon has a source at or near its draw size and stays on the original path unchanged. iconScaleFactor is not an alternative: it only changes the size the icon is drawn at, not the interpolation, so it can't address aliasing. The background path draws at device resolution, so the fix belongs there. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/forge/screens/home/PlayerPanel.java | 12 ++++------- .../src/main/java/forge/toolbox/FLabel.java | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java b/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java index f0009549ce3..bb18e423f10 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java @@ -228,7 +228,7 @@ void update() { avatarLabel.repaintSelf(); sleeveLabel.setEnabled(mayEdit); - setSleeveIcon(FSkin.getSleeves().get(type == LobbySlotType.OPEN ? -1 : sleeveIndex)); + sleeveLabel.setIcon(FSkin.getSleeves().get(type == LobbySlotType.OPEN ? -1 : sleeveIndex)); sleeveLabel.repaintSelf(); txtPlayerName.setEnabled(mayEdit); @@ -699,7 +699,7 @@ private void createSleeve() { final String[] currentPrefs = FModel.getPreferences().getPref(FPref.UI_SLEEVES).split(","); if (index < currentPrefs.length) { sleeveIndex = Integer.parseInt(currentPrefs[index]); - setSleeveIcon(FSkin.getSleeves().get(sleeveIndex)); + sleeveLabel.setIcon(FSkin.getSleeves().get(sleeveIndex)); } else { setRandomSleeve(false); } @@ -805,15 +805,11 @@ public int getSleeveIndex() { } public void setSleeveIndex(final int sleeveIndex0) { sleeveIndex = sleeveIndex0; - setSleeveIcon(FSkin.getSleeves().get(sleeveIndex)); + final SkinImage icon = FSkin.getSleeves().get(sleeveIndex); + sleeveLabel.setIcon(icon); sleeveLabel.repaintSelf(); } - // Scaling the full-size source straight to this small label looks rough; pre-scale once at 2x for HiDPI headroom - private void setSleeveIcon(final SkinImage icon) { - sleeveLabel.setIcon(icon == null ? null : icon.resize(58 * 2, 80 * 2)); - } - public int getTeam() { return teamComboBox.getSelectedIndex(); } diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/FLabel.java b/forge-gui-desktop/src/main/java/forge/toolbox/FLabel.java index 8f284ba7b8c..95d5a863907 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/FLabel.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/FLabel.java @@ -274,6 +274,8 @@ public void keyPressed(final KeyEvent e) { // Various variables used in image rendering. private Image img; + private Image scaledBg; + private int scaledBgW, scaledBgH; private Runnable cmdClick, cmdRightClick; @@ -492,6 +494,7 @@ public void setIcon(final Icon i0) { // Will need image (not icon) for scaled and non-scaled. // Will need image if not in background, but scaled. if (iconInBackground || iconScaleAuto) { + scaledBg = null; if (i0 != null) { img = ((ImageIcon) i0).getImage(); iw = img.getWidth(null); @@ -599,8 +602,24 @@ protected void paintContent(final Graphics2D g, final int w, final int h, final final int y = (int) (((h - sh) / 2) + iconInsets.getY()); + // A source much larger than the draw size aliases in a single bicubic pass; pre-scale it once to 2x + // with area-averaging and cache it, growing the cache only as needed so a resize can't re-trigger it + Image src = img; + int srcW = iw, srcH = ih; + if (sw > 0 && sh > 0 && (iw > sw * 3 || ih > sh * 3)) { + final int tw = sw * 2, th = sh * 2; + if (scaledBg == null || scaledBgW < tw || scaledBgH < th) { + scaledBg = new ImageIcon(img.getScaledInstance(tw, th, Image.SCALE_SMOOTH)).getImage(); + scaledBgW = tw; + scaledBgH = th; + } + src = scaledBg; + srcW = scaledBgW; + srcH = scaledBgH; + } + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - g.drawImage(img, x, y, sw + x, sh + y, 0, 0, iw, ih, null); + g.drawImage(src, x, y, sw + x, sh + y, 0, 0, srcW, srcH, null); } super.paintComponent(g);