/* * 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 org.mage.card.arcane; import mage.ObjectColor; import mage.cards.ArtRect; import mage.cards.FrameStyle; import mage.client.dialog.PreferencesDialog; import mage.constants.CardType; import mage.constants.MageObjectType; import mage.view.CardView; import mage.view.PermanentView; import org.apache.log4j.Logger; import javax.swing.*; import java.awt.*; import java.awt.font.*; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.text.CharacterIterator; import java.util.ArrayList; import java.util.Collection; import java.util.List; /* private void cardRendererBasedRender(Graphics2D g) { // Prepare for draw g.translate(cardXOffset, cardYOffset); int cardWidth = this.cardWidth - cardXOffset; int cardHeight = this.cardHeight - cardYOffset; // AA on g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Renderer CardRenderer render = new ModernCardRenderer(gameCard, transformed); Image img = imagePanel.getSrcImage(); if (img != null) { render.setArtImage(img); } render.draw(g, cardWidth, cardHeight); } */ /** * @author stravant@gmail.com * * Base rendering class for new border cards */ public class ModernCardRenderer extends CardRenderer { private final static Logger LOGGER = Logger.getLogger(ModernCardRenderer.class); /////////////////////////////////////////////////////////////////////////// // Textures for modern frame cards private static TexturePaint loadBackgroundTexture(String name) { URL url = ModernCardRenderer.class.getResource("/cardrender/background_texture_" + name + ".png"); ImageIcon icon = new ImageIcon(url); BufferedImage img = CardRendererUtils.toBufferedImage(icon.getImage()); return new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); } private static BufferedImage loadFramePart(String name) { URL url = ModernCardRenderer.class.getResource("/cardrender/" + name + ".png"); ImageIcon icon = new ImageIcon(url); return CardRendererUtils.toBufferedImage(icon.getImage()); } private static Font loadFont(String name) { try(InputStream in = ModernCardRenderer.class.getResourceAsStream("/cardrender/" + name + ".ttf")) { return Font.createFont( Font.TRUETYPE_FONT,in); } catch (IOException e) { LOGGER.info("Failed to load font `" + name + "`, couldn't find resource."); } catch (FontFormatException e) { LOGGER.info("Failed to load font `" + name + "`, bad format."); } return new Font("Arial", Font.PLAIN, 1); } public static final Font BASE_BELEREN_FONT = loadFont("beleren-bold"); public static final Paint BG_TEXTURE_WHITE = loadBackgroundTexture("white"); public static final Paint BG_TEXTURE_BLUE = loadBackgroundTexture("blue"); public static final Paint BG_TEXTURE_BLACK = loadBackgroundTexture("black"); public static final Paint BG_TEXTURE_RED = loadBackgroundTexture("red"); public static final Paint BG_TEXTURE_GREEN = loadBackgroundTexture("green"); public static final Paint BG_TEXTURE_GOLD = loadBackgroundTexture("gold"); public static final Paint BG_TEXTURE_ARTIFACT = loadBackgroundTexture("artifact"); public static final Paint BG_TEXTURE_LAND = loadBackgroundTexture("land"); public static final Paint BG_TEXTURE_VEHICLE = loadBackgroundTexture("vehicle"); public static final BufferedImage FRAME_INVENTION = loadFramePart("invention_frame"); public static final Color BORDER_WHITE = new Color(216, 203, 188); public static final Color BORDER_BLUE = new Color(20, 121, 175); public static final Color BORDER_BLACK = new Color(45, 45, 35); public static final Color BORDER_RED = new Color(201, 71, 58); public static final Color BORDER_GREEN = new Color(4, 136, 69); public static final Color BORDER_GOLD = new Color(255, 228, 124); public static final Color BORDER_COLORLESS = new Color(238, 242, 242); public static final Color BORDER_LAND = new Color(190, 173, 115); public static final Color BOX_WHITE = new Color(244, 245, 239); public static final Color BOX_BLUE = new Color(201, 223, 237); public static final Color BOX_BLACK = new Color(204, 194, 192); public static final Color BOX_RED = new Color(246, 208, 185); public static final Color BOX_GREEN = new Color(205, 221, 213); public static final Color BOX_GOLD = new Color(223, 195, 136); public static final Color BOX_COLORLESS = new Color(220, 228, 232); public static final Color BOX_LAND = new Color(220, 215, 213); public static final Color BOX_INVENTION = new Color(209, 97, 33); public static final Color BOX_VEHICLE = new Color(155, 105, 60); public static final Color BOX_WHITE_NIGHT = new Color(169, 160, 145); public static final Color BOX_BLUE_NIGHT = new Color(46, 133, 176); public static final Color BOX_BLACK_NIGHT = new Color(95, 90, 89); public static final Color BOX_RED_NIGHT = new Color(188, 87, 57); public static final Color BOX_GREEN_NIGHT = new Color(31, 100, 44); public static final Color BOX_GOLD_NIGHT = new Color(171, 134, 70); public static final Color BOX_COLORLESS_NIGHT = new Color(118, 147, 158); public static final Color LAND_TEXTBOX_WHITE = new Color(248, 232, 188, 244); public static final Color LAND_TEXTBOX_BLUE = new Color(189, 212, 236, 244); public static final Color LAND_TEXTBOX_BLACK = new Color(174, 164, 162, 244); public static final Color LAND_TEXTBOX_RED = new Color(242, 168, 133, 244); public static final Color LAND_TEXTBOX_GREEN = new Color(198, 220, 198, 244); public static final Color LAND_TEXTBOX_GOLD = new Color(236, 229, 207, 244); public static final Color TEXTBOX_WHITE = new Color(252, 249, 244, 244); public static final Color TEXTBOX_BLUE = new Color(229, 238, 247, 244); public static final Color TEXTBOX_BLACK = new Color(241, 241, 240, 244); public static final Color TEXTBOX_RED = new Color(243, 224, 217, 244); public static final Color TEXTBOX_GREEN = new Color(217, 232, 223, 244); public static final Color TEXTBOX_GOLD = new Color(240, 234, 209, 244); public static final Color TEXTBOX_COLORLESS = new Color(219, 229, 233, 244); public static final Color TEXTBOX_LAND = new Color(218, 214, 212, 244); public static final Color ERROR_COLOR = new Color(255, 0, 255); /////////////////////////////////////////////////////////////////////////// // Layout metrics for modern border cards // How far the main box, art, and name / type line are inset from the // card border. That is, the width of background texture that shows around // the edge of the card. protected int contentInset; // Helper: The total inset from card edge to rules box etc. // = borderWidth + contentInset protected int totalContentInset; // Width of the content region of the card // = cardWidth - 2 x totalContentInset protected int contentWidth; // How tall the name / type lines and P/T box are protected static final float BOX_HEIGHT_FRAC = 0.065f; // x cardHeight protected static final int BOX_HEIGHT_MIN = 16; protected int boxHeight; // How far down the card is the type line placed? protected static final float TYPE_LINE_Y_FRAC = 0.57f; // x cardHeight protected static final float TYPE_LINE_Y_FRAC_TOKEN = 0.70f; protected static final float TYPE_LINE_Y_FRAC_FULL_ART = 0.74f; protected int typeLineY; // Possible sizes of rules text font protected static final int[] RULES_TEXT_FONT_SIZES = {24, 18, 15, 12, 9}; // How large is the box text, and how far is it down the boxes protected int boxTextHeight; protected int boxTextOffset; protected Font boxTextFont; protected Font boxTextFontNarrow; // How large is the P/T text, and how far is it down the boxes protected int ptTextHeight; protected int ptTextOffset; protected Font ptTextFont; // Processed mana cost string protected final String manaCostString; public ModernCardRenderer(CardView card, boolean isTransformed) { // Pass off to parent super(card, isTransformed); // Mana cost string manaCostString = ManaSymbols.getStringManaCost(cardView.getManaCost()); } @Override protected void layout(int cardWidth, int cardHeight) { // Pass to parent super.layout(cardWidth, cardHeight); // Content inset, just equal to border width contentInset = borderWidth; // Total content inset helper totalContentInset = borderWidth + contentInset; // Content width contentWidth = cardWidth - 2 * totalContentInset; // Box height boxHeight = (int) Math.max( BOX_HEIGHT_MIN, BOX_HEIGHT_FRAC * cardHeight); // Type line at typeLineY = (int) (getTypeLineYFrac() * cardHeight); // Box text height boxTextHeight = getTextHeightForBoxHeight(boxHeight); boxTextOffset = (boxHeight - boxTextHeight) / 2; // Not using Beleren for now because it looks bad at small font sizes. Maybe we want to in the future? //boxTextFont = BASE_BELEREN_FONT.deriveFont(Font.PLAIN, boxTextHeight); boxTextFont = new Font("Arial", Font.PLAIN, boxTextHeight); boxTextFontNarrow = new Font("Arial Narrow", Font.PLAIN, boxTextHeight); // Box text height ptTextHeight = getPTTextHeightForLineHeight(boxHeight); ptTextOffset = (boxHeight - ptTextHeight) / 2; // Beleren font does work well for numbers though ptTextFont = BASE_BELEREN_FONT.deriveFont(Font.PLAIN, ptTextHeight); } @Override protected void drawBorder(Graphics2D g) { // Selection Borders Color borderColor; if (isSelected) { borderColor = Color.green; } else if (isChoosable) { borderColor = new Color(250, 250, 0, 230); } else if (cardView.isPlayable()) { borderColor = new Color(153, 102, 204, 200); } else if (cardView.isCanAttack()) { borderColor = new Color(0, 0, 255, 230); } else { borderColor = Color.BLACK; } // Draw border as one rounded rectangle g.setColor(borderColor); g.fillRoundRect(0, 0, cardWidth, cardHeight, cornerRadius, cornerRadius); /* // Separate selection highlight border from card itself. Not used right now if (borderColor != null) { float hwidth = borderWidth / 2.0f; Graphics2D g2 = (Graphics2D) g.create(); g2.setColor(borderColor); g2.setStroke(new BasicStroke(borderWidth)); RoundRectangle2D.Float rect = new RoundRectangle2D.Float( hwidth, hwidth, cardWidth - borderWidth, cardHeight - borderWidth, cornerRadius, cornerRadius); g2.draw(rect); g2.dispose(); } */ } @Override protected void drawBackground(Graphics2D g) { // Draw background, in 3 parts if (cardView.isFaceDown()) { // Just draw a brown rectangle drawCardBack(g); } else { // Set texture to paint with g.setPaint(getBackgroundPaint(cardView.getColor(), cardView.getCardTypes(), cardView.getSubTypes())); // Draw main part (most of card) g.fillRoundRect( borderWidth, borderWidth, cardWidth - borderWidth * 2, cardHeight - borderWidth * 4 - cornerRadius * 2, cornerRadius - 1, cornerRadius - 1); // Draw the M15 rounded "swoosh" at the bottom g.fillRoundRect( borderWidth, cardHeight - borderWidth * 4 - cornerRadius * 4, cardWidth - borderWidth * 2, cornerRadius * 4, cornerRadius * 2, cornerRadius * 2); // Draw the cutout into the "swoosh" for the textbox to lie over g.fillRect( borderWidth + contentInset, cardHeight - borderWidth * 5, cardWidth - borderWidth * 2 - contentInset * 2, borderWidth * 2); } } /** * Get the region to slice out of a source art image for the card * * @return */ private Rectangle2D getArtRect() { Rectangle2D rect; if (useInventionFrame()) { rect = new Rectangle2D.Float(0, 0, 1, 1); } else if (cardView.getFrameStyle().isFullArt() || (cardView.isToken())) { rect = new Rectangle2D.Float(.079f, .11f, .84f, .63f); } else { rect = ArtRect.NORMAL.rect; } return rect; } private float getTypeLineYFrac() { if (cardView.isToken() && cardView.getCardNumber() == null) { return TYPE_LINE_Y_FRAC_TOKEN; } else if (cardView.getFrameStyle().isFullArt()) { return TYPE_LINE_Y_FRAC_FULL_ART; } else { return TYPE_LINE_Y_FRAC; } } protected boolean isSourceArtFullArt() { int color = artImage.getRGB(0, artImage.getHeight() / 2); return (((color & 0x00FF0000) > 0x00200000) || ((color & 0x0000FF00) > 0x00002000) || ((color & 0x000000FF) > 0x00000020)); } private boolean useInventionFrame() { if (cardView.getFrameStyle() != FrameStyle.KLD_INVENTION) { return false; } else if (artImage == null) { return true; } else { return isSourceArtFullArt(); } } @Override protected void drawArt(Graphics2D g) { if (artImage != null && !cardView.isFaceDown()) { // Invention rendering, art fills the entire frame if (useInventionFrame()) { drawArtIntoRect(g, borderWidth, borderWidth, cardWidth - 2*borderWidth, cardHeight - 2*borderWidth, getArtRect(), false); } boolean shouldPreserveAspect = true; Rectangle2D sourceRect = getArtRect(); if (cardView.getMageObjectType() == MageObjectType.SPELL) { ArtRect rect = cardView.getArtRect(); if (rect == ArtRect.SPLIT_FUSED) { // Special handling for fused, draw the art from both halves stacked on top of one and other // each filling half of the art rect drawArtIntoRect(g, totalContentInset + 1, totalContentInset + boxHeight, contentWidth - 2, (typeLineY - totalContentInset - boxHeight)/2, ArtRect.SPLIT_LEFT.rect, useInventionFrame()); drawArtIntoRect(g, totalContentInset + 1, totalContentInset + boxHeight + (typeLineY - totalContentInset - boxHeight)/2, contentWidth - 2, (typeLineY - totalContentInset - boxHeight)/2, ArtRect.SPLIT_RIGHT.rect, useInventionFrame()); return; } else if (rect != ArtRect.NORMAL) { sourceRect = rect.rect; shouldPreserveAspect = false; } } // Normal drawing of art from a source part of the card frame into the rect drawArtIntoRect(g, totalContentInset + 1, totalContentInset + boxHeight, contentWidth - 2, typeLineY - totalContentInset - boxHeight, sourceRect, shouldPreserveAspect); } } @Override protected void drawFrame(Graphics2D g) { // Get the card colors to base the frame on ObjectColor frameColors = getFrameObjectColor(); // Get the border paint Color boxColor = getBoxColor(frameColors, cardView.getCardTypes(), isTransformed); Paint textboxPaint = getTextboxPaint(frameColors, cardView.getCardTypes(), cardWidth); Paint borderPaint = getBorderPaint(frameColors, cardView.getCardTypes(), cardWidth); // Special colors if (cardView.getFrameStyle() == FrameStyle.KLD_INVENTION) { boxColor = BOX_INVENTION; } // Draw the main card content border g.setPaint(borderPaint); if (cardView.getFrameStyle() == FrameStyle.KLD_INVENTION) { g.drawImage(FRAME_INVENTION, 0, 0, cardWidth, cardHeight, null); g.drawRect( totalContentInset, typeLineY, contentWidth - 1, cardHeight - borderWidth * 3 - typeLineY - 1); } else { g.drawRect( totalContentInset, totalContentInset, contentWidth - 1, cardHeight - borderWidth * 3 - totalContentInset - 1); } // Draw the textbox fill if (useInventionFrame()) { g.setPaint(new Color(255, 255, 255, 150)); } else { g.setPaint(textboxPaint); } g.fillRect( totalContentInset + 1, typeLineY, contentWidth - 2, cardHeight - borderWidth * 3 - typeLineY - 1); // If it's a planeswalker, extend the textbox left border by some if (cardView.isPlanesWalker()) { g.setPaint(borderPaint); g.fillRect( totalContentInset, typeLineY + boxHeight, cardWidth / 16, cardHeight - typeLineY - boxHeight - borderWidth * 3); } if (cardView.getFrameStyle() != FrameStyle.KLD_INVENTION) { // Draw a shadow highlight at the right edge of the content frame g.setColor(new Color(0, 0, 0, 100)); g.fillRect( totalContentInset - 1, totalContentInset, 1, cardHeight - borderWidth * 3 - totalContentInset - 1); // Draw a shadow highlight separating the card art and rest of frame g.drawRect( totalContentInset + 1, totalContentInset + boxHeight, contentWidth - 3, typeLineY - totalContentInset - boxHeight - 1); } // Draw the name line box CardRendererUtils.drawRoundedBox(g, borderWidth, totalContentInset, cardWidth - 2 * borderWidth, boxHeight, contentInset, borderPaint, boxColor); // Draw the type line box CardRendererUtils.drawRoundedBox(g, borderWidth, typeLineY, cardWidth - 2 * borderWidth, boxHeight, contentInset, borderPaint, boxColor); // Draw a small separator between the type line and box, and shadow // at the left of the texbox, and above the name line g.setColor(new Color(0, 0, 0, 150)); g.fillRect( totalContentInset - 1, totalContentInset - 1, contentWidth + 1, 1); g.fillRect( totalContentInset + 1, typeLineY + boxHeight, contentWidth - 2, 1); g.fillRect( cardWidth - totalContentInset - 1, typeLineY + boxHeight, 1, cardHeight - borderWidth * 3 - typeLineY - boxHeight); // Draw the transform circle int nameOffset = drawTransformationCircle(g, borderPaint); // Draw the name line drawNameLine(g, cardView.getDisplayName(), manaCostString, totalContentInset + nameOffset, totalContentInset, contentWidth - nameOffset, boxHeight); // Draw the type line drawTypeLine(g, getCardTypeLine(), totalContentInset, typeLineY, contentWidth, boxHeight); // Draw the textbox rules drawRulesText(g, textboxKeywords, textboxRules, totalContentInset + 2, typeLineY + boxHeight + 2, contentWidth - 4, cardHeight - typeLineY - boxHeight - 4 - borderWidth * 3); // Draw the bottom right stuff drawBottomRight(g, borderPaint, boxColor); } // Draw the name line protected void drawNameLine(Graphics2D g, String baseName, String manaCost, int x, int y, int w, int h) { // Width of the mana symbols int manaCostWidth; if (cardView.isAbility()) { manaCostWidth = 0; } else { manaCostWidth = CardRendererUtils.getManaCostWidth(manaCost, boxTextHeight); } // Available width for name. Add a little bit of slop so that one character // can partially go underneath the mana cost int availableWidth = w - manaCostWidth + 2; // Draw the name String nameStr; if (cardView.isFaceDown()) { if (cardView instanceof PermanentView && ((PermanentView) cardView).isManifested()) { nameStr = "Manifest: " + cardView.getName(); } else { nameStr = "Morph: " + cardView.getName(); } } else { nameStr = baseName; } if (!nameStr.isEmpty()) { AttributedString str = new AttributedString(nameStr); str.addAttribute(TextAttribute.FONT, boxTextFont); TextMeasurer measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext()); int breakIndex = measure.getLineBreakIndex(0, availableWidth); if (breakIndex < nameStr.length()) { str = new AttributedString(nameStr); str.addAttribute(TextAttribute.FONT, boxTextFontNarrow); measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext()); breakIndex = measure.getLineBreakIndex(0, availableWidth); } if (breakIndex > 0) { TextLayout layout = measure.getLayout(0, breakIndex); g.setColor(getBoxTextColor()); layout.draw(g, x, y + boxTextOffset + boxTextHeight - 1); } } // Draw the mana symbols if (!cardView.isAbility() && !cardView.isFaceDown()) { ManaSymbols.draw(g, manaCost, x + w - manaCostWidth, y + boxTextOffset, boxTextHeight); } } // Draw the type line (color indicator, types, and expansion symbol) protected void drawTypeLine(Graphics2D g, String baseTypeLine, int x, int y, int w, int h) { // Draw expansion symbol int expansionSymbolWidth; if (PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_RENDERING_SET_SYMBOL, "false").equals("false")) { if (cardView.isAbility()) { expansionSymbolWidth = 0; } else { expansionSymbolWidth = drawExpansionSymbol(g, x, y, w, h); } } else { expansionSymbolWidth = 0; } // Draw type line text int availableWidth = w - expansionSymbolWidth + 1; String types = baseTypeLine; g.setFont(boxTextFont); // Replace "Legendary" in type line if there's not enough space if (g.getFontMetrics().stringWidth(types) > availableWidth) { types = types.replace("Legendary", "L."); } if (!types.isEmpty()) { AttributedString str = new AttributedString(types); str.addAttribute(TextAttribute.FONT, boxTextFont); TextMeasurer measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext()); int breakIndex = measure.getLineBreakIndex(0, availableWidth); if (breakIndex < types.length()) { str = new AttributedString(types); str.addAttribute(TextAttribute.FONT, boxTextFontNarrow); measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext()); breakIndex = measure.getLineBreakIndex(0, availableWidth); } if (breakIndex > 0) { TextLayout layout = measure.getLayout(0, breakIndex); g.setColor(getBoxTextColor()); layout.draw(g, x, y + (h - boxTextHeight) / 2 + boxTextHeight - 1); } } } // Draw the P/T and/or Loyalty boxes protected void drawBottomRight(Graphics2D g, Paint borderPaint, Color fill) { // No bottom right for abilities if (cardView.isAbility()) { return; } // Where to start drawing the things int curY = cardHeight - (int) (0.03f * cardHeight); // Width of the boxes int partWidth = (int) Math.max(30, 0.20f * cardWidth); // Is it a creature? boolean isVehicle = cardView.getSubTypes().contains("Vehicle"); if (cardView.isCreature() || isVehicle) { int x = cardWidth - borderWidth - partWidth; // Draw PT box CardRendererUtils.drawRoundedBox(g, x, curY - boxHeight, partWidth, boxHeight, contentInset, borderPaint, isVehicle ? BOX_VEHICLE : fill); // Draw shadow line top g.setColor(new Color(0, 0, 0, 150)); g.fillRect( x + contentInset, curY - boxHeight - 1, partWidth - 2 * contentInset, 1); // Draw text Color textColor; if (isVehicle) { boolean isAnimated = !(cardView instanceof PermanentView) || cardView.isCreature(); if (isAnimated) { textColor = Color.white; } else { textColor = new Color(180, 180, 180); } } else { textColor = getBoxTextColor(); } g.setColor(textColor); g.setFont(ptTextFont); String ptText = cardView.getPower() + '/' + cardView.getToughness(); int ptTextWidth = g.getFontMetrics().stringWidth(ptText); g.drawString(ptText, x + (partWidth - ptTextWidth) / 2, curY - ptTextOffset - 1); // Advance curY -= boxHeight; } // Is it a walker? (But don't draw the box if it's a non-permanent view // of a walker without a starting loyalty (EG: Arlin Kord's flipped side). if (cardView.isPlanesWalker() && (cardView instanceof PermanentView || !cardView.getStartingLoyalty().equals("0"))) { // Draw the PW loyalty box int w = partWidth; int h = partWidth / 2; int x = cardWidth - partWidth - borderWidth; int y = curY - h; Polygon symbol = new Polygon( new int[]{ x + w / 2, (int) (x + w * 0.9), x + w, (int) (x + w * 0.6), x + w / 2, (int) (x + w * 0.4), x, (int) (x + w * 0.1),}, new int[]{ y + h, (int) (y + 0.8 * h), y, (int) (y - 0.2 * h), y, (int) (y - 0.2 * h), y, (int) (y + 0.8 * h),}, 8); // Draw + stroke g.setColor(Color.black); g.fillPolygon(symbol); g.setColor(new Color(200, 200, 200)); g.setStroke(new BasicStroke(2)); g.drawPolygon(symbol); g.setStroke(new BasicStroke(1)); // Loyalty number String loyalty; if (cardView instanceof PermanentView) { loyalty = cardView.getLoyalty(); } else { loyalty = cardView.getStartingLoyalty(); } g.setFont(ptTextFont); g.setColor(Color.white); int loyaltyWidth = g.getFontMetrics().stringWidth(loyalty); g.drawString(loyalty, x + (w - loyaltyWidth) / 2, y + ptTextHeight + (h - ptTextHeight) / 2); // Advance curY -= (int) (1.2 * y); } // does it have damage on it? if ((cardView instanceof PermanentView) && ((PermanentView) cardView).getDamage() > 0) { int x = cardWidth - partWidth - borderWidth; int y = curY - boxHeight; String damage = String.valueOf(((PermanentView) cardView).getDamage()); g.setFont(ptTextFont); int txWidth = g.getFontMetrics().stringWidth(damage); g.setColor(Color.red); g.fillRect(x, y, partWidth, boxHeight); g.setColor(Color.white); g.drawRect(x, y, partWidth, boxHeight); g.drawString(damage, x + (partWidth - txWidth) / 2, curY - 1); } } // Draw the card's textbox in a given rect protected boolean loyaltyAbilityColorToggle = false; private static class RuleLayout { public List<AttributedString> attributedRules; public int remainingHeight; public boolean fits; public Font font; public Font fontItalic; } /** * Figure out if a given text size will work for laying out the rules in a * card textbox */ protected RuleLayout layoutRules(Graphics2D g, List<TextboxRule> rules, int w, int h, int fontSize) { // The fonts to try Font font = new Font("Arial", Font.PLAIN, fontSize); Font fontItalic = new Font("Arial", Font.ITALIC, fontSize); // Get the total height of the rules List<AttributedString> attributedRules = new ArrayList<>(); boolean fits = true; int remaining = h; for (TextboxRule rule : rules) { AttributedString attributed = rule.generateAttributedString(font, fontItalic); attributedRules.add(attributed); remaining -= drawSingleRule(g, attributed, rule, 0, 0, w, remaining, /*doDraw=*/ false); if (remaining < 0) { fits = false; break; } } // Return the information RuleLayout layout = new RuleLayout(); layout.attributedRules = attributedRules; layout.remainingHeight = remaining; layout.fits = fits; layout.font = font; layout.fontItalic = fontItalic; return layout; } protected void drawRulesText(Graphics2D g, ArrayList<TextboxRule> keywords, ArrayList<TextboxRule> rules, int x, int y, int w, int h) { // Gather all rules to render List<TextboxRule> allRules = new ArrayList<>(rules); // Add the keyword rule if there are any keywords if (!keywords.isEmpty()) { String keywordRulesString = getKeywordRulesString(keywords); TextboxRule keywordsRule = new TextboxRule(keywordRulesString, new ArrayList<>()); allRules.add(0, keywordsRule); } // Basic mana draw mana symbol in textbox (for basic lands) if (allRules.size() == 1 && (allRules.get(0) instanceof TextboxBasicManaRule) && cardView.isLand()) { drawBasicManaTextbox(g, x, y, w, h, ((TextboxBasicManaRule) allRules.get(0)).getBasicManaSymbol()); return; } // Go through possible font sizes in descending order to find the best fit RuleLayout bestLayout = null; for (int fontSize : RULES_TEXT_FONT_SIZES) { bestLayout = layoutRules(g, allRules, w, h, fontSize); // Stop, we found a good fit if (bestLayout.fits) { break; } } // Nothing to draw if (bestLayout == null) { return; } // Do we have room for additional padding between the parts of text? // If so, calculate the padding based on how much space was left over int padding; if (bestLayout.fits) { padding = (int) (((float) bestLayout.remainingHeight) / (1 + allRules.size())); } else { // When the text doesn't fit to begin with there's no room for padding padding = 0; } // Do the actual draw loyaltyAbilityColorToggle = false; g.setColor(Color.black); int curY = y + padding; for (int i = 0; i < bestLayout.attributedRules.size(); ++i) { AttributedString attributedRule = bestLayout.attributedRules.get(i); TextboxRule rule = allRules.get(i); int adv = drawSingleRule(g, attributedRule, rule, x, curY, w, h, true); curY += adv + padding; h -= adv; if (h < 0) { break; } } } // Draw a basic mana symbol private void drawBasicManaTextbox(Graphics2D g, int x, int y, int w, int h, String symbol) { String symbs = symbol; int symbHeight = (int) (0.8 * h); int manaCostWidth = CardRendererUtils.getManaCostWidth(symbs, symbHeight); ManaSymbols.draw(g, symbs, x + (w - manaCostWidth) / 2, y + (h - symbHeight) / 2, symbHeight); } // Get the first line of the textbox, the keyword string private static String getKeywordRulesString(ArrayList<TextboxRule> keywords) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < keywords.size(); ++i) { builder.append(keywords.get(i).text); if (i != keywords.size() - 1) { builder.append(", "); } } return builder.toString(); } // Draw a single rule and returns the amount vertically advanced by, but // only if doDraw is true. If doDraw is false, just returns the vertical // advance if the rule were to be drawn. private int drawSingleRule(Graphics2D g, AttributedString text, TextboxRule rule, int x, int y, int w, int h, boolean doDraw) { // Inset, in case we are a leveler or loyalty ability int inset = 0; if (rule != null && rule.type == TextboxRuleType.LOYALTY) { inset = cardWidth / 12; } int availWidth = w - inset; FontRenderContext frc = g.getFontRenderContext(); AttributedCharacterIterator textIter = text.getIterator(); LineBreakMeasurer measure = new LineBreakMeasurer(textIter, frc); float yPos = y; float remain = h; AttributedCharacterIterator newLineCheck = text.getIterator(); while (measure.getPosition() < textIter.getEndIndex()) { // Advance iterator to next line break newLineCheck.setIndex(measure.getPosition()); char ch; while ((ch = newLineCheck.next()) != CharacterIterator.DONE) { if (ch == '\n') { break; } } // Get the text layout TextLayout layout = measure.nextLayout(availWidth, newLineCheck.getIndex(), false); float ascent = layout.getAscent(); yPos += ascent; remain -= ascent; if (remain < 0) { break; } if (doDraw) { g.setColor(Color.black); layout.draw(g, x + inset, yPos); } yPos += layout.getDescent() + layout.getLeading() - 2; } // Advance int advance = ((int) Math.ceil(yPos)) - y; // Is it a loyalty ability? if (rule != null && rule.type == TextboxRuleType.LOYALTY) { TextboxLoyaltyRule loyaltyRule = (TextboxLoyaltyRule) rule; Polygon symbol; int symbolWidth = (x + inset) - borderWidth - 4; int symbolHeight = (int) (0.7f * symbolWidth); if (symbolHeight > advance) { advance = symbolHeight; } int symbolX = x - borderWidth; int symbolY = y + (advance - symbolHeight) / 2; if (doDraw) { if (loyaltyRule.loyaltyChange < 0 || loyaltyRule.loyaltyChange == TextboxLoyaltyRule.MINUS_X) { symbol = new Polygon( new int[]{ symbolX, symbolX + symbolWidth, symbolX + symbolWidth, symbolX + symbolWidth / 2, symbolX,}, new int[]{ symbolY, symbolY, symbolY + symbolHeight - 3, symbolY + symbolHeight + 3, symbolY + symbolHeight - 3,}, 5); } else if (loyaltyRule.loyaltyChange > 0) { symbol = new Polygon( new int[]{ symbolX, symbolX + symbolWidth / 2, symbolX + symbolWidth, symbolX + symbolWidth, symbolX,}, new int[]{ symbolY + 3, symbolY - 3, symbolY + 3, symbolY + symbolHeight, symbolY + symbolHeight,}, 5); } else { symbol = new Polygon( new int[]{ symbolX, symbolX + symbolWidth, symbolX + symbolWidth, symbolX,}, new int[]{ symbolY, symbolY, symbolY + symbolHeight, symbolY + symbolHeight,}, 4); } g.setColor(new Color(0, 0, 0, 128)); g.fillRect(x + 2, y + advance + 1, w - 2, 1); g.setColor(Color.black); g.fillPolygon(symbol); g.setColor(new Color(200, 200, 200)); g.setStroke(new BasicStroke(2)); g.drawPolygon(symbol); g.setStroke(new BasicStroke(1)); g.setColor(Color.white); g.setFont(boxTextFont); String loyaltyString = loyaltyRule.getChangeString(); int textWidth = g.getFontMetrics().stringWidth(loyaltyString); g.drawString(loyaltyString, symbolX + (symbolWidth - textWidth) / 2, symbolY + symbolHeight - (symbolHeight - boxTextHeight) / 2); advance += 3; loyaltyAbilityColorToggle = !loyaltyAbilityColorToggle; } } return advance; } // Draw the transformation circle if there is one, and return the // horizontal width taken up into the content space by it. protected boolean isNightCard() { return isTransformed; } protected boolean isTransformCard() { return cardView.canTransform() || isTransformed; } protected int drawTransformationCircle(Graphics2D g, Paint borderPaint) { int transformCircleOffset = 0; if (isTransformCard()) { transformCircleOffset = boxHeight - contentInset; g.setPaint(borderPaint); g.drawOval(borderWidth, totalContentInset, boxHeight - 1, boxHeight - 1); g.setColor(Color.black); g.fillOval(borderWidth + 1, totalContentInset + 1, boxHeight - 2, boxHeight - 2); g.setColor(Color.white); if (isTransformed) { g.fillArc(borderWidth + 3, totalContentInset + 3, boxHeight - 6, boxHeight - 6, 90, 270); g.setColor(Color.black); g.fillArc(borderWidth + 3 + 3, totalContentInset + 3, boxHeight - 6 - 3, boxHeight - 6, 90, 270); } else { g.fillOval(borderWidth + 3, totalContentInset + 3, boxHeight - 6, boxHeight - 6); } } return transformCircleOffset; } // Get the text height for a given box height protected static int getTextHeightForBoxHeight(int h) { if (h < 15) { return h - 3; } else { return (int) Math.ceil(.6 * h); } } protected static int getPTTextHeightForLineHeight(int h) { return h - 4; } // Determine the color of the name / type line text protected Color getBoxTextColor() { if (isTransformed) { return Color.white; } else if (cardView.isAbility()) { return Color.white; } else { return Color.black; } } // Determine the colors to base the frame on protected ObjectColor getFrameObjectColor() { // TODO: Take into account devoid, land frame colors, etc return cardView.getColor().union(cardView.getFrameColor()); } // Determine which background paint to use from a set of colors // and the current card. protected static Paint getBackgroundPaint(ObjectColor colors, Collection<CardType> types, Collection<String> subTypes) { if (subTypes.contains("Vehicle")) { return BG_TEXTURE_VEHICLE; } else if (types.contains(CardType.LAND)) { return BG_TEXTURE_LAND; } else if (types.contains(CardType.ARTIFACT)) { return BG_TEXTURE_ARTIFACT; } else if (colors.isMulticolored()) { return BG_TEXTURE_GOLD; } else if (colors.isWhite()) { return BG_TEXTURE_WHITE; } else if (colors.isBlue()) { return BG_TEXTURE_BLUE; } else if (colors.isBlack()) { return BG_TEXTURE_BLACK; } else if (colors.isRed()) { return BG_TEXTURE_RED; } else if (colors.isGreen()) { return BG_TEXTURE_GREEN; } else { // Colorless return new Color(71, 86, 101); } } // Get the box color for the given colors protected Color getBoxColor(ObjectColor colors, Collection<CardType> types, boolean isNightCard) { if (cardView.isAbility()) { return Color.BLACK; } else if (colors.getColorCount() == 2 && types.contains(CardType.LAND)) { // Special case for two color lands. Boxes should be normal land colored // rather than multicolor. Three or greater color lands use a multi-color // box as normal. return BOX_LAND; } else if (colors.isMulticolored()) { return isNightCard ? BOX_GOLD_NIGHT : BOX_GOLD; } else if (colors.isColorless()) { if (types.contains(CardType.LAND)) { return BOX_LAND; } else { return isNightCard ? BOX_COLORLESS_NIGHT : BOX_COLORLESS; } } else if (colors.isWhite()) { return isNightCard ? BOX_WHITE_NIGHT : BOX_WHITE; } else if (colors.isBlue()) { return isNightCard ? BOX_BLUE_NIGHT : BOX_BLUE; } else if (colors.isBlack()) { return isNightCard ? BOX_BLACK_NIGHT : BOX_BLACK; } else if (colors.isRed()) { return isNightCard ? BOX_RED_NIGHT : BOX_RED; } else if (colors.isGreen()) { return isNightCard ? BOX_GREEN_NIGHT : BOX_GREEN; } else { return ERROR_COLOR; } } // Get the border color for a single color protected static Color getBorderColor(ObjectColor color) { if (color.isWhite()) { return BORDER_WHITE; } else if (color.isBlue()) { return BORDER_BLUE; } else if (color.isBlack()) { return BORDER_BLACK; } else if (color.isRed()) { return BORDER_RED; } else if (color.isGreen()) { return BORDER_GREEN; } else { return ERROR_COLOR; } } // Determine the border paint to use, based on an ObjectColors protected static Paint getBorderPaint(ObjectColor colors, Collection<CardType> types, int width) { if (colors.isMulticolored()) { if (colors.getColorCount() == 2) { List<ObjectColor> twoColors = colors.getColors(); // Two-color frames look better if we use a whiter white // than the normal white frame color for them, as the normal // white border color is very close to the gold background // color. Color color1, color2; if (twoColors.get(0).isWhite()) { color1 = new Color(240, 240, 240); } else { color1 = getBorderColor(twoColors.get(0)); } if (twoColors.get(1).isWhite()) { color2 = new Color(240, 240, 240); } else { color2 = getBorderColor(twoColors.get(1)); } // Special case for two colors, gradient paint return new LinearGradientPaint( 0, 0, width, 0, new float[]{0.4f, 0.6f}, new Color[]{color1, color2}); } else { return BORDER_GOLD; } } else if (colors.isColorless()) { if (types.contains(CardType.LAND)) { return BORDER_LAND; } else { return BORDER_COLORLESS; } } else { return getBorderColor(colors); } } // Determine the textbox color for a single color protected static Color getTextboxColor(ObjectColor color) { if (color.isWhite()) { return TEXTBOX_WHITE; } else if (color.isBlue()) { return TEXTBOX_BLUE; } else if (color.isBlack()) { return TEXTBOX_BLACK; } else if (color.isRed()) { return TEXTBOX_RED; } else if (color.isGreen()) { return TEXTBOX_GREEN; } else { return ERROR_COLOR; } } // Determine the land textbox color for a single color. Uses the same colors as the // type / name line. protected static Color getLandTextboxColor(ObjectColor color) { if (color.isWhite()) { return LAND_TEXTBOX_WHITE; } else if (color.isBlue()) { return LAND_TEXTBOX_BLUE; } else if (color.isBlack()) { return LAND_TEXTBOX_BLACK; } else if (color.isRed()) { return LAND_TEXTBOX_RED; } else if (color.isGreen()) { return LAND_TEXTBOX_GREEN; } else { return ERROR_COLOR; } } // Determine the border paint to use, based on an ObjectColors protected static Paint getTextboxPaint(ObjectColor colors, Collection<CardType> types, int width) { if (colors.isMulticolored()) { if (colors.getColorCount() == 2) { List<ObjectColor> twoColors = colors.getColors(); Color[] translatedColors; if (types.contains(CardType.LAND)) { translatedColors = new Color[]{ getLandTextboxColor(twoColors.get(0)), getLandTextboxColor(twoColors.get(1)) }; } else { translatedColors = new Color[]{ getTextboxColor(twoColors.get(0)), getTextboxColor(twoColors.get(1)) }; } // Special case for two colors, gradient paint return new LinearGradientPaint( 0, 0, width, 0, new float[]{0.4f, 0.6f}, translatedColors); } else if (types.contains(CardType.LAND)) { return LAND_TEXTBOX_GOLD; } else { return TEXTBOX_GOLD; } } else if (colors.isColorless()) { if (types.contains(CardType.LAND)) { return TEXTBOX_LAND; } else { return TEXTBOX_COLORLESS; } } else if (types.contains(CardType.LAND)) { return getLandTextboxColor(colors); } else { return getTextboxColor(colors); } } }