pil
Graphical objects
Rect ()
dataclass
Axis-aligned rectangle
x: float
dataclass-field
y: float
dataclass-field
width: float
dataclass-field
height: float
dataclass-field
line_width: float
dataclass-field
Triangle ()
dataclass
Triangle, defined by its center and the deviation of each point from its center.
x: float
dataclass-field
y: float
dataclass-field
dx1: float
dataclass-field
dy1: float
dataclass-field
dx2: float
dataclass-field
dy2: float
dataclass-field
dx3: float
dataclass-field
dy3: float
dataclass-field
line_width: float
dataclass-field
Group ()
dataclass
Group of PilRenderable
objects rendered together in order.
Type variable P
represents the type of each object contained.
items: Sequence[~P]
dataclass-field
x: float
property
readonly
y: float
property
readonly
center(self) -> tuple[float, float]
Source code in lanim/pil_types.py
def center(self) -> tuple[float, float]:
if len(self.items) == 0:
raise ValueError(f"Cannot find a center of an empty group, items: {self.items!r}")
cx = sum(item.x for item in self.items)/len(self.items)
cy = sum(item.y for item in self.items)/len(self.items)
return (cx, cy)
add(self, new_item: Q) -> Group[Union[P, Q]]
Source code in lanim/pil_types.py
def add(self, new_item: Q) -> Group[Union[P, Q]]:
return Group([*self.items, new_item])
concat(self, other: Group[Q]) -> Group[Union[P, Q]]
Source code in lanim/pil_types.py
def concat(self, other: Group[Q]) -> Group[Union[P, Q]]:
return Group([*self.items, *other.items])
Pair ()
dataclass
Pair of two PilRenderable
objects p and q, where
p is drawn before q. Exists primarily for better
type-checker support.
p: ~P
dataclass-field
q: ~Q
dataclass-field
x: float
property
readonly
y: float
property
readonly
as_group(self) -> Group[Union[P, Q]]
Source code in lanim/pil_types.py
def as_group(self) -> Group[Union[P, Q]]:
return Group(self)
center(self) -> tuple[float, float]
Source code in lanim/pil_types.py
def center(self) -> tuple[float, float]:
return Group((self.p, self.q)).center()
flip(self) -> Pair[Q, P]
Source code in lanim/pil_types.py
def flip(self) -> Pair[Q, P]:
return Pair[Q, P](self.q, self.p)
Latex ()
dataclass
Graphical primitive rendered via the LaTeX program.
x: float
dataclass-field
y: float
dataclass-field
source: str
dataclass-field
scale_factor: float
dataclass-field
align: Align
dataclass-field
packages: Collection[str]
dataclass-field
The LaTeX packages to include as \usepackage{...}
clauses
width(self) -> float
Source code in lanim/pil_types.py
def width(self) -> float:
img = self._render(self.scale_factor)
return img.width / 1920 * 16
height(self) -> float
Source code in lanim/pil_types.py
def height(self) -> float:
img = self._render(self.scale_factor)
return img.height / 1920 * 16
Nil ()
dataclass
Special graphical primitive that doesn’t do anything on render.
x: float
dataclass-field
y: float
dataclass-field
Sum ()
dataclass
Graphical primitive representing a union of two objects. The name Sum derives from sum type, not from a combination, which would be a product type.
item: Union[tuple[Literal['p'], ~PX], tuple[Literal['q'], ~QX]]
dataclass-field
mpq: Callable[[~PX, ~QX], Callable[[float], Union[tuple[Literal['p'], ~PX], tuple[Literal['q'], ~QX]]]]
dataclass-field
Strategy for morphing the left item into the right item
x: float
property
readonly
y: float
property
readonly
mqp: Callable[[QX, PX], Projector[Select[PX, QX]]]
property
readonly
with_left(self, left: PX) -> Sum[PX, QX]
Source code in lanim/pil_types.py
def with_left(self, left: PX) -> Sum[PX, QX]:
return self.with_pq(("p", left))
with_right(self, right: QX) -> Sum[PX, QX]
Source code in lanim/pil_types.py
def with_right(self, right: QX) -> Sum[PX, QX]:
return self.with_pq(("q", right))
with_pq(self, pq: Select[PX, QX]) -> Sum[PX, QX]
Source code in lanim/pil_types.py
def with_pq(self, pq: Select[PX, QX]) -> Sum[PX, QX]:
return Sum(pq, self.mpq)
map(self, fp: Callable[[PX], RX], fq: Callable[[QX], RX2]) -> Sum[RX, RX2]
Source code in lanim/pil_types.py
def map(self, fp: Callable[[PX], RX], fq: Callable[[QX], RX2]) -> Sum[RX, RX2]:
tag, item = self.item
if tag == "p":
return self.with_left(fp(item)) # type: ignore
else:
return self.with_right(fq(item)) # type: ignore
dispatch(self, fp: Callable[[PX], A], fq: Callable[[QX], A]) -> A
Source code in lanim/pil_types.py
def dispatch(self, fp: Callable[[PX], A], fq: Callable[[QX], A]) -> A:
tag, item = self.item
if tag == "p":
return fp(item) # type: ignore
else:
return fq(item) # type: ignore
Opacity ()
Wrapper around a PilRenderable value to render it with an opacity (from 0 to 1)
__init__(self, child: P, opacity: float = 1.0)
special
Source code in lanim/pil_types.py
def __init__(self, child: P, opacity: float = 1.0):
if not (0 <= opacity <= 1):
raise ValueError(f"Opacity should be between 0 and 1, got {opacity!r}")
self.x = child.x
self.y = child.y
self.child = child
self.opacity = opacity
fade(self, target: float = 0) -> Animation[Opacity[P]]
Source code in lanim/pil_types.py
def fade(self, target: float = 0) -> Animation[Opacity[P]]:
def projector(t: float):
return Opacity(self.child, self.opacity * (1 - t) + target * t)
return Animation(1, projector)
Manipulation of graphical primitives
moved_to(p: P, x: float, y: float) -> P
Return the object moved to a particular point.
Source code in lanim/pil_graphics.py
def moved_to(p: P, x: float, y: float) -> P:
"""
Return the object moved to a particular point.
"""
return p.moved(x - p.x, y - p.y)
morph_into(source: PX, destination: PX) -> Animation[PX]
Create a second-long animation of one object morphing into the other
Source code in lanim/pil_graphics.py
def morph_into(source: PX, destination: PX) -> Animation[PX]:
"""
Create a second-long animation of one object morphing into the other
"""
return Animation(1.0, lambda t: source.morphed(destination, t))
move_by(obj: PX, dx: float, dy: float) -> Animation[PX]
Create a second-long animation of an object moving by (dx
, dy
)
Source code in lanim/pil_graphics.py
def move_by(obj: PX, dx: float, dy: float) -> Animation[PX]:
"""
Create a second-long animation of an object moving by (`dx`, `dy`)
"""
return morph_into(obj, obj.moved(dx, dy))
scale(obj: PSX, factor: float) -> Animation[PSX]
Create a second-long animation of an object scaling factor
times
Source code in lanim/pil_graphics.py
def scale(obj: PSX, factor: float) -> Animation[PSX]:
"""
Create a second-long animation of an object scaling `factor` times
"""
return morph_into(obj, obj.scaled(factor))
align(obj: PAX, new_align: Align) -> Animation[PAX]
Create a second-long animation of an object changing alignment
Source code in lanim/pil_graphics.py
def align(obj: PAX, new_align: Align) -> Animation[PAX]:
"""
Create a second-long animation of an object changing alignment
"""
return morph_into(obj, obj.aligned(new_align))
lpair(ap: Animation[P], q: Q) -> Animation[Pair[P, Q]]
Add Q
as a still image to an animation of P
Source code in lanim/pil_graphics.py
def lpair(ap: Animation[P], q: Q) -> Animation[Pair[P, Q]]:
"""
Add `Q` as a still image to an animation of P
"""
return ap.map(lambda p: Pair(p, q))
rpair(p: P, aq: Animation[Q]) -> Animation[Pair[P, Q]]
Add P
as a still image to an animation of Q
Source code in lanim/pil_graphics.py
def rpair(p: P, aq: Animation[Q]) -> Animation[Pair[P, Q]]:
"""
Add `P` as a still image to an animation of Q
"""
return aq.map(lambda q: Pair(p, q))
lrpair_longest(ap: Animation[P], aq: Animation[Q]) -> Animation[Pair[P, Q]]
Combine two animations into an animation of a pair.
If one animation is longer, the other animation is extended.
Source code in lanim/pil_graphics.py
def lrpair_longest(ap: Animation[P], aq: Animation[Q]) -> Animation[Pair[P, Q]]:
"""
Combine two animations into an animation of a pair.
If one animation is longer, the other animation is extended.
"""
return par_a_longest(ap, aq).map(lambda pq: Pair(*pq))
lrpair_shortest(ap: Animation[P], aq: Animation[Q]) -> Animation[Pair[P, Q]]
Combine two animations into an animation of a pair.
If one animation is longer, it’s cut off.
Source code in lanim/pil_graphics.py
def lrpair_shortest(ap: Animation[P], aq: Animation[Q]) -> Animation[Pair[P, Q]]:
"""
Combine two animations into an animation of a pair.
If one animation is longer, it's cut off.
"""
return par_a_shortest(ap, aq).map(lambda pq: Pair(*pq))
gbackground(fg: Animation[P], bg: Iterable[Q]) -> Animation[Pair[Group[Q], P]]
Source code in lanim/pil_graphics.py
def gbackground(fg: Animation[P], bg: Iterable[Q]) -> Animation[Pair[Group[Q], P]]:
bg_list = list(bg)
return fg.map(lambda p: Pair(Group(bg_list), p))
gforeground(bg: Animation[P], fg: Iterable[Q]) -> Animation[Pair[P, Group[Q]]]
Source code in lanim/pil_graphics.py
def gforeground(bg: Animation[P], fg: Iterable[Q]) -> Animation[Pair[P, Group[Q]]]:
fg_list = list(fg)
return bg.map(lambda q: Pair(q, Group(fg_list)))
group_join(g: Group[Group[N]]) -> Group[N]
Source code in lanim/pil_graphics.py
def group_join(g: Group[Group[N]]) -> Group[N]:
items: list[N] = []
for subg in g.items:
items.extend(subg.items)
return Group(items)
mixed_group_join(g: Group[Union[N, Group[N]]]) -> Group[N]
Source code in lanim/pil_graphics.py
def mixed_group_join(g: Group[Union[N, Group[N]]]) -> Group[N]:
items: list[N] = []
for item in g.items:
if isinstance(item, Group):
items.extend(item.items)
else:
items.append(item)
return Group(items)
merge_group_animations(*animations: Animation[Group[N]]) -> Animation[Group[N]]
Source code in lanim/pil_graphics.py
def merge_group_animations(*animations: Animation[Group[N]]) -> Animation[Group[N]]:
return parallel(*animations).map(group_join)
parallel(*animations: Animation[P]) -> Animation[Group[P]]
Source code in lanim/pil_graphics.py
def parallel(*animations: Animation[P]) -> Animation[Group[P]]:
if animations == ():
raise ValueError("No animations!")
duration = max(animation.duration for animation in animations)
factors = [duration / animation.duration for animation in animations]
def projector(t: float) -> Group[P]:
return Group([a.projector(t * f if t < 1/f else 1.0) for (a, f) in zip(animations, factors)])
return Animation(duration, projector)
group(*ps: P) -> Group[P]
Source code in lanim/pil_graphics.py
def group(*ps: P) -> Group[P]:
return Group(ps)
with_last_frame(animation: Animation[A], last_frame: A) -> Animation[A]
Return the same animation but with the very last frame replaced
Source code in lanim/pil_graphics.py
def with_last_frame(animation: Animation[A], last_frame: A) -> Animation[A]:
"""
Return the same animation but with the very last frame replaced
"""
def projector(t: float):
if t < 1.0:
return animation.projector(t)
else:
return last_frame
return animation.with_projector(projector)
swap(group: Group[PX], index1: int, index2: int, traj1: Trajectory = <function <lambda> at 0x7f5c289b0670>, traj2: Trajectory = <function <lambda> at 0x7f5c289b0670>) -> Animation[Group[PX]]
Swap two items in a group, one being at index index1
and another at index2
.
The resulting animation lasts one second.
By default the objects will move towards each other in a straight line,
but you can customize this behaviour by providing the traj1
and traj2
parameters for the first and second object respectively
Source code in lanim/pil_graphics.py
def swap(
group: Group[PX],
index1: int,
index2: int,
traj1: Trajectory = linear_traj,
traj2: Trajectory = linear_traj
) -> Animation[Group[PX]]:
"""
Swap two items in a group, one being at index `index1` and another at `index2`.
The resulting animation lasts one second.
By default the objects will move towards each other in a straight line,
but you can customize this behaviour by providing the `traj1` and `traj2`
parameters for the first and second object respectively
"""
items = list(group.items)
items[index1], items[index2] = (
moved_to(items[index2], items[index1].x, items[index1].y),
moved_to(items[index1], items[index2].x, items[index2].y),
)
final_group = Group(items)
proj1 = proj_t(group.items[index1], group.items[index2].x, group.items[index2].y, traj1)
proj2 = proj_t(group.items[index2], group.items[index1].x, group.items[index1].y, traj2)
def projector(t: float) -> Group[PX]:
items = list(group.items)
items[index1] = proj1(t)
items[index2] = proj2(t)
return Group(items)
return with_last_frame(Animation(1.0, projector), final_group)
m_just(p: PSX) -> Maybe[PSX]
Source code in lanim/pil_graphics.py
def m_just(p: PSX) -> Maybe[PSX]:
return Sum(("p", p), disappear_into_nil)
m_none(x: float, y: float) -> Maybe[PSX]
Source code in lanim/pil_graphics.py
def m_none(x: float, y: float) -> Maybe[PSX]:
return Sum(("q", Nil(x, y)), disappear_into_nil)
appear(p: PSX) -> Animation[PSX]
Source code in lanim/pil_graphics.py
def appear(p: PSX) -> Animation[PSX]:
return morph_into(p.scaled(0.0), p)
disappear(p: PSX) -> Animation[PSX]
Source code in lanim/pil_graphics.py
def disappear(p: PSX) -> Animation[PSX]:
return appear(p).ease(easings.invert)
appear_from(p: PSX, x: float, y: float) -> Animation[PSX]
Source code in lanim/pil_graphics.py
def appear_from(p: PSX, x: float, y: float) -> Animation[PSX]:
return morph_into(p.scaled_about(0.0, x, y), p)
disappear_from(p: PSX, x: float, y: float) -> Animation[PSX]
Source code in lanim/pil_graphics.py
def disappear_from(p: PSX, x: float, y: float) -> Animation[PSX]:
return appear_from(p, x, y).ease(easings.invert)
Imperative API
scene_any(easing: easings.Easing = <function <lambda> at 0x7f5c28bf4430>, duration: float = 1.0)
Decorator used for making an imperative-feeling scene.
The decorated function should accept a single argument — a function — which it can call with an animation. That function will return the last frame of the animation and add it to an internal animation list, which will be rendered using lanim.core.seq_a.
The decorated function will be replaced with the generated animation.
Source code in lanim/pil_graphics.py
def scene_any(easing: easings.Easing = easings.linear, duration: float = 1.0):
"""
Decorator used for making an imperative-feeling scene.
The decorated function should accept a single argument --- a function ---
which it can call with an animation. That function will return the last
frame of the animation and add it to an internal animation list, which
will be rendered using [lanim.core.seq_a][].
The decorated function will be replaced with the generated animation.
"""
def _(f: Callable[[ThenAny], Any]) -> Animation[PilRenderable]:
animations: list[Animation[PilRenderable]] = []
def on_animate(a: Animation[Q]) -> Q:
animations.append(a.ease(easing) * duration)
return a.projector(1.0)
f(on_animate)
return seq_a(*animations)
return _
scene
scene
is just an alias for scene_any
but with
a different type signature.
Trajectories
Trajectory ()
A trajectory is a way of interpolating between two points
__call__(self, x1: float, y1: float, x2: float, y2: float, t: float) -> tuple[float, float]
special
Source code in lanim/pil_graphics.py
def __call__(
self,
x1: float, y1: float,
x2: float, y2: float,
t: float
) -> tuple[float, float]:
...
ease_t(traj: Trajectory, easing: easings.Easing) -> Trajectory
Apply an easing to a trajectory
Source code in lanim/pil_graphics.py
def ease_t(traj: Trajectory, easing: easings.Easing) -> Trajectory:
"""
Apply an easing to a trajectory
"""
return lambda x1, y1, x2, y2, t: traj(x1, y1, x2, y2, easing(t))
move_t(obj: PX, dest_x: float, dest_y: float, traj: Trajectory) -> Animation[PX]
Create a second-long animation of an object moving along a trajectory
from dest_x
to dest_y
Source code in lanim/pil_graphics.py
def move_t(obj: PX, dest_x: float, dest_y: float, traj: Trajectory) -> Animation[PX]:
"""
Create a second-long animation of an object moving along a trajectory
from `dest_x` to `dest_y`
"""
return Animation(1.0, proj_t(obj, dest_x, dest_y, traj))
proj_t(obj: PX, dest_x: float, dest_y: float, traj: Trajectory) -> Projector[PX]
Source code in lanim/pil_graphics.py
def proj_t(obj: PX, dest_x: float, dest_y: float, traj: Trajectory) -> Projector[PX]:
def projector(t: float) -> PX:
x, y = traj(obj.x, obj.y, dest_x, dest_y, t)
return obj.moved(x - obj.x, y - obj.y)
return projector
linear_traj(x1, y1, x2, y2, t)
Source code in lanim/pil_graphics.py
lambda x1, y1, x2, y2, t: (
x1 * (1 - t) + x2 * t,
y1 * (1 - t) + y2 * t,
)
make_arc_traj(distancing_function: Callable[[float], float]) -> Trajectory
Make an arc trajectory using a distancing function.
^^^^^^^^^^^^ _
^^^ ^^^ | distancing_function(t)
^ ^ _
A--------------------B
(x1,y1) (x2,y2)
t=0 t=0.5 t=1
Source code in lanim/pil_graphics.py
def make_arc_traj(distancing_function: Callable[[float], float]) -> Trajectory:
"""
Make an arc trajectory using a distancing function.
```
^^^^^^^^^^^^ _
^^^ ^^^ | distancing_function(t)
^ ^ _
A--------------------B
(x1,y1) (x2,y2)
t=0 t=0.5 t=1
```
"""
def _traj(x1: float, y1: float, x2: float, y2: float, t: float) -> tuple[float, float]:
nx, ny = normal(x1, y1, x2, y2)
lx, ly = linear_traj(x1, y1, x2, y2, t)
arm = distancing_function(t) * math.sqrt((x2 - x1)**2 + (y2 - y1)**2) / 2
mx, my = lx + arm*nx, ly + arm*ny
return (mx, my)
return _traj
halfcircle_traj(x1: float, y1: float, x2: float, y2: float, t: float) -> tuple[float, float]
Source code in lanim/pil_graphics.py
def _traj(x1: float, y1: float, x2: float, y2: float, t: float) -> tuple[float, float]:
nx, ny = normal(x1, y1, x2, y2)
lx, ly = linear_traj(x1, y1, x2, y2, t)
arm = distancing_function(t) * math.sqrt((x2 - x1)**2 + (y2 - y1)**2) / 2
mx, my = lx + arm*nx, ly + arm*ny
return (mx, my)
low_arc_traj(x1: float, y1: float, x2: float, y2: float, t: float) -> tuple[float, float]
Source code in lanim/pil_graphics.py
def _traj(x1: float, y1: float, x2: float, y2: float, t: float) -> tuple[float, float]:
nx, ny = normal(x1, y1, x2, y2)
lx, ly = linear_traj(x1, y1, x2, y2, t)
arm = distancing_function(t) * math.sqrt((x2 - x1)**2 + (y2 - y1)**2) / 2
mx, my = lx + arm*nx, ly + arm*ny
return (mx, my)
lift_traj(height: float) -> Trajectory
Lift the trajectory up by height
units
Source code in lanim/pil_graphics.py
def lift_traj(height: float) -> Trajectory:
"""
Lift the trajectory up by `height` units
"""
def traj(x1: float, y1: float, x2: float, y2: float, t: float):
if t < 0.25:
k = t * 4
return (x1, y1 - height * k)
elif t < 0.75:
k = 2 * (t - 0.25)
return (x1 * (1 - k) + x2 * k, y1 - height)
else:
k = 4 * (t - 0.75)
return (x2, (y1 - height) * (1 - k) + y2 * k)
return traj
Protocols
PilRenderable ()
moved(self: A, dx: float, dy: float) -> A
Source code in lanim/pil_types.py
def moved(self: A, dx: float, dy: float) -> A: ...
render_pil(self, ctx: PilContext) -> None
Source code in lanim/pil_types.py
def render_pil(self, ctx: PilContext) -> None: ...
Scalable ()
scaled(self: A, factor: float) -> A
Source code in lanim/pil_types.py
def scaled(self: A, factor: float) -> A: ...
scaled_about(self: A, factor: float, cx: float, cy: float) -> A
Source code in lanim/pil_types.py
def scaled_about(self: A, factor: float, cx: float, cy: float) -> A: ...
Morphable ()
morphed(self: A, other: A, t: float) -> A
Source code in lanim/pil_types.py
def morphed(self: A, other: A, t: float) -> A: ...
Alignable ()
aligned(self: A, new_align: Align) -> A
Source code in lanim/pil_types.py
def aligned(self: A, new_align: Align) -> A: ...
Utility data structures
These objects will be useful if you’re implementing your own primitives for rendering with PIL
Align ()
dataclass
Specification of the horizontal and vertical alignment
Predefined class variables:
LU----CU----RU
| |
| |
LC CC RC
| |
| |
LF----VF----RD
dx: float
dataclass-field
Proporton by which to move a LU-aligned object to the right
dy: float
dataclass-field
Proporton by which to move a LU-aligned object to the bottom
blend(self, other: Align, t: float) -> Align
Source code in lanim/pil_types.py
def blend(self, other: Align, t: float) -> Align:
return Align(self.dx * (1 - t) + other.dx * t, self.dy * (1 - t) + other.dy * t)
apply(self, x: float, y: float, width: float, height: float) -> tuple[float, float]
Compute the new bottom-left position of a (x, y, width, height)
rectangle
after applying this alignemnt
Source code in lanim/pil_types.py
def apply(self, x: float, y: float, width: float, height: float) -> tuple[float, float]:
"""
Compute the new bottom-left position of a `(x, y, width, height)` rectangle
after applying this alignemnt
"""
return x + self.dx * width, y + self.dy * height
Style ()
dataclass
Settings regarding the drawing of a single element
fill: Union[str, int]
dataclass-field
outline: Union[str, int]
dataclass-field
line_width: int
dataclass-field
PilContext ()
dataclass
Context object holding the current state of a frame
settings: PilSettings
dataclass-field
img: Image
dataclass-field
draw: ImageDraw
dataclass-field
coord(self, x: float, y: float) -> tuple[int, int]
Source code in lanim/pil_types.py
def coord(self, x: float, y: float) -> tuple[int, int]:
pixels_x = self.settings.center_x + self.settings.unit * x
pixels_y = self.settings.center_y + self.settings.unit * y
return round(pixels_x), round(pixels_y)
rectangle_wh(self, cx: float, cy: float, width: float, height: float, style: Style)
Source code in lanim/pil_types.py
def rectangle_wh(self, cx: float, cy: float, width: float, height: float, style: Style):
self.rectangle(cx - width/2, cy - height/2, cx + width/2, cy + height/2, style)
rectangle(self, x1: float, y1: float, x2: float, y2: float, style: Style)
Source code in lanim/pil_types.py
def rectangle(self, x1: float, y1: float, x2: float, y2: float, style: Style):
self.draw.rectangle(
(self.coord(x1, y1), self.coord(x2, y2) ),
fill=style.fill,
outline=style.outline,
width=style.line_width,
)
line(self, x1: float, y1: float, x2: float, y2: float, style: Style)
Source code in lanim/pil_types.py
def line(self, x1: float, y1: float, x2: float, y2: float, style: Style):
self.draw.line(
[*self.coord(x1, y1), *self.coord(x2, y2)],
fill=style.outline,
width=style.line_width,
)
triangle(self, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, style: Style)
Source code in lanim/pil_types.py
def triangle(self, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, style: Style):
self.draw.line(
[
*self.coord(x1, y1),
*self.coord(x2, y2),
*self.coord(x3, y3),
*self.coord(x1, y1),
*self.coord((x1+x2)/2, (y1+y2)/2),
],
fill=style.outline,
width=style.line_width,
joint="curve"
)