package magic.ui.widget.cards.canvas; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; import javax.swing.JPanel; import magic.model.IRenderableCard; import magic.ui.dialog.prefs.ImageSizePresets; import magic.ui.helpers.ImageHelper; import magic.ui.helpers.MouseHelper; import magic.ui.utility.MagicStyle; @SuppressWarnings("serial") public class CardsCanvas extends JPanel { public enum LayoutMode { SCALE_TO_FIT } private static final Color MOUSE_OVER_COLOR = MagicStyle.getRolloverColor(); private static final Color MOUSE_OVER_TCOLOR = MagicStyle.getTranslucentColor(MOUSE_OVER_COLOR, 20); private static final Color MOUSE_OVER_BORDER_COLOR = MagicStyle.getTranslucentColor(MOUSE_OVER_COLOR, 160); private int dealCardDelay = 80; // milliseconds private int removeCardDelay = 50; // millseconds private final List<CardCanvas> cards = new CopyOnWriteArrayList<>(); private final HashMap<Integer, Integer> cardTypeCount = new HashMap<>(); public boolean showIndex = true; private volatile boolean useAnimation = true; private volatile int maxCardsVisible = 0; private volatile boolean isAnimateThreadRunning = false; public Dimension preferredCardSize; private final ImageHandler imageHandler; private double cardCanvasScale = 1.0; private LayoutMode layoutMode = LayoutMode.SCALE_TO_FIT; private final double aspectRatio; private boolean stackDuplicateCards = true; private final ExecutorService executor = Executors.newFixedThreadPool(1); private Dimension canvasSize; private int currentCardIndex = -1; private boolean refreshLayout = false; private ICardsCanvasListener listener = new NullCardsCanvasListener(); private List<? extends IRenderableCard> renderableCards; public CardsCanvas() { setOpaque(false); this.preferredCardSize = ImageSizePresets.getDefaultSize(); aspectRatio = (double)this.preferredCardSize.width / this.preferredCardSize.height; this.imageHandler = new ImageHandler(null); setMouseListener(); setMouseMotionListener(); } public void setListener(ICardsCanvasListener aListener) { this.listener = aListener; } private void setMouseListener() { addMouseListener(new MouseAdapter() { @Override public void mouseExited(MouseEvent e) { clearCardHighlight(); } @Override public void mouseReleased(MouseEvent e) { if (!isAnimateThreadRunning) { MouseHelper.showBusyCursor(); final int cardIndex = getCardIndexAt(e.getPoint()); if (cardIndex >= 0) { new CardImageOverlay(cards.get(cardIndex).getCardDefinition()); } MouseHelper.showDefaultCursor(); } } }); } private void clearCardHighlight() { currentCardIndex = -1; repaint(); } private void setMouseMotionListener() { addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(final MouseEvent event) { final int cardIndex = getCardIndexAt(event.getX(), event.getY()); if (currentCardIndex != cardIndex) { if (cardIndex >= 0) { listener.cardSelected(cards.get(cardIndex).getCardDefinition()); } currentCardIndex = cardIndex; repaint(); } } }); } private int getCardIndexAt(final Point aPoint) { return getCardIndexAt(aPoint.x, aPoint.y); } private int getCardIndexAt(final int x, final int y) { for (int i = 0; i < cards.size(); i++) { final Rectangle rect = cards.get(i).getBounds(); if (x >= rect.x && y >= rect.y && x < rect.x + rect.width && y < rect.y + rect.height) { return i; } } return -1; } private Runnable getDealCardsRunnable(final List<CardCanvas> newCards) { return new Runnable() { @Override public void run() { isAnimateThreadRunning = true; clearCards(); if (isAnimationEnabled()) { preCacheImages(); } dealCards(); isAnimateThreadRunning = false; } private void dealCards() { if (useAnimation) { dealCardsAnimation(); } else { maxCardsVisible = cards.size(); repaint(); } } private void dealCardsAnimation() { while (maxCardsVisible++ < cards.size()) { repaint(); pause(dealCardDelay); } maxCardsVisible--; } private void clearCards() { if (cards != null && useAnimation) { clearCardsAnimation(); } createListOfCardCanvasObjects(newCards); } private void clearCardsAnimation() { if (maxCardsVisible > 0) { while (maxCardsVisible-- >= 0) { repaint(); pause(removeCardDelay); } } } private void pause(final long milliseconds) { try { Thread.sleep(milliseconds); } catch (InterruptedException e) { e.printStackTrace(); } } }; } private void preCacheImages() { for (CardCanvas card : cards) { final boolean isScalingRequired = !card.getSize().equals(preferredCardSize) || (cardCanvasScale != 1); if (isScalingRequired) { final BufferedImage unscaledImage = card.getFrontImage(); final int W = (int)(preferredCardSize.width * cardCanvasScale); imageHandler.getScaledImage(unscaledImage, W); } } } private void createListOfCardCanvasObjects(final List<CardCanvas> newCards) { cards.clear(); cardTypeCount.clear(); if (newCards != null) { CardCanvas lastCard = null; for (final CardCanvas cardCanvas : newCards) { if (stackDuplicateCards) { final int cardHashCode = cardCanvas.hashCode(); if (lastCard == null || cardHashCode != lastCard.hashCode()) { cards.add(cardCanvas); cardTypeCount.put(cardHashCode, 1); } else { final int cardCount = cardTypeCount.get(cardHashCode); cardTypeCount.put(cardHashCode, cardCount + 1); } lastCard = cardCanvas; } else { cards.add(cardCanvas); } } } } public void refresh(List<? extends IRenderableCard> newCards, Dimension aSize) { if (newCards == null) { return; } renderableCards = newCards; List<CardCanvas> canvasCards = getCanvasCards(newCards); this.preferredCardSize = aSize; refreshLayout = true; currentCardIndex = -1; if (useAnimation && newCards != null) { executor.execute(getDealCardsRunnable(canvasCards)); } else { createListOfCardCanvasObjects(canvasCards); maxCardsVisible = cards.size(); repaint(); } } public void refresh(List<? extends IRenderableCard> newCards) { refresh(newCards, preferredCardSize); } private List<CardCanvas> getCanvasCards(List<? extends IRenderableCard> cards) { return cards.stream() .map(card -> new CardCanvas(card)) .collect(Collectors.toList()); } public void setScale(final double newScale) { this.cardCanvasScale = newScale; repaint(); } public double getScale() { return this.cardCanvasScale; } public void setAnimationEnabled(final boolean b) { this.useAnimation = b; } public boolean isAnimationEnabled() { return useAnimation; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); drawCards(g); } private void drawCards(final Graphics g) { if (this.getWidth() > 0 && !cards.isEmpty()) { if (!getSize().equals(canvasSize) || refreshLayout || isAnimateThreadRunning) { refreshLayout = false; canvasSize = new Dimension(getSize()); setScaleToFitLayout(); } for (int i = 0; i < maxCardsVisible; i++) { final CardCanvas card = cards.get(i); drawCard(g, card); } highlightCardUnderMousePointer(g); } } private void highlightCardUnderMousePointer(final Graphics g) { if (currentCardIndex >= 0 && !isAnimateThreadRunning) { final Rectangle rect = cards.get(currentCardIndex).getBounds(); final Graphics2D g2d = (Graphics2D) g; drawHighlightOverlay(g2d, rect); // drawHighlightBorder(g2d, rect); } } private void drawHighlightBorder(Graphics2D g2d, Rectangle rect) { final int w = 4; g2d.setStroke(new BasicStroke(w)); g2d.setPaint(MOUSE_OVER_BORDER_COLOR); g2d.drawRect(rect.x + (w / 2), rect.y + (w / 2), rect.width - w, rect.height - w); } private void drawHighlightOverlay(Graphics2D g2d, Rectangle rect) { g2d.setPaint(MOUSE_OVER_TCOLOR); g2d.fillRect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2); } public void incrementCardWidth() { final int newWidth = preferredCardSize.width + 1; final int newHeight = (int)(newWidth / aspectRatio); preferredCardSize = new Dimension(newWidth, newHeight); repaint(); } private void drawCard(final Graphics g, final CardCanvas canvasCard) { final int X = canvasCard.getBounds().x; final int Y = canvasCard.getBounds().y; final int W = canvasCard.getBounds().width; final int H = canvasCard.getBounds().height; g.drawImage(ImageHelper.scale(canvasCard.getFrontImage(), W, H), X, Y, null); if (stackDuplicateCards) { drawCardCount(g, X, Y, W, H, canvasCard); } } private void drawCardCount(Graphics g, int X, int Y, int W, int H, final CardCanvas card) { if (cardTypeCount.isEmpty()) { return; } final int cardCount = cardTypeCount.get(card.hashCode()); if (cardCount > 1) { g.setColor(Color.WHITE); final String text = Integer.toString(cardCount); int h = (int)(H * 0.15); h = h > 8 ? (int)(H * 0.15) : 9; final Font f = new Font("Dialog", Font.BOLD, h); final int w = g.getFontMetrics(f).stringWidth(text); g.setFont(f); drawStringWithOutline(g, text, X + ((W - w) / 2), Y + ((H - h) / 3)); } } private void drawStringWithOutline(final Graphics g, final String str, int x, int y) { Graphics2D g2d = (Graphics2D)g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setColor(Color.DARK_GRAY); for (int i = 1; i <= 2; i++) { g.drawString(str,x+i,y); g.drawString(str,x-i,y); g.drawString(str,x,y+i); g.drawString(str,x,y-i); } g.setColor(Color.WHITE); g.drawString(str,x,y); } public void setLayoutMode(final LayoutMode layout) { this.layoutMode = layout; if (this.getWidth() > 0) { repaint(); } } public LayoutMode getLayoutMode() { return this.layoutMode; } public boolean isBusy() { return isAnimateThreadRunning; } private Dimension getLayoutGridDimensions() { final int cardCount = cards.size(); final int containerWidth = this.getWidth(); final int containerHeight = this.getHeight(); final double containerAspectRatio = (double)containerWidth / containerHeight; final double normalizedAspectRatio = containerAspectRatio / aspectRatio; final double cols = Math.sqrt(cardCount * normalizedAspectRatio); final double rows = Math.sqrt(cardCount / normalizedAspectRatio); final int floorR = (int)Math.floor(rows); final int ceilR = (int)Math.ceil(rows); final int floorC = (int)Math.floor(cols); final int ceilC = (int)Math.ceil(cols); final int[] cells = new int[3]; cells[0] = floorR * ceilC; cells[1] = ceilR * floorC; cells[2] = ceilR * ceilC; Arrays.sort(cells); if (cells[0] >= cardCount) { return new Dimension(ceilC, floorR); } else if (cells[1] >= cardCount) { return new Dimension(floorC, ceilR); } else { return new Dimension(ceilC, ceilR); } } private void setScaleToFitLayout() { final int containerHeight = this.getHeight(); final int containerWidth = this.getWidth(); final int totalCards = cards.size(); if (containerWidth == 0 || totalCards == 0) { return; } final Dimension grid = getLayoutGridDimensions(); int cardWidth = Math.min(containerWidth/grid.width, preferredCardSize.width); int cardHeight = (int)(cardWidth / aspectRatio); if ((cardHeight * grid.height) > containerHeight) { cardHeight = Math.min(containerHeight/grid.height, preferredCardSize.height); cardWidth = (int)(cardHeight * aspectRatio); } final Rectangle rect = new Rectangle(grid.width * cardWidth, grid.height * cardHeight); final int xStart = (containerWidth - rect.width) / 2; final int yStart = (containerHeight - rect.height) / 2; int row = 0; int col = 0; for (int cardIndex = 0; cardIndex < totalCards; cardIndex++) { final CardCanvas card = cards.get(cardIndex); int xPoint = xStart + (col * cardWidth); int yPoint = yStart + (row * (cardHeight-1)); card.setPosition(new Point(xPoint, yPoint)); card.setSize(cardWidth, cardHeight); col++; if (col >= grid.width) { col = 0; row++; } } cardCanvasScale = (double)cardWidth / preferredCardSize.width; } public void setAnimationDelay(final int dealCardDelay, final int removeCardDelay) { this.dealCardDelay = dealCardDelay; this.removeCardDelay = removeCardDelay; } public void setStackDuplicateCards(boolean stackDuplicateCards) { this.stackDuplicateCards = stackDuplicateCards; if (renderableCards != null) { List<CardCanvas> canvasCards = getCanvasCards(renderableCards); refreshLayout = true; currentCardIndex = -1; createListOfCardCanvasObjects(canvasCards); maxCardsVisible = cards.size(); repaint(); } } public void setCard(IRenderableCard aCard) { List<IRenderableCard> lst = new ArrayList<>(); lst.add(aCard); refresh(lst); } public void clear() { cards.clear(); cardTypeCount.clear(); repaint(); } }