From 0b13660348299c24d8c0110ec66e40a88606b372 Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 20:33:28 +0200 Subject: [PATCH 01/11] Use mouse point in event instead of calling getMousePosition() Improves performance and is also the correct way to do this. --- Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java index 480249f9bf..a760e19db8 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java @@ -1056,7 +1056,7 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti if (gameCard.hideInfo()) { return; } - if (getMousePosition(true) != null) { + if (this.contains(e.getPoint())) { return; } if (tooltipShowing) { From 8c2c1f48871e6f2ad381ab39850e293d37edb6e0 Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 16:49:21 +0200 Subject: [PATCH 02/11] Don't call setEditable in ColorPane Shows up in profiles, and should be unnecessary. --- Mage.Client/src/main/java/mage/client/components/ColorPane.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/components/ColorPane.java b/Mage.Client/src/main/java/mage/client/components/ColorPane.java index 240cc4380f..8fc39aa5be 100644 --- a/Mage.Client/src/main/java/mage/client/components/ColorPane.java +++ b/Mage.Client/src/main/java/mage/client/components/ColorPane.java @@ -139,9 +139,7 @@ public class ColorPane extends JEditorPane { if (hyperlinkEnabled) { text = text.replaceAll("(]*>([^<]*)) (\\[[0-9a-fA-F]*\\])", "$1 $3"); } - setEditable(true); kit.insertHTML(doc, doc.getLength(), text, 0, 0, null); - setEditable(false); int len = getDocument().getLength(); setCaretPosition(len); From 9513b5bf745dd0a8bd92d21e1dfb9865750d7cc7 Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 17:02:20 +0200 Subject: [PATCH 03/11] Don't call setText in MageTextArea if the text hasn't changed Minor UI speedup, shows up in profiles for simple games. --- .../main/java/mage/client/components/MageTextArea.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Mage.Client/src/main/java/mage/client/components/MageTextArea.java b/Mage.Client/src/main/java/mage/client/components/MageTextArea.java index 49b0184eda..219e0f72d7 100644 --- a/Mage.Client/src/main/java/mage/client/components/MageTextArea.java +++ b/Mage.Client/src/main/java/mage/client/components/MageTextArea.java @@ -13,6 +13,8 @@ import org.mage.card.arcane.UI; * @author nantuko */ public class MageTextArea extends JEditorPane { + private String currentText; + private int currentPanelWidth; public MageTextArea() { UI.setHTMLEditorKit(this); @@ -31,6 +33,12 @@ public class MageTextArea extends JEditorPane { return; } + if(text.equals(currentText) && panelWidth == currentPanelWidth) + return; + + currentText = text; + currentPanelWidth = panelWidth; + final StringBuilder buffer = new StringBuilder(512); // Dialog is a java logical font family, so it should work on all systems buffer.append(" Date: Wed, 1 Jun 2016 19:32:49 +0200 Subject: [PATCH 05/11] Add mechanism to register image caches and flush them all on GUI size change This should avoid the issue of having lots of unused entries in caches if the GUI size is changed multiple times. --- .../src/main/java/mage/client/MageFrame.java | 2 ++ .../java/mage/client/util/ImageCaches.java | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 Mage.Client/src/main/java/mage/client/util/ImageCaches.java diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index c36056dace..983db1f369 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -116,6 +116,7 @@ import mage.client.table.TablesPane; import mage.client.tournament.TournamentPane; import mage.client.util.EDTExceptionHandler; import mage.client.util.GUISizeHelper; +import mage.client.util.ImageCaches; import mage.client.util.SettingsManager; import mage.client.util.SystemUtil; import mage.client.util.audio.MusicPlayer; @@ -1455,6 +1456,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { } public void changeGUISize() { + ImageCaches.flush(); setGUISize(); Plugins.getInstance().changeGUISize(); CountryUtil.changeGUISize(); diff --git a/Mage.Client/src/main/java/mage/client/util/ImageCaches.java b/Mage.Client/src/main/java/mage/client/util/ImageCaches.java new file mode 100644 index 0000000000..2cb045c4d0 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/util/ImageCaches.java @@ -0,0 +1,34 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.client.util; + +import java.util.Map; +import java.util.Vector; + +/** + * + * @author user + */ +public class ImageCaches { + private static Vector IMAGE_CACHES; + + static { + IMAGE_CACHES = new Vector(); + } + + public static Map register(Map map) + { + IMAGE_CACHES.add(map); + return map; + } + + public static void flush() + { + for (Map map : IMAGE_CACHES) { + map.clear(); + } + } +} From e3d84ca2120f8407d4d05add4cf4733bd988b6b8 Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 17:05:28 +0200 Subject: [PATCH 06/11] Cache resized and rotated images RotatedResizedImageCache and MultiRotatedResizedImageCache contain the caching machinery. A cache of rotated and resized images is added to ImageCache, and is used by the resizing functions there. All the resizing and rotation functions in ImageHelper are redirected to the ones in ImageCache. This is slightly inefficient because it will cache some calls that are never repeated, but it prevents developers from mistakenly using uncached functions when calls are repeated, seriously impacting performance. Also resizing functions that only take a width or an height have been removed, and their calls fixed to provide the other dimension. It's still possible to specify -1 as width or height to ignore constraints in that dimension, though. Greatly speeds up UI performance. --- .../main/java/mage/client/cards/BigCard.java | 9 +- .../src/main/java/mage/client/cards/Card.java | 2 +- .../java/mage/client/cards/Permanent.java | 4 +- .../client/deckeditor/table/TableModel.java | 3 +- .../java/mage/client/draft/DraftPanel.java | 2 +- .../plugins/adapters/MageActionCallback.java | 8 +- .../java/mage/client/util/ImageHelper.java | 115 +------------ .../client/util/TransformedImageCache.java | 153 ++++++++++++++++++ .../mage/plugins/card/images/ImageCache.java | 40 +---- 9 files changed, 179 insertions(+), 157 deletions(-) create mode 100644 Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java diff --git a/Mage.Client/src/main/java/mage/client/cards/BigCard.java b/Mage.Client/src/main/java/mage/client/cards/BigCard.java index 75fa550878..cc3417c1b4 100644 --- a/Mage.Client/src/main/java/mage/client/cards/BigCard.java +++ b/Mage.Client/src/main/java/mage/client/cards/BigCard.java @@ -54,6 +54,7 @@ import mage.client.plugins.impl.Plugins; import mage.client.util.ImageHelper; import mage.constants.EnlargeMode; import org.jdesktop.swingx.JXPanel; +import mage.client.util.TransformedImageCache; /** * Class for displaying big image of the card @@ -103,7 +104,13 @@ public class BigCard extends JComponent { } - public void setCard(UUID cardId, EnlargeMode enlargeMode, Image image, List strings) { + public void setCard(UUID cardId, EnlargeMode enlargeMode, Image image, List strings, boolean rotate) { + if (rotate && getWidth() > getHeight()) { + image = TransformedImageCache.getRotatedResizedImage((BufferedImage)image, getHeight(), getWidth(), Math.toRadians(90.0)); + } else { + image = TransformedImageCache.getResizedImage((BufferedImage)image, getWidth(), getHeight()); + } + if (this.cardId == null || !enlargeMode.equals(this.enlargeMode) || !this.cardId.equals(cardId)) { if (this.panel != null) { remove(this.panel); diff --git a/Mage.Client/src/main/java/mage/client/cards/Card.java b/Mage.Client/src/main/java/mage/client/cards/Card.java index 8d6235a08a..1a43fca155 100644 --- a/Mage.Client/src/main/java/mage/client/cards/Card.java +++ b/Mage.Client/src/main/java/mage/client/cards/Card.java @@ -374,7 +374,7 @@ public class Card extends MagePermanent implements MouseMotionListener, MouseLis @Override public void mouseMoved(MouseEvent arg0) { this.bigCard.showTextComponent(); - this.bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, getRules()); + this.bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, getRules(), false); } @Override diff --git a/Mage.Client/src/main/java/mage/client/cards/Permanent.java b/Mage.Client/src/main/java/mage/client/cards/Permanent.java index a01e368641..0db10e5887 100644 --- a/Mage.Client/src/main/java/mage/client/cards/Permanent.java +++ b/Mage.Client/src/main/java/mage/client/cards/Permanent.java @@ -56,6 +56,8 @@ import mage.client.util.ImageHelper; import mage.constants.CardType; import mage.view.CounterView; import mage.view.PermanentView; +import org.mage.plugins.card.images.ImageCache; +import mage.client.util.TransformedImageCache; /** * @@ -215,7 +217,7 @@ public class Permanent extends Card { Graphics2D g = (Graphics2D) tappedImage.getGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.drawImage(this.createImage(ImageHelper.rotate(small, dimension)), 0, 0, this); + g.drawImage(TransformedImageCache.getRotatedResizedImage(small, dimension.frameWidth, dimension.frameHeight, Math.toRadians(90.0)), 0, 0, this); g.dispose(); } diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java b/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java index baa4cf5f85..65c5ebd027 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java @@ -409,8 +409,7 @@ public class TableModel extends AbstractTableModel implements ICardGrid { Image image = Plugins.getInstance().getOriginalImage(card); if (image != null && image instanceof BufferedImage) { // XXX: scaled to fit width - image = ImageHelper.getResizedImage((BufferedImage) image, bigCard.getWidth()); - bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, new ArrayList()); + bigCard.setCard(card.getId(), EnlargeMode.NORMAL, image, new ArrayList(), false); } else { drawCardText(card); } diff --git a/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java b/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java index 109e3c9e91..e55588b743 100644 --- a/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java +++ b/Mage.Client/src/main/java/mage/client/draft/DraftPanel.java @@ -228,7 +228,7 @@ public class DraftPanel extends javax.swing.JPanel { int height = left * 18; lblTableImage.setSize(new Dimension(lblTableImage.getWidth(), height)); Image tableImage = ImageHelper.getImageFromResources(draftView.getBoosterNum() == 2 ? "/draft/table_left.png" : "/draft/table_right.png"); - BufferedImage resizedTable = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(tableImage, BufferedImage.TYPE_INT_ARGB), lblTableImage.getWidth()); + BufferedImage resizedTable = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(tableImage, BufferedImage.TYPE_INT_ARGB), lblTableImage.getWidth(), lblTableImage.getHeight()); lblTableImage.setIcon(new ImageIcon(resizedTable)); int count = 0; diff --git a/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java b/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java index 8a0bee2366..4c18ca7fb2 100644 --- a/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java +++ b/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java @@ -598,13 +598,7 @@ public class MageActionCallback implements ActionCallback { private void displayCardInfo(MageCard mageCard, Image image, BigCard bigCard) { if (image != null && image instanceof BufferedImage) { // XXX: scaled to fit width - if (mageCard.getOriginal().isToRotate() && bigCard.getWidth() > bigCard.getHeight()) { - image = ImageHelper.getResizedImage((BufferedImage) image, bigCard.getHeight()); - image = ImageHelper.rotate((BufferedImage) image, Math.toRadians(90)); - } else { - image = ImageHelper.getResizedImage((BufferedImage) image, bigCard.getWidth()); - } - bigCard.setCard(mageCard.getOriginal().getId(), enlargeMode, image, mageCard.getOriginal().getRules()); + bigCard.setCard(mageCard.getOriginal().getId(), enlargeMode, image, mageCard.getOriginal().getRules(), mageCard.getOriginal().isToRotate()); // if it's an ability, show only the ability text as overlay if (mageCard.getOriginal().isAbility() && enlargeMode.equals(EnlargeMode.NORMAL)) { bigCard.showTextComponent(); diff --git a/Mage.Client/src/main/java/mage/client/util/ImageHelper.java b/Mage.Client/src/main/java/mage/client/util/ImageHelper.java index e4d55b0df4..1eddc28e0d 100644 --- a/Mage.Client/src/main/java/mage/client/util/ImageHelper.java +++ b/Mage.Client/src/main/java/mage/client/util/ImageHelper.java @@ -52,6 +52,7 @@ import static mage.client.constants.Constants.FRAME_MAX_WIDTH; import static mage.client.constants.Constants.SYMBOL_MAX_SPACE; import mage.view.CardView; import org.mage.card.arcane.UI; +import org.mage.plugins.card.images.ImageCache; /** * @@ -70,21 +71,6 @@ public class ImageHelper { return null; } - /** - * - * @param ref - image name - * @param height - height after scaling - * @return a scaled image that preserves the original aspect ratio, with a - * specified height - */ - public static BufferedImage loadImage(String ref, int height) { - BufferedImage image = loadImage(ref); - if (image != null) { - return scaleImage(image, height); - } - return null; - } - public static BufferedImage loadImage(String ref) { if (!images.containsKey(ref)) { try { @@ -107,67 +93,7 @@ public class ImageHelper { } public static BufferedImage scaleImage(BufferedImage image, int width, int height) { - BufferedImage scaledImage = image; - int w = image.getWidth(); - int h = image.getHeight(); - do { - w /= 2; - h /= 2; - if (w < width || h < height) { - w = width; - h = height; - } - BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics2D = newImage.createGraphics(); - graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - graphics2D.drawImage(scaledImage, 0, 0, w, h, null); - graphics2D.dispose(); - scaledImage = newImage; - } while (w != width || h != height); - return scaledImage; - } - - public static BufferedImage scaleImage(BufferedImage image, int height) { - double ratio = height / (double) image.getHeight(); - int width = (int) (image.getWidth() * ratio); - return scaleImage(image, width, height); - } - - public static MemoryImageSource rotate(Image image, CardDimensions dimensions) { - int buffer[] = new int[dimensions.frameWidth * dimensions.frameHeight]; - int rotate[] = new int[dimensions.frameHeight * dimensions.frameWidth]; - PixelGrabber grabber = new PixelGrabber(image, 0, 0, dimensions.frameWidth, dimensions.frameHeight, buffer, 0, dimensions.frameWidth); - try { - grabber.grabPixels(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - for (int y = 0; y < dimensions.frameHeight; y++) { - for (int x = 0; x < dimensions.frameWidth; x++) { - rotate[((dimensions.frameWidth - x - 1) * dimensions.frameHeight) + y] = buffer[(y * dimensions.frameWidth) + x]; - } - } - - return new MemoryImageSource(dimensions.frameHeight, dimensions.frameWidth, rotate, 0, dimensions.frameHeight); - - } - - public static BufferedImage rotate(BufferedImage image, double angle) { - double sin = Math.abs(Math.sin(angle)), cos = Math.abs(Math.cos(angle)); - int w = image.getWidth(), h = image.getHeight(); - int neww = (int) Math.floor(w * cos + h * sin), newh = (int) Math.floor(h * cos + w * sin); - - GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); - GraphicsDevice gs = ge.getDefaultScreenDevice(); - GraphicsConfiguration gc = gs.getDefaultConfiguration(); - - BufferedImage result = gc.createCompatibleImage(neww, newh, Transparency.TRANSLUCENT); - Graphics2D g = result.createGraphics(); - g.translate((neww - w) / 2, (newh - h) / 2); - g.rotate(angle, w / 2, h / 2); - g.drawRenderedImage(image, null); - g.dispose(); - return result; + return TransformedImageCache.getResizedImage(image, width, height); } public static void drawCosts(List costs, Graphics2D g, int xOffset, int yOffset, ImageObserver o) { @@ -191,26 +117,7 @@ public class ImageHelper { * @return */ public static BufferedImage getResizedImage(BufferedImage original, int width, int height) { - ResampleOp resampleOp = new ResampleOp(width, height); - BufferedImage image = resampleOp.filter(original, null); - return image; - } - - /** - * Returns an image scaled to fit width panel - * - * @param original - * @param width - * @return - */ - public static BufferedImage getResizedImage(BufferedImage original, int width) { - if (width != original.getWidth()) { - double ratio = width / (double) original.getWidth(); - int height = (int) (original.getHeight() * ratio); - return getResizedImage(original, width, height); - } else { - return original; - } + return TransformedImageCache.getResizedImage(original, width, height); } /** @@ -223,17 +130,7 @@ public class ImageHelper { * @return scaled image */ public static BufferedImage scale(BufferedImage sbi, int imageType, int dWidth, int dHeight) { - BufferedImage dbi = null; - if (sbi != null) { - double fWidth = dWidth / sbi.getWidth(); - double fHeight = dHeight / sbi.getHeight(); - dbi = new BufferedImage(dWidth, dHeight, imageType); - Graphics2D g = dbi.createGraphics(); - AffineTransform at = AffineTransform.getScaleInstance(fWidth, fHeight); - g.drawRenderedImage(sbi, at); - g.dispose(); - } - return dbi; + return TransformedImageCache.getResizedImage(sbi, dWidth, dHeight); } /** @@ -244,9 +141,7 @@ public class ImageHelper { * @return */ public static BufferedImage getResizedImage(BufferedImage original, Rectangle sizeNeed) { - ResampleOp resampleOp = new ResampleOp(sizeNeed.width, sizeNeed.height); - BufferedImage image = resampleOp.filter(original, null); - return image; + return TransformedImageCache.getResizedImage(original, sizeNeed.width, sizeNeed.height); } /** diff --git a/Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java b/Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java new file mode 100644 index 0000000000..9a2cf205dc --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/util/TransformedImageCache.java @@ -0,0 +1,153 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.client.util; + +import com.google.common.base.Function; +import com.google.common.collect.MapMaker; +import com.mortennobel.imagescaling.ResampleOp; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.util.Map; +import mage.client.util.ImageHelper; + +/** + * + * @author user + */ +public class TransformedImageCache { + private final static class Key + { + final int width; + final int height; + final double angle; + + public Key(int width, int height, double angle) { + this.width = width; + this.height = height; + this.angle = angle; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 53 * hash + this.width; + hash = 53 * hash + this.height; + hash = 53 * hash + (int) (Double.doubleToLongBits(this.angle) ^ (Double.doubleToLongBits(this.angle) >>> 32)); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Key other = (Key) obj; + if (this.width != other.width) { + return false; + } + if (this.height != other.height) { + return false; + } + if (Double.doubleToLongBits(this.angle) != Double.doubleToLongBits(other.angle)) { + return false; + } + return true; + } + } + + static Map> IMAGE_CACHE; + + static + { + // TODO: can we use a single map? + IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap(new Function>() { + @Override + public Map apply(final Key key) { + return new MapMaker().weakKeys().softValues().makeComputingMap(new Function() { + @Override + public BufferedImage apply(BufferedImage image) { + if(key.width != image.getWidth() || key.height != image.getHeight()) + image = resizeImage(image, key.width, key.height); + if(key.angle != 0.0) + image = rotateImage(image, key.angle); + return image; + } + }); + } + })); + } + + private static BufferedImage rotateImage(BufferedImage image, double angle) { + double sin = Math.abs(Math.sin(angle)), cos = Math.abs(Math.cos(angle)); + int w = image.getWidth(), h = image.getHeight(); + int neww = (int) Math.floor(w * cos + h * sin), newh = (int) Math.floor(h * cos + w * sin); + + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gs = ge.getDefaultScreenDevice(); + GraphicsConfiguration gc = gs.getDefaultConfiguration(); + + BufferedImage result = gc.createCompatibleImage(neww, newh, Transparency.TRANSLUCENT); + Graphics2D g = result.createGraphics(); + g.translate((neww - w) / 2, (newh - h) / 2); + g.rotate(angle, w / 2, h / 2); + g.drawRenderedImage(image, null); + g.dispose(); + return result; + } + + private static BufferedImage resizeImage(BufferedImage original, int width, int height) { + ResampleOp resampleOp = new ResampleOp(width, height); + BufferedImage image = resampleOp.filter(original, null); + return image; + } + + public static BufferedImage getResizedImage(BufferedImage image, int width, int height) + { + return getRotatedResizedImage(image, width, height, 0.0); + } + + public static BufferedImage getRotatedImage(BufferedImage image, double angle) + { + return getRotatedResizedImage(image, -1, -1, angle); + } + + public static BufferedImage getRotatedResizedImage(BufferedImage image, int width, int height, double angle) + { + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + + if(angle == 0.0 && (width < 0 || imageWidth == width) && (height < 0 || imageHeight == height)) + return image; + + int resWidth; + int resHeight; + if(width < 0 && height < 0) { + resWidth = imageWidth; + resHeight = imageHeight; + } else if((height < 0) || (width >= 0 && imageHeight * width <= imageWidth * height)) { + resWidth = width; + resHeight = imageHeight * width / imageWidth; + } else { + resWidth = imageWidth * height / imageHeight; + resHeight = height; + } + + if(angle == 0.0 && imageWidth == resWidth && imageHeight == resHeight) + return image; + + return IMAGE_CACHE.get(new Key(resWidth, resHeight, angle)).get(image); + } +} diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java index 86f7b6c5d0..3234bcc028 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java @@ -1,5 +1,6 @@ package org.mage.plugins.card.images; +import mage.client.util.TransformedImageCache; import com.google.common.base.Function; import com.google.common.collect.ComputationException; import com.google.common.collect.MapMaker; @@ -274,7 +275,7 @@ public class ImageCache { } public static BufferedImage makeThumbnail(BufferedImage original, String path) { - BufferedImage image = getResizedImage(original, Constants.THUMBNAIL_SIZE_FULL); + BufferedImage image = TransformedImageCache.getResizedImage(original, Constants.THUMBNAIL_SIZE_FULL.width, Constants.THUMBNAIL_SIZE_FULL.height); TFile imageFile = getTFile(path); if (imageFile == null) { return null; @@ -312,36 +313,7 @@ public class ImageCache { return original; } - ResampleOp resampleOp = new ResampleOp(tgtWidth, tgtHeight); - BufferedImage image = resampleOp.filter(original, null); - return image; - } - - /** - * Returns an image scaled to the size appropriate for the card picture - * panel For future use. - */ - private static BufferedImage getFullSizeImage(BufferedImage original, double scale) { - if (scale == 1) { - return original; - } - ResampleOp resampleOp = new ResampleOp((int) (original.getWidth() * scale), (int) (original.getHeight() * scale)); - BufferedImage image = resampleOp.filter(original, null); - return image; - } - - /** - * Returns an image scaled to the size appropriate for the card picture - * panel - * - * @param original - * @param sizeNeed - * @return - */ - public static BufferedImage getResizedImage(BufferedImage original, Rectangle sizeNeed) { - ResampleOp resampleOp = new ResampleOp(sizeNeed.width, sizeNeed.height); - BufferedImage image = resampleOp.filter(original, null); - return image; + return TransformedImageCache.getResizedImage(original, tgtWidth, tgtHeight); } /** @@ -364,11 +336,11 @@ public class ImageCache { } double scale = Math.min((double) width / original.getWidth(), (double) height / original.getHeight()); - if (scale > 1) { - scale = 1; + if (scale >= 1) { + return original; } - return getFullSizeImage(original, scale); + return TransformedImageCache.getResizedImage(original, (int)(original.getWidth() * scale), (int)(original.getHeight() * scale)); } public static TFile getTFile(String path) { From 4b190eeaf4686cb8834b48edc3ab934ebed9cf72 Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 21:35:00 +0200 Subject: [PATCH 07/11] Drastically simplify ScaledImagePanel by having it just use the resized image cache All the current behavior of ScaledImagePanel seems to be useless, since the source images are static, so we can just use the highest quality scaling possible and cache the result using TransformedImageCache. --- .../java/org/mage/card/arcane/CardPanel.java | 17 +- .../mage/card/arcane/ScaledImagePanel.java | 163 +----------------- 2 files changed, 6 insertions(+), 174 deletions(-) diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java index a760e19db8..3628b2e491 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java @@ -52,8 +52,6 @@ import mage.view.PermanentView; import mage.view.StackAbilityView; import net.java.truevfs.access.TFile; import org.apache.log4j.Logger; -import org.mage.card.arcane.ScaledImagePanel.MultipassType; -import org.mage.card.arcane.ScaledImagePanel.ScalingType; import static org.mage.plugins.card.constants.Constants.THUMBNAIL_SIZE_FULL; import org.mage.plugins.card.dl.sources.DirectLinksForDownload; import org.mage.plugins.card.images.ImageCache; @@ -304,9 +302,6 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti imagePanel = new ScaledImagePanel(); imagePanel.setBorder(BorderFactory.createLineBorder(Color.white)); add(imagePanel); - imagePanel.setScaleLarger(true); - imagePanel.setScalingType(ScalingType.nearestNeighbor); - imagePanel.setScalingMultiPassType(MultipassType.none); String cardType = getType(newGameCard); tooltipText.setText(getText(cardType, newGameCard)); @@ -387,7 +382,7 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti titleText.setText(!displayTitleAnyway && hasImage ? "" : card.getName()); } - private void setImage(Image srcImage) { + private void setImage(BufferedImage srcImage) { synchronized (imagePanel) { imagePanel.setImage(srcImage); repaint(); @@ -413,10 +408,6 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti return zone; } - public void setScalingType(ScalingType scalingType) { - imagePanel.setScalingType(scalingType); - } - public void setDisplayEnabled(boolean displayEnabled) { this.displayEnabled = displayEnabled; } @@ -619,12 +610,6 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti ptText.setLocation(cardXOffset + ptX - TEXT_GLOW_SIZE / 2 - offsetX, cardYOffset + ptY - TEXT_GLOW_SIZE / 2); } - - if (isAnimationPanel || cardWidth < 200) { - imagePanel.setScalingType(ScalingType.nearestNeighbor); - } else { - imagePanel.setScalingType(ScalingType.bilinear); - } } @Override diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ScaledImagePanel.java b/Mage.Client/src/main/java/org/mage/card/arcane/ScaledImagePanel.java index 8a2edd3edb..9ad1d47fec 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ScaledImagePanel.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ScaledImagePanel.java @@ -7,188 +7,35 @@ import java.awt.RenderingHints; import java.awt.image.BufferedImage; import javax.swing.JPanel; +import mage.client.util.TransformedImageCache; public class ScaledImagePanel extends JPanel { private static final long serialVersionUID = -1523279873208605664L; - private volatile Image srcImage; - - private ScalingType scalingType = ScalingType.bilinear; - private boolean scaleLarger; - private MultipassType multiPassType = MultipassType.bilinear; + private volatile BufferedImage srcImage; public ScaledImagePanel () { super(false); setOpaque(false); } - public void setImage(Image srcImage) { + public void setImage(BufferedImage srcImage) { this.srcImage = srcImage; } - public void clearImage () { - srcImage = null; - repaint(); - } - - public void setScalingMultiPassType (MultipassType multiPassType) { - this.multiPassType = multiPassType; - } - - public void setScalingType (ScalingType scalingType) { - this.scalingType = scalingType; - } - - public void setScaleLarger (boolean scaleLarger) { - this.scaleLarger = scaleLarger; - } - public boolean hasImage () { return srcImage != null; } - private ScalingInfo getScalingInfo () { - int panelWidth = getWidth(); - int panelHeight = getHeight(); - int srcWidth = srcImage.getWidth(null); - int srcHeight = srcImage.getHeight(null); - int targetWidth = srcWidth; - int targetHeight = srcHeight; - if (scaleLarger || srcWidth > panelWidth || srcHeight > panelHeight) { - targetWidth = Math.round(panelHeight * (srcWidth / (float)srcHeight)); - if (targetWidth > panelWidth) { - targetHeight = Math.round(panelWidth * (srcHeight / (float)srcWidth)); - targetWidth = panelWidth; - } else { - targetHeight = panelHeight; - } - } - ScalingInfo info = new ScalingInfo(); - info.targetWidth = targetWidth; - info.targetHeight = targetHeight; - info.srcWidth = srcWidth; - info.srcHeight = srcHeight; - info.x = panelWidth / 2 - targetWidth / 2; - info.y = panelHeight / 2 - targetHeight / 2; - return info; - } - @Override public void paint (Graphics g) { if (srcImage == null) { return; } - Graphics2D g2 = (Graphics2D)g.create(); - ScalingInfo info = getScalingInfo(); - - switch (scalingType) { - case nearestNeighbor: - scaleWithDrawImage(g2, info, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - break; - case bilinear: - scaleWithDrawImage(g2, info, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - break; - case bicubic: - scaleWithDrawImage(g2, info, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - break; - case areaAveraging: - scaleWithGetScaledInstance(g2, info, Image.SCALE_AREA_AVERAGING); - break; - case replicate: - scaleWithGetScaledInstance(g2, info, Image.SCALE_REPLICATE); - break; - } + g.drawImage(TransformedImageCache.getResizedImage(srcImage, getWidth(), getHeight()), 0, 0, null); } - private void scaleWithGetScaledInstance (Graphics2D g2, ScalingInfo info, int hints) { - Image scaledImage = srcImage.getScaledInstance(info.targetWidth, info.targetHeight, hints); - g2.drawImage(scaledImage, info.x, info.y, null); - } - - private void scaleWithDrawImage (Graphics2D g2, ScalingInfo info, Object hint) { - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); - - int tempDestWidth = info.srcWidth / 2, tempDestHeight = info.srcHeight / 2; - if (tempDestWidth < info.targetWidth) { - tempDestWidth = info.targetWidth; - } - if (tempDestHeight < info.targetHeight) { - tempDestHeight = info.targetHeight; - } - - // If not doing multipass or multipass only needs a single pass, just scale it once directly to the panel surface. - if (multiPassType == MultipassType.none || (tempDestWidth == info.targetWidth && tempDestHeight == info.targetHeight)) { - g2.drawImage(srcImage, info.x, info.y, info.targetWidth, info.targetHeight, null); - return; - } - - BufferedImage tempImage = new BufferedImage(tempDestWidth, tempDestHeight, BufferedImage.TYPE_INT_RGB); - Graphics2D g2temp = tempImage.createGraphics(); - switch (multiPassType) { - case nearestNeighbor: - g2temp.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - break; - case bilinear: - g2temp.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - break; - case bicubic: - g2temp.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - break; - } - // Render first pass from image to temp. - g2temp.drawImage(srcImage, 0, 0, tempDestWidth, tempDestHeight, null); - // Render passes between the first and last pass. - int tempSrcWidth = tempDestWidth; - int tempSrcHeight = tempDestHeight; - while (true) { - if (tempDestWidth > info.targetWidth) { - tempDestWidth = tempDestWidth / 2; - if (tempDestWidth < info.targetWidth) { - tempDestWidth = info.targetWidth; - } - } - - if (tempDestHeight > info.targetHeight) { - tempDestHeight = tempDestHeight / 2; - if (tempDestHeight < info.targetHeight) { - tempDestHeight = info.targetHeight; - } - } - - if (tempDestWidth == info.targetWidth && tempDestHeight == info.targetHeight) { - break; - } - - g2temp.drawImage(tempImage, 0, 0, tempDestWidth, tempDestHeight, 0, 0, tempSrcWidth, tempSrcHeight, null); - - tempSrcWidth = tempDestWidth; - tempSrcHeight = tempDestHeight; - } - g2temp.dispose(); - // Render last pass from temp to panel surface. - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); - g2.drawImage(tempImage, info.x, info.y, info.x + info.targetWidth, info.y + info.targetHeight, 0, 0, tempSrcWidth, - tempSrcHeight, null); - } - - public Image getSrcImage() { + public BufferedImage getSrcImage() { return srcImage; } - - private static class ScalingInfo { - public int targetWidth; - public int targetHeight; - public int srcWidth; - public int srcHeight; - public int x; - public int y; - } - - public static enum MultipassType { - none, nearestNeighbor, bilinear, bicubic - } - - public static enum ScalingType { - nearestNeighbor, replicate, bilinear, bicubic, areaAveraging - } } From ffb65d48fef3e7f18549c5bd33008cb271f7c1a4 Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 16:58:10 +0200 Subject: [PATCH 08/11] Draw GlowText to a buffered image and cache it across instances Speeds up GUI performance. --- .../java/org/mage/card/arcane/GlowText.java | 150 ++++++++++++++++-- 1 file changed, 133 insertions(+), 17 deletions(-) diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/GlowText.java b/Mage.Client/src/main/java/org/mage/card/arcane/GlowText.java index c5e2f40c0d..5a32f6b872 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/GlowText.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/GlowText.java @@ -1,15 +1,30 @@ package org.mage.card.arcane; +import com.google.common.base.Function; +import com.google.common.collect.MapMaker; import javax.swing.*; import java.awt.*; import java.awt.font.FontRenderContext; import java.awt.font.LineBreakMeasurer; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.text.BreakIterator; import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import mage.client.util.ImageCaches; +import org.jdesktop.swingx.graphics.GraphicsUtilities; public class GlowText extends JLabel { private static final long serialVersionUID = 1827677946939348001L; @@ -19,6 +34,109 @@ public class GlowText extends JLabel { private Color glowColor; private boolean wrap; private int lineCount = 0; + private static Map IMAGE_CACHE; + + private final static class Key + { + final int width; + final int height; + final String text; + final Map fontAttributes; + final Color color; + final int glowSize; + final float glowIntensity; + final Color glowColor; + final boolean wrap; + + // used to pass the native font to the create function so we don't waste performance recreating it, but without holding onto the native object + final transient WeakReference originalFont; + + Font getFont() { + Font res = this.originalFont.get(); + if(res == null) + res = Font.getFont(this.fontAttributes); + return res; + } + + public Key(int width, int height, String text, Font font, Color color, int glowSize, float glowIntensity, Color glowColor, boolean wrap) { + this.width = width; + this.height = height; + this.text = text; + this.originalFont = new WeakReference<>(font); + this.fontAttributes = font.getAttributes(); + this.color = color; + this.glowSize = glowSize; + this.glowIntensity = glowIntensity; + this.glowColor = glowColor; + this.wrap = wrap; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 23 * hash + this.width; + hash = 23 * hash + this.height; + hash = 23 * hash + Objects.hashCode(this.text); + hash = 23 * hash + Objects.hashCode(this.fontAttributes); + hash = 23 * hash + Objects.hashCode(this.color); + hash = 23 * hash + this.glowSize; + hash = 23 * hash + Float.floatToIntBits(this.glowIntensity); + hash = 23 * hash + Objects.hashCode(this.glowColor); + hash = 23 * hash + (this.wrap ? 1 : 0); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Key other = (Key) obj; + if (this.width != other.width) { + return false; + } + if (this.height != other.height) { + return false; + } + if (this.glowSize != other.glowSize) { + return false; + } + if (Float.floatToIntBits(this.glowIntensity) != Float.floatToIntBits(other.glowIntensity)) { + return false; + } + if (this.wrap != other.wrap) { + return false; + } + if (!Objects.equals(this.text, other.text)) { + return false; + } + if (!Objects.equals(this.fontAttributes, other.fontAttributes)) { + return false; + } + if (!Objects.equals(this.color, other.color)) { + return false; + } + if (!Objects.equals(this.glowColor, other.glowColor)) { + return false; + } + return true; + } + } + + static { + IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap(new Function() { + @Override + public BufferedImage apply(Key key) { + return createImage(key); + } + })); + } public void setGlow (Color glowColor, int size, float intensity) { this.glowColor = glowColor; @@ -38,32 +156,32 @@ public class GlowText extends JLabel { return size; } - @Override - public void setText (String text) { - super.setText(text); - } - @Override public void paint (Graphics g) { if (getText().length() == 0) { return; } - Graphics2D g2d = (Graphics2D)g; + g.drawImage(IMAGE_CACHE.get(new Key(getWidth(), getHeight(), getText(), getFont(), getForeground(), glowSize, glowIntensity, glowColor, wrap)), 0, 0, null); + } + + private static BufferedImage createImage (Key key) { + Dimension size = new Dimension(key.width, key.height); + BufferedImage image = GraphicsUtilities.createCompatibleTranslucentImage(size.width, size.height); + Graphics2D g2d = image.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - Dimension size = getSize(); int textX = 0, textY = 0; - int wrapWidth = Math.max(0, wrap ? size.width - glowSize : Integer.MAX_VALUE); + int wrapWidth = Math.max(0, key.wrap ? size.width - key.glowSize : Integer.MAX_VALUE); - AttributedString attributedString = new AttributedString(getText()); - attributedString.addAttribute(TextAttribute.FONT, getFont()); + AttributedString attributedString = new AttributedString(key.text); + attributedString.addAttribute(TextAttribute.FONT, key.getFont()); AttributedCharacterIterator charIterator = attributedString.getIterator(); FontRenderContext fontContext = g2d.getFontRenderContext(); LineBreakMeasurer measurer = new LineBreakMeasurer(charIterator, BreakIterator.getWordInstance(Locale.ENGLISH), fontContext); - lineCount = 0; + int lineCount = 0; while (measurer.getPosition() < charIterator.getEndIndex()) { //TextLayout textLayout = measurer.nextLayout(wrapWidth); lineCount++; @@ -83,23 +201,21 @@ public class GlowText extends JLabel { float ascent = textLayout.getAscent(); textY += ascent; // Move down to baseline. - g2d.setColor(glowColor); + g2d.setColor(key.glowColor); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f)); + int glowSize = key.glowSize; textLayout.draw(g2d, textX + glowSize / 2 + 1, textY + glowSize / 2 - 1); textLayout.draw(g2d, textX + glowSize / 2 + 1, textY + glowSize / 2 + 1); textLayout.draw(g2d, textX + glowSize / 2 - 1, textY + glowSize / 2 - 1); textLayout.draw(g2d, textX + glowSize / 2 - 1, textY + glowSize / 2 + 1); - g2d.setColor(getForeground()); + g2d.setColor(key.color); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); textLayout.draw(g2d, textX + glowSize / 2, textY + glowSize / 2); textY += textLayout.getDescent() + textLayout.getLeading(); // Move down to top of next line. } - } - - public int getLineCount() { - return this.lineCount; + return image; } public void setGlowColor(Color glowColor) { From 892cfdce45161f39de7adf7d1265e3f71b51f72f Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 17:00:42 +0200 Subject: [PATCH 09/11] Draw MageRoundPane and its shadow to a buffered image and cache it across instances Speeds up GUI performance. --- .../mage/client/components/MageRoundPane.java | 160 ++++++++++++++++-- 1 file changed, 143 insertions(+), 17 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/components/MageRoundPane.java b/Mage.Client/src/main/java/mage/client/components/MageRoundPane.java index 1e3c9a5168..da4afed58e 100644 --- a/Mage.Client/src/main/java/mage/client/components/MageRoundPane.java +++ b/Mage.Client/src/main/java/mage/client/components/MageRoundPane.java @@ -1,12 +1,18 @@ package mage.client.components; +import com.google.common.base.Function; +import com.google.common.collect.MapMaker; import java.awt.BasicStroke; import java.awt.Color; +import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import java.util.Map; +import java.util.Objects; import javax.swing.JPanel; +import mage.client.util.ImageCaches; import org.jdesktop.swingx.graphics.GraphicsUtilities; import org.jdesktop.swingx.graphics.ShadowRenderer; @@ -21,23 +27,145 @@ public class MageRoundPane extends JPanel { private int X_OFFSET = 30; private int Y_OFFSET = 30; - private BufferedImage shadow = null; private final Color defaultBackgroundColor = new Color(255, 255, 255, 200); private Color backgroundColor = defaultBackgroundColor; private final int alpha = 0; + private static Map SHADOW_IMAGE_CACHE; + private static Map IMAGE_CACHE; + + static { + SHADOW_IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap(new Function() { + @Override + public BufferedImage apply(ShadowKey key) { + return createShadowImage(key); + } + })); + + IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap(new Function() { + @Override + public BufferedImage apply(Key key) { + return createImage(key); + } + })); + } + + private final static class ShadowKey + { + final int width; + final int height; + + public ShadowKey(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + this.width; + hash = 97 * hash + this.height; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ShadowKey other = (ShadowKey) obj; + if (this.width != other.width) { + return false; + } + if (this.height != other.height) { + return false; + } + return true; + } + } + + private final static class Key + { + final int width; + final int height; + final int xOffset; + final int yOffset; + final Color backgroundColor; + + public Key(int width, int height, int xOffset, int yOffset, Color backgroundColor) { + this.width = width; + this.height = height; + this.xOffset = xOffset; + this.yOffset = yOffset; + this.backgroundColor = backgroundColor; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 59 * hash + this.width; + hash = 59 * hash + this.height; + hash = 59 * hash + this.xOffset; + hash = 59 * hash + this.yOffset; + hash = 59 * hash + Objects.hashCode(this.backgroundColor); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Key other = (Key) obj; + if (this.width != other.width) { + return false; + } + if (this.height != other.height) { + return false; + } + if (this.xOffset != other.xOffset) { + return false; + } + if (this.yOffset != other.yOffset) { + return false; + } + if (!Objects.equals(this.backgroundColor, other.backgroundColor)) { + return false; + } + return true; + } + } @Override protected void paintComponent(Graphics g) { - int x = X_OFFSET; - int y = Y_OFFSET; - int w = getWidth() - 2 * X_OFFSET; - int h = getHeight() - 2 * Y_OFFSET; + g.drawImage(IMAGE_CACHE.get(new Key(getWidth(), getHeight(), X_OFFSET, Y_OFFSET, backgroundColor)), 0, 0, null); + } + + private static BufferedImage createImage(Key key) { + int x = key.xOffset; + int y = key.yOffset; + int w = key.width - 2 * key.xOffset; + int h = key.height - 2 * key.yOffset; int arc = 10; - Graphics2D g2 = (Graphics2D) g.create(); + BufferedImage image = GraphicsUtilities.createCompatibleTranslucentImage(key.width, key.height); + Graphics2D g2 = image.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - if (shadow != null) { + BufferedImage shadow = SHADOW_IMAGE_CACHE.get(new ShadowKey(w, h)); + + { int xOffset = (shadow.getWidth() - w) / 2; int yOffset = (shadow.getHeight() - h) / 2; g2.drawImage(shadow, x - xOffset, y - yOffset, null); @@ -54,7 +182,7 @@ public class MageRoundPane extends JPanel { g2.fillRoundRect(x, y, w, h, arc, arc); }*/ - g2.setColor(backgroundColor); + g2.setColor(key.backgroundColor); g2.fillRoundRect(x, y, w, h, arc, arc); ////////////////////////////////////////////////////////////////// @@ -66,6 +194,7 @@ public class MageRoundPane extends JPanel { // //////////////////////////////////////////////////////////////// g2.dispose(); + return image; } public void setXOffset(int x_offset) { @@ -76,24 +205,21 @@ public class MageRoundPane extends JPanel { Y_OFFSET = y_offset; } - @Override - public void setBounds(int x, int y, int width, int height) { - super.setBounds(x, y, width, height); - - int w = getWidth() - 2 * X_OFFSET; - int h = getHeight() - 2 * Y_OFFSET; + private static BufferedImage createShadowImage(ShadowKey key) { + int w = key.width; + int h = key.height; int arc = 10; int shadowSize = 50; - shadow = GraphicsUtilities.createCompatibleTranslucentImage(w, h); - Graphics2D g2 = shadow.createGraphics(); + BufferedImage base = GraphicsUtilities.createCompatibleTranslucentImage(w, h); + Graphics2D g2 = base.createGraphics(); g2.setColor(Color.WHITE); g2.fillRoundRect(0, 0, w, h, arc, arc); g2.dispose(); ShadowRenderer renderer = new ShadowRenderer(shadowSize, 0.5f, Color.GRAY); - shadow = renderer.createShadow(shadow); + return renderer.createShadow(base); } public void showDialog(boolean bShow) { From b8e5f5f942ecd72bcc6751e53eb4916c0ae9bd0f Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 20:36:18 +0200 Subject: [PATCH 10/11] Draw CardPanel to a buffered image and cache it across instances Speeds up GUI performance. --- .../java/org/mage/card/arcane/CardPanel.java | 140 +++++++++++++++++- 1 file changed, 133 insertions(+), 7 deletions(-) diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java index 3628b2e491..2783354192 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java @@ -1,5 +1,7 @@ package org.mage.card.arcane; +import com.google.common.base.Function; +import com.google.common.collect.MapMaker; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; @@ -23,6 +25,7 @@ import java.awt.image.BufferedImage; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.StringTokenizer; import java.util.UUID; import javax.swing.BorderFactory; @@ -38,6 +41,7 @@ import mage.cards.action.TransferData; import mage.client.dialog.PreferencesDialog; import mage.client.plugins.adapters.MageActionCallback; import mage.client.plugins.impl.Plugins; +import mage.client.util.ImageCaches; import mage.client.util.ImageHelper; import mage.client.util.audio.AudioManager; import mage.components.ImagePanel; @@ -52,6 +56,7 @@ import mage.view.PermanentView; import mage.view.StackAbilityView; import net.java.truevfs.access.TFile; import org.apache.log4j.Logger; +import org.jdesktop.swingx.graphics.GraphicsUtilities; import static org.mage.plugins.card.constants.Constants.THUMBNAIL_SIZE_FULL; import org.mage.plugins.card.dl.sources.DirectLinksForDownload; import org.mage.plugins.card.images.ImageCache; @@ -158,6 +163,111 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti // if this is set, it's opened if the user right clicks on the card panel private JPopupMenu popupMenu; + private static Map IMAGE_CACHE; + + private final static class Key + { + final int width; + final int height; + final int cardWidth; + final int cardHeight; + final int cardXOffset; + final int cardYOffset; + final boolean hasImage; + final boolean isSelected; + final boolean isChoosable; + final boolean isPlayable; + final boolean canAttack; + + public Key(int width, int height, int cardWidth, int cardHeight, int cardXOffset, int cardYOffset, boolean hasImage, boolean isSelected, boolean isChoosable, boolean isPlayable, boolean canAttack) { + this.width = width; + this.height = height; + this.cardWidth = cardWidth; + this.cardHeight = cardHeight; + this.cardXOffset = cardXOffset; + this.cardYOffset = cardYOffset; + this.hasImage = hasImage; + this.isSelected = isSelected; + this.isChoosable = isChoosable; + this.isPlayable = isPlayable; + this.canAttack = canAttack; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 19 * hash + this.width; + hash = 19 * hash + this.height; + hash = 19 * hash + this.cardWidth; + hash = 19 * hash + this.cardHeight; + hash = 19 * hash + this.cardXOffset; + hash = 19 * hash + this.cardYOffset; + hash = 19 * hash + (this.hasImage ? 1 : 0); + hash = 19 * hash + (this.isSelected ? 1 : 0); + hash = 19 * hash + (this.isChoosable ? 1 : 0); + hash = 19 * hash + (this.isPlayable ? 1 : 0); + hash = 19 * hash + (this.canAttack ? 1 : 0); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Key other = (Key) obj; + if (this.width != other.width) { + return false; + } + if (this.height != other.height) { + return false; + } + if (this.cardWidth != other.cardWidth) { + return false; + } + if (this.cardHeight != other.cardHeight) { + return false; + } + if (this.cardXOffset != other.cardXOffset) { + return false; + } + if (this.cardYOffset != other.cardYOffset) { + return false; + } + if (this.hasImage != other.hasImage) { + return false; + } + if (this.isSelected != other.isSelected) { + return false; + } + if (this.isChoosable != other.isChoosable) { + return false; + } + if (this.isPlayable != other.isPlayable) { + return false; + } + if (this.canAttack != other.canAttack) { + return false; + } + return true; + } + } + + static { + IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap(new Function() { + @Override + public BufferedImage apply(Key key) { + return createImage(key); + } + })); + } + public CardPanel(CardView newGameCard, UUID gameId, final boolean loadImage, ActionCallback callback, final boolean foil, Dimension dimension) { this.gameCard = newGameCard; this.callback = callback; @@ -475,15 +585,28 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti @Override protected void paintComponent(Graphics g) { - Graphics2D g2d = (Graphics2D) g; - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Graphics2D g2d = (Graphics2D)(g.create()); if (alpha != 1.0f) { AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha); g2d.setComposite(composite); } - if (!hasImage) { + g2d.drawImage(IMAGE_CACHE.get(new Key(getWidth(), getHeight(), cardWidth, cardHeight, cardXOffset, cardYOffset, hasImage, isSelected, isChoosable, isPlayable, canAttack)), 0, 0, null); + g2d.dispose(); + } + + private static BufferedImage createImage(Key key) { + int cardWidth = key.cardWidth; + int cardHeight = key.cardHeight; + int cardXOffset = key.cardXOffset; + int cardYOffset = key.cardYOffset; + + BufferedImage image = GraphicsUtilities.createCompatibleTranslucentImage(key.width, key.height); + Graphics2D g2d = image.createGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + if (!key.hasImage) { g2d.setColor(new Color(30, 200, 200, 120)); } else { g2d.setColor(new Color(0, 0, 0, 0)); @@ -492,19 +615,19 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti int cornerSize = Math.max(4, Math.round(cardWidth * ROUNDED_CORNER_SIZE)); g2d.fillRoundRect(cardXOffset, cardYOffset, cardWidth, cardHeight, cornerSize, cornerSize); - if (isSelected) { + if (key.isSelected) { g2d.setColor(Color.green); g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); - } else if (isChoosable) { + } else if (key.isChoosable) { g2d.setColor(new Color(250, 250, 0, 230)); g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); - } else if (isPlayable) { + } else if (key.isPlayable) { g2d.setColor(new Color(153, 102, 204, 200)); //g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); g2d.fillRoundRect(cardXOffset, cardYOffset, cardWidth, cardHeight, cornerSize, cornerSize); } - if (canAttack) { + if (key.canAttack) { g2d.setColor(new Color(0, 0, 255, 230)); g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); } @@ -515,6 +638,9 @@ public class CardPanel extends MagePermanent implements MouseListener, MouseMoti g2d.setColor(new Color(200,10,10,200)); g2d.fillRoundRect(cardXOffset+1, cardYOffset+1, cardWidth-2, cardHeight-2, cornerSize, cornerSize); }*/ + g2d.dispose(); + + return image; } @Override From 053580c412d594f79c46c3a85d4bd75af11fcd0a Mon Sep 17 00:00:00 2001 From: draxdyn Date: Wed, 1 Jun 2016 17:03:37 +0200 Subject: [PATCH 11/11] Add support for disabling animations --- .../java/org/mage/card/arcane/Animation.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/Animation.java b/Mage.Client/src/main/java/org/mage/card/arcane/Animation.java index dec3764f70..8df10872f4 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/Animation.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/Animation.java @@ -8,6 +8,8 @@ import java.util.Timer; import java.util.TimerTask; public abstract class Animation { + private static boolean ENABLED = true; + private static final long TARGET_MILLIS_PER_FRAME = 30; private static Timer timer = new Timer("Animation", true); @@ -25,6 +27,18 @@ public abstract class Animation { } public Animation (final long duration, long delay) { + if(!ENABLED) { + UI.invokeLater(new Runnable() { + @Override + public void run () { + start(); + //update(1.0f); + end(); + } + }); + return; + } + timerTask = new TimerTask() { @Override public void run () { @@ -171,6 +185,12 @@ public abstract class Animation { @Override protected void end () { + if (!state) { + parent.toggleTransformed(); + } + state = true; + panel.transformAngle = 0; + parent.onEndAnimation(); parent.repaint(); }