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
meansfn(self)
self >> (fn, arg)
meansfn(self, arg)
self >> (fn, arg1, arg2)
meansfn(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)