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