Drawing Curved Text (Displayable)

This section is for people to post assets that people can use in their games. Everything here should have a creative commons or other open license, or be in the public domain.
Post Reply
Message
Author
User avatar
m_from_space
Miko-Class Veteran
Posts: 975
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Drawing Curved Text (Displayable)

#1 Post by m_from_space »

Hello creators! Since I don't want this to be buried in the original thread, here is a creator-defined Displayable I wrote for fun. It allows you to draw text along any bezier curve (three points and more) - two points work as well, but that would just be a line you know. :wink:

The Displayable will be an image you can use as any other image within Renpy. Feel free to use it, change it and comment or give feedback if you have suggestions regarding the code!

Example picture:
example.png
(46.34 KiB) Not downloaded yet
Code for the Displayable including detailed comments and argument description:

Code: Select all

init python:
    import math
    class CurvedText(renpy.Displayable):
        def __init__(self, what, points, rot=True, t_start=0.0, t_end=1.0, **kwargs):
            '''
            what: the text we want to curve
            points: a list of n > 2 points that define the curve, e.g. [(0, 0), (150, 150), (300, 0)]
            rot: whether or not to rotate the letters along the curve
            t_start: where to start on the curve (0.0 <= t_start < 1.0)
            t_end: where to end on the curve (0.0 < t_end <= 1.0)
            optional: pass keyword arguments for the text like "style", "font", "size", "color" etc.
            '''
            super(CurvedText, self).__init__(**kwargs)

            # a list of arguments we will use for the final composite image
            args = []
            l = len(what)
            n = len(points)
            # current position along the curve (0.0 = start, 1.0 = end)
            t = t_start
            # equal step distance depending on the text length
            td = (t_end - t_start) / max(1, l - 1)

            # loop through all characters of the text
            for i in range(l):
                # skip whitespaces
                if not what[i].strip():
                    t += td
                    continue
                # use "De Casteljau's algorithm" to find the point (x, y) on the curve at value t
                q = [list(p) for p in points]
                t0 = 1 - t
                for j in range(1, n):
                    for k in range(n - j):
                        q[k][0] = q[k][0] * t0 + q[k+1][0] * t
                        q[k][1] = q[k][1] * t0 + q[k+1][1] * t

                # the point on the curve was saved in q[0]
                x = int(q[0][0])
                y = int(q[0][1])

                # if we want to rotate, we need the vector of the tangent through this point (x, y)
                if rot:
                    # the last character's tangent if t_end == 1.0 is not found via the algorithm
                    # but we know that it equals the line between the last two points
                    if i == l - 1 and t_end == 1.0:
                        u = points[-1][0] - points[-2][0]
                        v = points[-1][1] - points[-2][1]
                    # in all other cases the algorithm's nature already got it
                    else:
                        u = q[1][0] - q[0][0]
                        v = q[1][1] - q[0][1]
                    # now just calculate the angle between the vector and the x-axis
                    r = math.degrees(math.atan2(v, u))
                    args.append(Transform(Text(what[i], **kwargs), xcenter=x, ycenter=y, rotate=r, subpixel=True))
                else:
                    args.append(Transform(Text(what[i], **kwargs), xcenter=x, ycenter=y, subpixel=True))

                # move one step along the curve
                t += td

            self.child = Fixed(*args)
            return

        def render(self, width, height, st, at):

            cr = renpy.render(self.child, width, height, st, at)
            self.width, self.height = cr.get_size()

            rv = renpy.Render(self.width, self.height)
            rv.blit(cr, (0, 0))

            return rv
Example code regarding usage:

Code: Select all

define curve_3_points = [(50, 50), (250, 150), (450, 50)]
define curve_4_points = [(50, 150), (400, 50), (500, 450), (800, 350)]
define curve_5_points = [(200, 550), (400, 400), (600, 700), (1000, 650), (800, 300)]

image ct1 = CurvedText("Hello, I'm a 3-point curved text!", curve_3_points)
image ct2 = CurvedText("I will be written from start to end by default!", curve_4_points, size=16, color="#f0f")
image ct3 = CurvedText("I am on the same curve, but start at 25%!", curve_4_points, t_start=0.25, size=16, color="#f0f")
image ct4 = CurvedText("And I love stopping prematurely!", curve_4_points, t_end=0.75, size=16, color="#f0f")
image ct5 = CurvedText("This one is without letter rotation!", curve_4_points, rot=False, color="#ff0")
image ct6 = CurvedText("Wow, 5 point curves really rock...!", curve_5_points, color="#0ff")

label start:

    show ct1
    show ct2
    show ct3:
        yoffset 50
    show ct4:
        yoffset 100
    show ct5:
        yoffset 150
    show ct6

    window auto hide
    pause

User avatar
m_from_space
Miko-Class Veteran
Posts: 975
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Drawing Curved Text (Displayable)

#2 Post by m_from_space »

If you want your text to be translated as well, use the special function starting with two underscores. But beware that since the curved text is created on game start, the translation only works when the player restarts the game.

Code: Select all

image ct = CurvedText(__("This text will be translated on game start, if another language is used."), my_bezier_curve)

User avatar
m_from_space
Miko-Class Veteran
Posts: 975
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Drawing Curved Text (Displayable)

#3 Post by m_from_space »

Hey again! Here is a similar Displayable, that allows long text to be curved along every bezier curve using line breaking, like you would expect it. At least I hope so. :lol:

Example picture:
CurvedMultiText.png
(106.76 KiB) Not downloaded yet

Code with lots of comments:

Code: Select all

init python:
    import math
    class CurvedMultiText(renpy.Displayable):
        def __init__(self, what, points, cpl=42, line_height=24, rot=True, t_start=0.0, t_end=1.0, **kwargs):
            '''
            what: the text we want to curve
            points: a list or tuple of n > 2 points that define the curve, e.g. [(0, 0), (150, 150), (300, 0)]
            cpl: characters per line that get stretched over the curve's range
            line_height: height of each line in pixels (this does not affect font size)
            rot: whether or not to rotate the letters along the curve
            t_start: where to start on the curve (0.0 <= t_start < 1.0)
            t_end: where to end on the curve (0.0 < t_end <= 1.0)
            OPTIONAL: pass keyword arguments for the text like "style", "font", "size", "color" etc.
            '''
            super(CurvedMultiText, self).__init__(**kwargs)

            n = len(points)
            # our curve starts here
            t = t_start
            # save all positions [x, y] inside this list
            t_pos = []
            # save all rotations r inside this list
            t_rot = []
            # define t_step by dividing the curve's range into _cpl_ parts, but at least 2 parts
            t_step = (t_end - t_start) / max(2, cpl - 1)

            # calculate all possible positions and rotations along the curve
            # we only have to do this for the first line, all other lines will use an y offset
            while t <= t_end:
                # use "De Casteljau's algorithm" to find the point (x, y) on the curve at value t
                q = [list(p) for p in points]
                t0 = 1 - t
                for j in range(1, n):
                    for k in range(n - j):
                        q[k][0] = q[k][0] * t0 + q[k+1][0] * t
                        q[k][1] = q[k][1] * t0 + q[k+1][1] * t

                # the point on the curve was saved in q[0]
                t_pos.append([int(q[0][0]), int(q[0][1])])

                # if we want to rotate, we need the vector of the tangent through this point (x, y)
                if rot:
                    # the tangent on the curve's exact end-point 1.0 is not found via the algorithm,
                    # but it equals the line between the last two points
                    if t == 1.0:
                        u = points[-1][0] - points[-2][0]
                        v = points[-1][1] - points[-2][1]
                    # in all other cases the algorithm's nature already got it
                    else:
                        u = q[1][0] - q[0][0]
                        v = q[1][1] - q[0][1]
                    # now just calculate the angle between the vector and the x-axis
                    t_rot.append(math.degrees(math.atan2(v, u)))

                # move one step along the curve
                t += t_step

            # a list of arguments we will use for the final composite image
            args = []
            # current position on the curve's index
            i = 0
            # line spacing offset
            ls = 0
            # amount of steps per line (should equal cpl, but who knows)
            lt = len(t_pos)

            lines = what.split("\n")
            # loop through every line of the text
            for line in lines:
                words = line.split(" ")
                # loop through every word of the line
                for word in words:
                    # the word could fit, but the rest of the line is too small, so start a new line
                    if len(word) <= lt and i + len(word) >= lt:
                        i = 0
                        ls += line_height
                    # place every character of the word
                    for c in word:
                        y = t_pos[i][1] + ls
                        if rot:
                            args.append(Transform(Text(c, **kwargs), xcenter=t_pos[i][0], ycenter=y, rotate=t_rot[i], subpixel=True))
                        else:
                            args.append(Transform(Text(c, **kwargs), xcenter=t_pos[i][0], ycenter=y, subpixel=True))
                        i += 1
                        # we're exceeding the line length (this happens if a word is too long for a whole line), so we have to break it up
                        if i >= lt:
                            i = 0
                            ls += line_height
                    # move one step accounting for a space that is following a word
                    i += 1
                    if i >= lt:
                        i = 0
                        ls += line_height
                # with every new line, start at the beginning
                i = 0
                ls += line_height

            self.child = Fixed(*args)
            return

        def render(self, width, height, st, at):

            cr = renpy.render(self.child, width, height, st, at)
            self.width, self.height = cr.get_size()

            rv = renpy.Render(self.width, self.height)
            rv.blit(cr, (0, 0))

            return rv
Example usage:

Code: Select all

define curve_5_points = ((50, 50), (250, 250), (500, -50), (750, 150), (1000, 200))

image ct1 = CurvedMultiText("This curved text will be written over multiple lines if necessary and allows for forced line breaks:\n\nThis is two lines later.", curve_5_points, size=20, color="#0ff")
image ct2 = CurvedMultiText("Here we disable letter rotation, and we're using a different font, size and color and a line height of fifty pixels.", curve_5_points, rot=False, line_height=50, size=32, font="fonts/SomeFont.ttf", color="#ff0")
image ct3 = CurvedMultiText("We're on the same curve, but this one allows 80 characters per line before it's going to break up the text. It looks better depending on the font and size you are using.", curve_5_points, cpl=80, color="#f0f", font="fonts/UbuntuMono-R.ttf")
image ct4 = CurvedMultiText("This is a short text starting at 25% and stopping at 75% of the curve.", curve_5_points, t_start=0.25, t_end=0.75, color="#0f0")

label start:

    show ct1
    show ct2:
        yoffset 200
    show ct3:
        yoffset 400
    show ct4:
        yoffset 500

    window auto hide
    pause

Post Reply

Who is online

Users browsing this forum: No registered users