Skip to content

The coordinate system

In this tutorial, we’re going to build this animation:

A point moving across the plane

1. Drawing the axes

We don’t have a Line primitive, so we’ll use very thin rectangles for lines. Good enough!


The horizontal axis is drawn like this. It’s just a static image, nothing fancy.

from lanim.core import const_a
from lanim.pil import Align, Group, Latex, Rect, Triangle


def horizontal_axis(width):
    return Group([
        Rect(
            x=-0.3, y=0,
            width=width-0.6, height=0.04,
            line_width=2
        ),
        Triangle(
            x=width/2, y=0,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=-0.6, dy3=0.6,
            line_width=2
        ),
        Latex(
            x=6.5, y=-0.1, source=f"${width}$", scale_factor=0.75, align=Align.CD
        )
    ])


export = const_a(horizontal_axis(16))

Horizontal axis

In previous tutorials, we used some sort of “units” without any explanation. Units are pretty simple: 1 unit equals 1/16th of the screen width. So if the dimensions of your animation are 16:9, your canvas is 16 units wide and 9 units high.

The horizontal (x) axis points to the right, and the vertical axis (y) points down. The center of the screen is the origin — the point where x and y are both 0.


The vertical axis is pretty much the same.

from lanim.core import const_a
from lanim.pil import Align, Group, Latex, Rect, Triangle


def horizontal_axis(width):
    return Group([
        Rect(
            x=-0.3, y=0,
            width=width-0.6, height=0.04,
            line_width=2
        ),
        Triangle(
            x=width/2, y=0,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=-0.6, dy3=0.6,
            line_width=2
        ),
        Latex(
            x=6.5, y=-0.1, source=f"${width}$", scale_factor=0.75, align=Align.CD
        )
    ])


def vertical_axis(height):
    return Group([
        Rect(
            x=0, y=-0.3,
            width=0.04, height=height-0.6,
            line_width=2
        ),
        Triangle(
            x=0, y=height/2,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=0.6, dy3=-0.6,
            line_width=2
        ),
        Latex(
            x=0.1, y=3, source=f"${height}$", scale_factor=0.75, align=Align.LC
        )
    ])


axes = Group([
    horizontal_axis(16),
    vertical_axis(9)
])


export = const_a(axes)

Both axes


Now we’ll add notches where the 1 mark is

from lanim.core import const_a
from lanim.pil import Align, Group, Latex, Rect, Triangle


def horizontal_axis(width):
    return Group([
        Rect(
            x=-0.3, y=0,
            width=width-0.6, height=0.04,
            line_width=2
        ),
        Triangle(
            x=width/2, y=0,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=-0.6, dy3=0.6,
            line_width=2
        ),
        Latex(
            x=6.5, y=-0.1, source=f"${width}$", scale_factor=0.75, align=Align.CD
        )
    ])


def vertical_axis(height):
    return Group([
        Rect(
            x=0, y=-0.3,
            width=0.04, height=height-0.6,
            line_width=2
        ),
        Triangle(
            x=0, y=height/2,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=0.6, dy3=-0.6,
            line_width=2
        ),
        Latex(
            x=0.1, y=3, source=f"${height}$", scale_factor=0.75, align=Align.LC
        )
    ])


axes = Group([
    horizontal_axis(16),
    vertical_axis(9)
])

notches = Group([
    Rect(x=1, y=0, width=0.05, height=0.5, line_width=2),
    Rect(x=0, y=1, width=0.5, height=0.05, line_width=2),
])

export = const_a(Group([axes, notches]))

Notches

2. Animating the point

Let’s make a function moving_point(x1, y1, x2, y2) that returns an animation

This is what it’s going to look like:

def moving_point(x1, y1, x2, y2):
    def projector(t):
        return Group([...])
    return Animation(1, projector)

(x, y) represents the point we’re currently drawing

def moving_point(x1, y1, x2, y2):
    def projector(t):
        x = x1*(1 - t) + x2*t
        y = y1*(1 - t) + y2*t
        latex = f"$(x:{x:.1f}, y:{y:.1f})$"
        return Group([
            Rect(x, y, 0.05, 0.05, line_width=2),
            Rect(x, y, 0.25, 0.25, line_width=0.7),
            Latex(x, y-0.1, latex, align=Align.CD).scaled(0.4),
        ])
    return Animation(1, projector)

latex stores a formatter LaTeX string, like $(x:5.0, y:-3.0)$.

def moving_point(x1, y1, x2, y2):
    def projector(t):
        x = x1*(1 - t) + x2*t
        y = y1*(1 - t) + y2*t
        latex = f"$(x:{x:.1f}, y:{y:.1f})$"
        return Group([
            Rect(x, y, 0.05, 0.05, line_width=2),
            Rect(x, y, 0.25, 0.25, line_width=0.7),
            Latex(x, y-0.1, latex, align=Align.CD).scaled(0.4),
        ])
    return Animation(1, projector)

We return a bundle of:

  • the inner square
  • the outer square
  • a Latex object with the label
def moving_point(x1, y1, x2, y2):
    def projector(t):
        x = x1*(1 - t) + x2*t
        y = y1*(1 - t) + y2*t
        latex = f"$(x:{x:.1f}, y:{y:.1f})$"
        return Group([
            Rect(x, y, 0.05, 0.05, line_width=2),
            Rect(x, y, 0.25, 0.25, line_width=0.7),
            Latex(x, y-0.1, latex, align=Align.CD).scaled(0.4),
        ])
    return Animation(1, projector)

Now let’s see it in action.

from lanim.core import Animation
from lanim.pil import Align, Group, Latex, Rect
from lanim.easings import in_out


def moving_point(x1, y1, x2, y2):
    def projector(t):
        x = x1*(1 - t) + x2*t
        y = y1*(1 - t) + y2*t
        latex = f"$(x:{x:.1f}, y:{y:.1f})$"
        return Group([
            Rect(x, y, 0.05, 0.05, line_width=2),
            Rect(x, y, 0.25, 0.25, line_width=0.7),
            Latex(x, y-0.1, latex, align=Align.CD).scaled(0.4),
        ])
    return Animation(1, projector)


export = moving_point(x1=-3, y1=2, x2=5, y2=-3).ease(in_out) * 6

A moving point


Let’s make the point go around in a loop.

export = (
      moving_point(x1=-3, y1=2, x2=5, y2=-3).ease(in_out) * 6
    + moving_point(x1=5, y1=-3, x2=-4, y2=-3).ease(in_out) * 4
    + moving_point(x1=-4, y1=-3, x2=-3, y2=2).ease(in_out) * 4
)

Point in a loop


If you have lots of animations, it can be inconvenient to use +. You can use the seq_a function, which essentially runs + on a list of animations.

from lanim.core import Animation, seq_a

...

export = seq_a(
    moving_point(x1=-3, y1=2, x2=5, y2=-3).ease(in_out) * 6,
    moving_point(x1=5, y1=-3, x2=-4, y2=-3).ease(in_out) * 4,
    moving_point(x1=-4, y1=-3, x2=-3, y2=2).ease(in_out) * 4,
)

3. Combining the animations

Let’s rename the export of the previous animation to point_animation.

All the code so far
from lanim.core import Animation, seq_a, const_a
from lanim.pil import Align, Group, Latex, Rect, Triangle
from lanim.easings import in_out


def horizontal_axis(width):
    return Group([
        Rect(
            x=-0.3, y=0,
            width=width-0.6, height=0.04,
            line_width=2
        ),
        Triangle(
            x=width/2, y=0,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=-0.6, dy3=0.6,
            line_width=2
        ),
        Latex(
            x=6.5, y=-0.1, source=f"${width}$", scale_factor=0.75, align=Align.CD
        )
    ])


def vertical_axis(height):
    return Group([
        Rect(
            x=0, y=-0.3,
            width=0.04, height=height-0.6,
            line_width=2
        ),
        Triangle(
            x=0, y=height/2,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=0.6, dy3=-0.6,
            line_width=2
        ),
        Latex(
            x=0.1, y=3, source=f"${height}$", scale_factor=0.75, align=Align.LC
        )
    ])


def moving_point(x1, y1, x2, y2):
    def projector(t):
        x = x1*(1 - t) + x2*t
        y = y1*(1 - t) + y2*t
        latex = f"$(x:{x:.1f}, y:{y:.1f})$"
        return Group([
            Rect(x, y, 0.05, 0.05, line_width=2),
            Rect(x, y, 0.25, 0.25, line_width=0.7),
            Latex(x, y-0.1, latex, align=Align.CD).scaled(0.4),
        ])
    return Animation(1, projector)


#---------------------------------------#


axes = Group([
    horizontal_axis(16),
    vertical_axis(9)
])

notches = Group([
    Rect(x=1, y=0, width=0.05, height=0.5, line_width=2),
    Rect(x=0, y=1, width=0.5, height=0.05, line_width=2),
])


point_animation = seq_a(
    moving_point(x1=-3, y1=2, x2=5, y2=-3).ease(in_out) * 6,
    moving_point(x1=5, y1=-3, x2=-4, y2=-3).ease(in_out) * 4,
    moving_point(x1=-4, y1=-3, x2=-3, y2=2).ease(in_out) * 4,
)


export = const_a(Group([axes, notches]))

What we need to do know is to make an animation which will play the point_animatino but place axes and notches in the background, so to speak.

We will do it in three ways: a low-level one, a high-level one and finally using a built-in function.

3.1. The low-level way

def total_projector(t):
    return Group([
        axes,
        notches,
        point_animation.projector(t)
    ])
export = Animation(point_animation.duration, total_projector)

In this approach, we follow the requirements quite literally. For each value of t, we return a bundle of the axes, the notches and a frame of our dynamic animation.

It works!

It works

But it’s pretty long, and we have to make seure we supply the correct duration to Animation.

3.2. The high-level way

Animations have a map method. It allows you to apply a function to each frame of the animation.

export = point_animation.map(lambda point: Group([axes, notches, point]))

A more advanced solution would be to use a bound method:

export = point_animation.map(Group([axes, notches]).add)

3.3. gbackground

lanim provides a built-in function to express adding a static background:

from lanim.pil import gbackground

...

export = gbackground(point_animation, [axes, notches])

4. Adding a delay

Use the pause_after and pause_before functions to freeze for a given amount of seconds before the start or after the end of an animation.

from lanim.core import Animation, pause_after, pause_before, seq_a

...

export = gbackground(point_animation, [axes, notches])
export = pause_after(pause_before(export, 2), 2)

Try implementing these functions yourself as an exercise!

Final animation

The final program
from lanim.pil import gbackground, Align, Group, Latex, Rect, Triangle
from lanim.core import Animation, pause_after, pause_before, seq_a
from lanim.easings import in_out


def horizontal_axis(width):
    return Group([
        Rect(
            x=-0.3, y=0,
            width=width-0.6, height=0.04,
            line_width=2
        ),
        Triangle(
            x=width/2, y=0,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=-0.6, dy3=0.6,
            line_width=2
        ),
        Latex(
            x=6.5, y=-0.1, source=f"${width}$", scale_factor=0.75, align=Align.CD
        )
    ])


def vertical_axis(height):
    return Group([
        Rect(
            x=0, y=-0.3,
            width=0.04, height=height-0.6,
            line_width=2
        ),
        Triangle(
            x=0, y=height/2,
            dx1=0, dy1=0,
            dx2=-0.6, dy2=-0.6,
            dx3=0.6, dy3=-0.6,
            line_width=2
        ),
        Latex(
            x=0.1, y=3, source=f"${height}$", scale_factor=0.75, align=Align.LC
        )
    ])


def moving_point(x1, y1, x2, y2):
    def projector(t):
        x = x1*(1 - t) + x2*t
        y = y1*(1 - t) + y2*t
        latex = f"$(x:{x:.1f}, y:{y:.1f})$"
        return Group([
            Rect(x, y, 0.05, 0.05, line_width=2),
            Rect(x, y, 0.25, 0.25, line_width=0.7),
            Latex(x, y-0.1, latex, align=Align.CD).scaled(0.4),
        ])
    return Animation(1, projector)


#---------------------------------------#


axes = Group([
    horizontal_axis(16),
    vertical_axis(9)
])

notches = Group([
    Rect(x=1, y=0, width=0.05, height=0.5, line_width=2),
    Rect(x=0, y=1, width=0.5, height=0.05, line_width=2),
])

point_animation = seq_a(
    moving_point(x1=-3, y1=2, x2=5, y2=-3).ease(in_out) * 6,
    moving_point(x1=5, y1=-3, x2=-4, y2=-3).ease(in_out) * 4,
    moving_point(x1=-4, y1=-3, x2=-3, y2=2).ease(in_out) * 4,
)

export = gbackground(point_animation, [axes, notches])
export = pause_after(pause_before(export, 2), 2)

5. Recap

In this tutorial: - you’ve learned what coordinate system lanim uses - you’ve learned how to combine a dynamic animation with a static background