Search code examples
python-3.xkivy

Bezier curves and label in Kivy


I'm relatively new to code and I'm currently learning Kivy. And I'm desperately stuck. I'm trying to create a label (which I have), and a Bezier curve. I want to animate the Label to move in the pat of the Bezier curve, not the control points.

This is the code I have so far:

from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.graphics import Color, Bezier
from kivy.animation import Animation

class BezierPath(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        with self.canvas:
      
            Color(1, 0, 0)

            self.bezier = Bezier(points=[100, 100,  # Control point 1
                                     200, 300,  # Control point 2
                                     300, 100,  # Control point 3
                                     400, 400]) # End point

class BezierLabelApp(App):
    def build(self):
        bezier_path = BezierPath()

        label = Label(text="Follow me!")
        label.font_size = 20

        bezier_path.add_widget(label)

        animation = Animation(points=bezier_path.bezier.points, duration=5)
        animation.repeat = True
        animation.start(label)

        return bezier_path

if __name__ == "__main__":
    BezierLabelApp().run()

What am I doing wrong?`


Solution

  • The default Animation animates a property of the Widget along a straight line from its starting position to its ending position. There is no built-in way to animate along a path.

    You can animate along a path by providing your own calculation of the path and using that in an on_progress binding for the Animation. First, you need something to calculate the path:

    class SimpleBezier:
        def __init__(self, **kwargs):
            self.points = kwargs.pop('points', [])
    
        def get_point(self, t):
            # get the x, y point at parameter value t (0->1)
            num_points = int(len(self.points)/2)
            x = 0
            y = 0
            for i in range(num_points):
                px = self.points[i * 2]
                py = self.points[i * 2 + 1]
                coeff = comb(num_points-1, i) * math.pow(t, i) * math.pow((1.0 - t), (num_points - 1 - i))
                x += coeff * px
                y += coeff * py
            return x, y
    

    Then, you can use this for the on_progress binding in the App class:

    class BezierLabelApp(App):
        def build(self):
            bezier_path = BezierPath()
    
            label = Label(text="Follow me!")
            label.font_size = 20
    
            bezier_path.add_widget(label)
    
            animation = Animation(duration=5)
            animation.bind(on_progress=self.update_pos)  # setup the binding
            # animation.repeat = True
            animation.start(label)
    
            self.sb = SimpleBezier(points=bezier_path.bezier.points)  # creat an instance of SimpleBezier
    
            return bezier_path
    
        def update_pos(self, anim, label, t):
            label.center = self.sb.get_point(t)  # use the SimpleBezier instance to calculate the position
    

    I have commented out the animation.repeat = True as I was not sure what your purpose was.