Skip to content

Text

keyed.text

Drawable objects related to Text.

Text

Bases: Base

A single line of text that can be drawn on screen.

Parameters:

Name Type Description Default
scene Scene

Scene to draw on

required
text HasValue[str]

Text content to display

required
size float

Font size.

24
x HasValue[float] | None

X position. Default uses scene center.

None
y HasValue[float] | None

Y position. Default uses scene center.

None
font str

Font family name.

'Anonymous Pro'
color tuple[float, float, float]

RGB color tuple.

(1, 1, 1)
fill_color tuple[float, float, float] | None

Optional color to use for inner portion of outlined text.

None
alpha float

Opacity from 0-1.

1.0
slant FontSlant

Font slant style.

FONT_SLANT_NORMAL
weight FontWeight

Font weight.

FONT_WEIGHT_NORMAL
operator Operator

Cairo operator for blending.

OPERATOR_OVER
Source code in src/keyed/text.py
class Text(Base):
    """A single line of text that can be drawn on screen.

    Args:
        scene: Scene to draw on
        text: Text content to display
        size: Font size.
        x: X position. Default uses scene center.
        y: Y position. Default uses scene center.
        font: Font family name.
        color: RGB color tuple.
        fill_color: Optional color to use for inner portion of outlined text.
        alpha: Opacity from 0-1.
        slant: Font slant style.
        weight: Font weight.
        operator: Cairo operator for blending.
    """

    def __init__(
        self,
        scene: Scene,
        text: HasValue[str],
        size: float = 24,
        x: HasValue[float] | None = None,
        y: HasValue[float] | None = None,
        font: str = "Anonymous Pro",
        color: tuple[float, float, float] = (1, 1, 1),
        fill_color: tuple[float, float, float] | None = None,
        alpha: float = 1.0,
        line_width: float = 2,
        slant: cairo.FontSlant = cairo.FONT_SLANT_NORMAL,
        weight: cairo.FontWeight = cairo.FONT_WEIGHT_NORMAL,
        operator: cairo.Operator = cairo.OPERATOR_OVER,
    ):
        super().__init__(scene)
        self.scene = scene
        self.text = as_signal(text)
        self.font = font
        self.color = as_color(color)
        self.fill_color = as_color(fill_color) if fill_color is not None else None
        self.alpha = as_signal(alpha)
        self.line_width = as_signal(line_width)
        self.slant = slant
        self.weight = weight
        self.size: ReactiveValue[float] = as_signal(size)
        self.x = x if x is not None else scene.nx(0.5)
        self.y = y if y is not None else scene.ny(0.5)
        self.controls.delta_x.value = self.x
        self.controls.delta_y.value = self.y
        self.ctx = scene.get_context()
        self.operator = operator
        self._dependencies.extend([self.size, self.text])
        assert isinstance(self.controls.matrix, Signal)
        self.controls.matrix.value = self.controls.base_matrix()

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(text={unref(self.text)!r}, x={self.x:2}, y={self.y:2})"

    @contextmanager
    def _style(self) -> Generator[None, None, None]:
        """Set up the font context for drawing."""
        try:
            self.ctx.save()
            self.ctx.set_operator(self.operator)
            self.ctx.select_font_face(self.font, self.slant, self.weight)
            self.ctx.set_font_size(self.size.value)
            self.ctx.set_source_rgba(*unref(self.color).rgb, self.alpha.value)
            yield None
        finally:
            self.ctx.restore()

    def draw(self) -> None:
        """Draw the text to the scene."""
        with self._style():
            self.ctx.new_path()
            self.ctx.transform(self.controls.matrix.value)

            if self.fill_color is None:
                # Common case: Just show text directly with color
                self.ctx.set_source_rgba(*unref(self.color).rgb, self.alpha.value)
                self.ctx.show_text(unref(self.text))
            else:
                # Special case: Draw outlined text
                self.ctx.text_path(unref(self.text))

                self.ctx.set_source_rgba(*unref(self.fill_color).rgb, self.alpha.value)
                self.ctx.fill_preserve()

                self.ctx.set_source_rgba(*unref(self.color).rgb, self.alpha.value)
                self.ctx.set_line_width(self.line_width.value)
                self.ctx.stroke()

    @property
    def _extents(self) -> cairo.TextExtents:
        """Get the text dimensions."""
        with self._style():
            return self.ctx.text_extents(unref(self.text))

    @property
    def _raw_geom_now(self) -> shapely.Polygon:
        """Get text bounds geometry before transforms."""
        extents = self._extents
        x = extents.x_bearing
        y = extents.y_bearing
        w = extents.width
        h = extents.height
        return shapely.box(x, y, x + w, y + h)

geom property

geom

Return a reactive value of the transformed geometry.

draw

draw()

Draw the text to the scene.

Source code in src/keyed/text.py
def draw(self) -> None:
    """Draw the text to the scene."""
    with self._style():
        self.ctx.new_path()
        self.ctx.transform(self.controls.matrix.value)

        if self.fill_color is None:
            # Common case: Just show text directly with color
            self.ctx.set_source_rgba(*unref(self.color).rgb, self.alpha.value)
            self.ctx.show_text(unref(self.text))
        else:
            # Special case: Draw outlined text
            self.ctx.text_path(unref(self.text))

            self.ctx.set_source_rgba(*unref(self.fill_color).rgb, self.alpha.value)
            self.ctx.fill_preserve()

            self.ctx.set_source_rgba(*unref(self.color).rgb, self.alpha.value)
            self.ctx.set_line_width(self.line_width.value)
            self.ctx.stroke()

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)

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)

TextSelection

Bases: Selection[CodeTextT]

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

Source code in src/keyed/text.py
class TextSelection(Selection[CodeTextT]):  # type: ignore[misc]
    """A sequence of BaseText objects, allowing collective transformations and animations."""

    @property
    def chars(self) -> TextSelection[_Character]:
        """Return a TextSelection of single characters."""
        return TextSelection(itertools.chain.from_iterable(item.chars for item in self))

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

        Args:
            property: The property to animate.
            animator: The animation factory function to apply to each object.
            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.
            skip_whitespace: Whether to skip whitespace characters.

        See Also:
            [keyed.animation.stagger][keyed.animation.stagger]

        """
        frame = start
        for item in self:
            if skip_whitespace and item.is_whitespace():
                continue
            animation = animator(start=frame, end=frame + duration)
            item._animate(property, animation)
            frame += delay
        return self

    def is_whitespace(self) -> bool:
        """Determine if all objects in the selection are whitespace.

        Returns:
            True if all objects are whitespace, False otherwise.
        """
        return all(obj.is_whitespace() for obj in self)

    def contains(self, query: _Character) -> bool:
        """Check if the query text is within the TextSelection's characters."""
        return query in self.chars

    def filter_whitespace(self) -> TextSelection:
        """Filter out all objects that are whitespace from the selection.

        Returns:
            A new TextSelection containing only non-whitespace objects.
        """
        return TextSelection(obj for obj in self if not obj.is_whitespace())

    def highlight(
        self,
        color: tuple[float, float, float] = (1, 1, 1),
        alpha: float = 1,
        dash: tuple[Sequence[float], float] | None = None,
        operator: cairo.Operator = cairo.OPERATOR_SCREEN,
        line_width: float = 1,
        tension: float = 1,
    ) -> Curve:
        """Highlight text by drawing a curve passing through the text.

        Args:
            color: The color to use for highlighting as an RGB tuple.
            alpha: The transparency level of the highlight.
            dash: Dash pattern for the highlight stroke.
            operator: The compositing operator to use for rendering the highlight.
            line_width: The width of the highlight stroke.
            tension: The tension for the curve fitting the text. A value of 0 will draw a
                linear path betwee points, where as a non-zero value will allow some
                slack in the bezier curve connecting each set of points.

        Returns:
            A Curve passing through all characters in the underlying text.
        """
        from .curve import Curve

        # TODO - c should be c.clone(), but clone not implemented for text.
        return Curve(
            self.scene,
            objects=[c for c in self.chars.filter_whitespace()],
            color=color,
            alpha=alpha,
            dash=dash,
            operator=operator,
            line_width=line_width,
            tension=tension,
        )

chars property

chars

Return a TextSelection of single characters.

write_on

write_on(property, animator, start, delay, duration, skip_whitespace=True)

Sequentially animates a property across all objects in the selection.

Parameters:

Name Type Description Default
property str

The property to animate.

required
animator Callable

The animation factory function to apply to each object.

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
skip_whitespace bool

Whether to skip whitespace characters.

True
See Also

keyed.animation.stagger

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

    Args:
        property: The property to animate.
        animator: The animation factory function to apply to each object.
        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.
        skip_whitespace: Whether to skip whitespace characters.

    See Also:
        [keyed.animation.stagger][keyed.animation.stagger]

    """
    frame = start
    for item in self:
        if skip_whitespace and item.is_whitespace():
            continue
        animation = animator(start=frame, end=frame + duration)
        item._animate(property, animation)
        frame += delay
    return self

is_whitespace

is_whitespace()

Determine if all objects in the selection are whitespace.

Returns:

Type Description
bool

True if all objects are whitespace, False otherwise.

Source code in src/keyed/text.py
def is_whitespace(self) -> bool:
    """Determine if all objects in the selection are whitespace.

    Returns:
        True if all objects are whitespace, False otherwise.
    """
    return all(obj.is_whitespace() for obj in self)

contains

contains(query)

Check if the query text is within the TextSelection's characters.

Source code in src/keyed/text.py
def contains(self, query: _Character) -> bool:
    """Check if the query text is within the TextSelection's characters."""
    return query in self.chars

filter_whitespace

filter_whitespace()

Filter out all objects that are whitespace from the selection.

Returns:

Type Description
TextSelection

A new TextSelection containing only non-whitespace objects.

Source code in src/keyed/text.py
def filter_whitespace(self) -> TextSelection:
    """Filter out all objects that are whitespace from the selection.

    Returns:
        A new TextSelection containing only non-whitespace objects.
    """
    return TextSelection(obj for obj in self if not obj.is_whitespace())

highlight

highlight(color=(1, 1, 1), alpha=1, dash=None, operator=OPERATOR_SCREEN, line_width=1, tension=1)

Highlight text by drawing a curve passing through the text.

Parameters:

Name Type Description Default
color tuple[float, float, float]

The color to use for highlighting as an RGB tuple.

(1, 1, 1)
alpha float

The transparency level of the highlight.

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

Dash pattern for the highlight stroke.

None
operator Operator

The compositing operator to use for rendering the highlight.

OPERATOR_SCREEN
line_width float

The width of the highlight stroke.

1
tension float

The tension for the curve fitting the text. A value of 0 will draw a linear path betwee points, where as a non-zero value will allow some slack in the bezier curve connecting each set of points.

1

Returns:

Type Description
Curve

A Curve passing through all characters in the underlying text.

Source code in src/keyed/text.py
def highlight(
    self,
    color: tuple[float, float, float] = (1, 1, 1),
    alpha: float = 1,
    dash: tuple[Sequence[float], float] | None = None,
    operator: cairo.Operator = cairo.OPERATOR_SCREEN,
    line_width: float = 1,
    tension: float = 1,
) -> Curve:
    """Highlight text by drawing a curve passing through the text.

    Args:
        color: The color to use for highlighting as an RGB tuple.
        alpha: The transparency level of the highlight.
        dash: Dash pattern for the highlight stroke.
        operator: The compositing operator to use for rendering the highlight.
        line_width: The width of the highlight stroke.
        tension: The tension for the curve fitting the text. A value of 0 will draw a
            linear path betwee points, where as a non-zero value will allow some
            slack in the bezier curve connecting each set of points.

    Returns:
        A Curve passing through all characters in the underlying text.
    """
    from .curve import Curve

    # TODO - c should be c.clone(), but clone not implemented for text.
    return Curve(
        self.scene,
        objects=[c for c in self.chars.filter_whitespace()],
        color=color,
        alpha=alpha,
        dash=dash,
        operator=operator,
        line_width=line_width,
        tension=tension,
    )

Code

Bases: TextSelection[_Line]

A code block.

Parameters:

Name Type Description Default
scene Scene

The scene in which the code is displayed.

required
tokens list[StyledToken]

A list of styled tokens that make up the code.

required
font str

The font family used for the code text.

'Anonymous Pro'
font_size int

The font size used for the code text.

24
x float

The x-coordinate for the position of the code.

10
y float

The y-coordinate for the position of the code.

10
alpha float

The opacity level of the code text.

1
operator Operator

The compositing operator used to render the code.

OPERATOR_OVER
_ascent_correction bool

Whether to adjust the y-position based on the font's ascent.

True
See Also

keyed.highlight.tokenize

Source code in src/keyed/text.py
class Code(TextSelection[_Line]):
    """A code block.

    Args:
        scene: The scene in which the code is displayed.
        tokens: A list of styled tokens that make up the code.
        font: The font family used for the code text.
        font_size: The font size used for the code text.
        x: The x-coordinate for the position of the code.
        y: The y-coordinate for the position of the code.
        alpha: The opacity level of the code text.
        operator: The compositing operator used to render the code.
        _ascent_correction: Whether to adjust the y-position based on the font's ascent.

    See Also:
        [keyed.highlight.tokenize][keyed.highlight.tokenize]
    """

    # TODO:
    # * Consider making this object a proper, slicable list-like thing (i.e., replace
    #   __init__ with a classmethod)
    # * Consider removing _ascent_correction.

    def __init__(
        self,
        scene: Scene,
        tokens: list[StyledToken],
        font: str = "Anonymous Pro",
        font_size: int = 24,
        x: float = 10,
        y: float = 10,
        alpha: float = 1,
        operator: cairo.Operator = cairo.OPERATOR_OVER,
        _ascent_correction: bool = True,
    ) -> None:
        self._tokens = tokens
        self.font = font
        self.font_size = font_size

        ctx = scene.get_context()
        self._set_default_font(ctx)
        ascent, _, height, *_ = ctx.font_extents()
        y += ascent if _ascent_correction else 0
        line_height = 1.2 * height

        lines = []
        line: list[StyledToken] = []
        for token in tokens:
            if (token.token_type, token.text) == (PygmentsToken.Text.Whitespace, "\n"):
                lines.append(line)
                line = []
            else:
                line.append(token)
        if line:
            lines.append(line)

        objects: TextSelection[_Line] = TextSelection()
        for line in lines:
            objects.append(
                _Line(
                    scene,
                    tokens=line,
                    x=x,
                    y=y,
                    font=font,
                    font_size=font_size,
                    alpha=alpha,
                    code=self,
                    operator=operator,
                )
            )
            y += line_height
        super().__init__(objects)

    def _set_default_font(self, ctx: cairo.Context) -> None:
        """Set the font/size.

        Args:
            ctx: cairo.Context
        """
        ctx.select_font_face(self.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        ctx.set_font_size(self.font_size)

    @property
    def tokens(self) -> TextSelection[_Token]:
        """Return a TextSelection of tokens in the code object."""
        return TextSelection(itertools.chain(*self.lines))

    @property
    def lines(self) -> TextSelection[_Line]:
        """Return a TextSelection of lines in the code object."""
        return TextSelection(self)

    def find_line(self, query: _Character) -> int:
        """Find the line index of a given character."""
        for idx, line in enumerate(self.lines):
            if line.contains(query):
                return idx
        return -1

    def find_token(self, query: _Character) -> int:
        """Find the token index of a given character."""
        for index, token in enumerate(self.tokens):
            if token.contains(query):
                return index
        return -1

    def find_char(self, query: _Character) -> int:
        """Find the charecter index of a given character."""
        for index, char in enumerate(self.chars):
            if char == query:
                return index
        return -1

tokens property

tokens

Return a TextSelection of tokens in the code object.

lines property

lines

Return a TextSelection of lines in the code object.

chars property

chars

Return a TextSelection of single characters.

find_line

find_line(query)

Find the line index of a given character.

Source code in src/keyed/text.py
def find_line(self, query: _Character) -> int:
    """Find the line index of a given character."""
    for idx, line in enumerate(self.lines):
        if line.contains(query):
            return idx
    return -1

find_token

find_token(query)

Find the token index of a given character.

Source code in src/keyed/text.py
def find_token(self, query: _Character) -> int:
    """Find the token index of a given character."""
    for index, token in enumerate(self.tokens):
        if token.contains(query):
            return index
    return -1

find_char

find_char(query)

Find the charecter index of a given character.

Source code in src/keyed/text.py
def find_char(self, query: _Character) -> int:
    """Find the charecter index of a given character."""
    for index, char in enumerate(self.chars):
        if char == query:
            return index
    return -1

write_on

write_on(property, animator, start, delay, duration, skip_whitespace=True)

Sequentially animates a property across all objects in the selection.

Parameters:

Name Type Description Default
property str

The property to animate.

required
animator Callable

The animation factory function to apply to each object.

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
skip_whitespace bool

Whether to skip whitespace characters.

True
See Also

keyed.animation.stagger

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

    Args:
        property: The property to animate.
        animator: The animation factory function to apply to each object.
        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.
        skip_whitespace: Whether to skip whitespace characters.

    See Also:
        [keyed.animation.stagger][keyed.animation.stagger]

    """
    frame = start
    for item in self:
        if skip_whitespace and item.is_whitespace():
            continue
        animation = animator(start=frame, end=frame + duration)
        item._animate(property, animation)
        frame += delay
    return self

is_whitespace

is_whitespace()

Determine if all objects in the selection are whitespace.

Returns:

Type Description
bool

True if all objects are whitespace, False otherwise.

Source code in src/keyed/text.py
def is_whitespace(self) -> bool:
    """Determine if all objects in the selection are whitespace.

    Returns:
        True if all objects are whitespace, False otherwise.
    """
    return all(obj.is_whitespace() for obj in self)

contains

contains(query)

Check if the query text is within the TextSelection's characters.

Source code in src/keyed/text.py
def contains(self, query: _Character) -> bool:
    """Check if the query text is within the TextSelection's characters."""
    return query in self.chars

filter_whitespace

filter_whitespace()

Filter out all objects that are whitespace from the selection.

Returns:

Type Description
TextSelection

A new TextSelection containing only non-whitespace objects.

Source code in src/keyed/text.py
def filter_whitespace(self) -> TextSelection:
    """Filter out all objects that are whitespace from the selection.

    Returns:
        A new TextSelection containing only non-whitespace objects.
    """
    return TextSelection(obj for obj in self if not obj.is_whitespace())

highlight

highlight(color=(1, 1, 1), alpha=1, dash=None, operator=OPERATOR_SCREEN, line_width=1, tension=1)

Highlight text by drawing a curve passing through the text.

Parameters:

Name Type Description Default
color tuple[float, float, float]

The color to use for highlighting as an RGB tuple.

(1, 1, 1)
alpha float

The transparency level of the highlight.

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

Dash pattern for the highlight stroke.

None
operator Operator

The compositing operator to use for rendering the highlight.

OPERATOR_SCREEN
line_width float

The width of the highlight stroke.

1
tension float

The tension for the curve fitting the text. A value of 0 will draw a linear path betwee points, where as a non-zero value will allow some slack in the bezier curve connecting each set of points.

1

Returns:

Type Description
Curve

A Curve passing through all characters in the underlying text.

Source code in src/keyed/text.py
def highlight(
    self,
    color: tuple[float, float, float] = (1, 1, 1),
    alpha: float = 1,
    dash: tuple[Sequence[float], float] | None = None,
    operator: cairo.Operator = cairo.OPERATOR_SCREEN,
    line_width: float = 1,
    tension: float = 1,
) -> Curve:
    """Highlight text by drawing a curve passing through the text.

    Args:
        color: The color to use for highlighting as an RGB tuple.
        alpha: The transparency level of the highlight.
        dash: Dash pattern for the highlight stroke.
        operator: The compositing operator to use for rendering the highlight.
        line_width: The width of the highlight stroke.
        tension: The tension for the curve fitting the text. A value of 0 will draw a
            linear path betwee points, where as a non-zero value will allow some
            slack in the bezier curve connecting each set of points.

    Returns:
        A Curve passing through all characters in the underlying text.
    """
    from .curve import Curve

    # TODO - c should be c.clone(), but clone not implemented for text.
    return Curve(
        self.scene,
        objects=[c for c in self.chars.filter_whitespace()],
        color=color,
        alpha=alpha,
        dash=dash,
        operator=operator,
        line_width=line_width,
        tension=tension,
    )

keyed.highlight

Syntax highlighting.

StyledToken

Bases: BaseModel

A pydantic model for serializing pygments output.

Source code in src/keyed/highlight.py
class StyledToken(BaseModel, arbitrary_types_allowed=True):
    """A pydantic model for serializing pygments output."""

    text: str
    token_type: _TokenType
    color: tuple[float, float, float]
    italic: bool
    bold: bool

    @field_serializer("token_type")
    def serialize_token_type(self, token_type: _TokenType, _info: Any) -> str:
        return str(token_type)

    @field_validator("token_type", mode="before")
    def deserialize_token_type(cls, val: Any) -> Any:
        if isinstance(val, str):
            return eval(val)
        return val

    def to_cairo(self) -> dict[str, Any]:
        import cairo

        return {
            "color": self.color,
            "slant": (cairo.FONT_SLANT_NORMAL if not self.italic else cairo.FONT_SLANT_ITALIC),
            "weight": (cairo.FONT_WEIGHT_NORMAL if not self.bold else cairo.FONT_WEIGHT_BOLD),
            "token_type": self.token_type,
        }

KeyedFormatter

Bases: Formatter

Format syntax highlighted text as JSON with color, slant, and weight metadata.

Source code in src/keyed/highlight.py
class KeyedFormatter(Formatter):
    """Format syntax highlighted text as JSON with color, slant, and weight metadata."""

    name = "KeyedFormatter"
    aliases = ["keyed"]
    filenames: list[str] = []

    def __init__(self, **options: Any) -> None:
        super().__init__(**options)

    @staticmethod
    def format_code(tokens: list[tuple[_TokenType, str]], style: StyleMeta) -> str:
        colors = style_to_color_map(style)
        styled_tokens: list[StyledToken] = []
        for token_type, token in tokens:
            token_style = colors.get(token_type, _Style(r=1, g=1, b=1))
            styled_tokens.append(
                StyledToken(
                    text=token,
                    token_type=token_type,
                    color=token_style.rgb,
                    italic=token_style.italic,
                    bold=token_style.bold,
                )
            )
        return StyledTokens.dump_json(styled_tokens).decode()

    def format_unencoded(self, tokensource, outfile) -> None:  # type: ignore[no-untyped-def]
        formatted_output = self.format_code(list(tokensource), style=self.style)
        outfile.write(formatted_output)

tokenize

tokenize(text, lexer=None, formatter=None, filename='<unknown>')

Tokenize code text into styled tokens.

Parameters:

Name Type Description Default
text str

The code text to tokenize.

required
lexer Lexer | None

The Pygments lexer to use. If None, PythonLexer is used.

None
formatter Formatter | None

The Pygments formatter to use. If None, KeyedFormatter is used.

None
filename str

The filename of the code, used for more accurate analysis with jedi. Default is ''.

'<unknown>'

Returns:

Type Description
list[StyledToken]

List of styled tokens.

Source code in src/keyed/highlight.py
def tokenize(
    text: str, lexer: Lexer | None = None, formatter: Formatter | None = None, filename: str = "<unknown>"
) -> list[StyledToken]:
    """Tokenize code text into styled tokens.

    Args:
        text: The code text to tokenize.
        lexer: The Pygments lexer to use. If None, PythonLexer is used.
        formatter: The Pygments formatter to use. If None, KeyedFormatter is used.
        filename: The filename of the code, used for more accurate analysis with `jedi`. Default is '<unknown>'.

    Returns:
        List of styled tokens.
    """
    from pygments import format, lex
    from pygments.lexers.python import PythonLexer

    formatter = formatter or KeyedFormatter(style=DEFAULT_STYLE)
    raw_tokens = _split_multiline_tokens(lex(text, lexer or PythonLexer()))

    # Apply post-processor to enhance token types
    # from .extras import post_process_tokens

    # processed_tokens = post_process_tokens(text, raw_tokens, filename)

    json_str = format(raw_tokens, formatter)
    return StyledTokens.validate_json(json_str)