/** * Copyright 2008 - 2015 The Loon Game Engine Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * @project loon * @author cping * @email:javachenpeng@yahoo.com * @version 0.5 */ package loon.font; import java.util.Iterator; import java.util.StringTokenizer; import loon.BaseIO; import loon.LSystem; import loon.LTexture; import loon.LTextureBatch.Cache; import loon.canvas.LColor; import loon.geom.Affine2f; import loon.geom.PointI; import loon.opengl.GLEx; import loon.utils.IntMap; import loon.utils.MathUtils; import loon.utils.ObjectMap; import loon.utils.StringUtils; import loon.utils.ObjectMap.Entries; import loon.utils.ObjectMap.Entry; import loon.utils.TArray; // AngelCode图像字体专用类(因为仅处理限定范围内的字体,此类速度会比较早前版本中提供的文字渲染类更快, // 但缺点在于,没有提供图像的文字不能被渲染). public class BMFont implements IFont { private final static String DEF_BMF_ONT = "deffont"; private static BMFont _font; /* * 获得一个默认的BMFont. * * 比如: * * 游戏全局使用默认BMFont(除log字体外,log字体需要设置setSystemLogFont) * * LSystem.setSystemGameFont(BMFont.getDefaultFont()); * */ public static BMFont getDefaultFont() { if (_font == null) { try { _font = new BMFont(LSystem.FRAMEWORK_IMG_NAME + DEF_BMF_ONT + ".txt", LSystem.FRAMEWORK_IMG_NAME + DEF_BMF_ONT + ".png"); } catch (Exception e) { LSystem.error("BMFont error!", e); } } return _font; } public static void setDefaultFont(BMFont font) { _font = font; } private static final int DEFAULT_MAX_CHAR = 256; private int totalCharSet = DEFAULT_MAX_CHAR; private int _initDraw = -1; private String _texPath = null; private String _imagePath = null; private boolean _initParse = false; private IntMap<CharDef> customChars = new IntMap<CharDef>(); private PointI _offset = new PointI(); private int _size = -1; private float _ascent = -1; private float fontScaleX = 1f, fontScaleY = 1f; private ObjectMap<String, Display> displays; private int lazyHashCode = 1; private LTexture displayList; private CharDef[] charArray; private int lineHeight, halfHeight; private boolean _isClose; private String info, common, page; private class Display { String text; Cache cache; int width; int height; } private class CharDef { int id; short tx; short ty; short width; short height; short xoffset; short yoffset; short advance; short[] kerning; public void draw(float x, float y, LColor c) { if (_isClose) { return; } displayList.draw((x + xoffset) * fontScaleX, (y + yoffset) * fontScaleY, width * fontScaleX, height * fontScaleY, tx, ty, tx + width, ty + height, c); } public void draw(GLEx g, float sx, float sy, float x, float y, LColor c) { if (_isClose) { return; } g.draw(displayList, sx + (x + xoffset) * fontScaleX, sy + (y + yoffset) * fontScaleX, width * fontScaleX, height * fontScaleY, tx, ty, width, height, c); } public int getKerning(int point) { if (kerning == null) { return 0; } int low = 0; int high = kerning.length - 1; while (low <= high) { int midIndex = (low + high) >>> 1; int value = kerning[midIndex]; int foundCodePoint = value & 0xff; if (foundCodePoint < point) { low = midIndex + 1; } else if (foundCodePoint > point) { high = midIndex - 1; } else { return value >> 8; } } return 0; } } public BMFont(String file, LTexture image) throws Exception { this._imagePath = image.getSource(); this._texPath = file; this.displayList = image; } public BMFont(String file, String imgFile) throws Exception { this._texPath = file; this._imagePath = imgFile; } public BMFont(String file) throws Exception { this(file, LSystem.getAllFileName(file) + ".png"); } private void parse(String text) throws Exception { if (displays == null) { displays = new ObjectMap<String, Display>(DEFAULT_MAX_CHAR); } else { displays.clear(); } StringTokenizer br = new StringTokenizer(text, "\r\n"); info = br.nextToken(); common = br.nextToken(); page = br.nextToken(); if (info != null && !StringUtils.isEmpty(info)) { int size = info.length(); StringBuilder sbr = new StringBuilder(); for (int i = 0; i < size; i++) { char ch = info.charAt(i); if (ch == ' ' && sbr.length() > 0) { String result = sbr.toString().toLowerCase().trim(); String[] list = StringUtils.split(result, '='); if (list.length == 2) { if (list[0].equals("size")) { _size = (int) Float.parseFloat(list[1]); break; } } sbr.delete(0, sbr.length()); } sbr.append(ch); } } ObjectMap<Short, TArray<Short>> kerning = new ObjectMap<Short, TArray<Short>>(64); TArray<CharDef> charDefs = new TArray<CharDef>(DEFAULT_MAX_CHAR); int maxChar = 0; boolean done = false; for (; !done;) { String line = br.hasMoreTokens() ? br.nextToken() : null; if (line == null) { done = true; } else { if (line.startsWith("chars c")) { } else if (line.startsWith("char")) { CharDef def = parseChar(line); if (def != null) { maxChar = MathUtils.max(maxChar, def.id); charDefs.add(def); } } if (line.startsWith("kernings c")) { } else if (line.startsWith("kerning")) { StringTokenizer tokens = new StringTokenizer(line, " ="); tokens.nextToken(); tokens.nextToken(); short first = Short.parseShort(tokens.nextToken()); tokens.nextToken(); int second = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); int offset = Integer.parseInt(tokens.nextToken()); TArray<Short> values = kerning.get(new Short(first)); if (values == null) { values = new TArray<Short>(); kerning.put(new Short(first), values); } values.add(new Short((short) ((offset << 8) | second))); } } } this.charArray = new CharDef[totalCharSet]; for (Iterator<CharDef> iter = charDefs.iterator(); iter.hasNext();) { CharDef def = iter.next(); if (def.id < totalCharSet) { charArray[def.id] = def; } else { customChars.put(def.id, def); } } for (Entries<Short, TArray<Short>> iter = kerning.entries(); iter.hasNext();) { Entry<Short, TArray<Short>> entry = iter.next(); short first = entry.key; TArray<Short> valueList = entry.value; short[] valueArray = new short[valueList.size]; int i = 0; for (Iterator<Short> valueIter = valueList.iterator(); valueIter.hasNext(); i++) { valueArray[i] = (valueIter.next()).shortValue(); } if (first < totalCharSet) { charArray[first].kerning = valueArray; } else { customChars.get((int) first).kerning = valueArray; } } } private CharDef parseChar(final String line) throws Exception { CharDef def = new CharDef(); StringTokenizer tokens = new StringTokenizer(line, " ="); tokens.nextToken(); tokens.nextToken(); def.id = Integer.parseInt(tokens.nextToken()); if (def.id < 0) { return null; } tokens.nextToken(); def.tx = Short.parseShort(tokens.nextToken()); tokens.nextToken(); def.ty = Short.parseShort(tokens.nextToken()); tokens.nextToken(); def.width = Short.parseShort(tokens.nextToken()); tokens.nextToken(); def.height = Short.parseShort(tokens.nextToken()); tokens.nextToken(); def.xoffset = Short.parseShort(tokens.nextToken()); tokens.nextToken(); def.yoffset = Short.parseShort(tokens.nextToken()); tokens.nextToken(); def.advance = Short.parseShort(tokens.nextToken()); if (def.id != (short) ' ') { lineHeight = MathUtils.max(def.height + def.yoffset, lineHeight); halfHeight = lineHeight >> 1; } return def; } public void drawString(String text, float x, float y) { drawString(text, x, y, null); } public void drawString(String text, float x, float y, LColor col) { drawBatchString(text, x, y, col, 0, text.length() - 1); } private void drawBatchString(String text, float tx, float ty, LColor c, int startIndex, int endIndex) { if (_isClose) { return; } if (StringUtils.isEmpty(text)) { return; } make(); if (displayList == null || displayList.isClose()) { this.displayList = BaseIO.loadTexture(_imagePath); return; } if (_initDraw < 1) { _initDraw++; return; } if (displays.size > DEFAULT_MAX_CHAR) { displays.clear(); } lazyHashCode = 1; if (c != null) { lazyHashCode = LSystem.unite(lazyHashCode, c.r); lazyHashCode = LSystem.unite(lazyHashCode, c.g); lazyHashCode = LSystem.unite(lazyHashCode, c.b); lazyHashCode = LSystem.unite(lazyHashCode, c.a); } String key = text + lazyHashCode; Display display = displays.get(key); if (display == null) { int x = 0, y = 0; displayList.glBegin(); displayList.setBatchPos(tx + _offset.x, ty + _offset.y); if (c != null) { displayList.setImageColor(c); } CharDef lastCharDef = null; for (int i = 0, size = text.length(); i < size; i++) { int id = text.charAt(i); if (id == '\n') { x = 0; y += lineHeight; continue; } CharDef charDef = null; if (id < totalCharSet) { charDef = charArray[id]; } else { charDef = customChars.get(id); } if (charDef == null) { continue; } if (lastCharDef != null) { x += lastCharDef.getKerning(id); } lastCharDef = charDef; if ((i >= startIndex) && (i <= endIndex)) { charDef.draw(x, y, c); } x += charDef.advance; } if (c != null) { displayList.setImageColor(LColor.white); } displayList.glEnd(); display = new Display(); display.cache = displayList.newBatchCache(); display.text = text; display.width = 0; display.height = 0; displays.put(key, display); } else if (display.cache != null) { display.cache.x = tx + _offset.x; display.cache.y = ty + _offset.y; displayList.postCache(display.cache); } } @Override public void drawString(GLEx g, String text, float x, float y) { drawString(g, text, x, y, null); } @Override public void drawString(GLEx g, String text, float x, float y, LColor col) { drawString(g, text, x, y, col, 0, text.length() - 1); } private void make() { if (!_initParse) { try { this.parse(BaseIO.loadText(_texPath)); } catch (Exception e) { LSystem.error("BMFont error!", e); } _initParse = true; } } private void drawString(GLEx g, String text, float tx, float ty, LColor c, int startIndex, int endIndex) { if (_isClose) { return; } if (StringUtils.isEmpty(text)) { return; } make(); if (displayList == null || displayList.isClose()) { this.displayList = BaseIO.loadTexture(_imagePath); return; } if (_initDraw < 1) { _initDraw++; return; } int x = 0, y = 0; CharDef lastCharDef = null; for (int i = 0, size = text.length(); i < size; i++) { int id = text.charAt(i); if (id == '\n') { x = 0; y += lineHeight; continue; } CharDef charDef = null; if (id < totalCharSet) { charDef = charArray[id]; } else { charDef = customChars.get(id); } if (charDef == null) { continue; } if (lastCharDef != null) { x += lastCharDef.getKerning(id); } lastCharDef = charDef; if ((i >= startIndex) && (i <= endIndex)) { charDef.draw(g, tx + _offset.x, ty + _offset.y, x, y, c); } x += charDef.advance; } } @Override public void drawString(GLEx g, String text, float x, float y, float rotation, LColor c) { if (_isClose) { return; } if (StringUtils.isEmpty(text)) { return; } if (rotation == 0) { drawString(g, text, x, y, c); return; } try { g.saveTx(); float centerX = x + stringWidth(text) / 2; float centerY = y + stringHeight(text) / 2; g.rotate(centerX, centerY, rotation); drawString(g, text, x, y, c); } finally { g.restoreTx(); } } @Override public void drawString(GLEx gl, String text, float x, float y, float sx, float sy, float ax, float ay, float rotation, LColor c) { if (_isClose) { return; } if (StringUtils.isEmpty(text)) { return; } boolean anchor = ax != 0 || ay != 0; boolean scale = sx != 1f || sy != 1f; boolean angle = rotation != 0; boolean update = scale || angle || anchor; try { if (update) { gl.saveTx(); Affine2f xf = gl.tx(); if (angle) { float centerX = x + this.stringWidth(text) / 2; float centerY = y + this.stringHeight(text) / 2; xf.translate(centerX, centerY); xf.preRotate(rotation); xf.translate(-centerX, -centerY); } if (scale) { float centerX = x + this.stringWidth(text) / 2; float centerY = y + this.stringHeight(text) / 2; xf.translate(centerX, centerY); xf.preScale(sx, sy); xf.translate(-centerX, -centerY); } if (anchor) { xf.translate(ax, ay); } } drawString(gl, text, x, y, c); } finally { if (update) { gl.restoreTx(); } } } @Override public int stringHeight(String text) { if (StringUtils.isEmpty(text)) { return 0; } make(); Display display = null; for (Display d : displays.values()) { if (d != null && text.equals(d.text)) { display = d; break; } } if (display != null && display.height != 0) { return display.height; } if (display == null) { display = new Display(); } int lines = 0; for (int i = 0; i < text.length(); i++) { int id = text.charAt(i); if (id == '\n') { lines++; display.height = 0; continue; } if (id == ' ') { continue; } CharDef charDef = null; if (id < totalCharSet) { charDef = charArray[id]; } else { charDef = customChars.get(id); } if (charDef == null) { continue; } display.height = MathUtils.max(charDef.height + charDef.yoffset, display.height); } display.height += lines * lineHeight; return (int) (display.height * fontScaleY); } @Override public int charWidth(char c) { if (c == '\n') { return 0; } make(); CharDef charDef = null; if (c < totalCharSet) { charDef = charArray[(int) c]; } else { charDef = customChars.get((int) c); } if (charDef == null) { return getSize(); } return charDef.width; } @Override public int stringWidth(String text) { if (StringUtils.isEmpty(text)) { return 0; } make(); Display display = null; for (Display d : displays.values()) { if (d != null && text.equals(d.text)) { display = d; break; } } if (display != null && display.width != 0) { return display.width; } if (display == null) { display = new Display(); } int width = 0; CharDef lastCharDef = null; for (int i = 0, n = text.length(); i < n; i++) { int id = text.charAt(i); if (id == '\n') { width = 0; continue; } CharDef charDef = null; if (id < totalCharSet) { charDef = charArray[id]; } else { charDef = customChars.get(id); } if (charDef == null) { continue; } if (lastCharDef != null) { width += lastCharDef.getKerning(id); } lastCharDef = charDef; if (i < n - 1) { width += charDef.advance; } else { width += charDef.width; } display.width = MathUtils.max(display.width, width); } return (int) (display.width * fontScaleX); } public String getCommon() { return common; } public String getInfo() { return info; } public String getPage() { return page; } @Override public int getHeight() { make(); return (int) (lineHeight * fontScaleY) - halfHeight; } public float getFontScaleX() { return this.fontScaleX; } public float getFontScaleY() { return this.fontScaleX; } public void setFontScale(float s) { this.setFontScale(s, s); } public void setFontScale(float sx, float sy) { this.fontScaleX = sx; this.fontScaleY = sy; } public void setFontScaleX(float x) { this.fontScaleX = x; } public void setFontScaleY(float y) { this.fontScaleY = y; } @Override public float getAscent() { make(); return _ascent == -1 ? (int) (lineHeight * this.fontScaleY) - halfHeight / 3 : _ascent; } @Override public int getSize() { make(); return _size == -1 ? (int) (lineHeight * this.fontScaleY) - halfHeight / 4 : _size; } @Override public String confineLength(String s, int width) { int length = 0; for (int i = 0; i < s.length(); i++) { length += stringWidth(String.valueOf(s.charAt(i))); if (length >= width) { int pLength = stringWidth("..."); while (length + pLength >= width && i >= 0) { length -= stringWidth(String.valueOf(s.charAt(i))); i--; } s = s.substring(0, ++i) + "..."; break; } } return s; } @Override public PointI getOffset() { return _offset; } @Override public void setOffset(PointI val) { _offset.set(val); } @Override public void setOffsetX(int x) { _offset.x = x; } @Override public void setOffsetY(int y) { _offset.y = y; } @Override public void setAssent(float assent) { this._ascent = assent; } @Override public void setSize(int size) { this._size = size; } public boolean isClosed() { return _isClose; } @Override public void close() { if (_isClose) { return; } this._isClose = true; if (displayList != null) { displayList.close(true); displayList = null; } if (displays != null) { for (Display d : displays.values()) { if (d != null && d.cache != null) { d.cache.close(); } } displays.clear(); } _initDraw = -1; _initParse = false; } }