OSDN Git Service

[added] Hiero tool for creating bitmap fonts.
authornathan.sweet <nathan.sweet@6c4fd544-2939-11df-bb46-9574ba5d0bfa>
Sun, 31 Oct 2010 23:13:50 +0000 (23:13 +0000)
committernathan.sweet <nathan.sweet@6c4fd544-2939-11df-bb46-9574ba5d0bfa>
Sun, 31 Oct 2010 23:13:50 +0000 (23:13 +0000)
22 files changed:
backends/gdx-backend-lwjgl/src/com/badlogic/gdx/backends/desktop/LwjglGraphics.java
extensions/hiero/.classpath [new file with mode: 0644]
extensions/hiero/.project [new file with mode: 0644]
extensions/hiero/data/splash.jpg [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/BMFontUtil.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/Hiero.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/Kerning.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/Glyph.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/GlyphPage.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/HieroSettings.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/UnicodeFont.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/UnicodeFontTest.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ColorEffect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ConfigurableEffect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/Effect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/EffectUtil.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/FilterEffect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/GradientEffect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineEffect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineWobbleEffect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineZigzagEffect.java [new file with mode: 0644]
extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ShadowEffect.java [new file with mode: 0644]

index af6e367..afd147d 100644 (file)
@@ -131,7 +131,7 @@ public final class LwjglGraphics implements Graphics, RenderListener {
                TextureWrap vWrap) {\r
                Pixmap pixmap = newPixmap(file);\r
                if (!isPowerOfTwo(pixmap.getHeight()) || !isPowerOfTwo(pixmap.getWidth()))\r
-                       throw new GdxRuntimeException("Texture dimensions must be a power of two");\r
+                       throw new GdxRuntimeException("Texture dimensions must be a power of two: " + file);\r
 \r
                return new LwjglTexture((BufferedImage)pixmap.getNativePixmap(), minFilter, magFilter, uWrap, vWrap, false);\r
        }\r
diff --git a/extensions/hiero/.classpath b/extensions/hiero/.classpath
new file mode 100644 (file)
index 0000000..3488d82
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<classpath>\r
+       <classpathentry excluding="**/.svn/*" kind="src" path="src"/>\r
+       <classpathentry excluding="**/.svn/*" kind="src" path="data"/>\r
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>\r
+       <classpathentry combineaccessrules="false" kind="src" path="/gdx"/>\r
+       <classpathentry combineaccessrules="false" kind="src" path="/gdx-backend-lwjgl"/>\r
+       <classpathentry kind="output" path="bin"/>\r
+</classpath>\r
diff --git a/extensions/hiero/.project b/extensions/hiero/.project
new file mode 100644 (file)
index 0000000..5c7f7aa
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<projectDescription>\r
+       <name>hiero</name>\r
+       <comment></comment>\r
+       <projects>\r
+       </projects>\r
+       <buildSpec>\r
+               <buildCommand>\r
+                       <name>org.eclipse.jdt.core.javabuilder</name>\r
+                       <arguments>\r
+                       </arguments>\r
+               </buildCommand>\r
+       </buildSpec>\r
+       <natures>\r
+               <nature>org.eclipse.jdt.core.javanature</nature>\r
+       </natures>\r
+</projectDescription>\r
diff --git a/extensions/hiero/data/splash.jpg b/extensions/hiero/data/splash.jpg
new file mode 100644 (file)
index 0000000..081ad84
Binary files /dev/null and b/extensions/hiero/data/splash.jpg differ
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/BMFontUtil.java b/extensions/hiero/src/com/badlogic/gdx/hiero/BMFontUtil.java
new file mode 100644 (file)
index 0000000..f79b887
--- /dev/null
@@ -0,0 +1,193 @@
+\r
+package com.badlogic.gdx.hiero;\r
+\r
+import java.awt.Font;\r
+import java.awt.Graphics;\r
+import java.awt.Image;\r
+import java.awt.font.GlyphMetrics;\r
+import java.awt.font.GlyphVector;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.image.AffineTransformOp;\r
+import java.awt.image.BufferedImage;\r
+import java.io.File;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.io.PrintStream;\r
+import java.util.ArrayList;\r
+import java.util.HashMap;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+import java.util.Map;\r
+\r
+import javax.imageio.ImageIO;\r
+import javax.swing.ImageIcon;\r
+\r
+import com.badlogic.gdx.Files.FileType;\r
+import com.badlogic.gdx.Gdx;\r
+import com.badlogic.gdx.hiero.unicodefont.Glyph;\r
+import com.badlogic.gdx.hiero.unicodefont.GlyphPage;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+\r
+/**\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class BMFontUtil {\r
+       private final UnicodeFont unicodeFont;\r
+\r
+       public BMFontUtil (UnicodeFont unicodeFont) {\r
+               this.unicodeFont = unicodeFont;\r
+       }\r
+\r
+       public void save (File outputBMFontFile) throws IOException {\r
+               File outputDir = outputBMFontFile.getParentFile();\r
+               String outputName = outputBMFontFile.getName();\r
+               if (outputName.endsWith(".fnt")) outputName = outputName.substring(0, outputName.length() - 4);\r
+\r
+               unicodeFont.loadGlyphs();\r
+\r
+               PrintStream out = new PrintStream(new FileOutputStream(new File(outputDir, outputName + ".fnt")));\r
+               Font font = unicodeFont.getFont();\r
+               int pageWidth = unicodeFont.getGlyphPageWidth();\r
+               int pageHeight = unicodeFont.getGlyphPageHeight();\r
+               out.println("info face=\"" + font.getFontName() + "\" size=" + font.getSize() + " bold=" + (font.isBold() ? 1 : 0)\r
+                       + " italic=" + (font.isItalic() ? 1 : 0)\r
+                       + " charset=\"\" unicode=0 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1");\r
+               out.println("common lineHeight=" + unicodeFont.getLineHeight() + " base=" + unicodeFont.getAscent() + " scaleW="\r
+                       + pageWidth + " scaleH=" + pageHeight + " pages=" + unicodeFont.getGlyphPages().size() + " packed=0");\r
+\r
+               int pageIndex = 0, glyphCount = 0;\r
+               for (Iterator pageIter = unicodeFont.getGlyphPages().iterator(); pageIter.hasNext();) {\r
+                       GlyphPage page = (GlyphPage)pageIter.next();\r
+                       String fileName;\r
+                       if (pageIndex == 0 && !pageIter.hasNext())\r
+                               fileName = outputName + ".png";\r
+                       else\r
+                               fileName = outputName + (pageIndex + 1) + ".png";\r
+                       out.println("page id=" + pageIndex + " file=\"" + fileName + "\"");\r
+                       glyphCount += page.getGlyphs().size();\r
+                       pageIndex++;\r
+               }\r
+\r
+               out.println("chars count=" + glyphCount);\r
+\r
+               // Always output space entry (codepoint 32).\r
+               int[] glyphMetrics = getGlyphMetrics(font, 32);\r
+               int xAdvance = glyphMetrics[1];\r
+               out.println("char id=32   x=0     y=0     width=0     height=0     xoffset=0     yoffset=" + unicodeFont.getAscent()\r
+                       + "    xadvance=" + xAdvance + "     page=0  chnl=0 ");\r
+\r
+               pageIndex = 0;\r
+               List allGlyphs = new ArrayList(512);\r
+               for (Iterator pageIter = unicodeFont.getGlyphPages().iterator(); pageIter.hasNext();) {\r
+                       GlyphPage page = (GlyphPage)pageIter.next();\r
+                       for (Iterator glyphIter = page.getGlyphs().iterator(); glyphIter.hasNext();) {\r
+                               Glyph glyph = (Glyph)glyphIter.next();\r
+\r
+                               glyphMetrics = getGlyphMetrics(font, glyph.getCodePoint());\r
+                               int xOffset = glyphMetrics[0];\r
+                               xAdvance = glyphMetrics[1];\r
+\r
+                               out.println("char id=" + glyph.getCodePoint() + "   " + "x=" + (int)(glyph.getU() * pageWidth) + "     y="\r
+                                       + (int)(glyph.getV() * pageHeight) + "     width=" + glyph.getWidth() + "     height=" + glyph.getHeight()\r
+                                       + "     xoffset=" + xOffset + "     yoffset=" + glyph.getYOffset() + "    xadvance=" + xAdvance + "     page="\r
+                                       + pageIndex + "  chnl=0 ");\r
+                       }\r
+                       allGlyphs.addAll(page.getGlyphs());\r
+                       pageIndex++;\r
+               }\r
+\r
+               String ttfFileRef = unicodeFont.getFontFile();\r
+               if (ttfFileRef == null)\r
+                       System.out.println("Kerning information could not be output because a TTF font file was not specified.");\r
+               else {\r
+                       Kerning kerning = new Kerning();\r
+                       try {\r
+                               kerning.load(Gdx.files.readFile(ttfFileRef, FileType.Internal), font.getSize());\r
+                       } catch (IOException ex) {\r
+                               System.out.println("Unable to read kerning information from font: " + ttfFileRef);\r
+                       }\r
+\r
+                       Map glyphCodeToCodePoint = new HashMap();\r
+                       for (Iterator iter = allGlyphs.iterator(); iter.hasNext();) {\r
+                               Glyph glyph = (Glyph)iter.next();\r
+                               glyphCodeToCodePoint.put(new Integer(getGlyphCode(font, glyph.getCodePoint())), new Integer(glyph.getCodePoint()));\r
+                       }\r
+\r
+                       List kernings = new ArrayList(256);\r
+                       class KerningPair {\r
+                               public int firstCodePoint, secondCodePoint, offset;\r
+                       }\r
+                       for (Iterator iter1 = allGlyphs.iterator(); iter1.hasNext();) {\r
+                               Glyph firstGlyph = (Glyph)iter1.next();\r
+                               int firstGlyphCode = getGlyphCode(font, firstGlyph.getCodePoint());\r
+                               int[] values = kerning.getValues(firstGlyphCode);\r
+                               if (values == null) continue;\r
+                               for (int i = 0; i < values.length; i++) {\r
+                                       Integer secondCodePoint = (Integer)glyphCodeToCodePoint.get(new Integer(values[i] & 0xffff));\r
+                                       if (secondCodePoint == null) continue; // We may not be outputting the second character.\r
+                                       int offset = values[i] >> 16;\r
+                                       KerningPair pair = new KerningPair();\r
+                                       pair.firstCodePoint = firstGlyph.getCodePoint();\r
+                                       pair.secondCodePoint = secondCodePoint.intValue();\r
+                                       pair.offset = offset;\r
+                                       kernings.add(pair);\r
+                               }\r
+                       }\r
+                       out.println("kernings count=" + kerning.getCount());\r
+                       for (Iterator iter = kernings.iterator(); iter.hasNext();) {\r
+                               KerningPair pair = (KerningPair)iter.next();\r
+                               out.println("kerning first=" + pair.firstCodePoint + "  second=" + pair.secondCodePoint + "  amount=" + pair.offset);\r
+                       }\r
+               }\r
+               out.close();\r
+\r
+               pageIndex = 0;\r
+               for (Iterator pageIter = unicodeFont.getGlyphPages().iterator(); pageIter.hasNext();) {\r
+                       GlyphPage page = (GlyphPage)pageIter.next();\r
+                       String fileName;\r
+                       if (pageIndex == 0 && !pageIter.hasNext())\r
+                               fileName = outputName + ".png";\r
+                       else\r
+                               fileName = outputName + (pageIndex + 1) + ".png";\r
+                       File imageOutputFile = new File(outputDir, fileName);\r
+                       FileOutputStream imageOutput = new FileOutputStream(imageOutputFile);\r
+                       try {\r
+                               // BOZO - Save texture to PNG.\r
+                               // saveImage(page.getTexture(), "png", imageOutput, true);\r
+                       } finally {\r
+                               imageOutput.close();\r
+                       }\r
+                       // Flip output image.\r
+                       Image image = new ImageIcon(imageOutputFile.getAbsolutePath()).getImage();\r
+                       BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);\r
+                       Graphics g = bufferedImage.getGraphics();\r
+                       g.drawImage(image, 0, 0, null);\r
+                       AffineTransform tx = AffineTransform.getScaleInstance(1, -1);\r
+                       tx.translate(0, -image.getHeight(null));\r
+                       AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);\r
+                       bufferedImage = op.filter(bufferedImage, null);\r
+                       ImageIO.write(bufferedImage, "png", imageOutputFile);\r
+\r
+                       pageIndex++;\r
+               }\r
+       }\r
+\r
+       private int getGlyphCode (Font font, int codePoint) {\r
+               char[] chars = Character.toChars(codePoint);\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+               return vector.getGlyphCode(0);\r
+       }\r
+\r
+       private int[] getGlyphMetrics (Font font, int codePoint) {\r
+               // xOffset and xAdvance will be incorrect for unicode characters such as combining marks or non-spacing characters\r
+               // (eg Pnujabi's "\u0A1C\u0A47") that require the context of surrounding glyphs to determine spacing, but thisis the\r
+               // best we can do with the BMFont format.\r
+               char[] chars = Character.toChars(codePoint);\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+               GlyphMetrics metrics = vector.getGlyphMetrics(0);\r
+               int xOffset = vector.getGlyphPixelBounds(0, GlyphPage.renderContext, 0.5f, 0).x - unicodeFont.getPaddingLeft();\r
+               int xAdvance = (int)(metrics.getAdvanceX() + unicodeFont.getPaddingAdvanceX() + unicodeFont.getPaddingLeft() + unicodeFont\r
+                       .getPaddingRight());\r
+               return new int[] {xOffset, xAdvance};\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/Hiero.java b/extensions/hiero/src/com/badlogic/gdx/hiero/Hiero.java
new file mode 100644 (file)
index 0000000..99043e8
--- /dev/null
@@ -0,0 +1,1239 @@
+\r
+package com.badlogic.gdx.hiero;\r
+\r
+import static org.lwjgl.opengl.GL11.*;\r
+\r
+import java.awt.BorderLayout;\r
+import java.awt.Canvas;\r
+import java.awt.Component;\r
+import java.awt.Container;\r
+import java.awt.Dimension;\r
+import java.awt.EventQueue;\r
+import java.awt.FileDialog;\r
+import java.awt.FlowLayout;\r
+import java.awt.Font;\r
+import java.awt.Frame;\r
+import java.awt.GraphicsEnvironment;\r
+import java.awt.GridBagConstraints;\r
+import java.awt.GridBagLayout;\r
+import java.awt.Insets;\r
+import java.awt.LayoutManager;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.KeyAdapter;\r
+import java.awt.event.KeyEvent;\r
+import java.awt.event.MouseAdapter;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.event.WindowAdapter;\r
+import java.awt.event.WindowEvent;\r
+import java.awt.image.BufferedImage;\r
+import java.io.File;\r
+import java.io.IOException;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+import java.util.prefs.Preferences;\r
+\r
+import javax.swing.BorderFactory;\r
+import javax.swing.ButtonGroup;\r
+import javax.swing.DefaultComboBoxModel;\r
+import javax.swing.Icon;\r
+import javax.swing.ImageIcon;\r
+import javax.swing.JButton;\r
+import javax.swing.JCheckBox;\r
+import javax.swing.JColorChooser;\r
+import javax.swing.JComboBox;\r
+import javax.swing.JFormattedTextField;\r
+import javax.swing.JFrame;\r
+import javax.swing.JLabel;\r
+import javax.swing.JList;\r
+import javax.swing.JMenu;\r
+import javax.swing.JMenuBar;\r
+import javax.swing.JMenuItem;\r
+import javax.swing.JPanel;\r
+import javax.swing.JRadioButton;\r
+import javax.swing.JScrollPane;\r
+import javax.swing.JSpinner;\r
+import javax.swing.JTextField;\r
+import javax.swing.JTextPane;\r
+import javax.swing.JWindow;\r
+import javax.swing.KeyStroke;\r
+import javax.swing.ScrollPaneConstants;\r
+import javax.swing.SpinnerNumberModel;\r
+import javax.swing.UIManager;\r
+import javax.swing.UIManager.LookAndFeelInfo;\r
+import javax.swing.border.EmptyBorder;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+import javax.swing.event.DocumentEvent;\r
+import javax.swing.event.DocumentListener;\r
+import javax.swing.event.ListSelectionEvent;\r
+import javax.swing.event.ListSelectionListener;\r
+\r
+import org.lwjgl.LWJGLException;\r
+import org.lwjgl.opengl.Display;\r
+import org.lwjgl.opengl.GL11;\r
+\r
+import sun.rmi.runtime.Log;\r
+\r
+import com.badlogic.gdx.Gdx;\r
+import com.badlogic.gdx.RenderListener;\r
+import com.badlogic.gdx.backends.desktop.LwjglApplication;\r
+import com.badlogic.gdx.graphics.BitmapFont;\r
+import com.badlogic.gdx.graphics.Color;\r
+import com.badlogic.gdx.graphics.Texture;\r
+import com.badlogic.gdx.hiero.unicodefont.GlyphPage;\r
+import com.badlogic.gdx.hiero.unicodefont.HieroSettings;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ColorEffect;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ConfigurableEffect;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ConfigurableEffect.Value;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.EffectUtil;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.GradientEffect;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.OutlineEffect;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.OutlineWobbleEffect;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.OutlineZigzagEffect;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ShadowEffect;\r
+import com.badlogic.gdx.utils.GdxRuntimeException;\r
+\r
+/**\r
+ * A tool to visualize settings for {@link UnicodeFont} and to export BMFont files for use with {@link BitmapFont}.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class Hiero extends JFrame {\r
+       static final String NEHE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n" //\r
+               + "abcdefghijklmnopqrstuvwxyz\n1234567890\n" //\r
+               + "\"!`?'.,;:()[]{}<>|/@\\^$-%+=#_&~*\u007F";\r
+\r
+       LwjglApplication app;\r
+       Canvas glCanvas;\r
+       volatile UnicodeFont newUnicodeFont;\r
+       UnicodeFont unicodeFont;\r
+       Color renderingBackgroundColor = Color.BLACK;\r
+       List effectPanels = new ArrayList();\r
+       Preferences prefs;\r
+       ColorEffect colorEffect;\r
+\r
+       JScrollPane appliedEffectsScroll;\r
+       JPanel appliedEffectsPanel;\r
+       JButton addEffectButton;\r
+       JTextPane sampleTextPane;\r
+       JSpinner padAdvanceXSpinner;\r
+       JList effectsList;\r
+       JPanel gamePanel;\r
+       JTextField fontFileText;\r
+       JRadioButton fontFileRadio;\r
+       JRadioButton systemFontRadio;\r
+       JSpinner padBottomSpinner;\r
+       JSpinner padLeftSpinner;\r
+       JSpinner padRightSpinner;\r
+       JSpinner padTopSpinner;\r
+       JList fontList;\r
+       JSpinner fontSizeSpinner;\r
+       DefaultComboBoxModel fontListModel;\r
+       JLabel backgroundColorLabel;\r
+       JButton browseButton;\r
+       JSpinner padAdvanceYSpinner;\r
+       JCheckBox italicCheckBox;\r
+       JCheckBox boldCheckBox;\r
+       JLabel glyphsTotalLabel;\r
+       JLabel glyphPagesTotalLabel;\r
+       JComboBox glyphPageHeightCombo;\r
+       JComboBox glyphPageWidthCombo;\r
+       JComboBox glyphPageCombo;\r
+       JPanel glyphCachePanel;\r
+       JRadioButton glyphCacheRadio;\r
+       JRadioButton sampleTextRadio;\r
+       DefaultComboBoxModel glyphPageComboModel;\r
+       JButton resetCacheButton;\r
+       JButton sampleAsciiButton;\r
+       JButton sampleNeheButton;\r
+       DefaultComboBoxModel effectsListModel;\r
+       JMenuItem openMenuItem;\r
+       JMenuItem saveMenuItem;\r
+       JMenuItem exitMenuItem;\r
+       JMenuItem saveBMFontMenuItem;\r
+       File saveBmFontFile;\r
+\r
+       public Hiero () {\r
+               super("Hiero v2.0 - Bitmap Font Tool");\r
+               Splash splash = new Splash(this, "/splash.jpg", 2000);\r
+               initialize();\r
+               splash.close();\r
+\r
+               prefs = Preferences.userNodeForPackage(Hiero.class);\r
+               java.awt.Color backgroundColor = EffectUtil.fromString(prefs.get("background", "000000"));\r
+               backgroundColorLabel.setIcon(getColorIcon(backgroundColor));\r
+               renderingBackgroundColor = new Color(backgroundColor.getRed() / 255f, backgroundColor.getGreen() / 255f,\r
+                       backgroundColor.getBlue() / 255f, 1);\r
+               fontList.setSelectedValue(prefs.get("system.font", "Arial"), true);\r
+               fontFileText.setText(prefs.get("font.file", ""));\r
+\r
+               java.awt.Color foregroundColor = EffectUtil.fromString(prefs.get("foreground", "ffffff"));\r
+               colorEffect = new ColorEffect();\r
+               colorEffect.setColor(foregroundColor);\r
+               effectsListModel.addElement(colorEffect);\r
+               effectsListModel.addElement(new GradientEffect());\r
+               effectsListModel.addElement(new OutlineEffect());\r
+               effectsListModel.addElement(new OutlineWobbleEffect());\r
+               effectsListModel.addElement(new OutlineZigzagEffect());\r
+               effectsListModel.addElement(new ShadowEffect());\r
+               new EffectPanel(colorEffect);\r
+\r
+               gamePanel.add(glCanvas = new Canvas() {\r
+                       private final Dimension minSize = new Dimension();\r
+\r
+                       public final void addNotify () {\r
+                               super.addNotify();\r
+                               app = new LwjglApplication("Hiero", 200, 200, false) {\r
+                                       protected void setupDisplay () throws LWJGLException {\r
+                                               try {\r
+                                                       Display.setParent(glCanvas);\r
+                                               } catch (LWJGLException ex) {\r
+                                                       throw new GdxRuntimeException("Error setting display parent.", ex);\r
+                                               }\r
+                                               super.setupDisplay();\r
+                                       }\r
+                               };\r
+                               app.getGraphics().setRenderListener(new Renderer());\r
+                               addWindowListener(new WindowAdapter() {\r
+                                       public void windowClosed (WindowEvent event) {\r
+                                               app.stop();\r
+                                       }\r
+                               });\r
+                       }\r
+\r
+                       public Dimension getMinimumSize () {\r
+                               return minSize;\r
+                       }\r
+               });\r
+\r
+               setVisible(true);\r
+       }\r
+\r
+       void initialize () {\r
+               initializeComponents();\r
+               initializeMenus();\r
+               initializeEvents();\r
+\r
+               setSize(800, 600);\r
+               setLocationRelativeTo(null);\r
+               setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);\r
+\r
+               sampleNeheButton.doClick();\r
+       }\r
+\r
+       void updateFont () {\r
+               updateFont(false);\r
+       }\r
+\r
+       private void updateFont (boolean ignoreFileText) {\r
+               UnicodeFont unicodeFont;\r
+\r
+               int fontSize = ((Integer)fontSizeSpinner.getValue()).intValue();\r
+\r
+               File file = new File(fontFileText.getText());\r
+               if (!ignoreFileText && file.exists() && file.isFile()) {\r
+                       // Load from file.\r
+                       fontFileRadio.setSelected(true);\r
+                       fontList.setEnabled(false);\r
+                       systemFontRadio.setEnabled(false);\r
+                       try {\r
+                               unicodeFont = new UnicodeFont(fontFileText.getText(), fontSize, boldCheckBox.isSelected(),\r
+                                       italicCheckBox.isSelected());\r
+                       } catch (Throwable ex) {\r
+                               ex.printStackTrace();\r
+                               updateFont(true);\r
+                               return;\r
+                       }\r
+               } else {\r
+                       // Load from java.awt.Font (kerning not available!).\r
+                       fontList.setEnabled(true);\r
+                       systemFontRadio.setEnabled(true);\r
+                       systemFontRadio.setSelected(true);\r
+                       unicodeFont = new UnicodeFont(Font.decode((String)fontList.getSelectedValue()), fontSize, boldCheckBox.isSelected(),\r
+                               italicCheckBox.isSelected());\r
+               }\r
+               unicodeFont.setPaddingTop(((Integer)padTopSpinner.getValue()).intValue());\r
+               unicodeFont.setPaddingRight(((Integer)padRightSpinner.getValue()).intValue());\r
+               unicodeFont.setPaddingBottom(((Integer)padBottomSpinner.getValue()).intValue());\r
+               unicodeFont.setPaddingLeft(((Integer)padLeftSpinner.getValue()).intValue());\r
+               unicodeFont.setPaddingAdvanceX(((Integer)padAdvanceXSpinner.getValue()).intValue());\r
+               unicodeFont.setPaddingAdvanceY(((Integer)padAdvanceYSpinner.getValue()).intValue());\r
+               unicodeFont.setGlyphPageWidth(((Integer)glyphPageWidthCombo.getSelectedItem()).intValue());\r
+               unicodeFont.setGlyphPageHeight(((Integer)glyphPageHeightCombo.getSelectedItem()).intValue());\r
+\r
+               for (Iterator iter = effectPanels.iterator(); iter.hasNext();) {\r
+                       EffectPanel panel = (EffectPanel)iter.next();\r
+                       unicodeFont.getEffects().add(panel.getEffect());\r
+               }\r
+\r
+               int size = sampleTextPane.getFont().getSize();\r
+               if (size < 14) size = 14;\r
+               sampleTextPane.setFont(unicodeFont.getFont().deriveFont((float)size));\r
+\r
+               this.newUnicodeFont = unicodeFont;\r
+       }\r
+\r
+       void save (File file) throws IOException {\r
+               HieroSettings settings = new HieroSettings();\r
+               settings.setFontSize(((Integer)fontSizeSpinner.getValue()).intValue());\r
+               settings.setBold(boldCheckBox.isSelected());\r
+               settings.setItalic(italicCheckBox.isSelected());\r
+               settings.setPaddingTop(((Integer)padTopSpinner.getValue()).intValue());\r
+               settings.setPaddingRight(((Integer)padRightSpinner.getValue()).intValue());\r
+               settings.setPaddingBottom(((Integer)padBottomSpinner.getValue()).intValue());\r
+               settings.setPaddingLeft(((Integer)padLeftSpinner.getValue()).intValue());\r
+               settings.setPaddingAdvanceX(((Integer)padAdvanceXSpinner.getValue()).intValue());\r
+               settings.setPaddingAdvanceY(((Integer)padAdvanceYSpinner.getValue()).intValue());\r
+               settings.setGlyphPageWidth(((Integer)glyphPageWidthCombo.getSelectedItem()).intValue());\r
+               settings.setGlyphPageHeight(((Integer)glyphPageHeightCombo.getSelectedItem()).intValue());\r
+               for (Iterator iter = effectPanels.iterator(); iter.hasNext();) {\r
+                       EffectPanel panel = (EffectPanel)iter.next();\r
+                       settings.getEffects().add(panel.getEffect());\r
+               }\r
+               settings.save(file);\r
+       }\r
+\r
+       void open (File file) {\r
+               EffectPanel[] panels = (EffectPanel[])effectPanels.toArray(new EffectPanel[effectPanels.size()]);\r
+               for (int i = 0; i < panels.length; i++)\r
+                       panels[i].remove();\r
+\r
+               HieroSettings settings = new HieroSettings(file.getAbsolutePath());\r
+               fontSizeSpinner.setValue(new Integer(settings.getFontSize()));\r
+               boldCheckBox.setSelected(settings.isBold());\r
+               italicCheckBox.setSelected(settings.isItalic());\r
+               padTopSpinner.setValue(new Integer(settings.getPaddingTop()));\r
+               padRightSpinner.setValue(new Integer(settings.getPaddingRight()));\r
+               padBottomSpinner.setValue(new Integer(settings.getPaddingBottom()));\r
+               padLeftSpinner.setValue(new Integer(settings.getPaddingLeft()));\r
+               padAdvanceXSpinner.setValue(new Integer(settings.getPaddingAdvanceX()));\r
+               padAdvanceYSpinner.setValue(new Integer(settings.getPaddingAdvanceY()));\r
+               glyphPageWidthCombo.setSelectedItem(new Integer(settings.getGlyphPageWidth()));\r
+               glyphPageHeightCombo.setSelectedItem(new Integer(settings.getGlyphPageHeight()));\r
+               for (Iterator iter = settings.getEffects().iterator(); iter.hasNext();) {\r
+                       ConfigurableEffect settingsEffect = (ConfigurableEffect)iter.next();\r
+                       for (int i = 0, n = effectsListModel.getSize(); i < n; i++) {\r
+                               ConfigurableEffect effect = (ConfigurableEffect)effectsListModel.getElementAt(i);\r
+                               if (effect.getClass() == settingsEffect.getClass()) {\r
+                                       effect.setValues(settingsEffect.getValues());\r
+                                       new EffectPanel(effect);\r
+                                       break;\r
+                               }\r
+                       }\r
+               }\r
+\r
+               updateFont();\r
+       }\r
+\r
+       private void initializeEvents () {\r
+               fontList.addListSelectionListener(new ListSelectionListener() {\r
+                       public void valueChanged (ListSelectionEvent evt) {\r
+                               if (evt.getValueIsAdjusting()) return;\r
+                               prefs.put("system.font", (String)fontList.getSelectedValue());\r
+                               updateFont();\r
+                       }\r
+               });\r
+\r
+               class FontUpdateListener implements ChangeListener, ActionListener {\r
+                       public void stateChanged (ChangeEvent evt) {\r
+                               updateFont();\r
+                       }\r
+\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               updateFont();\r
+                       }\r
+\r
+                       public void addSpinners (JSpinner[] spinners) {\r
+                               for (int i = 0; i < spinners.length; i++) {\r
+                                       final JSpinner spinner = spinners[i];\r
+                                       spinner.addChangeListener(this);\r
+                                       ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField().addKeyListener(new KeyAdapter() {\r
+                                               String lastText;\r
+\r
+                                               public void keyReleased (KeyEvent evt) {\r
+                                                       JFormattedTextField textField = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField();\r
+                                                       String text = textField.getText();\r
+                                                       if (text.length() == 0) return;\r
+                                                       if (text.equals(lastText)) return;\r
+                                                       lastText = text;\r
+                                                       int caretPosition = textField.getCaretPosition();\r
+                                                       try {\r
+                                                               spinner.setValue(Integer.valueOf(text));\r
+                                                               textField.setCaretPosition(caretPosition);\r
+                                                       } catch (Throwable ignored) {\r
+                                                       }\r
+                                               }\r
+                                       });\r
+                               }\r
+                       }\r
+               }\r
+               FontUpdateListener listener = new FontUpdateListener();\r
+\r
+               listener.addSpinners(new JSpinner[] {padTopSpinner, padRightSpinner, padBottomSpinner, padLeftSpinner, padAdvanceXSpinner,\r
+                       padAdvanceYSpinner});\r
+               fontSizeSpinner.addChangeListener(listener);\r
+\r
+               glyphPageWidthCombo.addActionListener(listener);\r
+               glyphPageHeightCombo.addActionListener(listener);\r
+               boldCheckBox.addActionListener(listener);\r
+               italicCheckBox.addActionListener(listener);\r
+               resetCacheButton.addActionListener(listener);\r
+\r
+               sampleTextRadio.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               glyphCachePanel.setVisible(false);\r
+                       }\r
+               });\r
+               glyphCacheRadio.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               glyphCachePanel.setVisible(true);\r
+                       }\r
+               });\r
+\r
+               fontFileText.getDocument().addDocumentListener(new DocumentListener() {\r
+                       public void removeUpdate (DocumentEvent evt) {\r
+                               changed();\r
+                       }\r
+\r
+                       public void insertUpdate (DocumentEvent evt) {\r
+                               changed();\r
+                       }\r
+\r
+                       public void changedUpdate (DocumentEvent evt) {\r
+                               changed();\r
+                       }\r
+\r
+                       private void changed () {\r
+                               File file = new File(fontFileText.getText());\r
+                               if (fontList.isEnabled() && (!file.exists() || !file.isFile())) return;\r
+                               prefs.put("font.file", fontFileText.getText());\r
+                               updateFont();\r
+                       }\r
+               });\r
+\r
+               fontFileRadio.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               if (fontList.isEnabled()) systemFontRadio.setSelected(true);\r
+                       }\r
+               });\r
+\r
+               browseButton.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               FileDialog dialog = new FileDialog(Hiero.this, "Choose TrueType font file", FileDialog.LOAD);\r
+                               dialog.setLocationRelativeTo(null);\r
+                               dialog.setFile("*.ttf");\r
+                               dialog.setVisible(true);\r
+                               String fileName = dialog.getFile();\r
+                               if (fileName == null) return;\r
+                               fontFileText.setText(new File(dialog.getDirectory(), fileName).getAbsolutePath());\r
+                       }\r
+               });\r
+\r
+               backgroundColorLabel.addMouseListener(new MouseAdapter() {\r
+                       public void mouseClicked (MouseEvent evt) {\r
+                               java.awt.Color color = JColorChooser.showDialog(null, "Choose a background color",\r
+                                       EffectUtil.fromString(prefs.get("background", "000000")));\r
+                               if (color == null) return;\r
+                               renderingBackgroundColor = new Color(color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1);\r
+                               backgroundColorLabel.setIcon(getColorIcon(color));\r
+                               prefs.put("background", EffectUtil.toString(color));\r
+                       }\r
+               });\r
+\r
+               effectsList.addListSelectionListener(new ListSelectionListener() {\r
+                       public void valueChanged (ListSelectionEvent evt) {\r
+                               ConfigurableEffect selectedEffect = (ConfigurableEffect)effectsList.getSelectedValue();\r
+                               boolean enabled = selectedEffect != null;\r
+                               for (Iterator iter = effectPanels.iterator(); iter.hasNext();) {\r
+                                       ConfigurableEffect effect = ((EffectPanel)iter.next()).getEffect();\r
+                                       if (effect == selectedEffect) {\r
+                                               enabled = false;\r
+                                               break;\r
+                                       }\r
+                               }\r
+                               addEffectButton.setEnabled(enabled);\r
+                       }\r
+               });\r
+\r
+               effectsList.addMouseListener(new MouseAdapter() {\r
+                       public void mouseClicked (MouseEvent evt) {\r
+                               if (evt.getClickCount() == 2 && addEffectButton.isEnabled()) addEffectButton.doClick();\r
+                       }\r
+               });\r
+\r
+               addEffectButton.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               new EffectPanel((ConfigurableEffect)effectsList.getSelectedValue());\r
+                       }\r
+               });\r
+\r
+               openMenuItem.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               FileDialog dialog = new FileDialog(Hiero.this, "Open Hiero settings file", FileDialog.LOAD);\r
+                               dialog.setLocationRelativeTo(null);\r
+                               dialog.setFile("*.hiero");\r
+                               dialog.setVisible(true);\r
+                               String fileName = dialog.getFile();\r
+                               if (fileName == null) return;\r
+                               open(new File(dialog.getDirectory(), fileName));\r
+                       }\r
+               });\r
+\r
+               saveMenuItem.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               FileDialog dialog = new FileDialog(Hiero.this, "Save Hiero settings file", FileDialog.SAVE);\r
+                               dialog.setLocationRelativeTo(null);\r
+                               dialog.setFile("*.hiero");\r
+                               dialog.setVisible(true);\r
+                               String fileName = dialog.getFile();\r
+                               if (fileName == null) return;\r
+                               File file = new File(dialog.getDirectory(), fileName);\r
+                               try {\r
+                                       save(file);\r
+                               } catch (IOException ex) {\r
+                                       throw new RuntimeException("Error saving Hiero settings file: " + file.getAbsolutePath(), ex);\r
+                               }\r
+                       }\r
+               });\r
+\r
+               saveBMFontMenuItem.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               FileDialog dialog = new FileDialog(Hiero.this, "Save BMFont files", FileDialog.SAVE);\r
+                               dialog.setLocationRelativeTo(null);\r
+                               dialog.setFile("*.fnt");\r
+                               dialog.setVisible(true);\r
+                               String fileName = dialog.getFile();\r
+                               if (fileName == null) return;\r
+                               saveBmFontFile = new File(dialog.getDirectory(), fileName);\r
+                       }\r
+               });\r
+\r
+               exitMenuItem.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               dispose();\r
+                       }\r
+               });\r
+\r
+               sampleNeheButton.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               sampleTextPane.setText(NEHE);\r
+                       }\r
+               });\r
+\r
+               sampleAsciiButton.addActionListener(new ActionListener() {\r
+                       public void actionPerformed (ActionEvent evt) {\r
+                               StringBuilder buffer = new StringBuilder();\r
+                               buffer.append(NEHE);\r
+                               buffer.append('\n');\r
+                               int count = 0;\r
+                               for (int i = 33; i <= 255; i++) {\r
+                                       if (buffer.indexOf(Character.toString((char)i)) != -1) continue;\r
+                                       buffer.append((char)i);\r
+                                       if (++count % 30 == 0) buffer.append('\n');\r
+                               }\r
+                               sampleTextPane.setText(buffer.toString());\r
+                       }\r
+               });\r
+       }\r
+\r
+       private void initializeComponents () {\r
+               getContentPane().setLayout(new GridBagLayout());\r
+               JPanel leftSidePanel = new JPanel();\r
+               leftSidePanel.setLayout(new GridBagLayout());\r
+               getContentPane().add(\r
+                       leftSidePanel,\r
+                       new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0),\r
+                               0, 0));\r
+               {\r
+                       JPanel fontPanel = new JPanel();\r
+                       leftSidePanel.add(fontPanel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                               GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));\r
+                       fontPanel.setLayout(new GridBagLayout());\r
+                       fontPanel.setBorder(BorderFactory.createTitledBorder("Font"));\r
+                       {\r
+                               fontSizeSpinner = new JSpinner(new SpinnerNumberModel(32, 0, 256, 1));\r
+                               fontPanel.add(fontSizeSpinner, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                               ((JSpinner.DefaultEditor)fontSizeSpinner.getEditor()).getTextField().setColumns(2);\r
+                       }\r
+                       {\r
+                               JScrollPane fontScroll = new JScrollPane();\r
+                               fontPanel.add(fontScroll, new GridBagConstraints(1, 1, 4, 1, 1.0, 1.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.BOTH, new Insets(0, 0, 5, 5), 0, 0));\r
+                               {\r
+                                       fontListModel = new DefaultComboBoxModel(GraphicsEnvironment.getLocalGraphicsEnvironment()\r
+                                               .getAvailableFontFamilyNames());\r
+                                       fontList = new JList();\r
+                                       fontScroll.setViewportView(fontList);\r
+                                       fontList.setModel(fontListModel);\r
+                                       fontList.setVisibleRowCount(6);\r
+                                       fontList.setSelectedIndex(0);\r
+                                       fontScroll.setMinimumSize(new Dimension(220, fontList.getPreferredScrollableViewportSize().height));\r
+                               }\r
+                       }\r
+                       {\r
+                               systemFontRadio = new JRadioButton("System:", true);\r
+                               fontPanel.add(systemFontRadio, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 5, 0, 5), 0, 0));\r
+                               systemFontRadio.setMargin(new Insets(0, 0, 0, 0));\r
+                       }\r
+                       {\r
+                               fontFileRadio = new JRadioButton("File:");\r
+                               fontPanel.add(fontFileRadio, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               fontFileRadio.setMargin(new Insets(0, 0, 0, 0));\r
+                       }\r
+                       {\r
+                               fontFileText = new JTextField();\r
+                               fontPanel.add(fontFileText, new GridBagConstraints(1, 2, 3, 1, 1.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));\r
+                       }\r
+                       {\r
+                               fontPanel.add(new JLabel("Size:"), new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                       }\r
+                       {\r
+                               boldCheckBox = new JCheckBox("Bold");\r
+                               fontPanel.add(boldCheckBox, new GridBagConstraints(2, 3, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                       }\r
+                       {\r
+                               italicCheckBox = new JCheckBox("Italic");\r
+                               fontPanel.add(italicCheckBox, new GridBagConstraints(3, 3, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                       }\r
+                       {\r
+                               browseButton = new JButton("...");\r
+                               fontPanel.add(browseButton, new GridBagConstraints(4, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                               browseButton.setMargin(new Insets(0, 0, 0, 0));\r
+                       }\r
+                       ButtonGroup buttonGroup = new ButtonGroup();\r
+                       buttonGroup.add(systemFontRadio);\r
+                       buttonGroup.add(fontFileRadio);\r
+               }\r
+               {\r
+                       JPanel samplePanel = new JPanel();\r
+                       leftSidePanel.add(samplePanel, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,\r
+                               GridBagConstraints.BOTH, new Insets(5, 0, 5, 5), 0, 0));\r
+                       samplePanel.setLayout(new GridBagLayout());\r
+                       samplePanel.setBorder(BorderFactory.createTitledBorder("Sample Text"));\r
+                       {\r
+                               JScrollPane textScroll = new JScrollPane();\r
+                               samplePanel.add(textScroll, new GridBagConstraints(0, 0, 3, 1, 1.0, 1.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.BOTH, new Insets(0, 5, 5, 5), 0, 0));\r
+                               {\r
+                                       sampleTextPane = new JTextPane();\r
+                                       textScroll.setViewportView(sampleTextPane);\r
+                               }\r
+                       }\r
+                       {\r
+                               sampleNeheButton = new JButton();\r
+                               sampleNeheButton.setText("NEHE");\r
+                               samplePanel.add(sampleNeheButton, new GridBagConstraints(2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                       }\r
+                       {\r
+                               sampleAsciiButton = new JButton();\r
+                               sampleAsciiButton.setText("ASCII");\r
+                               samplePanel.add(sampleAsciiButton, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.EAST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                       }\r
+               }\r
+               {\r
+                       JPanel renderingPanel = new JPanel();\r
+                       leftSidePanel.add(renderingPanel, new GridBagConstraints(0, 1, 2, 1, 1.0, 1.0, GridBagConstraints.CENTER,\r
+                               GridBagConstraints.BOTH, new Insets(0, 5, 5, 5), 0, 0));\r
+                       renderingPanel.setBorder(BorderFactory.createTitledBorder("Rendering"));\r
+                       renderingPanel.setLayout(new GridBagLayout());\r
+                       {\r
+                               JPanel wrapperPanel = new JPanel();\r
+                               renderingPanel.add(wrapperPanel, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.BOTH, new Insets(0, 5, 5, 5), 0, 0));\r
+                               wrapperPanel.setLayout(new BorderLayout());\r
+                               wrapperPanel.setBackground(java.awt.Color.white);\r
+                               {\r
+                                       gamePanel = new JPanel();\r
+                                       wrapperPanel.add(gamePanel);\r
+                                       gamePanel.setLayout(new BorderLayout());\r
+                                       gamePanel.setBackground(java.awt.Color.white);\r
+                               }\r
+                       }\r
+                       {\r
+                               glyphCachePanel = new JPanel() {\r
+                                       private int maxWidth;\r
+\r
+                                       public Dimension getPreferredSize () {\r
+                                               // Keep glyphCachePanel width from ever going down so the CanvasGameContainer doesn't change sizes and flicker.\r
+                                               Dimension size = super.getPreferredSize();\r
+                                               maxWidth = Math.max(maxWidth, size.width);\r
+                                               size.width = maxWidth;\r
+                                               return size;\r
+                                       }\r
+                               };\r
+                               glyphCachePanel.setVisible(false);\r
+                               renderingPanel.add(glyphCachePanel, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.NORTH,\r
+                                       GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));\r
+                               glyphCachePanel.setLayout(new GridBagLayout());\r
+                               {\r
+                                       glyphCachePanel.add(new JLabel("Glyphs:"), new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       glyphCachePanel.add(new JLabel("Pages:"), new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       glyphCachePanel.add(new JLabel("Page width:"), new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,\r
+                                               GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       glyphCachePanel.add(new JLabel("Page height:"), new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,\r
+                                               GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       glyphPageWidthCombo = new JComboBox(new DefaultComboBoxModel(new Integer[] {new Integer(256), new Integer(512),\r
+                                               new Integer(1024), new Integer(2048)}));\r
+                                       glyphCachePanel.add(glyphPageWidthCombo, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                                       glyphPageWidthCombo.setSelectedIndex(1);\r
+                               }\r
+                               {\r
+                                       glyphPageHeightCombo = new JComboBox(new DefaultComboBoxModel(new Integer[] {new Integer(256), new Integer(512),\r
+                                               new Integer(1024), new Integer(2048)}));\r
+                                       glyphCachePanel.add(glyphPageHeightCombo, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                                       glyphPageHeightCombo.setSelectedIndex(1);\r
+                               }\r
+                               {\r
+                                       resetCacheButton = new JButton("Reset Cache");\r
+                                       glyphCachePanel.add(resetCacheButton, new GridBagConstraints(0, 6, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER,\r
+                                               GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       glyphPagesTotalLabel = new JLabel("1");\r
+                                       glyphCachePanel.add(glyphPagesTotalLabel, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       glyphsTotalLabel = new JLabel("0");\r
+                                       glyphCachePanel.add(glyphsTotalLabel, new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       glyphPageComboModel = new DefaultComboBoxModel();\r
+                                       glyphPageCombo = new JComboBox();\r
+                                       glyphCachePanel.add(glyphPageCombo, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                                       glyphPageCombo.setModel(glyphPageComboModel);\r
+                               }\r
+                               {\r
+                                       glyphCachePanel.add(new JLabel("View:"), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               }\r
+                       }\r
+                       {\r
+                               JPanel radioButtonsPanel = new JPanel();\r
+                               renderingPanel.add(radioButtonsPanel, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));\r
+                               radioButtonsPanel.setLayout(new GridBagLayout());\r
+                               {\r
+                                       sampleTextRadio = new JRadioButton("Sample text");\r
+                                       radioButtonsPanel.add(sampleTextRadio, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                                       sampleTextRadio.setSelected(true);\r
+                               }\r
+                               {\r
+                                       glyphCacheRadio = new JRadioButton("Glyph cache");\r
+                                       radioButtonsPanel.add(glyphCacheRadio, new GridBagConstraints(3, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       radioButtonsPanel.add(new JLabel("Background:"), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,\r
+                                               GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));\r
+                               }\r
+                               {\r
+                                       backgroundColorLabel = new JLabel();\r
+                                       radioButtonsPanel.add(backgroundColorLabel, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,\r
+                                               GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));\r
+                               }\r
+                               ButtonGroup buttonGroup = new ButtonGroup();\r
+                               buttonGroup.add(glyphCacheRadio);\r
+                               buttonGroup.add(sampleTextRadio);\r
+                       }\r
+               }\r
+               JPanel rightSidePanel = new JPanel();\r
+               rightSidePanel.setLayout(new GridBagLayout());\r
+               getContentPane().add(\r
+                       rightSidePanel,\r
+                       new GridBagConstraints(1, 0, 1, 2, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0),\r
+                               0, 0));\r
+               {\r
+                       JPanel paddingPanel = new JPanel();\r
+                       paddingPanel.setLayout(new GridBagLayout());\r
+                       rightSidePanel.add(paddingPanel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                               GridBagConstraints.BOTH, new Insets(0, 0, 5, 5), 0, 0));\r
+                       paddingPanel.setBorder(BorderFactory.createTitledBorder("Padding"));\r
+                       {\r
+                               padTopSpinner = new JSpinner();\r
+                               paddingPanel.add(padTopSpinner, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));\r
+                               ((JSpinner.DefaultEditor)padTopSpinner.getEditor()).getTextField().setColumns(2);\r
+                       }\r
+                       {\r
+                               padRightSpinner = new JSpinner();\r
+                               paddingPanel.add(padRightSpinner, new GridBagConstraints(2, 2, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0));\r
+                               ((JSpinner.DefaultEditor)padRightSpinner.getEditor()).getTextField().setColumns(2);\r
+                       }\r
+                       {\r
+                               padLeftSpinner = new JSpinner();\r
+                               paddingPanel.add(padLeftSpinner, new GridBagConstraints(0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.EAST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 5, 0, 0), 0, 0));\r
+                               ((JSpinner.DefaultEditor)padLeftSpinner.getEditor()).getTextField().setColumns(2);\r
+                       }\r
+                       {\r
+                               padBottomSpinner = new JSpinner();\r
+                               paddingPanel.add(padBottomSpinner, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));\r
+                               ((JSpinner.DefaultEditor)padBottomSpinner.getEditor()).getTextField().setColumns(2);\r
+                       }\r
+                       {\r
+                               JPanel advancePanel = new JPanel();\r
+                               FlowLayout advancePanelLayout = new FlowLayout();\r
+                               advancePanel.setLayout(advancePanelLayout);\r
+                               paddingPanel.add(advancePanel, new GridBagConstraints(0, 4, 3, 1, 1.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));\r
+                               {\r
+                                       advancePanel.add(new JLabel("X:"));\r
+                               }\r
+                               {\r
+                                       padAdvanceXSpinner = new JSpinner();\r
+                                       advancePanel.add(padAdvanceXSpinner);\r
+                                       ((JSpinner.DefaultEditor)padAdvanceXSpinner.getEditor()).getTextField().setColumns(2);\r
+                               }\r
+                               {\r
+                                       advancePanel.add(new JLabel("Y:"));\r
+                               }\r
+                               {\r
+                                       padAdvanceYSpinner = new JSpinner();\r
+                                       advancePanel.add(padAdvanceYSpinner);\r
+                                       ((JSpinner.DefaultEditor)padAdvanceYSpinner.getEditor()).getTextField().setColumns(2);\r
+                               }\r
+                       }\r
+               }\r
+               {\r
+                       JPanel effectsPanel = new JPanel();\r
+                       effectsPanel.setLayout(new GridBagLayout());\r
+                       rightSidePanel.add(effectsPanel, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,\r
+                               GridBagConstraints.BOTH, new Insets(5, 0, 5, 5), 0, 0));\r
+                       effectsPanel.setBorder(BorderFactory.createTitledBorder("Effects"));\r
+                       effectsPanel.setMinimumSize(new Dimension(210, 1));\r
+                       {\r
+                               JScrollPane effectsScroll = new JScrollPane();\r
+                               effectsPanel.add(effectsScroll, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.NORTH,\r
+                                       GridBagConstraints.HORIZONTAL, new Insets(0, 5, 5, 5), 0, 0));\r
+                               {\r
+                                       effectsListModel = new DefaultComboBoxModel();\r
+                                       effectsList = new JList();\r
+                                       effectsScroll.setViewportView(effectsList);\r
+                                       effectsList.setModel(effectsListModel);\r
+                                       effectsList.setVisibleRowCount(6);\r
+                                       effectsScroll.setMinimumSize(effectsList.getPreferredScrollableViewportSize());\r
+                               }\r
+                       }\r
+                       {\r
+                               addEffectButton = new JButton("Add");\r
+                               effectsPanel.add(addEffectButton, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,\r
+                                       GridBagConstraints.NONE, new Insets(0, 5, 6, 5), 0, 0));\r
+                               addEffectButton.setEnabled(false);\r
+                       }\r
+                       {\r
+                               appliedEffectsScroll = new JScrollPane();\r
+                               effectsPanel.add(appliedEffectsScroll, new GridBagConstraints(1, 3, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH,\r
+                                       GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));\r
+                               appliedEffectsScroll.setBorder(new EmptyBorder(0, 0, 0, 0));\r
+                               appliedEffectsScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);\r
+                               {\r
+                                       JPanel panel = new JPanel();\r
+                                       panel.setLayout(new GridBagLayout());\r
+                                       appliedEffectsScroll.setViewportView(panel);\r
+                                       {\r
+                                               appliedEffectsPanel = new JPanel();\r
+                                               appliedEffectsPanel.setLayout(new GridBagLayout());\r
+                                               panel.add(appliedEffectsPanel, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH,\r
+                                                       GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));\r
+                                               appliedEffectsPanel.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, java.awt.Color.black));\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+\r
+       private void initializeMenus () {\r
+               {\r
+                       JMenuBar menuBar = new JMenuBar();\r
+                       setJMenuBar(menuBar);\r
+                       {\r
+                               JMenu fileMenu = new JMenu();\r
+                               menuBar.add(fileMenu);\r
+                               fileMenu.setText("File");\r
+                               fileMenu.setMnemonic(KeyEvent.VK_F);\r
+                               {\r
+                                       openMenuItem = new JMenuItem("Open Hiero settings file...");\r
+                                       openMenuItem.setMnemonic(KeyEvent.VK_O);\r
+                                       openMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_MASK));\r
+                                       fileMenu.add(openMenuItem);\r
+                               }\r
+                               {\r
+                                       saveMenuItem = new JMenuItem("Save Hiero settings file...");\r
+                                       saveMenuItem.setMnemonic(KeyEvent.VK_S);\r
+                                       saveMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK));\r
+                                       fileMenu.add(saveMenuItem);\r
+                               }\r
+                               fileMenu.addSeparator();\r
+                               {\r
+                                       saveBMFontMenuItem = new JMenuItem("Save BMFont files (text)...");\r
+                                       saveBMFontMenuItem.setMnemonic(KeyEvent.VK_B);\r
+                                       saveBMFontMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_MASK));\r
+                                       fileMenu.add(saveBMFontMenuItem);\r
+                               }\r
+                               fileMenu.addSeparator();\r
+                               {\r
+                                       exitMenuItem = new JMenuItem("Exit");\r
+                                       exitMenuItem.setMnemonic(KeyEvent.VK_X);\r
+                                       fileMenu.add(exitMenuItem);\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+\r
+       static Icon getColorIcon (java.awt.Color color) {\r
+               BufferedImage image = new BufferedImage(32, 16, BufferedImage.TYPE_INT_RGB);\r
+               java.awt.Graphics g = image.getGraphics();\r
+               g.setColor(color);\r
+               g.fillRect(1, 1, 30, 14);\r
+               g.setColor(java.awt.Color.black);\r
+               g.drawRect(0, 0, 31, 15);\r
+               return new ImageIcon(image);\r
+       }\r
+\r
+       private class EffectPanel extends JPanel {\r
+               final java.awt.Color selectedColor = new java.awt.Color(0xb1d2e9);\r
+\r
+               final ConfigurableEffect effect;\r
+               List values;\r
+\r
+               JButton deleteButton;\r
+               private JPanel valuesPanel;\r
+               JLabel nameLabel;\r
+\r
+               EffectPanel (final ConfigurableEffect effect) {\r
+                       this.effect = effect;\r
+                       effectPanels.add(this);\r
+                       effectsList.getListSelectionListeners()[0].valueChanged(null);\r
+\r
+                       setLayout(new GridBagLayout());\r
+                       setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, java.awt.Color.black));\r
+                       appliedEffectsPanel.add(this, new GridBagConstraints(0, -1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,\r
+                               GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));\r
+                       {\r
+                               JPanel titlePanel = new JPanel();\r
+                               titlePanel.setLayout(new LayoutManager() {\r
+                                       public void removeLayoutComponent (Component comp) {\r
+                                       }\r
+\r
+                                       public Dimension preferredLayoutSize (Container parent) {\r
+                                               return null;\r
+                                       }\r
+\r
+                                       public Dimension minimumLayoutSize (Container parent) {\r
+                                               return null;\r
+                                       }\r
+\r
+                                       public void layoutContainer (Container parent) {\r
+                                               Dimension buttonSize = deleteButton.getPreferredSize();\r
+                                               deleteButton.setBounds(getWidth() - buttonSize.width - 5, 0, buttonSize.width, buttonSize.height);\r
+\r
+                                               Dimension labelSize = nameLabel.getPreferredSize();\r
+                                               nameLabel.setBounds(5, buttonSize.height / 2 - labelSize.height / 2, getWidth() - buttonSize.width - 5 - 5,\r
+                                                       labelSize.height);\r
+                                       }\r
+\r
+                                       public void addLayoutComponent (String name, Component comp) {\r
+                                       }\r
+                               });\r
+                               {\r
+                                       deleteButton = new JButton();\r
+                                       titlePanel.add(deleteButton);\r
+                                       deleteButton.setText("X");\r
+                                       deleteButton.setMargin(new Insets(0, 0, 0, 0));\r
+                                       Font font = deleteButton.getFont();\r
+                                       deleteButton.setFont(new Font(font.getName(), font.getStyle(), font.getSize() - 2));\r
+                               }\r
+                               {\r
+                                       nameLabel = new JLabel(effect.toString());\r
+                                       titlePanel.add(nameLabel);\r
+                                       Font font = nameLabel.getFont();\r
+                                       nameLabel.setFont(new Font(font.getName(), Font.BOLD, font.getSize()));\r
+                               }\r
+                               titlePanel.setPreferredSize(new Dimension(0, Math.max(nameLabel.getPreferredSize().height,\r
+                                       deleteButton.getPreferredSize().height)));\r
+                               add(titlePanel, new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,\r
+                                       new Insets(5, 0, 0, 5), 0, 0));\r
+                               titlePanel.setOpaque(false);\r
+                       }\r
+                       {\r
+                               valuesPanel = new JPanel();\r
+                               valuesPanel.setOpaque(false);\r
+                               valuesPanel.setLayout(new GridBagLayout());\r
+                               add(valuesPanel, new GridBagConstraints(0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.HORIZONTAL, new Insets(0, 10, 5, 0), 0, 0));\r
+                       }\r
+\r
+                       deleteButton.addActionListener(new ActionListener() {\r
+                               public void actionPerformed (ActionEvent evt) {\r
+                                       remove();\r
+                                       updateFont();\r
+                               }\r
+                       });\r
+\r
+                       updateValues();\r
+                       updateFont();\r
+               }\r
+\r
+               public void remove () {\r
+                       effectPanels.remove(this);\r
+                       appliedEffectsPanel.remove(EffectPanel.this);\r
+                       getContentPane().validate();\r
+                       effectsList.getListSelectionListeners()[0].valueChanged(null);\r
+               }\r
+\r
+               public void updateValues () {\r
+                       prefs.put("foreground", EffectUtil.toString(colorEffect.getColor()));\r
+                       valuesPanel.removeAll();\r
+                       values = effect.getValues();\r
+                       for (Iterator iter = values.iterator(); iter.hasNext();)\r
+                               addValue((Value)iter.next());\r
+               }\r
+\r
+               public void addValue (final Value value) {\r
+                       JLabel valueNameLabel = new JLabel(value.getName() + ":");\r
+                       valuesPanel.add(valueNameLabel, new GridBagConstraints(0, -1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,\r
+                               GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0));\r
+\r
+                       final JLabel valueValueLabel = new JLabel();\r
+                       valuesPanel.add(valueValueLabel, new GridBagConstraints(1, -1, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,\r
+                               GridBagConstraints.BOTH, new Insets(0, 0, 0, 5), 0, 0));\r
+                       valueValueLabel.setOpaque(true);\r
+                       if (value.getObject() instanceof java.awt.Color)\r
+                               valueValueLabel.setIcon(getColorIcon((java.awt.Color)value.getObject()));\r
+                       else\r
+                               valueValueLabel.setText(value.toString());\r
+\r
+                       valueValueLabel.addMouseListener(new MouseAdapter() {\r
+                               public void mouseEntered (MouseEvent evt) {\r
+                                       valueValueLabel.setBackground(selectedColor);\r
+                               }\r
+\r
+                               public void mouseExited (MouseEvent evt) {\r
+                                       valueValueLabel.setBackground(null);\r
+                               }\r
+\r
+                               public void mouseClicked (MouseEvent evt) {\r
+                                       Object oldObject = value.getObject();\r
+                                       value.showDialog();\r
+                                       if (!value.getObject().equals(oldObject)) {\r
+                                               effect.setValues(values);\r
+                                               updateValues();\r
+                                               updateFont();\r
+                                       }\r
+                               }\r
+                       });\r
+               }\r
+\r
+               public ConfigurableEffect getEffect () {\r
+                       return effect;\r
+               }\r
+\r
+               public boolean equals (Object obj) {\r
+                       if (this == obj) return true;\r
+                       if (obj == null) return false;\r
+                       if (getClass() != obj.getClass()) return false;\r
+                       final EffectPanel other = (EffectPanel)obj;\r
+                       if (effect == null) {\r
+                               if (other.effect != null) return false;\r
+                       } else if (!effect.equals(other.effect)) return false;\r
+                       return true;\r
+               }\r
+       }\r
+\r
+       static private class Splash extends JWindow {\r
+               final int minMillis;\r
+               final long startTime;\r
+\r
+               public Splash (Frame frame, String imageFile, int minMillis) {\r
+                       super(frame);\r
+                       this.minMillis = minMillis;\r
+                       getContentPane().add(new JLabel(new ImageIcon(Splash.class.getResource(imageFile))), BorderLayout.CENTER);\r
+                       pack();\r
+                       setLocationRelativeTo(null);\r
+                       setVisible(true);\r
+                       startTime = System.currentTimeMillis();\r
+               }\r
+\r
+               public void close () {\r
+                       final long endTime = System.currentTimeMillis();\r
+                       new Thread(new Runnable() {\r
+                               public void run () {\r
+                                       if (endTime - startTime < minMillis) {\r
+                                               addMouseListener(new MouseAdapter() {\r
+                                                       public void mousePressed (MouseEvent evt) {\r
+                                                               dispose();\r
+                                                       }\r
+                                               });\r
+                                               try {\r
+                                                       Thread.sleep(minMillis - (endTime - startTime));\r
+                                               } catch (InterruptedException ignored) {\r
+                                               }\r
+                                       }\r
+                                       EventQueue.invokeLater(new Runnable() {\r
+                                               public void run () {\r
+                                                       dispose();\r
+                                               }\r
+                                       });\r
+                               }\r
+                       }, "Splash").start();\r
+               }\r
+       }\r
+\r
+       class Renderer implements RenderListener {\r
+               private String sampleText;\r
+\r
+               public void surfaceCreated () {\r
+                       glEnable(GL_SCISSOR_TEST);\r
+\r
+                       glEnable(GL_TEXTURE_2D);\r
+                       glEnableClientState(GL_TEXTURE_COORD_ARRAY);\r
+                       glEnableClientState(GL_VERTEX_ARRAY);\r
+\r
+                       glClearColor(0, 0, 0, 0);\r
+                       glClearDepth(1);\r
+\r
+                       glDisable(GL_LIGHTING);\r
+\r
+                       glEnable(GL_BLEND);\r
+                       glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\r
+               }\r
+\r
+               public void surfaceChanged (int width, int height) {\r
+                       glViewport(0, 0, width, height);\r
+                       glScissor(0, 0, width, height);\r
+\r
+                       glMatrixMode(GL_PROJECTION);\r
+                       glLoadIdentity();\r
+                       glOrtho(0, width, height, 0, 1, -1);\r
+                       glMatrixMode(GL_MODELVIEW);\r
+                       glLoadIdentity();\r
+               }\r
+\r
+               public void render () {\r
+                       if (glCanvas == null) return;\r
+                       int viewWidth = Gdx.graphics.getWidth();\r
+                       int viewHeight = Gdx.graphics.getHeight();\r
+                       if (viewWidth != glCanvas.getWidth() || viewHeight != glCanvas.getHeight()) {\r
+                               viewWidth = Math.max(1, glCanvas.getWidth());\r
+                               viewHeight = Math.max(1, glCanvas.getHeight());\r
+                               app.setSize(viewWidth, viewHeight);\r
+                       }\r
+\r
+                       if (newUnicodeFont != null) {\r
+                               if (unicodeFont != null) unicodeFont.destroy();\r
+                               unicodeFont = newUnicodeFont;\r
+                               newUnicodeFont = null;\r
+                       }\r
+\r
+                       // BOZO - Fix no effects.\r
+                       if (unicodeFont.loadGlyphs(25)) {\r
+                               glyphPageComboModel.removeAllElements();\r
+                               int pageCount = unicodeFont.getGlyphPages().size();\r
+                               int glyphCount = 0;\r
+                               for (int i = 0; i < pageCount; i++) {\r
+                                       glyphPageComboModel.addElement("Page " + (i + 1));\r
+                                       glyphCount += ((GlyphPage)unicodeFont.getGlyphPages().get(i)).getGlyphs().size();\r
+                               }\r
+                               glyphPagesTotalLabel.setText(String.valueOf(pageCount));\r
+                               glyphsTotalLabel.setText(String.valueOf(glyphCount));\r
+                       }\r
+\r
+                       if (saveBmFontFile != null) {\r
+                               try {\r
+                                       BMFontUtil bmFont = new BMFontUtil(unicodeFont);\r
+                                       bmFont.save(saveBmFontFile);\r
+                               } catch (Throwable ex) {\r
+                                       System.out.println("Error saving BMFont files: " + saveBmFontFile.getAbsolutePath());\r
+                                       ex.printStackTrace();\r
+                               } finally {\r
+                                       saveBmFontFile = null;\r
+                               }\r
+                       }\r
+\r
+                       if (unicodeFont == null) return;\r
+\r
+                       try {\r
+                               sampleText = sampleTextPane.getText();\r
+                       } catch (Exception ex) {\r
+                       }\r
+\r
+                       if (sampleTextRadio.isSelected()) {\r
+                               GL11.glColor4f(renderingBackgroundColor.r, renderingBackgroundColor.g, renderingBackgroundColor.b,\r
+                                       renderingBackgroundColor.a);\r
+                               GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);\r
+                               int offset = unicodeFont.getYOffset(sampleText);\r
+                               if (offset > 0) offset = 0;\r
+                               unicodeFont.drawString(0, -offset, sampleText, Color.WHITE, 0, sampleText.length());\r
+                       } else {\r
+                               GL11.glColor4f(1, 1, 1, 1);\r
+                               GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);\r
+                               unicodeFont.addGlyphs(sampleText);\r
+                               // GL11.glColor4f(renderingBackgroundColor.r, renderingBackgroundColor.g, renderingBackgroundColor.b,\r
+                               // renderingBackgroundColor.a);\r
+                               // fillRect(0, 0, unicodeFont.getGlyphPageWidth() + 2, unicodeFont.getGlyphPageHeight() + 2);\r
+                               int index = glyphPageCombo.getSelectedIndex();\r
+                               List pages = unicodeFont.getGlyphPages();\r
+                               if (index >= 0 && index < pages.size()) {\r
+                                       Texture texture = ((GlyphPage)pages.get(glyphPageCombo.getSelectedIndex())).getTexture();\r
+                                       GL11.glBegin(GL11.GL_QUADS);\r
+                                       GL11.glTexCoord2f(0, 0);\r
+                                       GL11.glVertex3f(0, 0, 0);\r
+                                       GL11.glTexCoord2f(0, 1);\r
+                                       GL11.glVertex3f(0, texture.getHeight(), 0);\r
+                                       GL11.glTexCoord2f(1, 1);\r
+                                       GL11.glVertex3f(texture.getWidth(), texture.getHeight(), 0);\r
+                                       GL11.glTexCoord2f(1, 0);\r
+                                       GL11.glVertex3f(texture.getWidth(), 0, 0);\r
+                                       GL11.glEnd();\r
+                               }\r
+                       }\r
+               }\r
+\r
+               public void dispose () {\r
+               }\r
+       }\r
+\r
+       public static void main (String[] args) throws Exception {\r
+               LookAndFeelInfo[] lookAndFeels = UIManager.getInstalledLookAndFeels();\r
+               for (int i = 0, n = lookAndFeels.length; i < n; i++) {\r
+                       if ("Nimbus".equals(lookAndFeels[i].getName())) {\r
+                               try {\r
+                                       UIManager.setLookAndFeel(lookAndFeels[i].getClassName());\r
+                               } catch (Throwable ignored) {\r
+                               }\r
+                               break;\r
+                       }\r
+               }\r
+               new Hiero();\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/Kerning.java b/extensions/hiero/src/com/badlogic/gdx/hiero/Kerning.java
new file mode 100644 (file)
index 0000000..09e20c9
--- /dev/null
@@ -0,0 +1,211 @@
+\r
+package com.badlogic.gdx.hiero;\r
+\r
+import java.awt.font.GlyphVector;\r
+import java.io.EOFException;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.HashMap;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+import java.util.ListIterator;\r
+import java.util.Map;\r
+import java.util.Map.Entry;\r
+\r
+/**\r
+ * Reads a TTF font file and provides access to kerning information.\r
+ * \r
+ * Thanks to the Apache FOP project for their inspiring work!\r
+ * \r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+class Kerning {\r
+       private Map values = Collections.EMPTY_MAP;\r
+       private int size = -1;\r
+       private int kerningPairCount = -1;\r
+       private float scale;\r
+       private long bytePosition;\r
+       private long headOffset = -1;\r
+       private long kernOffset = -1;\r
+\r
+       /**\r
+        * @param input The data for the TTF font.\r
+        * @param size The font size to use to determine kerning pixel offsets.\r
+        * @throws IOException If the font could not be read.\r
+        */\r
+       public void load (InputStream input, int size) throws IOException {\r
+               this.size = size;\r
+               if (input == null) throw new IllegalArgumentException("input cannot be null.");\r
+               readTableDirectory(input);\r
+               if (headOffset == -1) throw new IOException("HEAD table not found.");\r
+               if (kernOffset == -1) {\r
+                       values = Collections.EMPTY_MAP;\r
+                       return;\r
+               }\r
+               values = new HashMap(256);\r
+               if (headOffset < kernOffset) {\r
+                       readHEAD(input);\r
+                       readKERN(input);\r
+               } else {\r
+                       readKERN(input);\r
+                       readHEAD(input);\r
+               }\r
+               input.close();\r
+\r
+               for (Iterator entryIter = values.entrySet().iterator(); entryIter.hasNext();) {\r
+                       Entry entry = (Entry)entryIter.next();\r
+                       // Scale the offset values using the font size.\r
+                       List valueList = (List)entry.getValue();\r
+                       for (ListIterator valueIter = valueList.listIterator(); valueIter.hasNext();) {\r
+                               int value = ((Integer)valueIter.next()).intValue();\r
+                               int glyphCode = value & 0xffff;\r
+                               int offset = value >> 16;\r
+                               offset = Math.round(offset * scale);\r
+                               if (offset == 0)\r
+                                       valueIter.remove();\r
+                               else\r
+                                       valueIter.set(new Integer((offset << 16) | glyphCode));\r
+                       }\r
+                       if (valueList.isEmpty()) {\r
+                               entryIter.remove();\r
+                       } else {\r
+                               // Replace ArrayList with int[].\r
+                               int[] valueArray = new int[valueList.size()];\r
+                               int i = 0;\r
+                               for (Iterator valueIter = valueList.iterator(); valueIter.hasNext(); i++)\r
+                                       valueArray[i] = ((Integer)valueIter.next()).intValue();\r
+                               entry.setValue(valueArray);\r
+                               kerningPairCount += valueArray.length;\r
+                       }\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Returns the encoded kerning value for the specified glyph. The glyph code for a Unicode codepoint can be retrieved with\r
+        * {@link GlyphVector#getGlyphCode(int)}.\r
+        */\r
+       public int[] getValues (int firstGlyphCode) {\r
+               return (int[])values.get(new Integer(firstGlyphCode));\r
+       }\r
+\r
+       public int getKerning (int[] values, int otherGlyphCode) {\r
+               int low = 0;\r
+               int high = values.length - 1;\r
+               while (low <= high) {\r
+                       int midIndex = (low + high) >>> 1;\r
+                       int value = values[midIndex];\r
+                       int foundGlyphCode = value & 0xffff;\r
+                       if (foundGlyphCode < otherGlyphCode)\r
+                               low = midIndex + 1;\r
+                       else if (foundGlyphCode > otherGlyphCode)\r
+                               high = midIndex - 1;\r
+                       else\r
+                               return value >> 16;\r
+               }\r
+               return 0;\r
+       }\r
+\r
+       public int getCount () {\r
+               return kerningPairCount;\r
+       }\r
+\r
+       private void readTableDirectory (InputStream input) throws IOException {\r
+               skip(input, 4);\r
+               int tableCount = readUnsignedShort(input);\r
+               skip(input, 6);\r
+\r
+               byte[] tagBytes = new byte[4];\r
+               for (int i = 0; i < tableCount; i++) {\r
+                       tagBytes[0] = readByte(input);\r
+                       tagBytes[1] = readByte(input);\r
+                       tagBytes[2] = readByte(input);\r
+                       tagBytes[3] = readByte(input);\r
+                       skip(input, 4);\r
+                       long offset = readUnsignedLong(input);\r
+                       skip(input, 4);\r
+\r
+                       String tag = new String(tagBytes, "ISO-8859-1");\r
+                       if (tag.equals("head")) {\r
+                               headOffset = offset;\r
+                               if (kernOffset != -1) break;\r
+                       } else if (tag.equals("kern")) {\r
+                               kernOffset = offset;\r
+                               if (headOffset != -1) break;\r
+                       }\r
+               }\r
+       }\r
+\r
+       private void readHEAD (InputStream input) throws IOException {\r
+               seek(input, headOffset + 2 * 4 + 2 * 4 + 2);\r
+               int unitsPerEm = readUnsignedShort(input);\r
+               scale = (float)size / unitsPerEm;\r
+       }\r
+\r
+       private void readKERN (InputStream input) throws IOException {\r
+               seek(input, kernOffset + 2);\r
+               for (int subTableCount = readUnsignedShort(input); subTableCount > 0; subTableCount--) {\r
+                       skip(input, 2 * 2);\r
+                       int tupleIndex = readUnsignedShort(input);\r
+                       if (!((tupleIndex & 1) != 0) || (tupleIndex & 2) != 0 || (tupleIndex & 4) != 0) return;\r
+                       if (tupleIndex >> 8 != 0) continue;\r
+\r
+                       int kerningCount = readUnsignedShort(input);\r
+                       skip(input, 3 * 2);\r
+                       while (kerningCount-- > 0) {\r
+                               int firstGlyphCode = readUnsignedShort(input);\r
+                               int secondGlyphCode = readUnsignedShort(input);\r
+                               int offset = readShort(input);\r
+                               int value = (offset << 16) | secondGlyphCode;\r
+\r
+                               List firstGlyphValues = (List)values.get(new Integer(firstGlyphCode));\r
+                               if (firstGlyphValues == null) {\r
+                                       firstGlyphValues = new ArrayList(256);\r
+                                       values.put(new Integer(firstGlyphCode), firstGlyphValues);\r
+                               }\r
+                               firstGlyphValues.add(new Integer(value));\r
+                       }\r
+               }\r
+       }\r
+\r
+       private int readUnsignedByte (InputStream input) throws IOException {\r
+               bytePosition++;\r
+               int b = input.read();\r
+               if (b == -1) throw new EOFException("Unexpected end of file.");\r
+               return b;\r
+       }\r
+\r
+       private byte readByte (InputStream input) throws IOException {\r
+               return (byte)readUnsignedByte(input);\r
+       }\r
+\r
+       private int readUnsignedShort (InputStream input) throws IOException {\r
+               return (readUnsignedByte(input) << 8) + readUnsignedByte(input);\r
+       }\r
+\r
+       private short readShort (InputStream input) throws IOException {\r
+               return (short)readUnsignedShort(input);\r
+       }\r
+\r
+       private long readUnsignedLong (InputStream input) throws IOException {\r
+               long value = readUnsignedByte(input);\r
+               value = (value << 8) + readUnsignedByte(input);\r
+               value = (value << 8) + readUnsignedByte(input);\r
+               value = (value << 8) + readUnsignedByte(input);\r
+               return value;\r
+       }\r
+\r
+       private void skip (InputStream input, long skip) throws IOException {\r
+               while (skip > 0) {\r
+                       long skipped = input.skip(skip);\r
+                       if (skipped <= 0) break;\r
+                       bytePosition += skipped;\r
+                       skip -= skipped;\r
+               }\r
+       }\r
+\r
+       private void seek (InputStream input, long position) throws IOException {\r
+               skip(input, position - bytePosition);\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/Glyph.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/Glyph.java
new file mode 100644 (file)
index 0000000..152650d
--- /dev/null
@@ -0,0 +1,125 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont;\r
+\r
+import java.awt.Rectangle;\r
+import java.awt.Shape;\r
+import java.awt.font.GlyphMetrics;\r
+import java.awt.font.GlyphVector;\r
+\r
+import com.badlogic.gdx.graphics.Sprite;\r
+import com.badlogic.gdx.graphics.Texture;\r
+\r
+/**\r
+ * Represents the glyph in a font for a unicode codepoint.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class Glyph {\r
+       private int codePoint;\r
+       private short width, height;\r
+       private short yOffset;\r
+       private boolean isMissing;\r
+       private Shape shape;\r
+       private float u, v, u2, v2;\r
+       private Texture texture;\r
+\r
+       Glyph (int codePoint, Rectangle bounds, GlyphVector vector, int index, UnicodeFont unicodeFont) {\r
+               this.codePoint = codePoint;\r
+\r
+               GlyphMetrics metrics = vector.getGlyphMetrics(index);\r
+               int lsb = (int)metrics.getLSB();\r
+               if (lsb > 0) lsb = 0;\r
+               int rsb = (int)metrics.getRSB();\r
+               if (rsb > 0) rsb = 0;\r
+\r
+               int glyphWidth = bounds.width - lsb - rsb;\r
+               int glyphHeight = bounds.height;\r
+               if (glyphWidth > 0 && glyphHeight > 0) {\r
+                       int padTop = unicodeFont.getPaddingTop();\r
+                       int padRight = unicodeFont.getPaddingRight();\r
+                       int padBottom = unicodeFont.getPaddingBottom();\r
+                       int padLeft = unicodeFont.getPaddingLeft();\r
+                       int glyphSpacing = 1; // Needed to prevent filtering problems.\r
+                       width = (short)(glyphWidth + padLeft + padRight + glyphSpacing);\r
+                       height = (short)(glyphHeight + padTop + padBottom + glyphSpacing);\r
+                       yOffset = (short)(unicodeFont.getAscent() + bounds.y - padTop);\r
+               }\r
+\r
+               shape = vector.getGlyphOutline(index, -bounds.x + unicodeFont.getPaddingLeft(), -bounds.y + unicodeFont.getPaddingTop());\r
+\r
+               isMissing = !unicodeFont.getFont().canDisplay((char)codePoint);\r
+       }\r
+\r
+       /**\r
+        * The unicode codepoint the glyph represents.\r
+        */\r
+       public int getCodePoint () {\r
+               return codePoint;\r
+       }\r
+\r
+       /**\r
+        * Returns true if the font does not have a glyph for this codepoint.\r
+        */\r
+       public boolean isMissing () {\r
+               return isMissing;\r
+       }\r
+\r
+       /**\r
+        * The width of the glyph's image.\r
+        */\r
+       public int getWidth () {\r
+               return width;\r
+       }\r
+\r
+       /**\r
+        * The height of the glyph's image.\r
+        */\r
+       public int getHeight () {\r
+               return height;\r
+       }\r
+\r
+       /**\r
+        * The shape to use to draw this glyph. This is set to null after the glyph is stored in a GlyphPage.\r
+        */\r
+       public Shape getShape () {\r
+               return shape;\r
+       }\r
+\r
+       public void setShape (Shape shape) {\r
+               this.shape = shape;\r
+       }\r
+\r
+       public void setTexture (Texture texture, float u, float v, float u2, float v2) {\r
+               this.texture = texture;\r
+               this.u = u;\r
+               this.v = v;\r
+               this.u2 = u2;\r
+               this.v2 = v2;\r
+       }\r
+\r
+       public Texture getTexture () {\r
+               return texture;\r
+       }\r
+\r
+       public float getU () {\r
+               return u;\r
+       }\r
+\r
+       public float getV () {\r
+               return v;\r
+       }\r
+\r
+       public float getU2 () {\r
+               return u2;\r
+       }\r
+\r
+       public float getV2 () {\r
+               return v2;\r
+       }\r
+\r
+       /**\r
+        * The distance from drawing y location to top of this glyph, causing the glyph to sit on the baseline.\r
+        */\r
+       public int getYOffset () {\r
+               return yOffset;\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/GlyphPage.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/GlyphPage.java
new file mode 100644 (file)
index 0000000..cf80858
--- /dev/null
@@ -0,0 +1,214 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont;\r
+\r
+import java.awt.AlphaComposite;\r
+import java.awt.Graphics2D;\r
+import java.awt.RenderingHints;\r
+import java.awt.font.FontRenderContext;\r
+import java.awt.image.BufferedImage;\r
+import java.awt.image.WritableRaster;\r
+import java.nio.ByteBuffer;\r
+import java.nio.ByteOrder;\r
+import java.nio.IntBuffer;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+import java.util.ListIterator;\r
+\r
+import javax.swing.Renderer;\r
+\r
+import org.lwjgl.opengl.GL11;\r
+import org.lwjgl.opengl.GL12;\r
+\r
+import com.badlogic.gdx.Gdx;\r
+import com.badlogic.gdx.graphics.Pixmap;\r
+import com.badlogic.gdx.graphics.Pixmap.Format;\r
+import com.badlogic.gdx.graphics.Sprite;\r
+import com.badlogic.gdx.graphics.Texture;\r
+import com.badlogic.gdx.graphics.Texture.TextureFilter;\r
+import com.badlogic.gdx.graphics.Texture.TextureWrap;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.Effect;\r
+\r
+/**\r
+ * Stores a number of glyphs on a single texture.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class GlyphPage {\r
+       private final UnicodeFont unicodeFont;\r
+       private final int pageWidth, pageHeight;\r
+       private final Texture texture;\r
+       private int pageX, pageY, rowHeight;\r
+       private boolean orderAscending;\r
+       private final List pageGlyphs = new ArrayList(32);\r
+\r
+       /**\r
+        * @param pageWidth The width of the backing texture.\r
+        * @param pageHeight The height of the backing texture.\r
+        */\r
+       GlyphPage (UnicodeFont unicodeFont, int pageWidth, int pageHeight) {\r
+               this.unicodeFont = unicodeFont;\r
+               this.pageWidth = pageWidth;\r
+               this.pageHeight = pageHeight;\r
+\r
+               texture = Gdx.graphics.newUnmanagedTexture(pageWidth, pageHeight, Format.RGBA8888, TextureFilter.Linear,\r
+                       TextureFilter.Linear, TextureWrap.ClampToEdge, TextureWrap.ClampToEdge);\r
+       }\r
+\r
+       /**\r
+        * Loads glyphs to the backing texture and sets the image on each loaded glyph. Loaded glyphs are removed from the list.\r
+        * \r
+        * If this page already has glyphs and maxGlyphsToLoad is -1, then this method will return 0 if all the new glyphs don't fit.\r
+        * This reduces texture binds when drawing since glyphs loaded at once are typically displayed together.\r
+        * @param glyphs The glyphs to load.\r
+        * @param maxGlyphsToLoad This is the maximum number of glyphs to load from the list. Set to -1 to attempt to load all the\r
+        *           glyphs.\r
+        * @return The number of glyphs that were actually loaded.\r
+        */\r
+       int loadGlyphs (List glyphs, int maxGlyphsToLoad) {\r
+               if (rowHeight != 0 && maxGlyphsToLoad == -1) {\r
+                       // If this page has glyphs and we are not loading incrementally, return zero if any of the glyphs don't fit.\r
+                       int testX = pageX;\r
+                       int testY = pageY;\r
+                       int testRowHeight = rowHeight;\r
+                       for (Iterator iter = getIterator(glyphs); iter.hasNext();) {\r
+                               Glyph glyph = (Glyph)iter.next();\r
+                               int width = glyph.getWidth();\r
+                               int height = glyph.getHeight();\r
+                               if (testX + width >= pageWidth) {\r
+                                       testX = 0;\r
+                                       testY += testRowHeight;\r
+                                       testRowHeight = height;\r
+                               } else if (height > testRowHeight) {\r
+                                       testRowHeight = height;\r
+                               }\r
+                               if (testY + testRowHeight >= pageWidth) return 0;\r
+                               testX += width;\r
+                       }\r
+               }\r
+\r
+               GL11.glColor4f(1, 1, 1, 1);\r
+               texture.bind();\r
+\r
+               int i = 0;\r
+               for (Iterator iter = getIterator(glyphs); iter.hasNext();) {\r
+                       Glyph glyph = (Glyph)iter.next();\r
+                       int width = Math.min(MAX_GLYPH_SIZE, glyph.getWidth());\r
+                       int height = Math.min(MAX_GLYPH_SIZE, glyph.getHeight());\r
+\r
+                       if (rowHeight == 0) {\r
+                               // The first glyph always fits.\r
+                               rowHeight = height;\r
+                       } else {\r
+                               // Wrap to the next line if needed, or break if no more fit.\r
+                               if (pageX + width >= pageWidth) {\r
+                                       if (pageY + rowHeight + height >= pageHeight) break;\r
+                                       pageX = 0;\r
+                                       pageY += rowHeight;\r
+                                       rowHeight = height;\r
+                               } else if (height > rowHeight) {\r
+                                       if (pageY + height >= pageHeight) break;\r
+                                       rowHeight = height;\r
+                               }\r
+                       }\r
+\r
+                       renderGlyph(glyph, width, height);\r
+                       pageGlyphs.add(glyph);\r
+\r
+                       pageX += width;\r
+\r
+                       iter.remove();\r
+                       i++;\r
+                       if (i == maxGlyphsToLoad) {\r
+                               // If loading incrementally, flip orderAscending so it won't change, since we'll probably load the rest next time.\r
+                               orderAscending = !orderAscending;\r
+                               break;\r
+                       }\r
+               }\r
+\r
+               // Every other batch of glyphs added to a page are sorted the opposite way to attempt to keep same size glyps together.\r
+               orderAscending = !orderAscending;\r
+\r
+               return i;\r
+       }\r
+\r
+       /**\r
+        * Loads a single glyph to the backing texture, if it fits.\r
+        */\r
+       private void renderGlyph (Glyph glyph, int width, int height) {\r
+               // Draw the glyph to the scratch image using Java2D.\r
+               scratchGraphics.setComposite(AlphaComposite.Clear);\r
+               scratchGraphics.fillRect(0, 0, MAX_GLYPH_SIZE, MAX_GLYPH_SIZE);\r
+               scratchGraphics.setComposite(AlphaComposite.SrcOver);\r
+               scratchGraphics.setColor(java.awt.Color.white);\r
+               for (Iterator iter = unicodeFont.getEffects().iterator(); iter.hasNext();)\r
+                       ((Effect)iter.next()).draw(scratchImage, scratchGraphics, unicodeFont, glyph);\r
+               glyph.setShape(null); // The shape will never be needed again.\r
+\r
+               WritableRaster raster = scratchImage.getRaster();\r
+               int[] row = new int[width];\r
+               for (int y = 0; y < height; y++) {\r
+                       raster.getDataElements(0, y, width, 1, row);\r
+                       scratchIntBuffer.put(row);\r
+               }\r
+               GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, pageX, pageY, width, height, GL12.GL_BGRA, GL11.GL_UNSIGNED_BYTE,\r
+                       scratchByteBuffer);\r
+               scratchIntBuffer.clear();\r
+\r
+               float u = pageX / (float)texture.getWidth();\r
+               float v = pageY / (float)texture.getHeight();\r
+               float u2 = (pageX + width) / (float)texture.getWidth();\r
+               float v2 = (pageY + height) / (float)texture.getHeight();\r
+               glyph.setTexture(texture, u, v, u2, v2);\r
+       }\r
+\r
+       /**\r
+        * Returns an iterator for the specified glyphs, sorted either ascending or descending.\r
+        */\r
+       private Iterator getIterator (List glyphs) {\r
+               if (orderAscending) return glyphs.iterator();\r
+               final ListIterator iter = glyphs.listIterator(glyphs.size());\r
+               return new Iterator() {\r
+                       public boolean hasNext () {\r
+                               return iter.hasPrevious();\r
+                       }\r
+\r
+                       public Object next () {\r
+                               return iter.previous();\r
+                       }\r
+\r
+                       public void remove () {\r
+                               iter.remove();\r
+                       }\r
+               };\r
+       }\r
+\r
+       /**\r
+        * Returns the glyphs stored on this page.\r
+        */\r
+       public List getGlyphs () {\r
+               return pageGlyphs;\r
+       }\r
+\r
+       /**\r
+        * Returns the backing texture for this page.\r
+        */\r
+       public Texture getTexture () {\r
+               return texture;\r
+       }\r
+\r
+       static public final int MAX_GLYPH_SIZE = 256;\r
+\r
+       static private ByteBuffer scratchByteBuffer = ByteBuffer.allocateDirect(MAX_GLYPH_SIZE * MAX_GLYPH_SIZE * 4);\r
+       static {\r
+               scratchByteBuffer.order(ByteOrder.LITTLE_ENDIAN);\r
+       }\r
+       static private IntBuffer scratchIntBuffer = scratchByteBuffer.asIntBuffer();\r
+\r
+       static private BufferedImage scratchImage = new BufferedImage(MAX_GLYPH_SIZE, MAX_GLYPH_SIZE, BufferedImage.TYPE_INT_ARGB);\r
+       static Graphics2D scratchGraphics = (Graphics2D)scratchImage.getGraphics();\r
+       static {\r
+               scratchGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);\r
+               scratchGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);\r
+       }\r
+       static public FontRenderContext renderContext = scratchGraphics.getFontRenderContext();\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/HieroSettings.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/HieroSettings.java
new file mode 100644 (file)
index 0000000..d93d9a1
--- /dev/null
@@ -0,0 +1,295 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont;\r
+\r
+import java.io.BufferedReader;\r
+import java.io.File;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStreamReader;\r
+import java.io.PrintStream;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+import com.badlogic.gdx.Files.FileType;\r
+import com.badlogic.gdx.Gdx;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ConfigurableEffect;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ConfigurableEffect.Value;\r
+import com.badlogic.gdx.utils.GdxRuntimeException;\r
+\r
+/**\r
+ * Holds the settings needed to configure a UnicodeFont.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class HieroSettings {\r
+       private int fontSize = 12;\r
+       private boolean bold = false, italic = false;\r
+       private int paddingTop, paddingLeft, paddingBottom, paddingRight, paddingAdvanceX, paddingAdvanceY;\r
+       private int glyphPageWidth = 512, glyphPageHeight = 512;\r
+       private final List effects = new ArrayList();\r
+\r
+       public HieroSettings () {\r
+       }\r
+\r
+       /**\r
+        * @param hieroFileRef The file system or classpath location of the Hiero settings file.\r
+        */\r
+       public HieroSettings (String hieroFileRef) {\r
+               try {\r
+                       BufferedReader reader = new BufferedReader(new InputStreamReader(Gdx.files.readFile(hieroFileRef, FileType.Absolute)));\r
+                       while (true) {\r
+                               String line = reader.readLine();\r
+                               if (line == null) break;\r
+                               line = line.trim();\r
+                               if (line.length() == 0) continue;\r
+                               String[] pieces = line.split("=", 2);\r
+                               String name = pieces[0].trim();\r
+                               String value = pieces[1];\r
+                               if (name.equals("font.size")) {\r
+                                       fontSize = Integer.parseInt(value);\r
+                               } else if (name.equals("font.bold")) {\r
+                                       bold = Boolean.parseBoolean(value);\r
+                               } else if (name.equals("font.italic")) {\r
+                                       italic = Boolean.parseBoolean(value);\r
+                               } else if (name.equals("pad.top")) {\r
+                                       paddingTop = Integer.parseInt(value);\r
+                               } else if (name.equals("pad.right")) {\r
+                                       paddingRight = Integer.parseInt(value);\r
+                               } else if (name.equals("pad.bottom")) {\r
+                                       paddingBottom = Integer.parseInt(value);\r
+                               } else if (name.equals("pad.left")) {\r
+                                       paddingLeft = Integer.parseInt(value);\r
+                               } else if (name.equals("pad.advance.x")) {\r
+                                       paddingAdvanceX = Integer.parseInt(value);\r
+                               } else if (name.equals("pad.advance.y")) {\r
+                                       paddingAdvanceY = Integer.parseInt(value);\r
+                               } else if (name.equals("glyph.page.width")) {\r
+                                       glyphPageWidth = Integer.parseInt(value);\r
+                               } else if (name.equals("glyph.page.height")) {\r
+                                       glyphPageHeight = Integer.parseInt(value);\r
+                               } else if (name.equals("effect.class")) {\r
+                                       try {\r
+                                               effects.add(Class.forName(value).newInstance());\r
+                                       } catch (Throwable ex) {\r
+                                               throw new GdxRuntimeException("Unable to create effect instance: " + value, ex);\r
+                                       }\r
+                               } else if (name.startsWith("effect.")) {\r
+                                       // Set an effect value on the last added effect.\r
+                                       name = name.substring(7);\r
+                                       ConfigurableEffect effect = (ConfigurableEffect)effects.get(effects.size() - 1);\r
+                                       List values = effect.getValues();\r
+                                       for (Iterator iter = values.iterator(); iter.hasNext();) {\r
+                                               Value effectValue = (Value)iter.next();\r
+                                               if (effectValue.getName().equals(name)) {\r
+                                                       effectValue.setString(value);\r
+                                                       break;\r
+                                               }\r
+                                       }\r
+                                       effect.setValues(values);\r
+                               }\r
+                       }\r
+                       reader.close();\r
+               } catch (Throwable ex) {\r
+                       throw new GdxRuntimeException("Unable to load Hiero font file: " + hieroFileRef, ex);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getPaddingTop()\r
+        */\r
+       public int getPaddingTop () {\r
+               return paddingTop;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setPaddingTop(int)\r
+        */\r
+       public void setPaddingTop (int paddingTop) {\r
+               this.paddingTop = paddingTop;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getPaddingLeft()\r
+        */\r
+       public int getPaddingLeft () {\r
+               return paddingLeft;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setPaddingLeft(int)\r
+        */\r
+       public void setPaddingLeft (int paddingLeft) {\r
+               this.paddingLeft = paddingLeft;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getPaddingBottom()\r
+        */\r
+       public int getPaddingBottom () {\r
+               return paddingBottom;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setPaddingBottom(int)\r
+        */\r
+       public void setPaddingBottom (int paddingBottom) {\r
+               this.paddingBottom = paddingBottom;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getPaddingRight()\r
+        */\r
+       public int getPaddingRight () {\r
+               return paddingRight;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setPaddingRight(int)\r
+        */\r
+       public void setPaddingRight (int paddingRight) {\r
+               this.paddingRight = paddingRight;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getPaddingAdvanceX()\r
+        */\r
+       public int getPaddingAdvanceX () {\r
+               return paddingAdvanceX;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setPaddingAdvanceX(int)\r
+        */\r
+       public void setPaddingAdvanceX (int paddingAdvanceX) {\r
+               this.paddingAdvanceX = paddingAdvanceX;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getPaddingAdvanceY()\r
+        */\r
+       public int getPaddingAdvanceY () {\r
+               return paddingAdvanceY;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setPaddingAdvanceY(int)\r
+        */\r
+       public void setPaddingAdvanceY (int paddingAdvanceY) {\r
+               this.paddingAdvanceY = paddingAdvanceY;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getGlyphPageWidth()\r
+        */\r
+       public int getGlyphPageWidth () {\r
+               return glyphPageWidth;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setGlyphPageWidth(int)\r
+        */\r
+       public void setGlyphPageWidth (int glyphPageWidth) {\r
+               this.glyphPageWidth = glyphPageWidth;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getGlyphPageHeight()\r
+        */\r
+       public int getGlyphPageHeight () {\r
+               return glyphPageHeight;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#setGlyphPageHeight(int)\r
+        */\r
+       public void setGlyphPageHeight (int glyphPageHeight) {\r
+               this.glyphPageHeight = glyphPageHeight;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#UnicodeFont(String, int, boolean, boolean)\r
+        * @see UnicodeFont#UnicodeFont(java.awt.Font, int, boolean, boolean)\r
+        */\r
+       public int getFontSize () {\r
+               return fontSize;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#UnicodeFont(String, int, boolean, boolean)\r
+        * @see UnicodeFont#UnicodeFont(java.awt.Font, int, boolean, boolean)\r
+        */\r
+       public void setFontSize (int fontSize) {\r
+               this.fontSize = fontSize;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#UnicodeFont(String, int, boolean, boolean)\r
+        * @see UnicodeFont#UnicodeFont(java.awt.Font, int, boolean, boolean)\r
+        */\r
+       public boolean isBold () {\r
+               return bold;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#UnicodeFont(String, int, boolean, boolean)\r
+        * @see UnicodeFont#UnicodeFont(java.awt.Font, int, boolean, boolean)\r
+        */\r
+       public void setBold (boolean bold) {\r
+               this.bold = bold;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#UnicodeFont(String, int, boolean, boolean)\r
+        * @see UnicodeFont#UnicodeFont(java.awt.Font, int, boolean, boolean)\r
+        */\r
+       public boolean isItalic () {\r
+               return italic;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#UnicodeFont(String, int, boolean, boolean)\r
+        * @see UnicodeFont#UnicodeFont(java.awt.Font, int, boolean, boolean)\r
+        */\r
+       public void setItalic (boolean italic) {\r
+               this.italic = italic;\r
+       }\r
+\r
+       /**\r
+        * @see UnicodeFont#getEffects()\r
+        */\r
+       public List getEffects () {\r
+               return effects;\r
+       }\r
+\r
+       /**\r
+        * Saves the settings to a file.\r
+        * @throws IOException if the file could not be saved.\r
+        */\r
+       public void save (File file) throws IOException {\r
+               PrintStream out = new PrintStream(new FileOutputStream(file));\r
+               out.println("font.size=" + fontSize);\r
+               out.println("font.bold=" + bold);\r
+               out.println("font.italic=" + italic);\r
+               out.println();\r
+               out.println("pad.top=" + paddingTop);\r
+               out.println("pad.right=" + paddingRight);\r
+               out.println("pad.bottom=" + paddingBottom);\r
+               out.println("pad.left=" + paddingLeft);\r
+               out.println("pad.advance.x=" + paddingAdvanceX);\r
+               out.println("pad.advance.y=" + paddingAdvanceY);\r
+               out.println();\r
+               out.println("glyph.page.width=" + glyphPageWidth);\r
+               out.println("glyph.page.height=" + glyphPageHeight);\r
+               out.println();\r
+               for (Iterator iter = effects.iterator(); iter.hasNext();) {\r
+                       ConfigurableEffect effect = (ConfigurableEffect)iter.next();\r
+                       out.println("effect.class=" + effect.getClass().getName());\r
+                       for (Iterator iter2 = effect.getValues().iterator(); iter2.hasNext();) {\r
+                               Value value = (Value)iter2.next();\r
+                               out.println("effect." + value.getName() + "=" + value.getString());\r
+                       }\r
+                       out.println();\r
+               }\r
+               out.close();\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/UnicodeFont.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/UnicodeFont.java
new file mode 100644 (file)
index 0000000..3c632ef
--- /dev/null
@@ -0,0 +1,783 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont;\r
+\r
+import java.awt.Font;\r
+import java.awt.FontFormatException;\r
+import java.awt.FontMetrics;\r
+import java.awt.Rectangle;\r
+import java.awt.font.GlyphVector;\r
+import java.awt.font.TextAttribute;\r
+import java.io.IOException;\r
+import java.lang.reflect.Field;\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.Comparator;\r
+import java.util.Iterator;\r
+import java.util.LinkedHashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+import java.util.Map.Entry;\r
+\r
+import org.lwjgl.opengl.GL11;\r
+\r
+import com.badlogic.gdx.Files.FileType;\r
+import com.badlogic.gdx.Gdx;\r
+import com.badlogic.gdx.graphics.Color;\r
+import com.badlogic.gdx.graphics.Sprite;\r
+import com.badlogic.gdx.graphics.Texture;\r
+import com.badlogic.gdx.utils.GdxRuntimeException;\r
+\r
+/**\r
+ * A Slick bitmap font that can display unicode glyphs from a TrueTypeFont.\r
+ * \r
+ * For efficiency, glyphs are packed on to textures. Glyphs can be loaded to the textures on the fly, when they are first needed\r
+ * for display. However, it is best to load the glyphs that are known to be needed at startup.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class UnicodeFont {\r
+       static private final int DISPLAY_LIST_CACHE_SIZE = 200;\r
+       static private final int MAX_GLYPH_CODE = 0x10FFFF;\r
+       static private final int PAGE_SIZE = 512;\r
+       static private final int PAGES = MAX_GLYPH_CODE / PAGE_SIZE;\r
+\r
+       private Font font;\r
+       private String ttfFileRef;\r
+       private int ascent, descent, leading, spaceWidth;\r
+       private final Glyph[][] glyphs = new Glyph[PAGES][];\r
+       private final List glyphPages = new ArrayList();\r
+       private final List queuedGlyphs = new ArrayList(256);\r
+       private final List effects = new ArrayList();\r
+       private int paddingTop, paddingLeft, paddingBottom, paddingRight, paddingAdvanceX, paddingAdvanceY;\r
+       private Glyph missingGlyph;\r
+       private int glyphPageWidth = 512, glyphPageHeight = 512;\r
+       private final DisplayList emptyDisplayList = new DisplayList();\r
+\r
+       private boolean displayListCaching = true;\r
+       private int baseDisplayListID = -1;\r
+       int eldestDisplayListID;\r
+       private final LinkedHashMap displayLists = new LinkedHashMap(DISPLAY_LIST_CACHE_SIZE, 1, true) {\r
+               protected boolean removeEldestEntry (Entry eldest) {\r
+                       DisplayList displayList = (DisplayList)eldest.getValue();\r
+                       if (displayList != null) eldestDisplayListID = displayList.id;\r
+                       return size() > DISPLAY_LIST_CACHE_SIZE;\r
+               }\r
+       };\r
+\r
+       /**\r
+        * @param ttfFileRef The file system or classpath location of the TrueTypeFont file.\r
+        * @param hieroFileRef The file system or classpath location of the Hiero settings file.\r
+        */\r
+       public UnicodeFont (String ttfFileRef, String hieroFileRef) {\r
+               this(ttfFileRef, new HieroSettings(hieroFileRef));\r
+       }\r
+\r
+       /**\r
+        * @param ttfFileRef The file system or classpath location of the TrueTypeFont file.\r
+        */\r
+       public UnicodeFont (String ttfFileRef, HieroSettings settings) {\r
+               this.ttfFileRef = ttfFileRef;\r
+               Font font = createFont(ttfFileRef);\r
+               initializeFont(font, settings.getFontSize(), settings.isBold(), settings.isItalic());\r
+               loadSettings(settings);\r
+       }\r
+\r
+       /**\r
+        * @param ttfFileRef The file system or classpath location of the TrueTypeFont file.\r
+        */\r
+       public UnicodeFont (String ttfFileRef, int size, boolean bold, boolean italic) {\r
+               this.ttfFileRef = ttfFileRef;\r
+               initializeFont(createFont(ttfFileRef), size, bold, italic);\r
+       }\r
+\r
+       /**\r
+        * Creates a new UnicodeFont.\r
+        * @param hieroFileRef The file system or classpath location of the Hiero settings file.\r
+        */\r
+       public UnicodeFont (Font font, String hieroFileRef) {\r
+               this(font, new HieroSettings(hieroFileRef));\r
+       }\r
+\r
+       /**\r
+        * Creates a new UnicodeFont.\r
+        */\r
+       public UnicodeFont (Font font, HieroSettings settings) {\r
+               initializeFont(font, settings.getFontSize(), settings.isBold(), settings.isItalic());\r
+               loadSettings(settings);\r
+       }\r
+\r
+       /**\r
+        * Creates a new UnicodeFont.\r
+        */\r
+       public UnicodeFont (Font font) {\r
+               initializeFont(font, font.getSize(), font.isBold(), font.isItalic());\r
+       }\r
+\r
+       /**\r
+        * Creates a new UnicodeFont.\r
+        */\r
+       public UnicodeFont (Font font, int size, boolean bold, boolean italic) {\r
+               initializeFont(font, size, bold, italic);\r
+       }\r
+\r
+       private void initializeFont (Font baseFont, int size, boolean bold, boolean italic) {\r
+               Map attributes = baseFont.getAttributes();\r
+               attributes.put(TextAttribute.SIZE, new Float(size));\r
+               attributes.put(TextAttribute.WEIGHT, bold ? TextAttribute.WEIGHT_BOLD : TextAttribute.WEIGHT_REGULAR);\r
+               attributes.put(TextAttribute.POSTURE, italic ? TextAttribute.POSTURE_OBLIQUE : TextAttribute.POSTURE_REGULAR);\r
+               try {\r
+                       attributes.put(TextAttribute.class.getDeclaredField("KERNING").get(null),\r
+                               TextAttribute.class.getDeclaredField("KERNING_ON").get(null));\r
+               } catch (Throwable ignored) {\r
+               }\r
+               font = baseFont.deriveFont(attributes);\r
+\r
+               FontMetrics metrics = GlyphPage.scratchGraphics.getFontMetrics(font);\r
+               ascent = metrics.getAscent();\r
+               descent = metrics.getDescent();\r
+               leading = metrics.getLeading();\r
+\r
+               // Determine width of space glyph (getGlyphPixelBounds gives a width of zero).\r
+               char[] chars = " ".toCharArray();\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+               spaceWidth = vector.getGlyphLogicalBounds(0).getBounds().width;\r
+       }\r
+\r
+       private void loadSettings (HieroSettings settings) {\r
+               paddingTop = settings.getPaddingTop();\r
+               paddingLeft = settings.getPaddingLeft();\r
+               paddingBottom = settings.getPaddingBottom();\r
+               paddingRight = settings.getPaddingRight();\r
+               paddingAdvanceX = settings.getPaddingAdvanceX();\r
+               paddingAdvanceY = settings.getPaddingAdvanceY();\r
+               glyphPageWidth = settings.getGlyphPageWidth();\r
+               glyphPageHeight = settings.getGlyphPageHeight();\r
+               effects.addAll(settings.getEffects());\r
+       }\r
+\r
+       /**\r
+        * Queues the glyphs in the specified codepoint range (inclusive) to be loaded. Note that the glyphs are not actually loaded\r
+        * until {@link #loadGlyphs()} is called.\r
+        * \r
+        * Some characters like combining marks and non-spacing marks can only be rendered with the context of other glyphs. In this\r
+        * case, use {@link #addGlyphs(String)}.\r
+        */\r
+       public void addGlyphs (int startCodePoint, int endCodePoint) {\r
+               for (int codePoint = startCodePoint; codePoint <= endCodePoint; codePoint++)\r
+                       addGlyphs(new String(Character.toChars(codePoint)));\r
+       }\r
+\r
+       /**\r
+        * Queues the glyphs in the specified text to be loaded. Note that the glyphs are not actually loaded until\r
+        * {@link #loadGlyphs()} is called.\r
+        */\r
+       public void addGlyphs (String text) {\r
+               if (text == null) throw new IllegalArgumentException("text cannot be null.");\r
+\r
+               char[] chars = text.toCharArray();\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+               for (int i = 0, n = vector.getNumGlyphs(); i < n; i++) {\r
+                       int codePoint = text.codePointAt(vector.getGlyphCharIndex(i));\r
+                       Rectangle bounds = getGlyphBounds(vector, i, codePoint);\r
+                       getGlyph(vector.getGlyphCode(i), codePoint, bounds, vector, i);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Queues the glyphs in the ASCII character set (codepoints 32 through 255) to be loaded. Note that the glyphs are not actually\r
+        * loaded until {@link #loadGlyphs()} is called.\r
+        */\r
+       public void addAsciiGlyphs () {\r
+               addGlyphs(32, 255);\r
+       }\r
+\r
+       /**\r
+        * Queues the glyphs in the NEHE character set (codepoints 32 through 128) to be loaded. Note that the glyphs are not actually\r
+        * loaded until {@link #loadGlyphs()} is called.\r
+        */\r
+       public void addNeheGlyphs () {\r
+               addGlyphs(32, 32 + 96);\r
+       }\r
+\r
+       /**\r
+        * Loads all queued glyphs to the backing textures. Glyphs that are typically displayed together should be added and loaded at\r
+        * the same time so that they are stored on the same backing texture. This reduces the number of backing texture binds required\r
+        * to draw glyphs.\r
+        */\r
+       public boolean loadGlyphs () {\r
+               return loadGlyphs(-1);\r
+       }\r
+\r
+       /**\r
+        * Loads up to the specified number of queued glyphs to the backing textures. This is typically called from the game loop to\r
+        * load glyphs on the fly that were requested for display but have not yet been loaded.\r
+        */\r
+       public boolean loadGlyphs (int maxGlyphsToLoad) {\r
+               if (queuedGlyphs.isEmpty()) return false;\r
+\r
+               if (effects.isEmpty())\r
+                       throw new IllegalStateException("The UnicodeFont must have at least one effect before any glyphs can be loaded.");\r
+\r
+               for (Iterator iter = queuedGlyphs.iterator(); iter.hasNext();) {\r
+                       Glyph glyph = (Glyph)iter.next();\r
+                       int codePoint = glyph.getCodePoint();\r
+\r
+                       // Don't load an image for a glyph with nothing to display.\r
+                       if (glyph.getWidth() == 0 || codePoint == ' ') {\r
+                               iter.remove();\r
+                               continue;\r
+                       }\r
+\r
+                       // Only load the first missing glyph.\r
+                       if (glyph.isMissing()) {\r
+                               if (missingGlyph != null) {\r
+                                       if (glyph != missingGlyph) iter.remove();\r
+                                       continue;\r
+                               }\r
+                               missingGlyph = glyph;\r
+                       }\r
+               }\r
+\r
+               Collections.sort(queuedGlyphs, heightComparator);\r
+\r
+               // Add to existing pages.\r
+               for (Iterator iter = glyphPages.iterator(); iter.hasNext();) {\r
+                       GlyphPage glyphPage = (GlyphPage)iter.next();\r
+                       maxGlyphsToLoad -= glyphPage.loadGlyphs(queuedGlyphs, maxGlyphsToLoad);\r
+                       if (maxGlyphsToLoad == 0 || queuedGlyphs.isEmpty()) return true;\r
+               }\r
+\r
+               // Add to new pages.\r
+               while (!queuedGlyphs.isEmpty()) {\r
+                       GlyphPage glyphPage = new GlyphPage(this, glyphPageWidth, glyphPageHeight);\r
+                       glyphPages.add(glyphPage);\r
+                       maxGlyphsToLoad -= glyphPage.loadGlyphs(queuedGlyphs, maxGlyphsToLoad);\r
+                       if (maxGlyphsToLoad == 0) return true;\r
+               }\r
+\r
+               return true;\r
+       }\r
+\r
+       /**\r
+        * Clears all loaded and queued glyphs.\r
+        */\r
+       public void clearGlyphs () {\r
+               for (int i = 0; i < PAGES; i++)\r
+                       glyphs[i] = null;\r
+\r
+               for (Iterator iter = glyphPages.iterator(); iter.hasNext();) {\r
+                       GlyphPage page = (GlyphPage)iter.next();\r
+                       page.getTexture().dispose();\r
+               }\r
+               glyphPages.clear();\r
+\r
+               if (baseDisplayListID != -1) {\r
+                       GL11.glDeleteLists(baseDisplayListID, displayLists.size());\r
+                       baseDisplayListID = -1;\r
+               }\r
+\r
+               queuedGlyphs.clear();\r
+               missingGlyph = null;\r
+       }\r
+\r
+       /**\r
+        * Releases all resources used by this UnicodeFont. This method should be called when this UnicodeFont instance is no longer\r
+        * needed.\r
+        */\r
+       public void destroy () {\r
+               // The destroy() method is just to provide a consistent API for releasing resources.\r
+               clearGlyphs();\r
+       }\r
+\r
+       /**\r
+        * Identical to {@link #drawString(float, float, String, Color, int, int)} but returns a DisplayList which provides access to\r
+        * the width and height of the text drawn.\r
+        */\r
+       public DisplayList drawDisplayList (float x, float y, String text, Color color, int startIndex, int endIndex) {\r
+               if (text == null) throw new IllegalArgumentException("text cannot be null.");\r
+               if (text.length() == 0) return emptyDisplayList;\r
+               if (color == null) throw new IllegalArgumentException("color cannot be null.");\r
+\r
+               x -= paddingLeft;\r
+               y -= paddingTop;\r
+\r
+               String displayListKey = text.substring(startIndex, endIndex);\r
+\r
+               GL11.glColor4f(color.r, color.g, color.b, color.a);\r
+\r
+               DisplayList displayList = null;\r
+               if (displayListCaching && queuedGlyphs.isEmpty()) {\r
+                       if (baseDisplayListID == -1) {\r
+                               baseDisplayListID = GL11.glGenLists(DISPLAY_LIST_CACHE_SIZE);\r
+                               if (baseDisplayListID == 0) {\r
+                                       baseDisplayListID = -1;\r
+                                       displayListCaching = false;\r
+                                       return new DisplayList();\r
+                               }\r
+                       }\r
+                       // Try to use a display list compiled for this text.\r
+                       displayList = (DisplayList)displayLists.get(displayListKey);\r
+                       if (displayList != null) {\r
+                               if (displayList.invalid)\r
+                                       displayList.invalid = false;\r
+                               else {\r
+                                       GL11.glTranslatef(x, y, 0);\r
+                                       GL11.glCallList(displayList.id);\r
+                                       GL11.glTranslatef(-x, -y, 0);\r
+                                       return displayList;\r
+                               }\r
+                       } else if (displayList == null) {\r
+                               // Compile a new display list.\r
+                               displayList = new DisplayList();\r
+                               int displayListCount = displayLists.size();\r
+                               displayLists.put(displayListKey, displayList);\r
+                               if (displayListCount < DISPLAY_LIST_CACHE_SIZE)\r
+                                       displayList.id = baseDisplayListID + displayListCount;\r
+                               else\r
+                                       displayList.id = eldestDisplayListID;\r
+                       }\r
+               }\r
+\r
+               GL11.glTranslatef(x, y, 0);\r
+\r
+               if (displayList != null) GL11.glNewList(displayList.id, GL11.GL_COMPILE_AND_EXECUTE);\r
+\r
+               char[] chars = text.substring(0, endIndex).toCharArray();\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+\r
+               int maxWidth = 0, totalHeight = 0, lines = 0;\r
+               int extraX = 0, extraY = ascent;\r
+               boolean startNewLine = false;\r
+               Texture lastBind = null;\r
+               for (int glyphIndex = 0, n = vector.getNumGlyphs(); glyphIndex < n; glyphIndex++) {\r
+                       int charIndex = vector.getGlyphCharIndex(glyphIndex);\r
+                       if (charIndex < startIndex) continue;\r
+                       if (charIndex > endIndex) break;\r
+\r
+                       int codePoint = text.codePointAt(charIndex);\r
+\r
+                       Rectangle bounds = getGlyphBounds(vector, glyphIndex, codePoint);\r
+                       Glyph glyph = getGlyph(vector.getGlyphCode(glyphIndex), codePoint, bounds, vector, glyphIndex);\r
+\r
+                       if (startNewLine && codePoint != '\n') {\r
+                               extraX = -bounds.x;\r
+                               startNewLine = false;\r
+                       }\r
+\r
+                       if (glyph.getTexture() == null && missingGlyph != null && glyph.isMissing()) glyph = missingGlyph;\r
+                       if (glyph.getTexture() != null) {\r
+                               // Draw glyph, only binding a new glyph page texture when necessary.\r
+                               Texture texture = glyph.getTexture();\r
+                               if (lastBind != null && lastBind != texture) {\r
+                                       GL11.glEnd();\r
+                                       lastBind = null;\r
+                               }\r
+                               if (lastBind == null) {\r
+                                       texture.bind();\r
+                                       GL11.glBegin(GL11.GL_QUADS);\r
+                                       lastBind = texture;\r
+                               }\r
+                               int glyphX = bounds.x + extraX;\r
+                               int glyphY = bounds.y + extraY;\r
+                               GL11.glTexCoord2f(glyph.getU(), glyph.getV());\r
+                               GL11.glVertex3f(glyphX, glyphY, 0);\r
+                               GL11.glTexCoord2f(glyph.getU(), glyph.getV2());\r
+                               GL11.glVertex3f(glyphX, glyphY + glyph.getHeight(), 0);\r
+                               GL11.glTexCoord2f(glyph.getU2(), glyph.getV2());\r
+                               GL11.glVertex3f(glyphX + glyph.getWidth(), glyphY + glyph.getHeight(), 0);\r
+                               GL11.glTexCoord2f(glyph.getU2(), glyph.getV());\r
+                               GL11.glVertex3f(glyphX + glyph.getWidth(), glyphY, 0);\r
+                       }\r
+\r
+                       if (glyphIndex > 0) extraX += paddingRight + paddingLeft + paddingAdvanceX;\r
+                       maxWidth = Math.max(maxWidth, bounds.x + extraX + bounds.width);\r
+                       totalHeight = Math.max(totalHeight, ascent + bounds.y + bounds.height);\r
+\r
+                       if (codePoint == '\n') {\r
+                               startNewLine = true; // Mac gives -1 for bounds.x of '\n', so use the bounds.x of the next glyph.\r
+                               extraY += getLineHeight();\r
+                               lines++;\r
+                               totalHeight = 0;\r
+                       }\r
+               }\r
+               if (lastBind != null) GL11.glEnd();\r
+\r
+               if (displayList != null) {\r
+                       GL11.glEndList();\r
+                       // Invalidate the display list if it had glyphs that need to be loaded.\r
+                       if (!queuedGlyphs.isEmpty()) displayList.invalid = true;\r
+               }\r
+\r
+               GL11.glTranslatef(-x, -y, 0);\r
+\r
+               if (displayList == null) displayList = new DisplayList();\r
+               displayList.width = (short)maxWidth;\r
+               displayList.height = (short)(lines * getLineHeight() + totalHeight);\r
+               return displayList;\r
+       }\r
+\r
+       public void drawString (float x, float y, String text, Color color, int startIndex, int endIndex) {\r
+               drawDisplayList(x, y, text, color, startIndex, endIndex);\r
+       }\r
+\r
+       public void drawString (float x, float y, String text) {\r
+               drawString(x, y, text, Color.WHITE);\r
+       }\r
+\r
+       public void drawString (float x, float y, String text, Color col) {\r
+               drawString(x, y, text, col, 0, text.length());\r
+       }\r
+\r
+       /**\r
+        * Returns the glyph for the specified codePoint. If the glyph does not exist yet, it is created and queued to be loaded.\r
+        */\r
+       private Glyph getGlyph (int glyphCode, int codePoint, Rectangle bounds, GlyphVector vector, int index) {\r
+               if (glyphCode < 0 || glyphCode >= MAX_GLYPH_CODE) {\r
+                       // GlyphVector#getGlyphCode sometimes returns negative numbers on OS X!?\r
+                       return new Glyph(codePoint, bounds, vector, index, this) {\r
+                               public boolean isMissing () {\r
+                                       return true;\r
+                               }\r
+                       };\r
+               }\r
+               int pageIndex = glyphCode / PAGE_SIZE;\r
+               int glyphIndex = glyphCode & (PAGE_SIZE - 1);\r
+               Glyph glyph = null;\r
+               Glyph[] page = glyphs[pageIndex];\r
+               if (page != null) {\r
+                       glyph = page[glyphIndex];\r
+                       if (glyph != null) return glyph;\r
+               } else\r
+                       page = glyphs[pageIndex] = new Glyph[PAGE_SIZE];\r
+               // Add glyph so size information is available and queue it so its image can be loaded later.\r
+               glyph = page[glyphIndex] = new Glyph(codePoint, bounds, vector, index, this);\r
+               queuedGlyphs.add(glyph);\r
+               return glyph;\r
+       }\r
+\r
+       private Rectangle getGlyphBounds (GlyphVector vector, int index, int codePoint) {\r
+               Rectangle bounds = vector.getGlyphPixelBounds(index, GlyphPage.renderContext, 0, 0);\r
+               if (codePoint == ' ') bounds.width = spaceWidth;\r
+               return bounds;\r
+       }\r
+\r
+       public int getSpaceWidth () {\r
+               return spaceWidth;\r
+       }\r
+\r
+       public int getWidth (String text) {\r
+               if (text == null) throw new IllegalArgumentException("text cannot be null.");\r
+               if (text.length() == 0) return 0;\r
+\r
+               if (displayListCaching) {\r
+                       DisplayList displayList = (DisplayList)displayLists.get(text);\r
+                       if (displayList != null) return displayList.width;\r
+               }\r
+\r
+               char[] chars = text.toCharArray();\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+\r
+               int width = 0;\r
+               int extraX = 0;\r
+               boolean startNewLine = false;\r
+               for (int glyphIndex = 0, n = vector.getNumGlyphs(); glyphIndex < n; glyphIndex++) {\r
+                       int charIndex = vector.getGlyphCharIndex(glyphIndex);\r
+                       int codePoint = text.codePointAt(charIndex);\r
+                       Rectangle bounds = getGlyphBounds(vector, glyphIndex, codePoint);\r
+\r
+                       if (startNewLine && codePoint != '\n') extraX = -bounds.x;\r
+\r
+                       if (glyphIndex > 0) extraX += paddingLeft + paddingRight + paddingAdvanceX;\r
+                       width = Math.max(width, bounds.x + extraX + bounds.width);\r
+\r
+                       if (codePoint == '\n') startNewLine = true;\r
+               }\r
+\r
+               return width;\r
+       }\r
+\r
+       public int getHeight (String text) {\r
+               if (text == null) throw new IllegalArgumentException("text cannot be null.");\r
+               if (text.length() == 0) return 0;\r
+\r
+               if (displayListCaching) {\r
+                       DisplayList displayList = (DisplayList)displayLists.get(text);\r
+                       if (displayList != null) return displayList.height;\r
+               }\r
+\r
+               char[] chars = text.toCharArray();\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+\r
+               int lines = 0, height = 0;\r
+               for (int i = 0, n = vector.getNumGlyphs(); i < n; i++) {\r
+                       int charIndex = vector.getGlyphCharIndex(i);\r
+                       int codePoint = text.codePointAt(charIndex);\r
+                       if (codePoint == ' ') continue;\r
+                       Rectangle bounds = getGlyphBounds(vector, i, codePoint);\r
+\r
+                       height = Math.max(height, ascent + bounds.y + bounds.height);\r
+\r
+                       if (codePoint == '\n') {\r
+                               lines++;\r
+                               height = 0;\r
+                       }\r
+               }\r
+               return lines * getLineHeight() + height;\r
+       }\r
+\r
+       /**\r
+        * Returns the distance from the y drawing location to the top most pixel of the specified text.\r
+        */\r
+       public int getYOffset (String text) {\r
+               if (text == null) throw new IllegalArgumentException("text cannot be null.");\r
+\r
+               DisplayList displayList = null;\r
+               if (displayListCaching) {\r
+                       displayList = (DisplayList)displayLists.get(text);\r
+                       if (displayList != null && displayList.yOffset != null) return displayList.yOffset.intValue();\r
+               }\r
+\r
+               int index = text.indexOf('\n');\r
+               if (index != -1) text = text.substring(0, index);\r
+               char[] chars = text.toCharArray();\r
+               GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);\r
+               int yOffset = ascent + vector.getPixelBounds(null, 0, 0).y;\r
+\r
+               if (displayList != null) displayList.yOffset = new Short((short)yOffset);\r
+\r
+               return yOffset;\r
+       }\r
+\r
+       /**\r
+        * Returns the TrueTypeFont for this UnicodeFont.\r
+        */\r
+       public Font getFont () {\r
+               return font;\r
+       }\r
+\r
+       /**\r
+        * Returns the padding above a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public int getPaddingTop () {\r
+               return paddingTop;\r
+       }\r
+\r
+       /**\r
+        * Sets the padding above a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public void setPaddingTop (int paddingTop) {\r
+               this.paddingTop = paddingTop;\r
+       }\r
+\r
+       /**\r
+        * Returns the padding to the left of a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public int getPaddingLeft () {\r
+               return paddingLeft;\r
+       }\r
+\r
+       /**\r
+        * Sets the padding to the left of a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public void setPaddingLeft (int paddingLeft) {\r
+               this.paddingLeft = paddingLeft;\r
+       }\r
+\r
+       /**\r
+        * Returns the padding below a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public int getPaddingBottom () {\r
+               return paddingBottom;\r
+       }\r
+\r
+       /**\r
+        * Sets the padding below a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public void setPaddingBottom (int paddingBottom) {\r
+               this.paddingBottom = paddingBottom;\r
+       }\r
+\r
+       /**\r
+        * Returns the padding to the right of a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public int getPaddingRight () {\r
+               return paddingRight;\r
+       }\r
+\r
+       /**\r
+        * Sets the padding to the right of a glyph on the GlyphPage to allow for effects to be drawn.\r
+        */\r
+       public void setPaddingRight (int paddingRight) {\r
+               this.paddingRight = paddingRight;\r
+       }\r
+\r
+       /**\r
+        * Gets the additional amount to offset glyphs on the x axis.\r
+        */\r
+       public int getPaddingAdvanceX () {\r
+               return paddingAdvanceX;\r
+       }\r
+\r
+       /**\r
+        * Sets the additional amount to offset glyphs on the x axis. This is typically set to a negative number when left or right\r
+        * padding is used so that glyphs are not spaced too far apart.\r
+        */\r
+       public void setPaddingAdvanceX (int paddingAdvanceX) {\r
+               this.paddingAdvanceX = paddingAdvanceX;\r
+       }\r
+\r
+       /**\r
+        * Gets the additional amount to offset a line of text on the y axis.\r
+        */\r
+       public int getPaddingAdvanceY () {\r
+               return paddingAdvanceY;\r
+       }\r
+\r
+       /**\r
+        * Sets the additional amount to offset a line of text on the y axis. This is typically set to a negative number when top or\r
+        * bottom padding is used so that lines of text are not spaced too far apart.\r
+        */\r
+       public void setPaddingAdvanceY (int paddingAdvanceY) {\r
+               this.paddingAdvanceY = paddingAdvanceY;\r
+       }\r
+\r
+       /**\r
+        * Returns the distance from one line of text to the next. This is the sum of the descent, ascent, leading, padding top,\r
+        * padding bottom, and padding advance y. To change the line height, use {@link #setPaddingAdvanceY(int)}.\r
+        */\r
+       public int getLineHeight () {\r
+               return descent + ascent + leading + paddingTop + paddingBottom + paddingAdvanceY;\r
+       }\r
+\r
+       /**\r
+        * Gets the distance from the baseline to the y drawing location.\r
+        */\r
+       public int getAscent () {\r
+               return ascent;\r
+       }\r
+\r
+       /**\r
+        * Gets the distance from the baseline to the bottom of most alphanumeric characters with descenders.\r
+        */\r
+       public int getDescent () {\r
+               return descent;\r
+       }\r
+\r
+       /**\r
+        * Gets the extra distance between the descent of one line of text to the ascent of the next.\r
+        */\r
+       public int getLeading () {\r
+               return leading;\r
+       }\r
+\r
+       /**\r
+        * Returns the width of the backing textures.\r
+        */\r
+       public int getGlyphPageWidth () {\r
+               return glyphPageWidth;\r
+       }\r
+\r
+       /**\r
+        * Sets the width of the backing textures. Default is 512.\r
+        */\r
+       public void setGlyphPageWidth (int glyphPageWidth) {\r
+               this.glyphPageWidth = glyphPageWidth;\r
+       }\r
+\r
+       /**\r
+        * Returns the height of the backing textures.\r
+        */\r
+       public int getGlyphPageHeight () {\r
+               return glyphPageHeight;\r
+       }\r
+\r
+       /**\r
+        * Sets the height of the backing textures. Default is 512.\r
+        */\r
+       public void setGlyphPageHeight (int glyphPageHeight) {\r
+               this.glyphPageHeight = glyphPageHeight;\r
+       }\r
+\r
+       /**\r
+        * Returns the GlyphPages for this UnicodeFont.\r
+        */\r
+       public List getGlyphPages () {\r
+               return glyphPages;\r
+       }\r
+\r
+       /**\r
+        * Returns a list of {@link com.badlogic.gdx.hiero.unicodefont.effects.Effect}s that will be applied to the glyphs.\r
+        */\r
+       public List getEffects () {\r
+               return effects;\r
+       }\r
+\r
+       /**\r
+        * Returns true if this UnicodeFont caches the glyph drawing instructions to improve performance.\r
+        */\r
+       public boolean isCaching () {\r
+               return displayListCaching;\r
+       }\r
+\r
+       /**\r
+        * Sets if this UnicodeFont caches the glyph drawing instructions to improve performance. Default is true. Text rendering is\r
+        * very slow without display list caching.\r
+        */\r
+       public void setDisplayListCaching (boolean displayListCaching) {\r
+               this.displayListCaching = displayListCaching;\r
+       }\r
+\r
+       /**\r
+        * Returns the path to the TTF file for this UnicodeFont, or null. If this UnicodeFont was created without specifying the TTF\r
+        * file, it will try to determine the path using Sun classes. If this fails, null is returned.\r
+        */\r
+       public String getFontFile () {\r
+               if (ttfFileRef == null) {\r
+                       // Worst case if this UnicodeFont was loaded without a ttfFileRef, try to get the font file from Sun's classes.\r
+                       try {\r
+                               Object font2D = Class.forName("sun.font.FontManager").getDeclaredMethod("getFont2D", new Class[] {Font.class})\r
+                                       .invoke(null, new Object[] {font});\r
+                               Field platNameField = Class.forName("sun.font.PhysicalFont").getDeclaredField("platName");\r
+                               platNameField.setAccessible(true);\r
+                               ttfFileRef = (String)platNameField.get(font2D);\r
+                       } catch (Throwable ignored) {\r
+                       }\r
+                       if (ttfFileRef == null) ttfFileRef = "";\r
+               }\r
+               if (ttfFileRef.length() == 0) return null;\r
+               return ttfFileRef;\r
+       }\r
+\r
+       /**\r
+        * @param ttfFileRef The file system or classpath location of the TrueTypeFont file.\r
+        */\r
+       static private Font createFont (String ttfFileRef) {\r
+               try {\r
+                       return Font.createFont(Font.TRUETYPE_FONT, Gdx.files.readFile(ttfFileRef, FileType.Absolute));\r
+               } catch (FontFormatException ex) {\r
+                       throw new GdxRuntimeException("Invalid font: " + ttfFileRef, ex);\r
+               } catch (IOException ex) {\r
+                       throw new GdxRuntimeException("Error reading font: " + ttfFileRef, ex);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Sorts glyphs by height, tallest first.\r
+        */\r
+       static private final Comparator heightComparator = new Comparator() {\r
+               public int compare (Object o1, Object o2) {\r
+                       return ((Glyph)o1).getHeight() - ((Glyph)o2).getHeight();\r
+               }\r
+       };\r
+\r
+       public class DisplayList {\r
+               boolean invalid;\r
+               int id;\r
+               Short yOffset;\r
+\r
+               public short width, height;\r
+               public Object userData;\r
+\r
+               DisplayList () {\r
+               }\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/UnicodeFontTest.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/UnicodeFontTest.java
new file mode 100644 (file)
index 0000000..3ad0f4d
--- /dev/null
@@ -0,0 +1,68 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont;\r
+\r
+import static org.lwjgl.opengl.GL11.*;\r
+\r
+import org.lwjgl.opengl.GL11;\r
+\r
+import com.badlogic.gdx.Gdx;\r
+import com.badlogic.gdx.RenderListener;\r
+import com.badlogic.gdx.backends.desktop.LwjglApplication;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ColorEffect;\r
+\r
+public class UnicodeFontTest implements RenderListener {\r
+       private UnicodeFont unicodeFont;\r
+\r
+       public void surfaceCreated () {\r
+               unicodeFont = new UnicodeFont("c:/windows/fonts/arial.ttf", 48, false, false);\r
+               unicodeFont.getEffects().add(new ColorEffect(java.awt.Color.white));\r
+               // unicodeFont.addAsciiGlyphs();\r
+               // unicodeFont.loadGlyphs();\r
+       }\r
+\r
+       public void surfaceChanged (int width, int height) {\r
+               glViewport(0, 0, width, height);\r
+               glScissor(0, 0, width, height);\r
+               glEnable(GL_SCISSOR_TEST);\r
+\r
+               glMatrixMode(GL_PROJECTION);\r
+               glLoadIdentity();\r
+               glOrtho(0, width, height, 0, 1, -1);\r
+               glMatrixMode(GL_MODELVIEW);\r
+               glLoadIdentity();\r
+\r
+               glEnable(GL_TEXTURE_2D);\r
+               glEnableClientState(GL_TEXTURE_COORD_ARRAY);\r
+               glEnableClientState(GL_VERTEX_ARRAY);\r
+\r
+               glClearColor(0, 0, 0, 0);\r
+               glClearDepth(1);\r
+\r
+               glDisable(GL_LIGHTING);\r
+\r
+               glEnable(GL_BLEND);\r
+               glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\r
+       }\r
+\r
+       public void render () {\r
+               GL11.glClear(GL_COLOR_BUFFER_BIT);\r
+\r
+               unicodeFont.loadGlyphs(1);\r
+\r
+               String text = "This is UnicodeFont!\nIt rockz. Kerning: T,";\r
+               unicodeFont.setDisplayListCaching(false);\r
+               unicodeFont.drawString(10, 33, text);\r
+               unicodeFont.drawString(10, 330, text);\r
+\r
+               unicodeFont.addGlyphs("~!@!#!#$%___--");\r
+               // Cypriot Syllabary glyphs (Everson Mono font): \uD802\uDC02\uD802\uDC03\uD802\uDC12 == 0x10802, 0x10803, s0x10812\r
+       }\r
+\r
+       public void dispose () {\r
+       }\r
+\r
+       public static void main (String[] args) {\r
+               LwjglApplication app = new LwjglApplication("UnicodeFont Test", 800, 600, false);\r
+               app.getGraphics().setRenderListener(new UnicodeFontTest());\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ColorEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ColorEffect.java
new file mode 100644 (file)
index 0000000..97773e0
--- /dev/null
@@ -0,0 +1,60 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.Color;\r
+import java.awt.Graphics2D;\r
+import java.awt.image.BufferedImage;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+import com.badlogic.gdx.hiero.unicodefont.Glyph;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+\r
+/**\r
+ * Makes glyphs a solid color.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class ColorEffect implements ConfigurableEffect {\r
+       private Color color = Color.white;\r
+\r
+       public ColorEffect () {\r
+       }\r
+\r
+       public ColorEffect (Color color) {\r
+               this.color = color;\r
+       }\r
+\r
+       public void draw (BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph) {\r
+               g.setColor(color);\r
+               g.fill(glyph.getShape());\r
+       }\r
+\r
+       public Color getColor () {\r
+               return color;\r
+       }\r
+\r
+       public void setColor (Color color) {\r
+               if (color == null) throw new IllegalArgumentException("color cannot be null.");\r
+               this.color = color;\r
+       }\r
+\r
+       public String toString () {\r
+               return "Color";\r
+       }\r
+\r
+       public List getValues () {\r
+               List values = new ArrayList();\r
+               values.add(EffectUtil.colorValue("Color", color));\r
+               return values;\r
+       }\r
+\r
+       public void setValues (List values) {\r
+               for (Iterator iter = values.iterator(); iter.hasNext();) {\r
+                       Value value = (Value)iter.next();\r
+                       if (value.getName().equals("Color")) {\r
+                               setColor((Color)value.getObject());\r
+                       }\r
+               }\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ConfigurableEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ConfigurableEffect.java
new file mode 100644 (file)
index 0000000..97fe5f6
--- /dev/null
@@ -0,0 +1,52 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.util.List;\r
+\r
+/**\r
+ * An effect that has a number of configuration values. This allows the effect to be configured in the Hiero GUI and to be saved\r
+ * and loaded to and from a file.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public interface ConfigurableEffect extends Effect {\r
+       /**\r
+        * Returns the list of {@link Value}s for this effect. This list is not typically backed by the effect, so changes to the\r
+        * values will not take affect until {@link #setValues(List)} is called.\r
+        */\r
+       public List getValues ();\r
+\r
+       /**\r
+        * Sets the list of {@link Value}s for this effect.\r
+        */\r
+       public void setValues (List values);\r
+\r
+       /**\r
+        * Represents a configurable value for an effect.\r
+        */\r
+       static public interface Value {\r
+               /**\r
+                * Returns the name of the value.\r
+                */\r
+               public String getName ();\r
+\r
+               /**\r
+                * Sets the string representation of the value.\r
+                */\r
+               public void setString (String value);\r
+\r
+               /**\r
+                * Gets the string representation of the value.\r
+                */\r
+               public String getString ();\r
+\r
+               /**\r
+                * Gets the object representation of the value.\r
+                */\r
+               public Object getObject ();\r
+\r
+               /**\r
+                * Shows a dialog allowing a user to configure this value.\r
+                */\r
+               public void showDialog ();\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/Effect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/Effect.java
new file mode 100644 (file)
index 0000000..ae2bb7b
--- /dev/null
@@ -0,0 +1,19 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.Graphics2D;\r
+import java.awt.image.BufferedImage;\r
+\r
+import com.badlogic.gdx.hiero.unicodefont.Glyph;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+\r
+/**\r
+ * A graphical effect that is applied to glyphs in a {@link UnicodeFont}.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public interface Effect {\r
+       /**\r
+        * Called to draw the effect.\r
+        */\r
+       public void draw (BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph);\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/EffectUtil.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/EffectUtil.java
new file mode 100644 (file)
index 0000000..a230ee1
--- /dev/null
@@ -0,0 +1,292 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.AlphaComposite;\r
+import java.awt.Color;\r
+import java.awt.Dimension;\r
+import java.awt.EventQueue;\r
+import java.awt.Graphics2D;\r
+import java.awt.GridBagConstraints;\r
+import java.awt.GridBagLayout;\r
+import java.awt.Insets;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.image.BufferedImage;\r
+\r
+import javax.swing.BorderFactory;\r
+import javax.swing.DefaultComboBoxModel;\r
+import javax.swing.JButton;\r
+import javax.swing.JCheckBox;\r
+import javax.swing.JColorChooser;\r
+import javax.swing.JComboBox;\r
+import javax.swing.JComponent;\r
+import javax.swing.JDialog;\r
+import javax.swing.JLabel;\r
+import javax.swing.JPanel;\r
+import javax.swing.JSpinner;\r
+import javax.swing.JTextArea;\r
+import javax.swing.SpinnerNumberModel;\r
+\r
+import com.badlogic.gdx.hiero.unicodefont.GlyphPage;\r
+import com.badlogic.gdx.hiero.unicodefont.effects.ConfigurableEffect.Value;\r
+\r
+/**\r
+ * Provides utility methods for effects.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class EffectUtil {\r
+       static private BufferedImage scratchImage = new BufferedImage(GlyphPage.MAX_GLYPH_SIZE, GlyphPage.MAX_GLYPH_SIZE,\r
+               BufferedImage.TYPE_INT_ARGB);\r
+\r
+       /**\r
+        * Returns an image that can be used by effects as a temp image.\r
+        */\r
+       static public BufferedImage getScratchImage () {\r
+               Graphics2D g = (Graphics2D)scratchImage.getGraphics();\r
+               g.setComposite(AlphaComposite.Clear);\r
+               g.fillRect(0, 0, GlyphPage.MAX_GLYPH_SIZE, GlyphPage.MAX_GLYPH_SIZE);\r
+               g.setComposite(AlphaComposite.SrcOver);\r
+               g.setColor(java.awt.Color.white);\r
+               return scratchImage;\r
+       }\r
+\r
+       /**\r
+        * Returns a value that represents a color.\r
+        */\r
+       static public Value colorValue (String name, Color currentValue) {\r
+               return new DefaultValue(name, EffectUtil.toString(currentValue)) {\r
+                       public void showDialog () {\r
+                               Color newColor = JColorChooser.showDialog(null, "Choose a color", EffectUtil.fromString(value));\r
+                               if (newColor != null) value = EffectUtil.toString(newColor);\r
+                       }\r
+\r
+                       public Object getObject () {\r
+                               return EffectUtil.fromString(value);\r
+                       }\r
+               };\r
+       }\r
+\r
+       /**\r
+        * Returns a value that represents an int.\r
+        */\r
+       static public Value intValue (String name, final int currentValue, final String description) {\r
+               return new DefaultValue(name, String.valueOf(currentValue)) {\r
+                       public void showDialog () {\r
+                               JSpinner spinner = new JSpinner(new SpinnerNumberModel(currentValue, Short.MIN_VALUE, Short.MAX_VALUE, 1));\r
+                               if (showValueDialog(spinner, description)) value = String.valueOf(spinner.getValue());\r
+                       }\r
+\r
+                       public Object getObject () {\r
+                               return Integer.valueOf(value);\r
+                       }\r
+               };\r
+       }\r
+\r
+       /**\r
+        * Returns a value that represents a float, from 0 to 1 (inclusive).\r
+        */\r
+       static public Value floatValue (String name, final float currentValue, final float min, final float max,\r
+               final String description) {\r
+               return new DefaultValue(name, String.valueOf(currentValue)) {\r
+                       public void showDialog () {\r
+                               JSpinner spinner = new JSpinner(new SpinnerNumberModel(currentValue, min, max, 0.1f));\r
+                               if (showValueDialog(spinner, description)) value = String.valueOf(((Double)spinner.getValue()).floatValue());\r
+                       }\r
+\r
+                       public Object getObject () {\r
+                               return Float.valueOf(value);\r
+                       }\r
+               };\r
+       }\r
+\r
+       /**\r
+        * Returns a value that represents a boolean.\r
+        */\r
+       static public Value booleanValue (String name, final boolean currentValue, final String description) {\r
+               return new DefaultValue(name, String.valueOf(currentValue)) {\r
+                       public void showDialog () {\r
+                               JCheckBox checkBox = new JCheckBox();\r
+                               checkBox.setSelected(currentValue);\r
+                               if (showValueDialog(checkBox, description)) value = String.valueOf(checkBox.isSelected());\r
+                       }\r
+\r
+                       public Object getObject () {\r
+                               return Boolean.valueOf(value);\r
+                       }\r
+               };\r
+       }\r
+\r
+       /**\r
+        * Returns a value that represents a fixed number of options. All options are strings.\r
+        * @param options The first array has an entry for each option. Each entry is either a String[1] that is both the display value\r
+        *           and actual value, or a String[2] whose first element is the display value and second element is the actual value.\r
+        */\r
+       static public Value optionValue (String name, final String currentValue, final String[][] options, final String description) {\r
+               return new DefaultValue(name, currentValue.toString()) {\r
+                       public void showDialog () {\r
+                               int selectedIndex = -1;\r
+                               DefaultComboBoxModel model = new DefaultComboBoxModel();\r
+                               for (int i = 0; i < options.length; i++) {\r
+                                       model.addElement(options[i][0]);\r
+                                       if (getValue(i).equals(currentValue)) selectedIndex = i;\r
+                               }\r
+                               JComboBox comboBox = new JComboBox(model);\r
+                               comboBox.setSelectedIndex(selectedIndex);\r
+                               if (showValueDialog(comboBox, description)) value = getValue(comboBox.getSelectedIndex());\r
+                       }\r
+\r
+                       private String getValue (int i) {\r
+                               if (options[i].length == 1) return options[i][0];\r
+                               return options[i][1];\r
+                       }\r
+\r
+                       public String toString () {\r
+                               for (int i = 0; i < options.length; i++)\r
+                                       if (getValue(i).equals(value)) return options[i][0].toString();\r
+                               return "";\r
+                       }\r
+\r
+                       public Object getObject () {\r
+                               return value;\r
+                       }\r
+               };\r
+       }\r
+\r
+       /**\r
+        * Convers a color to a string.\r
+        */\r
+       static public String toString (Color color) {\r
+               if (color == null) throw new IllegalArgumentException("color cannot be null.");\r
+               String r = Integer.toHexString(color.getRed());\r
+               if (r.length() == 1) r = "0" + r;\r
+               String g = Integer.toHexString(color.getGreen());\r
+               if (g.length() == 1) g = "0" + g;\r
+               String b = Integer.toHexString(color.getBlue());\r
+               if (b.length() == 1) b = "0" + b;\r
+               return r + g + b;\r
+       }\r
+\r
+       /**\r
+        * Converts a string to a color.\r
+        */\r
+       static public Color fromString (String rgb) {\r
+               if (rgb == null || rgb.length() != 6) return Color.white;\r
+               return new Color(Integer.parseInt(rgb.substring(0, 2), 16), Integer.parseInt(rgb.substring(2, 4), 16), Integer.parseInt(rgb\r
+                       .substring(4, 6), 16));\r
+       }\r
+\r
+       /**\r
+        * Provides generic functionality for an effect's configurable value.\r
+        */\r
+       static private abstract class DefaultValue implements Value {\r
+               String value;\r
+               String name;\r
+\r
+               public DefaultValue (String name, String value) {\r
+                       this.value = value;\r
+                       this.name = name;\r
+               }\r
+\r
+               public void setString (String value) {\r
+                       this.value = value;\r
+               }\r
+\r
+               public String getString () {\r
+                       return value;\r
+               }\r
+\r
+               public String getName () {\r
+                       return name;\r
+               }\r
+\r
+               public String toString () {\r
+                       if (value == null) return "";\r
+                       return value.toString();\r
+               }\r
+\r
+               public boolean showValueDialog (final JComponent component, String description) {\r
+                       ValueDialog dialog = new ValueDialog(component, name, description);\r
+                       dialog.setTitle(name);\r
+                       dialog.setLocationRelativeTo(null);\r
+                       EventQueue.invokeLater(new Runnable() {\r
+                               public void run () {\r
+                                       JComponent focusComponent = component;\r
+                                       if (focusComponent instanceof JSpinner)\r
+                                               focusComponent = ((JSpinner.DefaultEditor)((JSpinner)component).getEditor()).getTextField();\r
+                                       focusComponent.requestFocusInWindow();\r
+                               }\r
+                       });\r
+                       dialog.setVisible(true);\r
+                       return dialog.okPressed;\r
+               }\r
+       };\r
+\r
+       /**\r
+        * Provides generic functionality for a dialog to configure a value.\r
+        */\r
+       static private class ValueDialog extends JDialog {\r
+               public boolean okPressed = false;\r
+\r
+               public ValueDialog (JComponent component, String name, String description) {\r
+                       setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);\r
+                       setLayout(new GridBagLayout());\r
+                       setModal(true);\r
+\r
+                       if (component instanceof JSpinner)\r
+                               ((JSpinner.DefaultEditor)((JSpinner)component).getEditor()).getTextField().setColumns(4);\r
+\r
+                       JPanel descriptionPanel = new JPanel();\r
+                       descriptionPanel.setLayout(new GridBagLayout());\r
+                       getContentPane().add(\r
+                               descriptionPanel,\r
+                               new GridBagConstraints(0, 0, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0,\r
+                                       0), 0, 0));\r
+                       descriptionPanel.setBackground(Color.white);\r
+                       descriptionPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.black));\r
+                       {\r
+                               JTextArea descriptionText = new JTextArea(description);\r
+                               descriptionPanel.add(descriptionText, new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,\r
+                                       GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));\r
+                               descriptionText.setWrapStyleWord(true);\r
+                               descriptionText.setLineWrap(true);\r
+                               descriptionText.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));\r
+                               descriptionText.setEditable(false);\r
+                       }\r
+\r
+                       JPanel panel = new JPanel();\r
+                       getContentPane().add(\r
+                               panel,\r
+                               new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(5, 5, 0,\r
+                                       5), 0, 0));\r
+                       panel.add(new JLabel(name + ":"));\r
+                       panel.add(component);\r
+\r
+                       JPanel buttonPanel = new JPanel();\r
+                       getContentPane().add(\r
+                               buttonPanel,\r
+                               new GridBagConstraints(0, 2, 2, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE,\r
+                                       new Insets(0, 0, 0, 0), 0, 0));\r
+                       {\r
+                               JButton okButton = new JButton("OK");\r
+                               buttonPanel.add(okButton);\r
+                               okButton.addActionListener(new ActionListener() {\r
+                                       public void actionPerformed (ActionEvent evt) {\r
+                                               okPressed = true;\r
+                                               setVisible(false);\r
+                                       }\r
+                               });\r
+                       }\r
+                       {\r
+                               JButton cancelButton = new JButton("Cancel");\r
+                               buttonPanel.add(cancelButton);\r
+                               cancelButton.addActionListener(new ActionListener() {\r
+                                       public void actionPerformed (ActionEvent evt) {\r
+                                               setVisible(false);\r
+                                       }\r
+                               });\r
+                       }\r
+\r
+                       setSize(new Dimension(320, 175));\r
+               }\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/FilterEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/FilterEffect.java
new file mode 100644 (file)
index 0000000..059ea39
--- /dev/null
@@ -0,0 +1,38 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.Graphics2D;\r
+import java.awt.image.BufferedImage;\r
+import java.awt.image.BufferedImageOp;\r
+\r
+import com.badlogic.gdx.hiero.unicodefont.Glyph;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+\r
+/**\r
+ * Applys a {@link BufferedImageOp} filter to glyphs. Many filters can be fond here: http://www.jhlabs.com/ip/filters/index.html\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class FilterEffect implements Effect {\r
+       private BufferedImageOp filter;\r
+\r
+       public FilterEffect () {\r
+       }\r
+\r
+       public FilterEffect (BufferedImageOp filter) {\r
+               this.filter = filter;\r
+       }\r
+\r
+       public void draw (BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph) {\r
+               BufferedImage scratchImage = EffectUtil.getScratchImage();\r
+               filter.filter(image, scratchImage);\r
+               image.getGraphics().drawImage(scratchImage, 0, 0, null);\r
+       }\r
+\r
+       public BufferedImageOp getFilter () {\r
+               return filter;\r
+       }\r
+\r
+       public void setFilter (BufferedImageOp filter) {\r
+               this.filter = filter;\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/GradientEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/GradientEffect.java
new file mode 100644 (file)
index 0000000..45ba061
--- /dev/null
@@ -0,0 +1,123 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.Color;\r
+import java.awt.GradientPaint;\r
+import java.awt.Graphics2D;\r
+import java.awt.image.BufferedImage;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+import com.badlogic.gdx.hiero.unicodefont.Glyph;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+\r
+/**\r
+ * Paints glyphs with a gradient fill.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class GradientEffect implements ConfigurableEffect {\r
+       private Color topColor = Color.cyan, bottomColor = Color.blue;\r
+       private int offset = 0;\r
+       private float scale = 1;\r
+       private boolean cyclic;\r
+\r
+       public GradientEffect () {\r
+       }\r
+\r
+       public GradientEffect (Color topColor, Color bottomColor, float scale) {\r
+               this.topColor = topColor;\r
+               this.bottomColor = bottomColor;\r
+               this.scale = scale;\r
+       }\r
+\r
+       public void draw (BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph) {\r
+               int ascent = unicodeFont.getAscent();\r
+               float height = (ascent) * scale;\r
+               float top = -glyph.getYOffset() + unicodeFont.getDescent() + offset + ascent / 2 - height / 2;\r
+               g.setPaint(new GradientPaint(0, top, topColor, 0, top + height, bottomColor, cyclic));\r
+               g.fill(glyph.getShape());\r
+       }\r
+\r
+       public Color getTopColor () {\r
+               return topColor;\r
+       }\r
+\r
+       public void setTopColor (Color topColor) {\r
+               this.topColor = topColor;\r
+       }\r
+\r
+       public Color getBottomColor () {\r
+               return bottomColor;\r
+       }\r
+\r
+       public void setBottomColor (Color bottomColor) {\r
+               this.bottomColor = bottomColor;\r
+       }\r
+\r
+       public int getOffset () {\r
+               return offset;\r
+       }\r
+\r
+       /**\r
+        * Sets the pixel offset to move the gradient up or down. The gradient is normally centered on the glyph.\r
+        */\r
+       public void setOffset (int offset) {\r
+               this.offset = offset;\r
+       }\r
+\r
+       public float getScale () {\r
+               return scale;\r
+       }\r
+\r
+       /**\r
+        * Changes the height of the gradient by a percentage. The gradient is normally the height of most glyphs in the font.\r
+        */\r
+       public void setScale (float scale) {\r
+               this.scale = scale;\r
+       }\r
+\r
+       public boolean isCyclic () {\r
+               return cyclic;\r
+       }\r
+\r
+       /**\r
+        * If set to true, the gradient will repeat.\r
+        */\r
+       public void setCyclic (boolean cyclic) {\r
+               this.cyclic = cyclic;\r
+       }\r
+\r
+       public String toString () {\r
+               return "Gradient";\r
+       }\r
+\r
+       public List getValues () {\r
+               List values = new ArrayList();\r
+               values.add(EffectUtil.colorValue("Top color", topColor));\r
+               values.add(EffectUtil.colorValue("Bottom color", bottomColor));\r
+               values.add(EffectUtil.intValue("Offset", offset,\r
+                       "This setting allows you to move the gradient up or down. The gradient is normally centered on the glyph."));\r
+               values.add(EffectUtil.floatValue("Scale", scale, 0, 10, "This setting allows you to change the height of the gradient by a"\r
+                       + "percentage. The gradient is normally the height of most glyphs in the font."));\r
+               values.add(EffectUtil.booleanValue("Cyclic", cyclic, "If this setting is checked, the gradient will repeat."));\r
+               return values;\r
+       }\r
+\r
+       public void setValues (List values) {\r
+               for (Iterator iter = values.iterator(); iter.hasNext();) {\r
+                       Value value = (Value)iter.next();\r
+                       if (value.getName().equals("Top color")) {\r
+                               topColor = (Color)value.getObject();\r
+                       } else if (value.getName().equals("Bottom color")) {\r
+                               bottomColor = (Color)value.getObject();\r
+                       } else if (value.getName().equals("Offset")) {\r
+                               offset = ((Integer)value.getObject()).intValue();\r
+                       } else if (value.getName().equals("Scale")) {\r
+                               scale = ((Float)value.getObject()).floatValue();\r
+                       } else if (value.getName().equals("Cyclic")) {\r
+                               cyclic = ((Boolean)value.getObject()).booleanValue();\r
+                       }\r
+               }\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineEffect.java
new file mode 100644 (file)
index 0000000..9adc0ec
--- /dev/null
@@ -0,0 +1,116 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.BasicStroke;\r
+import java.awt.Color;\r
+import java.awt.Graphics2D;\r
+import java.awt.Stroke;\r
+import java.awt.image.BufferedImage;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+import com.badlogic.gdx.hiero.unicodefont.Glyph;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+\r
+/**\r
+ * Strokes glyphs with an outline.\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class OutlineEffect implements ConfigurableEffect {\r
+       private float width = 2;\r
+       private Color color = Color.black;\r
+       private int join = BasicStroke.JOIN_BEVEL;\r
+       private Stroke stroke;\r
+\r
+       public OutlineEffect () {\r
+       }\r
+\r
+       public OutlineEffect (int width, Color color) {\r
+               this.width = width;\r
+               this.color = color;\r
+       }\r
+\r
+       public void draw (BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph) {\r
+               g = (Graphics2D)g.create();\r
+               if (stroke != null)\r
+                       g.setStroke(stroke);\r
+               else\r
+                       g.setStroke(getStroke());\r
+               g.setColor(color);\r
+               g.draw(glyph.getShape());\r
+               g.dispose();\r
+       }\r
+\r
+       public float getWidth () {\r
+               return width;\r
+       }\r
+\r
+       /**\r
+        * Sets the width of the outline. The glyphs will need padding so the outline doesn't get clipped.\r
+        */\r
+       public void setWidth (int width) {\r
+               this.width = width;\r
+       }\r
+\r
+       public Color getColor () {\r
+               return color;\r
+       }\r
+\r
+       public void setColor (Color color) {\r
+               this.color = color;\r
+       }\r
+\r
+       public int getJoin () {\r
+               return join;\r
+       }\r
+\r
+       public Stroke getStroke () {\r
+               if (stroke == null) return new BasicStroke(width, BasicStroke.CAP_SQUARE, join);\r
+               return stroke;\r
+       }\r
+\r
+       /**\r
+        * Sets the stroke to use for the outline. If this is set, the other outline settings are ignored.\r
+        */\r
+       public void setStroke (Stroke stroke) {\r
+               this.stroke = stroke;\r
+       }\r
+\r
+       /**\r
+        * Sets how the corners of the outline are drawn. This is usually only noticeable at large outline widths.\r
+        * @param join One of: {@link BasicStroke#JOIN_BEVEL}, {@link BasicStroke#JOIN_MITER}, {@link BasicStroke#JOIN_ROUND}\r
+        */\r
+       public void setJoin (int join) {\r
+               this.join = join;\r
+       }\r
+\r
+       public String toString () {\r
+               return "Outline";\r
+       }\r
+\r
+       public List getValues () {\r
+               List values = new ArrayList();\r
+               values.add(EffectUtil.colorValue("Color", color));\r
+               values.add(EffectUtil.floatValue("Width", width, 0.1f, 999, "This setting controls the width of the outline. "\r
+                       + "The glyphs will need padding so the outline doesn't get clipped."));\r
+               values.add(EffectUtil.optionValue("Join", String.valueOf(join), new String[][] { {"Bevel", BasicStroke.JOIN_BEVEL + ""},\r
+                       {"Miter", BasicStroke.JOIN_MITER + ""}, {"Round", BasicStroke.JOIN_ROUND + ""}},\r
+                       "This setting defines how the corners of the outline are drawn. "\r
+                               + "This is usually only noticeable at large outline widths."));\r
+               return values;\r
+       }\r
+\r
+       public void setValues (List values) {\r
+               for (Iterator iter = values.iterator(); iter.hasNext();) {\r
+                       Value value = (Value)iter.next();\r
+                       if (value.getName().equals("Color")) {\r
+                               color = (Color)value.getObject();\r
+                       } else if (value.getName().equals("Width")) {\r
+                               width = ((Float)value.getObject()).floatValue();\r
+                       } else if (value.getName().equals("Join")) {\r
+                               join = Integer.parseInt((String)value.getObject());\r
+                       }\r
+               }\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineWobbleEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineWobbleEffect.java
new file mode 100644 (file)
index 0000000..9340954
--- /dev/null
@@ -0,0 +1,125 @@
+/*\r
+ * Copyright 2006 Jerry Huxtable\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the\r
+ * License. You may obtain a copy of the License at\r
+ * \r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"\r
+ * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language\r
+ * governing permissions and limitations under the License.\r
+ */\r
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.BasicStroke;\r
+import java.awt.Color;\r
+import java.awt.Shape;\r
+import java.awt.Stroke;\r
+import java.awt.geom.FlatteningPathIterator;\r
+import java.awt.geom.GeneralPath;\r
+import java.awt.geom.PathIterator;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+/**\r
+ * @author Jerry Huxtable\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class OutlineWobbleEffect extends OutlineEffect {\r
+       float detail = 1;\r
+       float amplitude = 1;\r
+\r
+       public OutlineWobbleEffect () {\r
+               setStroke(new WobbleStroke());\r
+       }\r
+\r
+       public OutlineWobbleEffect (int width, Color color) {\r
+               super(width, color);\r
+       }\r
+\r
+       public String toString () {\r
+               return "Outline (Wobble)";\r
+       }\r
+\r
+       public List getValues () {\r
+               List values = super.getValues();\r
+               values.remove(2); // Remove "Join".\r
+               values.add(EffectUtil.floatValue("Detail", detail, 1, 50, "This setting controls how detailed the outline will be. "\r
+                       + "Smaller numbers cause the outline to have more detail."));\r
+               values.add(EffectUtil.floatValue("Amplitude", amplitude, 0.5f, 50, "This setting controls the amplitude of the outline."));\r
+               return values;\r
+       }\r
+\r
+       public void setValues (List values) {\r
+               super.setValues(values);\r
+               for (Iterator iter = values.iterator(); iter.hasNext();) {\r
+                       Value value = (Value)iter.next();\r
+                       if (value.getName().equals("Detail")) {\r
+                               detail = ((Float)value.getObject()).floatValue();\r
+                       } else if (value.getName().equals("Amplitude")) {\r
+                               amplitude = ((Float)value.getObject()).floatValue();\r
+                       }\r
+               }\r
+       }\r
+\r
+       class WobbleStroke implements Stroke {\r
+               private static final float FLATNESS = 1;\r
+\r
+               public Shape createStrokedShape (Shape shape) {\r
+                       GeneralPath result = new GeneralPath();\r
+                       shape = new BasicStroke(getWidth(), BasicStroke.CAP_SQUARE, getJoin()).createStrokedShape(shape);\r
+                       PathIterator it = new FlatteningPathIterator(shape.getPathIterator(null), FLATNESS);\r
+                       float points[] = new float[6];\r
+                       float moveX = 0, moveY = 0;\r
+                       float lastX = 0, lastY = 0;\r
+                       float thisX = 0, thisY = 0;\r
+                       int type = 0;\r
+                       float next = 0;\r
+                       while (!it.isDone()) {\r
+                               type = it.currentSegment(points);\r
+                               switch (type) {\r
+                               case PathIterator.SEG_MOVETO:\r
+                                       moveX = lastX = randomize(points[0]);\r
+                                       moveY = lastY = randomize(points[1]);\r
+                                       result.moveTo(moveX, moveY);\r
+                                       next = 0;\r
+                                       break;\r
+\r
+                               case PathIterator.SEG_CLOSE:\r
+                                       points[0] = moveX;\r
+                                       points[1] = moveY;\r
+                                       // Fall into....\r
+\r
+                               case PathIterator.SEG_LINETO:\r
+                                       thisX = randomize(points[0]);\r
+                                       thisY = randomize(points[1]);\r
+                                       float dx = thisX - lastX;\r
+                                       float dy = thisY - lastY;\r
+                                       float distance = (float)Math.sqrt(dx * dx + dy * dy);\r
+                                       if (distance >= next) {\r
+                                               float r = 1.0f / distance;\r
+                                               while (distance >= next) {\r
+                                                       float x = lastX + next * dx * r;\r
+                                                       float y = lastY + next * dy * r;\r
+                                                       result.lineTo(randomize(x), randomize(y));\r
+                                                       next += detail;\r
+                                               }\r
+                                       }\r
+                                       next -= distance;\r
+                                       lastX = thisX;\r
+                                       lastY = thisY;\r
+                                       break;\r
+                               }\r
+                               it.next();\r
+                       }\r
+\r
+                       return result;\r
+               }\r
+\r
+               private float randomize (float x) {\r
+                       return x + (float)Math.random() * amplitude * 2 - 1;\r
+               }\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineZigzagEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/OutlineZigzagEffect.java
new file mode 100644 (file)
index 0000000..cbadfb3
--- /dev/null
@@ -0,0 +1,125 @@
+/*\r
+ * Copyright 2006 Jerry Huxtable\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the\r
+ * License. You may obtain a copy of the License at\r
+ * \r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"\r
+ * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language\r
+ * governing permissions and limitations under the License.\r
+ */\r
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.BasicStroke;\r
+import java.awt.Color;\r
+import java.awt.Shape;\r
+import java.awt.Stroke;\r
+import java.awt.geom.FlatteningPathIterator;\r
+import java.awt.geom.GeneralPath;\r
+import java.awt.geom.PathIterator;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+/**\r
+ * @author Jerry Huxtable\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class OutlineZigzagEffect extends OutlineEffect {\r
+       float amplitude = 1;\r
+       float wavelength = 3;\r
+\r
+       public OutlineZigzagEffect () {\r
+               setStroke(new ZigzagStroke());\r
+       }\r
+\r
+       public OutlineZigzagEffect (int width, Color color) {\r
+               super(width, color);\r
+       }\r
+\r
+       public String toString () {\r
+               return "Outline (Zigzag)";\r
+       }\r
+\r
+       public List getValues () {\r
+               List values = super.getValues();\r
+               values.add(EffectUtil.floatValue("Wavelength", wavelength, 1, 100, "This setting controls the wavelength of the outline. "\r
+                       + "The smaller the value, the more segments will be used to draw the outline."));\r
+               values.add(EffectUtil.floatValue("Amplitude", amplitude, 0.5f, 50, "This setting controls the amplitude of the outline. "\r
+                       + "The bigger the value, the more the zigzags will vary."));\r
+               return values;\r
+       }\r
+\r
+       public void setValues (List values) {\r
+               super.setValues(values);\r
+               for (Iterator iter = values.iterator(); iter.hasNext();) {\r
+                       Value value = (Value)iter.next();\r
+                       if (value.getName().equals("Wavelength")) {\r
+                               wavelength = ((Float)value.getObject()).floatValue();\r
+                       } else if (value.getName().equals("Amplitude")) {\r
+                               amplitude = ((Float)value.getObject()).floatValue();\r
+                       }\r
+               }\r
+       }\r
+\r
+       class ZigzagStroke implements Stroke {\r
+               private static final float FLATNESS = 1;\r
+\r
+               public Shape createStrokedShape (Shape shape) {\r
+                       GeneralPath result = new GeneralPath();\r
+                       PathIterator it = new FlatteningPathIterator(shape.getPathIterator(null), FLATNESS);\r
+                       float points[] = new float[6];\r
+                       float moveX = 0, moveY = 0;\r
+                       float lastX = 0, lastY = 0;\r
+                       float thisX = 0, thisY = 0;\r
+                       int type = 0;\r
+                       float next = 0;\r
+                       int phase = 0;\r
+                       while (!it.isDone()) {\r
+                               type = it.currentSegment(points);\r
+                               switch (type) {\r
+                               case PathIterator.SEG_MOVETO:\r
+                                       moveX = lastX = points[0];\r
+                                       moveY = lastY = points[1];\r
+                                       result.moveTo(moveX, moveY);\r
+                                       next = wavelength / 2;\r
+                                       break;\r
+\r
+                               case PathIterator.SEG_CLOSE:\r
+                                       points[0] = moveX;\r
+                                       points[1] = moveY;\r
+                                       // Fall into....\r
+\r
+                               case PathIterator.SEG_LINETO:\r
+                                       thisX = points[0];\r
+                                       thisY = points[1];\r
+                                       float dx = thisX - lastX;\r
+                                       float dy = thisY - lastY;\r
+                                       float distance = (float)Math.sqrt(dx * dx + dy * dy);\r
+                                       if (distance >= next) {\r
+                                               float r = 1.0f / distance;\r
+                                               while (distance >= next) {\r
+                                                       float x = lastX + next * dx * r;\r
+                                                       float y = lastY + next * dy * r;\r
+                                                       if ((phase & 1) == 0)\r
+                                                               result.lineTo(x + amplitude * dy * r, y - amplitude * dx * r);\r
+                                                       else\r
+                                                               result.lineTo(x - amplitude * dy * r, y + amplitude * dx * r);\r
+                                                       next += wavelength;\r
+                                                       phase++;\r
+                                               }\r
+                                       }\r
+                                       next -= distance;\r
+                                       lastX = thisX;\r
+                                       lastY = thisY;\r
+                                       if (type == PathIterator.SEG_CLOSE) result.closePath();\r
+                                       break;\r
+                               }\r
+                               it.next();\r
+                       }\r
+                       return new BasicStroke(getWidth(), BasicStroke.CAP_SQUARE, getJoin()).createStrokedShape(result);\r
+               }\r
+       }\r
+}\r
diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ShadowEffect.java b/extensions/hiero/src/com/badlogic/gdx/hiero/unicodefont/effects/ShadowEffect.java
new file mode 100644 (file)
index 0000000..a5dc9e6
--- /dev/null
@@ -0,0 +1,232 @@
+\r
+package com.badlogic.gdx.hiero.unicodefont.effects;\r
+\r
+import java.awt.AlphaComposite;\r
+import java.awt.Color;\r
+import java.awt.Composite;\r
+import java.awt.Graphics2D;\r
+import java.awt.RenderingHints;\r
+import java.awt.image.BufferedImage;\r
+import java.awt.image.ConvolveOp;\r
+import java.awt.image.Kernel;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+import com.badlogic.gdx.hiero.unicodefont.Glyph;\r
+import com.badlogic.gdx.hiero.unicodefont.UnicodeFont;\r
+\r
+/**\r
+ * @author Nathan Sweet <misc@n4te.com>\r
+ */\r
+public class ShadowEffect implements ConfigurableEffect {\r
+       /** The numberof kernels to apply */\r
+       public static final int NUM_KERNELS = 16;\r
+       /** The blur kernels applied across the effect */\r
+       public static final float[][] GAUSSIAN_BLUR_KERNELS = generateGaussianBlurKernels(NUM_KERNELS);\r
+\r
+       private Color color = Color.black;\r
+       private float opacity = 0.6f;\r
+       private float xDistance = 2, yDistance = 2;\r
+       private int blurKernelSize = 0;\r
+       private int blurPasses = 1;\r
+\r
+       public ShadowEffect () {\r
+       }\r
+\r
+       public ShadowEffect (Color color, int xDistance, int yDistance, float opacity) {\r
+               this.color = color;\r
+               this.xDistance = xDistance;\r
+               this.yDistance = yDistance;\r
+               this.opacity = opacity;\r
+       }\r
+\r
+       public void draw (BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph) {\r
+               g = (Graphics2D)g.create();\r
+               g.translate(xDistance, yDistance);\r
+               g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), Math.round(opacity * 255)));\r
+               g.fill(glyph.getShape());\r
+\r
+               // Also shadow the outline, if one exists.\r
+               for (Iterator iter = unicodeFont.getEffects().iterator(); iter.hasNext();) {\r
+                       Effect effect = (Effect)iter.next();\r
+                       if (effect instanceof OutlineEffect) {\r
+                               Composite composite = g.getComposite();\r
+                               g.setComposite(AlphaComposite.Src); // Prevent shadow and outline shadow alpha from combining.\r
+\r
+                               g.setStroke(((OutlineEffect)effect).getStroke());\r
+                               g.draw(glyph.getShape());\r
+\r
+                               g.setComposite(composite);\r
+                               break;\r
+                       }\r
+               }\r
+\r
+               g.dispose();\r
+               if (blurKernelSize > 1 && blurKernelSize < NUM_KERNELS && blurPasses > 0) blur(image);\r
+       }\r
+\r
+       private void blur (BufferedImage image) {\r
+               float[] matrix = GAUSSIAN_BLUR_KERNELS[blurKernelSize - 1];\r
+               Kernel gaussianBlur1 = new Kernel(matrix.length, 1, matrix);\r
+               Kernel gaussianBlur2 = new Kernel(1, matrix.length, matrix);\r
+               RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);\r
+               ConvolveOp gaussianOp1 = new ConvolveOp(gaussianBlur1, ConvolveOp.EDGE_NO_OP, hints);\r
+               ConvolveOp gaussianOp2 = new ConvolveOp(gaussianBlur2, ConvolveOp.EDGE_NO_OP, hints);\r
+               BufferedImage scratchImage = EffectUtil.getScratchImage();\r
+               for (int i = 0; i < blurPasses; i++) {\r
+                       gaussianOp1.filter(image, scratchImage);\r
+                       gaussianOp2.filter(scratchImage, image);\r
+               }\r
+       }\r
+\r
+       public Color getColor () {\r
+               return color;\r
+       }\r
+\r
+       public void setColor (Color color) {\r
+               this.color = color;\r
+       }\r
+\r
+       public float getXDistance () {\r
+               return xDistance;\r
+       }\r
+\r
+       /**\r
+        * Sets the pixels to offset the shadow on the x axis. The glyphs will need padding so the shadow doesn't get clipped.\r
+        */\r
+       public void setXDistance (float distance) {\r
+               xDistance = distance;\r
+       }\r
+\r
+       public float getYDistance () {\r
+               return yDistance;\r
+       }\r
+\r
+       /**\r
+        * Sets the pixels to offset the shadow on the y axis. The glyphs will need padding so the shadow doesn't get clipped.\r
+        */\r
+       public void setYDistance (float distance) {\r
+               yDistance = distance;\r
+       }\r
+\r
+       public int getBlurKernelSize () {\r
+               return blurKernelSize;\r
+       }\r
+\r
+       /**\r
+        * Sets how many neighboring pixels are used to blur the shadow. Set to 0 for no blur.\r
+        */\r
+       public void setBlurKernelSize (int blurKernelSize) {\r
+               this.blurKernelSize = blurKernelSize;\r
+       }\r
+\r
+       public int getBlurPasses () {\r
+               return blurPasses;\r
+       }\r
+\r
+       /**\r
+        * Sets the number of times to apply a blur to the shadow. Set to 0 for no blur.\r
+        */\r
+       public void setBlurPasses (int blurPasses) {\r
+               this.blurPasses = blurPasses;\r
+       }\r
+\r
+       public float getOpacity () {\r
+               return opacity;\r
+       }\r
+\r
+       public void setOpacity (float opacity) {\r
+               this.opacity = opacity;\r
+       }\r
+\r
+       public String toString () {\r
+               return "Shadow";\r
+       }\r
+\r
+       public List getValues () {\r
+               List values = new ArrayList();\r
+               values.add(EffectUtil.colorValue("Color", color));\r
+               values.add(EffectUtil.floatValue("Opacity", opacity, 0, 1, "This setting sets the translucency of the shadow."));\r
+               values.add(EffectUtil.floatValue("X distance", xDistance, 0, 99, "This setting is the amount of pixels to offset the "\r
+                       + "shadow on the x axis. The glyphs will need padding so the shadow doesn't get clipped."));\r
+               values.add(EffectUtil.floatValue("Y distance", yDistance, 0, 99, "This setting is the amount of pixels to offset the "\r
+                       + "shadow on the y axis. The glyphs will need padding so the shadow doesn't get clipped."));\r
+\r
+               List options = new ArrayList();\r
+               options.add(new String[] {"None", "0"});\r
+               for (int i = 2; i < NUM_KERNELS; i++)\r
+                       options.add(new String[] {String.valueOf(i)});\r
+               String[][] optionsArray = (String[][])options.toArray(new String[options.size()][]);\r
+               values.add(EffectUtil.optionValue("Blur kernel size", String.valueOf(blurKernelSize), optionsArray,\r
+                       "This setting controls how many neighboring pixels are used to blur the shadow. Set to \"None\" for no blur."));\r
+\r
+               values.add(EffectUtil.intValue("Blur passes", blurPasses,\r
+                       "The setting is the number of times to apply a blur to the shadow. Set to \"0\" for no blur."));\r
+               return values;\r
+       }\r
+\r
+       public void setValues (List values) {\r
+               for (Iterator iter = values.iterator(); iter.hasNext();) {\r
+                       Value value = (Value)iter.next();\r
+                       if (value.getName().equals("Color")) {\r
+                               color = (Color)value.getObject();\r
+                       } else if (value.getName().equals("Opacity")) {\r
+                               opacity = ((Float)value.getObject()).floatValue();\r
+                       } else if (value.getName().equals("X distance")) {\r
+                               xDistance = ((Float)value.getObject()).floatValue();\r
+                       } else if (value.getName().equals("Y distance")) {\r
+                               yDistance = ((Float)value.getObject()).floatValue();\r
+                       } else if (value.getName().equals("Blur kernel size")) {\r
+                               blurKernelSize = Integer.parseInt((String)value.getObject());\r
+                       } else if (value.getName().equals("Blur passes")) {\r
+                               blurPasses = ((Integer)value.getObject()).intValue();\r
+                       }\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Generate the blur kernels which will be repeatedly applied when blurring images\r
+        * \r
+        * @param level The number of kernels to generate\r
+        * @return The kernels generated\r
+        */\r
+       private static float[][] generateGaussianBlurKernels (int level) {\r
+               float[][] pascalsTriangle = generatePascalsTriangle(level);\r
+               float[][] gaussianTriangle = new float[pascalsTriangle.length][];\r
+               for (int i = 0; i < gaussianTriangle.length; i++) {\r
+                       float total = 0.0f;\r
+                       gaussianTriangle[i] = new float[pascalsTriangle[i].length];\r
+                       for (int j = 0; j < pascalsTriangle[i].length; j++)\r
+                               total += pascalsTriangle[i][j];\r
+                       float coefficient = 1 / total;\r
+                       for (int j = 0; j < pascalsTriangle[i].length; j++)\r
+                               gaussianTriangle[i][j] = coefficient * pascalsTriangle[i][j];\r
+               }\r
+               return gaussianTriangle;\r
+       }\r
+\r
+       /**\r
+        * Generate Pascal's triangle\r
+        * \r
+        * @param level The level of the triangle to generate\r
+        * @return The Pascal's triangle kernel\r
+        */\r
+       private static float[][] generatePascalsTriangle (int level) {\r
+               if (level < 2) level = 2;\r
+               float[][] triangle = new float[level][];\r
+               triangle[0] = new float[1];\r
+               triangle[1] = new float[2];\r
+               triangle[0][0] = 1.0f;\r
+               triangle[1][0] = 1.0f;\r
+               triangle[1][1] = 1.0f;\r
+               for (int i = 2; i < level; i++) {\r
+                       triangle[i] = new float[i + 1];\r
+                       triangle[i][0] = 1.0f;\r
+                       triangle[i][i] = 1.0f;\r
+                       for (int j = 1; j < triangle[i].length - 1; j++)\r
+                               triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];\r
+               }\r
+               return triangle;\r
+       }\r
+}\r