Slick Forums

Discuss the Slick 2D Library
It is currently Sun May 19, 2013 7:56 am

All times are UTC




Post new topic Reply to topic  [ 5 posts ] 
Author Message
PostPosted: Sun Dec 14, 2008 11:11 pm 
Offline

Joined: Sun Jun 03, 2007 2:51 am
Posts: 80
This is a little class I hacked together for my game, and I think it might be useful enough to add to the slick library. It allows fonts to be scaled to any size with minimal effort and very low processing overhead.

It's heavily dependent on the AngelCodeFont written by Kev. It's essentially just a group of AngelFonts that are switched between seamlessly. To use this, you will need a bunch of fonts saved in heiro at different sizes, saved into a .jar. The contents of the Jar should look like this:

Code:
8.fnt
8.png
16.fnt
16.png
24.fnt
24.png
25.fnt
25.png


There are no constraints on which fonts you have in the directory; it will just scale the closest available font to the appropriate size if the desired height isn't there. Although, the more fonts you put in here, the cleaner the font will look when it renders. The overhead on initializing these things is negligible, so this really is a wonderful solution to the scaling font question.

I'm considering having the fonts load from some sort of archive file rather than a directory. If I set that up I'll post up a newer version here.

Code:
import org.newdawn.slick.Color;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.newdawn.slick.AngelCodeFont;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.lwjgl.opengl.GL11;

/**
* A font implementation heavily dependent on AngelCodeFont.  This is essentially
* a collection of AngelCodeFonts at different heights loaded from a Jar.
* It allows smooth and clean scaling of fonts to any height.
* The files should be named: "{8.fnt, 8.png, 9.fnt, 9.png, 10.fnt, 10.png...}".
* There is no need to include more than one AngelCodeFont in the jar.  It will
* Automatically use the closest-fitting font.
* @author JoshuaD
*/

public class DivineFont {
   
   //TODO: Add an option to render the font at a specific height

   ArrayList <SizedAngelCodeFont> fonts;
   
   int fontHeight;
   
   public DivineFont(String JarFileName, int fontHeight ) throws Exception {
      this(JarFileName, fontHeight, false);
   }
      
   public DivineFont(int fontHeight) {
      fonts = new ArrayList<SizedAngelCodeFont>(100);
      this.fontHeight = fontHeight;
   }
   
   public DivineFont(String jarFileName, int fontHeight, boolean caching ) throws Exception {
      
      this.fontHeight = fontHeight;
      
      JarFile jarfile = new JarFile(jarFileName);
      
      Enumeration<JarEntry> em = jarfile.entries();
      ArrayList<Integer> fontHeights = new ArrayList<Integer>(100);
      for (Enumeration<JarEntry> em1 = jarfile.entries(); em1.hasMoreElements(); ) {
         String fileName = em1.nextElement().toString();
         if( fileName.endsWith(".fnt") ) {
            fontHeights.add( Integer.parseInt(fileName.split("\\.")[0] ) );
         }
      }

      fonts = new ArrayList<SizedAngelCodeFont>(100);

      SizedAngelCodeFont buffer;
      int number;

      for(int k = 0, n = fontHeights.size(); k < n; k++ ) {
         number = fontHeights.get(k);
         
         InputStream fntFileStream = jarfile.getInputStream(jarfile.getEntry(number + ".fnt"));
         InputStream pngFileStream = jarfile.getInputStream(jarfile.getEntry(number + ".png"));
         
         buffer = new SizedAngelCodeFont( new AngelCodeFont(number + ".png", fntFileStream, pngFileStream ), number );

         fonts.add(buffer);
         
      }
      
      Collections.sort(fonts);
      
   }

   
   private static void copyFile(DataInputStream in, DataOutputStream out, int length) throws Exception {
      byte[] data = new byte[length];
      int bytesRead = 0;
      bytesRead = in.read(data);
      out.write(data);
      if( bytesRead != data.length ) System.out.println("We wrote less than we expected");
   }
         
      
   
   public void setHeight( int size ) {
      this.fontHeight = size;
   }
   
   public void addFont(AngelCodeFont font, int height) {
      SizedAngelCodeFont insertMe = new SizedAngelCodeFont( font, height);
      
      int fontCount = fonts.size();
      
      if( fontCount <= 0 ) {
         fonts.add(insertMe);
         return;
      }
      
      for( int k = 0; k < fontCount; k++ ) {
         if( fonts.get(k).height >= height ) {
            fonts.add( k,  insertMe );
            return;
         }
      }
   }
   
   public void addFont( String fntFile, String pngFile, int height ) throws SlickException {
      addFont( new AngelCodeFont(fntFile, pngFile), height );
   }
   
   public void drawString(Graphics g, String string, int size, float x, float y, Color col ) {
      drawString(g, string, size/((float)fontHeight), x, y, col);
   }
   
   public void drawString(Graphics g, String string, float scale, float x, float y, Color col ) {
      int size = (int)(fontHeight * scale);
      
      SizedAngelCodeFont closestMatch = getClosestMatch(size);

      float fontScale = size / (float)closestMatch.height;

      GL11.glPushMatrix();
      GL11.glScalef(fontScale, fontScale, 0);

      closestMatch.font.drawString( x / fontScale, y /fontScale, string, col );

      GL11.glPopMatrix();


   }
   
   public void drawString(Graphics g, String string, float scale, float x, float y ) {
      drawString(g, string, scale, x, y, g.getColor() );
   }   
   
   public void drawHCenteredString(Graphics g, String string, float scale, float x, float y, Color col ) {
      float width = getWidth(string, scale);
      
      drawString(g, string, scale, x - width/2, y, col);
   }
   
   public void drawHCenteredString(Graphics g, String string, float scale, float x, float y ) {
      drawHCenteredString(g, string, scale, x, y, g.getColor() );
   }
   
   private SizedAngelCodeFont getClosestMatch(int targetHeight) {
      
      if(fonts.isEmpty()) return null;
      
      for (int i = 0, n = fonts.size(); i < n; i++) {
         int fontHeight = fonts.get(i).height;
         if (fontHeight >= targetHeight) {
            if (i > 0 && Math.abs(targetHeight - fonts.get(i - 1).height) < Math.abs(targetHeight - fontHeight)) i--;
            return fonts.get(i);
         }
      }
      
      return fonts.get(fonts.size() - 1);
   }
   
   
   //TODO: Fix these
   public float getHeight(String text) {

      SizedAngelCodeFont closestMatch= getClosestMatch(fontHeight);

      float fontScale = fontHeight / (float)closestMatch.height;

      return closestMatch.font.getHeight(text) * fontScale;
   }
   
   public float getHeight(String text, float scale) {
      
      int targetHeight = (int)(fontHeight * scale);
      
      SizedAngelCodeFont closestMatch = getClosestMatch(targetHeight);

      float fontScale = targetHeight / (float)closestMatch.height;

      return closestMatch.font.getHeight(text) * fontScale;
   }
   
   public float getWidth(String text) {
      SizedAngelCodeFont closestMatch= getClosestMatch(fontHeight);

      float fontScale = fontHeight / (float)closestMatch.height;

      return closestMatch.font.getWidth(text) * fontScale;
   }
   
   public float getWidth(String text, float scale) {
      
      int targetHeight = (int)(fontHeight * scale);
      
      SizedAngelCodeFont closestMatch = getClosestMatch(targetHeight);

      float fontScale = targetHeight / (float)closestMatch.height;

      return closestMatch.font.getWidth(text) * fontScale;
   }
   
   public float getLineHeight() {
      SizedAngelCodeFont closestMatch= getClosestMatch(fontHeight);

      float fontScale = fontHeight / (float)closestMatch.height;

      return closestMatch.font.getLineHeight() * fontScale;
   }
   
   public float getYOffset(String text) {
      SizedAngelCodeFont closestMatch = getClosestMatch(fontHeight);

      float fontScale = fontHeight / (float)closestMatch.height;

      return closestMatch.font.getYOffset(text) * fontScale;
   }
}   


class SizedAngelCodeFont implements Comparable <SizedAngelCodeFont> {
   
   public AngelCodeFont font;
   public int height;
   
   public SizedAngelCodeFont(AngelCodeFont font, int height ) {
      this.font = font;
      this.height = height;
   }

   public int compareTo(SizedAngelCodeFont target) {
      if (height < target.height ) return -1;
      if( height > target.height ) return 1;
      else return 0;
   }
}



Kev: If you decide to add it to the library, just let me know if you need anything from me. I'd be glad to donate the code.


Oh, as a small aside, this code might be broken:

Code:
   public int getYOffset(String text) {
      return fonts.get(fontHeight).getYOffset(text);
   }


I wasn't sure what value this was getting, and so I wasn't sure if it should scale if the rest of the font does (like in getHeight() ). Kev?


Last edited by JoshuaD on Mon Feb 20, 2012 11:49 pm, edited 2 times in total.

Top
 Profile  
 
 Post subject:
PostPosted: Mon Dec 15, 2008 1:59 am 
Offline
Game Developer
User avatar

Joined: Sun May 25, 2008 9:45 am
Posts: 578
Very cool! Thanks for putting this together. I know I will make use of it.

Couple things:

* I don't think it should require a directory of fonts, since when using JWS all resources must be in JARs. It is nice to load either out of a directory or with the addFont method.

* HashMap is generally preferred over Hashtable when the synchronization Hashtable provides is not needed. However I believe they do use different hashing algorithms and that could matter in a rare scenario.

* A hash lookup is generally pretty expensive. Your getClosestMatch method can potentially do a lot of needless lookups. Eg, if I have font sizes 20 and 40 and I request a font at size 30, it will do 21 lookups. There are a couple ways to avoid this. One easy way is to store two lists with corresponding indices, one of Fonts and one of Integers. The lists must be sorted from lowest to highest size. Then the method could look like:
Code:
private Font getClosestFont (int size) {
   for (int i = 0, n = fontSizes.size(); i < n; i++) {
      int fontSize = fontSizes.get(i);
      if (fontSize >= size) {
         if (i > 0 && Math.abs(size - fontSizes.get(i - 1)) < Math.abs(size - fontSize)) i--;
         return fonts.get(i);
      }
   }
   throw new IllegalStateException("No fonts have been added.");
}

Another way would be to create a static private class that has two public fields: "Font font" and "int height". Then store a list of these, sorted from lowest to highest and use a method similar to the above.

* Since the font height to draw with is set on your class, there is no need to compute which font is being used every time you draw some text. Instead you could compute which font to use at the time the font height is set. This would simplify the methods getWidth, getHeight, etc.

_________________
SingSong Karaoke - http://singthegame.com


Top
 Profile  
 
 Post subject:
PostPosted: Mon Dec 15, 2008 6:21 am 
Offline

Joined: Sun Jun 03, 2007 2:51 am
Posts: 80
Glad you like it Nate. :D All your suggestions look good to me, especially using a jar to hold the font. I forgot java already has the framework setup for working with those. I'll work on implementing all of these suggestions soon.

Do you know if I should be scaling the results of getYOffset() like I do in getStringWidth()?

Edit: I've never worked with Jars progmatically before. Is there an easy to way to just do File file = new File("<Some Path To A File In the Jar>")? Or is it necessary to use a stream reader?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Dec 23, 2008 7:29 am 
Offline
Game Developer
User avatar

Joined: Sun May 25, 2008 9:45 am
Posts: 578
Not really sure about getYOffset, but I image since it seems to deal with the size of the font it should return a scaled value.

Loading resources from is done through the classpath. The ResourceLoader class in Slick looks at both the file system and the classpath, so is probably what you want to use. Otherwise you can use ClassLoader#getResource and ClassLoader#getResourceAsStream. These return null on failure so be sure to check. Usually you use "getClass().getClassLoader().getResourceAsStream(path)" where path is relative to your ".class" file. If path starts with "/" then it is relative to the root of the classpath. The classpath is defined when your program starts and consists of JARs and directories.

Also, just to be clear on Java terminology, if you have a stream then it reads bytes, if you have a reader then it reads characters. The difference is a reader knows what charset to use to interpret the bytes as characters.

_________________
SingSong Karaoke - http://singthegame.com


Top
 Profile  
 
PostPosted: Mon Feb 20, 2012 11:50 pm 
Offline

Joined: Sun Jun 03, 2007 2:51 am
Posts: 80
Added my updated code, which now uses .jars rather than a directory.


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 5 posts ] 

All times are UTC


Who is online

Users browsing this forum: No registered users and 2 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group