EASING_FUNCTIONS = {
    "linear": linear,
    "sine": sine,
    "quadratic": quadratic,
    "cubic": cubic,
    "quartic": quartic,
    "quintic": quintic,
    "bounce": bounce,
    "circular": circular,
    "exponential": exponential,
    "back": back,
    "elastic": elastic
} def get_array(prop, t, default):
    if prop is None:
        return default

    if callable(prop):
        return prop(t)
    elif prop and (len(prop) == 2) and is_arr(prop[0]) and len(prop[0]) and is_arr(prop[1]) and len(prop[1]):
        # array of arrays
        arr0 = prop[0]
        arr1 = prop[1]
        length = min(len(arr0), len(arr1))
        result = []
        for i in range(length):
            v0 = arr0[i]
            v1 = arr1[i]
            result.append(lerp(t, v0, v1))
        return result
    elif prop and len(prop) > 1:
        return prop
    return default def get_color(prop, t, default):
    if prop is None:
        if default is not None:
            return Color(default)
        elif default is None:
            return None

    out = None

    if callable(prop):
        out = prop(t)
    elif isinstance(prop, Color):
        out = prop
    elif is_arr(prop):
        if len(prop) == 2:
            out = clerp(t, Color(prop[0]), Color(prop[1]))
        elif len(prop) > 2:
            colors = map(Color, prop)
            out = multi_clerp(t, *colors)
        else:
            out = Color(prop[0])
    elif isinstance(prop, bool):
        out = prop
    else:
        out = Color(prop)

    return out def get_bool(prop, t, default):
    if prop is None:
        return default

    out = prop

    if callable(prop):
        out = prop(t)
    elif isinstance(prop, (list, tuple)):
        out = pick_from_array(prop, t, False)
    elif isinstance(prop, str):
        return str2bool(prop)

    return out def get_number(prop, t, default):
    if prop is None:
        return default

    out = None

    if isinstance(prop, Number):
        out = prop
    elif callable(prop):
        out = prop(t)
    elif is_arr(prop):
        out = pick_from_array(prop, t)
    elif isinstance(prop, str):
        # yolo
        try:
            out = int(prop)
        except Exception:
            out = float(prop)

    return out def get_string(prop, t, default):
    if prop is None:
        return default

    out = None

    if isinstance(prop, str):
        out = prop
    elif callable(prop):
        out = prop(t)
    elif is_arr(prop):
        out = pick_from_array(prop, t, False)

    return out def get_image(prop, t, default, mode):
    # TODO: add ping-pong loop mode

    if prop is None:
        return default
    elif callable(prop):
        return prop(t)
    elif is_arr(prop):
        if mode == "wrap":
            return prop[floor(t * len(prop)) % (len(prop))]
        else:
            return prop[clamp(floor(t * len(prop)), 0, len(prop) - 1)]
    return prop def get_cairo_constant(name, prop, t, default):
    if prop is None:
        return default

    out = None

    if callable(prop):
        out = prop(t)
    elif isinstance(prop, str):
        out = _CAIRO_CONSTANTS[name.lower()][prop.lower()]
    else:
        out = prop

    return out def get_point_array(prop, t, default):
    if prop is None:
        return default

    if callable(prop):
        return prop(t)
    elif prop and (len(prop) == 2) and is_arr(prop[0]) and len(prop[0]) and is_arr(prop[1]) and len(prop[1]):
        # array of arrays
        arr0 = prop[0]
        arr1 = prop[1]
        length = min(len(arr0), len(arr1))
        result = []
        for i in range(length):
            v0 = arr0[i]
            v1 = arr1[i]
            result.append([lerp(t, v0[0], v1[0]), lerp(t, v0[1], v1[1])])
        return result
    elif prop and len(prop) > 1:
        return prop
    return default def randrange(start, end):
    """Returns a random float in the range defined by ``start`` and ``end``.

    Returns
    -------
    float
        The random value.
    """
    return start + random() * (end - start)
from ..easing import EASING_FUNCTIONS
from ..value_parser import get_array, get_color, get_bool, get_number
from ..value_parser import get_string, get_image, get_cairo_constant, get_point_array
from ..utils import randrange
import cairo

    def __init__(self, *args, **kwargs):
        self.ease = None
        self.loop = None
        self.props = kwargs
        self.shapes = []

    def add(self, item):
        """Adds a child shape to this shape's list of children.

        Shouldn't be called normally, as this is called automatically
        when you set a shape as a parent of another, for example:

        .. code-block:: python

            circle_a = render_list.circle(x=100, y=100, radius=50)
            render_list.circle(x=100, y=0, radius=50, parent=circle_a)

        Parameters
        ----------
        item : :class:`Shape`
            The shape to add as a child of this one.

        Returns
        -------
        item : :class:`Shape`
            The added child.
        """
        self.shapes.append(item)
        return item

    def set_prop(self, **kwargs):
        """Sets properties for this shape.

        Returns
        -------
        self : :class:`Shape`
            For method chaining.
        """
        self.props.update(kwargs)
        return self

    def set_ease(self, ease="sine"):
        """Sets the easing function for this shape.

        Parameters
        ----------
        ease : callable or str
            Either a string, which denotes which easing function to pick from the defaults,
            or a callable, which should take in a time ``t`` and return a single value.
            Defaults to ``"sine"``.

        Returns
        -------
        self : :class:`Shape`
            For method chaining.
        """
        self.ease = ease
        return self

    def set_loop(self, loop=True):
        """Sets whether this shape should loop or not.

        Parameters
        ----------
        loop : bool
            Whether this shape should bounce back to its initial properties.
            Defaults to ``True``.

        Returns
        -------
        self : :class:`Shape`
            For method chaining.
        """
        self.loop = loop
        return self

    def render(self, context, t):
        time = t
        t *= self.props.get("speed_mult", 1)
        t += self.props.get("phase", 0)
        self.no_interp_time = t
        t = self.interpolate(t)

        self.start_draw(context, t)
        self.draw(context, t)
        for shape in self.shapes:
            shape.render(context, time)
        self.end_draw(context, t)

    def interpolate(self, t, wrap=True):
        if wrap:
            t %= 1

        if self.loop:
            t = t * 2 if t < 0.5 else (1 - t) * 2

        if callable(self.ease):
            return self.ease(t)

        if self.ease in EASING_FUNCTIONS:
            return EASING_FUNCTIONS[self.ease](t)

        return t

    def start_draw(self, context, t):
        context.save()

        context.set_line_width(self.get_number("line_width", t, self.default_styles["line_width"]))
        context.set_line_cap(self.get_cairo_constant("line_cap", "line_cap", t, self.default_styles["line_cap"]))
        context.set_line_join(self.get_cairo_constant("line_join", "line_join", t, self.default_styles["line_join"]))
        context.set_miter_limit(self.get_number("miter_limit", t, self.default_styles["miter_limit"]))

        context.translate(
            self.get_number("translation_x", t, self.default_styles["translation_x"]),
            self.get_number("translation_y", t, self.default_styles["translation_y"])
        )

        try:
            shake = self.get_number("shake", t, self.default_styles["shake"])

            context.translate(randrange(-shake, shake), randrange(-shake, shake))