Skip to content

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"
    )