Working Card Rendering

This commit is contained in:
Mark Langen 2016-08-31 04:43:28 -06:00
parent eeaea4c566
commit d5415d2d04
63 changed files with 17729 additions and 1769 deletions

View file

@ -159,7 +159,7 @@ public abstract class Animation {
public static void transformCard(final CardPanel panel, final MagePermanent parent, final boolean transformed) {
new Animation(1200) {
new Animation(600) {
private boolean state = false;
@Override
@ -256,7 +256,7 @@ public abstract class Animation {
public void run() {
if (placeholder != null) {
placeholder.setDisplayEnabled(true);
placeholder.setImage(animationPanel);
placeholder.transferResources(animationPanel);
}
animationPanel.setVisible(false);
animationPanel.repaint();
@ -303,7 +303,7 @@ public abstract class Animation {
public void run() {
if (placeholder != null) {
placeholder.setDisplayEnabled(true);
placeholder.setImage(animationPanel);
placeholder.transferResources(animationPanel);
}
animationPanel.setVisible(false);
animationPanel.repaint();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,711 @@
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;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.UUID;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import mage.cards.action.ActionCallback;
import mage.client.dialog.PreferencesDialog;
import mage.client.util.ImageCaches;
import mage.client.util.ImageHelper;
import mage.components.ImagePanel;
import mage.constants.AbilityType;
import mage.utils.CardUtil;
import mage.view.CardView;
import mage.view.CounterView;
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;
import org.mage.plugins.card.utils.impl.ImageManagerImpl;
/**
* Class for drawing the mage card object by using a form based JComponent approach
*
* @author arcane, nantuko, noxx, stravant
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public class CardPanelComponentImpl extends CardPanel {
private static final long serialVersionUID = -3272134219262184411L;
private static final Logger LOGGER = Logger.getLogger(CardPanelComponentImpl.class);
private static final int WIDTH_LIMIT = 90; // card width limit to create smaller counter
private static final float ROUNDED_CORNER_SIZE = 0.1f;
private static final float BLACK_BORDER_SIZE = 0.03f;
private static final int TEXT_GLOW_SIZE = 6;
private static final float TEXT_GLOW_INTENSITY = 3f;
public final ScaledImagePanel imagePanel;
public ImagePanel overlayPanel;
public JPanel iconPanel;
private JButton typeButton;
public JPanel counterPanel;
private JLabel loyaltyCounterLabel;
private JLabel plusCounterLabel;
private JLabel otherCounterLabel;
private JLabel minusCounterLabel;
private int loyaltyCounter;
private int plusCounter;
private int otherCounter;
private int minusCounter;
private int lastCardWidth;
private final GlowText titleText;
private final GlowText ptText;
private boolean hasImage = false;
private boolean displayTitleAnyway;
private final static Map<Key, BufferedImage> IMAGE_CACHE;
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<Key, BufferedImage>() {
@Override
public BufferedImage apply(Key key) {
return createImage(key);
}
}));
}
public CardPanelComponentImpl(CardView newGameCard, UUID gameId, final boolean loadImage, ActionCallback callback, final boolean foil, Dimension dimension) {
// Call to super
super(newGameCard, gameId, loadImage, callback, foil, dimension);
// Counter panel
if (!newGameCard.isAbility()) {
// panel to show counters on the card
counterPanel = new JPanel();
counterPanel.setLayout(null);
counterPanel.setOpaque(false);
add(counterPanel);
plusCounterLabel = new JLabel("");
plusCounterLabel.setToolTipText("+1/+1");
counterPanel.add(plusCounterLabel);
minusCounterLabel = new JLabel("");
minusCounterLabel.setToolTipText("-1/-1");
counterPanel.add(minusCounterLabel);
loyaltyCounterLabel = new JLabel("");
loyaltyCounterLabel.setToolTipText("loyalty");
counterPanel.add(loyaltyCounterLabel);
otherCounterLabel = new JLabel("");
counterPanel.add(otherCounterLabel);
counterPanel.setVisible(false);
}
// Ability icon
if (newGameCard.isAbility()) {
if (AbilityType.TRIGGERED.equals(newGameCard.getAbilityType())) {
setTypeIcon(ImageManagerImpl.getInstance().getTriggeredAbilityImage(), "Triggered Ability");
} else if (AbilityType.ACTIVATED.equals(newGameCard.getAbilityType())) {
setTypeIcon(ImageManagerImpl.getInstance().getActivatedAbilityImage(), "Activated Ability");
}
}
// Token icon
if (this.gameCard.isToken()) {
setTypeIcon(ImageManagerImpl.getInstance().getTokenIconImage(), "Token Permanent");
}
displayTitleAnyway = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_SHOW_CARD_NAMES, "true").equals("true");
// Title Text
titleText = new GlowText();
setText(gameCard);
// int fontSize = (int) cardHeight / 11;
// titleText.setFont(getFont().deriveFont(Font.BOLD, fontSize));
titleText.setForeground(Color.white);
titleText.setGlow(Color.black, TEXT_GLOW_SIZE, TEXT_GLOW_INTENSITY);
titleText.setWrap(true);
add(titleText);
// PT Text
ptText = new GlowText();
if (CardUtil.isCreature(gameCard)) {
ptText.setText(gameCard.getPower() + "/" + gameCard.getToughness());
} else if (CardUtil.isPlaneswalker(gameCard)) {
ptText.setText(gameCard.getLoyalty());
}
// ptText.setFont(getFont().deriveFont(Font.BOLD, fontSize));
ptText.setForeground(Color.white);
ptText.setGlow(Color.black, TEXT_GLOW_SIZE, TEXT_GLOW_INTENSITY);
add(ptText);
// Sickness overlay
BufferedImage sickness = ImageManagerImpl.getInstance().getSicknessImage();
overlayPanel = new ImagePanel(sickness, ImagePanel.SCALED);
overlayPanel.setOpaque(false);
add(overlayPanel);
// Imagel panel
imagePanel = new ScaledImagePanel();
imagePanel.setBorder(BorderFactory.createLineBorder(Color.white));
add(imagePanel);
// Do we need to load?
if (loadImage) {
initialDraw();
} else {
// Nothing to do
}
}
private void setTypeIcon(BufferedImage bufferedImage, String toolTipText) {
iconPanel = new JPanel();
iconPanel.setLayout(null);
iconPanel.setOpaque(false);
add(iconPanel);
typeButton = new JButton("");
typeButton.setLocation(2, 2);
typeButton.setSize(25, 25);
iconPanel.setVisible(true);
typeButton.setIcon(new ImageIcon(bufferedImage));
if (toolTipText != null) {
typeButton.setToolTipText(toolTipText);
}
iconPanel.add(typeButton);
}
@Override
public void cleanUp() {
super.cleanUp();
this.counterPanel = null;
}
private void setText(CardView card) {
titleText.setText(!displayTitleAnyway && hasImage ? "" : card.getName());
}
private void setImage(BufferedImage srcImage) {
synchronized (imagePanel) {
if (srcImage != null) {
imagePanel.setImage(srcImage);
} else {
imagePanel.clearImage();
}
repaint();
}
doLayout();
}
@Override
public void transferResources(final CardPanel panelAbstract) {
if (panelAbstract instanceof CardPanelComponentImpl) {
CardPanelComponentImpl panel = (CardPanelComponentImpl)panelAbstract;
synchronized (panel.imagePanel) {
if (panel.imagePanel.hasImage()) {
setImage(panel.imagePanel.getSrcImage());
}
}
}
}
@Override
public void setSelected(boolean isSelected) {
super.setSelected(isSelected);
if (isSelected) {
this.titleText.setGlowColor(Color.green);
} else {
this.titleText.setGlowColor(Color.black);
}
}
@Override
protected void paintCard(Graphics2D g2d) {
float alpha = getAlpha();
if (alpha != 1.0f) {
AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha);
g2d.setComposite(composite);
}
g2d.drawImage(
IMAGE_CACHE.get(
new Key(getWidth(), getHeight(), getCardWidth(), getCardHeight(), getCardXOffset(), getCardYOffset(),
hasImage, isSelected(), isChoosable(), gameCard.isPlayable(), gameCard.isCanAttack())),
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, 255));
}
int cornerSize = Math.max(4, Math.round(cardWidth * ROUNDED_CORNER_SIZE));
g2d.fillRoundRect(cardXOffset, cardYOffset, cardWidth, cardHeight, cornerSize, cornerSize);
if (key.isSelected) {
g2d.setColor(Color.green);
g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize);
} 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 (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 (key.canAttack) {
g2d.setColor(new Color(0, 0, 255, 230));
g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize);
}
//TODO:uncomment
/*
if (gameCard.isAttacking()) {
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
protected void paintChildren(Graphics g) {
super.paintChildren(g);
if (getShowCastingCost() && !isAnimationPanel() && getCardWidth() < 200 && getCardWidth() > 60) {
String manaCost = ManaSymbols.getStringManaCost(gameCard.getManaCost());
int width = getWidth(manaCost);
if (hasImage) {
ManaSymbols.draw(g, manaCost, getCardXOffset() + getCardWidth() - width - 5, getCardYOffset() + 5, getSymbolWidth());
} else {
ManaSymbols.draw(g, manaCost, getCardXOffset() + 8, getCardHeight() - 9, getSymbolWidth());
}
}
}
private int getWidth(String manaCost) {
int width = 0;
manaCost = manaCost.replace("\\", "");
StringTokenizer tok = new StringTokenizer(manaCost, " ");
while (tok.hasMoreTokens()) {
tok.nextToken();
width += getSymbolWidth();
}
return width;
}
@Override
public void doLayout() {
super.doLayout();
int cardWidth = getCardWidth();
int cardHeight = getCardHeight();
int cardXOffset = getCardXOffset();
int cardYOffset = getCardYOffset();
int borderSize = Math.round(cardWidth * BLACK_BORDER_SIZE);
imagePanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize);
imagePanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2);
if (hasSickness() && CardUtil.isCreature(gameCard) && isPermanent()) {
overlayPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize);
overlayPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2);
} else {
overlayPanel.setVisible(false);
}
if (iconPanel != null) {
iconPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize);
iconPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2);
}
if (counterPanel != null) {
counterPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize);
counterPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2);
int size = cardWidth > WIDTH_LIMIT ? 40 : 20;
minusCounterLabel.setLocation(counterPanel.getWidth() - size, counterPanel.getHeight() - size * 2);
minusCounterLabel.setSize(size, size);
plusCounterLabel.setLocation(5, counterPanel.getHeight() - size * 2);
plusCounterLabel.setSize(size, size);
loyaltyCounterLabel.setLocation(counterPanel.getWidth() - size, counterPanel.getHeight() - size);
loyaltyCounterLabel.setSize(size, size);
otherCounterLabel.setLocation(5, counterPanel.getHeight() - size);
otherCounterLabel.setSize(size, size);
}
int fontHeight = Math.round(cardHeight * (27f / 680));
boolean showText = (!isAnimationPanel() && fontHeight < 12);
titleText.setVisible(showText);
ptText.setVisible(showText);
if (showText) {
int fontSize = (int) cardHeight / 11;
titleText.setFont(getFont().deriveFont(Font.BOLD, fontSize));
int titleX = Math.round(cardWidth * (20f / 480));
int titleY = Math.round(cardHeight * (9f / 680)) + getTextOffset();
titleText.setBounds(cardXOffset + titleX, cardYOffset + titleY, cardWidth - titleX, cardHeight - titleY);
ptText.setFont(getFont().deriveFont(Font.BOLD, fontSize));
Dimension ptSize = ptText.getPreferredSize();
ptText.setSize(ptSize.width, ptSize.height);
int ptX = Math.round(cardWidth * (420f / 480)) - ptSize.width / 2;
int ptY = Math.round(cardHeight * (675f / 680)) - ptSize.height;
int offsetX = Math.round((CARD_SIZE_FULL.width - cardWidth) / 10.0f);
ptText.setLocation(cardXOffset + ptX - TEXT_GLOW_SIZE / 2 - offsetX, cardYOffset + ptY - TEXT_GLOW_SIZE / 2);
}
}
@Override
public String toString() {
return gameCard.toString();
}
@Override
public void setCardBounds(int x, int y, int cardWidth, int cardHeight) {
// Call to super
super.setCardBounds(x, y, cardWidth, cardHeight);
// Update image
if (imagePanel != null && imagePanel.getSrcImage() != null) {
updateImage();
}
}
@Override
public void setAlpha(float alpha) {
super.setAlpha(alpha);
// Update components
if (alpha == 0) {
this.ptText.setVisible(false);
this.titleText.setVisible(false);
} else if (alpha == 1.0f) {
this.ptText.setVisible(true);
this.titleText.setVisible(true);
}
}
///////////////////////////////////////////////////////////
// Image updating code
private int updateImageStamp;
@Override
public void updateImage() {
tappedAngle = isTapped() ? CardPanel.TAPPED_ANGLE : 0;
flippedAngle = isFlipped() ? CardPanel.FLIPPED_ANGLE : 0;
//final CardView gameCard = this.gameCard;
final int stamp = ++updateImageStamp;
Util.threadPool.submit(new Runnable() {
@Override
public void run() {
try {
final BufferedImage srcImage;
if (gameCard.isFaceDown()) {
srcImage = getFaceDownImage();
} else if (getCardWidth() > THUMBNAIL_SIZE_FULL.width) {
srcImage = ImageCache.getImage(gameCard, getCardWidth(), getCardHeight());
} else {
srcImage = ImageCache.getThumbnail(gameCard);
}
UI.invokeLater(new Runnable() {
@Override
public void run() {
if (stamp == updateImageStamp) {
hasImage = srcImage != null;
setText(gameCard);
setImage(srcImage);
}
}
});
} catch (Exception e) {
e.printStackTrace();
} catch (Error err) {
err.printStackTrace();
}
}
});
}
private BufferedImage getFaceDownImage() {
if (isPermanent()) {
if (((PermanentView) gameCard).isMorphed()) {
return ImageCache.getMorphImage();
} else {
return ImageCache.getManifestImage();
}
} else if (this.gameCard instanceof StackAbilityView) {
return ImageCache.getMorphImage();
} else {
return ImageCache.loadImage(new TFile(DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename));
}
}
@Override
public void showCardTitle() {
displayTitleAnyway = true;
setText(gameCard);
}
@Override
public void update(CardView card) {
// Super
super.update(card);
// Update card text
if (CardUtil.isCreature(card) && CardUtil.isPlaneswalker(card)) {
ptText.setText(card.getPower() + "/" + card.getToughness() + " (" + card.getLoyalty() + ")");
} else if (CardUtil.isCreature(card)) {
ptText.setText(card.getPower() + "/" + card.getToughness());
} else if (CardUtil.isPlaneswalker(card)) {
ptText.setText(card.getLoyalty());
} else {
ptText.setText("");
}
setText(card);
// Summoning Sickness overlay
if (hasSickness() && CardUtil.isCreature(gameCard) && isPermanent()) {
overlayPanel.setVisible(true);
} else {
overlayPanel.setVisible(false);
}
// Update counters panel
if (counterPanel != null) {
updateCounters(card);
}
// Finally, queue a repaint
repaint();
}
private void updateCounters(CardView card) {
if (card.getCounters() != null && !card.getCounters().isEmpty()) {
String name = "";
if (lastCardWidth != getCardWidth()) {
lastCardWidth = getCardWidth();
plusCounter = 0;
minusCounter = 0;
otherCounter = 0;
loyaltyCounter = 0;
}
plusCounterLabel.setVisible(false);
minusCounterLabel.setVisible(false);
loyaltyCounterLabel.setVisible(false);
otherCounterLabel.setVisible(false);
for (CounterView counterView : card.getCounters()) {
if (counterView.getCount() == 0) {
continue;
}
switch (counterView.getName()) {
case "+1/+1":
if (counterView.getCount() != plusCounter) {
plusCounter = counterView.getCount();
plusCounterLabel.setIcon(getCounterImageWithAmount(plusCounter, ImageManagerImpl.getInstance().getCounterImageGreen(), getCardWidth()));
}
plusCounterLabel.setVisible(true);
break;
case "-1/-1":
if (counterView.getCount() != minusCounter) {
minusCounter = counterView.getCount();
minusCounterLabel.setIcon(getCounterImageWithAmount(minusCounter, ImageManagerImpl.getInstance().getCounterImageRed(), getCardWidth()));
}
minusCounterLabel.setVisible(true);
break;
case "loyalty":
if (counterView.getCount() != loyaltyCounter) {
loyaltyCounter = counterView.getCount();
loyaltyCounterLabel.setIcon(getCounterImageWithAmount(loyaltyCounter, ImageManagerImpl.getInstance().getCounterImageViolet(), getCardWidth()));
}
loyaltyCounterLabel.setVisible(true);
break;
default:
if (name.isEmpty()) { // only first other counter is shown
name = counterView.getName();
otherCounter = counterView.getCount();
otherCounterLabel.setToolTipText(name);
otherCounterLabel.setIcon(getCounterImageWithAmount(otherCounter, ImageManagerImpl.getInstance().getCounterImageGrey(), getCardWidth()));
otherCounterLabel.setVisible(true);
}
}
}
counterPanel.setVisible(true);
} else {
plusCounterLabel.setVisible(false);
minusCounterLabel.setVisible(false);
loyaltyCounterLabel.setVisible(false);
otherCounterLabel.setVisible(false);
counterPanel.setVisible(false);
}
}
private static ImageIcon getCounterImageWithAmount(int amount, BufferedImage image, int cardWidth) {
int factor = cardWidth > WIDTH_LIMIT ? 2 : 1;
int xOffset = amount > 9 ? 2 : 5;
int fontSize = factor == 1 ? amount < 10 ? 12 : amount < 100 ? 10 : amount < 1000 ? 7 : 6
: amount < 10 ? 19 : amount < 100 ? 15 : amount < 1000 ? 12 : amount < 10000 ? 9 : 8;
BufferedImage newImage;
if (cardWidth > WIDTH_LIMIT) {
newImage = ImageManagerImpl.deepCopy(image);
} else {
newImage = ImageHelper.getResizedImage(image, 20, 20);
}
Graphics graphics = newImage.getGraphics();
graphics.setColor(Color.BLACK);
graphics.setFont(new Font("Arial Black", amount > 100 ? Font.PLAIN : Font.BOLD, fontSize));
graphics.drawString(Integer.toString(amount), xOffset * factor, 11 * factor);
return new ImageIcon(newImage);
}
@Override
public Image getImage() {
if (this.hasImage) {
if (gameCard.isFaceDown()) {
return getFaceDownImage();
} else {
return ImageCache.getImageOriginal(gameCard);
}
}
return null;
}
}

View file

@ -0,0 +1,323 @@
package org.mage.card.arcane;
import com.google.common.base.Function;
import com.google.common.collect.MapMaker;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.Map;
import java.util.UUID;
import mage.cards.action.ActionCallback;
import mage.client.util.ImageCaches;
import mage.constants.CardType;
import mage.view.CardView;
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.images.ImageCache;
public class CardPanelRenderImpl extends CardPanel {
private static final Logger LOGGER = Logger.getLogger(CardPanelRenderImpl.class);
private static boolean cardViewEquals(CardView a, CardView b) {
if (a == b) {
return true;
}
if (!a.getName().equals(b.getName())) {
return false;
}
if (!a.getPower().equals(b.getPower())) {
return false;
}
if (!a.getToughness().equals(b.getToughness())) {
return false;
}
if (!a.getLoyalty().equals(b.getLoyalty())) {
return false;
}
if (0 != a.getColor().compareTo(b.getColor())) {
return false;
}
if (!a.getCardTypes().equals(b.getCardTypes())) {
return false;
}
if (!a.getSubTypes().equals(b.getSubTypes())) {
return false;
}
if (!a.getSuperTypes().equals(b.getSuperTypes())) {
return false;
}
if (!a.getManaCost().equals(b.getManaCost())) {
return false;
}
if (!a.getRules().equals(b.getRules())) {
return false;
}
if (!a.getExpansionSetCode().equals(b.getExpansionSetCode())) {
return false;
}
return true;
}
class ImageKey {
final BufferedImage artImage;
final int width;
final int height;
final boolean isChoosable;
final boolean isSelected;
final CardView view;
final int hashCode;
public ImageKey(CardView view, BufferedImage artImage, int width, int height, boolean isChoosable, boolean isSelected) {
this.view = view;
this.artImage = artImage;
this.width = width;
this.height = height;
this.isChoosable = isChoosable;
this.isSelected = isSelected;
this.hashCode = hashCodeImpl();
}
private int hashCodeImpl() {
StringBuilder sb = new StringBuilder();
sb.append((char)(artImage != null ? 1 : 0));
sb.append((char)width);
sb.append((char)height);
sb.append((char)(isSelected ? 1 : 0));
sb.append((char)(isChoosable ? 1 : 0));
sb.append((char)(this.view.isPlayable() ? 1 : 0));
sb.append((char)(this.view.isCanAttack() ? 1 : 0));
sb.append(this.view.getName());
sb.append(this.view.getPower());
sb.append(this.view.getToughness());
sb.append(this.view.getLoyalty());
sb.append(this.view.getColor().toString());
sb.append(this.view.getExpansionSetCode());
for (CardType type: this.view.getCardTypes()) {
sb.append((char)type.ordinal());
}
for (String s: this.view.getSuperTypes()) {
sb.append(s);
}
for (String s: this.view.getSubTypes()) {
sb.append(s);
}
for (String s: this.view.getManaCost()) {
sb.append(s);
}
for (String s: this.view.getRules()) {
sb.append(s);
}
return sb.toString().hashCode();
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object object) {
// Initial checks
if (this == object) {
return true;
}
if (object == null) {
return false;
}
if (!(object instanceof ImageKey)) {
return false;
}
final ImageKey other = (ImageKey)object;
// Compare
if ((artImage != null) != (other.artImage != null)) {
return false;
}
if (width != other.width) {
return false;
}
if (height != other.height) {
return false;
}
if (isChoosable != other.isChoosable) {
return false;
}
if (isSelected != other.isSelected) {
return false;
}
return cardViewEquals(view, other.view);
}
}
// Map of generated images
private final static Map<ImageKey, BufferedImage> IMAGE_CACHE = new MapMaker().softValues().makeMap();
// The art image for the card, loaded in from the disk
private BufferedImage artImage;
// The rendered card image, with or without the art image loaded yet
// = null while invalid
private BufferedImage cardImage;
private CardRenderer cardRenderer;
public CardPanelRenderImpl(CardView newGameCard, UUID gameId, final boolean loadImage, ActionCallback callback, final boolean foil, Dimension dimension) {
// Call to super
super(newGameCard, gameId, loadImage, callback, foil, dimension);
// Renderer
cardRenderer = new ModernCardRenderer(gameCard, isTransformed());
// Draw the parts
initialDraw();
}
@Override
public void transferResources(CardPanel panel) {
if (panel instanceof CardPanelRenderImpl) {
CardPanelRenderImpl impl = (CardPanelRenderImpl)panel;
// Use the art image and current rendered image from the card
artImage = impl.artImage;
cardRenderer.setArtImage(artImage);
cardImage = impl.cardImage;
}
}
@Override
protected void paintCard(Graphics2D g) {
// Render the card if we don't have an image ready to use
if (cardImage == null) {
// Try to get card image from cache based on our card characteristics
ImageKey key =
new ImageKey(gameCard, artImage,
getCardWidth(), getCardHeight(),
isChoosable(), isSelected());
cardImage = IMAGE_CACHE.get(key);
// No cached copy exists? Render one and cache it
if (cardImage == null) {
cardImage = renderCard();
IMAGE_CACHE.put(key, cardImage);
}
}
// And draw the image we now have
g.drawImage(cardImage, 0, 0, null);
}
/**
* Render the card to a new BufferedImage at it's current dimensions
* @return
*/
private BufferedImage renderCard() {
int cardWidth = getCardWidth();
int cardHeight = getCardHeight();
int cardXOffset = getCardXOffset();
int cardYOffset = getCardYOffset();
// Create image to render to
BufferedImage image =
GraphicsUtilities.createCompatibleTranslucentImage(getWidth(), getHeight());
Graphics2D g2d = image.createGraphics();
// Render with Antialialsing
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Draw card itself
g2d.translate(cardXOffset, cardYOffset);
cardRenderer.draw(g2d, cardWidth - cardXOffset, cardHeight - cardYOffset);
g2d.translate(-cardXOffset, -cardYOffset);
// Done
g2d.dispose();
return image;
}
private int updateImageStamp;
@Override
public void updateImage() {
// Invalidate
artImage = null;
cardImage = null;
// Stop animation
tappedAngle = isTapped() ? CardPanel.TAPPED_ANGLE : 0;
flippedAngle = isFlipped() ? CardPanel.FLIPPED_ANGLE : 0;
// Schedule a repaint
repaint();
// Submit a task to draw with the card art when it arrives
final int stamp = ++updateImageStamp;
Util.threadPool.submit(new Runnable() {
@Override
public void run() {
try {
final BufferedImage srcImage;
if (gameCard.isFaceDown()) {
// Nothing to do
srcImage = null;
} else if (getCardWidth() > THUMBNAIL_SIZE_FULL.width) {
srcImage = ImageCache.getImage(gameCard, getCardWidth(), getCardHeight());
} else {
srcImage = ImageCache.getThumbnail(gameCard);
}
UI.invokeLater(new Runnable() {
@Override
public void run() {
if (stamp == updateImageStamp) {
CardPanelRenderImpl.this.artImage = srcImage;
CardPanelRenderImpl.this.cardRenderer.setArtImage(srcImage);
if (srcImage != null) {
// Invalidate and repaint
CardPanelRenderImpl.this.cardImage = null;
repaint();
}
}
}
});
} catch (Exception e) {
e.printStackTrace();
} catch (Error err) {
err.printStackTrace();
}
}
});
}
@Override
public void update(CardView card) {
// Update super
super.update(card);
// Update renderer
cardRenderer = new ModernCardRenderer(gameCard, isTransformed());
// Repaint
repaint();
}
@Override
public void setCardBounds(int x, int y, int cardWidth, int cardHeight) {
super.setCardBounds(x, y, cardWidth, cardHeight);
// Rerender
cardImage = null;
}
@Override
public Image getImage() {
// Render impl never returns a card image
return artImage;
}
@Override
public void showCardTitle() {
// Nothing to do, rendered cards always have a title
}
}

View file

@ -0,0 +1,348 @@
/*
* 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 java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;
import mage.constants.CardType;
import mage.constants.Rarity;
import mage.counters.Counter;
import mage.view.CardView;
import mage.view.CounterView;
import mage.view.PermanentView;
import org.apache.log4j.Logger;
/**
* @author stravant@gmail.com
*
* Common base class for card renderers for each card frame / card type.
*
* Follows the template method pattern to implement a new renderer, implement
* the following methods (they are called in the following order):
*
* * drawBorder()
* Draws the outermost border of the card, white border or black border
*
* * drawBackground()
* Draws the background texture / color of the card
*
* * drawArt()
* Draws the card's art
*
* * drawFrame()
* Draws the card frame (over the art and background)
*
* * drawOverlays()
* Draws summoning sickness and possible other overlays
*
* * drawCounters()
* Draws counters on the card, such as +1/+1 and -1/-1 counters
*
* Predefined methods that the implementations can use:
*
* * drawRules(font, bounding box)
*
* * drawNameLine(font, bounding box)
*
* * drawTypeLine(font, bounding box)
*
*/
public abstract class CardRenderer {
private static final Logger LOGGER = Logger.getLogger(CardPanel.class);
///////////////////////////////////////////////////////////////////////////
// Common layout metrics between all cards
// The card to be rendered
protected final CardView cardView;
// Is the card transformed?
protected final boolean isTransformed;
// The card image
protected BufferedImage artImage;
///////////////////////////////////////////////////////////////////////////
// Common layout metrics between all cards
// Polygons for counters
private static final Polygon PLUS_COUNTER_POLY = new Polygon(new int[]{
0, 5, 10, 10, 5, 0
}, new int[]{
3, 0, 3, 10, 9, 10
}, 6);
private static final Polygon MINUS_COUNTER_POLY = new Polygon(new int[]{
0, 5, 10, 10, 5, 0
}, new int[]{
0, 1, 0, 7, 10, 7
}, 6);
private static final Polygon TIME_COUNTER_POLY = new Polygon(new int[]{
0, 10, 8, 10, 0, 2
}, new int[]{
0, 0, 5, 10, 10, 5
}, 6);
private static final Polygon OTHER_COUNTER_POLY = new Polygon(new int[]{
1, 9, 9, 1
}, new int[]{
1, 1, 9, 9
}, 4);
// Paint for a card back
public static Paint BG_TEXTURE_CARDBACK = new Color(153, 102, 51);
// The size of the card
protected int cardWidth;
protected int cardHeight;
// Radius of the corners of the cards
protected static float CORNER_RADIUS_FRAC = 0.1f; //x cardWidth
protected static int CORNER_RADIUS_MIN = 3;
protected int cornerRadius;
// The inset of the actual card from the black / white border around it
protected static float BORDER_WIDTH_FRAC = 0.03f; //x cardWidth
protected static float BORDER_WIDTH_MIN = 2;
protected int borderWidth;
// The parsed text of the card
protected ArrayList<TextboxRule> textboxRules = new ArrayList<>();
protected ArrayList<TextboxRule> textboxKeywords = new ArrayList<>();
// The Construtor
// The constructor should prepare all of the things that it can
// without knowing the dimensions that the card will be rendered at.
// Then, the CardRenderer can be called on multiple times to render the
// card at various sizes (for instance, during animation)
public CardRenderer(CardView card, boolean isTransformed) {
// Set base parameters
this.cardView = card;
this.isTransformed = isTransformed;
// Translate the textbox text
for (String rule: card.getRules()) {
TextboxRule tbRule = TextboxRuleParser.parse(card, rule);
if (tbRule.type == TextboxRuleType.SIMPLE_KEYWORD) {
textboxKeywords.add(tbRule);
} else {
textboxRules.add(tbRule);
}
}
}
// Layout operation
// Calculate common layout metrics that will be used by several
// of the operations in the template method.
protected void layout(int cardWidth, int cardHeight) {
// Store the dimensions for the template methods to use
this.cardWidth = cardWidth;
this.cardHeight = cardHeight;
// Corner radius and border width
cornerRadius = (int)Math.max(
CORNER_RADIUS_MIN,
CORNER_RADIUS_FRAC * cardWidth);
borderWidth = (int)Math.max(
BORDER_WIDTH_MIN,
BORDER_WIDTH_FRAC * cardWidth);
}
// The Draw Method
// The draw method takes the information caculated by the constructor
// and uses it to draw to a concrete size of card and graphics.
public void draw(Graphics2D g, int cardWidth, int cardHeight) {
// Pre template method layout, to calculate shared layout info
layout(cardWidth, cardHeight);
// Call the template methods
drawBorder(g);
drawBackground(g);
drawArt(g);
drawFrame(g);
drawOverlays(g);
drawCounters(g);
}
// Template methods to be implemented by sub classes
// For instance, for the Modern vs Old border card frames
protected abstract void drawBorder(Graphics2D g);
protected abstract void drawBackground(Graphics2D g);
protected abstract void drawArt(Graphics2D g);
protected abstract void drawFrame(Graphics2D g);
// Template methods that are possible to override, but unlikely to be
// overridden.
// Draw the card back
protected void drawCardBack(Graphics2D g) {
g.setPaint(BG_TEXTURE_CARDBACK);
g.fillRect(borderWidth, borderWidth,
cardWidth - 2*borderWidth, cardHeight - 2*borderWidth);
}
// Draw summoning sickness overlay, and possibly other overlays
protected void drawOverlays(Graphics2D g) {
if (cardView instanceof PermanentView) {
if (((PermanentView)cardView).hasSummoningSickness()) {
int x1 = (int)(0.2*cardWidth);
int x2 = (int)(0.8*cardWidth);
int y1 = (int)(0.2*cardHeight);
int y2 = (int)(0.8*cardHeight);
int xPoints[] = {
x1, x2, x1, x2
};
int yPoints[] = {
y1, y1, y2, y2
};
g.setColor(new Color(255, 255, 255, 200));
g.setStroke(new BasicStroke(7));
g.drawPolygon(xPoints, yPoints, 4);
g.setColor(new Color(0, 0, 0, 200));
g.setStroke(new BasicStroke(5));
g.drawPolygon(xPoints, yPoints, 4);
g.setStroke(new BasicStroke(1));
int[] xPoints2 = {
x1, x2, cardWidth/2
};
int[] yPoints2 = {
y1, y1, cardHeight/2
};
g.setColor(new Color(0, 0, 0, 100));
g.fillPolygon(xPoints2, yPoints2, 3);
}
}
}
// Draw +1/+1 and other counters
protected void drawCounters(Graphics2D g) {
int xPos = (int)(0.7*cardWidth);
int yPos = (int)(0.15*cardHeight);
if (cardView.getCounters() != null) {
for (CounterView v: cardView.getCounters()) {
// Don't render loyalty, we do that in the bottom corner
if (!v.getName().equals("loyalty")) {
Polygon p;
if (v.getName().equals("+1/+1")) {
p = PLUS_COUNTER_POLY;
} else if (v.getName().equals("-1/-1")) {
p = MINUS_COUNTER_POLY;
} else if (v.getName().equals("time")) {
p = TIME_COUNTER_POLY;
} else {
p = OTHER_COUNTER_POLY;
}
double scale = (0.1*0.18*cardWidth);
Graphics2D g2 = (Graphics2D)g.create();
g2.translate(xPos, yPos);
g2.scale(scale, scale);
g2.setColor(Color.white);
g2.fillPolygon(p);
g2.setColor(Color.black);
g2.drawPolygon(p);
g2.setFont(new Font("Arial", Font.BOLD, 7));
String cstr = "" + v.getCount();
int strW = g2.getFontMetrics().stringWidth(cstr);
g2.drawString(cstr, 5 - strW/2, 8);
g2.dispose();
yPos += ((int)(0.22*cardWidth));
}
}
}
}
// Draw an expansion symbol, right justified, in a given region
// Return the width of the drawn symbol
protected int drawExpansionSymbol(Graphics2D g, int x, int y, int w, int h) {
// Draw the expansion symbol
Image setSymbol = ManaSymbols.getSetSymbolImage(cardView.getExpansionSetCode(), cardView.getRarity().getCode());
int setSymbolWidth;
if (setSymbol == null) {
// Just draw the as a code
String code = cardView.getExpansionSetCode().toUpperCase();
FontMetrics metrics = g.getFontMetrics();
setSymbolWidth = metrics.stringWidth(code);
if (cardView.getRarity() == Rarity.COMMON) {
g.setColor(Color.white);
} else {
g.setColor(Color.black);
}
g.fillRoundRect(
x + w - setSymbolWidth - 1, y + 2,
setSymbolWidth+2, h - 5,
5, 5);
g.setColor(getRarityColor());
g.drawString(code, x + w - setSymbolWidth, y + h - 3);
} else {
// Draw the set symbol
int height = setSymbol.getHeight(null);
int scale = 1;
if (height != -1) {
while (height > h+2) {
scale *= 2;
height /= 2;
}
}
setSymbolWidth = setSymbol.getWidth(null) / scale;
g.drawImage(setSymbol,
x + w - setSymbolWidth, y + (h - height)/2,
setSymbolWidth, height,
null);
}
return setSymbolWidth;
}
private Color getRarityColor() {
switch (cardView.getRarity()) {
case RARE:
return new Color(255, 191, 0);
case UNCOMMON:
return new Color(192, 192, 192);
case MYTHIC:
return new Color(213, 51, 11);
case SPECIAL:
return new Color(204, 0, 255);
case BONUS:
return new Color(129, 228, 228);
case COMMON:
default:
return Color.black;
}
}
// Get a string representing the type line
protected String getCardTypeLine() {
StringBuilder sbType = new StringBuilder();
for (String superType : cardView.getSuperTypes()) {
sbType.append(superType).append(" ");
}
for (CardType cardType : cardView.getCardTypes()) {
sbType.append(cardType.toString()).append(" ");
}
if (cardView.getSubTypes().size() > 0) {
sbType.append("- ");
for (String subType : cardView.getSubTypes()) {
sbType.append(subType).append(" ");
}
}
return sbType.toString();
}
// Set the card art image (CardPanel will give it to us when it
// is loaded and ready)
public void setArtImage(Image image) {
artImage = CardRendererUtils.toBufferedImage(image);
}
}

View file

@ -0,0 +1,124 @@
/*
* 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 java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author stravant@gmail.com
*
* Various static utilities for use in the card renderer
*/
public class CardRendererUtils {
/**
* Convert an abstract image, whose underlying implementation may or may
* not be a BufferedImage into a BufferedImage by creating one and coping
* the contents if it is not, and simply up-casting if it is.
* @param img The image to convert
* @return The converted image
*/
public static BufferedImage toBufferedImage(Image img) {
// Null? No conversion to do
if (img == null) {
return null;
}
// Already a buffered image?
if (img instanceof BufferedImage) {
return (BufferedImage) img;
}
// Create a buffered image with transparency
BufferedImage bimage = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
// Draw the image on to the buffered image
Graphics2D bGr = bimage.createGraphics();
bGr.drawImage(img, 0, 0, null);
bGr.dispose();
// Return the buffered image
return bimage;
}
// Draw a rounded box with a 2-pixel border
// Used on various card parts.
public static void drawRoundedBox(Graphics2D g, int x, int y, int w, int h, int bevel, Paint border, Color fill) {
g.setColor(new Color(0, 0, 0, 150));
g.drawOval(x-1, y-1, bevel*2, h);
g.setPaint(border);
g.drawOval(x, y, bevel*2-1, h-1);
g.drawOval(x + w - bevel*2, y, bevel*2-1, h-1);
g.drawOval(x+1, y+1, bevel*2-3, h-3);
g.drawOval(x+1 + w - bevel*2, y+1, bevel*2-3, h-3);
g.drawRect(x + bevel, y, w - 2*bevel, h-1);
g.drawRect(x+1 + bevel, y+1, w - 2*bevel-2, h-3);
g.setColor(fill);
g.fillOval(x+2, y+2, bevel*2-4, h-4);
g.fillOval(x+2 + w - bevel*2, y+2, bevel*2-4, h-4);
g.fillRect(x + bevel, y+2, w - 2*bevel, h-4);
}
// Get the width of a mana cost rendered with ManaSymbols.draw
public static int getManaCostWidth(String manaCost, int symbolSize) {
int width = 0;
manaCost = manaCost.replace("\\", "");
StringTokenizer tok = new StringTokenizer(manaCost, " ");
while (tok.hasMoreTokens()) {
tok.nextToken();
width += symbolSize;
}
return width;
}
// Abbreviate a piece of rules text, making substitutions to decrease its
// length. Also abbreviate reminder text.
private static Pattern abbreviationPattern;
private static Map<String, String> abbreviations = new HashMap<String, String>();
private static Pattern killReminderTextPattern;
static {
// Available abbreviations
abbreviations.put("enters the battlefield", "ETB");
abbreviations.put("less than", "<");
abbreviations.put("greater than", ">");
// Compile into regex
String patternString = "(";
Iterator<String> it = abbreviations.keySet().iterator();
while (it.hasNext()) {
patternString += it.next();
if (it.hasNext()) {
patternString += "|";
}
}
patternString += ")";
abbreviationPattern = Pattern.compile(patternString);
// Reminder text killing
killReminderTextPattern = Pattern.compile("\\([^\\)]*\\)");
}
public static String abbreviateRule(String rule) {
StringBuffer build = new StringBuffer();
Matcher match = abbreviationPattern.matcher(rule);
while (match.find()) {
match.appendReplacement(build, abbreviations.get(match.group(1)));
}
match.appendTail(build);
return build.toString();
}
public static String killReminderText(String rule) {
return killReminderTextPattern.matcher(rule).replaceAll("");
}
}

View file

@ -37,7 +37,7 @@ public class ManaSymbols {
private static boolean smallSymbolsFound = false;
private static boolean mediumSymbolsFound = false;
private static final Map<String, Image> setImages = new HashMap<>();
private static final Map<String, Map<String, Image>> setImages = new HashMap<>();
private static final Map<String, Dimension> setImagesExist = new HashMap<>();
private static final Pattern REPLACE_SYMBOLS_PATTERN = Pattern.compile("\\{([^}/]*)/?([^}]*)\\}");
private static String cachedPath;
@ -57,25 +57,32 @@ public class ManaSymbols {
return;
}
for (String set : setCodes) {
File file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET + set + "-C.jpg");
try {
Image image = UI.getImageIcon(file.getAbsolutePath()).getImage();
int width = image.getWidth(null);
if (width > 21) {
int h = image.getHeight(null);
if (h > 0) {
Rectangle r = new Rectangle(21, (int) (h * 21.0f / width));
BufferedImage resized = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(image, BufferedImage.TYPE_INT_ARGB), r);
setImages.put(set, resized);
}
} else {
setImages.put(set, image);
}
} catch (Exception e) {
}
String[] codes = new String[]{"C", "U", "R", "M"};
Map<String, Image> rarityImages = new HashMap<>();
setImages.put(set, rarityImages);
for (String rarityCode: codes) {
File file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET + set + "-" + rarityCode + ".jpg");
try {
Image image = UI.getImageIcon(file.getAbsolutePath()).getImage();
int width = image.getWidth(null);
if (width > 21) {
int h = image.getHeight(null);
if (h > 0) {
Rectangle r = new Rectangle(21, (int) (h * 21.0f / width));
BufferedImage resized = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(image, BufferedImage.TYPE_INT_ARGB), r);
rarityImages.put(set, resized);
}
} else {
rarityImages.put(rarityCode, image);
}
} catch (Exception e) {
}
}
try {
file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET_SMALL);
File file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET_SMALL);
if (!file.exists()) {
file.mkdirs();
}
@ -298,14 +305,27 @@ public class ManaSymbols {
}
public static Image getSetSymbolImage(String set) {
return setImages.get(set);
return getSetSymbolImage(set, "C");
}
public static Image getSetSymbolImage(String set, String rarity) {
Map<String, Image> rarityImages = setImages.get(set);
if (rarityImages != null) {
return rarityImages.get(rarity);
} else {
return null;
}
}
public static BufferedImage getSizedManaSymbol(String symbol) {
if (!manaImages.containsKey(GUISizeHelper.symbolDialogSize)) {
loadSymbolsImages(GUISizeHelper.symbolDialogSize);
return getSizedManaSymbol(symbol, GUISizeHelper.symbolDialogSize);
}
public static BufferedImage getSizedManaSymbol(String symbol, int size) {
if (!manaImages.containsKey(size)) {
loadSymbolsImages(size);
}
Map<String, BufferedImage> sizedSymbols = manaImages.get(GUISizeHelper.symbolDialogSize);
Map<String, BufferedImage> sizedSymbols = manaImages.get(size);
return sizedSymbols.get(symbol);
}
}

View file

@ -0,0 +1,934 @@
/*
* 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 java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.font.TextMeasurer;
import java.awt.image.BufferedImage;
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;
import javax.swing.ImageIcon;
import mage.ObjectColor;
import mage.constants.CardType;
import mage.view.CardView;
import mage.view.PermanentView;
import org.apache.log4j.Logger;
import org.mage.card.arcane.CardRenderer;
import org.mage.card.arcane.CardRendererUtils;
import org.mage.card.arcane.ManaSymbols;
import org.mage.card.arcane.TextboxLoyaltyRule;
import org.mage.card.arcane.TextboxRule;
import org.mage.card.arcane.TextboxRuleType;
import sun.security.pkcs11.P11TlsKeyMaterialGenerator;
/*
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 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()));
}
public static Paint BG_TEXTURE_WHITE = loadBackgroundTexture("white");
public static Paint BG_TEXTURE_BLUE = loadBackgroundTexture("blue");
public static Paint BG_TEXTURE_BLACK = loadBackgroundTexture("black");
public static Paint BG_TEXTURE_RED = loadBackgroundTexture("red");
public static Paint BG_TEXTURE_GREEN = loadBackgroundTexture("green");
public static Paint BG_TEXTURE_GOLD = loadBackgroundTexture("gold");
public static Paint BG_TEXTURE_ARTIFACT = loadBackgroundTexture("artifact");
public static Paint BG_TEXTURE_LAND = loadBackgroundTexture("land");
public static Color BORDER_WHITE = new Color(216, 203, 188);
public static Color BORDER_BLUE = new Color(20, 121, 175);
public static Color BORDER_BLACK = new Color(45, 45, 35);
public static Color BORDER_RED = new Color(201, 71, 58);
public static Color BORDER_GREEN = new Color(4, 136, 69);
public static Color BORDER_GOLD = new Color(255, 228, 124);
public static Color BORDER_COLORLESS = new Color(238, 242, 242);
public static Color BORDER_LAND = new Color(190, 173, 115);
public static Color BOX_WHITE = new Color(244, 245, 239);
public static Color BOX_BLUE = new Color(201, 223, 237);
public static Color BOX_BLACK = new Color(204, 194, 192);
public static Color BOX_RED = new Color(246, 208, 185);
public static Color BOX_GREEN = new Color(205, 221, 213);
public static Color BOX_GOLD = new Color(223, 195, 136);
public static Color BOX_COLORLESS = new Color(220, 228, 232);
public static Color BOX_LAND = new Color(220, 215, 213);
public static Color BOX_WHITE_NIGHT = new Color(169, 160, 145);
public static Color BOX_BLUE_NIGHT = new Color(46, 133, 176);
public static Color BOX_BLACK_NIGHT = new Color(95, 90, 89);
public static Color BOX_RED_NIGHT = new Color(188, 87, 57);
public static Color BOX_GREEN_NIGHT = new Color(31, 100, 44);
public static Color BOX_GOLD_NIGHT = new Color(171, 134, 70);
public static Color BOX_COLORLESS_NIGHT = new Color(118, 147, 158);
public static Color TEXTBOX_WHITE = new Color(252, 249, 244, 244);
public static Color TEXTBOX_BLUE = new Color(229, 238, 247, 244);
public static Color TEXTBOX_BLACK = new Color(241, 241, 240, 244);
public static Color TEXTBOX_RED = new Color(243, 224, 217, 244);
public static Color TEXTBOX_GREEN = new Color(217, 232, 223, 244);
public static Color TEXTBOX_GOLD = new Color(240, 234, 209, 244);
public static Color TEXTBOX_COLORLESS = new Color(219, 229, 233, 244);
public static Color TEXTBOX_LAND = new Color(218, 214, 212, 244);
public static 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 float BOX_HEIGHT_FRAC = 0.065f; // x cardHeight
protected static int BOX_HEIGHT_MIN = 16;
protected int boxHeight;
// How far down the card is the type line placed?
protected static float TYPE_LINE_Y_FRAC = 0.57f; // x cardHeight
protected int typeLineY;
// How large is the box text, and how far is it down the boxes
protected int boxTextHeight;
protected int boxTextOffset;
protected Font boxTextFont;
// 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 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)(TYPE_LINE_Y_FRAC * cardHeight);
// Box text height
boxTextHeight = getTextHeightForBoxHeight(boxHeight);
boxTextOffset = (boxHeight - boxTextHeight)/2;
boxTextFont = new Font("Beleren", Font.PLAIN, boxTextHeight);
// Box text height
ptTextHeight = getPTTextHeightForLineHeight(boxHeight);
ptTextOffset = (boxHeight - ptTextHeight)/2;
ptTextFont = new Font("Beleren", Font.PLAIN, ptTextHeight);
}
@Override
protected void drawBorder(Graphics2D g) {
// Draw border as one rounded rectangle
g.setColor(Color.black);
g.fillRoundRect(0, 0, cardWidth, cardHeight, cornerRadius, cornerRadius);
}
@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()));
// 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);
}
}
@Override
protected void drawArt(Graphics2D g) {
if (artImage != null) {
int imgWidth = artImage.getWidth();
int imgHeight = artImage.getHeight();
BufferedImage subImg =
artImage.getSubimage(
(int)(.079*imgWidth), (int)(.11*imgHeight),
(int)(.84*imgWidth), (int)(.42*imgHeight));
g.drawImage(subImg,
totalContentInset+1, totalContentInset+boxHeight,
contentWidth - 2, typeLineY - totalContentInset - boxHeight,
null);
}
}
@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);
// Draw the main card content border
g.setPaint(borderPaint);
g.drawRect(
totalContentInset, totalContentInset,
contentWidth - 1, cardHeight - borderWidth*3 - totalContentInset - 1);
// Draw the textbox fill
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.getCardTypes().contains(CardType.PLANESWALKER)) {
g.setPaint(borderPaint);
g.fillRect(
totalContentInset, typeLineY + boxHeight,
cardWidth/16, cardHeight - typeLineY - boxHeight - borderWidth*3);
}
// 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,
totalContentInset + nameOffset, totalContentInset,
contentWidth - nameOffset, boxHeight);
// Draw the type line
drawTypeLine(g,
totalContentInset, typeLineY,
contentWidth, boxHeight);
// Draw the textbox rules
drawRulesText(g,
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, int x, int y, int w, int h) {
// Width of the mana symbols
int manaCostWidth = CardRendererUtils.getManaCostWidth(manaCostString, 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
AttributedString str = new AttributedString(cardView.getName());
str.addAttribute(TextAttribute.FONT, boxTextFont);
TextMeasurer measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext());
TextLayout layout = measure.getLayout(0, measure.getLineBreakIndex(0, availableWidth));
g.setColor(getBoxTextColor());
layout.draw(g, x, y + boxTextOffset + boxTextHeight - 1);
// Draw the mana symbols
ManaSymbols.draw(g, manaCostString, x + w - manaCostWidth, y + boxTextOffset, boxTextHeight);
}
// Draw the type line (color indicator, types, and expansion symbol)
protected void drawTypeLine(Graphics2D g, int x, int y, int w, int h) {
// Draw expansion symbol
int expansionSymbolWidth = drawExpansionSymbol(g, x, y, w, h);
// Draw type line text
int availableWidth = w - expansionSymbolWidth + 1;
String types = getCardTypeLine();
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.");
}
AttributedString str = new AttributedString(types);
str.addAttribute(TextAttribute.FONT, boxTextFont);
TextMeasurer measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext());
TextLayout layout = measure.getLayout(0, measure.getLineBreakIndex(0, availableWidth));
g.setColor(getBoxTextColor());
layout.draw(g, x, y + boxTextOffset + boxTextHeight - 1);
}
// Draw the P/T and/or Loyalty boxes
protected void drawBottomRight(Graphics2D g, Paint borderPaint, Color fill) {
// 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?
if (cardView.getCardTypes().contains(CardType.CREATURE)) {
int x = cardWidth - borderWidth - partWidth;
// Draw PT box
CardRendererUtils.drawRoundedBox(g,
x, curY - boxHeight,
partWidth, boxHeight,
contentInset,
borderPaint,
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
g.setColor(getBoxTextColor());
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);
// Does it have damage on it?
if ((cardView instanceof PermanentView) && ((PermanentView)cardView).getDamage() > 0) {
// Show marked damage
}
curY -= boxHeight;
}
// Is it a walker?
if (cardView.getCardTypes().contains(CardType.PLANESWALKER)) {
// 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);
}
}
// Draw the card's textbox in a given rect
protected boolean loyaltyAbilityColorToggle = false;
protected void drawRulesText(Graphics2D g, int x, int y, int w, int h) {
// Initial font size to try to render at
Font font = new Font("Arial", Font.PLAIN, 12);
Font fontItalic = new Font("Arial", Font.ITALIC, 12);
// Handle the keyword rules
boolean hasKeywords = !textboxKeywords.isEmpty();
String keywordRulesString = getKeywordRulesString();
AttributedString keywordRulesAttributed = new AttributedString(keywordRulesString);
if (hasKeywords) {
keywordRulesAttributed.addAttribute(TextAttribute.FONT, font);
}
// Get the total height
List<AttributedString> attributedRules = new ArrayList<>();
boolean useSmallFont = false;
int remaining = h;
{
if (hasKeywords) {
remaining -= drawSingleRule(g, keywordRulesAttributed, null, 0, 0, w, remaining, false);
}
for (TextboxRule rule: textboxRules) {
AttributedString attributed = rule.generateAttributedString(font, fontItalic);
attributedRules.add(attributed);
remaining -= drawSingleRule(g, attributed, rule, 0, 0, w, remaining, false);
if (remaining < 0) {
useSmallFont = true;
break;
}
}
}
// If there wasn't enough room, try using a smaller font
if (useSmallFont) {
font = new Font("Arial", Font.PLAIN, 9);
fontItalic = new Font("Arial", Font.ITALIC, 9);
if (hasKeywords) {
keywordRulesAttributed = new AttributedString(keywordRulesString);
keywordRulesAttributed.addAttribute(TextAttribute.FONT, font);
}
// Clear out the attributed rules and reatribute them with the new font size
attributedRules.clear();
for (TextboxRule rule: textboxRules) {
AttributedString attributed = rule.generateAttributedString(font, fontItalic);
attributedRules.add(attributed);
}
}
// Do we have room for additional spacing between the parts of text?
// If so, calculate the spacing based on how much space was left over
int spacing;
if (useSmallFont) {
spacing = 0;
} else {
spacing = (int)(remaining / (hasKeywords ? (textboxRules.size() + 2) : (textboxRules.size() + 1)));
}
// Do the actual draw
loyaltyAbilityColorToggle = false;
g.setColor(Color.black);
int curY = y + spacing;
if (hasKeywords) {
int adv = drawSingleRule(g, keywordRulesAttributed, null, x, y, w, h, true);
curY += adv + spacing;
h -= adv;
}
for (int i = 0; i < textboxRules.size(); ++i) {
TextboxRule rule = textboxRules.get(i);
AttributedString attributedRule = attributedRules.get(i);
int adv = drawSingleRule(g, attributedRule, rule, x, curY, w, h, true);
curY += adv + spacing;
h -= adv;
if (h < 0) {
break;
}
}
}
// Get the first line of the textbox, the keyword string
private String getKeywordRulesString() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < textboxKeywords.size(); ++i) {
builder.append(textboxKeywords.get(i).text);
if (i != textboxKeywords.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
char ch = newLineCheck.setIndex(measure.getPosition());
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 (isNightCard()) {
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 (isNightCard()) {
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) {
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 static Color getBoxColor(ObjectColor colors, Collection<CardType> types, boolean isNightCard) {
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 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();
// Special case for two colors, gradient paint
return new LinearGradientPaint(
0, 0, width, 0,
new float[]{0.4f, 0.6f},
new Color[]{
getTextboxColor(twoColors.get(0)),
getTextboxColor(twoColors.get(1))
});
} else {
return TEXTBOX_GOLD;
}
} else if (colors.isColorless()) {
if (types.contains(CardType.LAND)) {
return TEXTBOX_LAND;
} else {
return TEXTBOX_COLORLESS;
}
} else {
return getTextboxColor(colors);
}
}
}

View file

@ -0,0 +1,17 @@
/*
* 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 java.util.List;
/**
* @author stravant@gmail.com
*/
public class TextboxKeywordRule extends TextboxRule {
public TextboxKeywordRule(String text, List<AttributeRegion> regions) {
super(text, regions, TextboxRuleType.SIMPLE_KEYWORD);
}
}

View file

@ -0,0 +1,28 @@
/*
* 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 java.text.AttributedString;
import java.util.List;
/**
* @author StravantUser
*
* Level rule associated with leveler cards
*/
public class TextboxLevelRule extends TextboxRule {
// The levels that this rule applies to
public int levelFrom;
public int levelTo;
public static int AND_HIGHER = 100;
public TextboxLevelRule(String text, List<AttributeRegion> regions, int levelFrom, int levelTo) {
super(text, regions, TextboxRuleType.LEVEL);
this.levelFrom = levelFrom;
this.levelTo = levelTo;
}
}

View file

@ -0,0 +1,33 @@
/*
* 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 java.text.AttributedString;
import java.util.List;
/**
* @author StravantUser
*/
public class TextboxLoyaltyRule extends TextboxRule {
public int loyaltyChange;
public static int MINUS_X = 100;
public String getChangeString() {
if (loyaltyChange == MINUS_X) {
return "-X";
} else if (loyaltyChange > 0) {
return "+" + loyaltyChange;
} else {
return "" + loyaltyChange;
}
}
public TextboxLoyaltyRule(String text, List<AttributeRegion> regions, int loyaltyChange) {
super(text, regions, TextboxRuleType.LOYALTY);
this.loyaltyChange = loyaltyChange;
}
}

View file

@ -0,0 +1,94 @@
/*
* 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 java.awt.Font;
import java.awt.Image;
import java.awt.font.GraphicAttribute;
import java.awt.font.ImageGraphicAttribute;
import java.awt.font.TextAttribute;
import java.text.AttributedString;
import java.util.List;
/**
* @author stravant@gmail.com
*
* Class describing parsed & translated rules in the text box of a card,
* ready to be rendered.
*/
public class TextboxRule {
// An attributed region in the text, which can be applied to an
// attributed string.
public interface AttributeRegion {
public void applyToAttributedString(AttributedString str, Font normal, Font italic);
}
// A region of italics, or bold text in a
public static class ItalicRegion implements AttributeRegion {
ItalicRegion(int start, int end) {
this.start = start;
this.end = end;
}
private final int start;
private final int end;
@Override
public void applyToAttributedString(AttributedString str, Font normal, Font italic) {
if (end > start+1) {
str.addAttribute(TextAttribute.FONT, italic, start, end);
}
}
}
// A special symbol embedded at some point in a string
public static class EmbeddedSymbol implements AttributeRegion {
EmbeddedSymbol(String symbol, int location) {
this.symbol = symbol;
this.location = location;
}
private final String symbol;
private final int location;
@Override
public void applyToAttributedString(AttributedString str, Font normal, Font italic) {
Image symbolImage = ManaSymbols.getSizedManaSymbol(symbol, normal.getSize());
if (symbolImage != null) {
ImageGraphicAttribute imgAttr =
new ImageGraphicAttribute(symbolImage, GraphicAttribute.BOTTOM_ALIGNMENT);
str.addAttribute(TextAttribute.CHAR_REPLACEMENT, imgAttr, location, location+1);
}
}
}
public String text;
public TextboxRuleType type;
private List<AttributeRegion> regions;
protected TextboxRule(String text, List<AttributeRegion> regions, TextboxRuleType type) {
this.text = text;
this.type = type;
this.regions = regions;
}
public TextboxRule(String text, List<AttributeRegion> regions) {
this(text, regions, TextboxRuleType.NORMAL);
}
public AttributedString generateAttributedString(Font normal, Font italic) {
// Build the final attributed text using the regions
// Do it in reverse order for proper handling of regions where
// there are multiple attributes stacked (EG: bold + italic)
AttributedString attributedRule = new AttributedString(text);
if (text.length() != 0) {
attributedRule.addAttribute(TextAttribute.FONT, normal);
for (int i = regions.size()-1; i >= 0; --i) {
regions.get(i).applyToAttributedString(attributedRule, normal, italic);
}
}
return attributedRule;
}
}

View file

@ -0,0 +1,250 @@
/*
* 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 java.awt.Font;
import java.awt.Image;
import java.awt.font.GraphicAttribute;
import java.awt.font.ImageGraphicAttribute;
import java.awt.font.TextAttribute;
import java.text.AttributedString;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import mage.view.CardView;
import org.apache.log4j.Logger;
/**
*
* @author StravantUser
*/
public class TextboxRuleParser {
private static final Logger LOGGER = Logger.getLogger(CardPanel.class);
private static final Pattern LevelAbilityPattern = Pattern.compile("Level (\\d+)-?(\\d*)(\\+?)");
private static final Pattern LoyaltyAbilityPattern = Pattern.compile("^(\\+|\\-)(\\d+|X): ");
private static final Pattern SimpleKeywordPattern = Pattern.compile("^(\\w+( \\w+)?)\\s*(\\([^\\)]*\\))?\\s*$");
// Parse a given rule (given as a string) into a TextboxRule, replacing
// symbol annotations, italics, etc, parsing out information such as
// if the ability is a loyalty ability, and returning an TextboxRule
// representing that information, which can be used to render the rule in
// the textbox of a card.
public static TextboxRule parse(CardView source, String rule) {
// List of regions to apply
ArrayList<TextboxRule.AttributeRegion> regions = new ArrayList<>();
// Leveler / loyalty
boolean isLeveler = false;
int levelFrom = 0;
int levelTo = 0;
boolean isLoyalty = false;
int loyaltyChange = 0;
// Parse the attributedString contents
int index = 0;
int outputIndex = 0;
// Is it a simple keyword ability?
{
Matcher simpleKeywordMatch = SimpleKeywordPattern.matcher(rule);
if (simpleKeywordMatch.find()) {
return new TextboxKeywordRule(simpleKeywordMatch.group(1), regions);
}
}
// Check if it's a loyalty ability. Must be right at the start of the rule
{
Matcher loyaltyMatch = LoyaltyAbilityPattern.matcher(rule);
if (loyaltyMatch.find()) {
// Get the loyalty change
if (loyaltyMatch.group(2).equals("X")) {
loyaltyChange = TextboxLoyaltyRule.MINUS_X;
} else {
loyaltyChange = Integer.parseInt(loyaltyMatch.group(2));
if (loyaltyMatch.group(1).equals("-")) {
loyaltyChange = -loyaltyChange;
}
}
isLoyalty = true;
// Go past the match
index = loyaltyMatch.group().length();
}
}
Deque<Integer> openingStack = new ArrayDeque<>();
StringBuilder build = new StringBuilder();
while (index < rule.length()) {
int initialIndex = index;
char ch = rule.charAt(index);
if (ch == '{') {
// Handling for `{this}`
int closeIndex = rule.indexOf('}', index);
if (closeIndex == -1) {
// Malformed input, nothing to do
++index;
++outputIndex;
build.append(ch);
} else {
String contents = rule.substring(index+1, closeIndex);
if (contents.equals("this") || contents.equals("source")) {
// Replace {this} with the card's name
String cardName = source.getName();
build.append(cardName);
index += contents.length() + 2;
outputIndex += cardName.length();
} else {
Image symbol = ManaSymbols.getSizedManaSymbol(contents, 10);
if (symbol != null) {
// Mana or other inline symbol
build.append('#');
regions.add(new TextboxRule.EmbeddedSymbol(contents, outputIndex));
++outputIndex;
index = closeIndex+1;
} else {
// Bad entry
build.append('{');
build.append(contents);
build.append('}');
index = closeIndex+1;
outputIndex += (contents.length() + 2);
}
}
}
} else if (ch == '&') {
// Handling for `&mdash;`
if (rule.startsWith("&mdash;", index)) {
build.append('—');
index += 7;
++outputIndex;
} else if (rule.startsWith("&bull", index)) {
build.append('•');
index += 5;
++outputIndex;
} else {
LOGGER.error("Bad &...; sequence `" + rule.substring(index+1, index+10) + "` in rule.");
build.append('&');
++index;
++outputIndex;
}
} else if (ch == '<') {
// Handling for `<i>` and `<br/>`
int closeIndex = rule.indexOf('>', index);
if (closeIndex != -1) {
// Is a tag
String tag = rule.substring(index+1, closeIndex);
if (tag.charAt(tag.length()-1) == '/') {
// Pure closing tag (like <br/>)
if (tag.equals("br/")) {
build.append('\n');
++outputIndex;
} else {
// Unknown
build.append('<').append(tag).append('>');
outputIndex += (tag.length() + 2);
}
} else if (tag.charAt(0) == '/') {
// Opening index for the tag
int openingIndex;
if (openingStack.isEmpty()) {
// Malformed input, just make an empty interval
openingIndex = outputIndex;
} else {
openingIndex = openingStack.pop();
}
// What tag is it?
if (tag.equals("/i")) {
// Italics
regions.add(new TextboxRule.ItalicRegion(openingIndex, outputIndex));
} else if (tag.equals("/b")) {
// Bold, see if it's a level ability
String content = build.substring(openingIndex);
Matcher levelMatch = LevelAbilityPattern.matcher(content);
if (levelMatch.find()) {
try {
levelFrom = Integer.parseInt(levelMatch.group(1));
if (!levelMatch.group(2).equals("")) {
levelTo = Integer.parseInt(levelMatch.group(2));
}
if (!levelMatch.group(3).equals("")) {
levelTo = TextboxLevelRule.AND_HIGHER;
}
isLeveler = true;
} catch (Exception e) {
LOGGER.error("Bad leveler levels in rule `" + rule + "`.");
}
}
} else {
// Unknown
build.append('<').append(tag).append('>');
outputIndex += (tag.length() + 2);
}
} else {
// Is it a <br> tag special case? [Why can't it have a closing `/`... =( ]
if (tag.equals("br")) {
build.append('\n');
++outputIndex;
} else {
// Opening tag
openingStack.push(outputIndex);
}
}
// Skip characters
index = closeIndex+1;
} else {
// Malformed tag
build.append('<');
++outputIndex;
++index;
}
} else {
// Normal character
++index;
++outputIndex;
build.append(ch);
}
if (outputIndex != build.length()) {
// Somehow our parsing code output symbols but didn't update the output index correspondingly
LOGGER.error("The human is dead; mismatch! Failed on rule: `" + rule + "` due to not updating outputIndex properly.");
// Bail out
build = new StringBuilder(rule);
regions.clear();
break;
}
if (index == initialIndex) {
// Somehow our parsing failed to consume the
LOGGER.error("Failed on rule `" + rule + "` due to not consuming a character.");
// Bail out
build = new StringBuilder(rule);
regions.clear();
break;
}
}
// Build and return the rule
rule = build.toString();
if (isLoyalty) {
return new TextboxLoyaltyRule(rule, regions, loyaltyChange);
} else if (isLeveler) {
return new TextboxLevelRule(rule, regions, levelFrom, levelTo);
} else {
return new TextboxRule(rule, regions);
}
}
}

View file

@ -0,0 +1,25 @@
/*
* 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;
/**
* @author stravant@gmail.com
*/
public enum TextboxRuleType {
/* Normal abilities, just rendered as lines of text with embedded symbols
* replaced to the relevant images. */
NORMAL,
/* Keyword ability. To be displayed in the comma separated list at the
* very top of the rules box */
SIMPLE_KEYWORD,
/* Loyalty abilities on planeswalkers */
LOYALTY,
/* Levelup creature - static ability at a given level */
LEVEL
}

View file

@ -17,6 +17,7 @@ import javax.swing.JDialog;
import javax.swing.JLayeredPane;
import mage.cards.MagePermanent;
import mage.cards.action.ActionCallback;
import mage.client.dialog.PreferencesDialog;
import mage.client.util.GUISizeHelper;
import mage.constants.Rarity;
import mage.interfaces.plugin.CardPlugin;
@ -31,6 +32,7 @@ import net.xeoh.plugins.base.annotations.meta.Author;
import org.apache.log4j.Logger;
import org.mage.card.arcane.Animation;
import org.mage.card.arcane.CardPanel;
import org.mage.card.arcane.CardPanelComponentImpl;
import org.mage.card.arcane.ManaSymbols;
import org.mage.plugins.card.dl.DownloadGui;
import org.mage.plugins.card.dl.DownloadJob;
@ -41,6 +43,7 @@ import org.mage.plugins.card.dl.sources.GathererSets;
import org.mage.plugins.card.dl.sources.GathererSymbols;
import org.mage.plugins.card.images.ImageCache;
import org.mage.plugins.card.info.CardInfoPaneImpl;
import org.mage.card.arcane.CardPanelRenderImpl;
/**
* {@link CardPlugin} implementation.
@ -105,10 +108,23 @@ public class CardPluginImpl implements CardPlugin {
cardWidthMin = (int) GUISizeHelper.battlefieldCardMinDimension.getWidth();
cardWidthMax = (int) GUISizeHelper.battlefieldCardMaxDimension.getWidth();
}
/**
* Temporary card rendering shim. Split card rendering isn't implemented yet, so
* use old component based rendering for the split cards.
*/
private CardPanel makePanel(CardView view, UUID gameId, boolean loadImage, ActionCallback callback, boolean isFoil, Dimension dimension) {
String fallback = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_RENDERING_FALLBACK, "false");
if (view.isSplitCard() || fallback.equals("true")) {
return new CardPanelComponentImpl(view, gameId, loadImage, callback, isFoil, dimension);
} else {
return new CardPanelRenderImpl(view, gameId, loadImage, callback, isFoil, dimension);
}
}
@Override
public MagePermanent getMagePermanent(PermanentView permanent, Dimension dimension, UUID gameId, ActionCallback callback, boolean canBeFoil, boolean loadImage) {
CardPanel cardPanel = new CardPanel(permanent, gameId, loadImage, callback, false, dimension);
CardPanel cardPanel = makePanel(permanent, gameId, loadImage, callback, false, dimension);
boolean implemented = !permanent.getRarity().equals(Rarity.NA);
cardPanel.setShowCastingCost(implemented);
return cardPanel;
@ -116,7 +132,7 @@ public class CardPluginImpl implements CardPlugin {
@Override
public MagePermanent getMageCard(CardView cardView, Dimension dimension, UUID gameId, ActionCallback callback, boolean canBeFoil, boolean loadImage) {
CardPanel cardPanel = new CardPanel(cardView, gameId, loadImage, callback, false, dimension);
CardPanel cardPanel = makePanel(cardView, gameId, loadImage, callback, false, dimension);
boolean implemented = cardView.getRarity() != null && !cardView.getRarity().equals(Rarity.NA);
cardPanel.setShowCastingCost(implemented);
return cardPanel;