使用 Java AWT 在圆圈中绘制文本(相应方向的字母)

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

我正在尝试使用 Java AWT 和 AffineTranform 在圆圈中绘制给定的字符串,其中字母也会沿着圆圈上下颠倒。

我从 the following program 中的代码开始,将文本单独绘制成一条曲线。
我还使用了 a snippet I found here 的坐标计算来绘制模拟时钟的数字。

下面是我的代码。老实说,我不是 100% 理解这些方法是如何修复我的代码的。在 trial-and-error 尝试中,我一直在摆弄 coordstheta 值。

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);
    }
  }
}

这是当前输出:

我还注意到,如果我在 theta 公式中使用 (2 * length) 而不是 (length + 1),则字符串的前半部分似乎位于正确的位置,但没有倾斜方向正确(字符“6”侧向/旋转 90 度,而不是倒置/旋转 180 度):

正如我提到的,我真的不知道 AffineTransform 在给定坐标和 theta 方面是如何工作的。对此的解释将不胜感激,如果有人可以帮助我修复代码,则更是如此。
另请注意,我想为可变长度的字符串实现此公式。我现在 hard-coded 到 "0123456789AB" (12 个字符,所以它类似于一个步长为 30 度的时钟),但它也应该适用于 8 个字符或 66 个字符的字符串。


编辑:根据@MBo的建议,我对代码进行了以下修改:

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);

我现在确实有一个圈子,这就是问题所在,但不幸的是仍然存在三个问题:

最后一个问题很容易解决,方法是将轮换中的 -2 更改为 2

但是另外两个呢?


EDIT2:我误读了一小部分 @MBo 关于初始字形集的回答。它现在正在工作。与上面的编辑相比,这里生成的代码再次发生变化:

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

尽管我仍然发现较大的输入字符串存在一些小问题,所以会调查一下。


EDIT3:已经有一段时间了,但我刚刚回到这个问题上,我发现了我很容易尝试的长度为 66 的测试用例的错误:360 应该是 360d,因为 360/length 会使用 integer-division,否则如果 360 不能被长度整除。
我现在有了这个,它可以按预期工作任何长度。请注意,中心并不完全正确,@Mbo 提供的答案可以提供帮助。我唯一的目标是制作文本圈(长度为 66)。它在屏幕上的位置和大小并不是那么重要。

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);

您的位置初始角度为 Pi/2,字形旋转初始角度为 0。 要正确设置旋转和位置,我建议:

  • 将字形放在坐标原点(0,0)
  • 旋转 -2*Math.PI * i / length
  • r*cos(Math.PI/2 - 2*Math.PI * i / length)r*sin(Math.PI/2 - 2*Math.PI * i / length)
  • 翻译
  • 按圆心坐标翻译

步骤:

注意 - 旋转,然后移动。

这种方法可能会给出好的但不完美的结果。为了更好看,您可以添加第一步 - 将字形平移一半大小以提供围绕其中心的旋转。所以序列:

  • 偏移-glyphpixelsize/2
  • 旋转
  • 移动到最终位置(相对于零,然后移动圆心)