<reponame>OrangeChannel/vs-transitions<gh_stars>1-10
"""Powerpoint-like transitions implemented in VapourSynth."""
__all__ = [
    "cover",
    "cube_rotate",
    "curtain_cover",
    "curtain_reveal",
    "fade",
    "fade_from_black",
    "fade_to_black",
    "linear_boundary",
    "poly_fade",
    "push",
    "reveal",
    "slide_expand",
    "squeeze_expand",
    "squeeze_slide",
    "wipe",
]

import enum
import math
from fractions import Fraction
from typing import Callable, Optional, Tuple, TYPE_CHECKING
from warnings import simplefilter, warn

import vapoursynth as vs

try:
    from ._metadata import __author__, __date__, __version__  # type: ignore
except (ImportError, ModuleNotFoundError):
    __author__ = __date__ = __version__ = "unknown (portable version)"  # type: ignore

simplefilter("always")

core = vs.core


class Direction(str, enum.Enum):
    """Direction enumeration.

    Members can be simply referenced by their names,
    i.e. ``vs_transitions.LEFT`` instead of ``vs_transitions.Direction.LEFT``.
    """

    LEFT = "left"
    RIGHT = "right"
    UP = "up"
    DOWN = "down"
    HORIZONTAL = "horizontal"
    VERTICAL = "vertical"


LEFT = Direction.LEFT
RIGHT = Direction.RIGHT
UP = Direction.UP
DOWN = Direction.DOWN
HORIZONTAL = Direction.HORIZONTAL
VERTICAL = Direction.VERTICAL
__all__ += list(Direction.__members__)


class MiscConstants(str, enum.Enum):
    """Miscellanious enumeration for some functions.

    Members can be simply referenced by their names,
    i.e. ``vs_transitions.SLIDE`` instead of ``vs_transitions.MiscConstants.SLIDE``.
    """

    SLIDE = "slide"
    SQUEEZE = "squeeze"
    EXPAND = "expand"


SLIDE = MiscConstants.SLIDE
SQUEEZE = MiscConstants.SQUEEZE
EXPAND = MiscConstants.EXPAND
__all__ += list(MiscConstants.__members__)


def _check_clips(frames: int, caller: Callable, *clips: vs.VideoNode, **kwargs) -> None:
    """General checker for clip formats, resolutions, length, and other keywords.

    Possible kwargs:
        'subsampling': checks that all clips have 444 subsampling for resize purposes
    """
    if frames <= 0:
        raise ValueError(f"{caller.__name__}: `frames` cannot be less than 1")
    same_check = set()
    for clip in clips:
        if clip.format is None:
            raise ValueError(f"{caller.__name__}: all clips must be constant-format")
        if 0 in (clip.width, clip.height):
            raise ValueError(f"{caller.__name__}: all clips must be constant-resolution")
        if clip.num_frames < frames:
            raise ValueError(f"{caller.__name__}: all clips must have at least {frames} frames")
        same_check.add((clip.format.id, clip.width, clip.height))

        if kwargs:
            if ("subsampling" in kwargs) and kwargs["subsampling"]:
                if clip.format.subsampling_w != 0 or clip.format.subsampling_h != 0:
                    raise ValueError(
                        f"{caller.__name__}: all clips must have 444 chroma subsampling for a non-mod2 resize"
                    )

    if len(same_check) > 1:
        raise ValueError(f"{caller.__name__}: all clips must be same format and resolution")


def _return_combo(
    clip1: Optional[vs.VideoNode],
    clip_middle: vs.VideoNode,
    clip2: Optional[vs.VideoNode],
) -> vs.VideoNode:
    """Prevents splicing empty clips.

    :param clip1:        optional start clip
    :param clip_middle:  mandatory middle clip
    :param clip2:        optional ending clip
    :return: splice of existing clips in order
    """
    if clip1 is not None and clip2 is not None:
        return clip1 + clip_middle + clip2
    elif clip1 is not None and clip2 is None:
        return clip1 + clip_middle
    elif clip1 is None and clip2 is not None:
        return clip_middle + clip2
    elif clip1 is None and clip2 is None:
        return clip_middle


def _transition_clips(
    clip1: vs.VideoNode, clip2: vs.VideoNode, frames: int
) -> Tuple[Optional[vs.VideoNode], Optional[vs.VideoNode], vs.VideoNode, vs.VideoNode]:
    """Returns clean (non-transition) and transition sections of the given clips based on frames."""
    if clip1.num_frames == frames:
        clip1_t_zone = clip1
        clip1_clean = None
    else:
        clip1_t_zone = clip1[-frames:]
        clip1_clean = clip1[:-frames]

    if clip2.num_frames == frames:
        clip2_t_zone = clip2
        clip2_clean = None
    else:
        clip2_t_zone = clip2[:frames]
        clip2_clean = clip2[frames:]

    return clip1_clean, clip2_clean, clip1_t_zone, clip2_t_zone


def fade(clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None) -> vs.VideoNode:
    """Cross-fade clips."""
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, fade, clipa, clipb)
    clipa_clean, clipb_clean, clipa_fade_zone, clipb_fade_zone = _transition_clips(clipa, clipb, frames_)

    def _fade(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)
        if progress == 0:
            return clipa_fade_zone
        elif progress == 1:
            return clipb_fade_zone
        else:
            return core.std.Merge(
                clipa_fade_zone,
                clipb_fade_zone,
                weight=[float(progress)],
            )

    faded = core.std.FrameEval(clipa_fade_zone, _fade)

    return _return_combo(clipa_clean, faded, clipb_clean)


def poly_fade(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    exponent: int = 1,
) -> vs.VideoNode:
    """Cross-fade clips according to a curve.

    :param exponent: An integer in the range from 1-5 (inclusive)
        where 1 represents a parabolic curve, 2 represents a quartic curve,
        and higher powers more resembling an tight ease-in-out function with constant speed for most of the transition.

        An `exponent` of ``1`` is probably most useful,
        as higher exponents tend towards a constant speed and therefore are
        almost indistinguishable from a normal :func:`fade`.
    """
    if not (1 <= exponent <= 5):
        raise ValueError("poly_fade: exponent must be an int between 1 and 5 (inclusive)")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, fade, clipa, clipb)
    clipa_clean, clipb_clean, clipa_fade_zone, clipb_fade_zone = _transition_clips(clipa, clipb, frames_)

    def get_pos(x: Fraction) -> Fraction:
        """Returns position as a fractions.Fraction, based on a input percentage fractions.Fraction"""

        def _curve(y: Fraction) -> Fraction:
            return -(((2 * y - 1) ** (2 * exponent + 1)) / (4 * exponent + 2)) + y - Fraction(1, 2)

        return ((_curve(Fraction(1, 1)) - _curve(Fraction())) ** -1) * (_curve(x) - _curve(Fraction()))

    def _fade(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)
        if progress == 0:
            return clipa_fade_zone
        elif progress == 1:
            return clipb_fade_zone
        else:
            return core.std.Merge(clipa_fade_zone, clipb_fade_zone, weight=[float(get_pos(progress))])

    faded = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _fade)

    return _return_combo(clipa_clean, faded, clipb_clean)


def fade_to_black(src_clip: vs.VideoNode, frames: Optional[int] = None) -> vs.VideoNode:
    """Simple convenience function to :func:`fade` a clip to black.

    `frames` will be the number of frames consumed from the end of the `src_clip` during the transition.
    The first frame of the transition will be the first frame of the `src_clip`,
    while the last frame of the transition will be a pure black frame.

    If `frames` is not given, will fade to black over the entire duration of the `src_clip`.
    """
    if src_clip.format is None:
        raise ValueError("fade_to_black: `src_clip` must be a constant format VideoNode")
    black_clip = core.std.BlankClip(format=vs.GRAY8, length=frames, color=[0])
    if TYPE_CHECKING:
        assert black_clip.format is not None
        assert src_clip.format is not None
    black_clip_resized = black_clip.resize.Point(
        width=src_clip.width,
        height=src_clip.height,
        format=black_clip.format.replace(
            color_family=src_clip.format.color_family,
            sample_type=src_clip.format.sample_type,
            bits_per_sample=src_clip.format.bits_per_sample,
            subsampling_w=src_clip.format.subsampling_w,
            subsampling_h=src_clip.format.subsampling_h,
        ).id,
    )
    return fade(src_clip, black_clip_resized, frames)


def fade_from_black(src_clip: vs.VideoNode, frames: Optional[int] = None) -> vs.VideoNode:
    """Simple convenience function to :func:`fade` a clip into view from black.

    `frames` will be the number of frames consumed from the start of the `src_clip` during the transition.
    The first frame of the transition will be a pure black frame,
    while the last frame of the transition will be the last frame of the `src_clip`.

    If `frames` is not given, will fade in over the entire duration of the `src_clip`.
    """
    if src_clip.format is None:
        raise ValueError("fade_to_black: `src_clip` must be a constant format VideoNode")
    black_clip = core.std.BlankClip(format=vs.GRAY8, length=frames, color=[0])
    if TYPE_CHECKING:
        assert black_clip.format is not None
        assert src_clip.format is not None
    black_clip_resized = black_clip.resize.Point(
        width=src_clip.width,
        height=src_clip.height,
        format=black_clip.format.replace(
            color_family=src_clip.format.color_family,
            sample_type=src_clip.format.sample_type,
            bits_per_sample=src_clip.format.bits_per_sample,
            subsampling_w=src_clip.format.subsampling_w,
            subsampling_h=src_clip.format.subsampling_h,
        ).id,
    )
    return fade(black_clip_resized, src_clip, frames)


def wipe(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    direction: Direction = Direction.LEFT,
) -> vs.VideoNode:
    """A moving directional fade.

    Similar to a :func:`fade`, but with a moving mask.
    The `direction` will be the direction the fade progresses towards.
    (i.e. the second clip begins fading in from the **opposite** given direction,
    and the first clip begins fading out starting from the **opposite** given direction,
    progressing towards `direction`)

    Uses a pure white to black gradient for the fade.
    If possible, uses `numpy <https://pypi.org/project/numpy/>`_ to generate the mask.
    If the numpy module is not found, falls back to a slower
    and possibly less accurate approach using lists
    and the ``ctypes`` module for writing to a VapourSynth frame.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("wipe: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, wipe, clipa, clipb)
    clipa_clean, clipb_clean, clipa_wipe_zone, clipb_wipe_zone = _transition_clips(clipa, clipb, frames_)

    blank_clip = core.std.BlankClip(width=1 << 16, height=1, format=vs.GRAYS, color=[0.0], length=1)
    write_frame = blank_clip.get_frame(0).copy()
    try:
        import numpy as np

        def frame_writer(n, f):
            if n is not None:
                pass
            fout = f.copy()
            ptr = np.asarray(fout.get_write_array(0))
            ptr[0] = np.linspace(0, 1, 1 << 16)
            return fout

        mask = blank_clip.std.ModifyFrame([blank_clip], frame_writer)

    except (ImportError, ModuleNotFoundError):
        warn("wipe: numpy module not found, falling back to slower less accurate method", Warning)
        import ctypes

        ptr_type = ctypes.c_float * (1 << 16)
        vs_ptr = ctypes.cast(write_frame.get_write_ptr(0), ctypes.POINTER(ptr_type))
        float_lin_array = [i / ((1 << 16) - 1) for i in list(range(1 << 16))]
        float_Arr_ptr = ctypes.pointer(ptr_type(*float_lin_array))
        vs_ptr[0] = float_Arr_ptr[0]
        mask = blank_clip.std.ModifyFrame([blank_clip], lambda n, f: write_frame)

    mask = mask.grain.Add()
    if TYPE_CHECKING:
        assert mask.format is not None
        assert clipa.format is not None
    mask_horiz = mask.resize.Spline64(
        clipa.width,
        clipa.height,
        dither_type="error_diffusion",
        format=mask.format.replace(
            bits_per_sample=clipa.format.bits_per_sample, color_family=vs.GRAY, sample_type=clipa.format.sample_type
        ).id,
        matrix_in_s="rgb",
    )
    mask_vert = core.std.Transpose(mask).resize.Spline64(
        clipa.width,
        clipa.height,
        dither_type="error_diffusion",
        format=mask.format.replace(
            bits_per_sample=clipa.format.bits_per_sample, color_family=vs.GRAY, sample_type=clipa.format.sample_type
        ).id,
        matrix_in_s="rgb",
    )
    black_clip = core.std.BlankClip(mask_horiz, length=1, color=[0])
    white_clip = core.std.BlankClip(mask_horiz, length=1, color=[(1 << clipa.format.bits_per_sample) - 1])

    if direction in [Direction.LEFT, Direction.RIGHT]:
        stack = core.std.StackHorizontal([black_clip, mask_horiz, white_clip])
        w = mask_horiz.width

        if direction == Direction.LEFT:

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    width=w,
                    src_left=2 * w * n / (frames_ - 1),
                    src_width=w,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

        elif direction == Direction.RIGHT:
            stack = core.std.FlipHorizontal(stack)

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    width=w,
                    src_left=(2 * w) * (1 - n / (frames_ - 1)),
                    src_width=w,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

    elif direction in [Direction.UP, Direction.DOWN]:
        stack = core.std.StackVertical([black_clip, mask_vert, white_clip])
        h = mask_vert.height

        if direction == Direction.UP:

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    height=h,
                    src_top=2 * h * n / (frames_ - 1),
                    src_height=h,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

        elif direction == Direction.DOWN:
            stack = core.std.FlipVertical(stack)

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    height=h,
                    src_top=(2 * h) * (1 - n / (frames_ - 1)),
                    src_height=h,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

    wiped = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _wipe)

    return _return_combo(clipa_clean, wiped, clipb_clean)


def cube_rotate(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    direction: Direction = Direction.LEFT,
    exaggeration: int = 0,
) -> vs.VideoNode:
    """Mimics a cube face rotation by adjusting the speed at which the :func:`squeeze_expand` boundary moves.

    Cube face containing `clipa` rotates away from the viewer in projected 3-D space towards `direction`.

    :param exaggeration: An integer between 0 and 100 (inclusive)
        representing how much the effect of the cosine wave should be exaggerated.
        `0` corresponds to a mathematically correct projection of a 90 degree rotation offset by 45 degrees.
        `100` corresponds to a fitted cosine wave.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("cube_rotate: give a proper direction")
    if not (0 <= exaggeration <= 100):
        raise ValueError(f"cube_rotate: exaggeration {exaggeration} not between 0 and 100")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, cube_rotate, clipa, clipb)
    clipa_clean, clipb_clean, clipa_squeeze_zone, clipb_squeeze_zone = _transition_clips(clipa, clipb, frames_)

    def rotation(percentage: float) -> float:
        """Return a radian rotation based on `percentage` ranging from -pi/4 at 0% to -3pi/4 at 100%"""
        return (-math.pi / 2) * percentage - math.pi / 4

    def position(percentage: float, bias: int) -> float:
        """
        Return position of a rotated edge as a percentage
        0% at 0%, 23% at 25%, 50% at 50%, 77% at 75%, 100% at 100%
        """

        def _projection(x: float):
            """mathmatically correct projection"""
            return (-math.cos(rotation(x)) + (math.sqrt(2) / 2)) / math.sqrt(2)

        def _fitted(x: float):
            """fitted cosine wave to exaggerate the effects"""
            return -0.5 * math.cos(2 * rotation(x) + math.pi / 2) + 0.5

        if bias == 0:
            return round(_projection(percentage), 9)
        elif bias == 100:
            return round(_fitted(percentage), 9)
        else:
            fitted = (bias / 100) * _fitted(percentage)
            projection = ((100 - bias) / 100) * _projection(percentage)
            return round(fitted + projection, 9)

    if direction in [Direction.LEFT, Direction.RIGHT]:

        def _rotate(n: int):
            w_inc = math.floor(clipa.width * position(n / (frames_ - 1), exaggeration))
            w_dec = clipa.width - w_inc

            if w_dec == clipa.width:
                return clipa_squeeze_zone
            elif w_inc == clipa.width:
                return clipb_squeeze_zone
            else:
                clipa_squeezed = clipa_squeeze_zone.resize.Spline36(width=w_dec)
                clipb_squeezed = clipb_squeeze_zone.resize.Spline36(width=w_inc)
                if direction == Direction.LEFT:
                    return core.std.StackHorizontal([clipa_squeezed, clipb_squeezed])
                elif direction == Direction.RIGHT:
                    return core.std.StackHorizontal([clipb_squeezed, clipa_squeezed])

    elif direction in [Direction.UP, Direction.DOWN]:

        def _rotate(n: int):
            h_inc = math.floor(clipa.height * position(n / (frames_ - 1), exaggeration))
            h_dec = clipa.height - h_inc

            if h_dec == clipa.height:
                return clipa_squeeze_zone
            elif h_inc == clipa.height:
                return clipb_squeeze_zone
            else:
                clipa_squeezed = clipa_squeeze_zone.resize.Spline36(height=h_dec)
                clipb_squeezed = clipb_squeeze_zone.resize.Spline36(height=h_inc)
                if direction == Direction.UP:
                    return core.std.StackVertical([clipa_squeezed, clipb_squeezed])
                elif direction == Direction.DOWN:
                    return core.std.StackVertical([clipb_squeezed, clipa_squeezed])

    rotated = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _rotate)

    return _return_combo(clipa_clean, rotated, clipb_clean)


def linear_boundary(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    clipa_movement: MiscConstants,
    clipb_movement: MiscConstants,
    frames: Optional[int] = None,
    direction: Direction = Direction.LEFT,
) -> vs.VideoNode:
    """Generalized boundary moving function for a linear transition between two stacked clips.

    `clipa` can either slide out of view (having its size unchanged) or be squeezed to nothing from its original size.
    `clipb` can either slide into view (having its size unchanged) or be expanded from nothing to its full size.
    The boundary between the two clips moves towards `direction`.

    The parameter `clipa_movement` can be :attr:`MiscConstants.SLIDE` or :attr:`MiscConstants.SQUEEZE`.
    The parameter `clipb_movement` can be :attr:`MiscConstants.SLIDE` or :attr:`MiscConstants.EXPAND`.

    See :func:`push`, :func:`slide_expand`, :func:`squeeze_slide`, or :func:`squeeze_expand`
    for simpler aliases in the same form as most other linear, directional transitions.
    """
    if clipa_movement not in [MiscConstants.SLIDE, MiscConstants.SQUEEZE]:
        raise ValueError("linear_boundary: clipa_movement must be either a slide or a squeeze")
    if clipb_movement not in [MiscConstants.SLIDE, MiscConstants.EXPAND]:
        raise ValueError("linear_boundary: clipb_movement must be either a slide or an expand")
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("linear_boundary: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    check_for_subsampling = not (
        clipa_movement == clipb_movement == MiscConstants.SLIDE
    )  # only need subsampling check if resizing
    _check_clips(frames_, linear_boundary, clipa, clipb, subsampling=check_for_subsampling)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    if clipa_movement == clipb_movement == MiscConstants.SLIDE:
        w, h = clipa.width, clipa.height

        def _stack(clipa_: vs.VideoNode, clipb_: vs.VideoNode) -> vs.VideoNode:
            if direction == Direction.LEFT:
                return core.std.StackHorizontal([clipa_, clipb_])
            elif direction == Direction.RIGHT:
                return core.std.StackHorizontal([clipb_, clipa_])
            elif direction == Direction.UP:
                return core.std.StackVertical([clipa_, clipb_])
            elif direction == Direction.DOWN:
                return core.std.StackVertical([clipb_, clipa_])

        stack = _stack(clipa_t_zone, clipb_t_zone)

        def _push(n: int) -> vs.VideoNode:
            if direction == Direction.LEFT:
                return stack.resize.Spline36(width=w, src_left=w * n / (frames_ - 1), src_width=w)
            elif direction == Direction.RIGHT:
                return stack.resize.Spline36(width=w, src_left=w * (1 - n / (frames_ - 1)), src_width=w)
            elif direction == Direction.UP:
                return stack.resize.Spline36(height=h, src_top=h * n / (frames_ - 1), src_height=h)
            elif direction == Direction.DOWN:
                return stack.resize.Spline36(height=h, src_top=h * (1 - n / (frames_ - 1)), src_height=h)

        pushed = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _push)

        return _return_combo(clipa_clean, pushed, clipb_clean)

    elif clipa_movement == MiscConstants.SLIDE and clipb_movement == MiscConstants.EXPAND:

        def _slide_expand(n: int):
            scale = Fraction(n, frames_ - 1)

            if scale == 0:
                return clipa_t_zone
            elif scale == 1:
                return clipb_t_zone

            if direction in [Direction.LEFT, Direction.RIGHT]:
                w = math.floor(scale * clipa.width)

                if w == 0:
                    return clipa_t_zone

                if direction == Direction.LEFT:
                    stack = core.std.StackHorizontal([clipa_t_zone, clipb_t_zone.resize.Spline36(width=w)])
                    return stack.std.Crop(left=w)
                elif direction == Direction.RIGHT:
                    stack = core.std.StackHorizontal([clipb_t_zone.resize.Spline36(width=w), clipa_t_zone])
                    return stack.std.Crop(right=w)

            elif direction in [Direction.UP, Direction.DOWN]:
                h = math.floor(scale * clipa.height)

                if h == 0:
                    return clipa_t_zone

                if direction == Direction.UP:
                    stack = core.std.StackVertical([clipa_t_zone, clipb_t_zone.resize.Spline36(height=h)])
                    return stack.std.Crop(top=h)
                elif direction == Direction.DOWN:
                    stack = core.std.StackVertical([clipb_t_zone.resize.Spline36(height=h), clipa_t_zone])
                    return stack.std.Crop(bottom=h)

        slide_expanded = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _slide_expand)

        return _return_combo(clipa_clean, slide_expanded, clipb_clean)

    elif clipa_movement == MiscConstants.SQUEEZE and clipb_movement == MiscConstants.SLIDE:

        def _squeeze_slide(n: int):
            scale = 1 - Fraction(n, frames_ - 1)

            if scale == 1:
                return clipa_t_zone
            elif scale == 0:
                return clipb_t_zone

            if direction in [Direction.LEFT, Direction.RIGHT]:
                w = math.floor(scale * clipa.width)

                if w == 0:
                    return clipb_t_zone

                if direction == Direction.LEFT:
                    stack = core.std.StackHorizontal([clipa_t_zone.resize.Spline36(width=w), clipb_t_zone])
                    return stack.std.Crop(right=w)
                elif direction == Direction.RIGHT:
                    stack = core.std.StackHorizontal([clipb_t_zone, clipa_t_zone.resize.Spline36(width=w)])
                    return stack.std.Crop(left=w)

            elif direction in [Direction.UP, Direction.DOWN]:
                h = math.floor(scale * clipa.height)

                if h == 0:
                    return clipb_t_zone

                if direction == Direction.UP:
                    stack = core.std.StackVertical([clipa_t_zone.resize.Spline36(height=h), clipb_t_zone])
                    return stack.std.Crop(bottom=h)

                elif direction == Direction.DOWN:
                    stack = core.std.StackVertical([clipb_t_zone, clipa_t_zone.resize.Spline36(height=h)])
                    return stack.std.Crop(top=h)

        squeeze_slided = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _squeeze_slide)

        return _return_combo(clipa_clean, squeeze_slided, clipb_clean)

    elif clipa_movement == MiscConstants.SQUEEZE and clipb_movement == MiscConstants.EXPAND:

        def _squeeze_expand(n: int) -> vs.VideoNode:
            scale = Fraction(n, frames_ - 1)

            if scale == 0:
                return clipa_t_zone
            elif scale == 1:
                return clipb_t_zone

            if direction in [Direction.LEFT, Direction.RIGHT]:
                w_inc = math.floor(scale * clipa.width)
                w_dec = clipa.width - w_inc

                if w_inc == 0:
                    return clipa_t_zone

                if direction == Direction.LEFT:
                    return core.std.StackHorizontal(
                        [clipa_t_zone.resize.Spline36(width=w_dec), clipb_t_zone.resize.Spline36(width=w_inc)]
                    )
                elif direction == Direction.RIGHT:
                    return core.std.StackHorizontal(
                        [clipb_t_zone.resize.Spline36(width=w_inc), clipa_t_zone.resize.Spline36(width=w_dec)]
                    )

            elif direction in [Direction.UP, Direction.DOWN]:
                h_inc = math.floor(scale * clipa.width)
                h_dec = clipa.width - h_inc

                if h_inc == 0:
                    return clipa_t_zone

                if direction == Direction.UP:
                    return core.std.StackVertical(
                        [clipa_t_zone.resize.Spline36(width=h_dec), clipb_t_zone.resize.Spline36(width=h_inc)]
                    )
                elif direction == Direction.RIGHT:
                    return core.std.StackVertical(
                        [clipb_t_zone.resize.Spline36(width=h_inc), clipa_t_zone.resize.Spline36(width=h_dec)]
                    )

        squeeze_expanded = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _squeeze_expand)

        return _return_combo(clipa_clean, squeeze_expanded, clipb_clean)


def push(clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT):
    """Second clip pushes first clip off of the screen.

    The first clip moves off of the screen moving towards the given `direction`.

    Alias for :func:`linear_boundary` with ``clipa_movement=SLIDE`` and ``clipb_movement=SLIDE``.
    """
    return linear_boundary(clipa, clipb, MiscConstants.SLIDE, MiscConstants.SLIDE, frames=frames, direction=direction)


def slide_expand(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
):
    """First clip slides out of view, while second clip expands into view from nothing.

    `clipa` slides off of the screen towards `direction`.
    `clipb` expands into view from the opposite side of the given direction.

    Alias for :func:`linear_boundary` with ``clipa_movement=SLIDE`` and ``clipb_movement=EXPAND``.
    """
    return linear_boundary(clipa, clipb, MiscConstants.SLIDE, MiscConstants.EXPAND, frames=frames, direction=direction)


def squeeze_slide(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
):
    """First clip squeezes into nothing, while second clip slides into view.

    `clipa` gets compressed off of the screen towards `direction`.
    `clipb` slides into view from the opposite side of the given direction.

    Alias for :func:`linear_boundary` with ``clipa_movement=SQUEEZE`` and ``clipb_movement=SLIDE``.
    """
    return linear_boundary(clipa, clipb, MiscConstants.SQUEEZE, MiscConstants.SLIDE, frames=frames, direction=direction)


def squeeze_expand(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
):
    """First clip squeezes into nothing, while second clip expands into view from nothing.

    `clipa` gets compressed off of the screen towards `direction`.
    `clipb` expands into view from the opposite side of the given direction.

    Alias for :func:`linear_boundary` with ``clipa_movement=SQUEEZE`` and ``clipb_movement=EXPAND``.
    """
    return linear_boundary(
        clipa, clipb, MiscConstants.SQUEEZE, MiscConstants.EXPAND, frames=frames, direction=direction
    )


def cover(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
) -> vs.VideoNode:
    """Second clip slides in and covers the first clip which stays in place.

    `clipb` slides into frame towards `direction` covering `clipa`.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("cover: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, cover, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _cover(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)
        w = math.floor(progress * clipa.width)
        h = math.floor(progress * clipa.height)

        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone

        if direction in [Direction.LEFT, Direction.RIGHT]:
            if w == 0:
                return clipa_t_zone
        elif direction in [Direction.UP, Direction.DOWN]:
            if h == 0:
                return clipa_t_zone

        if direction == Direction.LEFT:
            cropped_a = clipa_t_zone.std.Crop(right=w)
            stack = core.std.StackHorizontal([cropped_a, clipb_t_zone])
            return stack.resize.Spline36(width=clipa.width, src_width=clipa.width)
        elif direction == Direction.RIGHT:
            cropped_a = clipa_t_zone.std.Crop(left=w)
            stack = core.std.StackHorizontal([clipb_t_zone, cropped_a])
            return stack.resize.Spline36(width=clipa.width, src_left=clipa.width - w, src_width=clipa.width)
        elif direction == Direction.UP:
            cropped_a = clipa_t_zone.std.Crop(bottom=h)
            stack = core.std.StackVertical([cropped_a, clipb_t_zone])
            return stack.resize.Spline36(height=clipa.height, src_height=clipa.height)
        elif direction == Direction.DOWN:
            cropped_a = clipa_t_zone.std.Crop(top=h)
            stack = core.std.StackVertical([clipb_t_zone, cropped_a])
            return stack.resize.Spline36(height=clipa.height, src_top=clipa.height - h, src_height=clipa.height)

    covered = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _cover)

    return _return_combo(clipa_clean, covered, clipb_clean)


def reveal(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
) -> vs.VideoNode:
    """First clip slides out of view exposing second clip that stays in place.

    `clipa` slides out of frame towards `direction` revealing `clipb`.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("reveal: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, reveal, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _reveal(n: int) -> vs.VideoNode:
        progress = 1 - Fraction(n, frames_ - 1)
        w = math.floor(progress * clipa.width)
        h = math.floor(progress * clipa.height)

        if progress == 1:
            return clipa_t_zone
        elif progress == 0:
            return clipb_t_zone

        if direction in [Direction.LEFT, Direction.RIGHT]:
            if w == 0:
                return clipb_t_zone
        elif direction in [Direction.UP, Direction.DOWN]:
            if h == 0:
                return clipb_t_zone

        if direction == Direction.LEFT:
            cropped_b = clipb_t_zone.std.Crop(left=w)
            stack = core.std.StackHorizontal([clipa_t_zone, cropped_b])
            return stack.resize.Spline36(width=clipa.width, src_width=clipa.width, src_left=clipa.width - w)
        elif direction == Direction.RIGHT:
            cropped_b = clipb_t_zone.std.Crop(right=w)
            stack = core.std.StackHorizontal([cropped_b, clipa_t_zone])
            return stack.resize.Spline36(width=clipa.width, src_width=clipa.width)
        elif direction == Direction.UP:
            cropped_b = clipb_t_zone.std.Crop(top=h)
            stack = core.std.StackVertical([clipa_t_zone, cropped_b])
            return stack.resize.Spline36(height=clipa.height, src_height=clipa.height, src_top=clipa.height - h)
        elif direction == Direction.DOWN:
            cropped_b = clipb_t_zone.std.Crop(bottom=h)
            stack = core.std.StackVertical([cropped_b, clipa_t_zone])
            return stack.resize.Spline36(height=clipa.height, src_height=clipa.height)

    covered = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _reveal)

    return _return_combo(clipa_clean, covered, clipb_clean)


def curtain_cover(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, axis: Direction = Direction.HORIZONTAL
) -> vs.VideoNode:
    """Second clip comes into view from both directions split along the given axis covering the first clip in place.

    `clipb` splits and moves inwards along the given `axis`.

    If `axis` is given as :attr:`Direction.HORIZONTAL`, the clips must have an even integer width.
    If `axis` is given as :attr:`Direction.VERTICAL`, the clips must have an even integer height.
    """
    if axis not in [Direction.HORIZONTAL, Direction.VERTICAL]:
        raise ValueError("curtain_cover: give a proper axis")
    if axis == Direction.HORIZONTAL and clipa.width % 2:
        raise ValueError("curtain_cover: for horizontal reveal, input clips must have an even width")
    elif axis == Direction.VERTICAL and clipa.height % 2:
        raise ValueError("curtain_cover: for vertical reveal, input clips must have an even height")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, curtain_cover, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _curtain_cover(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)

        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone

        if axis == Direction.HORIZONTAL:
            w = round(float(clipa.width * progress / 2)) * 2

            if w == 0:
                return clipa_t_zone
            elif w == clipa.width:
                return clipb_t_zone

            clipb_left = clipb_t_zone.std.Crop(right=clipa.width // 2)
            clipb_right = clipb_t_zone.std.Crop(left=clipa.width // 2)

            clipb_left = clipb_left.std.Crop(left=clipb_left.width - w // 2)
            clipb_right = clipb_right.std.Crop(right=clipb_right.width - w // 2)

            clipa_cropped = clipa_t_zone.std.Crop(left=clipb_left.width, right=clipb_right.width)
            return core.std.StackHorizontal([clipb_left, clipa_cropped, clipb_right])

        elif axis == Direction.VERTICAL:
            h = round(float(clipa.height * progress / 2)) * 2

            if h == 0:
                return clipa_t_zone
            elif h == clipa.height:
                return clipb_t_zone

            clipb_top = clipb_t_zone.std.Crop(bottom=clipa.height // 2)
            clipb_bottom = clipb_t_zone.std.Crop(top=clipa.height // 2)

            clipb_top = clipb_top.std.Crop(top=clipb_top.height - h // 2)
            clipb_bottom = clipb_bottom.std.Crop(bottom=clipb_bottom.height - h // 2)

            clipa_cropped = clipa_t_zone.std.Crop(top=clipb_top.height, bottom=clipb_bottom.height)
            return core.std.StackVertical([clipb_top, clipa_cropped, clipb_bottom])

    curtain_covered = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _curtain_cover)

    return _return_combo(clipa_clean, curtain_covered, clipb_clean)


def curtain_reveal(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, axis: Direction = Direction.HORIZONTAL
) -> vs.VideoNode:
    """First clip splits apart to reveal the second clip in place.

    `clipa` splits and moves apart along the given `axis`.

    If `axis` is given as :attr:`Direction.HORIZONTAL`, the clips must have an even integer width.
    If `axis` is given as :attr:`Direction.VERTICAL`, the clips must have an even integer height.
    """
    if axis not in [Direction.HORIZONTAL, Direction.VERTICAL]:
        raise ValueError("curtain_reveal: give a proper axis")
    if axis == Direction.HORIZONTAL and clipa.width % 2:
        raise ValueError("curtain_reveal: for horizontal reveal, input clips must have an even width")
    elif axis == Direction.VERTICAL and clipa.height % 2:
        raise ValueError("curtain_reveal: for vertical reveal, input clips must have an even height")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, curtain_reveal, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _curtain_reveal(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)

        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone

        if axis == Direction.HORIZONTAL:
            w = round(float(clipa.width * progress / 2)) * 2

            if w == 0:
                return clipa_t_zone
            elif w == clipa.width:
                return clipb_t_zone

            clipa_left = clipa_t_zone.std.Crop(right=clipa.width // 2)
            clipa_right = clipa_t_zone.std.Crop(left=clipa.width // 2)

            clipa_left = clipa_left.std.Crop(left=w // 2)
            clipa_right = clipa_right.std.Crop(right=w // 2)

            clipb_cropped = clipb_t_zone.std.Crop(left=clipa_left.width, right=clipa_right.width)
            return core.std.StackHorizontal([clipa_left, clipb_cropped, clipa_right])

        elif axis == Direction.VERTICAL:
            h = round(float(clipa.height * progress / 2)) * 2

            if h == 0:
                return clipa_t_zone
            elif h == clipa.height:
                return clipb_t_zone

            clipa_top = clipa_t_zone.std.Crop(bottom=clipa.height // 2)
            clipa_bottom = clipa_t_zone.std.Crop(top=clipa.height // 2)

            clipa_top = clipa_top.std.Crop(top=h // 2)
            clipa_bottom = clipa_bottom.std.Crop(bottom=h // 2)

            clipb_cropped = clipb_t_zone.std.Crop(top=clipa_top.height, bottom=clipa_bottom.height)
            return core.std.StackVertical([clipa_top, clipb_cropped, clipa_bottom])

    curtain_revealed = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _curtain_reveal)

    return _return_combo(clipa_clean, curtain_revealed, clipb_clean)


def pixellate(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    lowest_target_w: Optional[int] = 2,
    lowest_target_h: Optional[int] = 2,
) -> vs.VideoNode:
    """Pixellate using rescales and aggressively fade at the center.

    For large clips (width `x` height), the effect might not be too noticeable
    until the transition is near the middle point.
    This is due to bicubic downscales and point re-upscales
    at very high percentages of the original dimensions
    not being noticeably different.

    Due to the way pixellation progress is calculated,
    the transition `must` be at least 4 frames long.

    Longer transitions paired with larger target dimensions
    will cause the pixellation effect to appear to pause towards the center of the transition.

    :param lowest_target_w: An integer that determines the minimum width target to downscale to.
        By specifying ``None``, or by specifying the width of the source clips, the clips will not be scaled in the
        `x` or width direction, making this only pixellate vertically.

    :param lowest_target_h: An integer that determines the minimum height target to downscale to.
        By specifying ``None``, or by specifying the height of the source clips, the clips will not be scaled in the
        `y` or height direction, making this only pixellate horizontally.
    """
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if frames_ < 4:
        raise ValueError("pixellate: transition must be at least 4 frames long")

    if lowest_target_w is None:
        lowest_target_w = clipa.width
    if lowest_target_h is None:
        lowest_target_h = clipa.height
    if lowest_target_w < 1 or lowest_target_w > clipa.width:
        raise ValueError("pixellate: `lowest_target_w` must be at least one and at most the width of the source clips")
    if lowest_target_h < 1 or lowest_target_h > clipa.height:
        raise ValueError("pixellate: `lowest_target_h` must be at least one and at most the height of the source clips")
    if lowest_target_w == clipa.width and lowest_target_h == clipa.height:
        raise ValueError("pixellate: at least one target dimension must be lower than the source dimensions")
    lowest_target_w_ = lowest_target_w
    lowest_target_h_ = lowest_target_h

    if TYPE_CHECKING:
        assert isinstance(frames_, int)
        assert isinstance(lowest_target_w_, int)
        assert isinstance(lowest_target_h_, int)
    _check_clips(frames_, pixellate, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _pixellate(n: int):
        if iseven(frames_):
            center = (frames_ - 1) / 2
            if n < center:
                progress_a = Fraction(math.floor(center) - n, math.floor(center)) ** 2

                target_w_a = max([lowest_target_w_, math.floor(progress_a * clipa.width)])
                target_h_a = max([lowest_target_h_, math.floor(progress_a * clipa.height)])

                target_w_b = lowest_target_w_
                target_h_b = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == math.floor(center):
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [1 / 3])
                else:
                    return clipa_pixellated

            else:
                progress_b = Fraction(n - math.ceil(center), math.floor(center)) ** 2

                target_w_b = max([lowest_target_w_, math.floor(progress_b * clipa.width)])
                target_h_b = max([lowest_target_h_, math.floor(progress_b * clipa.height)])

                target_w_a = lowest_target_w_
                target_h_a = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == math.ceil(center):
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [2 / 3])
                else:
                    return clipb_pixellated
        else:
            center = (frames_ - 1) // 2
            if n < center:
                progress_a = Fraction(center - n, center) ** 2

                target_w_a = max([lowest_target_w_, math.floor(progress_a * clipa.width)])
                target_h_a = max([lowest_target_h_, math.floor(progress_a * clipa.height)])

                target_w_b = lowest_target_w_
                target_h_b = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == center - 1:
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [1 / 4])
                else:
                    return clipa_pixellated

            elif n == center:
                target_w_a = target_w_b = lowest_target_w_
                target_h_a = target_h_b = lowest_target_h_
                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                return core.std.Merge(clipa_pixellated, clipb_pixellated, [1 / 2])

            else:
                progress_b = Fraction(n - center, center) ** 2

                target_w_b = max([lowest_target_w_, math.floor(progress_b * clipa.width)])
                target_h_b = max([lowest_target_h_, math.floor(progress_b * clipa.height)])

                target_w_a = lowest_target_w_
                target_h_a = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == center + 1:
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [3 / 4])
                else:
                    return clipb_pixellated

    pixellated = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _pixellate)

    return _return_combo(clipa_clean, pixellated, clipb_clean)


def peel(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
) -> vs.VideoNode:
    """First clip peels away revealing the second clip beneath.

    Both clips remain in place during the transition. The boundary between clips moves towards `direction`.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("peel: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, peel, clipa, clipb, subsampling=True)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _peel(n: int):
        progress = Fraction(n, frames_ - 1)
        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone
        else:
            if direction in [Direction.LEFT, Direction.RIGHT]:
                clipb_w = math.floor(clipa.width * progress)
                clipa_w = clipa.width - clipb_w

                if clipb_w == 0:
                    return clipa_t_zone
                elif clipb_w == clipa.width:
                    return clipb_t_zone
                else:
                    if direction == Direction.LEFT:
                        clipa_cropped = clipa_t_zone.std.Crop(right=clipb_w)
                        clipb_cropped = clipb_t_zone.std.Crop(left=clipa_w)
                        return core.std.StackHorizontal([clipa_cropped, clipb_cropped])
                    elif direction == Direction.RIGHT:
                        clipa_cropped = clipa_t_zone.std.Crop(left=clipb_w)
                        clipb_cropped = clipb_t_zone.std.Crop(right=clipa_w)
                        return core.std.StackHorizontal([clipb_cropped, clipa_cropped])
            elif direction in [Direction.UP, Direction.DOWN]:
                clipb_h = math.floor(clipa.height * progress)
                clipa_h = clipa.height - clipb_h

                if clipb_h == 0:
                    return clipa_t_zone
                elif clipb_h == clipa.width:
                    return clipb_t_zone
                else:
                    if direction == Direction.UP:
                        clipa_cropped = clipa_t_zone.std.Crop(bottom=clipb_h)
                        clipb_cropped = clipb_t_zone.std.Crop(top=clipa_h)
                        return core.std.StackVertical([clipa_cropped, clipb_cropped])
                    elif direction == Direction.DOWN:
                        clipa_cropped = clipa_t_zone.std.Crop(top=clipb_h)
                        clipb_cropped = clipb_t_zone.std.Crop(bottom=clipa_h)
                        return core.std.StackVertical([clipb_cropped, clipa_cropped])

    peeled = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _peel)

    return _return_combo(clipa_clean, peeled, clipb_clean)


def round_to(f: Fraction, n: int) -> int:
    """Rounds a fractional value to the nearest `n`, rounding half up and never returning less than `n`"""
    if n < 1:
        raise ValueError("round_to: `n` must be an integer greater than 0")
    if n == 1:
        return max(1, round(float(f)))
    else:
        return max(n, round(float(f / n)) * n)


def isodd(value: int) -> bool:
    return bool(value % 2)


def iseven(value: int) -> bool:
    return not bool(value % 2)

"""Powerpoint-like transitions implemented in VapourSynth."""
__all__ = [
    "cover",
    "cube_rotate",
    "curtain_cover",
    "curtain_reveal",
    "fade",
    "fade_from_black",
    "fade_to_black",
    "linear_boundary",
    "poly_fade",
    "push",
    "reveal",
    "slide_expand",
    "squeeze_expand",
    "squeeze_slide",
    "wipe",
]

import enum
import math
from fractions import Fraction
from typing import Callable, Optional, Tuple, TYPE_CHECKING
from warnings import simplefilter, warn

import vapoursynth as vs

try:
    from ._metadata import __author__, __date__, __version__  # type: ignore
except (ImportError, ModuleNotFoundError):
    __author__ = __date__ = __version__ = "unknown (portable version)"  # type: ignore

simplefilter("always")

core = vs.core


class Direction(str, enum.Enum):
    """Direction enumeration.

    Members can be simply referenced by their names,
    i.e. ``vs_transitions.LEFT`` instead of ``vs_transitions.Direction.LEFT``.
    """

    LEFT = "left"
    RIGHT = "right"
    UP = "up"
    DOWN = "down"
    HORIZONTAL = "horizontal"
    VERTICAL = "vertical"


LEFT = Direction.LEFT
RIGHT = Direction.RIGHT
UP = Direction.UP
DOWN = Direction.DOWN
HORIZONTAL = Direction.HORIZONTAL
VERTICAL = Direction.VERTICAL
__all__ += list(Direction.__members__)


class MiscConstants(str, enum.Enum):
    """Miscellanious enumeration for some functions.

    Members can be simply referenced by their names,
    i.e. ``vs_transitions.SLIDE`` instead of ``vs_transitions.MiscConstants.SLIDE``.
    """

    SLIDE = "slide"
    SQUEEZE = "squeeze"
    EXPAND = "expand"


SLIDE = MiscConstants.SLIDE
SQUEEZE = MiscConstants.SQUEEZE
EXPAND = MiscConstants.EXPAND
__all__ += list(MiscConstants.__members__)


def _check_clips(frames: int, caller: Callable, *clips: vs.VideoNode, **kwargs) -> None:
    """General checker for clip formats, resolutions, length, and other keywords.

    Possible kwargs:
        'subsampling': checks that all clips have 444 subsampling for resize purposes
    """
    if frames <= 0:
        raise ValueError(f"{caller.__name__}: `frames` cannot be less than 1")
    same_check = set()
    for clip in clips:
        if clip.format is None:
            raise ValueError(f"{caller.__name__}: all clips must be constant-format")
        if 0 in (clip.width, clip.height):
            raise ValueError(f"{caller.__name__}: all clips must be constant-resolution")
        if clip.num_frames < frames:
            raise ValueError(f"{caller.__name__}: all clips must have at least {frames} frames")
        same_check.add((clip.format.id, clip.width, clip.height))

        if kwargs:
            if ("subsampling" in kwargs) and kwargs["subsampling"]:
                if clip.format.subsampling_w != 0 or clip.format.subsampling_h != 0:
                    raise ValueError(
                        f"{caller.__name__}: all clips must have 444 chroma subsampling for a non-mod2 resize"
                    )

    if len(same_check) > 1:
        raise ValueError(f"{caller.__name__}: all clips must be same format and resolution")


def _return_combo(
    clip1: Optional[vs.VideoNode],
    clip_middle: vs.VideoNode,
    clip2: Optional[vs.VideoNode],
) -> vs.VideoNode:
    """Prevents splicing empty clips.

    :param clip1:        optional start clip
    :param clip_middle:  mandatory middle clip
    :param clip2:        optional ending clip
    :return: splice of existing clips in order
    """
    if clip1 is not None and clip2 is not None:
        return clip1 + clip_middle + clip2
    elif clip1 is not None and clip2 is None:
        return clip1 + clip_middle
    elif clip1 is None and clip2 is not None:
        return clip_middle + clip2
    elif clip1 is None and clip2 is None:
        return clip_middle


def _transition_clips(
    clip1: vs.VideoNode, clip2: vs.VideoNode, frames: int
) -> Tuple[Optional[vs.VideoNode], Optional[vs.VideoNode], vs.VideoNode, vs.VideoNode]:
    """Returns clean (non-transition) and transition sections of the given clips based on frames."""
    if clip1.num_frames == frames:
        clip1_t_zone = clip1
        clip1_clean = None
    else:
        clip1_t_zone = clip1[-frames:]
        clip1_clean = clip1[:-frames]

    if clip2.num_frames == frames:
        clip2_t_zone = clip2
        clip2_clean = None
    else:
        clip2_t_zone = clip2[:frames]
        clip2_clean = clip2[frames:]

    return clip1_clean, clip2_clean, clip1_t_zone, clip2_t_zone


def fade(clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None) -> vs.VideoNode:
    """Cross-fade clips."""
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, fade, clipa, clipb)
    clipa_clean, clipb_clean, clipa_fade_zone, clipb_fade_zone = _transition_clips(clipa, clipb, frames_)

    def _fade(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)
        if progress == 0:
            return clipa_fade_zone
        elif progress == 1:
            return clipb_fade_zone
        else:
            return core.std.Merge(
                clipa_fade_zone,
                clipb_fade_zone,
                weight=[float(progress)],
            )

    faded = core.std.FrameEval(clipa_fade_zone, _fade)

    return _return_combo(clipa_clean, faded, clipb_clean)


def poly_fade(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    exponent: int = 1,
) -> vs.VideoNode:
    """Cross-fade clips according to a curve.

    :param exponent: An integer in the range from 1-5 (inclusive)
        where 1 represents a parabolic curve, 2 represents a quartic curve,
        and higher powers more resembling an tight ease-in-out function with constant speed for most of the transition.

        An `exponent` of ``1`` is probably most useful,
        as higher exponents tend towards a constant speed and therefore are
        almost indistinguishable from a normal :func:`fade`.
    """
    if not (1 <= exponent <= 5):
        raise ValueError("poly_fade: exponent must be an int between 1 and 5 (inclusive)")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, fade, clipa, clipb)
    clipa_clean, clipb_clean, clipa_fade_zone, clipb_fade_zone = _transition_clips(clipa, clipb, frames_)

    def get_pos(x: Fraction) -> Fraction:
        """Returns position as a fractions.Fraction, based on a input percentage fractions.Fraction"""

        def _curve(y: Fraction) -> Fraction:
            return -(((2 * y - 1) ** (2 * exponent + 1)) / (4 * exponent + 2)) + y - Fraction(1, 2)

        return ((_curve(Fraction(1, 1)) - _curve(Fraction())) ** -1) * (_curve(x) - _curve(Fraction()))

    def _fade(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)
        if progress == 0:
            return clipa_fade_zone
        elif progress == 1:
            return clipb_fade_zone
        else:
            return core.std.Merge(clipa_fade_zone, clipb_fade_zone, weight=[float(get_pos(progress))])

    faded = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _fade)

    return _return_combo(clipa_clean, faded, clipb_clean)


def fade_to_black(src_clip: vs.VideoNode, frames: Optional[int] = None) -> vs.VideoNode:
    """Simple convenience function to :func:`fade` a clip to black.

    `frames` will be the number of frames consumed from the end of the `src_clip` during the transition.
    The first frame of the transition will be the first frame of the `src_clip`,
    while the last frame of the transition will be a pure black frame.

    If `frames` is not given, will fade to black over the entire duration of the `src_clip`.
    """
    if src_clip.format is None:
        raise ValueError("fade_to_black: `src_clip` must be a constant format VideoNode")
    black_clip = core.std.BlankClip(format=vs.GRAY8, length=frames, color=[0])
    if TYPE_CHECKING:
        assert black_clip.format is not None
        assert src_clip.format is not None
    black_clip_resized = black_clip.resize.Point(
        width=src_clip.width,
        height=src_clip.height,
        format=black_clip.format.replace(
            color_family=src_clip.format.color_family,
            sample_type=src_clip.format.sample_type,
            bits_per_sample=src_clip.format.bits_per_sample,
            subsampling_w=src_clip.format.subsampling_w,
            subsampling_h=src_clip.format.subsampling_h,
        ).id,
    )
    return fade(src_clip, black_clip_resized, frames)


def fade_from_black(src_clip: vs.VideoNode, frames: Optional[int] = None) -> vs.VideoNode:
    """Simple convenience function to :func:`fade` a clip into view from black.

    `frames` will be the number of frames consumed from the start of the `src_clip` during the transition.
    The first frame of the transition will be a pure black frame,
    while the last frame of the transition will be the last frame of the `src_clip`.

    If `frames` is not given, will fade in over the entire duration of the `src_clip`.
    """
    if src_clip.format is None:
        raise ValueError("fade_to_black: `src_clip` must be a constant format VideoNode")
    black_clip = core.std.BlankClip(format=vs.GRAY8, length=frames, color=[0])
    if TYPE_CHECKING:
        assert black_clip.format is not None
        assert src_clip.format is not None
    black_clip_resized = black_clip.resize.Point(
        width=src_clip.width,
        height=src_clip.height,
        format=black_clip.format.replace(
            color_family=src_clip.format.color_family,
            sample_type=src_clip.format.sample_type,
            bits_per_sample=src_clip.format.bits_per_sample,
            subsampling_w=src_clip.format.subsampling_w,
            subsampling_h=src_clip.format.subsampling_h,
        ).id,
    )
    return fade(black_clip_resized, src_clip, frames)


def wipe(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    direction: Direction = Direction.LEFT,
) -> vs.VideoNode:
    """A moving directional fade.

    Similar to a :func:`fade`, but with a moving mask.
    The `direction` will be the direction the fade progresses towards.
    (i.e. the second clip begins fading in from the **opposite** given direction,
    and the first clip begins fading out starting from the **opposite** given direction,
    progressing towards `direction`)

    Uses a pure white to black gradient for the fade.
    If possible, uses `numpy <https://pypi.org/project/numpy/>`_ to generate the mask.
    If the numpy module is not found, falls back to a slower
    and possibly less accurate approach using lists
    and the ``ctypes`` module for writing to a VapourSynth frame.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("wipe: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, wipe, clipa, clipb)
    clipa_clean, clipb_clean, clipa_wipe_zone, clipb_wipe_zone = _transition_clips(clipa, clipb, frames_)

    blank_clip = core.std.BlankClip(width=1 << 16, height=1, format=vs.GRAYS, color=[0.0], length=1)
    write_frame = blank_clip.get_frame(0).copy()
    try:
        import numpy as np

        def frame_writer(n, f):
            if n is not None:
                pass
            fout = f.copy()
            ptr = np.asarray(fout.get_write_array(0))
            ptr[0] = np.linspace(0, 1, 1 << 16)
            return fout

        mask = blank_clip.std.ModifyFrame([blank_clip], frame_writer)

    except (ImportError, ModuleNotFoundError):
        warn("wipe: numpy module not found, falling back to slower less accurate method", Warning)
        import ctypes

        ptr_type = ctypes.c_float * (1 << 16)
        vs_ptr = ctypes.cast(write_frame.get_write_ptr(0), ctypes.POINTER(ptr_type))
        float_lin_array = [i / ((1 << 16) - 1) for i in list(range(1 << 16))]
        float_Arr_ptr = ctypes.pointer(ptr_type(*float_lin_array))
        vs_ptr[0] = float_Arr_ptr[0]
        mask = blank_clip.std.ModifyFrame([blank_clip], lambda n, f: write_frame)

    mask = mask.grain.Add()
    if TYPE_CHECKING:
        assert mask.format is not None
        assert clipa.format is not None
    mask_horiz = mask.resize.Spline64(
        clipa.width,
        clipa.height,
        dither_type="error_diffusion",
        format=mask.format.replace(
            bits_per_sample=clipa.format.bits_per_sample, color_family=vs.GRAY, sample_type=clipa.format.sample_type
        ).id,
        matrix_in_s="rgb",
    )
    mask_vert = core.std.Transpose(mask).resize.Spline64(
        clipa.width,
        clipa.height,
        dither_type="error_diffusion",
        format=mask.format.replace(
            bits_per_sample=clipa.format.bits_per_sample, color_family=vs.GRAY, sample_type=clipa.format.sample_type
        ).id,
        matrix_in_s="rgb",
    )
    black_clip = core.std.BlankClip(mask_horiz, length=1, color=[0])
    white_clip = core.std.BlankClip(mask_horiz, length=1, color=[(1 << clipa.format.bits_per_sample) - 1])

    if direction in [Direction.LEFT, Direction.RIGHT]:
        stack = core.std.StackHorizontal([black_clip, mask_horiz, white_clip])
        w = mask_horiz.width

        if direction == Direction.LEFT:

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    width=w,
                    src_left=2 * w * n / (frames_ - 1),
                    src_width=w,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

        elif direction == Direction.RIGHT:
            stack = core.std.FlipHorizontal(stack)

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    width=w,
                    src_left=(2 * w) * (1 - n / (frames_ - 1)),
                    src_width=w,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

    elif direction in [Direction.UP, Direction.DOWN]:
        stack = core.std.StackVertical([black_clip, mask_vert, white_clip])
        h = mask_vert.height

        if direction == Direction.UP:

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    height=h,
                    src_top=2 * h * n / (frames_ - 1),
                    src_height=h,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

        elif direction == Direction.DOWN:
            stack = core.std.FlipVertical(stack)

            def _wipe(n: int) -> vs.VideoNode:
                stack_ = stack.resize.Spline36(
                    height=h,
                    src_top=(2 * h) * (1 - n / (frames_ - 1)),
                    src_height=h,
                )
                return core.std.MaskedMerge(clipa_wipe_zone, clipb_wipe_zone, stack_)

    wiped = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _wipe)

    return _return_combo(clipa_clean, wiped, clipb_clean)


def cube_rotate(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    direction: Direction = Direction.LEFT,
    exaggeration: int = 0,
) -> vs.VideoNode:
    """Mimics a cube face rotation by adjusting the speed at which the :func:`squeeze_expand` boundary moves.

    Cube face containing `clipa` rotates away from the viewer in projected 3-D space towards `direction`.

    :param exaggeration: An integer between 0 and 100 (inclusive)
        representing how much the effect of the cosine wave should be exaggerated.
        `0` corresponds to a mathematically correct projection of a 90 degree rotation offset by 45 degrees.
        `100` corresponds to a fitted cosine wave.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("cube_rotate: give a proper direction")
    if not (0 <= exaggeration <= 100):
        raise ValueError(f"cube_rotate: exaggeration {exaggeration} not between 0 and 100")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, cube_rotate, clipa, clipb)
    clipa_clean, clipb_clean, clipa_squeeze_zone, clipb_squeeze_zone = _transition_clips(clipa, clipb, frames_)

    def rotation(percentage: float) -> float:
        """Return a radian rotation based on `percentage` ranging from -pi/4 at 0% to -3pi/4 at 100%"""
        return (-math.pi / 2) * percentage - math.pi / 4

    def position(percentage: float, bias: int) -> float:
        """
        Return position of a rotated edge as a percentage
        0% at 0%, 23% at 25%, 50% at 50%, 77% at 75%, 100% at 100%
        """

        def _projection(x: float):
            """mathmatically correct projection"""
            return (-math.cos(rotation(x)) + (math.sqrt(2) / 2)) / math.sqrt(2)

        def _fitted(x: float):
            """fitted cosine wave to exaggerate the effects"""
            return -0.5 * math.cos(2 * rotation(x) + math.pi / 2) + 0.5

        if bias == 0:
            return round(_projection(percentage), 9)
        elif bias == 100:
            return round(_fitted(percentage), 9)
        else:
            fitted = (bias / 100) * _fitted(percentage)
            projection = ((100 - bias) / 100) * _projection(percentage)
            return round(fitted + projection, 9)

    if direction in [Direction.LEFT, Direction.RIGHT]:

        def _rotate(n: int):
            w_inc = math.floor(clipa.width * position(n / (frames_ - 1), exaggeration))
            w_dec = clipa.width - w_inc

            if w_dec == clipa.width:
                return clipa_squeeze_zone
            elif w_inc == clipa.width:
                return clipb_squeeze_zone
            else:
                clipa_squeezed = clipa_squeeze_zone.resize.Spline36(width=w_dec)
                clipb_squeezed = clipb_squeeze_zone.resize.Spline36(width=w_inc)
                if direction == Direction.LEFT:
                    return core.std.StackHorizontal([clipa_squeezed, clipb_squeezed])
                elif direction == Direction.RIGHT:
                    return core.std.StackHorizontal([clipb_squeezed, clipa_squeezed])

    elif direction in [Direction.UP, Direction.DOWN]:

        def _rotate(n: int):
            h_inc = math.floor(clipa.height * position(n / (frames_ - 1), exaggeration))
            h_dec = clipa.height - h_inc

            if h_dec == clipa.height:
                return clipa_squeeze_zone
            elif h_inc == clipa.height:
                return clipb_squeeze_zone
            else:
                clipa_squeezed = clipa_squeeze_zone.resize.Spline36(height=h_dec)
                clipb_squeezed = clipb_squeeze_zone.resize.Spline36(height=h_inc)
                if direction == Direction.UP:
                    return core.std.StackVertical([clipa_squeezed, clipb_squeezed])
                elif direction == Direction.DOWN:
                    return core.std.StackVertical([clipb_squeezed, clipa_squeezed])

    rotated = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _rotate)

    return _return_combo(clipa_clean, rotated, clipb_clean)


def linear_boundary(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    clipa_movement: MiscConstants,
    clipb_movement: MiscConstants,
    frames: Optional[int] = None,
    direction: Direction = Direction.LEFT,
) -> vs.VideoNode:
    """Generalized boundary moving function for a linear transition between two stacked clips.

    `clipa` can either slide out of view (having its size unchanged) or be squeezed to nothing from its original size.
    `clipb` can either slide into view (having its size unchanged) or be expanded from nothing to its full size.
    The boundary between the two clips moves towards `direction`.

    The parameter `clipa_movement` can be :attr:`MiscConstants.SLIDE` or :attr:`MiscConstants.SQUEEZE`.
    The parameter `clipb_movement` can be :attr:`MiscConstants.SLIDE` or :attr:`MiscConstants.EXPAND`.

    See :func:`push`, :func:`slide_expand`, :func:`squeeze_slide`, or :func:`squeeze_expand`
    for simpler aliases in the same form as most other linear, directional transitions.
    """
    if clipa_movement not in [MiscConstants.SLIDE, MiscConstants.SQUEEZE]:
        raise ValueError("linear_boundary: clipa_movement must be either a slide or a squeeze")
    if clipb_movement not in [MiscConstants.SLIDE, MiscConstants.EXPAND]:
        raise ValueError("linear_boundary: clipb_movement must be either a slide or an expand")
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("linear_boundary: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    check_for_subsampling = not (
        clipa_movement == clipb_movement == MiscConstants.SLIDE
    )  # only need subsampling check if resizing
    _check_clips(frames_, linear_boundary, clipa, clipb, subsampling=check_for_subsampling)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    if clipa_movement == clipb_movement == MiscConstants.SLIDE:
        w, h = clipa.width, clipa.height

        def _stack(clipa_: vs.VideoNode, clipb_: vs.VideoNode) -> vs.VideoNode:
            if direction == Direction.LEFT:
                return core.std.StackHorizontal([clipa_, clipb_])
            elif direction == Direction.RIGHT:
                return core.std.StackHorizontal([clipb_, clipa_])
            elif direction == Direction.UP:
                return core.std.StackVertical([clipa_, clipb_])
            elif direction == Direction.DOWN:
                return core.std.StackVertical([clipb_, clipa_])

        stack = _stack(clipa_t_zone, clipb_t_zone)

        def _push(n: int) -> vs.VideoNode:
            if direction == Direction.LEFT:
                return stack.resize.Spline36(width=w, src_left=w * n / (frames_ - 1), src_width=w)
            elif direction == Direction.RIGHT:
                return stack.resize.Spline36(width=w, src_left=w * (1 - n / (frames_ - 1)), src_width=w)
            elif direction == Direction.UP:
                return stack.resize.Spline36(height=h, src_top=h * n / (frames_ - 1), src_height=h)
            elif direction == Direction.DOWN:
                return stack.resize.Spline36(height=h, src_top=h * (1 - n / (frames_ - 1)), src_height=h)

        pushed = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _push)

        return _return_combo(clipa_clean, pushed, clipb_clean)

    elif clipa_movement == MiscConstants.SLIDE and clipb_movement == MiscConstants.EXPAND:

        def _slide_expand(n: int):
            scale = Fraction(n, frames_ - 1)

            if scale == 0:
                return clipa_t_zone
            elif scale == 1:
                return clipb_t_zone

            if direction in [Direction.LEFT, Direction.RIGHT]:
                w = math.floor(scale * clipa.width)

                if w == 0:
                    return clipa_t_zone

                if direction == Direction.LEFT:
                    stack = core.std.StackHorizontal([clipa_t_zone, clipb_t_zone.resize.Spline36(width=w)])
                    return stack.std.Crop(left=w)
                elif direction == Direction.RIGHT:
                    stack = core.std.StackHorizontal([clipb_t_zone.resize.Spline36(width=w), clipa_t_zone])
                    return stack.std.Crop(right=w)

            elif direction in [Direction.UP, Direction.DOWN]:
                h = math.floor(scale * clipa.height)

                if h == 0:
                    return clipa_t_zone

                if direction == Direction.UP:
                    stack = core.std.StackVertical([clipa_t_zone, clipb_t_zone.resize.Spline36(height=h)])
                    return stack.std.Crop(top=h)
                elif direction == Direction.DOWN:
                    stack = core.std.StackVertical([clipb_t_zone.resize.Spline36(height=h), clipa_t_zone])
                    return stack.std.Crop(bottom=h)

        slide_expanded = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _slide_expand)

        return _return_combo(clipa_clean, slide_expanded, clipb_clean)

    elif clipa_movement == MiscConstants.SQUEEZE and clipb_movement == MiscConstants.SLIDE:

        def _squeeze_slide(n: int):
            scale = 1 - Fraction(n, frames_ - 1)

            if scale == 1:
                return clipa_t_zone
            elif scale == 0:
                return clipb_t_zone

            if direction in [Direction.LEFT, Direction.RIGHT]:
                w = math.floor(scale * clipa.width)

                if w == 0:
                    return clipb_t_zone

                if direction == Direction.LEFT:
                    stack = core.std.StackHorizontal([clipa_t_zone.resize.Spline36(width=w), clipb_t_zone])
                    return stack.std.Crop(right=w)
                elif direction == Direction.RIGHT:
                    stack = core.std.StackHorizontal([clipb_t_zone, clipa_t_zone.resize.Spline36(width=w)])
                    return stack.std.Crop(left=w)

            elif direction in [Direction.UP, Direction.DOWN]:
                h = math.floor(scale * clipa.height)

                if h == 0:
                    return clipb_t_zone

                if direction == Direction.UP:
                    stack = core.std.StackVertical([clipa_t_zone.resize.Spline36(height=h), clipb_t_zone])
                    return stack.std.Crop(bottom=h)

                elif direction == Direction.DOWN:
                    stack = core.std.StackVertical([clipb_t_zone, clipa_t_zone.resize.Spline36(height=h)])
                    return stack.std.Crop(top=h)

        squeeze_slided = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _squeeze_slide)

        return _return_combo(clipa_clean, squeeze_slided, clipb_clean)

    elif clipa_movement == MiscConstants.SQUEEZE and clipb_movement == MiscConstants.EXPAND:

        def _squeeze_expand(n: int) -> vs.VideoNode:
            scale = Fraction(n, frames_ - 1)

            if scale == 0:
                return clipa_t_zone
            elif scale == 1:
                return clipb_t_zone

            if direction in [Direction.LEFT, Direction.RIGHT]:
                w_inc = math.floor(scale * clipa.width)
                w_dec = clipa.width - w_inc

                if w_inc == 0:
                    return clipa_t_zone

                if direction == Direction.LEFT:
                    return core.std.StackHorizontal(
                        [clipa_t_zone.resize.Spline36(width=w_dec), clipb_t_zone.resize.Spline36(width=w_inc)]
                    )
                elif direction == Direction.RIGHT:
                    return core.std.StackHorizontal(
                        [clipb_t_zone.resize.Spline36(width=w_inc), clipa_t_zone.resize.Spline36(width=w_dec)]
                    )

            elif direction in [Direction.UP, Direction.DOWN]:
                h_inc = math.floor(scale * clipa.width)
                h_dec = clipa.width - h_inc

                if h_inc == 0:
                    return clipa_t_zone

                if direction == Direction.UP:
                    return core.std.StackVertical(
                        [clipa_t_zone.resize.Spline36(width=h_dec), clipb_t_zone.resize.Spline36(width=h_inc)]
                    )
                elif direction == Direction.RIGHT:
                    return core.std.StackVertical(
                        [clipb_t_zone.resize.Spline36(width=h_inc), clipa_t_zone.resize.Spline36(width=h_dec)]
                    )

        squeeze_expanded = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _squeeze_expand)

        return _return_combo(clipa_clean, squeeze_expanded, clipb_clean)


def push(clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT):
    """Second clip pushes first clip off of the screen.

    The first clip moves off of the screen moving towards the given `direction`.

    Alias for :func:`linear_boundary` with ``clipa_movement=SLIDE`` and ``clipb_movement=SLIDE``.
    """
    return linear_boundary(clipa, clipb, MiscConstants.SLIDE, MiscConstants.SLIDE, frames=frames, direction=direction)


def slide_expand(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
):
    """First clip slides out of view, while second clip expands into view from nothing.

    `clipa` slides off of the screen towards `direction`.
    `clipb` expands into view from the opposite side of the given direction.

    Alias for :func:`linear_boundary` with ``clipa_movement=SLIDE`` and ``clipb_movement=EXPAND``.
    """
    return linear_boundary(clipa, clipb, MiscConstants.SLIDE, MiscConstants.EXPAND, frames=frames, direction=direction)


def squeeze_slide(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
):
    """First clip squeezes into nothing, while second clip slides into view.

    `clipa` gets compressed off of the screen towards `direction`.
    `clipb` slides into view from the opposite side of the given direction.

    Alias for :func:`linear_boundary` with ``clipa_movement=SQUEEZE`` and ``clipb_movement=SLIDE``.
    """
    return linear_boundary(clipa, clipb, MiscConstants.SQUEEZE, MiscConstants.SLIDE, frames=frames, direction=direction)


def squeeze_expand(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
):
    """First clip squeezes into nothing, while second clip expands into view from nothing.

    `clipa` gets compressed off of the screen towards `direction`.
    `clipb` expands into view from the opposite side of the given direction.

    Alias for :func:`linear_boundary` with ``clipa_movement=SQUEEZE`` and ``clipb_movement=EXPAND``.
    """
    return linear_boundary(
        clipa, clipb, MiscConstants.SQUEEZE, MiscConstants.EXPAND, frames=frames, direction=direction
    )


def cover(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
) -> vs.VideoNode:
    """Second clip slides in and covers the first clip which stays in place.

    `clipb` slides into frame towards `direction` covering `clipa`.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("cover: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, cover, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _cover(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)
        w = math.floor(progress * clipa.width)
        h = math.floor(progress * clipa.height)

        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone

        if direction in [Direction.LEFT, Direction.RIGHT]:
            if w == 0:
                return clipa_t_zone
        elif direction in [Direction.UP, Direction.DOWN]:
            if h == 0:
                return clipa_t_zone

        if direction == Direction.LEFT:
            cropped_a = clipa_t_zone.std.Crop(right=w)
            stack = core.std.StackHorizontal([cropped_a, clipb_t_zone])
            return stack.resize.Spline36(width=clipa.width, src_width=clipa.width)
        elif direction == Direction.RIGHT:
            cropped_a = clipa_t_zone.std.Crop(left=w)
            stack = core.std.StackHorizontal([clipb_t_zone, cropped_a])
            return stack.resize.Spline36(width=clipa.width, src_left=clipa.width - w, src_width=clipa.width)
        elif direction == Direction.UP:
            cropped_a = clipa_t_zone.std.Crop(bottom=h)
            stack = core.std.StackVertical([cropped_a, clipb_t_zone])
            return stack.resize.Spline36(height=clipa.height, src_height=clipa.height)
        elif direction == Direction.DOWN:
            cropped_a = clipa_t_zone.std.Crop(top=h)
            stack = core.std.StackVertical([clipb_t_zone, cropped_a])
            return stack.resize.Spline36(height=clipa.height, src_top=clipa.height - h, src_height=clipa.height)

    covered = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _cover)

    return _return_combo(clipa_clean, covered, clipb_clean)


def reveal(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
) -> vs.VideoNode:
    """First clip slides out of view exposing second clip that stays in place.

    `clipa` slides out of frame towards `direction` revealing `clipb`.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("reveal: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, reveal, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _reveal(n: int) -> vs.VideoNode:
        progress = 1 - Fraction(n, frames_ - 1)
        w = math.floor(progress * clipa.width)
        h = math.floor(progress * clipa.height)

        if progress == 1:
            return clipa_t_zone
        elif progress == 0:
            return clipb_t_zone

        if direction in [Direction.LEFT, Direction.RIGHT]:
            if w == 0:
                return clipb_t_zone
        elif direction in [Direction.UP, Direction.DOWN]:
            if h == 0:
                return clipb_t_zone

        if direction == Direction.LEFT:
            cropped_b = clipb_t_zone.std.Crop(left=w)
            stack = core.std.StackHorizontal([clipa_t_zone, cropped_b])
            return stack.resize.Spline36(width=clipa.width, src_width=clipa.width, src_left=clipa.width - w)
        elif direction == Direction.RIGHT:
            cropped_b = clipb_t_zone.std.Crop(right=w)
            stack = core.std.StackHorizontal([cropped_b, clipa_t_zone])
            return stack.resize.Spline36(width=clipa.width, src_width=clipa.width)
        elif direction == Direction.UP:
            cropped_b = clipb_t_zone.std.Crop(top=h)
            stack = core.std.StackVertical([clipa_t_zone, cropped_b])
            return stack.resize.Spline36(height=clipa.height, src_height=clipa.height, src_top=clipa.height - h)
        elif direction == Direction.DOWN:
            cropped_b = clipb_t_zone.std.Crop(bottom=h)
            stack = core.std.StackVertical([cropped_b, clipa_t_zone])
            return stack.resize.Spline36(height=clipa.height, src_height=clipa.height)

    covered = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _reveal)

    return _return_combo(clipa_clean, covered, clipb_clean)


def curtain_cover(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, axis: Direction = Direction.HORIZONTAL
) -> vs.VideoNode:
    """Second clip comes into view from both directions split along the given axis covering the first clip in place.

    `clipb` splits and moves inwards along the given `axis`.

    If `axis` is given as :attr:`Direction.HORIZONTAL`, the clips must have an even integer width.
    If `axis` is given as :attr:`Direction.VERTICAL`, the clips must have an even integer height.
    """
    if axis not in [Direction.HORIZONTAL, Direction.VERTICAL]:
        raise ValueError("curtain_cover: give a proper axis")
    if axis == Direction.HORIZONTAL and clipa.width % 2:
        raise ValueError("curtain_cover: for horizontal reveal, input clips must have an even width")
    elif axis == Direction.VERTICAL and clipa.height % 2:
        raise ValueError("curtain_cover: for vertical reveal, input clips must have an even height")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, curtain_cover, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _curtain_cover(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)

        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone

        if axis == Direction.HORIZONTAL:
            w = round(float(clipa.width * progress / 2)) * 2

            if w == 0:
                return clipa_t_zone
            elif w == clipa.width:
                return clipb_t_zone

            clipb_left = clipb_t_zone.std.Crop(right=clipa.width // 2)
            clipb_right = clipb_t_zone.std.Crop(left=clipa.width // 2)

            clipb_left = clipb_left.std.Crop(left=clipb_left.width - w // 2)
            clipb_right = clipb_right.std.Crop(right=clipb_right.width - w // 2)

            clipa_cropped = clipa_t_zone.std.Crop(left=clipb_left.width, right=clipb_right.width)
            return core.std.StackHorizontal([clipb_left, clipa_cropped, clipb_right])

        elif axis == Direction.VERTICAL:
            h = round(float(clipa.height * progress / 2)) * 2

            if h == 0:
                return clipa_t_zone
            elif h == clipa.height:
                return clipb_t_zone

            clipb_top = clipb_t_zone.std.Crop(bottom=clipa.height // 2)
            clipb_bottom = clipb_t_zone.std.Crop(top=clipa.height // 2)

            clipb_top = clipb_top.std.Crop(top=clipb_top.height - h // 2)
            clipb_bottom = clipb_bottom.std.Crop(bottom=clipb_bottom.height - h // 2)

            clipa_cropped = clipa_t_zone.std.Crop(top=clipb_top.height, bottom=clipb_bottom.height)
            return core.std.StackVertical([clipb_top, clipa_cropped, clipb_bottom])

    curtain_covered = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _curtain_cover)

    return _return_combo(clipa_clean, curtain_covered, clipb_clean)


def curtain_reveal(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, axis: Direction = Direction.HORIZONTAL
) -> vs.VideoNode:
    """First clip splits apart to reveal the second clip in place.

    `clipa` splits and moves apart along the given `axis`.

    If `axis` is given as :attr:`Direction.HORIZONTAL`, the clips must have an even integer width.
    If `axis` is given as :attr:`Direction.VERTICAL`, the clips must have an even integer height.
    """
    if axis not in [Direction.HORIZONTAL, Direction.VERTICAL]:
        raise ValueError("curtain_reveal: give a proper axis")
    if axis == Direction.HORIZONTAL and clipa.width % 2:
        raise ValueError("curtain_reveal: for horizontal reveal, input clips must have an even width")
    elif axis == Direction.VERTICAL and clipa.height % 2:
        raise ValueError("curtain_reveal: for vertical reveal, input clips must have an even height")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, curtain_reveal, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _curtain_reveal(n: int) -> vs.VideoNode:
        progress = Fraction(n, frames_ - 1)

        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone

        if axis == Direction.HORIZONTAL:
            w = round(float(clipa.width * progress / 2)) * 2

            if w == 0:
                return clipa_t_zone
            elif w == clipa.width:
                return clipb_t_zone

            clipa_left = clipa_t_zone.std.Crop(right=clipa.width // 2)
            clipa_right = clipa_t_zone.std.Crop(left=clipa.width // 2)

            clipa_left = clipa_left.std.Crop(left=w // 2)
            clipa_right = clipa_right.std.Crop(right=w // 2)

            clipb_cropped = clipb_t_zone.std.Crop(left=clipa_left.width, right=clipa_right.width)
            return core.std.StackHorizontal([clipa_left, clipb_cropped, clipa_right])

        elif axis == Direction.VERTICAL:
            h = round(float(clipa.height * progress / 2)) * 2

            if h == 0:
                return clipa_t_zone
            elif h == clipa.height:
                return clipb_t_zone

            clipa_top = clipa_t_zone.std.Crop(bottom=clipa.height // 2)
            clipa_bottom = clipa_t_zone.std.Crop(top=clipa.height // 2)

            clipa_top = clipa_top.std.Crop(top=h // 2)
            clipa_bottom = clipa_bottom.std.Crop(bottom=h // 2)

            clipb_cropped = clipb_t_zone.std.Crop(top=clipa_top.height, bottom=clipa_bottom.height)
            return core.std.StackVertical([clipa_top, clipb_cropped, clipa_bottom])

    curtain_revealed = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _curtain_reveal)

    return _return_combo(clipa_clean, curtain_revealed, clipb_clean)


def pixellate(
    clipa: vs.VideoNode,
    clipb: vs.VideoNode,
    frames: Optional[int] = None,
    lowest_target_w: Optional[int] = 2,
    lowest_target_h: Optional[int] = 2,
) -> vs.VideoNode:
    """Pixellate using rescales and aggressively fade at the center.

    For large clips (width `x` height), the effect might not be too noticeable
    until the transition is near the middle point.
    This is due to bicubic downscales and point re-upscales
    at very high percentages of the original dimensions
    not being noticeably different.

    Due to the way pixellation progress is calculated,
    the transition `must` be at least 4 frames long.

    Longer transitions paired with larger target dimensions
    will cause the pixellation effect to appear to pause towards the center of the transition.

    :param lowest_target_w: An integer that determines the minimum width target to downscale to.
        By specifying ``None``, or by specifying the width of the source clips, the clips will not be scaled in the
        `x` or width direction, making this only pixellate vertically.

    :param lowest_target_h: An integer that determines the minimum height target to downscale to.
        By specifying ``None``, or by specifying the height of the source clips, the clips will not be scaled in the
        `y` or height direction, making this only pixellate horizontally.
    """
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if frames_ < 4:
        raise ValueError("pixellate: transition must be at least 4 frames long")

    if lowest_target_w is None:
        lowest_target_w = clipa.width
    if lowest_target_h is None:
        lowest_target_h = clipa.height
    if lowest_target_w < 1 or lowest_target_w > clipa.width:
        raise ValueError("pixellate: `lowest_target_w` must be at least one and at most the width of the source clips")
    if lowest_target_h < 1 or lowest_target_h > clipa.height:
        raise ValueError("pixellate: `lowest_target_h` must be at least one and at most the height of the source clips")
    if lowest_target_w == clipa.width and lowest_target_h == clipa.height:
        raise ValueError("pixellate: at least one target dimension must be lower than the source dimensions")
    lowest_target_w_ = lowest_target_w
    lowest_target_h_ = lowest_target_h

    if TYPE_CHECKING:
        assert isinstance(frames_, int)
        assert isinstance(lowest_target_w_, int)
        assert isinstance(lowest_target_h_, int)
    _check_clips(frames_, pixellate, clipa, clipb)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _pixellate(n: int):
        if iseven(frames_):
            center = (frames_ - 1) / 2
            if n < center:
                progress_a = Fraction(math.floor(center) - n, math.floor(center)) ** 2

                target_w_a = max([lowest_target_w_, math.floor(progress_a * clipa.width)])
                target_h_a = max([lowest_target_h_, math.floor(progress_a * clipa.height)])

                target_w_b = lowest_target_w_
                target_h_b = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == math.floor(center):
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [1 / 3])
                else:
                    return clipa_pixellated

            else:
                progress_b = Fraction(n - math.ceil(center), math.floor(center)) ** 2

                target_w_b = max([lowest_target_w_, math.floor(progress_b * clipa.width)])
                target_h_b = max([lowest_target_h_, math.floor(progress_b * clipa.height)])

                target_w_a = lowest_target_w_
                target_h_a = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == math.ceil(center):
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [2 / 3])
                else:
                    return clipb_pixellated
        else:
            center = (frames_ - 1) // 2
            if n < center:
                progress_a = Fraction(center - n, center) ** 2

                target_w_a = max([lowest_target_w_, math.floor(progress_a * clipa.width)])
                target_h_a = max([lowest_target_h_, math.floor(progress_a * clipa.height)])

                target_w_b = lowest_target_w_
                target_h_b = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == center - 1:
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [1 / 4])
                else:
                    return clipa_pixellated

            elif n == center:
                target_w_a = target_w_b = lowest_target_w_
                target_h_a = target_h_b = lowest_target_h_
                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                return core.std.Merge(clipa_pixellated, clipb_pixellated, [1 / 2])

            else:
                progress_b = Fraction(n - center, center) ** 2

                target_w_b = max([lowest_target_w_, math.floor(progress_b * clipa.width)])
                target_h_b = max([lowest_target_h_, math.floor(progress_b * clipa.height)])

                target_w_a = lowest_target_w_
                target_h_a = lowest_target_h_

                clipa_small = clipa_t_zone.resize.Bicubic(target_w_a, target_h_a)
                clipb_small = clipb_t_zone.resize.Bicubic(target_w_b, target_h_b)

                clipa_pixellated = clipa_small.resize.Point(clipa.width, clipa.height)
                clipb_pixellated = clipb_small.resize.Point(clipa.width, clipa.height)

                if n == center + 1:
                    return core.std.Merge(clipa_pixellated, clipb_pixellated, [3 / 4])
                else:
                    return clipb_pixellated

    pixellated = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _pixellate)

    return _return_combo(clipa_clean, pixellated, clipb_clean)


def peel(
    clipa: vs.VideoNode, clipb: vs.VideoNode, frames: Optional[int] = None, direction: Direction = Direction.LEFT
) -> vs.VideoNode:
    """First clip peels away revealing the second clip beneath.

    Both clips remain in place during the transition. The boundary between clips moves towards `direction`.
    """
    if direction not in [Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN]:
        raise ValueError("peel: give a proper direction")
    frames_ = frames or min(clipa.num_frames, clipb.num_frames)
    if TYPE_CHECKING:
        assert isinstance(frames_, int)
    _check_clips(frames_, peel, clipa, clipb, subsampling=True)
    clipa_clean, clipb_clean, clipa_t_zone, clipb_t_zone = _transition_clips(clipa, clipb, frames_)

    def _peel(n: int):
        progress = Fraction(n, frames_ - 1)
        if progress == 0:
            return clipa_t_zone
        elif progress == 1:
            return clipb_t_zone
        else:
            if direction in [Direction.LEFT, Direction.RIGHT]:
                clipb_w = math.floor(clipa.width * progress)
                clipa_w = clipa.width - clipb_w

                if clipb_w == 0:
                    return clipa_t_zone
                elif clipb_w == clipa.width:
                    return clipb_t_zone
                else:
                    if direction == Direction.LEFT:
                        clipa_cropped = clipa_t_zone.std.Crop(right=clipb_w)
                        clipb_cropped = clipb_t_zone.std.Crop(left=clipa_w)
                        return core.std.StackHorizontal([clipa_cropped, clipb_cropped])
                    elif direction == Direction.RIGHT:
                        clipa_cropped = clipa_t_zone.std.Crop(left=clipb_w)
                        clipb_cropped = clipb_t_zone.std.Crop(right=clipa_w)
                        return core.std.StackHorizontal([clipb_cropped, clipa_cropped])
            elif direction in [Direction.UP, Direction.DOWN]:
                clipb_h = math.floor(clipa.height * progress)
                clipa_h = clipa.height - clipb_h

                if clipb_h == 0:
                    return clipa_t_zone
                elif clipb_h == clipa.width:
                    return clipb_t_zone
                else:
                    if direction == Direction.UP:
                        clipa_cropped = clipa_t_zone.std.Crop(bottom=clipb_h)
                        clipb_cropped = clipb_t_zone.std.Crop(top=clipa_h)
                        return core.std.StackVertical([clipa_cropped, clipb_cropped])
                    elif direction == Direction.DOWN:
                        clipa_cropped = clipa_t_zone.std.Crop(top=clipb_h)
                        clipb_cropped = clipb_t_zone.std.Crop(bottom=clipa_h)
                        return core.std.StackVertical([clipb_cropped, clipa_cropped])

    peeled = core.std.FrameEval(core.std.BlankClip(clipa, length=frames_), _peel)

    return _return_combo(clipa_clean, peeled, clipb_clean)


def round_to(f: Fraction, n: int) -> int:
    """Rounds a fractional value to the nearest `n`, rounding half up and never returning less than `n`"""
    if n < 1:
        raise ValueError("round_to: `n` must be an integer greater than 0")
    if n == 1:
        return max(1, round(float(f)))
    else:
        return max(n, round(float(f / n)) * n)


def isodd(value: int) -> bool:
    return bool(value % 2)


def iseven(value: int) -> bool:
    return not bool(value % 2)
Powerpoint-like transitions implemented in VapourSynth. # type: ignore # type: ignore Direction enumeration.      Members can be simply referenced by their names,     i.e. ``vs_transitions.LEFT`` instead of ``vs_transitions.Direction.LEFT``. Miscellanious enumeration for some functions.      Members can be simply referenced by their names,     i.e. ``vs_transitions.SLIDE`` instead of ``vs_transitions.MiscConstants.SLIDE``. General checker for clip formats, resolutions, length, and other keywords.      Possible kwargs:         'subsampling': checks that all clips have 444 subsampling for resize purposes Prevents splicing empty clips.      :param clip1:        optional start clip     :param clip_middle:  mandatory middle clip     :param clip2:        optional ending clip     :return: splice of existing clips in order Returns clean (non-transition) and transition sections of the given clips based on frames. Cross-fade clips. Cross-fade clips according to a curve.      :param exponent: An integer in the range from 1-5 (inclusive)         where 1 represents a parabolic curve, 2 represents a quartic curve,         and higher powers more resembling an tight ease-in-out function with constant speed for most of the transition.          An `exponent` of ``1`` is probably most useful,         as higher exponents tend towards a constant speed and therefore are         almost indistinguishable from a normal :func:`fade`. Returns position as a fractions.Fraction, based on a input percentage fractions.Fraction Simple convenience function to :func:`fade` a clip to black.      `frames` will be the number of frames consumed from the end of the `src_clip` during the transition.     The first frame of the transition will be the first frame of the `src_clip`,     while the last frame of the transition will be a pure black frame.      If `frames` is not given, will fade to black over the entire duration of the `src_clip`. Simple convenience function to :func:`fade` a clip into view from black.      `frames` will be the number of frames consumed from the start of the `src_clip` during the transition.     The first frame of the transition will be a pure black frame,     while the last frame of the transition will be the last frame of the `src_clip`.      If `frames` is not given, will fade in over the entire duration of the `src_clip`. A moving directional fade.      Similar to a :func:`fade`, but with a moving mask.     The `direction` will be the direction the fade progresses towards.     (i.e. the second clip begins fading in from the **opposite** given direction,     and the first clip begins fading out starting from the **opposite** given direction,     progressing towards `direction`)      Uses a pure white to black gradient for the fade.     If possible, uses `numpy <https://pypi.org/project/numpy/>`_ to generate the mask.     If the numpy module is not found, falls back to a slower     and possibly less accurate approach using lists     and the ``ctypes`` module for writing to a VapourSynth frame. Mimics a cube face rotation by adjusting the speed at which the :func:`squeeze_expand` boundary moves.      Cube face containing `clipa` rotates away from the viewer in projected 3-D space towards `direction`.      :param exaggeration: An integer between 0 and 100 (inclusive)         representing how much the effect of the cosine wave should be exaggerated.         `0` corresponds to a mathematically correct projection of a 90 degree rotation offset by 45 degrees.         `100` corresponds to a fitted cosine wave. Return a radian rotation based on `percentage` ranging from -pi/4 at 0% to -3pi/4 at 100% Return position of a rotated edge as a percentage         0% at 0%, 23% at 25%, 50% at 50%, 77% at 75%, 100% at 100% mathmatically correct projection fitted cosine wave to exaggerate the effects Generalized boundary moving function for a linear transition between two stacked clips.      `clipa` can either slide out of view (having its size unchanged) or be squeezed to nothing from its original size.     `clipb` can either slide into view (having its size unchanged) or be expanded from nothing to its full size.     The boundary between the two clips moves towards `direction`.      The parameter `clipa_movement` can be :attr:`MiscConstants.SLIDE` or :attr:`MiscConstants.SQUEEZE`.     The parameter `clipb_movement` can be :attr:`MiscConstants.SLIDE` or :attr:`MiscConstants.EXPAND`.      See :func:`push`, :func:`slide_expand`, :func:`squeeze_slide`, or :func:`squeeze_expand`     for simpler aliases in the same form as most other linear, directional transitions. # only need subsampling check if resizing Second clip pushes first clip off of the screen.      The first clip moves off of the screen moving towards the given `direction`.      Alias for :func:`linear_boundary` with ``clipa_movement=SLIDE`` and ``clipb_movement=SLIDE``. First clip slides out of view, while second clip expands into view from nothing.      `clipa` slides off of the screen towards `direction`.     `clipb` expands into view from the opposite side of the given direction.      Alias for :func:`linear_boundary` with ``clipa_movement=SLIDE`` and ``clipb_movement=EXPAND``. First clip squeezes into nothing, while second clip slides into view.      `clipa` gets compressed off of the screen towards `direction`.     `clipb` slides into view from the opposite side of the given direction.      Alias for :func:`linear_boundary` with ``clipa_movement=SQUEEZE`` and ``clipb_movement=SLIDE``. First clip squeezes into nothing, while second clip expands into view from nothing.      `clipa` gets compressed off of the screen towards `direction`.     `clipb` expands into view from the opposite side of the given direction.      Alias for :func:`linear_boundary` with ``clipa_movement=SQUEEZE`` and ``clipb_movement=EXPAND``. Second clip slides in and covers the first clip which stays in place.      `clipb` slides into frame towards `direction` covering `clipa`. First clip slides out of view exposing second clip that stays in place.      `clipa` slides out of frame towards `direction` revealing `clipb`. Second clip comes into view from both directions split along the given axis covering the first clip in place.      `clipb` splits and moves inwards along the given `axis`.      If `axis` is given as :attr:`Direction.HORIZONTAL`, the clips must have an even integer width.     If `axis` is given as :attr:`Direction.VERTICAL`, the clips must have an even integer height. First clip splits apart to reveal the second clip in place.      `clipa` splits and moves apart along the given `axis`.      If `axis` is given as :attr:`Direction.HORIZONTAL`, the clips must have an even integer width.     If `axis` is given as :attr:`Direction.VERTICAL`, the clips must have an even integer height. Pixellate using rescales and aggressively fade at the center.      For large clips (width `x` height), the effect might not be too noticeable     until the transition is near the middle point.     This is due to bicubic downscales and point re-upscales     at very high percentages of the original dimensions     not being noticeably different.      Due to the way pixellation progress is calculated,     the transition `must` be at least 4 frames long.      Longer transitions paired with larger target dimensions     will cause the pixellation effect to appear to pause towards the center of the transition.      :param lowest_target_w: An integer that determines the minimum width target to downscale to.         By specifying ``None``, or by specifying the width of the source clips, the clips will not be scaled in the         `x` or width direction, making this only pixellate vertically.      :param lowest_target_h: An integer that determines the minimum height target to downscale to.         By specifying ``None``, or by specifying the height of the source clips, the clips will not be scaled in the         `y` or height direction, making this only pixellate horizontally. First clip peels away revealing the second clip beneath.      Both clips remain in place during the transition. The boundary between clips moves towards `direction`. Rounds a fractional value to the nearest `n`, rounding half up and never returning less than `n`