Files
forge/forge-gui-mobile/src/forge/card/CardImageRenderer.java
2017-07-05 02:09:52 +00:00

502 lines
22 KiB
Java

package forge.card;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont.HAlignment;
import com.google.common.collect.ImmutableList;
import forge.Graphics;
import forge.assets.FBufferedImage;
import forge.assets.FSkinColor;
import forge.assets.FSkinFont;
import forge.assets.FSkinImage;
import forge.assets.FSkinTexture;
import forge.assets.ImageCache;
import forge.assets.TextRenderer;
import forge.card.CardDetailUtil.DetailColors;
import forge.card.CardRenderer.CardStackPosition;
import forge.card.mana.ManaCost;
import forge.game.GameView;
import forge.game.card.CardView;
import forge.game.card.CardView.CardStateView;
import forge.game.zone.ZoneType;
import forge.screens.FScreen;
import forge.screens.match.MatchController;
import forge.util.Utils;
public class CardImageRenderer {
private static final float BASE_IMAGE_WIDTH = 360;
private static final float BASE_IMAGE_HEIGHT = 504;
private static float MANA_SYMBOL_SIZE, PT_BOX_WIDTH, HEADER_PADDING, BORDER_THICKNESS;
private static FSkinFont NAME_FONT, TYPE_FONT, TEXT_FONT, PT_FONT;
private static float prevImageWidth, prevImageHeight;
private static final float BLACK_BORDER_THICKNESS_RATIO = 0.021f;
public static void forceStaticFieldUpdate() {
//force static fields to be updated the next time a card image is rendered
prevImageWidth = 0;
prevImageHeight = 0;
forgeArt.clear();
}
private static void updateStaticFields(float w, float h) {
if (w == prevImageWidth && h == prevImageHeight) {
//for performance sake, only update static fields if card image size is different than previous rendered card
return;
}
float ratio = Math.min(w / BASE_IMAGE_WIDTH, h / BASE_IMAGE_HEIGHT);
MANA_SYMBOL_SIZE = 20 * ratio;
PT_BOX_WIDTH = 56 * ratio;
HEADER_PADDING = 5 * ratio;
NAME_FONT = FSkinFont.forHeight(MANA_SYMBOL_SIZE);
TYPE_FONT = FSkinFont.forHeight(MANA_SYMBOL_SIZE * 0.9f);
TEXT_FONT = FSkinFont.forHeight(MANA_SYMBOL_SIZE * 0.95f);
PT_FONT = NAME_FONT;
BORDER_THICKNESS = Math.max(1.5f * ratio, 1f); //don't let border go below 1
prevImageWidth = w;
prevImageHeight = h;
}
public static void drawFaceDownCard(Graphics g, float x, float y, float w, float h) {
// TODO: improve the way a face-down card back is represented so it doesn't look as ugly
drawArt(g, x, y, w, h);
}
public static void drawCardImage(Graphics g, CardView card, boolean altState, float x, float y, float w, float h, CardStackPosition pos) {
updateStaticFields(w, h);
float blackBorderThickness = w * BLACK_BORDER_THICKNESS_RATIO;
g.fillRect(Color.BLACK, x, y, w, h);
x += blackBorderThickness;
y += blackBorderThickness;
w -= 2 * blackBorderThickness;
h -= 2 * blackBorderThickness;
final CardStateView state = card.getState(altState);
final boolean canShow = MatchController.instance.mayView(card);
if (!canShow) {
drawFaceDownCard(g, x, y, w, h);
return;
}
//determine colors for borders
final List<DetailColors> borderColors;
final boolean isFaceDown = card.getCurrentState().getState() == CardStateName.FaceDown;
if (isFaceDown) {
borderColors = ImmutableList.of(DetailColors.FACE_DOWN);
}
else {
borderColors = CardDetailUtil.getBorderColors(state, canShow);
}
Color[] colors = fillColorBackground(g, borderColors, x, y, w, h);
float artInset = blackBorderThickness * 0.5f;
float outerBorderThickness = 2 * blackBorderThickness - artInset;
x += outerBorderThickness;
y += outerBorderThickness;
w -= 2 * outerBorderThickness;
float headerHeight = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * NAME_FONT.getCapHeight()) + 2;
//draw header containing name and mana cost
Color[] headerColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.NAME_BOX_TINT);
drawHeader(g, card, state, headerColors, x, y, w, headerHeight);
if (pos == CardStackPosition.BehindVert) { return; } //remaining rendering not needed if card is behind another card in a vertical stack
boolean onTop = (pos == CardStackPosition.Top);
y += headerHeight;
float artWidth = w - 2 * artInset;
float artHeight = artWidth / CardRenderer.CARD_ART_RATIO;
float typeBoxHeight = 2 * TYPE_FONT.getCapHeight();
float ptBoxHeight = 0;
float textBoxHeight = h - headerHeight - artHeight - typeBoxHeight - outerBorderThickness - artInset;
if (state.isCreature() || state.isPlaneswalker() || state.getType().hasSubtype("Vehicle")) {
//if P/T box needed, make room for it
ptBoxHeight = 2 * PT_FONT.getCapHeight();
textBoxHeight -= ptBoxHeight;
}
else {
textBoxHeight -= 2 * artInset;
}
float minTextBoxHeight = 2 * headerHeight;
if (textBoxHeight < minTextBoxHeight) {
if (textBoxHeight < minTextBoxHeight) {
artHeight -= (minTextBoxHeight - textBoxHeight); //subtract from art height if text box not big enough otherwise
textBoxHeight = minTextBoxHeight;
if (artHeight < 0) {
textBoxHeight += artHeight;
artHeight = 0;
}
}
}
//draw art box with Forge icon
if (artHeight > 0) {
drawArt(g, x + artInset, y, artWidth, artHeight);
y += artHeight;
}
//draw type line
drawTypeLine(g, card, state, canShow, headerColors, x, y, w, typeBoxHeight);
y += typeBoxHeight;
//draw text box
Color[] textBoxColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.TEXT_BOX_TINT);
drawTextBox(g, card, state, textBoxColors, x + artInset, y, w - 2 * artInset, textBoxHeight, onTop);
y += textBoxHeight;
//draw P/T box
if (onTop && ptBoxHeight > 0) {
//only needed if on top since otherwise P/T will be hidden
Color[] ptColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.PT_BOX_TINT);
drawPtBox(g, card, state, ptColors, x, y - 2 * artInset, w, ptBoxHeight);
}
}
private static void drawHeader(Graphics g, CardView card, CardStateView state, Color[] colors, float x, float y, float w, float h) {
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
float padding = h / 8;
//draw mana cost for card
float manaCostWidth = 0;
ManaCost mainManaCost = state.getManaCost();
if (card.isSplitCard() && card.getAlternateState() != null) {
//handle rendering both parts of split card
mainManaCost = state.getManaCost();
ManaCost otherManaCost = card.getAlternateState().getManaCost();
manaCostWidth = CardFaceSymbols.getWidth(otherManaCost, MANA_SYMBOL_SIZE) + HEADER_PADDING;
CardFaceSymbols.drawManaCost(g, otherManaCost, x + w - manaCostWidth, y + (h - MANA_SYMBOL_SIZE) / 2, MANA_SYMBOL_SIZE);
//draw "//" between two parts of mana cost
manaCostWidth += NAME_FONT.getBounds("//").width + HEADER_PADDING;
g.drawText("//", NAME_FONT, Color.BLACK, x + w - manaCostWidth, y, w, h, false, HAlignment.LEFT, true);
}
manaCostWidth += CardFaceSymbols.getWidth(mainManaCost, MANA_SYMBOL_SIZE) + HEADER_PADDING;
CardFaceSymbols.drawManaCost(g, mainManaCost, x + w - manaCostWidth, y + (h - MANA_SYMBOL_SIZE) / 2, MANA_SYMBOL_SIZE);
//draw name for card
x += padding;
w -= 2 * padding;
g.drawText(state.getName(), NAME_FONT, Color.BLACK, x, y, w - manaCostWidth - padding, h, false, HAlignment.LEFT, true);
}
public static final FBufferedImage forgeArt;
static {
final float logoWidth = FSkinImage.LOGO.getWidth();
final float logoHeight = FSkinImage.LOGO.getHeight();
float h = logoHeight * 1.1f;
float w = h * CardRenderer.CARD_ART_RATIO;
forgeArt = new FBufferedImage(w, h) {
@Override
protected void draw(Graphics g, float w, float h) {
g.drawImage(FSkinTexture.BG_TEXTURE, 0, 0, w, h);
g.fillRect(FScreen.TEXTURE_OVERLAY_COLOR, 0, 0, w, h);
g.drawImage(FSkinImage.LOGO, (w - logoWidth) / 2, (h - logoHeight) / 2, logoWidth, logoHeight);
}
};
}
private static void drawArt(Graphics g, float x, float y, float w, float h) {
g.drawImage(forgeArt, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
}
private static void drawTypeLine(Graphics g, CardView card, CardStateView state, boolean canShow, Color[] colors, float x, float y, float w, float h) {
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
float padding = h / 8;
//draw square icon for rarity
float iconSize = h * 0.55f;
float iconPadding = (h - iconSize) / 2;
w -= iconSize + iconPadding * 2;
g.fillRect(CardRenderer.getRarityColor(state.getRarity()), x + w + iconPadding, y + (h - iconSize) / 2, iconSize, iconSize);
//draw type
x += padding;
g.drawText(CardDetailUtil.formatCardType(state, canShow), TYPE_FONT, Color.BLACK, x, y, w, h, false, HAlignment.LEFT, true);
}
//use text renderer to handle mana symbols and reminder text
private static final TextRenderer cardTextRenderer = new TextRenderer(true);
private static void drawTextBox(Graphics g, CardView card, CardStateView state, Color[] colors, float x, float y, float w, float h, boolean onTop) {
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
if (!onTop) { return; } //remaining rendering only needed if card on top
if (state.isBasicLand()) {
//draw icons for basic lands
FSkinImage image;
switch (state.getName().replaceFirst("^Snow-Covered ", "")) {
case "Plains":
image = FSkinImage.MANA_W;
break;
case "Island":
image = FSkinImage.MANA_U;
break;
case "Swamp":
image = FSkinImage.MANA_B;
break;
case "Mountain":
image = FSkinImage.MANA_R;
break;
case "Forest":
image = FSkinImage.MANA_G;
break;
default:
image = FSkinImage.MANA_COLORLESS;
break;
}
float iconSize = h * 0.75f;
g.drawImage(image, x + (w - iconSize) / 2, y + (h - iconSize) / 2, iconSize, iconSize);
}
else {
final String text = card.getText(state);
if (StringUtils.isEmpty(text)) { return; }
float padding = TEXT_FONT.getCapHeight() * 0.75f;
x += padding;
y += padding;
w -= 2 * padding;
h -= 2 * padding;
cardTextRenderer.drawText(g, text, TEXT_FONT, Color.BLACK, x, y, w, h, y, h, true, HAlignment.LEFT, true);
}
}
private static void drawPtBox(Graphics g, CardView card, CardStateView state, Color[] colors, float x, float y, float w, float h) {
List<String> pieces = new ArrayList<String>();
if (state.isCreature()) {
pieces.add(String.valueOf(state.getPower()));
pieces.add("/");
pieces.add(String.valueOf(state.getToughness()));
}
else if (state.isPlaneswalker()) {
pieces.add(String.valueOf(state.getLoyalty()));
}
else if (state.getType().hasSubtype("Vehicle")) {
// TODO Invert color box for Vehicles?
pieces.add("[");
pieces.add(String.valueOf(state.getPower()));
pieces.add("/");
pieces.add(String.valueOf(state.getToughness()));
pieces.add("]");
}
else { return; }
float padding = Math.round(PT_FONT.getCapHeight() / 4);
float totalPieceWidth = -padding;
float[] pieceWidths = new float[pieces.size()];
for (int i = 0; i < pieces.size(); i++) {
float pieceWidth = PT_FONT.getBounds(pieces.get(i)).width + padding;
pieceWidths[i] = pieceWidth;
totalPieceWidth += pieceWidth;
}
float boxHeight = PT_FONT.getCapHeight() + PT_FONT.getAscent() + 3 * padding;
float boxWidth = Math.max(PT_BOX_WIDTH, totalPieceWidth + 2 * padding);
x += w - boxWidth;
y += h - boxHeight;
w = boxWidth;
h = boxHeight;
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
x += (boxWidth - totalPieceWidth) / 2;
for (int i = 0; i < pieces.size(); i++) {
g.drawText(pieces.get(i), PT_FONT, Color.BLACK, x, y, w, h, false, HAlignment.LEFT, true);
x += pieceWidths[i];
}
}
public static void drawZoom(Graphics g, CardView card, GameView gameView, boolean altState, float x, float y, float w, float h) {
final Texture image = ImageCache.getImage(card.getState(altState).getImageKey(MatchController.instance.getLocalPlayers()), true);
if (image == null) { //draw details if can't draw zoom
drawDetails(g, card, gameView, altState, x, y, w, h);
return;
}
if (image == ImageCache.defaultImage) { //support drawing card image manually if card image not found
drawCardImage(g, card, altState, x, y, w, h, CardStackPosition.Top);
}
else {
g.drawImage(image, x, y, w, h);
}
CardRenderer.drawFoilEffect(g, card, x, y, w, h);
}
public static void drawDetails(Graphics g, CardView card, GameView gameView, boolean altState, float x, float y, float w, float h) {
updateStaticFields(w, h);
float blackBorderThickness = w * BLACK_BORDER_THICKNESS_RATIO;
g.fillRect(Color.BLACK, x, y, w, h);
x += blackBorderThickness;
y += blackBorderThickness;
w -= 2 * blackBorderThickness;
h -= 2 * blackBorderThickness;
final CardStateView state = card.getState(altState);
final boolean canShow = MatchController.instance.mayView(card);
//determine colors for borders
final List<DetailColors> borderColors;
final boolean isFaceDown = card.getCurrentState().getState() == CardStateName.FaceDown;
if (isFaceDown) {
borderColors = ImmutableList.of(DetailColors.FACE_DOWN);
}
else {
borderColors = CardDetailUtil.getBorderColors(state, canShow);
}
Color[] colors = fillColorBackground(g, borderColors, x, y, w, h);
Color idForeColor = FSkinColor.getHighContrastColor(colors[0]);
float outerBorderThickness = 2 * blackBorderThickness;
x += outerBorderThickness;
y += outerBorderThickness;
w -= 2 * outerBorderThickness;
float cardNameBoxHeight = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * NAME_FONT.getCapHeight()) + 2 * TYPE_FONT.getCapHeight() + 2;
//draw name/type box
Color[] nameBoxColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.NAME_BOX_TINT);
drawDetailsNameBox(g, card, state, canShow, nameBoxColors, x, y, w, cardNameBoxHeight);
float innerBorderThickness = outerBorderThickness / 2;
float ptBoxHeight = 2 * PT_FONT.getCapHeight();
float textBoxHeight = h - cardNameBoxHeight - ptBoxHeight - outerBorderThickness - 3 * innerBorderThickness;
y += cardNameBoxHeight + innerBorderThickness;
Color[] textBoxColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.TEXT_BOX_TINT);
drawDetailsTextBox(g, state, gameView, canShow, textBoxColors, x, y, w, textBoxHeight);
y += textBoxHeight + innerBorderThickness;
Color[] ptColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.PT_BOX_TINT);
drawDetailsIdAndPtBox(g, card, state, canShow, idForeColor, ptColors, x, y, w, ptBoxHeight);
}
public static Color[] fillColorBackground(Graphics g, List<DetailColors> backColors, float x, float y, float w, float h) {
Color[] colors = new Color[backColors.size()];
for (int i = 0; i < colors.length; i++) {
DetailColors dc = backColors.get(i);
colors[i] = FSkinColor.fromRGB(dc.r, dc.g, dc.b);
}
fillColorBackground(g, colors, x, y, w, h);
return colors;
}
public static void fillColorBackground(Graphics g, Color[] colors, float x, float y, float w, float h) {
switch (colors.length) {
case 1:
g.fillRect(colors[0], x, y, w, h);
break;
case 2:
g.fillGradientRect(colors[0], colors[1], false, x, y, w, h);
break;
case 3:
float halfWidth = w / 2;
g.fillGradientRect(colors[0], colors[1], false, x, y, halfWidth, h);
g.fillGradientRect(colors[1], colors[2], false, x + halfWidth, y, halfWidth, h);
break;
}
}
private static void drawDetailsNameBox(Graphics g, CardView card, CardStateView state, boolean canShow, Color[] colors, float x, float y, float w, float h) {
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
float padding = h / 8;
//make sure name/mana cost row height is tall enough for both
h = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * NAME_FONT.getCapHeight());
//draw mana cost for card
float manaCostWidth = 0;
if (canShow) {
ManaCost mainManaCost = state.getManaCost();
if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack) { //only display current state's mana cost when on stack
//handle rendering both parts of split card
mainManaCost = state.getManaCost();
ManaCost otherManaCost = card.getAlternateState().getManaCost();
manaCostWidth = CardFaceSymbols.getWidth(otherManaCost, MANA_SYMBOL_SIZE) + HEADER_PADDING;
CardFaceSymbols.drawManaCost(g, otherManaCost, x + w - manaCostWidth, y + (h - MANA_SYMBOL_SIZE) / 2, MANA_SYMBOL_SIZE);
//draw "//" between two parts of mana cost
manaCostWidth += NAME_FONT.getBounds("//").width + HEADER_PADDING;
g.drawText("//", NAME_FONT, Color.BLACK, x + w - manaCostWidth, y, w, h, false, HAlignment.LEFT, true);
}
manaCostWidth += CardFaceSymbols.getWidth(mainManaCost, MANA_SYMBOL_SIZE) + HEADER_PADDING;
CardFaceSymbols.drawManaCost(g, mainManaCost, x + w - manaCostWidth, y + (h - MANA_SYMBOL_SIZE) / 2, MANA_SYMBOL_SIZE);
}
//draw name for card
x += padding;
w -= 2 * padding;
g.drawText(CardDetailUtil.formatCardName(card, canShow, state == card.getAlternateState()), NAME_FONT, Color.BLACK, x, y, w - manaCostWidth - padding, h, false, HAlignment.LEFT, true);
//draw type and set label for card
y += h;
h = 2 * TYPE_FONT.getCapHeight();
String set = state.getSetCode();
CardRarity rarity = state.getRarity();
if (!canShow) {
set = CardEdition.UNKNOWN.getCode();
rarity = CardRarity.Unknown;
}
if (!StringUtils.isEmpty(set)) {
float setWidth = CardRenderer.getSetWidth(TYPE_FONT, set);
CardRenderer.drawSetLabel(g, TYPE_FONT, set, rarity, x + w + padding - setWidth - HEADER_PADDING + CardRenderer.SET_BOX_MARGIN, y + CardRenderer.SET_BOX_MARGIN, setWidth, h - CardRenderer.SET_BOX_MARGIN);
w -= setWidth; //reduce available width for type
}
g.drawText(CardDetailUtil.formatCardType(state, canShow), TYPE_FONT, Color.BLACK, x, y, w, h, false, HAlignment.LEFT, true);
}
private static void drawDetailsTextBox(Graphics g, CardStateView state, GameView gameView, boolean canShow, Color[] colors, float x, float y, float w, float h) {
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
float padX = TEXT_FONT.getCapHeight() / 2;
float padY = padX + Utils.scale(2); //add a little more vertical padding
x += padX;
y += padY;
w -= 2 * padX;
h -= 2 * padY;
cardTextRenderer.drawText(g, CardDetailUtil.composeCardText(state, gameView, canShow), TEXT_FONT, Color.BLACK, x, y, w, h, y, h, true, HAlignment.LEFT, false);
}
private static void drawDetailsIdAndPtBox(Graphics g, CardView card, CardStateView state, boolean canShow, Color idForeColor, Color[] colors, float x, float y, float w, float h) {
float idWidth = 0;
if (canShow) {
String idText = CardDetailUtil.formatCardId(state);
g.drawText(idText, TYPE_FONT, idForeColor, x, y + TYPE_FONT.getCapHeight() / 2, w, h, false, HAlignment.LEFT, false);
idWidth = TYPE_FONT.getBounds(idText).width;
}
String ptText = CardDetailUtil.formatPowerToughness(state, canShow);
if (StringUtils.isEmpty(ptText)) { return; }
float padding = PT_FONT.getCapHeight() / 2;
float boxWidth = Math.min(PT_FONT.getBounds(ptText).width + 2 * padding,
w - idWidth - padding); //prevent box overlapping ID
x += w - boxWidth;
w = boxWidth;
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
g.drawText(ptText, PT_FONT, Color.BLACK, x, y, w, h, false, HAlignment.CENTER, true);
}
}