Implement custom text box renderer

This commit is contained in:
Lyu Zong-Hong
2021-07-26 22:15:08 +09:00
parent 27856e65a0
commit e6c5c8989c
2 changed files with 293 additions and 19 deletions

View File

@@ -77,8 +77,10 @@ public class CardFaceSymbols {
MANA_IMAGES.put("S", FSkin.getImage(FSkinProp.IMG_MANA_SNOW));
MANA_IMAGES.put("T", FSkin.getImage(FSkinProp.IMG_TAP));
MANA_IMAGES.put("Q", FSkin.getImage(FSkinProp.IMG_UNTAP));
MANA_IMAGES.put("E", FSkin.getImage(FSkinProp.IMG_ENERGY, 40, 40));
MANA_IMAGES.put("EXPERIENCE", FSkin.getImage(FSkinProp.IMG_EXPERIENCE, 40, 30));
MANA_IMAGES.put("CHAOS", FSkin.getImage(FSkinProp.IMG_CHAOS));
MANA_IMAGES.put("slash", FSkin.getImage(FSkinProp.IMG_SLASH));
MANA_IMAGES.put("attack", FSkin.getImage(FSkinProp.IMG_ATTACK, 32, 32));
MANA_IMAGES.put("defend", FSkin.getImage(FSkinProp.IMG_DEFEND, 32, 32));

View File

@@ -9,10 +9,14 @@ import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.swing.JEditorPane;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
@@ -24,7 +28,9 @@ import forge.game.card.CardView.CardStateView;
import forge.gui.card.CardDetailUtil;
import forge.gui.card.CardDetailUtil.DetailColors;
import forge.item.PaperCard;
import forge.localinstance.properties.ForgePreferences.FPref;
import forge.localinstance.skin.FSkinProp;
import forge.model.FModel;
import forge.toolbox.CardFaceSymbols;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinIcon;
@@ -34,8 +40,8 @@ public class FCardImageRenderer {
private static final float BASE_IMAGE_WIDTH = 480;
private static final float BASE_IMAGE_HEIGHT = 680;
private static float MANA_SYMBOL_SIZE, PT_BOX_WIDTH, HEADER_PADDING, BORDER_THICKNESS;
private static Font NAME_FONT, TYPE_FONT, TEXT_FONT, PT_FONT;
private static FontMetrics NAME_METRICS, TYPE_METRICS, TEXT_METRICS, PT_METRICS;
private static Font NAME_FONT, TYPE_FONT, TEXT_FONT, REMINDER_FONT, PT_FONT;
private static FontMetrics NAME_METRICS, TYPE_METRICS, TEXT_METRICS, REMINDER_METRICS, PT_METRICS;
private static float prevImageWidth, prevImageHeight;
private static final float BLACK_BORDER_THICKNESS_RATIO = 0.021f;
private static final float NAME_BOX_TINT = 0.2f;
@@ -43,6 +49,13 @@ public class FCardImageRenderer {
private static final float PT_BOX_TINT = 0.2f;
private static final float CARD_ART_RATIO = 1.32f;
private static Locale locale;
private static BreakIterator boundary;
private static Pattern linebreakPattern;
private static Pattern reminderPattern;
private static Pattern symbolPattern;
private static final Map<Font, Font[]> shrinkFonts = new HashMap<>();
private static Color tintColor(Color source, Color tint, float alpha) {
float r = (tint.getRed() - source.getRed()) * alpha + source.getRed();
float g = (tint.getGreen() - source.getGreen()) * alpha + source.getGreen();
@@ -83,12 +96,27 @@ public class FCardImageRenderer {
}
}
private static Font getShrinkFont(Font orgFont, int newSize) {
Font font = shrinkFonts.get(orgFont)[newSize];
if (font == null) {
font = orgFont.deriveFont((float)newSize);
shrinkFonts.get(orgFont)[newSize] = font;
}
return font;
}
private static void updateStaticFields(Graphics2D g, 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;
}
locale = new Locale(FModel.getPreferences().getPref(FPref.UI_LANGUAGE));
boundary = BreakIterator.getLineInstance(locale);
linebreakPattern = Pattern.compile("(\r\n\r\n)|(\n)");
reminderPattern = Pattern.compile("\\((.+?)\\)");
symbolPattern = Pattern.compile("\\{([A-Z0-9]+)\\}|\\{([A-Z0-9]+)/([A-Z0-9]+)\\}");
float ratio = Math.min(w / BASE_IMAGE_WIDTH, h / BASE_IMAGE_HEIGHT);
MANA_SYMBOL_SIZE = 26 * ratio;
@@ -96,14 +124,26 @@ public class FCardImageRenderer {
HEADER_PADDING = 7 * ratio;
NAME_FONT = new Font(Font.SERIF, Font.BOLD, Math.round(MANA_SYMBOL_SIZE));
TYPE_FONT = new Font(Font.SERIF, Font.BOLD, Math.round(MANA_SYMBOL_SIZE * 0.8f));
TEXT_FONT = new Font(Font.SERIF, Font.PLAIN, Math.round(MANA_SYMBOL_SIZE * 0.7f));
if ("ja-JP".equals(FModel.getPreferences().getPref(FPref.UI_LANGUAGE)) || "zh-CN".equals(FModel.getPreferences().getPref(FPref.UI_LANGUAGE))) {
TEXT_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(MANA_SYMBOL_SIZE * 0.93f));
REMINDER_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(MANA_SYMBOL_SIZE * 0.93f) - 2);
} else {
TEXT_FONT = new Font(Font.SERIF, Font.PLAIN, Math.round(MANA_SYMBOL_SIZE * 0.93f));
REMINDER_FONT = new Font(Font.SERIF, Font.ITALIC, Math.round(MANA_SYMBOL_SIZE * 0.93f));
}
PT_FONT = NAME_FONT;
NAME_METRICS = g.getFontMetrics(NAME_FONT);
TYPE_METRICS = g.getFontMetrics(TYPE_FONT);
TEXT_METRICS = g.getFontMetrics(TEXT_FONT);
REMINDER_METRICS = g.getFontMetrics(REMINDER_FONT);
PT_METRICS = NAME_METRICS;
BORDER_THICKNESS = Math.max(2 * ratio, 1f); //don't let border go below 1
shrinkFonts.put(NAME_FONT, new Font[NAME_FONT.getSize()]);
shrinkFonts.put(TYPE_FONT, new Font[TYPE_FONT.getSize()]);
shrinkFonts.put(TEXT_FONT, new Font[TEXT_FONT.getSize()]);
shrinkFonts.put(REMINDER_FONT, new Font[REMINDER_FONT.getSize()]);
prevImageWidth = w;
prevImageHeight = h;
}
@@ -155,7 +195,7 @@ public class FCardImageRenderer {
//draw text box
Color[] textBoxColors = tintColors(Color.WHITE, colors, TEXT_BOX_TINT);
drawTextBox(g, card, state, textBoxColors, x + artInset, textY, w - 2 * artInset, textBoxHeight);
drawTextBox(g, card, state, textBoxColors, x + artInset, textY, w - 2 * artInset, textBoxHeight, ptBoxHeight > 0);
//draw header containing name and mana cost
Color[] headerColors = tintColors(Color.WHITE, colors, NAME_BOX_TINT);
@@ -206,16 +246,21 @@ public class FCardImageRenderer {
g.setPaint(oldPaint);
}
private static void drawVerticallyCenteredString(Graphics2D g, String text, Rectangle area, Font font, final FontMetrics fontMetrics) {
Font oldFont = g.getFont();
private static void drawVerticallyCenteredString(Graphics2D g, String text, Rectangle area, Font font, FontMetrics fontMetrics) {
Font originalFont = font;
// Shrink font if the text is too long
while (fontMetrics.stringWidth(text) > area.width) {
int newSize = font.getSize() - 1;
font = getShrinkFont(originalFont, newSize);
fontMetrics = g.getFontMetrics(font);
}
int x = area.x;
int y = area.y + (area.height - fontMetrics.getHeight()) / 2 + fontMetrics.getAscent();
g.setFont(font);
g.drawString(text, x, y);
g.setFont(oldFont);
}
private static void drawHeader(Graphics2D g, CardView card, CardStateView state, Color[] colors, float x, float y, float w, float h) {
@@ -293,7 +338,7 @@ public class FCardImageRenderer {
TYPE_FONT, TYPE_METRICS);
}
private static void drawTextBox(Graphics2D g, CardView card, CardStateView state, Color[] colors, float x, float y, float w, float h) {
private static void drawTextBox(Graphics2D g, CardView card, CardStateView state, Color[] colors, float x, float y, float w, float h, boolean hasPTBox) {
fillColorBackground(g, colors, x, y, w, h);
g.setStroke(new BasicStroke(BORDER_THICKNESS));
g.setColor(Color.BLACK);
@@ -339,14 +384,7 @@ public class FCardImageRenderer {
float padding = TEXT_METRICS.getAscent() * 0.25f;
x += padding;
w -= 2 * padding;
String textShow = text.replaceAll("(\r\n\r\n)|(\n)", "<br>");
textShow = FSkin.encodeSymbols(textShow, true);
JEditorPane textArea = new JEditorPane("text/html", textShow);
textArea.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
textArea.setOpaque(false);
textArea.setFont(TEXT_FONT);
textArea.setSize(Math.round(w), Math.round(h));
textArea.print(g.create(Math.round(x), Math.round(y), Math.round(w), Math.round(h)));
drawTextBoxText(g, text, Math.round(x), Math.round(y), Math.round(w), Math.round(h), hasPTBox);
}
}
@@ -398,4 +436,238 @@ public class FCardImageRenderer {
x += pieceWidths[i];
}
}
private static abstract class Piece {
protected final boolean isReminder;
protected Piece(boolean isReminder) {
this.isReminder = isReminder;
}
public abstract void restart();
public abstract int getNextWidth(FontMetrics txMetrics, FontMetrics rmMetrics);
public abstract void drawPrev(Graphics2D g, int x, int y, Font txFont, Font rmFont, FontMetrics txMetrics);
}
private static class TextPiece extends Piece {
private String text;
private int index;
private List<Integer> boundaryList;
private void buildBoundaryList() {
boundaryList = new ArrayList<>();
boundary.setText(text);
boundaryList.add(boundary.first());
for (int next = boundary.next(); next != BreakIterator.DONE; next = boundary.next()) {
boundaryList.add(next);
}
}
public TextPiece(String text, boolean isReminder) {
super(isReminder);
this.text = text;
buildBoundaryList();
}
public void restart()
{
index = 0;
}
public int getNextWidth(FontMetrics txMetrics, FontMetrics rmMetrics) {
++index;
if (index == boundaryList.size()) {
return -1;
}
String subtext = text.substring(boundaryList.get(index - 1), boundaryList.get(index));
if (isReminder) {
return rmMetrics.stringWidth(subtext);
}
return txMetrics.stringWidth(subtext);
}
public void drawPrev(Graphics2D g, int x, int y, Font txFont, Font rmFont, FontMetrics txMetrics) {
int ascent = txMetrics.getAscent();
String subtext = text.substring(boundaryList.get(index - 1), boundaryList.get(index));
if (isReminder) {
g.setFont(rmFont);
} else {
g.setFont(txFont);
}
g.drawString(subtext, x, y + ascent);
}
}
private static class SymbolPiece extends Piece {
private List<String> symbols;
private boolean restarted;
public SymbolPiece(List<String> symbols) {
super(false);
this.symbols = symbols;
restarted = false;
}
public void restart() {
restarted = true;
}
public int getNextWidth(FontMetrics txMetrics, FontMetrics rmMetrics) {
if (restarted) {
int offset = Math.round(txMetrics.getAscent() * 0.8f);
restarted = false;
return offset * symbols.size();
}
return -1;
}
public void drawPrev(Graphics2D g, int x, int y, Font txFont, Font rmFont, FontMetrics txMetrics) {
int xoffset = Math.round(txMetrics.getAscent() * 0.8f);
int yoffset = txMetrics.getAscent() - xoffset + 1;
for (String s : symbols) {
CardFaceSymbols.drawSymbol(s, g, x, y + yoffset, xoffset - 1);
x += xoffset;
}
}
}
private static class Paragraph {
private String text;
private List<Piece> pieces;
private void parseSymbols(String subtext, boolean isReminder) {
List<String> symbols = new ArrayList<>();
Matcher sbMatcher = symbolPattern.matcher(subtext);
int parsed = 0;
while (sbMatcher.find()) {
if (sbMatcher.start() > parsed) {
if (!symbols.isEmpty()) {
pieces.add(new SymbolPiece(symbols));
symbols = new ArrayList<>();
}
pieces.add(new TextPiece(subtext.substring(parsed, sbMatcher.start()), isReminder));
}
symbols.add(sbMatcher.group(1) != null ? sbMatcher.group(1) : sbMatcher.group(2) + sbMatcher.group(3));
parsed = sbMatcher.end();
}
if (!symbols.isEmpty()) {
pieces.add(new SymbolPiece(symbols));
}
if (parsed < subtext.length())
pieces.add(new TextPiece(subtext.substring(parsed, subtext.length()), isReminder));
}
private void buildPieceList() {
pieces = new ArrayList<>();
Matcher rmMatcher = reminderPattern.matcher(text);
int parsed = 0;
while (rmMatcher.find()) {
// Non-reminder text
if (rmMatcher.start() > parsed) {
parseSymbols(text.substring(parsed, rmMatcher.start()), false);
}
parseSymbols(text.substring(rmMatcher.start(), rmMatcher.end()), true);
parsed = rmMatcher.end();
}
// Remaining text
if (parsed < text.length())
parseSymbols(text.substring(parsed, text.length()), false);
}
public Paragraph(String text) {
this.text = text;
buildPieceList();
}
public int calculateLines(int width, FontMetrics txMetrics, FontMetrics rmMetrics, boolean hasPTBox) {
int pos = 0;
int lines = 1;
for (Piece p : pieces) {
p.restart();
int w = p.getNextWidth(txMetrics, rmMetrics);
while (w != -1) {
if (pos + w > width) {
++lines;
pos = 0;
}
pos += w;
w = p.getNextWidth(txMetrics, rmMetrics);
}
}
// If last line will overlapp with PT box, add one more line.
if (hasPTBox && pos >= width - PT_BOX_WIDTH)
++lines;
return lines;
}
public int drawPieces(Graphics2D g, int x, int y, int width, int lineHeight,
Font txFont, FontMetrics txMetrics, Font rmFont, FontMetrics rmMetrics) {
int pos = 0;
int lines = 1;
for (Piece p : pieces) {
p.restart();
int w = p.getNextWidth(txMetrics, rmMetrics);
while (w != -1) {
if (pos + w > width) {
++lines;
pos = 0;
y += lineHeight;
}
p.drawPrev(g, x + pos, y, txFont, rmFont, txMetrics);
pos += w;
w = p.getNextWidth(txMetrics, rmMetrics);
}
}
return lines * lineHeight;
}
}
private static void drawTextBoxText(Graphics2D g, final String text, int x, int y, int w, int h, boolean hasPTBox) {
String [] paragraphs = linebreakPattern.split(text);
List<Paragraph> pgList = new ArrayList<>();
for (String pg : paragraphs) {
pgList.add(new Paragraph(pg));
}
// Find font size that fit in the text box area
Font txFont = TEXT_FONT, rmFont = REMINDER_FONT;
FontMetrics txMetrics = TEXT_METRICS, rmMetrics = REMINDER_METRICS;
int txFontSize = txFont.getSize(), rmFontSize = rmFont.getSize();
int lineHeight, paraSpacing, lineSpacing, totalHeight;
do {
int totalLineSpacings = 0;
totalHeight = 0;
paraSpacing = txMetrics.getLeading() + txMetrics.getDescent();
lineHeight = txMetrics.getAscent() + txMetrics.getDescent();
lineSpacing = -2;
for (int i = 0; i < pgList.size(); ++i) {
boolean ptBox = (i < pgList.size() - 1) ? false : hasPTBox;
Paragraph pg = pgList.get(i);
totalHeight += paraSpacing;
int lines = pg.calculateLines(w, txMetrics, rmMetrics, ptBox);
totalLineSpacings += lines - 1;
totalHeight += lines * lineHeight + (lines - 1) * lineSpacing;
}
while (totalHeight > h && lineSpacing > -txMetrics.getDescent()) {
--lineSpacing;
totalHeight -= totalLineSpacings;
}
if (totalHeight <= h)
break;
//Shrink font and do again
--txFontSize;
txFont = getShrinkFont(TEXT_FONT, txFontSize);
txMetrics = g.getFontMetrics(txFont);
--rmFontSize;
rmFont = getShrinkFont(REMINDER_FONT, rmFontSize);
rmMetrics = g.getFontMetrics(rmFont);
} while (txFontSize >= 8 && rmFontSize >= 8);
// Draw text
y += (h - totalHeight - paraSpacing / 2) / 2;
for (Paragraph pg : pgList) {
y += pg.drawPieces(g, x, y, w, lineSpacing + lineHeight, txFont, txMetrics, rmFont, rmMetrics);
y += paraSpacing - lineSpacing;
}
}
}