Playable as a high-level api
diff --git a/include/rive/playable.hpp b/include/rive/playable.hpp
new file mode 100644
index 0000000..2b834a7
--- /dev/null
+++ b/include/rive/playable.hpp
@@ -0,0 +1,70 @@
+#ifndef _RIVE_PLAYABLE_HPP_
+#define _RIVE_PLAYABLE_HPP_
+
+#include "rive/math/aabb.hpp"
+#include "rive/span.hpp"
+
+namespace rive {
+    class Factory;
+    class FileAssetResolver;
+    class Renderer;
+
+    class ArtboardInstance;
+    class File;
+    class LinearAnimationInstance;
+    class StateMachineInstance;
+
+    class SMIInput;
+    class SMIBool;
+    class SMINumber;
+    class SMITrigger;
+
+    class Playable {
+    private:
+        std::unique_ptr<File> m_File;
+        std::unique_ptr<ArtboardInstance> m_ABI;
+        std::unique_ptr<LinearAnimationInstance> m_LAI;
+        std::unique_ptr<StateMachineInstance> m_SMI;
+    
+        Playable();
+
+    public:
+        static std::unique_ptr<Playable> import(Span<const uint8_t>,
+                                                Factory*,
+                                                FileAssetResolver* = nullptr);
+
+        static std::unique_ptr<Playable>
+        animationAt(File*, size_t artboardIndex, size_t animationIndex);
+    
+        static std::unique_ptr<Playable>
+        stateMachineAt(File*, size_t artboardIndex, size_t machineIndex);
+    
+        ~Playable();
+
+        AABB bounds() const;
+
+        // returns -1 for continuous
+        float durationSeconds() const;
+
+        // returns true if draw() should be called
+        bool advance(float elapsedSeconds);
+
+        void draw(Renderer*);
+
+        enum class PointerState {
+            kDown,      // pointer changed from 'up' to 'down'
+            kUp,        // pointer changed from 'down' to 'up'
+            kMoved,     // pointer position changed
+        };
+        void pointerEvent(Vec2D, PointerState);
+
+        size_t inputCount() const;
+        SMIInput* inputAt(size_t index) const;
+        SMIBool* boolNamed(const std::string&) const;
+        SMINumber* numberNamed(const std::string&) const;
+        SMITrigger* triggerNamed(const std::string&) const;
+    };
+
+} // namespace rive
+
+#endif
diff --git a/src/playable.cpp b/src/playable.cpp
new file mode 100644
index 0000000..7c2e072
--- /dev/null
+++ b/src/playable.cpp
@@ -0,0 +1,126 @@
+#include "rive/artboard.hpp"
+#include "rive/file.hpp"
+#include "rive/file_asset_resolver.hpp"
+#include "rive/playable.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+using namespace rive;
+
+Playable::Playable() {}
+
+Playable::~Playable() {}
+
+std::unique_ptr<Playable> Playable::animationAt(File* file,
+                                                size_t artboardIndex,
+                                                size_t animationIndex) {
+    auto abi = file->artboardAt(artboardIndex);
+    if (abi) {
+        auto lai = abi->animationAt(animationIndex);
+        if (lai) {
+            std::unique_ptr<Playable> play(new Playable);
+            play->m_ABI = std::move(abi);
+            play->m_LAI = std::move(lai);
+            return play;
+        }
+    }
+    return nullptr;
+}
+
+std::unique_ptr<Playable> Playable::stateMachineAt(File* file,
+                                                   size_t artboardIndex,
+                                                   size_t machineIndex) {
+    auto abi = file->artboardAt(artboardIndex);
+    if (abi) {
+        auto smi = abi->stateMachineAt(machineIndex);
+        if (smi) {
+            std::unique_ptr<Playable> play(new Playable);
+            play->m_ABI = std::move(abi);
+            play->m_SMI = std::move(smi);
+            return play;
+        }
+    }
+    return nullptr;
+}
+
+std::unique_ptr<Playable> Playable::import(Span<const uint8_t> bytes,
+                                           Factory* factory,
+                                           FileAssetResolver* resolver)
+{
+    auto file = File::import(bytes, factory, nullptr, resolver);
+    if (!file) {
+        return nullptr;
+    }
+
+    auto play = Playable::stateMachineAt(file.get(), 0, 0);
+    if (!play) {
+        play = Playable::animationAt(file.get(), 0, 0);
+    }
+    if (play) {
+        play->m_File = std::move(file);
+    }
+    return play;
+}
+
+AABB Playable::bounds() const {
+    return m_ABI->bounds();
+}
+
+float Playable::durationSeconds() const {
+    return m_SMI ? -1 : m_LAI->durationSeconds();
+}
+
+// returns true if draw() should be called
+bool Playable::advance(float elapsedSeconds) {
+    bool needsRedraw;
+    if (m_SMI) {
+        needsRedraw = m_SMI->advance(elapsedSeconds);
+    } else {
+        needsRedraw = m_LAI->advance(elapsedSeconds);
+        m_LAI->apply();
+    }
+    return needsRedraw;
+}
+
+void Playable::draw(Renderer* renderer) {
+    m_ABI->draw(renderer);
+}
+
+void Playable::pointerEvent(Vec2D pos, PointerState state) {
+    if (m_SMI) {
+        switch (state) {
+            case PointerState::kDown:
+                m_SMI->pointerDown(pos);
+                break;
+            case PointerState::kMoved:
+                m_SMI->pointerMove(pos);
+                break;
+            case PointerState::kUp:
+                m_SMI->pointerUp(pos);
+                break;
+        }
+    }
+    // else the animation just ignores the pointer events
+}
+
+// StateMachine Inputs
+
+size_t Playable::inputCount() const {
+    return m_SMI ? m_SMI->inputCount() : 0;
+}
+
+SMIInput* Playable::inputAt(size_t index) const {
+    return m_SMI ? m_SMI->input(index) : nullptr;
+}
+
+SMIBool* Playable::boolNamed(const std::string& name) const {
+    return m_SMI ? m_SMI->getBool(name) : nullptr;
+}
+
+SMINumber* Playable::numberNamed(const std::string& name) const {
+    return m_SMI ? m_SMI->getNumber(name) : nullptr;
+}
+
+SMITrigger* Playable::triggerNamed(const std::string& name) const {
+    return m_SMI ? m_SMI->getTrigger(name) : nullptr;
+}