Rajawali / Rajawali

Android OpenGL ES 2.0/3.0 Engine
https://rajawali.github.io/Rajawali/
Other
2.34k stars 701 forks source link

Font to Texture Support #693

Open ToxicBakery opened 11 years ago

ToxicBakery commented 11 years ago

Once Texture Atlas is implement we should move to implement font support. As font support is not exactly an task this I have gone ahead and jump started the development. Attached is a debug image showing my poorly aligned and placed texture printing. Once the texture atlas code is in place I can re-use it greatly improve the letter placements and thus generate a nice clean atlas.

My implementation currently is below. This will change heavily once the atlas implementation pushes to master at which time I will remove this placeholder.

screenshot_2013-05-11-00-01-52

public class FontAtlas {

    public enum CharacterType {
        //@formatter:off
        ALPHA_LOWER(97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108,
                109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
                121, 122), 
        ALPHA_UPPER(65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
                79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90), 
        NUMERIC(48, 49, 50, 51, 52, 53, 54, 55, 56, 57), 
        SYMBOLS(32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
                47, 58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 95, 96);
        //@formatter:on

        int[] chars;

        CharacterType(final int... chars) {
            this.chars = chars;
        }
    }

    protected final SparseArray<Character> charList;

    protected Paint mPaint;
    protected Bitmap mTexture;

    protected FontAtlas(Builder builder) {
        // Validate data
        if (builder.types.size() == 0
                && builder.mCustomCharacters.length() == 0)
            throw new FontAtlasException("No characters or types specified!");
        if (builder.mTypeface == null)
            throw new FontAtlasException("Typeface never set!");

        // Construct paint for determining character sizes
        mPaint = new Paint();
        mPaint.setColor(builder.mColor);
        mPaint.setTextSize(builder.mTextSize);
        mPaint.setTypeface(builder.mTypeface);

        // Determine the number of characters that need to be displayed
        int typeCount = builder.mCustomCharacters.length();
        for (int i = 0, j = builder.types.size(); i < j; i++)
            typeCount += builder.types.get(i).chars.length;

        // Calculate the size of each character
        charList = new SparseArray<Character>(typeCount);
        Character tempChar;
        int maxCharHeight = 0;
        int maxCharWidth = 0;
        for (int i = 0, j = builder.types.size(); i < j; i++) {
            int[] chars = builder.types.get(i).chars;
            for (int k = 0; k < chars.length; k++) {
                tempChar = new Character(chars[k], mPaint);
                maxCharWidth = maxCharWidth < tempChar.w ? tempChar.w
                        : maxCharWidth;
                maxCharHeight = maxCharHeight < tempChar.h ? tempChar.h
                        : maxCharHeight;
                charList.append(chars[k], tempChar);
            }
        }
        for (int i = 0, j = builder.mCustomCharacters.length(); i < j; i++) {
            tempChar = new Character(builder.mCustomCharacters.substring(i, i+1), mPaint);
            maxCharWidth = maxCharWidth < tempChar.w ? tempChar.w
                    : maxCharWidth;
            maxCharHeight = maxCharHeight < tempChar.h ? tempChar.h
                    : maxCharHeight;
            charList.append(9000 + i, tempChar);
        }

        // Determine the estimated minimum size, this is a little wasteful but safe
        int nextPOT = maxCharHeight * maxCharWidth * charList.size();
        nextPOT--;
        nextPOT = (nextPOT >> 1) | nextPOT;
        nextPOT = (nextPOT >> 2) | nextPOT;
        nextPOT = (nextPOT >> 4) | nextPOT;
        nextPOT = (nextPOT >> 8) | nextPOT;
        nextPOT = (nextPOT >> 16) | nextPOT;
        nextPOT++;
        nextPOT = (int) Math.sqrt(nextPOT);

        // Create the bitmap using the next power of two
        mTexture = Bitmap.createBitmap(nextPOT, nextPOT, Config.ARGB_8888);

        final Canvas c = new Canvas(mTexture);

        Character ch;
        int key;
        int lineWidth = 0;
        for (int i = 0, j = charList.size(); i < j; i++) {
            if (lineWidth > nextPOT) {
                lineWidth = 0;
                c.translate(0, (float) Math.ceil(mPaint.getTextSize()));
            }

            key = charList.keyAt(i);
            ch = charList.get(key);
            c.drawText(String.valueOf((char) key), lineWidth, ch.h, mPaint);
            lineWidth += ch.w;
        }
    }

    public Bitmap getTexture() {
        return mTexture;
    }

    /*
     * private static Rect rectSrc = new Rect(); private static Rect rectDst = new Rect();
     * 
     * public static void drawLetter(final FontAtlas atlas, final Canvas canvas, final String c) {
     * canvas.drawBitmap(atlas.mTexture, rectSrc, rectDst, atlas.mPaint); }
     */

    /**
     * Stores data about a character such as width and height. This class is not thread safe.
     */
    private final static class Character {
        private static final Rect rect = new Rect();

        public int w;
        public int h;
        public int x;
        public int y;

        public Character(int charID, Paint paint) {
            determineSize(String.valueOf((char) charID), paint);
        }

        public Character(String c, Paint paint) {
            determineSize(c, paint);
        }

        private final void determineSize(final String c, final Paint paint) {
            paint.getTextBounds(c, 0, 1, rect);

            w = rect.right - rect.left;
            h = rect.bottom - rect.top;
        }
    }

    public static class Builder {

        protected final List<CharacterType> types = new ArrayList<CharacterType>();

        protected int mColor = 0xFFFFFFFF;
        protected String mCustomCharacters = "";
        protected float mTextSize = 12;
        protected Typeface mTypeface;

        public FontAtlas build() {
            return new FontAtlas(this);
        }

        public Builder addCharacterType(final CharacterType type) {
            if (types.contains(type))
                throw new RuntimeException(
                        "You can only add a CharacterType once!");

            types.add(type);
            return this;
        }

        public Builder setColor(final int color) {
            mColor = color;
            return this;
        }

        public Builder setCustomCharacters(final String customCharacters) {
            mCustomCharacters = customCharacters;
            return this;
        }

        public Builder setTextSize(float textSize) {
            if (textSize <= 0)
                throw new FontAtlasException(
                        "Text size must be greater than 0!");

            mTextSize = textSize;
            return this;
        }

        public Builder setTypeface(final Typeface typeface) {
            if (typeface == null)
                throw new FontAtlasException("Typeface can not be null.");

            mTypeface = typeface;
            return this;
        }

    }

    public static class FontAtlasException extends RuntimeException {
        private static final long serialVersionUID = 5553942987103836543L;

        public FontAtlasException(final String ex) {
            super(ex);
        }
    }

}
Davhed commented 11 years ago

Oh very nice! I was hoping I wouldn't have to tackle this... so much appreciation here. :+1:

ToxicBakery commented 11 years ago

Yep, it seemed like it might be fun to do.

MasDennis commented 11 years ago

This is great. I'm glad you are picking this up!

Davhed commented 11 years ago

So the packing and retrieval code is working now if you want to play with it a bit.

620

Davhed commented 11 years ago

@ToxicBakery I will try and pick this up tonight/tomorrow. After this week I'm going to be really busy with the new job, so if I can't get it done before then I will be sure to communicate that.