Нарисуйте текст в круге с Java AWT (с соответствующим расположением букв) - PullRequest
1 голос
/ 07 августа 2020

Я пытаюсь использовать Java AWT с AffineTranform, чтобы нарисовать заданную строку в круге, где буквы также будут go перевернутыми вдоль круга.

Я начал с код из следующей программы для рисования только текста кривой. Я также использовал вычисление координат из фрагмента, который я нашел здесь для рисования чисел аналоговых часов.

Ниже мой код. Честно говоря, я не понимаю на 100%, как работают эти методы для исправления моего кода. Я немного поигрался в попытке проб и ошибок со значениями coords и theta.

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

И это текущий результат:

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 starting position of the first character is at ~4 o'clock instead of top.
  • The characters aren't correctly angled with their tops towards the center of the circle
  • The string is drawn counterclockwise instead of clockwise

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

введите описание изображения здесь

1 Ответ

1 голос
/ 07 августа 2020

Ваш начальный угол 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
  • поворот
  • сдвиг в конечное положение (относительно нуля, затем сдвиг по центру круга)
...