Skip to content

Core Objects

keyed.base

Base classes for drawable stuff.

Lifetime

Represents the lifespan of a drawable object in an animation.

An object will only be drawn if the current frame is within the specified start and end frames.

Parameters:

Name Type Description Default
start float | None

The start frame of the lifetime. Defaults to negative infinity if not provided.

None
end float | None

The end frame of the lifetime. Defaults to positive infinity if not provided.

None

Attributes:

Name Type Description
start

The starting frame of the object's lifetime.

end

The ending frame of the object's lifetime.

Source code in src/keyed/base.py
class Lifetime:
    """Represents the lifespan of a drawable object in an animation.

    An object will only be drawn if the current frame is within the specified start and end frames.

    Args:
        start: The start frame of the lifetime. Defaults to negative infinity if not provided.
        end: The end frame of the lifetime. Defaults to positive infinity if not provided.

    Attributes:
        start: The starting frame of the object's lifetime.
        end: The ending frame of the object's lifetime.
    """

    def __init__(self, start: float | None = None, end: float | None = None):
        self.start = start if start else -float("inf")
        self.end = end if end else float("inf")

    def __contains__(self, frame: int) -> bool:
        """Check if a specific frame is within the lifetime of the object.

        Args:
            frame: The frame number to check.

        Returns:
            True if the frame is within the lifetime, False otherwise.
        """
        return (self.start <= frame) and (self.end >= frame)

Base

Bases: Transformable

Base class for drawable objects in an animation scene.

Attributes:

Name Type Description
scene Scene

The scene to which the object belongs.

lifetime Lifetime

The lifetime of the object.

Source code in src/keyed/base.py
class Base(Transformable):
    """Base class for drawable objects in an animation scene.

    Attributes:
        scene: The scene to which the object belongs.
        lifetime: The lifetime of the object.
    """

    scene: Scene
    lifetime: Lifetime

    def __init__(self, scene: Scene) -> None:
        Transformable.__init__(self, scene.frame)
        self.lifetime = Lifetime()

    @property
    def dependencies(self) -> list[Variable]:
        return self._dependencies

    def draw(self) -> None:
        """Draw the object on the scene."""
        pass

    def _animate(self, property: str, animation: Animation) -> Self:
        """Apply an animation to a property of the shape.

        Note:
            You probably want to directly create a reactive value and provide it
            as an argument for your object rather than using this.

            This is a vestigial method that still exists almost entirely for
            implementing the [Group.write_on][keyed.group.Group.write_on] method.

            It may be removed in the future.

        Args:
            property: The name of the property to animate.
            animation: The animation object defining how the property changes over time.
        """
        p = getattr(self, property)
        assert isinstance(p, Variable)
        setattr(self, property, animation(p, self.frame))
        return self

    def emphasize(
        self,
        buffer: float = 5,
        radius: float = 0,
        fill_color: tuple[float, float, float] = (1, 1, 1),
        color: tuple[float, float, float] = (1, 1, 1),
        alpha: float = 1,
        dash: tuple[Sequence[float], float] | None = None,
        line_width: float = 2,
        draw_fill: bool = True,
        draw_stroke: bool = True,
        operator: cairo.Operator = cairo.OPERATOR_SCREEN,
    ) -> Rectangle:
        """Emphasize the object by drawing a rectangle around it.

        Args:
            buffer: The buffer distance around the object's geometry for the emphasis.
            radius: The corner radius of the emphasized area.
            fill_color: The fill color of the emphasis as an RGB tuple.
            color: The stroke color of the emphasis as an RGB tuple.
            alpha: The alpha transparency of the emphasis.
            dash: The dash pattern for the emphasis outline. Default is solid line.
            line_width: The line width of the emphasis outline.
            draw_fill: Whether to draw the fill of the emphasis.
            draw_stroke: Whether to draw the stroke of the emphasis.
            operator: The compositing operator to use for drawing the emphasis.

        Returns:
            A Rectangle object representing the emphasized area around the original object.

        Notes:
            This creates a Rectangle instance and sets up dynamic expressions to follow the
            geometry of the object as it changes through different frames, applying the specified
            emphasis effects. Emphasis should generally be applied after all animations on the
            original object have been added.
        """
        # TODO: Consider renaming "buffer" to margin.
        from .shapes import Rectangle

        r = Rectangle(
            self.scene,
            color=color,
            x=self.center_x,
            y=self.center_y,
            width=self.width + buffer,
            height=self.height + buffer,
            fill_color=fill_color,
            alpha=alpha,
            dash=dash,
            operator=operator,
            line_width=line_width,
            draw_fill=draw_fill,
            draw_stroke=draw_stroke,
            radius=radius,
        )
        return r

    def set(self, property: str, value: Any, frame: int = ALWAYS) -> Self:
        """Set a property of the object at a specific frame.

        Args:
            property: The name of the property to set.
            value: The new value for the property.
            frame: The frame at which the property value should be set.

        Returns:
            Self

        See Also:
            [keyed.Base.set_literal][keyed.Base.set_literal]
        """
        prop = getattr(self, property)
        new = step(value, frame)(prop, self.frame)
        if isinstance(prop, Variable):
            for p in prop._observers:
                if isinstance(p, Variable):
                    p.observe(new)
                # new.subscribe(p)  # TODO: Using subscribe directly causes color interpolation test to have infinite recursion?

        setattr(self, property, new)
        return self

    def set_literal(self, property: str, value: Any) -> Self:
        """Overwrite a property with a literal value.

        Args:
            property: The name of the property to set.
            value: The new value for the property.

        Returns:
            Self

        See Also:
            [keyed.Base.set][keyed.Base.set]
        """
        setattr(self, property, value)
        return self

    def center(self, frame: int = ALWAYS) -> Self:
        """Center the object within the scene.

        Args:
            frame: The frame at which to center the object.

        Returns:
            self
        """
        self.align_to(self.scene, start=frame, end=frame, direction=ORIGIN, center_on_zero=True)
        return self

    def cleanup(self) -> None:
        return None

    def fade(self, value: HasValue[float], start: int, end: int, ease: EasingFunctionT = linear_in_out) -> Self:
        """Control the object's alpha parameter to fade it in or out.

        Args:
            value: Value to set alpha to.
            start: Frame to start changing alpha.
            end: Frame to finish changing alpha.
            ease: Easing function

        Returns:
            Self
        """
        assert hasattr(self, "alpha")
        self.alpha = Animation(start, end, self.alpha, value, ease=ease)(self.alpha, self.frame)  # type: ignore[assignment]
        return self

    def line_to(
        self, other: Base, self_direction: Direction = RIGHT, other_direction: Direction = LEFT, **line_kwargs: Any
    ) -> "Line":
        """Create a line connecting this object to another object.

        Args:
            other: The target object to connect to
            self_direction: Direction for the connection point on this object
            other_direction: Direction for the connection point on the target object
            **line_kwargs: Additional arguments to pass to the [Line][keyed.line.Line] constructor.

        Returns:
            The created Line object
        """
        from .line import Line

        self_point = get_critical_point(self.geom, direction=self_direction)
        other_point = get_critical_point(other.geom, direction=other_direction)
        return Line(self.scene, x0=self_point[0], y0=self_point[1], x1=other_point[0], y1=other_point[1], **line_kwargs)

geom property

geom

Return a reactive value of the transformed geometry.

draw

draw()

Draw the object on the scene.

Source code in src/keyed/base.py
def draw(self) -> None:
    """Draw the object on the scene."""
    pass

emphasize

emphasize(buffer=5, radius=0, fill_color=(1, 1, 1), color=(1, 1, 1), alpha=1, dash=None, line_width=2, draw_fill=True, draw_stroke=True, operator=OPERATOR_SCREEN)

Emphasize the object by drawing a rectangle around it.

Parameters:

Name Type Description Default
buffer float

The buffer distance around the object's geometry for the emphasis.

5
radius float

The corner radius of the emphasized area.

0
fill_color tuple[float, float, float]

The fill color of the emphasis as an RGB tuple.

(1, 1, 1)
color tuple[float, float, float]

The stroke color of the emphasis as an RGB tuple.

(1, 1, 1)
alpha float

The alpha transparency of the emphasis.

1
dash tuple[Sequence[float], float] | None

The dash pattern for the emphasis outline. Default is solid line.

None
line_width float

The line width of the emphasis outline.

2
draw_fill bool

Whether to draw the fill of the emphasis.

True
draw_stroke bool

Whether to draw the stroke of the emphasis.

True
operator Operator

The compositing operator to use for drawing the emphasis.

OPERATOR_SCREEN

Returns:

Type Description
Rectangle

A Rectangle object representing the emphasized area around the original object.

Notes

This creates a Rectangle instance and sets up dynamic expressions to follow the geometry of the object as it changes through different frames, applying the specified emphasis effects. Emphasis should generally be applied after all animations on the original object have been added.

Source code in src/keyed/base.py
def emphasize(
    self,
    buffer: float = 5,
    radius: float = 0,
    fill_color: tuple[float, float, float] = (1, 1, 1),
    color: tuple[float, float, float] = (1, 1, 1),
    alpha: float = 1,
    dash: tuple[Sequence[float], float] | None = None,
    line_width: float = 2,
    draw_fill: bool = True,
    draw_stroke: bool = True,
    operator: cairo.Operator = cairo.OPERATOR_SCREEN,
) -> Rectangle:
    """Emphasize the object by drawing a rectangle around it.

    Args:
        buffer: The buffer distance around the object's geometry for the emphasis.
        radius: The corner radius of the emphasized area.
        fill_color: The fill color of the emphasis as an RGB tuple.
        color: The stroke color of the emphasis as an RGB tuple.
        alpha: The alpha transparency of the emphasis.
        dash: The dash pattern for the emphasis outline. Default is solid line.
        line_width: The line width of the emphasis outline.
        draw_fill: Whether to draw the fill of the emphasis.
        draw_stroke: Whether to draw the stroke of the emphasis.
        operator: The compositing operator to use for drawing the emphasis.

    Returns:
        A Rectangle object representing the emphasized area around the original object.

    Notes:
        This creates a Rectangle instance and sets up dynamic expressions to follow the
        geometry of the object as it changes through different frames, applying the specified
        emphasis effects. Emphasis should generally be applied after all animations on the
        original object have been added.
    """
    # TODO: Consider renaming "buffer" to margin.
    from .shapes import Rectangle

    r = Rectangle(
        self.scene,
        color=color,
        x=self.center_x,
        y=self.center_y,
        width=self.width + buffer,
        height=self.height + buffer,
        fill_color=fill_color,
        alpha=alpha,
        dash=dash,
        operator=operator,
        line_width=line_width,
        draw_fill=draw_fill,
        draw_stroke=draw_stroke,
        radius=radius,
    )
    return r

set

set(property, value, frame=ALWAYS)

Set a property of the object at a specific frame.

Parameters:

Name Type Description Default
property str

The name of the property to set.

required
value Any

The new value for the property.

required
frame int

The frame at which the property value should be set.

ALWAYS

Returns:

Type Description
Self

Self

See Also

keyed.Base.set_literal

Source code in src/keyed/base.py
def set(self, property: str, value: Any, frame: int = ALWAYS) -> Self:
    """Set a property of the object at a specific frame.

    Args:
        property: The name of the property to set.
        value: The new value for the property.
        frame: The frame at which the property value should be set.

    Returns:
        Self

    See Also:
        [keyed.Base.set_literal][keyed.Base.set_literal]
    """
    prop = getattr(self, property)
    new = step(value, frame)(prop, self.frame)
    if isinstance(prop, Variable):
        for p in prop._observers:
            if isinstance(p, Variable):
                p.observe(new)
            # new.subscribe(p)  # TODO: Using subscribe directly causes color interpolation test to have infinite recursion?

    setattr(self, property, new)
    return self

set_literal

set_literal(property, value)

Overwrite a property with a literal value.

Parameters:

Name Type Description Default
property str

The name of the property to set.

required
value Any

The new value for the property.

required

Returns:

Type Description
Self

Self

See Also

keyed.Base.set

Source code in src/keyed/base.py
def set_literal(self, property: str, value: Any) -> Self:
    """Overwrite a property with a literal value.

    Args:
        property: The name of the property to set.
        value: The new value for the property.

    Returns:
        Self

    See Also:
        [keyed.Base.set][keyed.Base.set]
    """
    setattr(self, property, value)
    return self

center

center(frame=ALWAYS)

Center the object within the scene.

Parameters:

Name Type Description Default
frame int

The frame at which to center the object.

ALWAYS

Returns:

Type Description
Self

self

Source code in src/keyed/base.py
def center(self, frame: int = ALWAYS) -> Self:
    """Center the object within the scene.

    Args:
        frame: The frame at which to center the object.

    Returns:
        self
    """
    self.align_to(self.scene, start=frame, end=frame, direction=ORIGIN, center_on_zero=True)
    return self

fade

fade(value, start, end, ease=linear_in_out)

Control the object's alpha parameter to fade it in or out.

Parameters:

Name Type Description Default
value HasValue[float]

Value to set alpha to.

required
start int

Frame to start changing alpha.

required
end int

Frame to finish changing alpha.

required
ease EasingFunctionT

Easing function

linear_in_out

Returns:

Type Description
Self

Self

Source code in src/keyed/base.py
def fade(self, value: HasValue[float], start: int, end: int, ease: EasingFunctionT = linear_in_out) -> Self:
    """Control the object's alpha parameter to fade it in or out.

    Args:
        value: Value to set alpha to.
        start: Frame to start changing alpha.
        end: Frame to finish changing alpha.
        ease: Easing function

    Returns:
        Self
    """
    assert hasattr(self, "alpha")
    self.alpha = Animation(start, end, self.alpha, value, ease=ease)(self.alpha, self.frame)  # type: ignore[assignment]
    return self

line_to

line_to(other, self_direction=RIGHT, other_direction=LEFT, **line_kwargs)

Create a line connecting this object to another object.

Parameters:

Name Type Description Default
other Base

The target object to connect to

required
self_direction Direction

Direction for the connection point on this object

RIGHT
other_direction Direction

Direction for the connection point on the target object

LEFT
**line_kwargs Any

Additional arguments to pass to the Line constructor.

{}

Returns:

Type Description
'Line'

The created Line object

Source code in src/keyed/base.py
def line_to(
    self, other: Base, self_direction: Direction = RIGHT, other_direction: Direction = LEFT, **line_kwargs: Any
) -> "Line":
    """Create a line connecting this object to another object.

    Args:
        other: The target object to connect to
        self_direction: Direction for the connection point on this object
        other_direction: Direction for the connection point on the target object
        **line_kwargs: Additional arguments to pass to the [Line][keyed.line.Line] constructor.

    Returns:
        The created Line object
    """
    from .line import Line

    self_point = get_critical_point(self.geom, direction=self_direction)
    other_point = get_critical_point(other.geom, direction=other_direction)
    return Line(self.scene, x0=self_point[0], y0=self_point[1], x1=other_point[0], y1=other_point[1], **line_kwargs)

rotate

rotate(amount, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, center=None, direction=ORIGIN)

Rotate the object.

Parameters:

Name Type Description Default
amount HasValue[float]

Amount to rotate by.

required
start int

The frame to start rotating.

ALWAYS
end int

The frame to end rotating.

ALWAYS
easing EasingFunctionT

The easing function to use.

cubic_in_out
center ReactiveValue[GeometryT] | None

The object around which to rotate.

None
direction Direction

The relative critical point of the center.

ORIGIN

Returns:

Type Description
Self

self

Source code in src/keyed/transforms.py
def rotate(
    self,
    amount: HasValue[float],
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    center: ReactiveValue[GeometryT] | None = None,
    direction: Direction = ORIGIN,
) -> Self:
    """Rotate the object.

    Args:
        amount: Amount to rotate by.
        start: The frame to start rotating.
        end: The frame to end rotating.
        easing: The easing function to use.
        center: The object around which to rotate.
        direction: The relative critical point of the center.

    Returns:
        self
    """
    center = center if center is not None else self.geom
    cx, cy = get_critical_point(center, direction)
    return self.apply_transform(rotate(start, end, amount, cx, cy, self.frame, easing))

scale

scale(amount, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, center=None, direction=ORIGIN)

Scale the object.

Parameters:

Name Type Description Default
amount HasValue[float]

Amount to scale by.

required
start int

The frame to start scaling.

ALWAYS
end int

The frame to end scaling.

ALWAYS
easing EasingFunctionT

The easing function to use.

cubic_in_out
center ReactiveValue[GeometryT] | None

The object around which to rotate.

None
direction Direction

The relative critical point of the center.

ORIGIN

Returns:

Type Description
Self

self

Source code in src/keyed/transforms.py
def scale(
    self,
    amount: HasValue[float],
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    center: ReactiveValue[GeometryT] | None = None,
    direction: Direction = ORIGIN,
) -> Self:
    """Scale the object.

    Args:
        amount: Amount to scale by.
        start: The frame to start scaling.
        end: The frame to end scaling.
        easing: The easing function to use.
        center: The object around which to rotate.
        direction: The relative critical point of the center.

    Returns:
        self
    """
    center = center if center is not None else self.geom
    cx, cy = get_critical_point(center, direction)
    return self.apply_transform(scale(start, end, amount, cx, cy, self.frame, easing))

translate

translate(x=0, y=0, start=ALWAYS, end=ALWAYS, easing=cubic_in_out)

Translate the object.

Parameters:

Name Type Description Default
x HasValue[float]

x offset.

0
y HasValue[float]

y offset.

0
start int

The frame to start translating.

ALWAYS
end int

The frame to end translating.

ALWAYS
easing EasingFunctionT

The easing function to use.

cubic_in_out
Source code in src/keyed/transforms.py
def translate(
    self,
    x: HasValue[float] = 0,
    y: HasValue[float] = 0,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
) -> Self:
    """Translate the object.

    Args:
        x: x offset.
        y: y offset.
        start: The frame to start translating.
        end: The frame to end translating.
        easing: The easing function to use.
    """
    return self.apply_transform(translate(start, end, x, y, self.frame, easing))

move_to

move_to(x=None, y=None, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, center=None, direction=ORIGIN)

Move object to absolute coordinates.

Parameters:

Name Type Description Default
x HasValue[float] | None

Destination x coordinate

None
y HasValue[float] | None

Destination y coordinate

None
start int

Starting frame, by default ALWAYS

ALWAYS
end int

Ending frame, by default ALWAYS

ALWAYS
easing EasingFunctionT

Easing function, by default cubic_in_out

cubic_in_out

Returns:

Type Description
Self

Self

Source code in src/keyed/transforms.py
def move_to(
    self,
    x: HasValue[float] | None = None,
    y: HasValue[float] | None = None,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    center: ReactiveValue[GeometryT] | None = None,
    direction: Direction = ORIGIN,
) -> Self:
    """Move object to absolute coordinates.

    Args:
        x: Destination x coordinate
        y: Destination y coordinate
        start: Starting frame, by default ALWAYS
        end: Ending frame, by default ALWAYS
        easing: Easing function, by default cubic_in_out

    Returns:
        Self
    """
    center = center if center is not None else self.geom
    cx, cy = get_critical_point(center, direction)
    self.apply_transform(move_to(start=start, end=end, x=x, y=y, cx=cx, cy=cy, frame=self.frame, easing=easing))
    return self

align_to

align_to(to, start=ALWAYS, lock=None, end=ALWAYS, from_=None, easing=cubic_in_out, direction=ORIGIN, center_on_zero=False)

Align the object to another object.

Parameters:

Name Type Description Default
to Transformable

The object to align to.

required
start int

Start of animation (begin aligning to the object).

ALWAYS
end int

End of animation (finish aligning to the object at this frame, and then stay there).

ALWAYS
from_ ReactiveValue[GeometryT] | None

Use this object as self when doing the alignment. This is helpful for code animations. It is sometimes desirable to align, say, the top-left edge of one character in a TextSelection to the top-left of another character.

None
easing EasingFunctionT

The easing function to use.

cubic_in_out
direction Direction

The critical point of to and from_to use for the alignment.

ORIGIN
center_on_zero bool

If true, align along the "0"-valued dimensions. Otherwise, only align to on non-zero directions. This is beneficial for, say, centering the object at the origin (which has a vector that consists of two zeros).

False

Returns:

Type Description
Self

self

Source code in src/keyed/transforms.py
def align_to(
    self,
    to: Transformable,
    start: int = ALWAYS,
    lock: int | None = None,
    end: int = ALWAYS,
    from_: ReactiveValue[GeometryT] | None = None,
    easing: EasingFunctionT = cubic_in_out,
    direction: Direction = ORIGIN,
    center_on_zero: bool = False,
) -> Self:
    """Align the object to another object.

    Args:
        to: The object to align to.
        start: Start of animation (begin aligning to the object).
        end: End of animation (finish aligning to the object at this frame, and then stay there).
        from_: Use this object as self when doing the alignment. This is helpful for code
            animations. It is sometimes desirable to align, say, the top-left edge of one
            character in a TextSelection to the top-left of another character.
        easing: The easing function to use.
        direction: The critical point of to and from_to use for the alignment.
        center_on_zero: If true, align along the "0"-valued dimensions. Otherwise, only align to on non-zero
            directions. This is beneficial for, say, centering the object at the origin (which has
            a vector that consists of two zeros).

    Returns:
        self
    """
    # TODO: I'd like to get rid of center_on_zero.
    from_ = from_ or self.geom
    lock = lock if lock is not None else end
    return self.apply_transform(
        align_to(
            to.geom,
            from_,
            frame=self.frame,
            start=start,
            lock=lock,
            end=end,
            ease=easing,
            direction=direction,
            center_on_zero=center_on_zero,
        )
    )

lock_on

lock_on(target, reference=None, start=ALWAYS, end=-ALWAYS, direction=ORIGIN, x=True, y=True)

Lock on to a target.

Parameters:

Name Type Description Default
target Transformable

Object to lock onto

required
reference ReactiveValue[GeometryT] | None

Measure from this object. This is useful for TextSelections, where you want to align to a particular character in the selection.

None
start int

When to start locking on.

ALWAYS
end int

When to end locking on.

-ALWAYS
x bool

If true, lock on in the x dimension.

True
y bool

If true, lock on in the y dimension.

True
Source code in src/keyed/transforms.py
def lock_on(
    self,
    target: Transformable,
    reference: ReactiveValue[GeometryT] | None = None,
    start: int = ALWAYS,
    end: int = -ALWAYS,
    direction: Direction = ORIGIN,
    x: bool = True,
    y: bool = True,
) -> Self:
    """Lock on to a target.

    Args:
        target: Object to lock onto
        reference: Measure from this object. This is useful for TextSelections, where you want to align
            to a particular character in the selection.
        start: When to start locking on.
        end: When to end locking on.
        x: If true, lock on in the x dimension.
        y: If true, lock on in the y dimension.
    """
    reference = reference or self.geom
    return self.apply_transform(
        lock_on(
            target=target.geom,
            reference=reference,
            frame=self.frame,
            start=start,
            end=end,
            direction=direction,
            x=x,
            y=y,
        )
    )

lock_on2

lock_on2(target, reference=None, direction=ORIGIN, x=True, y=True)

Lock on to a target.

Parameters:

Name Type Description Default
target Transformable

Object to lock onto

required
reference ReactiveValue[GeometryT] | None

Measure from this object. This is useful for TextSelections, where you want to align to a particular character in the selection.

None
x bool

If true, lock on in the x dimension.

True
y bool

if true, lock on in the y dimension.

True
Source code in src/keyed/transforms.py
def lock_on2(
    self,
    target: Transformable,
    reference: ReactiveValue[GeometryT] | None = None,
    direction: Direction = ORIGIN,
    x: bool = True,
    y: bool = True,
) -> Self:
    """Lock on to a target.

    Args:
        target: Object to lock onto
        reference: Measure from this object. This is useful for TextSelections, where you want to align
            to a particular character in the selection.
        x: If true, lock on in the x dimension.
        y: if true, lock on in the y dimension.
    """
    reference = reference or self.geom
    return self.apply_transform(
        align_now(
            target=target.geom,
            reference=reference,
            direction=direction,
            x=x,
            y=y,
        )
    )

shear

shear(angle_x=0, angle_y=0, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, center=None)

Shear the object.

Parameters:

Name Type Description Default
angle_x HasValue[float]

Angle (in degrees) to shear by along x direction.

0
angle_y HasValue[float]

Angle (in degrees) to shear by along x direction.

0
start int

The frame to start scaling.

ALWAYS
end int

The frame to end scaling.

ALWAYS
easing EasingFunctionT

The easing function to use.

cubic_in_out
center ReactiveValue[GeometryT] | None

The object around which to rotate.

None

Returns:

Type Description
Self

self

Source code in src/keyed/transforms.py
def shear(
    self,
    angle_x: HasValue[float] = 0,
    angle_y: HasValue[float] = 0,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    center: ReactiveValue[GeometryT] | None = None,
) -> Self:
    """Shear the object.

    Args:
        angle_x: Angle (in degrees) to shear by along x direction.
        angle_y: Angle (in degrees) to shear by along x direction.
        start: The frame to start scaling.
        end: The frame to end scaling.
        easing: The easing function to use.
        center: The object around which to rotate.

    Returns:
        self
    """
    center = center if center is not None else self.geom
    cx, cy = get_critical_point(center, ORIGIN)
    return self.apply_transform(
        shear(
            start=start,
            end=end,
            angle_x=angle_x,
            angle_y=angle_y,
            cx=cx,
            cy=cy,
            frame=self.frame,
            ease=easing,
        )
    )

stretch

stretch(scale_x=1, scale_y=1, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, center=None, direction=ORIGIN)

Stretch the object.

Parameters:

Name Type Description Default
scale_x HasValue[float]

Amount to scale by in x direction.

1
scale_y HasValue[float]

Amount to scale by in y direction.

1
start int

The frame to start scaling.

ALWAYS
end int

The frame to end scaling.

ALWAYS
easing EasingFunctionT

The easing function to use.

cubic_in_out
center ReactiveValue[GeometryT] | None

The object around which to rotate.

None
direction Direction

The relative critical point of the center.

ORIGIN

Returns:

Type Description
Self

self

Source code in src/keyed/transforms.py
def stretch(
    self,
    scale_x: HasValue[float] = 1,
    scale_y: HasValue[float] = 1,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    center: ReactiveValue[GeometryT] | None = None,
    direction: Direction = ORIGIN,
) -> Self:
    """Stretch the object.

    Args:
        scale_x: Amount to scale by in x direction.
        scale_y: Amount to scale by in y direction.
        start: The frame to start scaling.
        end: The frame to end scaling.
        easing: The easing function to use.
        center: The object around which to rotate.
        direction: The relative critical point of the center.

    Returns:
        self
    """
    center = center if center is not None else self.geom
    cx, cy = get_critical_point(center, direction)
    return self.apply_transform(
        stretch(
            start=start,
            end=end,
            scale_x=scale_x,
            scale_y=scale_y,
            cx=cx,
            cy=cy,
            frame=self.frame,
            ease=easing,
        )
    )

next_to

next_to(to, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, offset=10.0, direction=LEFT)

Align the object to another object.

Parameters:

Name Type Description Default
to Transformable

The object to align to.

required
start int

Start of animation (begin aligning to the object).

ALWAYS
end int

End of animation (finish aligning to the object at this frame, and then stay there).

ALWAYS
easing EasingFunctionT

The easing function to use.

cubic_in_out
offset HasValue[float]

Distance between objects (in pixels).

10.0
direction Direction

The critical point of to and from_to use for the alignment.

LEFT

Returns:

Type Description
Self

self

Source code in src/keyed/transforms.py
def next_to(
    self,
    to: Transformable,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    offset: HasValue[float] = 10.0,
    direction: Direction = LEFT,
) -> Self:
    """Align the object to another object.

    Args:
        to: The object to align to.
        start: Start of animation (begin aligning to the object).
        end: End of animation (finish aligning to the object at this frame, and then stay there).
        easing: The easing function to use.
        offset: Distance between objects (in pixels).
        direction: The critical point of to and from_to use for the alignment.

    Returns:
        self
    """
    self_x, self_y = get_critical_point(self.geom, -1 * direction)
    target_x, target_y = get_critical_point(to.geom, direction)
    matrix = next_to(
        start=start,
        end=end,
        target_x=target_x,
        target_y=target_y,
        self_x=self_x,
        self_y=self_y,
        direction=direction,
        offset=offset,
        ease=easing,
        frame=self.frame,
    )
    return self.apply_transform(matrix)

is_visible

is_visible(obj)

Check if an object is visible.

Parameters:

Name Type Description Default
obj Any

Query object.

required

Returns:

Type Description
bool

True if the object is visible, False otherwise.

Note

Does not consider if an object is within the bounds of the canvas.

Source code in src/keyed/base.py
def is_visible(obj: Any) -> bool:
    """Check if an object is visible.

    Args:
        obj: Query object.

    Returns:
        True if the object is visible, False otherwise.

    Note:
        Does not consider if an object is within the bounds of the canvas.
    """
    return isinstance(obj, HasAlpha) and unref(obj.alpha) > 0

keyed.group

A class for manipulating groups of things.

Selection module-attribute

Selection = Group

Alias of Group.

Group

Bases: Base, list[T]

A sequence of drawable objects, allowing collective transformations and animations.

Parameters:

Name Type Description Default
iterable Iterable[T]

An iterable of drawable objects.

tuple()
Source code in src/keyed/group.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
class Group(Base, list[T]):  # type: ignore[misc]
    """A sequence of drawable objects, allowing collective transformations and animations.

    Args:
        iterable: An iterable of drawable objects.
    """

    def __init__(self, iterable: Iterable[T] = tuple(), /) -> None:
        ## Note: This intentionally reimplements portions of the Base and
        # Transformable __init__ methods.

        # list
        list.__init__(self, iterable)

        # Base
        self.lifetime = Lifetime()
        self._dependencies: list[Variable] = []

        # From Transformable
        self.controls = TransformControls(self)
        self._cache: dict[str, Computed[Any]] = {}

    @property
    def scene(self) -> Scene:  # type: ignore[override]
        """Returns the scene associated with the first object in the group.

        Raises:
            ValueError: If the group is empty and the scene cannot be retrieved.
        """
        if not self:
            raise ValueError("Cannot retrieve 'scene': Selection is empty.")
        return self[0].scene

    @property
    def frame(self) -> Signal[int]:  # type: ignore[override]
        """Returns the frame associated with the first object in the group.

        Raises:
            ValueError: If the group is empty and the frame cannot be retrieved.
        """
        if not self:
            raise ValueError("Cannot retrieve 'frame': Selection is empty.")
        return self.scene.frame

    def _animate(self, property: str, animation: Animation) -> Self:
        """Animate a property across all objects in the group.

        Args:
            property: str
            animation: Animation

        Returns:
            None
        """
        for item in self:
            item._animate(property, animation)
        return self

    def draw(self) -> None:
        """Draws all objects in the group."""
        for item in self:
            item.draw()

    def set(self, property: str, value: Any, frame: int = 0) -> Self:
        """Set a property to a new value for all objects in the group at the specified frame.

        Args:
            property: The name of the property to set.
            value: The value to set it to.
            frame: The frame at which to set the value.

        Returns:
            Self

        See Also:
            [keyed.Group.set_literal][keyed.Group.set_literal]
        """
        for item in self:
            item.set(property, value, frame)
        return self

    def set_literal(self, property: str, value: Any) -> Self:
        """Overwrite a property to a new value for all objects in the group.

        Args:
            property: The name of the property to set.
            value: Value to set to.

        Returns:
            Self

        See Also:
            [keyed.Group.set][keyed.Group.set]
        """
        for item in self:
            item.set_literal(property, value)
        return self

    def write_on(
        self,
        property: str,
        animator: Callable,
        start: int,
        delay: int,
        duration: int,
    ) -> Self:
        """Sequentially animates a property across all objects in the group.

        Args:
            property: The property to animate.
            animator : The animation function to apply, which should create an Animation.
                See :func:`keyed.animations.stagger`.
            start: The frame at which the first animation should start.
            delay: The delay in frames before starting the next object's animation.
            duration: The duration of each object's animation in frames.
        """
        frame = start
        for item in self:
            animation = animator(start=frame, end=frame + duration)
            item._animate(property, animation)
            frame += delay
        return self

    @overload
    def __getitem__(self, key: SupportsIndex) -> T:
        pass

    @overload
    def __getitem__(self, key: slice) -> Self:
        pass

    def __getitem__(self, key: SupportsIndex | slice) -> T | Self:
        """Retrieve an item or slice of items from the group based on the given key."""
        if isinstance(key, slice):
            return type(self)(super().__getitem__(key))
        else:
            return super().__getitem__(key)

    @property
    def _raw_geom_now(self) -> shapely.Polygon:
        """Not really used. Only here to comply with the best class."""
        raise NotImplementedError("Don't call this method on Selections.")

    @property
    def geom(self) -> Computed[shapely.GeometryCollection[shapely.geometry.base.BaseGeometry]]:  # pyright: ignore[reportIncompatibleMethodOverride]
        """Return a reactive value of the geometry.

        Returns:
            A reactive value of the geometry.
        """

        @computed
        def f(geoms: list[shapely.geometry.base.BaseGeometry]) -> shapely.GeometryCollection:
            return shapely.GeometryCollection([unref(geom) for geom in geoms])

        return f([obj.geom for obj in self])

    @property
    def geom_now(self) -> shapely.GeometryCollection:
        return shapely.GeometryCollection([obj.geom_now for obj in self])

    # def __copy__(self) -> Self:
    #     return type(self)(list(self))

    def apply_transform(self, matrix: ReactiveValue[cairo.Matrix]) -> Self:
        # TODO should we allow transform by HasValue[cairo.Matrix]? Probably...
        for obj in self:
            obj.apply_transform(matrix)
        return self

    def translate(
        self,
        x: HasValue[float] = 0,
        y: HasValue[float] = 0,
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
    ) -> Self:
        matrix = translate(start, end, x, y, self.frame, easing)
        self.apply_transform(matrix)
        return self

    def move_to(
        self,
        x: HasValue[float] | None = None,
        y: HasValue[float] | None = None,
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        center: ReactiveValue[GeometryT] | None = None,
        direction: Direction = ORIGIN,
    ) -> Self:
        """Move object to absolute coordinates.

        Args:
            x: Destination x coordinate
            y: Destination y coordinate
            start: Starting frame, by default ALWAYS
            end: Ending frame, by default ALWAYS
            easing: Easing function, by default cubic_in_out

        Returns:
            Self
        """
        center = center if center is not None else self.geom  # type: ignore[assignment]
        cx, cy = get_critical_point(center, direction)  # type: ignore[argument]
        self.apply_transform(move_to(start=start, end=end, x=x, y=y, cx=cx, cy=cy, frame=self.frame, easing=easing))
        return self

    def rotate(
        self,
        amount: HasValue[float],
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        center: ReactiveValue[GeometryT] | None = None,
        direction: Direction = ORIGIN,
    ) -> Self:
        center_ = center if center is not None else self.geom
        cx, cy = get_critical_point(center_, direction)  # type: ignore[argument]
        matrix = rotate(start, end, amount, cx, cy, self.frame, easing)
        self.apply_transform(matrix)
        return self

    def scale(
        self,
        amount: HasValue[float],
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        center: ReactiveValue[GeometryT] | None = None,
        direction: Direction = ORIGIN,
    ) -> Self:
        center_ = center if center is not None else self.geom
        cx, cy = get_critical_point(center_, direction)  # type: ignore[argument]
        matrix = scale(start, end, amount, cx, cy, self.frame, easing)
        self.apply_transform(matrix)
        return self

    def stretch(
        self,
        scale_x: HasValue[float] = 1,
        scale_y: HasValue[float] = 1,
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        center: ReactiveValue[GeometryT] | None = None,
        direction: Direction = ORIGIN,
    ) -> Self:
        center_ = center if center is not None else self.geom
        cx, cy = get_critical_point(center_, direction)  # type: ignore[argument]
        matrix = stretch(start, end, scale_x, scale_y, cx, cy, self.frame, easing)
        self.apply_transform(matrix)
        return self

    def shear(
        self,
        angle_x: HasValue[float] = 0,
        angle_y: HasValue[float] = 0,
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        center: ReactiveValue[GeometryT] | None = None,
    ) -> Self:
        center_ = center if center is not None else self.geom
        cx, cy = get_critical_point(center_, ORIGIN)  # type: ignore[argument]
        matrix = shear(start, end, angle_x, angle_y, cx, cy, self.frame, easing)
        self.apply_transform(matrix)
        return self

    def align_to(
        self,
        to: Transformable,
        start: int = ALWAYS,
        lock: int | None = None,
        end: int = ALWAYS,
        from_: ReactiveValue[GeometryT] | None = None,
        easing: EasingFunctionT = cubic_in_out,
        direction: Direction = ORIGIN,
        center_on_zero: bool = False,
    ) -> Self:
        lock = lock if lock is not None else end
        matrix = align_to(
            to.geom,
            from_ if from_ is not None else self.geom,  # type: ignore[argument]
            frame=self.frame,
            start=start,
            lock=lock,
            end=end,
            ease=easing,
            direction=direction,
            center_on_zero=center_on_zero,
        )
        self.apply_transform(matrix)
        return self

    def lock_on(
        self,
        target: Transformable,
        reference: ReactiveValue[GeometryT] | None = None,
        start: int = ALWAYS,
        end: int = -ALWAYS,
        direction: Direction = ORIGIN,
        x: bool = True,
        y: bool = True,
    ) -> Self:
        matrix = lock_on(
            target=target.geom,
            reference=reference if reference is not None else self.geom,  # type: ignore[argument]
            frame=self.frame,
            start=start,
            end=end,
            direction=direction,
            x=x,
            y=y,
        )
        self.apply_transform(matrix)
        return self

    def match_size(
        self,
        other: Transformable,
        match_x: bool = True,
        match_y: bool = True,
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        center: ReactiveValue[GeometryT] | None = None,
        direction: Direction = ORIGIN,
    ) -> Self:
        center_ = center if center is not None else self.geom
        cx, cy = get_critical_point(center_, direction)  # type: ignore[argument]
        matrix = match_size(
            start=start,
            end=end,
            match_x=match_x,
            match_y=match_y,
            target_width=other.width,
            target_height=other.height,
            original_width=self.width,
            original_height=self.height,
            cx=cx,
            cy=cy,
            frame=self.frame,
            ease=easing,
        )
        self.apply_transform(matrix)
        return self

    def next_to(
        self,
        to: Transformable,
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        offset: HasValue[float] = 10.0,
        direction: Direction = LEFT,
    ) -> Self:
        """Align the object to another object.

        Args:
            to: The object to align to.
            start: Start of animation (begin aligning to the object).
            end: End of animation (finish aligning to the object at this frame, and then stay there).
            easing: The easing function to use.
            offset: Distance between objects (in pixels).
            direction: The critical point of to and from_to use for the alignment.

        Returns:
            self
        """
        self_x, self_y = get_critical_point(self.geom, -1 * direction)  # type: ignore[argument]
        target_x, target_y = get_critical_point(to.geom, direction)
        matrix = next_to(
            start=start,
            end=end,
            target_x=target_x,
            target_y=target_y,
            self_x=self_x,
            self_y=self_y,
            direction=direction,
            offset=offset,
            ease=easing,
            frame=self.frame,
        )
        return self.apply_transform(matrix)

    @property
    def dependencies(self) -> list[Variable]:
        out = []
        for obj in self:
            out.extend(obj.dependencies)
        return out

    def cleanup(self) -> None:
        for obj in self:
            obj.cleanup()

    def fade(self, value: HasValue[float], start: int, end: int, ease: EasingFunctionT = linear_in_out) -> Self:
        for obj in self:
            obj.fade(value, start, end, ease)
        return self

    def distribute(
        self,
        direction: Direction = ORIGIN,
        start: int = ALWAYS,
        end: int = ALWAYS,
        easing: EasingFunctionT = cubic_in_out,
        x: bool = True,
        y: bool = True,
    ) -> Self:
        """Distribute objects evenly between the first and last objects in the group.

        This keeps the first and last objects in their initial positions and distributes
        the remaining objects in between with equal spacing.

        Args:
            direction: Direction used to get anchor points on objects
            start: Starting frame for the animation
            end: Ending frame for the animation
            easing: Easing function to use
            x: Whether to distribute along the x-axis
            y: Whether to distribute along the y-axis

        Returns:
            self
        """
        objects = list(self)
        if len(objects) <= 2:
            # No distribution needed for 0, 1, or 2 objects
            return self

        # Get the first and last objects
        first, *middle, last = objects

        # Get positions of the first and last objects using the specified direction
        first_x, first_y = get_critical_point(first.geom, direction)
        last_x, last_y = get_critical_point(last.geom, -1 * direction)

        # Use these positions as the distribution bounds
        start_x, end_x = first_x, last_x
        start_y, end_y = first_y, last_y

        # Position each middle object
        for i, obj in enumerate(middle, 1):
            # Calculate interpolation factor (fraction of position in the sequence)
            t = i / (len(objects) - 1)

            # Get current position of this object
            obj_x, obj_y = get_critical_point(obj.geom, direction)

            # Calculate target position and translation
            dx = (start_x + t * (end_x - start_x) - obj_x) if x else 0
            dy = (start_y + t * (end_y - start_y) - obj_y) if y else 0

            # Apply transformation
            obj.translate(x=dx, y=dy, start=start, end=end, easing=easing)

        return self

scene property

scene

Returns the scene associated with the first object in the group.

Raises:

Type Description
ValueError

If the group is empty and the scene cannot be retrieved.

frame property

frame

Returns the frame associated with the first object in the group.

Raises:

Type Description
ValueError

If the group is empty and the frame cannot be retrieved.

geom property

geom

Return a reactive value of the geometry.

Returns:

Type Description
Computed[GeometryCollection[BaseGeometry]]

A reactive value of the geometry.

draw

draw()

Draws all objects in the group.

Source code in src/keyed/group.py
def draw(self) -> None:
    """Draws all objects in the group."""
    for item in self:
        item.draw()

set

set(property, value, frame=0)

Set a property to a new value for all objects in the group at the specified frame.

Parameters:

Name Type Description Default
property str

The name of the property to set.

required
value Any

The value to set it to.

required
frame int

The frame at which to set the value.

0

Returns:

Type Description
Self

Self

See Also

keyed.Group.set_literal

Source code in src/keyed/group.py
def set(self, property: str, value: Any, frame: int = 0) -> Self:
    """Set a property to a new value for all objects in the group at the specified frame.

    Args:
        property: The name of the property to set.
        value: The value to set it to.
        frame: The frame at which to set the value.

    Returns:
        Self

    See Also:
        [keyed.Group.set_literal][keyed.Group.set_literal]
    """
    for item in self:
        item.set(property, value, frame)
    return self

set_literal

set_literal(property, value)

Overwrite a property to a new value for all objects in the group.

Parameters:

Name Type Description Default
property str

The name of the property to set.

required
value Any

Value to set to.

required

Returns:

Type Description
Self

Self

See Also

keyed.Group.set

Source code in src/keyed/group.py
def set_literal(self, property: str, value: Any) -> Self:
    """Overwrite a property to a new value for all objects in the group.

    Args:
        property: The name of the property to set.
        value: Value to set to.

    Returns:
        Self

    See Also:
        [keyed.Group.set][keyed.Group.set]
    """
    for item in self:
        item.set_literal(property, value)
    return self

write_on

write_on(property, animator, start, delay, duration)

Sequentially animates a property across all objects in the group.

Parameters:

Name Type Description Default
property str

The property to animate.

required
animator

The animation function to apply, which should create an Animation. See :func:keyed.animations.stagger.

required
start int

The frame at which the first animation should start.

required
delay int

The delay in frames before starting the next object's animation.

required
duration int

The duration of each object's animation in frames.

required
Source code in src/keyed/group.py
def write_on(
    self,
    property: str,
    animator: Callable,
    start: int,
    delay: int,
    duration: int,
) -> Self:
    """Sequentially animates a property across all objects in the group.

    Args:
        property: The property to animate.
        animator : The animation function to apply, which should create an Animation.
            See :func:`keyed.animations.stagger`.
        start: The frame at which the first animation should start.
        delay: The delay in frames before starting the next object's animation.
        duration: The duration of each object's animation in frames.
    """
    frame = start
    for item in self:
        animation = animator(start=frame, end=frame + duration)
        item._animate(property, animation)
        frame += delay
    return self

move_to

move_to(x=None, y=None, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, center=None, direction=ORIGIN)

Move object to absolute coordinates.

Parameters:

Name Type Description Default
x HasValue[float] | None

Destination x coordinate

None
y HasValue[float] | None

Destination y coordinate

None
start int

Starting frame, by default ALWAYS

ALWAYS
end int

Ending frame, by default ALWAYS

ALWAYS
easing EasingFunctionT

Easing function, by default cubic_in_out

cubic_in_out

Returns:

Type Description
Self

Self

Source code in src/keyed/group.py
def move_to(
    self,
    x: HasValue[float] | None = None,
    y: HasValue[float] | None = None,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    center: ReactiveValue[GeometryT] | None = None,
    direction: Direction = ORIGIN,
) -> Self:
    """Move object to absolute coordinates.

    Args:
        x: Destination x coordinate
        y: Destination y coordinate
        start: Starting frame, by default ALWAYS
        end: Ending frame, by default ALWAYS
        easing: Easing function, by default cubic_in_out

    Returns:
        Self
    """
    center = center if center is not None else self.geom  # type: ignore[assignment]
    cx, cy = get_critical_point(center, direction)  # type: ignore[argument]
    self.apply_transform(move_to(start=start, end=end, x=x, y=y, cx=cx, cy=cy, frame=self.frame, easing=easing))
    return self

next_to

next_to(to, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, offset=10.0, direction=LEFT)

Align the object to another object.

Parameters:

Name Type Description Default
to Transformable

The object to align to.

required
start int

Start of animation (begin aligning to the object).

ALWAYS
end int

End of animation (finish aligning to the object at this frame, and then stay there).

ALWAYS
easing EasingFunctionT

The easing function to use.

cubic_in_out
offset HasValue[float]

Distance between objects (in pixels).

10.0
direction Direction

The critical point of to and from_to use for the alignment.

LEFT

Returns:

Type Description
Self

self

Source code in src/keyed/group.py
def next_to(
    self,
    to: Transformable,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    offset: HasValue[float] = 10.0,
    direction: Direction = LEFT,
) -> Self:
    """Align the object to another object.

    Args:
        to: The object to align to.
        start: Start of animation (begin aligning to the object).
        end: End of animation (finish aligning to the object at this frame, and then stay there).
        easing: The easing function to use.
        offset: Distance between objects (in pixels).
        direction: The critical point of to and from_to use for the alignment.

    Returns:
        self
    """
    self_x, self_y = get_critical_point(self.geom, -1 * direction)  # type: ignore[argument]
    target_x, target_y = get_critical_point(to.geom, direction)
    matrix = next_to(
        start=start,
        end=end,
        target_x=target_x,
        target_y=target_y,
        self_x=self_x,
        self_y=self_y,
        direction=direction,
        offset=offset,
        ease=easing,
        frame=self.frame,
    )
    return self.apply_transform(matrix)

distribute

distribute(direction=ORIGIN, start=ALWAYS, end=ALWAYS, easing=cubic_in_out, x=True, y=True)

Distribute objects evenly between the first and last objects in the group.

This keeps the first and last objects in their initial positions and distributes the remaining objects in between with equal spacing.

Parameters:

Name Type Description Default
direction Direction

Direction used to get anchor points on objects

ORIGIN
start int

Starting frame for the animation

ALWAYS
end int

Ending frame for the animation

ALWAYS
easing EasingFunctionT

Easing function to use

cubic_in_out
x bool

Whether to distribute along the x-axis

True
y bool

Whether to distribute along the y-axis

True

Returns:

Type Description
Self

self

Source code in src/keyed/group.py
def distribute(
    self,
    direction: Direction = ORIGIN,
    start: int = ALWAYS,
    end: int = ALWAYS,
    easing: EasingFunctionT = cubic_in_out,
    x: bool = True,
    y: bool = True,
) -> Self:
    """Distribute objects evenly between the first and last objects in the group.

    This keeps the first and last objects in their initial positions and distributes
    the remaining objects in between with equal spacing.

    Args:
        direction: Direction used to get anchor points on objects
        start: Starting frame for the animation
        end: Ending frame for the animation
        easing: Easing function to use
        x: Whether to distribute along the x-axis
        y: Whether to distribute along the y-axis

    Returns:
        self
    """
    objects = list(self)
    if len(objects) <= 2:
        # No distribution needed for 0, 1, or 2 objects
        return self

    # Get the first and last objects
    first, *middle, last = objects

    # Get positions of the first and last objects using the specified direction
    first_x, first_y = get_critical_point(first.geom, direction)
    last_x, last_y = get_critical_point(last.geom, -1 * direction)

    # Use these positions as the distribution bounds
    start_x, end_x = first_x, last_x
    start_y, end_y = first_y, last_y

    # Position each middle object
    for i, obj in enumerate(middle, 1):
        # Calculate interpolation factor (fraction of position in the sequence)
        t = i / (len(objects) - 1)

        # Get current position of this object
        obj_x, obj_y = get_critical_point(obj.geom, direction)

        # Calculate target position and translation
        dx = (start_x + t * (end_x - start_x) - obj_x) if x else 0
        dy = (start_y + t * (end_y - start_y) - obj_y) if y else 0

        # Apply transformation
        obj.translate(x=dx, y=dy, start=start, end=end, easing=easing)

    return self

lock_on2

lock_on2(target, reference=None, direction=ORIGIN, x=True, y=True)

Lock on to a target.

Parameters:

Name Type Description Default
target Transformable

Object to lock onto

required
reference ReactiveValue[GeometryT] | None

Measure from this object. This is useful for TextSelections, where you want to align to a particular character in the selection.

None
x bool

If true, lock on in the x dimension.

True
y bool

if true, lock on in the y dimension.

True
Source code in src/keyed/transforms.py
def lock_on2(
    self,
    target: Transformable,
    reference: ReactiveValue[GeometryT] | None = None,
    direction: Direction = ORIGIN,
    x: bool = True,
    y: bool = True,
) -> Self:
    """Lock on to a target.

    Args:
        target: Object to lock onto
        reference: Measure from this object. This is useful for TextSelections, where you want to align
            to a particular character in the selection.
        x: If true, lock on in the x dimension.
        y: if true, lock on in the y dimension.
    """
    reference = reference or self.geom
    return self.apply_transform(
        align_now(
            target=target.geom,
            reference=reference,
            direction=direction,
            x=x,
            y=y,
        )
    )

emphasize

emphasize(buffer=5, radius=0, fill_color=(1, 1, 1), color=(1, 1, 1), alpha=1, dash=None, line_width=2, draw_fill=True, draw_stroke=True, operator=OPERATOR_SCREEN)

Emphasize the object by drawing a rectangle around it.

Parameters:

Name Type Description Default
buffer float

The buffer distance around the object's geometry for the emphasis.

5
radius float

The corner radius of the emphasized area.

0
fill_color tuple[float, float, float]

The fill color of the emphasis as an RGB tuple.

(1, 1, 1)
color tuple[float, float, float]

The stroke color of the emphasis as an RGB tuple.

(1, 1, 1)
alpha float

The alpha transparency of the emphasis.

1
dash tuple[Sequence[float], float] | None

The dash pattern for the emphasis outline. Default is solid line.

None
line_width float

The line width of the emphasis outline.

2
draw_fill bool

Whether to draw the fill of the emphasis.

True
draw_stroke bool

Whether to draw the stroke of the emphasis.

True
operator Operator

The compositing operator to use for drawing the emphasis.

OPERATOR_SCREEN

Returns:

Type Description
Rectangle

A Rectangle object representing the emphasized area around the original object.

Notes

This creates a Rectangle instance and sets up dynamic expressions to follow the geometry of the object as it changes through different frames, applying the specified emphasis effects. Emphasis should generally be applied after all animations on the original object have been added.

Source code in src/keyed/base.py
def emphasize(
    self,
    buffer: float = 5,
    radius: float = 0,
    fill_color: tuple[float, float, float] = (1, 1, 1),
    color: tuple[float, float, float] = (1, 1, 1),
    alpha: float = 1,
    dash: tuple[Sequence[float], float] | None = None,
    line_width: float = 2,
    draw_fill: bool = True,
    draw_stroke: bool = True,
    operator: cairo.Operator = cairo.OPERATOR_SCREEN,
) -> Rectangle:
    """Emphasize the object by drawing a rectangle around it.

    Args:
        buffer: The buffer distance around the object's geometry for the emphasis.
        radius: The corner radius of the emphasized area.
        fill_color: The fill color of the emphasis as an RGB tuple.
        color: The stroke color of the emphasis as an RGB tuple.
        alpha: The alpha transparency of the emphasis.
        dash: The dash pattern for the emphasis outline. Default is solid line.
        line_width: The line width of the emphasis outline.
        draw_fill: Whether to draw the fill of the emphasis.
        draw_stroke: Whether to draw the stroke of the emphasis.
        operator: The compositing operator to use for drawing the emphasis.

    Returns:
        A Rectangle object representing the emphasized area around the original object.

    Notes:
        This creates a Rectangle instance and sets up dynamic expressions to follow the
        geometry of the object as it changes through different frames, applying the specified
        emphasis effects. Emphasis should generally be applied after all animations on the
        original object have been added.
    """
    # TODO: Consider renaming "buffer" to margin.
    from .shapes import Rectangle

    r = Rectangle(
        self.scene,
        color=color,
        x=self.center_x,
        y=self.center_y,
        width=self.width + buffer,
        height=self.height + buffer,
        fill_color=fill_color,
        alpha=alpha,
        dash=dash,
        operator=operator,
        line_width=line_width,
        draw_fill=draw_fill,
        draw_stroke=draw_stroke,
        radius=radius,
    )
    return r

center

center(frame=ALWAYS)

Center the object within the scene.

Parameters:

Name Type Description Default
frame int

The frame at which to center the object.

ALWAYS

Returns:

Type Description
Self

self

Source code in src/keyed/base.py
def center(self, frame: int = ALWAYS) -> Self:
    """Center the object within the scene.

    Args:
        frame: The frame at which to center the object.

    Returns:
        self
    """
    self.align_to(self.scene, start=frame, end=frame, direction=ORIGIN, center_on_zero=True)
    return self

line_to

line_to(other, self_direction=RIGHT, other_direction=LEFT, **line_kwargs)

Create a line connecting this object to another object.

Parameters:

Name Type Description Default
other Base

The target object to connect to

required
self_direction Direction

Direction for the connection point on this object

RIGHT
other_direction Direction

Direction for the connection point on the target object

LEFT
**line_kwargs Any

Additional arguments to pass to the Line constructor.

{}

Returns:

Type Description
'Line'

The created Line object

Source code in src/keyed/base.py
def line_to(
    self, other: Base, self_direction: Direction = RIGHT, other_direction: Direction = LEFT, **line_kwargs: Any
) -> "Line":
    """Create a line connecting this object to another object.

    Args:
        other: The target object to connect to
        self_direction: Direction for the connection point on this object
        other_direction: Direction for the connection point on the target object
        **line_kwargs: Additional arguments to pass to the [Line][keyed.line.Line] constructor.

    Returns:
        The created Line object
    """
    from .line import Line

    self_point = get_critical_point(self.geom, direction=self_direction)
    other_point = get_critical_point(other.geom, direction=other_direction)
    return Line(self.scene, x0=self_point[0], y0=self_point[1], x1=other_point[0], y1=other_point[1], **line_kwargs)