// =============================================================================
// Easing
// =============================================================================

///|
/// Step timing function jump position.
pub(all) enum StepPosition {
  Start
  End
  JumpNone
  JumpBoth
} derive(Debug, Eq)

///|
pub impl Show for StepPosition with fn output(self, logger) {
  match self {
    Start => logger.write_string("Start")
    End => logger.write_string("End")
    JumpNone => logger.write_string("JumpNone")
    JumpBoth => logger.write_string("JumpBoth")
  }
}

///|
/// One point in a CSS `linear()` easing function.
pub(all) struct LinearEasingPoint {
  input : Double
  output : Double
} derive(Debug, Eq)

///|
pub impl Show for LinearEasingPoint with fn output(self, logger) {
  logger.write_string("LinearEasingPoint { input: ")
  self.input.output(logger)
  logger.write_string(", output: ")
  self.output.output(logger)
  logger.write_string(" }")
}

///|
pub fn LinearEasingPoint::new(
  input : Double,
  output : Double,
) -> LinearEasingPoint {
  { input, output }
}

///|
/// CSS easing/timing-function value without binding it to transitions or
/// animations. Consumers can sample it against normalized progress `[0, 1]`.
pub(all) enum Easing {
  Linear
  Ease
  EaseIn
  EaseOut
  EaseInOut
  EaseInSine
  EaseOutSine
  EaseInOutSine
  EaseInQuad
  EaseOutQuad
  EaseInOutQuad
  EaseInCubic
  EaseOutCubic
  EaseInOutCubic
  EaseInQuart
  EaseOutQuart
  EaseInOutQuart
  EaseInQuint
  EaseOutQuint
  EaseInOutQuint
  EaseInExpo
  EaseOutExpo
  EaseInOutExpo
  EaseInCirc
  EaseOutCirc
  EaseInOutCirc
  EaseInBack
  EaseOutBack
  EaseInOutBack
  EaseInElastic
  EaseOutElastic
  EaseInOutElastic
  EaseInBounce
  EaseOutBounce
  EaseInOutBounce
  LinearFunction(Array[LinearEasingPoint])
  CubicBezier(Double, Double, Double, Double)
  Steps(Int, StepPosition)
} derive(Debug, Eq)

///|
/// Eased value at one point in an interpolation sequence.
pub(all) struct EasingFrame {
  progress : Double
  eased_progress : Double
  value : Double
}

///|
pub impl Show for EasingFrame with fn output(self, logger) {
  logger.write_string("EasingFrame { progress: ")
  self.progress.output(logger)
  logger.write_string(", eased_progress: ")
  self.eased_progress.output(logger)
  logger.write_string(", value: ")
  self.value.output(logger)
  logger.write_string(" }")
}

///|
fn easing_variant_name(easing : Easing) -> String? {
  match easing {
    Linear => Some("Linear")
    Ease => Some("Ease")
    EaseIn => Some("EaseIn")
    EaseOut => Some("EaseOut")
    EaseInOut => Some("EaseInOut")
    EaseInSine => Some("EaseInSine")
    EaseOutSine => Some("EaseOutSine")
    EaseInOutSine => Some("EaseInOutSine")
    EaseInQuad => Some("EaseInQuad")
    EaseOutQuad => Some("EaseOutQuad")
    EaseInOutQuad => Some("EaseInOutQuad")
    EaseInCubic => Some("EaseInCubic")
    EaseOutCubic => Some("EaseOutCubic")
    EaseInOutCubic => Some("EaseInOutCubic")
    EaseInQuart => Some("EaseInQuart")
    EaseOutQuart => Some("EaseOutQuart")
    EaseInOutQuart => Some("EaseInOutQuart")
    EaseInQuint => Some("EaseInQuint")
    EaseOutQuint => Some("EaseOutQuint")
    EaseInOutQuint => Some("EaseInOutQuint")
    EaseInExpo => Some("EaseInExpo")
    EaseOutExpo => Some("EaseOutExpo")
    EaseInOutExpo => Some("EaseInOutExpo")
    EaseInCirc => Some("EaseInCirc")
    EaseOutCirc => Some("EaseOutCirc")
    EaseInOutCirc => Some("EaseInOutCirc")
    EaseInBack => Some("EaseInBack")
    EaseOutBack => Some("EaseOutBack")
    EaseInOutBack => Some("EaseInOutBack")
    EaseInElastic => Some("EaseInElastic")
    EaseOutElastic => Some("EaseOutElastic")
    EaseInOutElastic => Some("EaseInOutElastic")
    EaseInBounce => Some("EaseInBounce")
    EaseOutBounce => Some("EaseOutBounce")
    EaseInOutBounce => Some("EaseInOutBounce")
    LinearFunction(_) | CubicBezier(_, _, _, _) | Steps(_, _) => None
  }
}

///|
pub impl Show for Easing with fn output(self, logger) {
  match easing_variant_name(self) {
    Some(name) => logger.write_string(name)
    None =>
      match self {
        LinearFunction(points) => {
          logger.write_string("LinearFunction(")
          logger.write_string("[")
          for i = 0; i < points.length(); i = i + 1 {
            if i > 0 {
              logger.write_string(", ")
            }
            points[i].output(logger)
          }
          logger.write_string("]")
          logger.write_string(")")
        }
        CubicBezier(x1, y1, x2, y2) => {
          logger.write_string("CubicBezier(")
          x1.output(logger)
          logger.write_string(", ")
          y1.output(logger)
          logger.write_string(", ")
          x2.output(logger)
          logger.write_string(", ")
          y2.output(logger)
          logger.write_string(")")
        }
        Steps(count, position) => {
          logger.write_string("Steps(")
          count.output(logger)
          logger.write_string(", ")
          position.output(logger)
          logger.write_string(")")
        }
        _ => ()
      }
  }
}

///|
pub fn Easing::cubic_bezier(
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
) -> Easing {
  CubicBezier(x1, y1, x2, y2)
}

///|
pub fn Easing::steps(count : Int, position : StepPosition) -> Easing {
  Steps(count, position)
}

///|
pub fn Easing::linear(points : Array[LinearEasingPoint]) -> Easing? {
  if points.length() < 2 {
    return None
  }
  for i = 1; i < points.length(); i = i + 1 {
    if points[i].input < points[i - 1].input {
      return None
    }
  }
  Some(LinearFunction(points.copy()))
}

///|
fn apply_eased_progress(
  start_value : Double,
  end_value : Double,
  eased_progress : Double,
) -> Double {
  start_value + (end_value - start_value) * eased_progress
}

///|
fn clamp_unit(value : Double) -> Double {
  if value < 0.0 {
    0.0
  } else if value > 1.0 {
    1.0
  } else {
    value
  }
}

///|
fn cubic_bezier_axis(t : Double, p1 : Double, p2 : Double) -> Double {
  let inv = 1.0 - t
  3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t
}

///|
fn cubic_bezier_axis_derivative(t : Double, p1 : Double, p2 : Double) -> Double {
  let inv = 1.0 - t
  3.0 * inv * inv * p1 + 6.0 * inv * t * (p2 - p1) + 3.0 * t * t * (1.0 - p2)
}

///|
fn solve_cubic_bezier_t(x : Double, x1 : Double, x2 : Double) -> Double {
  let mut t = x
  for _i = 0; _i < 8; _i = _i + 1 {
    let x_at_t = cubic_bezier_axis(t, x1, x2)
    let dx = x_at_t - x
    if dx.abs() < 0.000001 {
      return t
    }
    let d = cubic_bezier_axis_derivative(t, x1, x2)
    if d.abs() < 0.000001 {
      break
    }
    t = clamp_unit(t - dx / d)
  }
  let mut low = 0.0
  let mut high = 1.0
  t = x
  for _i = 0; _i < 16; _i = _i + 1 {
    t = (low + high) / 2.0
    let x_at_t = cubic_bezier_axis(t, x1, x2)
    if (x_at_t - x).abs() < 0.000001 {
      return t
    }
    if x_at_t < x {
      low = t
    } else {
      high = t
    }
  }
  t
}

///|
fn pow(value : Double, exponent : Double) -> Double {
  @math.pow(value, exponent)
}

///|
fn ease_in_out_power(t : Double, exponent : Double) -> Double {
  if t < 0.5 {
    pow(2.0 * t, exponent) / 2.0
  } else {
    (2.0 - pow(-2.0 * t + 2.0, exponent)) / 2.0
  }
}

///|
fn ease_out_bounce(t : Double) -> Double {
  let n1 = 7.5625
  let d1 = 2.75
  if t < 1.0 / d1 {
    n1 * t * t
  } else if t < 2.0 / d1 {
    let x = t - 1.5 / d1
    n1 * x * x + 0.75
  } else if t < 2.5 / d1 {
    let x = t - 2.25 / d1
    n1 * x * x + 0.9375
  } else {
    let x = t - 2.625 / d1
    n1 * x * x + 0.984375
  }
}

///|
fn sample_linear_function(
  points : Array[LinearEasingPoint],
  t : Double,
) -> Double {
  if points.length() < 2 {
    return t
  }
  let mut point_a_index = 0
  for i = 0; i < points.length(); i = i + 1 {
    if points[i].input <= t {
      point_a_index = i
    }
  }
  if point_a_index == points.length() - 1 {
    point_a_index = point_a_index - 1
  }
  let point_a = points[point_a_index]
  let point_b = points[point_a_index + 1]
  if point_a.input == point_b.input {
    point_b.output
  } else {
    let progress_between_points = (t - point_a.input) /
      (point_b.input - point_a.input)
    point_a.output + progress_between_points * (point_b.output - point_a.output)
  }
}

///|
fn sample_steps(count : Int, position : StepPosition, t : Double) -> Double {
  if count <= 0 || (position == JumpNone && count <= 1) {
    return t
  }
  let steps = count.to_double()
  let mut current_step = (t * steps).floor()
  match position {
    Start | JumpBoth => current_step = current_step + 1.0
    End | JumpNone => ()
  }
  if current_step < 0.0 {
    current_step = 0.0
  }
  let jumps = match position {
    JumpNone => count - 1
    JumpBoth => count + 1
    Start | End => count
  }
  let jumps = jumps.to_double()
  if current_step > jumps {
    current_step = jumps
  }
  current_step / jumps
}

///|
/// Sample the easing at normalized progress. Input progress is clamped to
/// `[0, 1]`; cubic-bezier output may overshoot if its Y control points do.
pub fn Easing::sample(self : Easing, progress : Double) -> Double {
  let t = clamp_unit(progress)
  let one_minus_t = 1.0 - t
  match self {
    Linear => t
    Ease => CubicBezier(0.25, 0.1, 0.25, 1.0).sample(t)
    EaseIn => CubicBezier(0.42, 0.0, 1.0, 1.0).sample(t)
    EaseOut => CubicBezier(0.0, 0.0, 0.58, 1.0).sample(t)
    EaseInOut => CubicBezier(0.42, 0.0, 0.58, 1.0).sample(t)
    EaseInSine => 1.0 - @math.cos(t * @math.PI / 2.0)
    EaseOutSine => @math.sin(t * @math.PI / 2.0)
    EaseInOutSine => -(@math.cos(@math.PI * t) - 1.0) / 2.0
    EaseInQuad => t * t
    EaseOutQuad => 1.0 - one_minus_t * one_minus_t
    EaseInOutQuad => ease_in_out_power(t, 2.0)
    EaseInCubic => t * t * t
    EaseOutCubic => 1.0 - one_minus_t * one_minus_t * one_minus_t
    EaseInOutCubic => ease_in_out_power(t, 3.0)
    EaseInQuart => t * t * t * t
    EaseOutQuart => 1.0 - pow(one_minus_t, 4.0)
    EaseInOutQuart => ease_in_out_power(t, 4.0)
    EaseInQuint => t * t * t * t * t
    EaseOutQuint => 1.0 - pow(one_minus_t, 5.0)
    EaseInOutQuint => ease_in_out_power(t, 5.0)
    EaseInExpo => if t == 0.0 { 0.0 } else { pow(2.0, 10.0 * t - 10.0) }
    EaseOutExpo => if t == 1.0 { 1.0 } else { 1.0 - pow(2.0, -10.0 * t) }
    EaseInOutExpo =>
      if t == 0.0 {
        0.0
      } else if t == 1.0 {
        1.0
      } else if t < 0.5 {
        pow(2.0, 20.0 * t - 10.0) / 2.0
      } else {
        (2.0 - pow(2.0, -20.0 * t + 10.0)) / 2.0
      }
    EaseInCirc => 1.0 - (1.0 - t * t).sqrt()
    EaseOutCirc => (1.0 - (t - 1.0) * (t - 1.0)).sqrt()
    EaseInOutCirc =>
      if t < 0.5 {
        (1.0 - (1.0 - pow(2.0 * t, 2.0)).sqrt()) / 2.0
      } else {
        ((1.0 - pow(-2.0 * t + 2.0, 2.0)).sqrt() + 1.0) / 2.0
      }
    EaseInBack => {
      let c1 = 1.70158
      let c3 = c1 + 1.0
      c3 * t * t * t - c1 * t * t
    }
    EaseOutBack => {
      let c1 = 1.70158
      let c3 = c1 + 1.0
      1.0 + c3 * pow(t - 1.0, 3.0) + c1 * pow(t - 1.0, 2.0)
    }
    EaseInOutBack => {
      let c1 = 1.70158
      let c2 = c1 * 1.525
      if t < 0.5 {
        pow(2.0 * t, 2.0) * ((c2 + 1.0) * 2.0 * t - c2) / 2.0
      } else {
        (pow(2.0 * t - 2.0, 2.0) * ((c2 + 1.0) * (t * 2.0 - 2.0) + c2) + 2.0) /
        2.0
      }
    }
    EaseInElastic =>
      if t == 0.0 {
        0.0
      } else if t == 1.0 {
        1.0
      } else {
        let c4 = 2.0 * @math.PI / 3.0
        -pow(2.0, 10.0 * t - 10.0) * @math.sin((t * 10.0 - 10.75) * c4)
      }
    EaseOutElastic =>
      if t == 0.0 {
        0.0
      } else if t == 1.0 {
        1.0
      } else {
        let c4 = 2.0 * @math.PI / 3.0
        pow(2.0, -10.0 * t) * @math.sin((t * 10.0 - 0.75) * c4) + 1.0
      }
    EaseInOutElastic =>
      if t == 0.0 {
        0.0
      } else if t == 1.0 {
        1.0
      } else {
        let c5 = 2.0 * @math.PI / 4.5
        if t < 0.5 {
          -(pow(2.0, 20.0 * t - 10.0) * @math.sin((20.0 * t - 11.125) * c5)) /
          2.0
        } else {
          pow(2.0, -20.0 * t + 10.0) * @math.sin((20.0 * t - 11.125) * c5) / 2.0 +
          1.0
        }
      }
    EaseInBounce => 1.0 - ease_out_bounce(1.0 - t)
    EaseOutBounce => ease_out_bounce(t)
    EaseInOutBounce =>
      if t < 0.5 {
        (1.0 - ease_out_bounce(1.0 - 2.0 * t)) / 2.0
      } else {
        (1.0 + ease_out_bounce(2.0 * t - 1.0)) / 2.0
      }
    LinearFunction(points) => sample_linear_function(points, t)
    CubicBezier(x1, y1, x2, y2) =>
      if t == 0.0 || t == 1.0 {
        t
      } else {
        let bezier_t = solve_cubic_bezier_t(t, x1, x2)
        cubic_bezier_axis(bezier_t, y1, y2)
      }
    Steps(count, position) => sample_steps(count, position, t)
  }
}

///|
/// Apply the easing to a numeric range at normalized progress.
pub fn Easing::apply(
  self : Easing,
  start_value : Double,
  end_value : Double,
  progress : Double,
) -> Double {
  apply_eased_progress(start_value, end_value, self.sample(progress))
}

///|
/// Build one frame with the clamped input progress, eased progress and value.
pub fn Easing::frame(
  self : Easing,
  start_value : Double,
  end_value : Double,
  progress : Double,
) -> EasingFrame {
  let progress = clamp_unit(progress)
  let eased_progress = self.sample(progress)
  {
    progress,
    eased_progress,
    value: apply_eased_progress(start_value, end_value, eased_progress),
  }
}

///|
/// Build evenly spaced frames across the easing range. The sequence includes
/// both endpoints when `count >= 2`.
pub fn Easing::frames(
  self : Easing,
  start_value : Double,
  end_value : Double,
  count : Int,
) -> Array[EasingFrame] {
  let frames : Array[EasingFrame] = []
  if count <= 0 {
    return frames
  }
  if count == 1 {
    frames.push(self.frame(start_value, end_value, 0.0))
    return frames
  }
  let last_index = count - 1
  let denominator = last_index.to_double()
  for i = 0; i < count; i = i + 1 {
    let progress = i.to_double() / denominator
    frames.push(self.frame(start_value, end_value, progress))
  }
  frames
}

///|
fn parse_easing_number(value : String) -> Double? {
  Some(@string.parse_double(value.trim().to_owned())) catch {
    _ => None
  }
}

///|
fn parse_easing_int(value : String) -> Int? {
  Some(@string.parse_int(value.trim().to_owned())) catch {
    _ => None
  }
}

///|
fn parse_function_args(value : String, prefix : String) -> Array[String]? {
  if !value.has_prefix(prefix) || !value.has_suffix(")") {
    return None
  }
  let inner = value.unsafe_substring(
    start=prefix.length(),
    end=value.length() - 1,
  )
  Some(inner.split(",").map(fn(part) { part.trim().to_owned() }).collect())
}

///|
priv struct LinearStop {
  output : Double
  inputs : Array[Double]
}

///|
priv struct PendingLinearPoint {
  input : Double?
  output : Double
}

///|
fn parse_easing_percentage(value : String) -> Double? {
  let value = value.trim().to_owned()
  if !value.has_suffix("%") {
    return None
  }
  let number = value.unsafe_substring(start=0, end=value.length() - 1)
  match parse_easing_number(number) {
    Some(v) => Some(v / 100.0)
    None => None
  }
}

///|
fn parse_linear_stop(value : String) -> LinearStop? {
  let tokens = value
    .split(" ")
    .map(fn(token) { token.trim().to_owned() })
    .filter(fn(token) { !token.is_empty() })
  let inputs : Array[Double] = []
  let mut output : Double? = None
  for token in tokens {
    match parse_easing_percentage(token) {
      Some(input) => inputs.push(input)
      None =>
        match parse_easing_number(token) {
          Some(value) =>
            match output {
              Some(_) => return None
              None => output = Some(value)
            }
          None => return None
        }
    }
  }
  if inputs.length() > 2 {
    return None
  }
  match output {
    Some(output) => Some({ output, inputs })
    None => None
  }
}

///|
fn resolve_pending_linear_points(
  pending : Array[PendingLinearPoint],
) -> Array[LinearEasingPoint]? {
  let points : Array[LinearEasingPoint] = []
  let mut index = 0
  while index < pending.length() {
    match pending[index].input {
      Some(input) => {
        points.push(LinearEasingPoint::new(input, pending[index].output))
        index = index + 1
      }
      None => {
        if points.length() == 0 {
          return None
        }
        let previous_input = points[points.length() - 1].input
        let run_start = index
        let mut run_end = index
        while run_end < pending.length() && pending[run_end].input is None {
          run_end = run_end + 1
        }
        if run_end >= pending.length() {
          return None
        }
        let next_input = match pending[run_end].input {
          Some(v) => v
          None => return None
        }
        let denominator = (run_end - run_start + 1).to_double()
        for i = run_start; i < run_end; i = i + 1 {
          let ratio = (i - run_start + 1).to_double() / denominator
          let input = previous_input + (next_input - previous_input) * ratio
          points.push(LinearEasingPoint::new(input, pending[i].output))
        }
        index = run_end
      }
    }
  }
  Some(points)
}

///|
fn create_linear_easing_function(stops : Array[LinearStop]) -> Easing? {
  if stops.length() < 2 {
    return None
  }
  let pending : Array[PendingLinearPoint] = []
  let mut largest_input = 0.0
  let mut has_largest_input = false
  let last_stop_index = stops.length() - 1
  for i = 0; i < stops.length(); i = i + 1 {
    let stop = stops[i]
    if stop.inputs.length() > 0 {
      let input = if has_largest_input && stop.inputs[0] < largest_input {
        largest_input
      } else {
        stop.inputs[0]
      }
      pending.push({ input: Some(input), output: stop.output })
      largest_input = input
      has_largest_input = true
      if stop.inputs.length() == 2 {
        let input = if stop.inputs[1] < largest_input {
          largest_input
        } else {
          stop.inputs[1]
        }
        pending.push({ input: Some(input), output: stop.output })
        largest_input = input
      }
    } else if i == 0 {
      pending.push({ input: Some(0.0), output: stop.output })
      largest_input = 0.0
      has_largest_input = true
    } else if i == last_stop_index {
      let input = if has_largest_input && largest_input > 1.0 {
        largest_input
      } else {
        1.0
      }
      pending.push({ input: Some(input), output: stop.output })
    } else {
      pending.push({ input: None, output: stop.output })
    }
  }
  match resolve_pending_linear_points(pending) {
    Some(points) => Easing::linear(points)
    None => None
  }
}

///|
fn parse_linear_function(value : String) -> Easing? {
  match parse_function_args(value, "linear(") {
    Some(parts) if parts.length() >= 2 => {
      let stops : Array[LinearStop] = []
      for part in parts {
        match parse_linear_stop(part) {
          Some(stop) => stops.push(stop)
          None => return None
        }
      }
      create_linear_easing_function(stops)
    }
    _ => None
  }
}

///|
fn parse_cubic_bezier(value : String) -> Easing? {
  match parse_function_args(value, "cubic-bezier(") {
    Some(parts) if parts.length() == 4 =>
      match
        (
          parse_easing_number(parts[0]),
          parse_easing_number(parts[1]),
          parse_easing_number(parts[2]),
          parse_easing_number(parts[3]),
        ) {
        (Some(x1), Some(y1), Some(x2), Some(y2)) =>
          if x1 >= 0.0 && x1 <= 1.0 && x2 >= 0.0 && x2 <= 1.0 {
            Some(CubicBezier(x1, y1, x2, y2))
          } else {
            None
          }
        _ => None
      }
    _ => None
  }
}

///|
fn parse_steps(value : String) -> Easing? {
  match parse_function_args(value, "steps(") {
    Some(parts) if parts.length() == 1 =>
      match parse_easing_int(parts[0]) {
        Some(count) if count > 0 => Some(Steps(count, End))
        _ => None
      }
    Some(parts) if parts.length() == 2 =>
      match parse_easing_int(parts[0]) {
        Some(count) if count > 0 => {
          let position = match parts[1].to_lower() {
            "start" | "jump-start" => Some(StepPosition::Start)
            "end" | "jump-end" => Some(End)
            "jump-none" => Some(JumpNone)
            "jump-both" => Some(JumpBoth)
            _ => None
          }
          match position {
            Some(JumpNone) if count <= 1 => None
            Some(pos) => Some(Steps(count, pos))
            None => None
          }
        }
        _ => None
      }
    _ => None
  }
}

///|
fn remove_all(value : String, separator : String) -> String {
  value.split(separator).map(fn(part) { part.to_owned() }).collect().join("")
}

///|
fn normalize_easing_name(value : String) -> String {
  let lower = value.trim().to_lower().to_owned()
  remove_all(remove_all(remove_all(lower, "-"), "_"), " ")
}

///|
fn parse_named_easing(value : String) -> Easing? {
  match normalize_easing_name(value) {
    "linear" => Some(Linear)
    "ease" => Some(Ease)
    "easein" => Some(EaseIn)
    "easeout" => Some(EaseOut)
    "easeinout" => Some(EaseInOut)
    "stepstart" => Some(Steps(1, Start))
    "stepend" => Some(Steps(1, End))
    "easeinsine" => Some(EaseInSine)
    "easeoutsine" => Some(EaseOutSine)
    "easeinoutsine" => Some(EaseInOutSine)
    "easeinquad" => Some(EaseInQuad)
    "easeoutquad" => Some(EaseOutQuad)
    "easeinoutquad" => Some(EaseInOutQuad)
    "easeincubic" => Some(EaseInCubic)
    "easeoutcubic" => Some(EaseOutCubic)
    "easeinoutcubic" => Some(EaseInOutCubic)
    "easeinquart" => Some(EaseInQuart)
    "easeoutquart" => Some(EaseOutQuart)
    "easeinoutquart" => Some(EaseInOutQuart)
    "easeinquint" => Some(EaseInQuint)
    "easeoutquint" => Some(EaseOutQuint)
    "easeinoutquint" => Some(EaseInOutQuint)
    "easeinexpo" => Some(EaseInExpo)
    "easeoutexpo" => Some(EaseOutExpo)
    "easeinoutexpo" => Some(EaseInOutExpo)
    "easeincirc" => Some(EaseInCirc)
    "easeoutcirc" => Some(EaseOutCirc)
    "easeinoutcirc" => Some(EaseInOutCirc)
    "easeinback" => Some(EaseInBack)
    "easeoutback" => Some(EaseOutBack)
    "easeinoutback" => Some(EaseInOutBack)
    "easeinelastic" => Some(EaseInElastic)
    "easeoutelastic" => Some(EaseOutElastic)
    "easeinoutelastic" => Some(EaseInOutElastic)
    "easeinbounce" => Some(EaseInBounce)
    "easeoutbounce" => Some(EaseOutBounce)
    "easeinoutbounce" => Some(EaseInOutBounce)
    _ => None
  }
}