Skip to content

core

The lanim.core module provides a rendering frontend-agnostic core for creating and transforming general animations.

Projector

Generic type alias standing for Callable[[float], A].

The semantics are as follows: a projector is a function mapping a progress (a float from 0 to 1 inclusive) to a still frame A.

Passing a value outside of the [0; 1] range is undefined behaviour. You should watch out for this, especially in light of floating-point errors.

Animation () dataclass

Animation with a given duration on a set A of still frames

duration: float dataclass-field

How long the animation lasts, in seconds

projector: Callable[[float], +A] dataclass-field

Strategy to get a still frame from a progress in [0; 1]

map(self, f: Callable[[A], B]) -> Animation[B]

Apply a function to each frame of an animation

Source code in lanim/core.py
def map(self, f: Callable[[A], B]) -> Animation[B]:
    """
    Apply a function to each frame of an animation
    """
    return self.map_projector(lambda p: map_p(p, f))

progress_map(self, f: Callable[[A, float], B]) -> Animation[B]

Similar to map, but also passes the t parameter to the provided function f.

Source code in lanim/core.py
def progress_map(self, f: Callable[[A, float], B]) -> Animation[B]:
    """
    Similar to [`map`][lanim.core.Animation.map], but also passes the `t`
    parameter to the provided function `f`.
    """
    return self.map_projector(lambda p: lambda t: f(p(t), t))

ease(self, easing: Easing) -> Animation[A]

Apply an easing to an animation

Source code in lanim/core.py
def ease(self, easing: Easing) -> Animation[A]:
    """
    Apply an easing to an animation
    """
    return self.map_projector(lambda p: ease_p(p, easing))

__mul__(self, factor: float) -> Animation[A] special

Overloading of self * factor.

Stretch the duration of the animation by factor

Source code in lanim/core.py
def __mul__(self, factor: float) -> Animation[A]:
    """
    Overloading of `self * factor`.

    Stretch the duration of the animation by `factor`
    """
    return self.with_duration(self.duration * factor)

__add__(self, other: Animation[B]) -> Animation[Union[A, B]] special

Overloading of self + other.

Put one animation after the other

Source code in lanim/core.py
def __add__(self, other: Animation[B]) -> Animation[Union[A, B]]:
    """
    Overloading of `self + other`.

    Put one animation after the other
    """
    return seq_a(self, other)  # type: ignore

__rshift__(self, other) special

Overloading of self >> other.

Apply a function in a pipes-and-filters style:

  • self >> fn means fn(self)
  • self >> (fn, arg) means fn(self, arg)
  • self >> (fn, arg1, arg2) means fn(self, arg1, arg2)
  • etc.
Source code in lanim/core.py
def __rshift__(self, other):
    """
    Overloading of `self >> other`.

    Apply a function in a pipes-and-filters style:

    - `self >> fn` means `fn(self)`
    - `self >> (fn, arg)` means `fn(self, arg)`
    - `self >> (fn, arg1, arg2)` means `fn(self, arg1, arg2)`
    - etc.
    """
    if hasattr(other, "__call__"):
        return other(self)
    if not isinstance(other, tuple):
        return NotImplemented
    fn, *args = other
    return fn(self, *args)

Functions on projectors

const_p(a: C) -> Projector[C]

Make a projector that always returns the same still frame

Source code in lanim/core.py
def const_p(a: C) -> Projector[C]:
    """
    Make a projector that always returns the same still frame
    """
    return lambda _: a

map_p(proj: Projector[A], f: Callable[[A], B]) -> Projector[B]

Apply a function to the result of a projector

Source code in lanim/core.py
def map_p(proj: Projector[A], f: Callable[[A], B]) -> Projector[B]:
    """
    Apply a function to the result of a projector
    """
    return lambda t: f(proj(t))

join_p(proj: Projector[Projector[A]]) -> Projector[A]

Flatten a projector of projectors into just a projector

Source code in lanim/core.py
def join_p(proj: Projector[Projector[A]]) -> Projector[A]:
    """
    Flatten a projector of projectors into just a projector
    """
    return lambda t: proj(t)(t)

flatmap_p(proj: Projector[A], f: Callable[[A], Projector[B]]) -> Projector[B]

Composition of map_p and join_p

Source code in lanim/core.py
def flatmap_p(proj: Projector[A], f: Callable[[A], Projector[B]]) -> Projector[B]:
    """
    Composition of `map_p` and `join_p`
    """
    return lambda t: f(proj(t))(t)

ease_p(proj: Projector[A], e: Easing) -> Projector[A]

Apply an easing to a projector

>>> from lanim.easings import in_out
>>> projector1 = lambda t: (3*t, 4*t)
>>> projector2 = ease_p(projector1, in_out)
>>> projector1(0.54)
(1.62, 2.16)
>>> projector2(0.54)
(1.832, 2.443)

Source code in lanim/core.py
def ease_p(proj: Projector[A], e: Easing) -> Projector[A]:
    """
    Apply an easing to a projector
    ```py
    >>> from lanim.easings import in_out
    >>> projector1 = lambda t: (3*t, 4*t)
    >>> projector2 = ease_p(projector1, in_out)
    >>> projector1(0.54)
    (1.62, 2.16)
    >>> projector2(0.54)
    (1.832, 2.443)
    ```
    """
    return lambda t: proj(e(t))

Functions on animations

const_a(a: C) -> Animation[C]

Create a second-long animation consisting of a still frame

Source code in lanim/core.py
def const_a(a: C) -> Animation[C]:
    """
    Create a second-long animation consisting of a still frame
    """
    return Animation(1.0, const_p(a))

seq_a(*animations: Animation[A]) -> Animation[A]

Put animations in sequence, one after another.

Source code in lanim/core.py
def seq_a(*animations: Animation[A]) -> Animation[A]:
    """
    Put animations in sequence, one after another.
    """
    if animations == ():
        raise ValueError("No animations")

    total = 0.0
    steps: list[tuple[float, float, Animation[A]]] = []
    for a in animations:
        dur = a.duration * 512
        steps.append((total, total+dur, a))
        total += dur

    # 100__________500________________1200
    #       +400             +700
    # (0, 100, ...), (400, 500, ...), (500, 1200, ...)

    def projector(t: float) -> A:
        for (begin, end, a) in reversed(steps):
            if begin <= t * total <= end:
                break
        t_offset = begin / total  # type: ignore
        t_delta = t - t_offset
        t_scale = total / (end - begin) # type: ignore

        # this is required to account for floating-point errors:
        t = max(0, min(1, t_scale * t_delta))
        return a.projector(t)  # type: ignore

    return Animation(sum(a.duration for a in animations), projector)

flatmap_a(animations: Iterable[Animation[A]], f: Callable[[Animation[A]], Iterable[Animation[B]]]) -> Animation[B]

For each animation animations, apply f to get a list of animations which in turn will be concatenated.

Source code in lanim/core.py
def flatmap_a(
    animations: Iterable[Animation[A]],
    f: Callable[[Animation[A]], Iterable[Animation[B]]],
) -> Animation[B]:
    """
    For each animation `animations`, apply `f` to get a list of
    animations which in turn will be concatenated.
    """
    return seq_a(*_it_flatmap(animations, f))

par_a_longest(anim1: Animation[A], anim2: Animation[B]) -> Animation[tuple[A, B]]

Run two animations side by side.

If one animation is longer than the other, the last frame of the shorter animation will be kept hanging.

Source code in lanim/core.py
def par_a_longest(anim1: Animation[A], anim2: Animation[B]) -> Animation[tuple[A, B]]:
    """
    Run two animations side by side.

    If one animation is longer than the other, the last frame of
    the shorter animation will be kept hanging.
    """
    duration = max(anim1.duration, anim2.duration)
    def projector(t: float) -> tuple[A, B]:
        t1 = t * min(1.0, duration / anim1.duration)
        t2 = t * min(1.0, duration / anim2.duration)
        return (anim1.projector(t1), anim2.projector(t2))
    return Animation(duration, projector)

par_a_shortest(anim1: Animation[A], anim2: Animation[B]) -> Animation[tuple[A, B]]

Run two animations side by side.

If one animation is longer animation, the longer animation will be cut off when the short one ends.

Source code in lanim/core.py
def par_a_shortest(anim1: Animation[A], anim2: Animation[B]) -> Animation[tuple[A, B]]:
    """
    Run two animations side by side.

    If one animation is longer animation, the longer animation
    will be cut off when the short one ends.
    """
    duration = min(anim1.duration, anim2.duration)
    def projector(t: float) -> tuple[A, B]:
        t1 = t * duration / anim1.duration
        t2 = t * duration / anim2.duration
        return (anim1.projector(t1), anim2.projector(t2))
    return Animation(duration, projector)

pause_after(anim: Animation[A], duration: float) -> Animation[A]

Stretch the last frame for duration seconds after the animation is over.

Source code in lanim/core.py
def pause_after(anim: Animation[A], duration: float) -> Animation[A]:
    """
    Stretch the last frame for `duration` seconds after the
    animation is over.
    """
    last_frame = anim.projector(1.0)
    total_duration = duration + anim.duration
    def projector(t: float) -> A:
        if t < anim.duration / total_duration:
            return anim.projector(t * total_duration / anim.duration)
        else:
            return last_frame
    return Animation(total_duration, projector)

pause_before(anim: Animation[A], duration: float) -> Animation[A]

Stretch the first frame for duration seconds before the animation starts.

Source code in lanim/core.py
def pause_before(anim: Animation[A], duration: float) -> Animation[A]:
    """
    Stretch the first frame for `duration` seconds before the
    animation starts.
    """
    first_frame = anim.projector(0.0)
    total_duration = duration + anim.duration
    def projector(t: float) -> A:
        if t > duration / total_duration:
            return anim.projector((t - duration / total_duration) * total_duration / anim.duration)
        else:
            return first_frame
    return Animation(total_duration, projector)

crop_by_range(anim: Animation[A], start: float, finish: float) -> Animation[A]

Render a range of an animation. start and finish should be floats from 0 to 1.

Source code in lanim/core.py
def crop_by_range(anim: Animation[A], start: float, finish: float) -> Animation[A]:
    """
    Render a range of an animation.
    `start` and `finish` should be floats from 0 to 1.
    """
    if not (0 <= start < finish <= 1):
        raise ValueError(
            "Invalid range ({}, {}). Expected 0 <= start <= finish < 1"
            .format(start, finish)
        )
    scale_factor = finish - start
    new_duration = anim.duration * scale_factor

    def projector(t: float) -> A:
        return anim.projector(t * scale_factor + start)

    return Animation(new_duration, projector)

frames(animation: Animation[A], fps: float) -> Iterator[A]

Generate a series of discrete frames from an animation given the frames per second.

Source code in lanim/core.py
def frames(animation: Animation[A], fps: float) -> Iterator[A]:
    """
    Generate a series of discrete frames from an animation given
    the frames per second.
    """
    total_steps = round(fps * animation.duration)
    for step in range(total_steps + 1):
        yield animation.projector(step / total_steps)