From: nathan.sweet Date: Sun, 31 Oct 2010 23:13:50 +0000 (+0000) Subject: [added] Hiero tool for creating bitmap fonts. X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=3d4955a7d8f15ff69c4e4dc698dbffb91f774ca6;p=mikumikustudio%2Flibgdx-mikumikustudio.git [added] Hiero tool for creating bitmap fonts. --- 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 000000000..081ad8496 Binary files /dev/null and b/extensions/hiero/data/splash.jpg differ diff --git a/extensions/hiero/src/com/badlogic/gdx/hiero/BMFontUtil.java b/extensions/hiero/src/com/badlogic/gdx/hiero/BMFontUtil.java new file mode 100644 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; + } +}