Skip to content

Commit

Permalink
refactor text rotation methods
Browse files Browse the repository at this point in the history
  • Loading branch information
xzel23 committed Jan 17, 2025
1 parent ae07d5f commit 5462957
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.stage.Stage;
Expand Down Expand Up @@ -50,7 +51,7 @@ Tab[] createSlides(float w, float h) {
createSlide(ArcToAndEllipse::new, w, h),
createSlide(DrawText::new, w, h),
createSlide(RenderText::new, w, h),
createSlide(RenderRotatedText::new, w, h)
createBigSlide(RenderRotatedText::new, w, h)
};
}

Expand All @@ -61,5 +62,13 @@ Tab createSlide(Supplier<Slide> factory, float w, float h) {
slide.draw(g);
return new Tab(slide.title(), canvas);
}

Tab createBigSlide(Supplier<Slide> factory, float w, float h) {
Slide slide = factory.get();
Canvas canvas = new Canvas(w, 2 * h);
FxGraphics g = new FxGraphics(canvas);
slide.draw(g);
return new Tab(slide.title(), new ScrollPane(canvas));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,23 @@

public class RenderRotatedText implements Slide {

private static RichText getText(Graphics.TextRotationMode mode, double rotation) {
return new RichTextBuilder()
.append("rotated text\n")
.append("using different modes\n")
.append("and angles\n")
.push(Style.BOLD)
.append("bold ")
.pop(Style.BOLD)
.push(Style.ITALIC)
.append("italic\n")
.pop(Style.ITALIC)
.push(Style.UNDERLINE)
.append("underline ")
.pop(Style.UNDERLINE)
.push(Style.LINE_THROUGH)
.append("line through")
.pop(Style.LINE_THROUGH)
.toRichText();
}
public static final RichText TEXT = new RichTextBuilder()
.append("rotated text\n")
.append("using different modes\n")
.append("and angles\n")
.push(Style.BOLD)
.append("bold ")
.pop(Style.BOLD)
.push(Style.ITALIC)
.append("italic\n")
.pop(Style.ITALIC)
.push(Style.UNDERLINE)
.append("underline ")
.pop(Style.UNDERLINE)
.push(Style.LINE_THROUGH)
.append("line through")
.pop(Style.LINE_THROUGH)
.toRichText();

@Override
public String title() {
Expand All @@ -52,7 +50,21 @@ public void drawText(Graphics g) {
.map( v -> v + 10)
.toArray();

Graphics.TextRotationMode[] modes = Graphics.TextRotationMode.values();
record Mode(Graphics.TextRotationMode mode, Graphics.AlignmentAxis axis) {
@Override
public String toString() {
return mode == Graphics.TextRotationMode.ROTATE_LINES
? mode.name() + "[" + axis.name() + "]"
: mode.name();
}
}
Mode[] modes = {
new Mode(Graphics.TextRotationMode.ROTATE_BLOCK, Graphics.AlignmentAxis.AUTOMATIC),
new Mode(Graphics.TextRotationMode.ROTATE_AND_TRANSLATE_BLOCK, Graphics.AlignmentAxis.AUTOMATIC),
new Mode(Graphics.TextRotationMode.ROTATE_LINES, Graphics.AlignmentAxis.AUTOMATIC),
new Mode(Graphics.TextRotationMode.ROTATE_LINES, Graphics.AlignmentAxis.X_AXIS),
new Mode(Graphics.TextRotationMode.ROTATE_LINES, Graphics.AlignmentAxis.Y_AXIS)
};

float margin = 10.0f;
Dimension2f tileDimension = g.getBounds().getDimension().withMargin(-margin).scaled(Scale2f.of(1.0f / angles.length, 1.0f / (modes.length + 1)));
Expand All @@ -78,7 +90,7 @@ public void drawText(Graphics g) {
for (int i=0; i<modes.length; i++) {
float x = margin;
float y = margin + (i + 0.8f) * tileHeight;
g.drawText(modes[i].name(), x, y, Graphics.HAnchor.LEFT, Graphics.VAnchor.TOP);
g.drawText(modes[i].toString(), x, y, Graphics.HAnchor.LEFT, Graphics.VAnchor.TOP);
}

for (int i=0; i<modes.length; i++) {
Expand All @@ -87,14 +99,15 @@ public void drawText(Graphics g) {
float y = margin + (i + 1) * tileHeight;
double rotation = MathUtil.rad(angles[j]);

Rectangle2f r = Rectangle2f.of(x, y, tileWidth, tileHeight * 0.75f);

// draw pivot
g.setFill(Color.RED);
g.fillCircle(x, y, 3);

// draw axis
g.setStroke(Color.RED, 1);
g.strokeLine(x,y, x + tileWidth / 3, y);
g.strokeLine(x,y, x, y + tileWidth / 3);
g.strokeRect(r);

// draw rotated axis
g.setStroke(Color.BLUE, 1);
Expand All @@ -103,8 +116,16 @@ public void drawText(Graphics g) {
g.strokeLine(x,y, x + dx, y + dy);
g.strokeLine(x,y, x - dy, y + dx);

Rectangle2f r = Rectangle2f.of(x, y, tileWidth, tileHeight);
g.renderText(r, getText(modes[i], angles[j]), Alignment.LEFT, VerticalAlignment.TOP, true, rotation, modes[i]);
g.renderText(
r,
TEXT,
Alignment.LEFT,
VerticalAlignment.TOP,
true,
rotation,
modes[i].mode(),
modes[i].axis()
);
}
}
}
Expand Down
131 changes: 94 additions & 37 deletions utility/src/main/java/com/dua3/utility/ui/Graphics.java
Original file line number Diff line number Diff line change
Expand Up @@ -414,25 +414,16 @@ enum TextRotationMode {
* x-coordinate. Also apply a vertical translation accordingly.
*/
ROTATE_AND_TRANSLATE_BLOCK,
/**
* Rotate each line independently. As a result, All lines of a left aligned text will start at the given
* x-coordinate (for rotation angles between -π/4 and π/4, or -45° and 45°).
*
* <p>For rotation with an absolute amount larger than π/4, the y-coordinate is used for alignment instead.
*/
ROTATE_LINES,
/**
* Rotate each line independently. Align lines horizontally, i.e., all lines start at the same y-coordinate.
*/
ROTATE_LINES_AND_ALIGN_HORIZONTALLY,
/**
* Rotate each line independently. Align lines vertically, i.e., all lines start at the same x-coordinate.
*/
ROTATE_LINES_AND_ALIGN_VERTICALLY,
/**
* Rotate each line independently. Lines are aligned horizontally or vertically based on the given angle.
*/
ROTATE_AND_ALIGN_LINES
ROTATE_LINES,
}

enum AlignmentAxis {
AUTOMATIC,
X_AXIS,
Y_AXIS,
}

/**
Expand All @@ -447,23 +438,42 @@ enum TextRotationMode {
*/
default void renderText(Rectangle2f r, RichText text, Alignment hAlign, VerticalAlignment vAlign, boolean wrapping) {
FragmentedText fragments = generateFragments(text, r, hAlign, vAlign, wrapping);
renderFragments(r, hAlign, vAlign, fragments.textWidth(), fragments.textHeight(), fragments.baseLine(), 0.0, fragments.fragmentLines());
renderFragments(
r,
hAlign,
vAlign,
fragments.textWidth(),
fragments.textHeight(),
fragments.baseLine(),
0.0,
AlignmentAxis.AUTOMATIC,
fragments.fragmentLines()
);
}

/**
* Renders the given text within the specified bounding rectangle using the provided font,
* alignment, wrapping, and rotation settings.
*
* @param r the bounding rectangle to render the text into
* @param text the text to be rendered
* @param hAlign the horizontal alignment of the text within the bounding rectangle
* @param vAlign the vertical alignment of the text within the bounding rectangle
* @param wrapping determines if text wrapping should be applied
* @param angle the rotatiion angle in radians
* @param mode the {@link TextRotationMode} to use
*/
default void renderText(Rectangle2f r, RichText text, Alignment hAlign, VerticalAlignment vAlign, boolean wrapping, double angle, TextRotationMode mode) {
if (angle == 0.0) {
* @param r the bounding rectangle to render the text into
* @param text the text to be rendered
* @param hAlign the horizontal alignment of the text within the bounding rectangle
* @param vAlign the vertical alignment of the text within the bounding rectangle
* @param wrapping determines if text wrapping should be applied
* @param angle the rotatiion angle in radians
* @param mode the {@link TextRotationMode} to use
* @param alignmentAxis the axis to align rotated text on
*/
default void renderText(
Rectangle2f r,
RichText text,
Alignment hAlign,
VerticalAlignment vAlign,
boolean wrapping,
double angle,
TextRotationMode mode,
AlignmentAxis alignmentAxis) {
if (angle == 0.0 && (mode != TextRotationMode.ROTATE_LINES || alignmentAxis != AlignmentAxis.X_AXIS)) {
// fast path when no rotation is applied
renderText(r, text, hAlign, vAlign, wrapping);
return;
Expand All @@ -476,7 +486,17 @@ default void renderText(Rectangle2f r, RichText text, Alignment hAlign, Vertical
switch (mode) {
case ROTATE_BLOCK -> {
setTransformation(AffineTransformation2f.combine(t, AffineTransformation2f.rotate(angle, Vector2f.of(r.x(), r.y()))));
renderFragments(r, hAlign, vAlign, fragments.textWidth(), fragments.textHeight(), fragments.baseLine(), 0.0, fragments.fragmentLines());
renderFragments(
r,
hAlign,
vAlign,
fragments.textWidth(),
fragments.textHeight(),
fragments.baseLine(),
0.0,
AlignmentAxis.AUTOMATIC,
fragments.fragmentLines()
);
}
case ROTATE_AND_TRANSLATE_BLOCK -> {
float tx;
Expand Down Expand Up @@ -514,10 +534,30 @@ default void renderText(Rectangle2f r, RichText text, Alignment hAlign, Vertical
AffineTransformation2f.rotate(angle, Vector2f.of(r.x(), r.y())),
AffineTransformation2f.translate(tx, ty)
));
renderFragments(r, hAlign, vAlign, fragments.textWidth(), fragments.textHeight(), fragments.baseLine(), 0.0, fragments.fragmentLines());
renderFragments(
r,
hAlign,
vAlign,
fragments.textWidth(),
fragments.textHeight(),
fragments.baseLine(),
0.0,
AlignmentAxis.AUTOMATIC,
fragments.fragmentLines()
);
}
case ROTATE_LINES -> {
renderFragments(r, hAlign, vAlign, fragments.textWidth(), fragments.textHeight(), fragments.baseLine(), angle, fragments.fragmentLines());
renderFragments(
r,
hAlign,
vAlign,
fragments.textWidth(),
fragments.textHeight(),
fragments.baseLine(),
angle,
alignmentAxis,
fragments.fragmentLines()
);
}
}
setTransformation(t);
Expand All @@ -531,7 +571,7 @@ default void renderText(Rectangle2f r, RichText text, Alignment hAlign, Vertical
* @param y the y-position
* @param w the width
* @param h the height
* @param baseLine the basline value (of the line the fragment belongs to
* @param baseLine the baseline value (of the line the fragment belongs to
* @param font the font
* @param text the text
*/
Expand Down Expand Up @@ -642,9 +682,20 @@ private FragmentedText generateFragments(RichText text, Rectangle2f r, Alignment
* @param textHeight the total height of the text fragments within all lines
* @param baseLine the baseline position of the text fragments
* @param angle the angle in radians to rotate each line (must be normalized)
* @param alignmentAxis the axis on which to align the text on
* @param fragmentLines a list of fragment lines, where each line contains a list of fragments
*/
private void renderFragments(Rectangle2f cr, Alignment hAlign, VerticalAlignment vAlign, float textWidth, float textHeight, float baseLine, double angle, List<List<Fragment>> fragmentLines) {
private void renderFragments(
Rectangle2f cr,
Alignment hAlign,
VerticalAlignment vAlign,
float textWidth,
float textHeight,
float baseLine,
double angle,
AlignmentAxis alignmentAxis,
List<List<Fragment>> fragmentLines
) {
assert 0 <= angle && angle < MathUtil.TWO_PI : "invalid angle: " + angle;

//
Expand All @@ -659,20 +710,26 @@ private void renderFragments(Rectangle2f cr, Alignment hAlign, VerticalAlignment
sx_h = 0.0f;
} else {
setTransformation(AffineTransformation2f.combine(t, AffineTransformation2f.rotate(angle, Vector2f.of(cr.x(), cr.y()))));
switch ((int) (angle / MathUtil.PI_QUARTER)) {
case 0, 4 -> {
int[] layoutCases = switch (alignmentAxis) {
case AUTOMATIC -> new int[]{0, 1, 2, 3};
case X_AXIS -> new int[]{1, 1, 2, 2};
case Y_AXIS -> new int[]{0, 0, 3, 3};
};
int layoutCase = layoutCases[(int) (angle / MathUtil.PI_QUARTER) % 4];
switch (layoutCase) {
case 0 -> {
sx_y = (float) (Math.tan(angle));
sx_h = sx_y;
}
case 1, 5 -> {
case 1 -> {
sx_y = (float) (Math.tan(angle + MathUtil.PI_HALF));
sx_h = 0;
}
case 2, 6 -> {
case 2 -> {
sx_y = (float) (Math.tan(angle + MathUtil.PI_HALF));
sx_h = sx_y;
}
case 3, 7 -> {
case 3 -> {
sx_y = (float) (Math.tan(angle));
sx_h = 0;
}
Expand Down

0 comments on commit 5462957

Please sign in to comment.