/******************************************************************************* * Copyright 2011 See AUTHORS file. * * 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. ******************************************************************************/ package com.badlogic.gdx.graphics.g2d; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData; import com.badlogic.gdx.graphics.g2d.BitmapFont.Glyph; import com.badlogic.gdx.graphics.g2d.BitmapFont.HAlignment; import com.badlogic.gdx.graphics.g2d.BitmapFont.TextBounds; import com.badlogic.gdx.utils.NumberUtils; /** * Caches glyph geometry for a BitmapFont, providing a fast way to render static text. This saves needing to compute the * location of each glyph each frame. * * @author Nathan Sweet * @author Matthias Mann */ public class BitmapFontCache { private final BitmapFont font; private float[] vertices = new float[0]; private int idx; private float x, y; private float color = Color.WHITE.toFloatBits(); private final Color tempColor = new Color(Color.WHITE); private final TextBounds textBounds = new TextBounds(); private boolean integer = true; public BitmapFontCache(BitmapFont font) { this(font, font.usesIntegerPositions()); } /** * Creates a new BitmapFontCache * * @param font * the font to use * @param integer * whether to use integer positions and sizes. */ public BitmapFontCache(BitmapFont font, boolean integer) { this.font = font; this.integer = integer; } /** * Sets the position of the text, relative to the position when the cached text was created. * * @param x * The x coordinate * @param y * The y coodinate */ public void setPosition(float x, float y) { translate(x - this.x, y - this.y); } /** * Sets the position of the text, relative to its current position. * * @param xAmount * The amount in x to move the text * @param yAmount * The amount in y to move the text */ public void translate(float xAmount, float yAmount) { if (xAmount == 0 && yAmount == 0) return; if (integer) { xAmount = Math.round(xAmount); yAmount = Math.round(yAmount); } x += xAmount; y += yAmount; float[] vertices = this.vertices; for (int i = 0, n = idx; i < n; i += 5) { vertices[i] += xAmount; vertices[i + 1] += yAmount; } } public void setColor(float color) { if (color == this.color) return; this.color = color; float[] vertices = this.vertices; for (int i = 2, n = idx; i < n; i += 5) vertices[i] = color; } public void setColor(Color tint) { final float color = tint.toFloatBits(); if (color == this.color) return; this.color = color; float[] vertices = this.vertices; for (int i = 2, n = idx; i < n; i += 5) vertices[i] = color; } public void setColor(float r, float g, float b, float a) { int intBits = ((int) (255 * a) << 24) | ((int) (255 * b) << 16) | ((int) (255 * g) << 8) | ((int) (255 * r)); float color = NumberUtils.intToFloatColor(intBits); if (color == this.color) return; this.color = color; float[] vertices = this.vertices; for (int i = 2, n = idx; i < n; i += 5) vertices[i] = color; } /** * Sets the color of the specified characters. This may only be called after * {@link #setText(CharSequence, float, float)} and is reset every time setText is called. */ public void setColor(Color tint, int start, int end) { final float color = tint.toFloatBits(); float[] vertices = this.vertices; for (int i = start * 20 + 2, n = end * 20; i < n; i += 5) vertices[i] = color; } public void draw(SpriteBatch spriteBatch) { spriteBatch.draw(font.getRegion().getTexture(), vertices, 0, idx); } public void draw(SpriteBatch spriteBatch, float alphaModulation) { if (alphaModulation == 1) { draw(spriteBatch); return; } Color color = getColor(); float oldAlpha = color.a; color.a *= alphaModulation; setColor(color); draw(spriteBatch); color.a = oldAlpha; setColor(color); } public Color getColor() { float floatBits = color; int intBits = NumberUtils.floatToIntColor(color); Color color = tempColor; color.r = (intBits & 0xff) / 255f; color.g = ((intBits >>> 8) & 0xff) / 255f; color.b = ((intBits >>> 16) & 0xff) / 255f; color.a = ((intBits >>> 24) & 0xff) / 255f; return color; } /** Removes all glyphs in the cache. */ public void clear() { x = 0; y = 0; idx = 0; } private void require(int glyphCount) { int vertexCount = idx + glyphCount * 20; if (vertices == null || vertices.length < vertexCount) { float[] newVertices = new float[vertexCount]; System.arraycopy(vertices, 0, newVertices, 0, idx); vertices = newVertices; } } private float addToCache(CharSequence str, float x, float y, int start, int end) { float startX = x; BitmapFont font = this.font; Glyph lastGlyph = null; BitmapFontData data = font.data; if (data.scaleX == 1 && data.scaleY == 1) { while (start < end) { lastGlyph = data.getGlyph(str.charAt(start++)); if (lastGlyph != null) { addGlyph(lastGlyph, x + lastGlyph.xoffset, y + lastGlyph.yoffset, lastGlyph.width, lastGlyph.height); x += lastGlyph.xadvance; break; } } while (start < end) { char ch = str.charAt(start++); Glyph g = data.getGlyph(ch); if (g != null) { x += lastGlyph.getKerning(ch); lastGlyph = g; addGlyph(lastGlyph, x + g.xoffset, y + g.yoffset, g.width, g.height); x += g.xadvance; } } } else { float scaleX = data.scaleX, scaleY = data.scaleY; while (start < end) { lastGlyph = data.getGlyph(str.charAt(start++)); if (lastGlyph != null) { addGlyph(lastGlyph, // x + lastGlyph.xoffset * scaleX, // y + lastGlyph.yoffset * scaleY, // lastGlyph.width * scaleX, // lastGlyph.height * scaleY); x += lastGlyph.xadvance * scaleX; break; } } while (start < end) { char ch = str.charAt(start++); Glyph g = data.getGlyph(ch); if (g != null) { x += lastGlyph.getKerning(ch) * scaleX; lastGlyph = g; addGlyph(lastGlyph, // x + g.xoffset * scaleX, // y + g.yoffset * scaleY, // g.width * scaleX, // g.height * scaleY); x += g.xadvance * scaleX; } } } return x - startX; } private void addGlyph(Glyph glyph, float x, float y, float width, float height) { float x2 = x + width; float y2 = y + height; final float u = glyph.u; final float u2 = glyph.u2; final float v = glyph.v; final float v2 = glyph.v2; final float[] vertices = this.vertices; if (integer) { x = Math.round(x); y = Math.round(y); x2 = Math.round(x2); y2 = Math.round(y2); } int idx = this.idx; this.idx += 20; vertices[idx++] = x; vertices[idx++] = y; vertices[idx++] = color; vertices[idx++] = u; vertices[idx++] = v; vertices[idx++] = x; vertices[idx++] = y2; vertices[idx++] = color; vertices[idx++] = u; vertices[idx++] = v2; vertices[idx++] = x2; vertices[idx++] = y2; vertices[idx++] = color; vertices[idx++] = u2; vertices[idx++] = v2; vertices[idx++] = x2; vertices[idx++] = y; vertices[idx++] = color; vertices[idx++] = u2; vertices[idx] = v; } /** * Clears any cached glyphs and adds glyphs for the specified text. * * @see #addText(CharSequence, float, float, int, int) */ public TextBounds setText(CharSequence str, float x, float y) { clear(); return addText(str, x, y, 0, str.length()); } /** * Clears any cached glyphs and adds glyphs for the specified text. * * @see #addText(CharSequence, float, float, int, int) */ public TextBounds setText(CharSequence str, float x, float y, int start, int end) { clear(); return addText(str, x, y, start, end); } /** * Adds glyphs for the specified text. * * @see #addText(CharSequence, float, float, int, int) */ public TextBounds addText(CharSequence str, float x, float y) { return addText(str, x, y, 0, str.length()); } /** * Adds glyphs for the the specified text. * * @param x * The x position for the left most character. * @param y * The y position for the top of most capital letters in the font (the {@link BitmapFont#getCapHeight() * cap height}). * @param start * The first character of the string to draw. * @param end * The last character of the string to draw (exclusive). * @return The bounds of the cached string (the height is the distance from y to the baseline). */ public TextBounds addText(CharSequence str, float x, float y, int start, int end) { require(end - start); y += font.data.ascent; textBounds.width = addToCache(str, x, y, start, end); textBounds.height = font.data.capHeight; return textBounds; } /** * Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n). * * @see #addMultiLineText(CharSequence, float, float, float, HAlignment) */ public TextBounds setMultiLineText(CharSequence str, float x, float y) { clear(); return addMultiLineText(str, x, y, 0, HAlignment.LEFT); } /** * Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n). * * @see #addMultiLineText(CharSequence, float, float, float, HAlignment) */ public TextBounds setMultiLineText(CharSequence str, float x, float y, float alignmentWidth, HAlignment alignment) { clear(); return addMultiLineText(str, x, y, alignmentWidth, alignment); } /** * Adds glyphs for the specified text, which may contain newlines (\n). * * @see #addMultiLineText(CharSequence, float, float, float, HAlignment) */ public TextBounds addMultiLineText(CharSequence str, float x, float y) { return addMultiLineText(str, x, y, 0, HAlignment.LEFT); } /** * Adds glyphs for the specified text, which may contain newlines (\n). Each line is aligned horizontally within a * rectangle of the specified width. * * @param x * The x position for the left most character. * @param y * The y position for the top of most capital letters in the font (the {@link BitmapFont#getCapHeight() * cap height}). * @param alignment * The horizontal alignment of wrapped line. * @return The bounds of the cached string (the height is the distance from y to the baseline of the last line). */ public TextBounds addMultiLineText(CharSequence str, float x, float y, float alignmentWidth, HAlignment alignment) { BitmapFont font = this.font; int length = str.length(); require(length); y += font.data.ascent; float down = font.data.down; float maxWidth = 0; float startY = y; int start = 0; int numLines = 0; while (start < length) { int lineEnd = BitmapFont.indexOf(str, '\n', start); float xOffset = 0; if (alignment != HAlignment.LEFT) { float lineWidth = font.getBounds(str, start, lineEnd).width; xOffset = alignmentWidth - lineWidth; if (alignment == HAlignment.CENTER) xOffset /= 2; } float lineWidth = addToCache(str, x + xOffset, y, start, lineEnd); maxWidth = Math.max(maxWidth, lineWidth); start = lineEnd + 1; y += down; numLines++; } textBounds.width = maxWidth; textBounds.height = font.data.capHeight + (numLines - 1) * font.data.lineHeight; return textBounds; } /** * Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n) and is * automatically wrapped within the specified width. * * @see #addWrappedText(CharSequence, float, float, float, HAlignment) */ public TextBounds setWrappedText(CharSequence str, float x, float y, float wrapWidth) { clear(); return addWrappedText(str, x, y, wrapWidth, HAlignment.LEFT); } /** * Clears any cached glyphs and adds glyphs for the specified text, which may contain newlines (\n) and is * automatically wrapped within the specified width. * * @see #addWrappedText(CharSequence, float, float, float, HAlignment) */ public TextBounds setWrappedText(CharSequence str, float x, float y, float wrapWidth, HAlignment alignment) { clear(); return addWrappedText(str, x, y, wrapWidth, alignment); } /** * Adds glyphs for the specified text, which may contain newlines (\n) and is automatically wrapped within the * specified width. * * @see #addWrappedText(CharSequence, float, float, float, HAlignment) */ public TextBounds addWrappedText(CharSequence str, float x, float y, float wrapWidth) { return addWrappedText(str, x, y, wrapWidth, HAlignment.LEFT); } /** * Adds glyphs for the specified text, which may contain newlines (\n) and is automatically wrapped within the * specified width. * * @param x * The x position for the left most character. * @param y * The y position for the top of most capital letters in the font (the {@link BitmapFont#getCapHeight() * cap height}). * @param alignment * The horizontal alignment of wrapped line. * @return The bounds of the cached string (the height is the distance from y to the baseline of the last line). */ public TextBounds addWrappedText(CharSequence str, float x, float y, float wrapWidth, HAlignment alignment) { BitmapFont font = this.font; int length = str.length(); require(length); y += font.data.ascent; float down = font.data.down; if (wrapWidth <= 0) wrapWidth = Integer.MAX_VALUE; float maxWidth = 0; int start = 0; int numLines = 0; while (start < length) { int newLine = BitmapFont.indexOf(str, '\n', start); // Eat whitespace at start of line. while (start < newLine) { if (!BitmapFont.isWhitespace(str.charAt(start))) break; start++; } int lineEnd = start + font.computeVisibleGlyphs(str, start, newLine, wrapWidth); int nextStart = lineEnd + 1; if (lineEnd < newLine) { // Find char to break on. while (lineEnd > start) { if (BitmapFont.isWhitespace(str.charAt(lineEnd))) break; lineEnd--; } if (lineEnd == start) { if (nextStart > start + 1) nextStart--; lineEnd = nextStart; // If no characters to break, show all. } else { nextStart = lineEnd; // Eat whitespace at end of line. while (lineEnd > start) { if (!BitmapFont.isWhitespace(str.charAt(lineEnd - 1))) break; lineEnd--; } } } if (lineEnd > start) { float xOffset = 0; if (alignment != HAlignment.LEFT) { float lineWidth = font.getBounds(str, start, lineEnd).width; xOffset = wrapWidth - lineWidth; if (alignment == HAlignment.CENTER) xOffset /= 2; } float lineWidth = addToCache(str, x + xOffset, y, start, lineEnd); maxWidth = Math.max(maxWidth, lineWidth); } start = nextStart; y += down; numLines++; } textBounds.width = maxWidth; textBounds.height = font.data.capHeight + (numLines - 1) * font.data.lineHeight; return textBounds; } /** * Returns the size of the cached string. The height is the distance from the top of most capital letters in the * font (the {@link BitmapFont#getCapHeight() cap height}) to the baseline of the last line of text. */ public TextBounds getBounds() { return textBounds; } /** Returns the x position of the cached string, relative to the position when the string was cached. */ public float getX() { return x; } /** Returns the y position of the cached string, relative to the position when the string was cached. */ public float getY() { return y; } public BitmapFont getFont() { return font; } /** * Specifies whether to use integer positions or not. Default is to use them so filtering doesn't kick in as badly. * * @param use */ public void setUseIntegerPositions(boolean use) { this.integer = use; } /** @return whether this font uses integer positions for drawing. */ public boolean usesIntegerPositions() { return integer; } public float[] getVertices() { return vertices; } }