pythonmanim

Is there a Manim/Python function to turn a moving shape (or line) into a drawing tool?


I am using Manim/Python to draw a clock face, then illustrate the idea of fractions of an hour. The minute hand sweeps from 12 to 3 o'clock (ie, 15 minutes) and as it draws, I want it to ink in as it goes, creating a wedge. I can't find out how to make it leave a trail. My usual source (ChatGPT) can't do it somehow (it keeps giving me failure after failure, drawing the clock face and the sweep of the minute hand, but not painting the arc as it goes).


Solution

  • A filled wedge between the minute hands on a clock can be created using a filled angle and an updater function. The filled angle creates the wedge. The updater function moves your minute hands and updates the wedge accordingly.

    (1) Filled Wedge between Minute Hands

    For simplicity, let's take a circle as the clock and two lines as the minute hand at 12 and 3pm. The line at 12 will be a placeholder for us to compute the angle and we will not move it over time.
    The wedge you're looking for is the polygon spanned by the clock's center (where the minute hands touch) and the circular arc between the endpoints of the lines on the clock's outer frame.

    from manim import *
    
    class ClockDrawer(Scene):
        def construct(self):
            radius = 2
            clock = Circle(radius = radius)
    
            # minute hand at 12
            l1 = Line(ORIGIN, 2 * UP).set_color(GREEN)
            # minute hand at 3
            l2 = (
                Line(ORIGIN, 2*RIGHT)
                .set_color(GREEN)
            )
            
            # Compute the angles between both hands. We want to fill between the
            # inner corner of the angle (radius = 0, where the hands touch) and 
            # the outer corner of the angle (radius = radius, the outer edge of the clock)
            angle_inner = Angle(l1, l2, other_angle=True, radius=0).set_color(GREEN)
            angle_outer = Angle(l1, l2, other_angle=True, radius=radius).set_color(GREEN)
            
            coord_angle_inner = angle_inner.points
            coord_angle_outer = angle_outer.points
            
            # combine coordinates to a polygon: (inner arc path, outer arc path,
            # first coordinate of the inner arc to close the polygon)
            pnts = np.concatenate([coord_angle_inner, 
                                   coord_angle_outer, 
                                   coord_angle_inner[0].reshape(1, 3)])
    
            # fill in the polygon
            mfill = VMobject().set_color(ORANGE)
            mfill.set_points_as_corners(pnts).set_fill(GREEN, opacity=1)
    
            self.add(l1, l2) # add minute hands
            self.add(mfill) # fill angle
            self.add(clock)
    

    and the resulting wedge looks like this:

    Circular arc between two minute hands on a clock

    Now to make this angle fill gradually as time passes (i.e. line2 moves from 12 to 3), we need an updater function

    (2) Updater function for gradual filling

    The updater function (1) rotates the second minute hand and (2) fills in the new angle.

    Structure of an updater function

    An updater functions takes a moving object (mob) and a time difference (dt) as inputs. In our case, mob is the filled polygon, so that the code structure looks looks like this:

    # a moving object, whose corner points we update
    mfill = VMobject().set_color(ORANGE).set_fill(GREEN, opacity=1)
    
    def update_mfill(mob, dt):
        # define your updates here in
        # dependence of the time steps dt
    
        # then set the new wedge corners
        mob.set_points_as_corners(pnts)
    
    # equip the moving object with the updater
    mfill.add_updater(update_mfill)
    
    # the updater will run for this duration:
    self.wait(8)
    

    Rotation of minute hand

    To update the second minute hand we can use the rotate function:

    inv_speed = 10
    l2.rotate(- dt * PI/inv_speed, about_point=ORIGIN)
    

    I added a speed parameter here, where higher values make the minute hand travel slower.

    We also need to make sure that the hand stops at 3 o'clock. We can check the current angle between the minute hands and then exit the updater:

    v1 = l1.get_unit_vector()
    v2 = l2.get_unit_vector()
    current_angle = angle_between_vectors(v1, v2)
    
    if current_angle >= PI / 2:
        # some exit condition if rotated by 90 degree = 3 o'clock
    

    Full Example

    Combined in an updater and with the code from before, we get the desired clock:

    from manim import *
    
    class ClockDrawer(Scene):
        def construct(self):
            # Use a circle as a clock, you can add
            # additional styling here
            radius = 2
            clock = Circle(radius=radius)
    
            # Minute hand at 12 (this helps us to 
            # compute the angle)
            l1 = Line(ORIGIN, 2 * UP).set_color(GREEN)
            # Minute hands that travels to 3 and starts at 12
            l2 = Line(ORIGIN, 2 * UP).set_color(GREEN)
    
            # Wedge between minute hands
            mfill = VMobject().set_color(ORANGE).set_fill(GREEN, opacity=1)
    
            # Updater function to rotate l2 and update wedge
            def update_mfill(mob, dt):
    
                # Rotate second minute hand by a small negative angle   
                # (meaning we go clockwise)
                inv_speed = 10
                l2.rotate(- dt * PI/inv_speed, about_point=ORIGIN)
                
                # check the current angle between the minute hands
                v1 = l1.get_unit_vector()
                v2 = l2.get_unit_vector()
                current_angle = angle_between_vectors(v1, v2)
    
                # don't fill wedge if angle is too small
                # (if the arc is 0, manim throws an error)
                if current_angle < 1e-12:
                    return
    
                # Stop at 90 degrees (PI/4 radians)
                # = 3 o clock
                if current_angle >= PI / 2:
                    # remove updater when the the second
                    # minute hand is at desired position
                    mfill.remove_updater(update_mfill)
                    return
    
                # Update angles for outer and inner arc
                angle_inner = Angle(l1, l2, other_angle=True, radius=0)
                angle_outer = Angle(l1, l2, other_angle=True, radius=radius)
                coord_angle_inner = angle_inner.points
                coord_angle_outer = angle_outer.points
    
                # Combine points
                pnts = np.concatenate([
                    coord_angle_inner,
                    coord_angle_outer,
                    coord_angle_inner[0].reshape(1, 3)
                ])
    
                # Update wedge
                mob.set_points_as_corners(pnts)
    
            # Attach updater to mfill
            mfill.add_updater(update_mfill)
            self.add(clock, l1, l2, mfill)
    
            self.wait(8)
    

    Here are snapshots of 3 points in time for this animation:

    Wedge being drawn in clock