Blob Blame Raw
# pylint: disable=line-too-long
"""
misc.py
Some miscellaneous functions and classes will be provided here
"""

import sys
import math
import settings
from common.Vector import Vector
from common.Color import Color
sys.path.append("..")


def approximate_equal(a, b):
    """
    Need to define this function somewhere else, a and b are of type "float"

    Args:
        a (float) : First number to be compared
        b (float) : Second number to be compared

    Returns:
        (bool) : True if the numbers are approximately equal under precision
               : False otherwise
    """
    precision = 1e-8
    if a < b:
        return b - a < precision
    return a - b < precision


def calculate_pixels_per_unit():
    """
    Gives the value of 1 unit in terms of pixels according to the canvas defined

    Args:
        (None)

    Returns:
        (float) : Pixels per unit
    """
    image_width = float(settings.lottie_format["w"])
    image_area_width = settings.view_box_canvas["val"][2] - settings.view_box_canvas["val"][0]
    settings.PIX_PER_UNIT = image_width / image_area_width
    return settings.PIX_PER_UNIT


def change_axis(x_val, y_val, is_transform=True):
    """
    Convert synfig axis coordinates into lottie format

    Args:
        x_val (float | str) : x axis value in pixels
        y_val (float | str) : y axis value in pixels
        is_transform (`obj`: bool, optional) : Is this value used in transform module?

    Returns:
        (list)  : x and y axis value in Lottie format
    """
    x_val, y_val = float(x_val), float(y_val)
    if is_transform:
        x_val, y_val = x_val, -y_val
    else:
        x_val, y_val = x_val + settings.lottie_format["w"]/2, -y_val + settings.lottie_format["h"]/2
    return [x_val, y_val]


def parse_position(animated, i):
    """
    To convert the synfig coordinates from units(initially a string) to pixels
    Depends on whether a vector is provided to it or a real value
    If real value is provided, then time is also taken into consideration

    Args:
        animated (lxml.etree._Element) : Stores animation which contains waypoints
        i        (int)                 : Iterator over animation

    Returns:
        (common.Vector.Vector) If the animated type is not color
        (common.Color.Color)  Else if the animated type is color
    """
    if animated.attrib["type"] == "vector":
        pos = [float(animated[i][0][0].text),
               float(animated[i][0][1].text)]
        pos = [settings.PIX_PER_UNIT*x for x in pos]

    elif animated.attrib["type"] == "real":
        pos = parse_value(animated, i)

    elif animated.attrib["type"] == "circle_radius":
        pos = parse_value(animated, i)
        pos[0] *= 2 # Diameter

    elif animated.attrib["type"] == "angle":
        pos = [get_angle(float(animated[i][0].attrib["value"])),
               get_frame(animated[i])]

    elif animated.attrib["type"] in {"composite_convert", "region_angle", "star_angle_new", "scalar_multiply"}:
        pos = [float(animated[i][0].attrib["value"]),
               get_frame(animated[i])]

    elif animated.attrib["type"] == "rotate_layer_angle":
        # Angle needs to made neg of what they are
        pos = [-float(animated[i][0].attrib["value"]),
               get_frame(animated[i])]

    elif animated.attrib["type"] == "opacity":
        pos = [float(animated[i][0].attrib["value"]) * settings.OPACITY_CONSTANT,
               get_frame(animated[i])]

    elif animated.attrib["type"] == "effects_opacity":
        pos = [float(animated[i][0].attrib["value"]),
               get_frame(animated[i])]

    elif animated.attrib["type"] == "points":
        pos = [int(animated[i][0].attrib["value"]),
               get_frame(animated[i])]

    elif animated.attrib["type"] == "bool":
        val = animated[i][0].attrib["value"]
        if val == "false":
            val = 0
        else:
            val = 1
        pos = [val, get_frame(animated[i])]

    elif animated.attrib["type"] == "rectangle_size":
        pos = parse_value(animated, i)
        vec = Vector(pos[0], pos[1], animated.attrib["type"])
        vec.add_new_val(float(animated[i][0].attrib["value2"]) * settings.PIX_PER_UNIT)
        return vec

    elif animated.attrib["type"] == "image_scale":
        val = float(animated[i][0].attrib["value"])
        val2 = get_frame(animated[i])
        vec = Vector(val, val2, animated.attrib["type"])
        vec.add_new_val(float(animated[i][0].attrib["value2"]))
        return vec

    elif animated.attrib["type"] == "scale_layer_zoom":
        val = (math.e ** float(animated[i][0].attrib["value"])) * 100
        val2 = get_frame(animated[i])
        vec = Vector(val, val2, animated.attrib["type"])
        vec.add_new_val(val)
        return vec

    elif animated.attrib["type"] == "stretch_layer_scale":
        val1 = float(animated[i][0][0].text) * 100
        val3 = float(animated[i][0][1].text) * 100
        vec = Vector(val1, get_frame(animated[i]), animated.attrib["type"])
        vec.add_new_val(val3)
        return vec

    elif animated.attrib["type"] == "group_layer_scale":
        val1 = float(animated[i][0][0].text) * 100
        val3 = float(animated[i][0][1].text) * 100
        vec = Vector(val1, get_frame(animated[i]), animated.attrib["type"])
        vec.add_new_val(val3)
        return vec

    elif animated.attrib["type"] == "time":
        val = parse_time(animated[i][0].attrib["value"])    # Needed in seconds
        val2 = get_frame(animated[i])   # Needed in frames
        return Vector(val, val2, animated.attrib["type"])

    elif animated.attrib["type"] == "color":
        red = float(animated[i][0][0].text)
        green = float(animated[i][0][1].text)
        blue = float(animated[i][0][2].text)
        alpha = float(animated[i][0][3].text)
        red = red ** (1/settings.GAMMA)
        green = green ** (1/settings.GAMMA)
        blue = blue ** (1/settings.GAMMA)
        return Color(red, green, blue, alpha)

    return Vector(pos[0], pos[1], animated.attrib["type"])


def parse_value(animated, i):
    """
    To convert the synfig value parameter from units to pixels
    and also take into consideration the time parameter

    Args:
        animated (lxml.etree._Element) : Stores animation which holds waypoints
        i        (int)                 : Iterator for animation

    Returns:
        (list)  : [value, time] is returned
    """
    pos = [float(animated[i][0].attrib["value"]) * settings.PIX_PER_UNIT,
           get_frame(animated[i])]
    return pos


def get_angle(theta):
    """
    Converts the .sif angle into lottie angle
    .sif uses positive x-axis as the start point and goes anticlockwise
    lottie uses positive y-axis as the start point and goes clockwise

    Args:
        theta (float) : Stores Synfig format angle

    Returns:
        (int)   : Lottie format angle
    """
    theta = int(theta)
    shift = -int(theta / 360)
    theta = theta % 360
    theta = (90 - theta) % 360

    theta = theta + shift * 360
    return theta


def is_animated(node):
    """
    Tells whether a parater is animated or not

    Args:
        node (lxml.etree._Element) : Animation which stores waypoints

    Returns:
        (int) : Depending upon whether the parameter is animated, following
                values are returned
                0: If not animated
                1: If only single waypoint is present
                2: If more than one waypoint is present
    """
    case = 0
    if node.tag == "animated":
        if len(node) == 1:
            case = 1
        else:
            case = 2
    else:
        case = 0
    return case


def clamp_col(color):
    """
    This function converts the colors into int and takes them to the range of
    0-255

    Args:
        color (float) : Synfig format color value

    Returns:
        (int) : Color value between 0-255
    """
    color = color ** (1/settings.GAMMA)
    color *= 255
    color = int(color)
    return max(0, min(color, 255))


def get_color_hex(node):
    """
    Convert the <color></color> from rgba to hex format

    Args:
        node (lxml.etree._Element) : Synfig format color parameter

    Returns:
        (str) : hex format of color
    """
    red, green, blue = 1, 0, 0
    for col in node:
        if col.tag == "r":
            red = float(col.text)
        elif col.tag == "g":
            green = float(col.text)
        elif col.tag == "b":
            blue = float(col.text)
    # Convert to 0-255 range
    red, green, blue = clamp_col(red), clamp_col(green), clamp_col(blue)

    # https://stackoverflow.com/questions/3380726/converting-a-rgb-color-tuple-to-a-six-digit-code-in-python/3380739#3380739
    ret = "#{0:02x}{1:02x}{2:02x}".format(red, green, blue)
    return ret


def get_frame(waypoint):
    """
    Given a waypoint, it parses the time to frames

    Args:
        waypoint (lxml.etree._Element) : Synfig format waypoint

    Returns:
        (int) : the frame at which waypoint is present
    """
    time = get_time(waypoint)
    frame = time * settings.lottie_format["fr"]
    frame = round(frame)
    return frame


def get_time(waypoint):
    """
    Given a waypoint, it parses the string time to float time

    Args:
        waypoint (lxml.etree._Element) : Synfig format waypoint

    Returns:
        (float) : the time in seconds at which the waypoint is present
    """
    return parse_time(waypoint.attrib["time"])


def parse_time(time_in_str):
    """
    Given a string, it parses time to float time

    Args:
        time_in_str (str) : Time in string format

    Returns:
        (float) : the time in seconds at represented by the string
    """
    time = time_in_str.split(" ")
    final = 0
    for frame in time:
        if frame[-1] == "h":
            final += float(frame[:-1]) * 60 * 60
        elif frame[-1] == "m":
            final += float(frame[:-1]) * 60
        elif frame[-1] == "s":
            final += float(frame[:-1])
        elif frame[-1] == "f":  # This should never happen according to my code
            raise ValueError("In waypoint, time is never expected in frames.")
    return final


def get_vector(waypoint):
    """
    Given a waypoint, it parses the string vector into Vector class defined in
    this converter

    Args:
        waypoint (lxml.etree._Element) : Synfig format waypoint

    Returns:
        (common.Vector.Vector) : x and y axis values stores in Vector format
    """
    # converting radius and angle to a vector
    if waypoint.tag == "radial_composite":
        for child in waypoint:
            if child.tag == "radius":
                radius = float(child[0].attrib["value"])
                radius *= settings.PIX_PER_UNIT
            elif child.tag == "theta":
                angle = float(child[0].attrib["value"])
        x, y = radial_to_tangent(radius, angle)
    else:
        x = float(waypoint[0][0].text)
        y = float(waypoint[0][1].text)
    return Vector(x, y)


def radial_to_tangent(radius, angle):
    """
    Converts a tangent from radius and angle format to x, y axis co-ordinate
    system

    Args:
        radius (float) : radius of a vector
        angle  (float) : angle in degrees

    Returns:
        (float) : The x-coordinate of the vector
        (float) : The y-coordinate of the vector
    """
    angle = math.radians(angle)
    x = radius * math.cos(angle)
    y = radius * math.sin(angle)
    return x, y


def set_vector(waypoint, pos):
    """
    Given a waypoint and pos(Vector), it set's the waypoint's vectors

    Args:
        waypoint (lxml.etree._Element) : Synfig format waypoint

    Returns:
        (None)
    """
    waypoint[0][0].text = str(pos.val1)
    waypoint[0][1].text = str(pos.val2)


def modify_final_dump(obj):
    """
    This function will remove unwanted keys from the final dictionary and also
    modify the floats according to our precision

    Args:
        obj (float | dict | list | tuple) : The object to be modified

    Returns:
        (float | dict | list) : Depending upon arguments, obj type is decided
    """
    if isinstance(obj, float):
        return round(obj, settings.FLOAT_PRECISION)
    elif isinstance(obj, dict):
        return dict((k, modify_final_dump(v)) for k, v in obj.items() if k not in ["synfig_i", "synfig_o"])
    elif isinstance(obj, (list, tuple)):
        return list(map(modify_final_dump, obj))
    return obj