Elastic easing

Adds support for a (feature flagged) elastic easing interpolator option.

Needs a new icon and potentially some more UX to pick parameters under the string input field with the icon delineated parameters.

Currently only works in the timeline but it could be used on StateMachine Transitions too.

Diffs=
92c8f1164 Elastic easing (#6143)

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index a0b41a4..2e9e535 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-d65b239c5c6ce44c3b58cd71ff17721d874a4ab1
+92c8f1164db98bb5462f26dd73c89da4fa2c8b76
diff --git a/dev/defs/animation/cubic_interpolator.json b/dev/defs/animation/cubic_interpolator.json
index a810f29..cd2bddf 100644
--- a/dev/defs/animation/cubic_interpolator.json
+++ b/dev/defs/animation/cubic_interpolator.json
@@ -6,6 +6,7 @@
   },
   "abstract": true,
   "exportsWithContext": true,
+  "extends": "animation/keyframe_interpolator.json",
   "properties": {
     "x1": {
       "type": "double",
diff --git a/dev/defs/animation/elastic_interpolator.json b/dev/defs/animation/elastic_interpolator.json
new file mode 100644
index 0000000..0fd7895
--- /dev/null
+++ b/dev/defs/animation/elastic_interpolator.json
@@ -0,0 +1,37 @@
+{
+  "name": "ElasticInterpolator",
+  "key": {
+    "int": 174,
+    "string": "elastic_interpolator"
+  },
+  "exportsWithContext": true,
+  "extends": "animation/keyframe_interpolator.json",
+  "properties": {
+    "easingValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 405,
+        "string": "easing"
+      }
+    },
+    "amplitude": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 406,
+        "string": "amplitude"
+      },
+      "description": "The amplitude for the easing expressed as a percentage of the change."
+    },
+    "period": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 407,
+        "string": "period"
+      },
+      "description": "The period of the elastic expressed as a percentage of the time difference."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_interpolator.json b/dev/defs/animation/keyframe_interpolator.json
new file mode 100644
index 0000000..0e10a1c
--- /dev/null
+++ b/dev/defs/animation/keyframe_interpolator.json
@@ -0,0 +1,8 @@
+{
+  "name": "KeyFrameInterpolator",
+  "key": {
+    "int": 175,
+    "string": "keyframeinterpolator"
+  },
+  "abstract": true
+}
\ No newline at end of file
diff --git a/include/rive/animation/cubic_interpolator.hpp b/include/rive/animation/cubic_interpolator.hpp
index 85e5ed1..4b27621 100644
--- a/include/rive/animation/cubic_interpolator.hpp
+++ b/include/rive/animation/cubic_interpolator.hpp
@@ -10,14 +10,6 @@
 public:
     StatusCode onAddedDirty(CoreContext* context) override;
 
-    /// Convert a linear interpolation value to an eased one.
-    virtual float transformValue(float valueFrom, float valueTo, float factor) = 0;
-
-    /// Convert a linear interpolation factor to an eased one.
-    virtual float transform(float factor) const = 0;
-
-    StatusCode import(ImportStack& importStack) override;
-
 protected:
     CubicInterpolatorSolver m_solver;
 };
diff --git a/include/rive/animation/easing.hpp b/include/rive/animation/easing.hpp
new file mode 100644
index 0000000..46fd973
--- /dev/null
+++ b/include/rive/animation/easing.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_EASING_HPP_
+#define _RIVE_EASING_HPP_
+#include <cstdint>
+
+namespace rive
+{
+enum class Easing : uint8_t
+{
+    easeIn = 0,
+    easeOut = 1,
+    easeInOut = 2
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/elastic_ease.hpp b/include/rive/animation/elastic_ease.hpp
new file mode 100644
index 0000000..27fecae
--- /dev/null
+++ b/include/rive/animation/elastic_ease.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_ELASTIC_EASE_HPP_
+#define _RIVE_ELASTIC_EASE_HPP_
+
+namespace rive
+{
+class ElasticEase
+{
+public:
+    ElasticEase(float amplitude, float period);
+    float easeOut(float factor) const;
+    float easeIn(float factor) const;
+    float easeInOut(float factor) const;
+
+private:
+    float computeActualAmplitude(float time) const;
+    float m_amplitude;
+    float m_period;
+
+    // Computed phase shift for starting the sin function at 0 at factor 0.
+    float m_s;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/elastic_interpolator.hpp b/include/rive/animation/elastic_interpolator.hpp
new file mode 100644
index 0000000..0661f38
--- /dev/null
+++ b/include/rive/animation/elastic_interpolator.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_ELASTIC_INTERPOLATOR_HPP_
+#define _RIVE_ELASTIC_INTERPOLATOR_HPP_
+#include "rive/generated/animation/elastic_interpolator_base.hpp"
+#include "rive/animation/elastic_ease.hpp"
+#include "rive/animation/easing.hpp"
+
+namespace rive
+{
+class ElasticInterpolator : public ElasticInterpolatorBase
+{
+public:
+    ElasticInterpolator();
+    StatusCode onAddedDirty(CoreContext* context) override;
+    float transformValue(float valueFrom, float valueTo, float factor) override;
+    float transform(float factor) const override;
+
+    Easing easing() const { return (Easing)easingValue(); }
+
+private:
+    ElasticEase m_elastic;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/interpolating_keyframe.hpp b/include/rive/animation/interpolating_keyframe.hpp
index 8255f77..a2d31f6 100644
--- a/include/rive/animation/interpolating_keyframe.hpp
+++ b/include/rive/animation/interpolating_keyframe.hpp
@@ -1,13 +1,14 @@
 #ifndef _RIVE_INTERPOLATING_KEY_FRAME_HPP_
 #define _RIVE_INTERPOLATING_KEY_FRAME_HPP_
 #include "rive/generated/animation/interpolating_keyframe_base.hpp"
-#include <stdio.h>
+
 namespace rive
 {
+class KeyFrameInterpolator;
 class InterpolatingKeyFrame : public InterpolatingKeyFrameBase
 {
 public:
-    inline CubicInterpolator* interpolator() const { return m_interpolator; }
+    inline KeyFrameInterpolator* interpolator() const { return m_interpolator; }
     virtual void apply(Core* object, int propertyKey, float mix) = 0;
     virtual void applyInterpolation(Core* object,
                                     int propertyKey,
@@ -17,7 +18,7 @@
     StatusCode onAddedDirty(CoreContext* context) override;
 
 private:
-    CubicInterpolator* m_interpolator = nullptr;
+    KeyFrameInterpolator* m_interpolator = nullptr;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/keyframe.hpp b/include/rive/animation/keyframe.hpp
index a8b9397..b1621c8 100644
--- a/include/rive/animation/keyframe.hpp
+++ b/include/rive/animation/keyframe.hpp
@@ -3,8 +3,6 @@
 #include "rive/generated/animation/keyframe_base.hpp"
 namespace rive
 {
-class CubicInterpolator;
-
 class KeyFrame : public KeyFrameBase
 {
 public:
diff --git a/include/rive/animation/keyframe_interpolator.hpp b/include/rive/animation/keyframe_interpolator.hpp
new file mode 100644
index 0000000..392d634
--- /dev/null
+++ b/include/rive/animation/keyframe_interpolator.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_KEY_FRAME_INTERPOLATOR_HPP_
+#define _RIVE_KEY_FRAME_INTERPOLATOR_HPP_
+#include "rive/generated/animation/keyframe_interpolator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class KeyFrameInterpolator : public KeyFrameInterpolatorBase
+{
+public:
+    /// Convert a linear interpolation value to an eased one.
+    virtual float transformValue(float valueFrom, float valueTo, float factor) = 0;
+
+    /// Convert a linear interpolation factor to an eased one.
+    virtual float transform(float factor) const = 0;
+
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/cubic_ease_interpolator_base.hpp b/include/rive/generated/animation/cubic_ease_interpolator_base.hpp
index b650744..aae4cf0 100644
--- a/include/rive/generated/animation/cubic_ease_interpolator_base.hpp
+++ b/include/rive/generated/animation/cubic_ease_interpolator_base.hpp
@@ -19,6 +19,7 @@
         {
             case CubicEaseInterpolatorBase::typeKey:
             case CubicInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
                 return true;
             default:
                 return false;
diff --git a/include/rive/generated/animation/cubic_interpolator_base.hpp b/include/rive/generated/animation/cubic_interpolator_base.hpp
index 4506353..3ea421d 100644
--- a/include/rive/generated/animation/cubic_interpolator_base.hpp
+++ b/include/rive/generated/animation/cubic_interpolator_base.hpp
@@ -1,13 +1,13 @@
 #ifndef _RIVE_CUBIC_INTERPOLATOR_BASE_HPP_
 #define _RIVE_CUBIC_INTERPOLATOR_BASE_HPP_
-#include "rive/core.hpp"
+#include "rive/animation/keyframe_interpolator.hpp"
 #include "rive/core/field_types/core_double_type.hpp"
 namespace rive
 {
-class CubicInterpolatorBase : public Core
+class CubicInterpolatorBase : public KeyFrameInterpolator
 {
 protected:
-    typedef Core Super;
+    typedef KeyFrameInterpolator Super;
 
 public:
     static const uint16_t typeKey = 139;
@@ -19,6 +19,7 @@
         switch (typeKey)
         {
             case CubicInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
                 return true;
             default:
                 return false;
@@ -89,6 +90,7 @@
         m_Y1 = object.m_Y1;
         m_X2 = object.m_X2;
         m_Y2 = object.m_Y2;
+        KeyFrameInterpolator::copy(object);
     }
 
     bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
@@ -108,7 +110,7 @@
                 m_Y2 = CoreDoubleType::deserialize(reader);
                 return true;
         }
-        return false;
+        return KeyFrameInterpolator::deserialize(propertyKey, reader);
     }
 
 protected:
diff --git a/include/rive/generated/animation/cubic_value_interpolator_base.hpp b/include/rive/generated/animation/cubic_value_interpolator_base.hpp
index 7dedcce..1b159c2 100644
--- a/include/rive/generated/animation/cubic_value_interpolator_base.hpp
+++ b/include/rive/generated/animation/cubic_value_interpolator_base.hpp
@@ -19,6 +19,7 @@
         {
             case CubicValueInterpolatorBase::typeKey:
             case CubicInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
                 return true;
             default:
                 return false;
diff --git a/include/rive/generated/animation/elastic_interpolator_base.hpp b/include/rive/generated/animation/elastic_interpolator_base.hpp
new file mode 100644
index 0000000..ea1df11
--- /dev/null
+++ b/include/rive/generated/animation/elastic_interpolator_base.hpp
@@ -0,0 +1,108 @@
+#ifndef _RIVE_ELASTIC_INTERPOLATOR_BASE_HPP_
+#define _RIVE_ELASTIC_INTERPOLATOR_BASE_HPP_
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ElasticInterpolatorBase : public KeyFrameInterpolator
+{
+protected:
+    typedef KeyFrameInterpolator Super;
+
+public:
+    static const uint16_t typeKey = 174;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ElasticInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t easingValuePropertyKey = 405;
+    static const uint16_t amplitudePropertyKey = 406;
+    static const uint16_t periodPropertyKey = 407;
+
+private:
+    uint32_t m_EasingValue = 1;
+    float m_Amplitude = 1.0f;
+    float m_Period = 1.0f;
+
+public:
+    inline uint32_t easingValue() const { return m_EasingValue; }
+    void easingValue(uint32_t value)
+    {
+        if (m_EasingValue == value)
+        {
+            return;
+        }
+        m_EasingValue = value;
+        easingValueChanged();
+    }
+
+    inline float amplitude() const { return m_Amplitude; }
+    void amplitude(float value)
+    {
+        if (m_Amplitude == value)
+        {
+            return;
+        }
+        m_Amplitude = value;
+        amplitudeChanged();
+    }
+
+    inline float period() const { return m_Period; }
+    void period(float value)
+    {
+        if (m_Period == value)
+        {
+            return;
+        }
+        m_Period = value;
+        periodChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ElasticInterpolatorBase& object)
+    {
+        m_EasingValue = object.m_EasingValue;
+        m_Amplitude = object.m_Amplitude;
+        m_Period = object.m_Period;
+        KeyFrameInterpolator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case easingValuePropertyKey:
+                m_EasingValue = CoreUintType::deserialize(reader);
+                return true;
+            case amplitudePropertyKey:
+                m_Amplitude = CoreDoubleType::deserialize(reader);
+                return true;
+            case periodPropertyKey:
+                m_Period = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return KeyFrameInterpolator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void easingValueChanged() {}
+    virtual void amplitudeChanged() {}
+    virtual void periodChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_interpolator_base.hpp b/include/rive/generated/animation/keyframe_interpolator_base.hpp
new file mode 100644
index 0000000..f3e5773
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_interpolator_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_KEY_FRAME_INTERPOLATOR_BASE_HPP_
+#define _RIVE_KEY_FRAME_INTERPOLATOR_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class KeyFrameInterpolatorBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 175;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameInterpolatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    void copy(const KeyFrameInterpolatorBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index 1f8987f..658b81b 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -15,6 +15,7 @@
 #include "rive/animation/cubic_interpolator.hpp"
 #include "rive/animation/cubic_interpolator_component.hpp"
 #include "rive/animation/cubic_value_interpolator.hpp"
+#include "rive/animation/elastic_interpolator.hpp"
 #include "rive/animation/entry_state.hpp"
 #include "rive/animation/exit_state.hpp"
 #include "rive/animation/interpolating_keyframe.hpp"
@@ -26,6 +27,7 @@
 #include "rive/animation/keyframe_color.hpp"
 #include "rive/animation/keyframe_double.hpp"
 #include "rive/animation/keyframe_id.hpp"
+#include "rive/animation/keyframe_interpolator.hpp"
 #include "rive/animation/keyframe_string.hpp"
 #include "rive/animation/layer_state.hpp"
 #include "rive/animation/linear_animation.hpp"
@@ -248,6 +250,8 @@
                 return new BlendStateDirect();
             case NestedStateMachineBase::typeKey:
                 return new NestedStateMachine();
+            case ElasticInterpolatorBase::typeKey:
+                return new ElasticInterpolator();
             case ExitStateBase::typeKey:
                 return new ExitState();
             case NestedNumberBase::typeKey:
@@ -548,6 +552,9 @@
             case LinearAnimationBase::workEndPropertyKey:
                 object->as<LinearAnimationBase>()->workEnd(value);
                 break;
+            case ElasticInterpolatorBase::easingValuePropertyKey:
+                object->as<ElasticInterpolatorBase>()->easingValue(value);
+                break;
             case BlendState1DBase::inputIdPropertyKey:
                 object->as<BlendState1DBase>()->inputId(value);
                 break;
@@ -782,6 +789,12 @@
             case LinearAnimationBase::speedPropertyKey:
                 object->as<LinearAnimationBase>()->speed(value);
                 break;
+            case ElasticInterpolatorBase::amplitudePropertyKey:
+                object->as<ElasticInterpolatorBase>()->amplitude(value);
+                break;
+            case ElasticInterpolatorBase::periodPropertyKey:
+                object->as<ElasticInterpolatorBase>()->period(value);
+                break;
             case NestedNumberBase::nestedValuePropertyKey:
                 object->as<NestedNumberBase>()->nestedValue(value);
                 break;
@@ -1296,6 +1309,8 @@
                 return object->as<LinearAnimationBase>()->workStart();
             case LinearAnimationBase::workEndPropertyKey:
                 return object->as<LinearAnimationBase>()->workEnd();
+            case ElasticInterpolatorBase::easingValuePropertyKey:
+                return object->as<ElasticInterpolatorBase>()->easingValue();
             case BlendState1DBase::inputIdPropertyKey:
                 return object->as<BlendState1DBase>()->inputId();
             case BlendStateTransitionBase::exitBlendAnimationIdPropertyKey:
@@ -1455,6 +1470,10 @@
                 return object->as<KeyFrameDoubleBase>()->value();
             case LinearAnimationBase::speedPropertyKey:
                 return object->as<LinearAnimationBase>()->speed();
+            case ElasticInterpolatorBase::amplitudePropertyKey:
+                return object->as<ElasticInterpolatorBase>()->amplitude();
+            case ElasticInterpolatorBase::periodPropertyKey:
+                return object->as<ElasticInterpolatorBase>()->period();
             case NestedNumberBase::nestedValuePropertyKey:
                 return object->as<NestedNumberBase>()->nestedValue();
             case NestedRemapAnimationBase::timePropertyKey:
@@ -1776,6 +1795,7 @@
             case LinearAnimationBase::loopValuePropertyKey:
             case LinearAnimationBase::workStartPropertyKey:
             case LinearAnimationBase::workEndPropertyKey:
+            case ElasticInterpolatorBase::easingValuePropertyKey:
             case BlendState1DBase::inputIdPropertyKey:
             case BlendStateTransitionBase::exitBlendAnimationIdPropertyKey:
             case StrokeBase::capPropertyKey:
@@ -1853,6 +1873,8 @@
             case ListenerNumberChangeBase::valuePropertyKey:
             case KeyFrameDoubleBase::valuePropertyKey:
             case LinearAnimationBase::speedPropertyKey:
+            case ElasticInterpolatorBase::amplitudePropertyKey:
+            case ElasticInterpolatorBase::periodPropertyKey:
             case NestedNumberBase::nestedValuePropertyKey:
             case NestedRemapAnimationBase::timePropertyKey:
             case BlendAnimation1DBase::valuePropertyKey:
diff --git a/src/animation/cubic_interpolator.cpp b/src/animation/cubic_interpolator.cpp
index dd48298..e1e0b3c 100644
--- a/src/animation/cubic_interpolator.cpp
+++ b/src/animation/cubic_interpolator.cpp
@@ -1,8 +1,4 @@
 #include "rive/animation/cubic_interpolator.hpp"
-#include "rive/artboard.hpp"
-#include "rive/importers/artboard_importer.hpp"
-#include "rive/importers/import_stack.hpp"
-#include <cmath>
 
 using namespace rive;
 
@@ -10,15 +6,4 @@
 {
     m_solver.build(x1(), x2());
     return StatusCode::Ok;
-}
-
-StatusCode CubicInterpolator::import(ImportStack& importStack)
-{
-    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
-    if (artboardImporter == nullptr)
-    {
-        return StatusCode::MissingObject;
-    }
-    artboardImporter->addComponent(this);
-    return Super::import(importStack);
 }
\ No newline at end of file
diff --git a/src/animation/elastic_ease.cpp b/src/animation/elastic_ease.cpp
new file mode 100644
index 0000000..fd96d81
--- /dev/null
+++ b/src/animation/elastic_ease.cpp
@@ -0,0 +1,68 @@
+#include "rive/animation/elastic_ease.hpp"
+#include "rive/math/math_types.hpp"
+#include "math.h"
+
+using namespace rive;
+
+ElasticEase::ElasticEase(float amplitude, float period) :
+    m_amplitude(amplitude),
+    m_period(period),
+    m_s(amplitude < 1.0f ? period / 4.0f : period / (2.0f * math::PI) * asinf(1.0f / amplitude))
+{}
+
+float ElasticEase::computeActualAmplitude(float time) const
+{
+    if (m_amplitude < 1.0f)
+    {
+        /// We use this when the amplitude is less than 1.0 (amplitude is
+        /// described as factor of change in value). We also precompute s which is
+        /// the effective starting period we use to align the decaying sin with
+        /// our keyframe.
+        float t = abs(m_s);
+        float absTime = abs(time);
+        if (absTime < t)
+        {
+            float l = absTime / t;
+            return (m_amplitude * l) + (1.0f - l);
+        }
+    }
+
+    return m_amplitude;
+}
+
+float ElasticEase::easeOut(float factor) const
+{
+    float time = factor;
+    float actualAmplitude = computeActualAmplitude(time);
+
+    return (actualAmplitude * pow(2.0f, 10.0f * -time) *
+            sinf((time - m_s) * (2.0f * math::PI) / m_period)) +
+           1.0f;
+}
+
+float ElasticEase::easeIn(float factor) const
+{
+    float time = factor - 1.0f;
+
+    float actualAmplitude = computeActualAmplitude(time);
+
+    return -(actualAmplitude * pow(2.0f, 10.0f * time) *
+             sinf((-time - m_s) * (2.0f * math::PI) / m_period));
+}
+
+float ElasticEase::easeInOut(float factor) const
+{
+    float time = factor * 2.0f - 1.0f;
+    float actualAmplitude = computeActualAmplitude(time);
+    if (time < 0.0f)
+    {
+        return -0.5f * actualAmplitude * pow(2.0f, 10.0f * time) *
+               sinf((-time - m_s) * (2.0f * math::PI) / m_period);
+    }
+    else
+    {
+        return 0.5f * (actualAmplitude * pow(2.0f, 10.0f * -time) *
+                       sinf((time - m_s) * (2.0f * math::PI) / m_period)) +
+               1.0f;
+    }
+}
diff --git a/src/animation/elastic_interpolator.cpp b/src/animation/elastic_interpolator.cpp
new file mode 100644
index 0000000..bbf3907
--- /dev/null
+++ b/src/animation/elastic_interpolator.cpp
@@ -0,0 +1,30 @@
+#include "rive/animation/elastic_interpolator.hpp"
+
+using namespace rive;
+
+ElasticInterpolator::ElasticInterpolator() : m_elastic(1.0f, 1.0f) {}
+
+StatusCode ElasticInterpolator::onAddedDirty(CoreContext* context)
+{
+    m_elastic = ElasticEase(amplitude(), period());
+    return StatusCode::Ok;
+}
+
+float ElasticInterpolator::transformValue(float valueFrom, float valueTo, float factor)
+{
+    return valueFrom + (valueTo - valueFrom) * transform(factor);
+}
+
+float ElasticInterpolator::transform(float factor) const
+{
+    switch (easing())
+    {
+        case Easing::easeIn:
+            return m_elastic.easeIn(factor);
+        case Easing::easeOut:
+            return m_elastic.easeOut(factor);
+        case Easing::easeInOut:
+            return m_elastic.easeInOut(factor);
+    }
+    return factor;
+}
\ No newline at end of file
diff --git a/src/animation/interpolating_keyframe.cpp b/src/animation/interpolating_keyframe.cpp
index 5330538..7f7a0da 100644
--- a/src/animation/interpolating_keyframe.cpp
+++ b/src/animation/interpolating_keyframe.cpp
@@ -1,5 +1,5 @@
 #include "rive/animation/interpolating_keyframe.hpp"
-#include "rive/animation/cubic_interpolator.hpp"
+#include "rive/animation/keyframe_interpolator.hpp"
 #include "rive/core_context.hpp"
 
 using namespace rive;
@@ -9,11 +9,11 @@
     if (interpolatorId() != -1)
     {
         auto coreObject = context->resolve(interpolatorId());
-        if (coreObject == nullptr || !coreObject->is<CubicInterpolator>())
+        if (coreObject == nullptr || !coreObject->is<KeyFrameInterpolator>())
         {
             return StatusCode::MissingObject;
         }
-        m_interpolator = coreObject->as<CubicInterpolator>();
+        m_interpolator = coreObject->as<KeyFrameInterpolator>();
     }
 
     return StatusCode::Ok;
diff --git a/src/animation/keyframe_color.cpp b/src/animation/keyframe_color.cpp
index b9788c9..c5228de 100644
--- a/src/animation/keyframe_color.cpp
+++ b/src/animation/keyframe_color.cpp
@@ -32,9 +32,9 @@
     const KeyFrameColor& nextColor = *kfc;
     float f = (currentTime - seconds()) / (nextColor.seconds() - seconds());
 
-    if (CubicInterpolator* cubic = interpolator())
+    if (KeyFrameInterpolator* keyframeInterpolator = interpolator())
     {
-        f = cubic->transform(f);
+        f = keyframeInterpolator->transform(f);
     }
 
     applyColor(object, propertyKey, mix, colorLerp(value(), nextColor.value(), f));
diff --git a/src/animation/keyframe_double.cpp b/src/animation/keyframe_double.cpp
index ab5bea9..180981b 100644
--- a/src/animation/keyframe_double.cpp
+++ b/src/animation/keyframe_double.cpp
@@ -39,9 +39,9 @@
     float f = (currentTime - seconds()) / (nextDouble.seconds() - seconds());
 
     float frameValue;
-    if (CubicInterpolator* cubic = interpolator())
+    if (KeyFrameInterpolator* keyframeInterpolator = interpolator())
     {
-        frameValue = cubic->transformValue(value(), nextDouble.value(), f);
+        frameValue = keyframeInterpolator->transformValue(value(), nextDouble.value(), f);
     }
     else
     {
diff --git a/src/animation/keyframe_interpolator.cpp b/src/animation/keyframe_interpolator.cpp
new file mode 100644
index 0000000..6c972a8
--- /dev/null
+++ b/src/animation/keyframe_interpolator.cpp
@@ -0,0 +1,17 @@
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+StatusCode KeyFrameInterpolator::import(ImportStack& importStack)
+{
+    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+    if (artboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    artboardImporter->addComponent(this);
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/generated/animation/elastic_interpolator_base.cpp b/src/generated/animation/elastic_interpolator_base.cpp
new file mode 100644
index 0000000..757cb6b
--- /dev/null
+++ b/src/generated/animation/elastic_interpolator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/elastic_interpolator_base.hpp"
+#include "rive/animation/elastic_interpolator.hpp"
+
+using namespace rive;
+
+Core* ElasticInterpolatorBase::clone() const
+{
+    auto cloned = new ElasticInterpolator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/test/assets/test_elastic.riv b/test/assets/test_elastic.riv
new file mode 100644
index 0000000..a60b644
--- /dev/null
+++ b/test/assets/test_elastic.riv
Binary files differ
diff --git a/test/cubic_value_test.cpp b/test/cubic_value_test.cpp
index 05e1f3b..c36fd40 100644
--- a/test/cubic_value_test.cpp
+++ b/test/cubic_value_test.cpp
@@ -15,16 +15,7 @@
     auto greyRect = artboard->find<rive::Node>("grey_rectangle");
     REQUIRE(greyRect != nullptr);
 
-    int interpolatorCount = 0;
-    for (auto object : artboard->objects())
-    {
-        if (object->coreType() == rive::CubicValueInterpolatorBase::typeKey)
-        {
-            interpolatorCount++;
-        }
-    }
-
-    REQUIRE(interpolatorCount == 3);
+    REQUIRE(artboard->find<rive::CubicValueInterpolatorBase>().size() == 3);
 
     auto animation = artboard->animation("Timeline 1");
     REQUIRE(animation != nullptr);
diff --git a/test/elastic_easing_test.cpp b/test/elastic_easing_test.cpp
new file mode 100644
index 0000000..4fa4a94
--- /dev/null
+++ b/test/elastic_easing_test.cpp
@@ -0,0 +1,35 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/animation/elastic_interpolator.hpp>
+#include "rive/shapes/shape.hpp"
+#include "catch.hpp"
+#include "rive_file_reader.hpp"
+#include <cstdio>
+
+TEST_CASE("test elastic easing loads properly", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/test_elastic.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard != nullptr);
+
+    REQUIRE(artboard->find<rive::ElasticInterpolator>().size() == 1);
+
+    auto interpolator = artboard->find<rive::ElasticInterpolator>()[0];
+    REQUIRE(interpolator->easing() == rive::Easing::easeOut);
+    REQUIRE(interpolator->amplitude() == 1.0f);
+    REQUIRE(interpolator->period() == 0.25f);
+
+    REQUIRE(artboard->find<rive::Shape>().size() == 1);
+
+    auto shape = artboard->find<rive::Shape>()[0];
+    REQUIRE(shape->x() == Approx(145.19f));
+    auto animation = artboard->animation("Timeline 1");
+    REQUIRE(animation != nullptr);
+    // Go to frame 15.
+    animation->apply(artboard, 7.0f / animation->fps(), 1.0f);
+    REQUIRE(shape->x() == Approx(423.98f));
+
+    animation->apply(artboard, 14.0f / animation->fps(), 1.0f);
+    REQUIRE(shape->x() == Approx(303.995f));
+}