Circular/Curved Text Effects

Godot Version

4.4.1

Question

` I’m working on a project that has some specific label requirements that involve curved text. One of them I’ve already hacked together, but would love some extra guidance on, which is bending text around a curve. My jerry-rigged solution is to use a Path2D to demarcate the curve, and then re-process the label (making a new label for every character- bad I know!) and rotate those new labels to match the rotation of the Path2D. It works, but it does look kind of bad because the characters don’t deform to the curve, they just rotate to match it.

My other issue, which I haven’t yet solved, is having the bounding box for a label instead be a bounding circle. In other words, changing the text wrapping to wrap at the edge of a circle rather than at the edge of the default rectangle. I know in the absolute worst case I could hardcode this effect for a given font and font size by just counting the number of lines in the label and truncating them as necessary, but I would love a more elegant solution before I do all of that work.

I’m really loving using Godot, but it does definitely make it hard to do creative things with text when you can’t easily bend it!`

Have you tried a RichTextLabel with a Tornado effect?

The other solution I can think of is finding a curved text library in C# and plugging that in by using Godot Mono (the C# version) because it can also use GDScript code. Which would be easier than using a plugin from another language and using GDExtension.

It’s possible to draw a text following a curve:

@tool
extends Path2D


@export var text: String:
	set(value):
		if text != value:
			text = value
			queue_redraw()


@export var label_settings: LabelSettings:
	set(value):
		if is_instance_valid(label_settings) and label_settings.changed.is_connected(queue_redraw):
			label_settings.changed.disconnect(queue_redraw)

		label_settings = value

		if is_instance_valid(label_settings):
			label_settings.changed.connect(queue_redraw)


var _line = TextLine.new()


func _draw() -> void:
	# Get the font, font size and color from the ThemeDB
	var font = ThemeDB.fallback_font
	var font_size = ThemeDB.fallback_font_size
	var font_color = Color.WHITE

	# If the label_settings is valid, then use the values from it
	if is_instance_valid(label_settings):
		font = label_settings.font
		font_size = label_settings.font_size
		font_color = label_settings.font_color

	# Clear the line and add the new string
	_line.clear()
	_line.add_string(text, font, font_size)
	# Get the primary TextServer
	var ts = TextServerManager.get_primary_interface()
	# And get the glyph information from the line
	var glyphs = ts.shaped_text_get_glyphs(_line.get_rid())

	var offset = 0.0
	for glyph_data in glyphs:
		# Sample the curve with rotation at the offset
		var trans = curve.sample_baked_with_rotation(offset)
		# set the draw matrix to that transform
		draw_set_transform_matrix(trans)
		# draw the glyph
		ts.font_draw_glyph(glyph_data["font_rid"], get_canvas_item(), font_size, Vector2.ZERO, glyph_data["index"], font_color, 2.0)
		# add the advance to the offset
		offset += glyph_data.get("advance", 0.0)
4 Likes

Wow, this is incredible! This should be a built in feature!! I might make a new topic to get help on a circular bounding box for text wrapping, but this definitely solves my curve problem.