Skip to content

Scene

keyed.scene

The core Scene object upon which objects are drawn/animated.

Layer

Bases: Freezeable

Source code in src/keyed/scene.py
class Layer(Freezeable):
    def __init__(
        self,
        scene: Scene,
        name: str = "",
        z_index: int = 0,
        blend: BlendMode = BlendMode.OVER,
        alpha: float = 1.0,
    ):
        super().__init__()
        self.scene = scene
        self.name = name
        self.z_index = z_index
        self.content: list[Base] = []
        self.effects: list[Effect] = []
        self.blend = blend
        self.opacity = Signal(alpha)

    @guard_frozen
    def add(self, *objects: Base) -> None:
        self.content.extend(objects)

    if EXTRAS_INSTALLED:

        def apply_effect(self, effect: Effect) -> Self:
            self.effects.append(effect)
            return self

    def rasterize(self, frame: int) -> np.ndarray | cairo.SVGSurface:
        """Be sure to pass frame to have proper caching behavior."""
        # Draw objects onto the layer surface
        self.scene.clear()
        for obj in self.content:
            if frame in obj.lifetime:
                obj.cleanup()
                obj.draw()

        if not self.effects:
            return self.scene.surface

        # Otherwise, rasterize the scene and apply raster effects.
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.scene._width, self.scene._height)
        ctx = cairo.Context(surface)
        ctx.set_source_surface(self.scene.surface, 0, 0)
        ctx.paint()

        # Convert to NumPy array
        buf = surface.get_data()
        arr = np.ndarray(shape=(self.scene._height, self.scene._width, 4), dtype=np.uint8, buffer=buf)

        # Apply effects
        for effect in self.effects:
            effect.apply(arr)

        return arr

    def _freeze(self):
        """Freeze each layer's contents to enable caching."""
        if not self._is_frozen:
            for content in self.content:
                content.apply_transform(self.scene.controls.matrix)
            super()._freeze()

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

rasterize

rasterize(frame)

Be sure to pass frame to have proper caching behavior.

Source code in src/keyed/scene.py
def rasterize(self, frame: int) -> np.ndarray | cairo.SVGSurface:
    """Be sure to pass frame to have proper caching behavior."""
    # Draw objects onto the layer surface
    self.scene.clear()
    for obj in self.content:
        if frame in obj.lifetime:
            obj.cleanup()
            obj.draw()

    if not self.effects:
        return self.scene.surface

    # Otherwise, rasterize the scene and apply raster effects.
    surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.scene._width, self.scene._height)
    ctx = cairo.Context(surface)
    ctx.set_source_surface(self.scene.surface, 0, 0)
    ctx.paint()

    # Convert to NumPy array
    buf = surface.get_data()
    arr = np.ndarray(shape=(self.scene._height, self.scene._width, 4), dtype=np.uint8, buffer=buf)

    # Apply effects
    for effect in self.effects:
        effect.apply(arr)

    return arr

Scene

Bases: Transformable, Freezeable

A scene within which graphical objects are placed and manipulated.

Parameters:

Name Type Description Default
scene_name str | None

The name of the scene, used for naming output directories and files.

None
num_frames int

The number of frames to render in the scene.

60
output_dir Path

The directory path where output files will be stored.

Path('media')
width int

The width of the scene in pixels.

3840
height int

The height of the scene in pixels.

2160
antialias Antialias

The antialiasing level for rendering the scene.

ANTIALIAS_DEFAULT
freehand bool

Indicates whether to enable freehand drawing mode.

False
Source code in src/keyed/scene.py
class Scene(Transformable, Freezeable):
    """A scene within which graphical objects are placed and manipulated.

    Args:
        scene_name: The name of the scene, used for naming output directories and files.
        num_frames: The number of frames to render in the scene.
        output_dir: The directory path where output files will be stored.
        width: The width of the scene in pixels.
        height: The height of the scene in pixels.
        antialias: The antialiasing level for rendering the scene.
        freehand: Indicates whether to enable freehand drawing mode.
    """

    def __init__(
        self,
        scene_name: str | None = None,
        num_frames: int = 60,
        output_dir: Path = Path("media"),
        width: int = 3840,
        height: int = 2160,
        antialias: cairo.Antialias = cairo.ANTIALIAS_DEFAULT,
        freehand: bool = False,
    ) -> None:
        self.frame = Signal(0)
        Freezeable.__init__(self)
        super().__init__(self.frame)
        self.scene_name = scene_name
        self.num_frames = num_frames
        self.output_dir = output_dir
        self._width = width
        self._height = height
        self.surface = cairo.SVGSurface(None, width, height)  # type: ignore[arg-type]
        self.ctx = cairo.Context(self.surface)
        self.antialias = antialias
        self.freehand = freehand
        assert isinstance(self.controls.matrix, Signal)
        self.controls.matrix.value = self.controls.base_matrix()
        self.layers: list[Layer] = []
        self.default_layer = self.create_layer("default", z_index=0)
        self.renderer = Renderer(self)

    def nx(self, n: float) -> float:
        """Convert normalized x coordinate (0-1) to pixel value.

        Args:
            n: Normalized coordinate between 0 and 1

        Returns:
            Pixel value
        """
        return n * self._width

    def ny(self, n: float) -> float:
        """Convert normalized y coordinate (0-1) to pixel value.

        Args:
            n: Normalized coordinate between 0 and 1

        Returns:
            Pixel value
        """
        return n * self._height

    def create_layer(
        self,
        name: str = "",
        z_index: int | None = None,
        blend: BlendMode = BlendMode.OVER,
        alpha: float = 1.0,
    ) -> Layer:
        if z_index is None:
            z_index = len(self.layers)
        layer = Layer(self, name, z_index, blend, alpha)
        self.layers.append(layer)
        self._sort_layers()
        return layer

    def _sort_layers(self) -> None:
        self.layers.sort(key=lambda layer: layer.z_index)

    def move_layer(self, layer: Layer, position: Literal["up", "down", "top", "bottom"]) -> None:
        current_index = self.layers.index(layer)
        if position == "up" and current_index > 0:
            layer.z_index = self.layers[current_index - 1].z_index - 1
        elif position == "down" and current_index < len(self.layers) - 1:
            layer.z_index = self.layers[current_index + 1].z_index + 1
        elif position == "top":
            layer.z_index = max(layer.z_index for layer in self.layers) + 1
        elif position == "bottom":
            layer.z_index = min(layer.z_index for layer in self.layers) - 1
        self._sort_layers()

    def tic(self) -> None:
        self.frame.value = self.frame.value + 1

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"scene_name={self.scene_name!r}, "
            f"num_frames={self.num_frames}, "
            f"output_dir={self.output_dir!r}, "
            f"width={self._width}, "
            f"height={self._height})"
        )

    @property
    def full_output_dir(self) -> Path:
        """Full output directory.

        Returns:
            Path
        """
        assert self.scene_name is not None
        return self.output_dir / self.scene_name

    @guard_frozen
    def add(self, *content: Base) -> None:
        """Add one or more graphical objects to the scene.

        Args:
            content: One or more Base-derived objects to be added to the scene.
        """
        self.default_layer.add(*content)

    def clear(self) -> None:
        """Clear the drawing surface of the scene."""
        self.ctx.set_source_rgba(0, 0, 0, 0)
        self.ctx.set_operator(cairo.OPERATOR_CLEAR)
        self.ctx.paint()
        self.ctx.set_operator(cairo.OPERATOR_OVER)

    def delete_old_frames(self) -> None:
        """Delete old frame files from the output directory."""
        for file in self.full_output_dir.glob("*.png"):
            file.unlink()

    def _create_folder(self) -> None:
        """Create the output directory for the scene if it doesn't already exist."""
        if self.scene_name is None:
            raise ValueError("Must set scene name before drawing to file.")
        self.full_output_dir.mkdir(exist_ok=True, parents=True)

    def _open_folder(self) -> None:
        """Open the output directory using the default file explorer."""
        subprocess.run(["open", str(self.full_output_dir)])

    @freeze
    def draw(self, layers: Sequence[int] | None = None, delete: bool = True, open_dir: bool = False) -> None:
        """Draw the scene by rendering it to images.

        Args:
            layers: Specific layer(s) to render. If None, all layers are rendered.
            delete: Whether to delete old frames before drawing new ones.
            open_dir: Whether to open the output directory after drawing.
        """
        self._create_folder()
        if delete:
            self.delete_old_frames()

        layer_name = "-".join([str(layer) for layer in layers]) if layers is not None else "all"
        for frame in tqdm(range(self.num_frames)):
            self.frame.value = frame
            raster = self.rasterize(frame, layers=tuple(layers) if layers is not None else None)
            filename = self.full_output_dir / f"{layer_name}_{frame:03}.png"
            raster.write_to_png(filename)  # type: ignore[arg-type]

        if open_dir:
            self._open_folder()

    @freeze
    def draw_as_layers(self, open_dir: bool = False) -> None:
        """Draw each layer of the scene separately.

        Args:
            open_dir: Whether to open the output directory after drawing. Default is False.
        """
        self._create_folder()
        self.delete_old_frames()
        for i, _ in enumerate(self.layers):
            self.draw([i], delete=False)
        if open_dir:
            self._open_folder()

    @freeze
    def rasterize(self, frame: int, layers: Sequence[int] | None = None) -> cairo.ImageSurface:
        self.frame.value = frame
        layers_to_render = self.layers if layers is None else [self.layers[idx] for idx in layers]

        layer_arrays = []
        blend_modes = []

        for layer in layers_to_render:
            layer_out = layer.rasterize(frame)

            # If the layer surface is SVG, convert it to ImageSurface
            if isinstance(layer_out, cairo.SVGSurface):
                temp_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self._width, self._height)
                temp_ctx = cairo.Context(temp_surface)
                temp_ctx.set_source_surface(layer_out, 0, 0)
                temp_ctx.paint()
                buf = temp_surface.get_data()
                layer_out = np.ndarray(shape=(self._height, self._width, 4), dtype=np.uint8, buffer=buf)

            assert isinstance(layer_out, np.ndarray)

            # Apply layer opacity if not 1.0
            if layer.opacity.value < 1.0:
                layer_out[:, :, 3] = (layer_out[:, :, 3] * layer.opacity.value).astype(np.uint8)

            layer_arrays.append(layer_out)
            blend_modes.append(BlendMode(layer.blend))

        result = composite_layers(layer_arrays, blend_modes, self._width, self._height)

        # Create a new cairo.ImageSurface from the composited result
        output_surface = cairo.ImageSurface.create_for_data(
            result,  # pyright: ignore[reportArgumentType]
            cairo.FORMAT_ARGB32,
            self._width,
            self._height,
        )
        return output_surface

    @freeze
    def asarray(self, frame: int = 0, layers: Sequence[int] | None = None) -> np.ndarray:
        """Convert a frame of the scene to a NumPy array.

        Args:
            frame: The frame number to convert.
            layers: Specific layer(s) to convert. If None, all layers are converted.

        Returns:
            A NumPy array representing the raster image of the specified frame.
        """
        self.frame.value = frame
        return np.ndarray(
            shape=(self._height, self._width, 4),
            dtype=np.uint8,
            buffer=self.rasterize(frame, tuple(layers) if layers is not None else None).get_data(),
        )

    @freeze
    def find(self, x: HasValue[float], y: HasValue[float], frame: int = 0) -> Base | None:
        """Find the nearest object on the canvas to the given x, y coordinates.

        For composite objects, this will return the most atomic object possible. Namely, if
        a user clicks on Code, which itself contains Lines, Tokens, and Chars (Text), this
        method will return the nearest Text.

        Args:
            x: The x-coordinate to check.
            y: The y-coordinate to check.
            frame: The frame number to check against. Default is 0.

        Returns:
            The nearest object if found; otherwise, None.
        """
        from .extras import Editor

        x = unref(x)
        y = unref(y)

        try:
            point = shapely.Point(x, y)
            nearest: Base | None = None
            min_distance = float("inf")
            self.frame.value = frame

            def check_objects(objects: Iterable[Base]) -> None:
                nonlocal nearest, min_distance
                for obj in objects:
                    if isinstance(obj, Editor):
                        editor_nearest, editor_distance = obj.find(x, y, frame)
                        if editor_nearest and editor_distance < min_distance:
                            nearest = editor_nearest
                            min_distance = editor_distance

                    elif isinstance(obj, Selection):
                        check_objects(list(obj))
                    else:
                        if not is_visible(obj):
                            continue

                        with warnings.catch_warnings():
                            warnings.simplefilter("ignore", RuntimeWarning)
                            distance = point.distance(obj.geom.value)

                        if distance < min_distance:
                            min_distance = distance
                            nearest = obj

            for layer in self.layers:
                check_objects(layer.content)
            return nearest
        except Exception:
            return None

    @freeze
    def preview(self, frame_rate: int = 24) -> None:
        """Preview the scene in a window with specified frame rate.

        Args:
            frame_rate: The frame rate at which to preview the animation. Default is 24 fps.
        """
        from .previewer import create_animation_window

        create_animation_window(self, frame_rate=frame_rate)

    def get_context(self) -> cairo.Context[cairo.SVGSurface] | FreeHandContext:
        """Get the drawing context for the scene.

        Returns:
            The context to use for drawing.
        """
        ctx = cairo.Context(self.surface)
        from .extras import FreeHandContext

        return FreeHandContext(ctx) if self.freehand else ctx

    @property
    def _raw_geom_now(self) -> shapely.geometry.Polygon:
        """Get the raw geometric representation of the scene at the specified frame.

        Returns:
            The geometric representation of the scene at the specified frame.
        """
        return shapely.box(
            self.controls.delta_x.value,
            self.controls.delta_y.value,
            self._width,
            self._height,
        )

    def _freeze(self) -> None:
        """Freeze the scene to enable caching."""
        if not self._is_frozen:
            self.rasterize = cache(self.rasterize)  # type: ignore[method-assign]
            for layer in self.layers:
                layer._freeze()
            super()._freeze()

    def render(
        self,
        format: VideoFormat = VideoFormat.MOV_PRORES,
        engine: RenderEngine | None = None,
        output_path: Path | None = None,
        frame_rate: int = 24,
        **kwargs,
    ) -> None:
        self.renderer.render(
            format=format,
            engine=engine or get_default_render_engine(),
            output_path=output_path,
            frame_rate=frame_rate,
            **kwargs,
        )

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

full_output_dir property

full_output_dir

Full output directory.

Returns:

Type Description
Path

Path

geom property

geom

Return a reactive value of the transformed geometry.

nx

nx(n)

Convert normalized x coordinate (0-1) to pixel value.

Parameters:

Name Type Description Default
n float

Normalized coordinate between 0 and 1

required

Returns:

Type Description
float

Pixel value

Source code in src/keyed/scene.py
def nx(self, n: float) -> float:
    """Convert normalized x coordinate (0-1) to pixel value.

    Args:
        n: Normalized coordinate between 0 and 1

    Returns:
        Pixel value
    """
    return n * self._width

ny

ny(n)

Convert normalized y coordinate (0-1) to pixel value.

Parameters:

Name Type Description Default
n float

Normalized coordinate between 0 and 1

required

Returns:

Type Description
float

Pixel value

Source code in src/keyed/scene.py
def ny(self, n: float) -> float:
    """Convert normalized y coordinate (0-1) to pixel value.

    Args:
        n: Normalized coordinate between 0 and 1

    Returns:
        Pixel value
    """
    return n * self._height

add

add(*content)

Add one or more graphical objects to the scene.

Parameters:

Name Type Description Default
content Base

One or more Base-derived objects to be added to the scene.

()
Source code in src/keyed/scene.py
@guard_frozen
def add(self, *content: Base) -> None:
    """Add one or more graphical objects to the scene.

    Args:
        content: One or more Base-derived objects to be added to the scene.
    """
    self.default_layer.add(*content)

clear

clear()

Clear the drawing surface of the scene.

Source code in src/keyed/scene.py
def clear(self) -> None:
    """Clear the drawing surface of the scene."""
    self.ctx.set_source_rgba(0, 0, 0, 0)
    self.ctx.set_operator(cairo.OPERATOR_CLEAR)
    self.ctx.paint()
    self.ctx.set_operator(cairo.OPERATOR_OVER)

delete_old_frames

delete_old_frames()

Delete old frame files from the output directory.

Source code in src/keyed/scene.py
def delete_old_frames(self) -> None:
    """Delete old frame files from the output directory."""
    for file in self.full_output_dir.glob("*.png"):
        file.unlink()

draw

draw(layers=None, delete=True, open_dir=False)

Draw the scene by rendering it to images.

Parameters:

Name Type Description Default
layers Sequence[int] | None

Specific layer(s) to render. If None, all layers are rendered.

None
delete bool

Whether to delete old frames before drawing new ones.

True
open_dir bool

Whether to open the output directory after drawing.

False
Source code in src/keyed/scene.py
@freeze
def draw(self, layers: Sequence[int] | None = None, delete: bool = True, open_dir: bool = False) -> None:
    """Draw the scene by rendering it to images.

    Args:
        layers: Specific layer(s) to render. If None, all layers are rendered.
        delete: Whether to delete old frames before drawing new ones.
        open_dir: Whether to open the output directory after drawing.
    """
    self._create_folder()
    if delete:
        self.delete_old_frames()

    layer_name = "-".join([str(layer) for layer in layers]) if layers is not None else "all"
    for frame in tqdm(range(self.num_frames)):
        self.frame.value = frame
        raster = self.rasterize(frame, layers=tuple(layers) if layers is not None else None)
        filename = self.full_output_dir / f"{layer_name}_{frame:03}.png"
        raster.write_to_png(filename)  # type: ignore[arg-type]

    if open_dir:
        self._open_folder()

draw_as_layers

draw_as_layers(open_dir=False)

Draw each layer of the scene separately.

Parameters:

Name Type Description Default
open_dir bool

Whether to open the output directory after drawing. Default is False.

False
Source code in src/keyed/scene.py
@freeze
def draw_as_layers(self, open_dir: bool = False) -> None:
    """Draw each layer of the scene separately.

    Args:
        open_dir: Whether to open the output directory after drawing. Default is False.
    """
    self._create_folder()
    self.delete_old_frames()
    for i, _ in enumerate(self.layers):
        self.draw([i], delete=False)
    if open_dir:
        self._open_folder()

asarray

asarray(frame=0, layers=None)

Convert a frame of the scene to a NumPy array.

Parameters:

Name Type Description Default
frame int

The frame number to convert.

0
layers Sequence[int] | None

Specific layer(s) to convert. If None, all layers are converted.

None

Returns:

Type Description
ndarray

A NumPy array representing the raster image of the specified frame.

Source code in src/keyed/scene.py
@freeze
def asarray(self, frame: int = 0, layers: Sequence[int] | None = None) -> np.ndarray:
    """Convert a frame of the scene to a NumPy array.

    Args:
        frame: The frame number to convert.
        layers: Specific layer(s) to convert. If None, all layers are converted.

    Returns:
        A NumPy array representing the raster image of the specified frame.
    """
    self.frame.value = frame
    return np.ndarray(
        shape=(self._height, self._width, 4),
        dtype=np.uint8,
        buffer=self.rasterize(frame, tuple(layers) if layers is not None else None).get_data(),
    )

find

find(x, y, frame=0)

Find the nearest object on the canvas to the given x, y coordinates.

For composite objects, this will return the most atomic object possible. Namely, if a user clicks on Code, which itself contains Lines, Tokens, and Chars (Text), this method will return the nearest Text.

Parameters:

Name Type Description Default
x HasValue[float]

The x-coordinate to check.

required
y HasValue[float]

The y-coordinate to check.

required
frame int

The frame number to check against. Default is 0.

0

Returns:

Type Description
Base | None

The nearest object if found; otherwise, None.

Source code in src/keyed/scene.py
@freeze
def find(self, x: HasValue[float], y: HasValue[float], frame: int = 0) -> Base | None:
    """Find the nearest object on the canvas to the given x, y coordinates.

    For composite objects, this will return the most atomic object possible. Namely, if
    a user clicks on Code, which itself contains Lines, Tokens, and Chars (Text), this
    method will return the nearest Text.

    Args:
        x: The x-coordinate to check.
        y: The y-coordinate to check.
        frame: The frame number to check against. Default is 0.

    Returns:
        The nearest object if found; otherwise, None.
    """
    from .extras import Editor

    x = unref(x)
    y = unref(y)

    try:
        point = shapely.Point(x, y)
        nearest: Base | None = None
        min_distance = float("inf")
        self.frame.value = frame

        def check_objects(objects: Iterable[Base]) -> None:
            nonlocal nearest, min_distance
            for obj in objects:
                if isinstance(obj, Editor):
                    editor_nearest, editor_distance = obj.find(x, y, frame)
                    if editor_nearest and editor_distance < min_distance:
                        nearest = editor_nearest
                        min_distance = editor_distance

                elif isinstance(obj, Selection):
                    check_objects(list(obj))
                else:
                    if not is_visible(obj):
                        continue

                    with warnings.catch_warnings():
                        warnings.simplefilter("ignore", RuntimeWarning)
                        distance = point.distance(obj.geom.value)

                    if distance < min_distance:
                        min_distance = distance
                        nearest = obj

        for layer in self.layers:
            check_objects(layer.content)
        return nearest
    except Exception:
        return None

preview

preview(frame_rate=24)

Preview the scene in a window with specified frame rate.

Parameters:

Name Type Description Default
frame_rate int

The frame rate at which to preview the animation. Default is 24 fps.

24
Source code in src/keyed/scene.py
@freeze
def preview(self, frame_rate: int = 24) -> None:
    """Preview the scene in a window with specified frame rate.

    Args:
        frame_rate: The frame rate at which to preview the animation. Default is 24 fps.
    """
    from .previewer import create_animation_window

    create_animation_window(self, frame_rate=frame_rate)

get_context

get_context()

Get the drawing context for the scene.

Returns:

Type Description
Context[SVGSurface] | FreeHandContext

The context to use for drawing.

Source code in src/keyed/scene.py
def get_context(self) -> cairo.Context[cairo.SVGSurface] | FreeHandContext:
    """Get the drawing context for the scene.

    Returns:
        The context to use for drawing.
    """
    ctx = cairo.Context(self.surface)
    from .extras import FreeHandContext

    return FreeHandContext(ctx) if self.freehand else ctx

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)