Reputation: 9336
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:
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):
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);
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:
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);
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);
Upvotes: 1
Views: 807
Reputation: 80325
Your initial angle is Pi/2
for position and 0
for glyph rotation.
To set rotation and position properly, I suggest:
-2*Math.PI * i / length
r*cos(Math.PI/2 - 2*Math.PI * i / length)
and r*sin(Math.PI/2 - 2*Math.PI * i / length)
Steps:
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:
Upvotes: 1