Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
KingmakerVN
Regular
Posts: 37
Joined: Sat Nov 23, 2019 7:03 pm
Projects: Kingmaker
Contact:

Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#1 Post by KingmakerVN »

I don't know if I'm explaining myself. I'm not talking about italics, but rather having the whole text be in a curve. Particularly for a screen.

I was check the text style properties but I could not find anything that could do that, at least to my knowledge.
Last edited by KingmakerVN on Sun Oct 08, 2023 1:03 am, edited 1 time in total.

User avatar
PyTom
Ren'Py Creator
Posts: 16096
Joined: Mon Feb 02, 2004 10:58 am
Completed: Moonlight Walks
Projects: Ren'Py
IRC Nick: renpytom
Github: renpytom
itch: renpytom
Location: Kings Park, NY
Contact:

Re: Is it possible to make text align with a curve rather than a straight line on a screen?

#2 Post by PyTom »

No - at least right now, that's not a feature Ren'Py has.
Supporting creators since 2004
(When was the last time you backed up your game?)
"Do good work." - Virgil Ivan "Gus" Grissom
Software > Drama • https://www.patreon.com/renpytom

KingmakerVN
Regular
Posts: 37
Joined: Sat Nov 23, 2019 7:03 pm
Projects: Kingmaker
Contact:

Re: Is it possible to make text align with a curve rather than a straight line on a screen?

#3 Post by KingmakerVN »

PyTom wrote: Sat Oct 07, 2023 11:12 pm No - at least right now, that's not a feature Ren'Py has.
Shame. Thank you for taking the time to answer.

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

Re: Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#4 Post by m_from_space »

KingmakerVN wrote: Sun Oct 08, 2023 1:01 amShame.
In principle you should be able to write yourself a function that does just that:

1. Split the text into single characters
2. Follow a bezier curve using a fixed distance (like if you would plot it).
3. Place the next character at this specific location and then rotate it accordingly.

I have the feeling that it wouldn't be that hard in Python using Renpy, but maybe I am wrong about it.

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

Re: Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#5 Post by m_from_space »

Haha, okay. Here is what I came up with. It's not perfect but it works.

edit: some things that probably can be improved here:
1) figure out how to plot it to have fixed distance between the characters
2) make sure we only calculate the positions and rotation once and save it in some kind of object, so Renpy doesn't call the function every time it renders the screen

Code: Select all

init python:
    import math
    # return point (x, y) on bezier curve (p0, p1, p2, p3) and rotation r of that point's tangent at t-value
    def bezier_info(p0, p1, p2, p3, t):
        x = int((1 - t)**3 * p0[0] + 3 * (1 - t)**2 * t * p1[0] + 3 * (1 - t) * t**2 * p2[0] + t**3 * p3[0])
        y = int((1 - t)**3 * p0[1] + 3 * (1 - t)**2 * t * p1[1] + 3 * (1 - t) * t**2 * p2[1] + t**3 * p3[1])
        u = (1 - t)**2 * (p1[0] - p0[0]) + 2*t * (1 - t) * (p2[0] - p1[0]) + t**2 * (p3[0] - p2[0])
        v = (1 - t)**2 * (p1[1] - p0[1]) + 2*t * (1 - t) * (p2[1] - p1[1]) + t**2 * (p3[1] - p2[1])
        r = int(math.degrees(math.atan2(v, u)))
        return x, y, r

transform atl_bezier(x, y, r):
    xcenter x
    ycenter y
    rotate r

# draw a curved text (change td for different character spacing)
screen curved_text(what, p0, p1, p2, p3, td=0.05):
    default l = len(what)
    default t = 0.0
    for i in range(l):
        $ x, y, r = bezier_info(p0, p1, p2, p3, t)
        text what[i] color "#f00" at atl_bezier(x, y, r)
        $ t += td

label start:
    # define a bezier curve's points
    $ p0 = (100, 300)  # start point
    $ p1 = (300, 100)  # control point 1
    $ p2 = (500, 500)  # control point 2
    $ p3 = (700, 300)  # end point
    show screen curved_text("HELLO, I AM A CURVED TEXT!", p0, p1, p2, p3)
    "Can you see it?"

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

Re: Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#6 Post by m_from_space »

Alright, so here is a better version, using a creator-defined displayable. This allows you to create curved texts that you can use like regular images. Feel free to make it better. :)

Code: Select all

init python:
    import math
    class CurvedText(renpy.Displayable):
        def __init__(self, what, p0, p1, p2, p3, **kwargs):

            super(CurvedText, self).__init__(**kwargs)

            # a list of arguments we will use for the final composite image
            args = []

            l = len(what)
            # current position along the curve (0.0 = start, 1.0 = end)
            t = 0.0
            # equal step distance depending on the text length
            td = 1.0 / l

            for i in range(l):
                # the position on the bezier curve at t value
                x = int((1 - t)**3 * p0[0] + 3 * (1 - t)**2 * t * p1[0] + 3 * (1 - t) * t**2 * p2[0] + t**3 * p3[0])
                y = int((1 - t)**3 * p0[1] + 3 * (1 - t)**2 * t * p1[1] + 3 * (1 - t) * t**2 * p2[1] + t**3 * p3[1])
                # the vector of the tangent on that position
                u = (1 - t)**2 * (p1[0] - p0[0]) + 2*t * (1 - t) * (p2[0] - p1[0]) + t**2 * (p3[0] - p2[0])
                v = (1 - t)**2 * (p1[1] - p0[1]) + 2*t * (1 - t) * (p2[1] - p1[1]) + t**2 * (p3[1] - p2[1])
                # the rotation of the vector in relation to the x-axis
                r = math.degrees(math.atan2(v, u))

                args.append(Transform(Text(what[i], **kwargs), xpos=x, ypos=y, rotate=r))

                # 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

transform atl_blink:
    alpha 0.0
    linear 1.0 alpha 1.0
    linear 1.0 alpha 0.0
    repeat

image ct1 = CurvedText("Hello, I'm a curved text!", (100, 100), (400, 0), (500, 400), (800, 300))
image ct2 = CurvedText("I will always be written from start to end.", (100, 100), (400, 0), (500, 400), (800, 300), size=16, color="#00f")
image ct3 = CurvedText("I'm big, bold and I blink.", (200, 300), (600, 400), (400, 200), (900, 500), color="#0f0", bold=True, size=24)

label start:

    show ct1
    show ct2:
        yoffset 100
    show ct3 at atl_blink

    "You should see curved texts."

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

Re: Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#7 Post by m_from_space »

So since this was so much fun, I read more about bezier curves and found out that there is this famous "De Casteljau's algorithm" which can be used for any type of curve (two, three, four and more points...). My previous example was only working for 4 points curves. But here you go, put whatever control points you have in there and have fun with it. It should be way more efficient too now.

/Update1: Added subpixel placement and the option to define where to start and end writing on the curve.
/Update2: The last character was not placed on the end of the curve, but one step before. This is fixed now.
/Update3: Only fix the last character's rotation if t_end == 1.0, not in other cases.
/Update4: Skip whitespaces, we neither need the calculation, nor a text object for them

Picture of the following example:
example.png
(38.1 KiB) Not downloaded yet

Arguments:

Code: Select all

what:		The text you want to curve
points:		a list or tuple of points that define the curve (points can be a list or a tuple)
rot:		whether you want the letters to rotate along the curve (default True)
t_start:	where to start writing on the curve (default 0.0)
t_end:		where to end writing on the curve (default 1.0)
optional:	you can pass keyword arguments to the text object like "font", "color", "size" and so on.
Working example code:

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

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

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("Wow, 5 point curves rock...", curve_5_points, color="#0ff")
image ct5 = CurvedText("This one is without letter rotation.", curve_4_points, rot=False, color="#ff0")

label start:

    show ct1
    show ct2
    show ct3:
        yoffset 50
    show ct4
    show ct5:
        yoffset 200

    "Have fun playing around!"

KingmakerVN
Regular
Posts: 37
Joined: Sat Nov 23, 2019 7:03 pm
Projects: Kingmaker
Contact:

Re: Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#8 Post by KingmakerVN »

m_from_space wrote: Sun Oct 08, 2023 4:02 pm
KingmakerVN wrote: Sun Oct 08, 2023 1:01 amShame.
In principle you should be able to write yourself a function that does just that:

1. Split the text into single characters
2. Follow a bezier curve using a fixed distance (like if you would plot it).
3. Place the next character at this specific location and then rotate it accordingly.

I have the feeling that it wouldn't be that hard in Python using Renpy, but maybe I am wrong about it.
Woah! You really went above and beyond. Thanks a lot! Personally I would never understand the world of high finances when it comes to coding so I wouldn't have been capable of any of that hahahaha. Once again thank you. You are a hero without a cape.

User avatar
Andredron
Miko-Class Veteran
Posts: 729
Joined: Thu Dec 28, 2017 2:37 pm
Location: Russia
Contact:

Re: Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#9 Post by Andredron »

m_from_space wrote: Mon Oct 09, 2023 4:16 pm So since this was so much fun, I read more about bezier curves and found out that there is this famous "De Casteljau's algorithm" which can be used for any type of curve (two, three, four and more points...). My previous example was only working for 4 points curves. But here you go, put whatever control points you have in there and have fun with it. It should be way more efficient too now.

/Update1: Added subpixel placement and the option to define where to start and end writing on the curve.
/Update2: The last character was not placed on the end of the curve, but one step before. This is fixed now.
/Update3: Only fix the last character's rotation if t_end == 1.0, not in other cases.
/Update4: Skip whitespaces, we neither need the calculation, nor a text object for them

Picture of the following example:
example.png


Arguments:

Code: Select all

what:		The text you want to curve
points:		a list or tuple of points that define the curve (points can be a list or a tuple)
rot:		whether you want the letters to rotate along the curve (default True)
t_start:	where to start writing on the curve (default 0.0)
t_end:		where to end writing on the curve (default 1.0)
optional:	you can pass keyword arguments to the text object like "font", "color", "size" and so on.
Working example code:

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

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

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("Wow, 5 point curves rock...", curve_5_points, color="#0ff")
image ct5 = CurvedText("This one is without letter rotation.", curve_4_points, rot=False, color="#ff0")

label start:

    show ct1
    show ct2
    show ct3:
        yoffset 50
    show ct4
    show ct5:
        yoffset 200

    "Have fun playing around!"
Wow!

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

Re: Is it possible to make text align with a curve rather than a straight line on a screen? [Solved]

#10 Post by m_from_space »

Andredron wrote: Tue Oct 31, 2023 5:57 am Wow!
I posted this code here for better visibility by the way: viewtopic.php?t=67350

And I also created another CDD named "CurvedMultiText" that draws multiple lines of text along a curve. It's also inside the post I linked (including an image so you can check it out beforehand).

Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot]