Kevin Cruijssen
Kevin Cruijssen

Reputation: 9336

Draw text in circle with Java AWT (with the letters oriented accordingly)

I'm trying to use Java AWT with AffineTranform to draw a given String in a circle, where the letters would also go upside down along the circle.

I started with the code from the following program to draw text alone a curve.
I've also used the calculation of the coordinates from a snippet I found here for drawing the numbers of an analog clock.

Below is my code. To be completely honest, I don't understand 100% how these methods work in order to fix my code. I've been fiddling around a bit in a trial-and-error attempt with the coords and theta values.

import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Panel;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;

public class Main extends Panel {
  public static void main(String[] args){
    Frame f = new Frame("Circle Text");
    f.add(new Main());
    f.setSize(750, 750);
    f.setVisible(true);
  }

  private int[] getPointXY(int dist, double rad){
    int[] coord = new int[2];
    coord[0] = (int) (dist * Math.cos(rad) + dist);
    coord[1] = (int) (-dist * Math.sin(rad) + dist);
    return coord;
  }

  @Override
  public void paint(Graphics g){
    Graphics2D g2 = (Graphics2D) g;

    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);

    // Hard-coded for now, using 12 characters for 30 degrees angles (like  a clock)
    String text = "0123456789AB";

    Font font = new Font("Serif", 0, 25);
    FontRenderContext frc = g2.getFontRenderContext();
    g2.translate(200, 200); // Starting position of the text

    GlyphVector gv = font.createGlyphVector(frc, text);
    int length = gv.getNumGlyphs(); // Same as text.length()
    final double toRad = Math.PI / 180;
    for(int i = 0; i < length; i++){
      //Point2D p = gv.getGlyphPosition(i);
      int[] coords = getPointXY(100, -360 / length * i * toRad + Math.PI / 2);
      double theta = 2 * Math.PI / (length + 1) * i;
      AffineTransform at = AffineTransform.getTranslateInstance(coords[0], coords[1]);
      at.rotate(theta);
      Shape glyph = gv.getGlyphOutline(i);
      Shape transformedGlyph = at.createTransformedShape(glyph);
      g2.fill(transformedGlyph);
    }
  }
}

And this is the current output:

enter image description here

I also noticed that if I use (2 * length) instead of (length + 1) in the theta formula, the first halve of the string seems to be in the correct positions, except not angled properly oriented (the character '6' is sideways / 90 degrees rotated, instead of upside down / 180 degrees rotated):

enter image description here

As I mentioned, I don't really know how the AffineTransform works regarding the given coordinates and theta. An explanation of that would be greatly appreciated, and even more so if someone could help me fix the code.
Also note that I want to implement this formula for a variable length of the String. I've now hard-coded it to "0123456789AB" (12 characters, so it's similar to a clock with 30 degrees steps), but it should also work with let's say a String of 8 characters or 66 characters.


EDIT: After the suggestions of @MBo I made the following modifications to the code:

int r = 50;
int[] coords = getPointXY(r, -360 / length * i * toRad + Math.PI / 2);
gv.setGlyphPosition(i, new Point(coords[0], coords[1]));
final AffineTransform at = AffineTransform.getTranslateInstance(0, 0);
at.rotate(-2 * Math.PI * i / length);
at.translate(r * Math.cos(Math.PI / 2 - 2 * Math.PI * i / length),
             r * Math.sin(Math.PI / 2 - 2 * Math.PI * i / length));
Shape glyph = gv.getGlyphOutline(i);
Shape transformedGlyph = at.createTransformedShape(glyph);
g2.fill(transformedGlyph);

enter image description here

I now do have a circle, so that's something, but unfortunately still with three issues:

The last issue is easily fixed, by changing the -2 to 2 in the rotate:

enter image description here

But the other two?


EDIT2: I misread a small section of @MBo's answer regarding the initial glyph set. It's now working. Here the resulting code changes again in comparison to the Edit above:

gv.setGlyphPosition(i, new Point(-length / 2, -length / 2));
AffineTransform at = AffineTransform.getTranslateInstance(coords[0], coords[1]);
at.rotate(2 * Math.PI * i / length);

enter image description here

Although I still see some minor issues with larger input Strings, so will look into that.


EDIT3: It's been a while, but I just got back to this, and I spotted my mistake for the length 66 test case I tried pretty easily: 360 should be a 360d, because the 360/length would use integer-division otherwise if 360 isn't evenly divisible by the length.
I now have this, which works as intended for any length. Note that the center isn't completely correct, for which the answer provided by @Mbo can help. My only goal was to make the circle of text (of length 66). Where it is on the screen and how big wasn't really that important.

int[] coords = this.getPointXY(r, -360.0 / length * i * toRad + Math.PI / 2);
  gv.setGlyphPosition(i, new Point(0, 0));
  AffineTransform at = AffineTransform.getTranslateInstance(coords[0], coords[1]);
  at.rotate(2 * Math.PI * i / length);
  at.translate(r * Math.cos(Math.PI / 2 - 2 * Math.PI * i / length),
      r * Math.sin(Math.PI / 2 - 2 * Math.PI * i / length));
  at.translate(-FONT_SIZE / 2, 0);

enter image description here

Upvotes: 1

Views: 807

Answers (1)

MBo
MBo

Reputation: 80325

Your initial angle is Pi/2 for position and 0 for glyph rotation. To set rotation and position properly, I suggest:

  • put glyph in the coordinate origin (0,0)
  • rotate it by -2*Math.PI * i / length
  • translate it by r*cos(Math.PI/2 - 2*Math.PI * i / length) and r*sin(Math.PI/2 - 2*Math.PI * i / length)
  • translate it by circle center coordinates

Steps:

enter image description here

Note - rotate, then shift.

This approach perhaps give good but not perfect result. For better looking you can add the first step - translate glyph by half of it's size to provide rotation about it's center. So sequence:

  • shift by -glyphpixelsize/2
  • rotate
  • shift into final position (relative to zero, then shift by circle center)

Upvotes: 1

Related Questions