Use SVG graphics to draw chess pieces.

Use SVG graphics to draw chess pieces instead of a custom font. This should fix
the problem where some android devices ignore the requested font so the chess
pieces are drawn as two overlapping regular letters.

This should also make it easier to add alternative piece sets in the future.
This commit is contained in:
Peter Osterlund 2019-04-07 17:46:32 +02:00
parent c0eaf35bf6
commit 7c660323f0
7 changed files with 255 additions and 98 deletions

Binary file not shown.

View File

@ -111,7 +111,9 @@ public class SVG {
}
/**
* Gets the bounding rectangle for the SVG that was computed upon parsing. It may not be entirely accurate for certain curves or transformations, but is often better than nothing.
* Gets the bounding rectangle for the SVG that was computed upon parsing.
* It may not be entirely accurate for certain curves or transformations,
* but is often better than nothing.
* @return rectangle representing the computed bounds.
*/
public RectF getLimits() {

View File

@ -17,6 +17,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/*
@ -62,7 +63,7 @@ public class SVGParser {
* @throws SVGParseException if there is an error while parsing.
*/
public static SVG getSVGFromInputStream(InputStream svgData) throws SVGParseException {
return SVGParser.parse(svgData, 0, 0, false);
return SVGParser.parse(svgData, null, false);
}
/**
@ -73,7 +74,7 @@ public class SVGParser {
* @throws SVGParseException if there is an error while parsing.
*/
public static SVG getSVGFromString(String svgData) throws SVGParseException {
return SVGParser.parse(new ByteArrayInputStream(svgData.getBytes()), 0, 0, false);
return SVGParser.parse(new ByteArrayInputStream(svgData.getBytes()), null, false);
}
/**
@ -85,7 +86,7 @@ public class SVGParser {
* @throws SVGParseException if there is an error while parsing.
*/
public static SVG getSVGFromResource(Resources resources, int resId) throws SVGParseException {
return SVGParser.parse(resources.openRawResource(resId), 0, 0, false);
return SVGParser.parse(resources.openRawResource(resId), null, false);
}
/**
@ -108,26 +109,26 @@ public class SVGParser {
* Parse SVG data from an input stream, replacing a single color with another color.
*
* @param svgData the input stream, with SVG XML data in UTF-8 character encoding.
* @param searchColor the color in the SVG to replace.
* @param replaceColor the color with which to replace the search color.
* @param colorReplace Map from colors in the SVG to colors to use instead. May be null.
* @return the parsed SVG.
* @throws SVGParseException if there is an error while parsing.
*/
public static SVG getSVGFromInputStream(InputStream svgData, int searchColor, int replaceColor) throws SVGParseException {
return SVGParser.parse(svgData, searchColor, replaceColor, false);
public static SVG getSVGFromInputStream(InputStream svgData,
Map<Integer,Integer> colorReplace) throws SVGParseException {
return SVGParser.parse(svgData, colorReplace, false);
}
/**
* Parse SVG data from a string.
*
* @param svgData the string containing SVG XML data.
* @param searchColor the color in the SVG to replace.
* @param replaceColor the color with which to replace the search color.
* @param colorReplace Map from colors in the SVG to colors to use instead. May be null.
* @return the parsed SVG.
* @throws SVGParseException if there is an error while parsing.
*/
public static SVG getSVGFromString(String svgData, int searchColor, int replaceColor) throws SVGParseException {
return SVGParser.parse(new ByteArrayInputStream(svgData.getBytes()), searchColor, replaceColor, false);
public static SVG getSVGFromString(String svgData,
HashMap<Integer,Integer> colorReplace) throws SVGParseException {
return SVGParser.parse(new ByteArrayInputStream(svgData.getBytes()), colorReplace, false);
}
/**
@ -135,13 +136,13 @@ public class SVGParser {
*
* @param resources the Android context
* @param resId the ID of the raw resource SVG.
* @param searchColor the color in the SVG to replace.
* @param replaceColor the color with which to replace the search color.
* @param colorReplace Map from colors in the SVG to colors to use instead. May be null.
* @return the parsed SVG.
* @throws SVGParseException if there is an error while parsing.
*/
public static SVG getSVGFromResource(Resources resources, int resId, int searchColor, int replaceColor) throws SVGParseException {
return SVGParser.parse(resources.openRawResource(resId), searchColor, replaceColor, false);
public static SVG getSVGFromResource(Resources resources, int resId,
HashMap<Integer,Integer> colorReplace) throws SVGParseException {
return SVGParser.parse(resources.openRawResource(resId), colorReplace, false);
}
/**
@ -149,15 +150,15 @@ public class SVGParser {
*
* @param assetMngr the Android asset manager.
* @param svgPath the path to the SVG file in the application's assets.
* @param searchColor the color in the SVG to replace.
* @param replaceColor the color with which to replace the search color.
* @param colorReplace Map from colors in the SVG to colors to use instead. May be null.
* @return the parsed SVG.
* @throws SVGParseException if there is an error while parsing.
* @throws IOException if there was a problem reading the file.
*/
public static SVG getSVGFromAsset(AssetManager assetMngr, String svgPath, int searchColor, int replaceColor) throws SVGParseException, IOException {
public static SVG getSVGFromAsset(AssetManager assetMngr, String svgPath,
Map<Integer,Integer> colorReplace) throws SVGParseException, IOException {
InputStream inputStream = assetMngr.open(svgPath);
SVG svg = getSVGFromInputStream(inputStream, searchColor, replaceColor);
SVG svg = getSVGFromInputStream(inputStream, colorReplace);
inputStream.close();
return svg;
}
@ -172,7 +173,7 @@ public class SVGParser {
return doPath(pathString);
}
private static SVG parse(InputStream in, Integer searchColor, Integer replaceColor, boolean whiteMode) throws SVGParseException {
private static SVG parse(InputStream in, Map<Integer,Integer> colorReplace, boolean whiteMode) throws SVGParseException {
// Util.debug("Parsing SVG...");
try {
// long start = System.currentTimeMillis();
@ -181,7 +182,7 @@ public class SVGParser {
XMLReader xr = sp.getXMLReader();
final Picture picture = new Picture();
SVGHandler handler = new SVGHandler(picture);
handler.setColorSwap(searchColor, replaceColor);
handler.setColorSwap(colorReplace);
handler.setWhiteMode(whiteMode);
xr.setContentHandler(handler);
xr.parse(new InputSource(in));
@ -405,17 +406,29 @@ public class SVGParser {
case '6':
case '7':
case '8':
case '9':
if (prevCmd == 'm' || prevCmd == 'M') {
cmd = (char) (((int) prevCmd) - 1);
break;
} else if (prevCmd == 'c' || prevCmd == 'C') {
cmd = prevCmd;
break;
} else if (prevCmd == 'l' || prevCmd == 'L') {
cmd = prevCmd;
break;
case '9': {
boolean handled = true;
switch (prevCmd) {
case 'm':
cmd = 'l';
break;
case 'M':
cmd = 'L';
break;
case 'l': case 'L':
case 'c': case 'C':
case 's': case 'S':
case 'q': case 'Q':
case 't': case 'T':
cmd = prevCmd;
break;
default:
handled = false;
break;
}
if (handled)
break;
}
default: {
ph.advance();
prevCmd = cmd;
@ -539,6 +552,44 @@ public class SVGParser {
lastY = y;
break;
}
case 'Q':
case 'q': {
wasCurve = true;
float x1 = ph.nextFloat();
float y1 = ph.nextFloat();
float x = ph.nextFloat();
float y = ph.nextFloat();
if (cmd == 'q') {
x1 += lastX;
x += lastX;
y1 += lastY;
y += lastY;
}
p.quadTo(x1, y1, x, y);
lastX1 = x1;
lastY1 = y1;
lastX = x;
lastY = y;
break;
}
case 'T':
case 't': {
wasCurve = true;
float x = ph.nextFloat();
float y = ph.nextFloat();
if (cmd == 't') {
x += lastX;
y += lastY;
}
float x1 = 2 * lastX - lastX1;
float y1 = 2 * lastY - lastY1;
p.quadTo(x1, y1, x, y);
lastX1 = x1;
lastY1 = y1;
lastX = x;
lastY = y;
break;
}
case 'A':
case 'a': {
float rx = ph.nextFloat();
@ -778,8 +829,7 @@ public class SVGParser {
RectF bounds = null;
RectF limits = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
Integer searchColor = null;
Integer replaceColor = null;
Map<Integer,Integer> colorReplace = null;
boolean whiteMode = false;
@ -793,9 +843,8 @@ public class SVGParser {
paint.setAntiAlias(true);
}
public void setColorSwap(Integer searchColor, Integer replaceColor) {
this.searchColor = searchColor;
this.replaceColor = replaceColor;
public void setColorSwap(Map<Integer,Integer> colorReplace) {
this.colorReplace = colorReplace;
}
public void setWhiteMode(boolean whiteMode) {
@ -934,19 +983,27 @@ public class SVGParser {
private void doColor(Properties atts, Integer color, boolean fillMode) {
int c = (0xFFFFFF & color) | 0xFF000000;
if (searchColor != null && searchColor.intValue() == c) {
c = replaceColor;
int opac = -1;
if (colorReplace != null) {
Integer replaceColor = colorReplace.get(c);
if (replaceColor != null) {
c = replaceColor;
opac = replaceColor >>> 24;
}
}
if (opac == -1) {
Float opacity = atts.getFloat("opacity", false);
if (opacity == null) {
opacity = atts.getFloat(fillMode ? "fill-opacity" : "stroke-opacity", false);
}
if (opacity == null) {
opac = 255;
} else {
opac = (int) (255 * opacity);
}
}
paint.setColor(c);
Float opacity = atts.getFloat("opacity", false);
if (opacity == null) {
opacity = atts.getFloat(fillMode ? "fill-opacity" : "stroke-opacity", false);
}
if (opacity == null) {
paint.setAlpha(255);
} else {
paint.setAlpha((int) (255 * opacity));
}
paint.setAlpha(opac);
}
private boolean hidden = false;

View File

@ -1370,6 +1370,7 @@ public class DroidFish extends Activity
pgnOptions.exp.clockInfo = settings.getBoolean("exportTime", false);
ColorTheme.instance().readColors(settings);
PieceSet.instance().readPrefs(settings);
cb.setColors();
overrideViewAttribs();
@ -2867,6 +2868,7 @@ public class DroidFish extends Activity
builder.setSingleChoiceItems(themeNames, -1, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
ColorTheme.instance().setTheme(settings, item);
PieceSet.instance().readPrefs(settings);
cb.setColors();
gameTextListener.clear();
ctrl.prefsChanged(false);

View File

@ -0,0 +1,136 @@
package org.petero.droidfish;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import com.larvalabs.svgandroid.SVG;
import com.larvalabs.svgandroid.SVGParser;
import org.petero.droidfish.gamelogic.Piece;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/** Handle rendering of chess pieces. */
public class PieceSet {
private static PieceSet inst = null;
private HashMap<String,Integer> nameToPieceType;
private SVG[] svgTable = new SVG[Piece.nPieceTypes];
private Bitmap[] bitmapTable = new Bitmap[Piece.nPieceTypes];
private int cachedSquareSize = -1;
private int cachedWhiteColor = 0xffffffff;
private int cachedBlackColor = 0xff000000;
/** Get singleton instance. */
public static PieceSet instance() {
if (inst == null)
inst = new PieceSet();
return inst;
}
private PieceSet() {
nameToPieceType = new HashMap<>();
nameToPieceType.put("wk.svg", Piece.WKING);
nameToPieceType.put("wq.svg", Piece.WQUEEN);
nameToPieceType.put("wr.svg", Piece.WROOK);
nameToPieceType.put("wb.svg", Piece.WBISHOP);
nameToPieceType.put("wn.svg", Piece.WKNIGHT);
nameToPieceType.put("wp.svg", Piece.WPAWN);
nameToPieceType.put("bk.svg", Piece.BKING);
nameToPieceType.put("bq.svg", Piece.BQUEEN);
nameToPieceType.put("br.svg", Piece.BROOK);
nameToPieceType.put("bb.svg", Piece.BBISHOP);
nameToPieceType.put("bn.svg", Piece.BKNIGHT);
nameToPieceType.put("bp.svg", Piece.BPAWN);
parseSvgData(cachedWhiteColor, cachedBlackColor);
}
/** Re-parse SVG data if piece properties have changed. */
final void readPrefs(SharedPreferences settings) {
ColorTheme ct = ColorTheme.instance();
int whiteColor = ct.getColor(ColorTheme.BRIGHT_PIECE);
int blackColor = ct.getColor(ColorTheme.DARK_PIECE);
if (whiteColor != cachedWhiteColor || blackColor != cachedBlackColor) {
recycleBitmaps();
parseSvgData(whiteColor, blackColor);
cachedWhiteColor = whiteColor;
cachedBlackColor = blackColor;
cachedSquareSize = -1;
}
}
/** Return a bitmap for the specified piece type and square size. */
public Bitmap getPieceBitmap(int pType, int sqSize) {
if (sqSize != cachedSquareSize) {
recycleBitmaps();
createBitmaps(sqSize);
cachedSquareSize = sqSize;
}
return bitmapTable[pType];
}
private void parseSvgData(int whiteColor, int blackColor) {
HashMap<Integer,Integer> colorReplace = new HashMap<>();
colorReplace.put(0xffffffff, whiteColor);
colorReplace.put(0xff000000, blackColor);
try {
ZipInputStream zis = getZipStream();
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
String name = entry.getName();
Integer pType = nameToPieceType.get(name);
if (pType != null) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = zis.read(buf)) != -1)
bos.write(buf, 0, len);
buf = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(buf);
svgTable[pType] = SVGParser.getSVGFromInputStream(bis, colorReplace);
}
}
zis.closeEntry();
}
zis.close();
} catch (IOException ex) {
throw new RuntimeException("Cannot read chess pieces data", ex);
}
}
private ZipInputStream getZipStream() throws IOException {
InputStream is = DroidFishApp.getContext().getAssets().open("pieces/chesscases.zip");
return new ZipInputStream(is);
}
private void recycleBitmaps() {
for (int i = 0; i < Piece.nPieceTypes; i++) {
if (bitmapTable[i] != null) {
bitmapTable[i].recycle();
bitmapTable[i] = null;
}
}
}
private void createBitmaps(int sqSize) {
for (int i = 0; i < Piece.nPieceTypes; i++) {
SVG svg = svgTable[i];
if (svg != null) {
Bitmap bm = Bitmap.createBitmap(sqSize, sqSize, Bitmap.Config.ARGB_8888);
Canvas bmCanvas = new Canvas(bm);
bmCanvas.drawPicture(svg.getPicture(), new Rect(0, 0, sqSize, sqSize));
bitmapTable[i] = bm;
}
}
}
}

View File

@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.List;
import org.petero.droidfish.ColorTheme;
import org.petero.droidfish.PieceSet;
import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.Piece;
import org.petero.droidfish.gamelogic.Position;
@ -30,12 +31,12 @@ import org.petero.droidfish.gamelogic.UndoInfo;
import org.petero.droidfish.tb.ProbeResult;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
@ -51,7 +52,6 @@ public abstract class ChessBoard extends View {
public boolean cursorVisible;
protected int x0, y0;
public int sqSize;
int pieceXDelta, pieceYDelta; // top/left pixel draw position relative to square
public boolean flipped;
public boolean drawSquareLabels;
public boolean toggleSelection;
@ -79,8 +79,7 @@ public abstract class ChessBoard extends View {
protected Paint brightPaint;
private Paint selectedSquarePaint;
private Paint cursorSquarePaint;
private Paint whitePiecePaint;
private Paint blackPiecePaint;
private Paint piecePaint;
private Paint labelPaint;
private Paint decorationPaint;
private ArrayList<Paint> moveMarkPaint;
@ -93,7 +92,6 @@ public abstract class ChessBoard extends View {
cursorX = cursorY = 0;
cursorVisible = false;
x0 = y0 = sqSize = 0;
pieceXDelta = pieceYDelta = -1;
flipped = false;
drawSquareLabels = false;
toggleSelection = false;
@ -111,11 +109,8 @@ public abstract class ChessBoard extends View {
cursorSquarePaint.setStyle(Paint.Style.STROKE);
cursorSquarePaint.setAntiAlias(true);
whitePiecePaint = new Paint();
whitePiecePaint.setAntiAlias(true);
blackPiecePaint = new Paint();
blackPiecePaint.setAntiAlias(true);
piecePaint = new Paint();
piecePaint.setAntiAlias(true);
labelPaint = new Paint();
labelPaint.setAntiAlias(true);
@ -134,10 +129,6 @@ public abstract class ChessBoard extends View {
if (isInEditMode())
return;
Typeface chessFont = Typeface.createFromAsset(getContext().getAssets(), "fonts/ChessCases.ttf");
whitePiecePaint.setTypeface(chessFont);
blackPiecePaint.setTypeface(chessFont);
setColors();
}
@ -148,8 +139,6 @@ public abstract class ChessBoard extends View {
brightPaint.setColor(ct.getColor(ColorTheme.BRIGHT_SQUARE));
selectedSquarePaint.setColor(ct.getColor(ColorTheme.SELECTED_SQUARE));
cursorSquarePaint.setColor(ct.getColor(ColorTheme.CURSOR_SQUARE));
whitePiecePaint.setColor(ct.getColor(ColorTheme.BRIGHT_PIECE));
blackPiecePaint.setColor(ct.getColor(ColorTheme.DARK_PIECE));
labelPaint.setColor(ct.getColor(ColorTheme.SQUARE_LABEL));
decorationPaint.setColor(ct.getColor(ColorTheme.DECORATION));
for (int i = 0; i < ColorTheme.MAX_ARROWS; i++)
@ -369,7 +358,6 @@ public abstract class ChessBoard extends View {
int sqSizeW = getSqSizeW(width);
int sqSizeH = getSqSizeH(height);
int sqSize = Math.min(sqSizeW, sqSizeH);
pieceXDelta = pieceYDelta = -1;
labelBounds = null;
if (height > width) {
int p = getMaxHeightPercentage();
@ -394,8 +382,6 @@ public abstract class ChessBoard extends View {
final int width = getWidth();
final int height = getHeight();
sqSize = Math.min(getSqSizeW(width), getSqSizeH(height));
blackPiecePaint.setTextSize(sqSize);
whitePiecePaint.setTextSize(sqSize);
labelPaint.setTextSize(sqSize/4.0f);
decorationPaint.setTextSize(sqSize/3.0f);
computeOrigin(width, height);
@ -507,41 +493,15 @@ public abstract class ChessBoard extends View {
protected final void drawPiece(Canvas canvas, int xCrd, int yCrd, int p) {
if (blindMode)
return;
String psb, psw;
boolean rotate = false;
switch (p) {
default:
case Piece.EMPTY: psb = null; psw = null; break;
case Piece.WKING: psb = "H"; psw = "k"; break;
case Piece.WQUEEN: psb = "I"; psw = "l"; break;
case Piece.WROOK: psb = "J"; psw = "m"; break;
case Piece.WBISHOP: psb = "K"; psw = "n"; break;
case Piece.WKNIGHT: psb = "L"; psw = "o"; break;
case Piece.WPAWN: psb = "M"; psw = "p"; break;
case Piece.BKING: psb = "N"; psw = "q"; rotate = true; break;
case Piece.BQUEEN: psb = "O"; psw = "r"; rotate = true; break;
case Piece.BROOK: psb = "P"; psw = "s"; rotate = true; break;
case Piece.BBISHOP: psb = "Q"; psw = "t"; rotate = true; break;
case Piece.BKNIGHT: psb = "R"; psw = "u"; rotate = true; break;
case Piece.BPAWN: psb = "S"; psw = "v"; rotate = true; break;
}
if (psb != null) {
if (pieceXDelta < 0) {
Rect bounds = new Rect();
blackPiecePaint.getTextBounds("H", 0, 1, bounds);
pieceXDelta = (sqSize - (bounds.left + bounds.right)) / 2;
pieceYDelta = (sqSize - (bounds.top + bounds.bottom)) / 2;
}
rotate ^= flipped;
rotate = false; // Disabled for now
Bitmap bm = PieceSet.instance().getPieceBitmap(p, sqSize);
if (bm != null) {
boolean rotate = flipped & false; // Disabled for now
if (rotate) {
canvas.save();
canvas.rotate(180, xCrd + sqSize * 0.5f, yCrd + sqSize * 0.5f);
}
xCrd += pieceXDelta;
yCrd += pieceYDelta;
canvas.drawText(psw, xCrd, yCrd, whitePiecePaint);
canvas.drawText(psb, xCrd, yCrd, blackPiecePaint);
canvas.drawBitmap(bm, xCrd, yCrd, piecePaint);
if (rotate)
canvas.restore();
}