Add common base-class for playable instances
diff --git a/include/rive/animation/linear_animation_instance.hpp b/include/rive/animation/linear_animation_instance.hpp
index dcbdc67..71b0218 100644
--- a/include/rive/animation/linear_animation_instance.hpp
+++ b/include/rive/animation/linear_animation_instance.hpp
@@ -1,14 +1,15 @@
 #ifndef _RIVE_LINEAR_ANIMATION_INSTANCE_HPP_
 #define _RIVE_LINEAR_ANIMATION_INSTANCE_HPP_
+
 #include "rive/artboard.hpp"
+#include "rive/scene.hpp"
 
 namespace rive {
     class LinearAnimation;
 
-    class LinearAnimationInstance {
+    class LinearAnimationInstance : public Scene {
     private:
         const LinearAnimation* m_Animation = nullptr;
-        ArtboardInstance* m_ArtboardInstance;
         float m_Time;
         float m_TotalTime;
         float m_LastTotalTime;
@@ -17,13 +18,18 @@
         bool m_DidLoop;
         int m_LoopValue = -1;
 
+    protected:
+        bool isTranslucent() const override;
+        bool advanceAndApply(float seconds) override;
+        std::string name() const override;
+
     public:
-        LinearAnimationInstance(const LinearAnimation*, ArtboardInstance* instance);
+        LinearAnimationInstance(const LinearAnimation*, ArtboardInstance*);
 
         // Advance the animation by the specified time. Returns true if the
         // animation will continue to animate after this advance.
         bool advance(float seconds);
-
+    
         // Returns a pointer to the instance's animation
         const LinearAnimation* animation() const { return m_Animation; }
 
@@ -61,18 +67,17 @@
         float totalTime() const { return m_TotalTime; }
         float lastTotalTime() const { return m_LastTotalTime; }
         float spilledTime() const { return m_SpilledTime; }
-        float durationSeconds() const;
+        float durationSeconds() const override;
 
         // Forwarded from animation
         uint32_t fps() const;
         uint32_t duration() const;
         float speed() const;
         float startSeconds() const;
-        std::string name() const;
     
         // Returns either the animation's default or overridden loop values
-        Loop loop() { return (Loop)loopValue(); }
-        int loopValue();
+        Loop loop() const override { return (Loop)loopValue(); }
+        int loopValue() const;
         // Override the animation's default loop
         void loopValue(int value);
     };
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index 57b3e64..0506d23 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -6,6 +6,7 @@
 #include <vector>
 #include "rive/animation/linear_animation_instance.hpp"
 #include "rive/event_type.hpp"
+#include "rive/scene.hpp"
 
 namespace rive {
     class StateMachine;
@@ -19,12 +20,11 @@
     class StateMachineLayerInstance;
     class HitShape;
 
-    class StateMachineInstance {
+    class StateMachineInstance : public Scene {
         friend class SMIInput;
 
     private:
         const StateMachine* m_Machine;
-        ArtboardInstance* m_ArtboardInstance;
         bool m_NeedsAdvance = false;
 
         std::vector<SMIInput*> m_InputInstances;    // we own each pointer
@@ -40,9 +40,18 @@
         template <typename SMType, typename InstType>
         InstType* getNamedInput(const std::string& name) const;
 
+    protected:
+        float durationSeconds() const override { return -1; }
+        bool advanceAndApply(float secs) override;
+        Loop loop() const override { return Loop::oneShot; }
+        // For now play it safe -- unless we can inspect all of our
+        // possible states and animations...
+        bool isTranslucent() const override { return true; }
+        std::string name() const override;
+
     public:
         StateMachineInstance(const StateMachine* machine, ArtboardInstance* instance);
-        ~StateMachineInstance();
+        ~StateMachineInstance() override;
 
         // Advance the state machine by the specified time. Returns true if the
         // state machine will continue to animate after this advance.
@@ -54,14 +63,11 @@
         // Returns a pointer to the instance's stateMachine
         const StateMachine* stateMachine() const { return m_Machine; }
 
-        size_t inputCount() const { return m_InputInstances.size(); }
-        SMIInput* input(size_t index) const;
-
-        SMIBool* getBool(const std::string& name) const;
-        SMINumber* getNumber(const std::string& name) const;
-        SMITrigger* getTrigger(const std::string& name) const;
-
-        std::string name() const;
+        size_t inputCount() const override { return m_InputInstances.size(); }
+        SMIInput* input(size_t index) const override;
+        SMIBool* getBool(const std::string& name) const override;
+        SMINumber* getNumber(const std::string& name) const override;
+        SMITrigger* getTrigger(const std::string& name) const override;
 
         const size_t currentAnimationCount() const;
         const LinearAnimationInstance* currentAnimationByIndex(size_t index) const;
@@ -75,9 +81,9 @@
         // the empty string.
         const LayerState* stateChangedByIndex(size_t index) const;
 
-        void pointerMove(Vec2D position);
-        void pointerDown(Vec2D position);
-        void pointerUp(Vec2D position);
+        void pointerMove(Vec2D position) override;
+        void pointerDown(Vec2D position) override;
+        void pointerUp(Vec2D position) override;
     };
 } // namespace rive
 #endif
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
index 2306890..17daac9 100644
--- a/include/rive/artboard.hpp
+++ b/include/rive/artboard.hpp
@@ -119,6 +119,8 @@
         const std::vector<Core*>& objects() const { return m_Objects; }
 
         AABB bounds() const;
+
+        // Can we hide these from the public? (they use playable)
         bool isTranslucent(const LinearAnimation*) const;
         bool isTranslucent(const LinearAnimationInstance*) const;
 
diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp
new file mode 100644
index 0000000..90de466
--- /dev/null
+++ b/include/rive/scene.hpp
@@ -0,0 +1,58 @@
+#ifndef _RIVE_SCENE_HPP_
+#define _RIVE_SCENE_HPP_
+
+#include "rive/animation/loop.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/vec2d.hpp"
+#include <string>
+
+namespace rive {
+    class ArtboardInstance;
+    class Renderer;
+
+    class SMIInput;
+    class SMIBool;
+    class SMINumber;
+    class SMITrigger;
+
+    class Scene {    
+    protected:
+        ArtboardInstance* m_ArtboardInstance;
+
+        Scene(ArtboardInstance*);
+
+    public:
+        virtual ~Scene() {}
+    
+        float width() const;
+        float height() const;
+        AABB bounds() const { return {0, 0, this->width(), this->height()}; }
+
+        virtual std::string name() const = 0;
+
+        // Returns onShot if this has no looping (e.g. a statemachine)
+        virtual Loop loop() const = 0;
+        // Returns true iff the Scene is known to not be fully opaque
+        virtual bool isTranslucent() const = 0;
+        // returns -1 for continuous
+        virtual float durationSeconds() const = 0;
+    
+        // returns true if draw() should be called
+        virtual bool advanceAndApply(float elapsedSeconds) = 0;
+
+        void draw(Renderer*);
+
+        virtual void pointerDown(Vec2D);
+        virtual void pointerMove(Vec2D);
+        virtual void pointerUp(Vec2D);
+
+        virtual size_t inputCount() const;
+        virtual SMIInput* input(size_t index) const;
+        virtual SMIBool* getBool(const std::string&) const;
+        virtual SMINumber* getNumber(const std::string&) const;
+        virtual SMITrigger* getTrigger(const std::string&) const;
+    };
+
+} // namespace rive
+
+#endif
diff --git a/skia/viewer/src/main.cpp b/skia/viewer/src/main.cpp
index 8568137..a0ff9a7 100644
--- a/skia/viewer/src/main.cpp
+++ b/skia/viewer/src/main.cpp
@@ -22,6 +22,7 @@
 #include "rive/artboard.hpp"
 #include "rive/file.hpp"
 #include "rive/layout.hpp"
+#include "rive/playable.hpp"
 #include "rive/math/aabb.hpp"
 #include "skia_factory.hpp"
 #include "skia_renderer.hpp"
@@ -37,8 +38,7 @@
 std::string filename;
 std::unique_ptr<rive::File> currentFile;
 std::unique_ptr<rive::ArtboardInstance> artboardInstance;
-std::unique_ptr<rive::StateMachineInstance> stateMachineInstance;
-std::unique_ptr<rive::LinearAnimationInstance> animationInstance;
+std::unique_ptr<rive::Playable> currentPlayable;
 
 // ImGui wants raw pointers to names, but our public API returns
 // names as strings (by value), so we cache these names each time we
@@ -77,8 +77,7 @@
         fprintf(stderr, "failed to import file\n");
         return;
     }
-    animationInstance = nullptr;
-    stateMachineInstance = nullptr;
+    currentPlayable = nullptr;
     artboardInstance = nullptr;
 
     currentFile = std::move(file);
@@ -87,7 +86,8 @@
     loadNames(artboardInstance.get());
 
     if (index >= 0 && index < artboardInstance->stateMachineCount()) {
-        stateMachineInstance = artboardInstance->stateMachineAt(index);
+        currentPlayable = artboardInstance->stateMachineAt(index);
+        currentPlayable->inputCount();
     }
 }
 
@@ -101,8 +101,7 @@
         fprintf(stderr, "failed to import file\n");
         return;
     }
-    animationInstance = nullptr;
-    stateMachineInstance = nullptr;
+    currentPlayable = nullptr;
     artboardInstance = nullptr;
 
     currentFile = std::move(file);
@@ -111,7 +110,8 @@
     loadNames(artboardInstance.get());
 
     if (index >= 0 && index < artboardInstance->animationCount()) {
-        animationInstance = artboardInstance->animationAt(index);
+        currentPlayable = artboardInstance->animationAt(index);
+        currentPlayable->inputCount();
     }
 }
 
@@ -121,18 +121,18 @@
     float xscale, yscale;
     glfwGetWindowContentScale(window, &xscale, &yscale);
     lastWorldMouse = gInverseViewTransform * rive::Vec2D(x * xscale, y * yscale);
-    if (stateMachineInstance != nullptr) {
-        stateMachineInstance->pointerMove(lastWorldMouse);
+    if (currentPlayable) {
+        currentPlayable->pointerMove(lastWorldMouse);
     }
 }
 void glfwMouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
-    if (stateMachineInstance != nullptr) {
+    if (currentPlayable) {
         switch (action) {
             case GLFW_PRESS:
-                stateMachineInstance->pointerDown(lastWorldMouse);
+                currentPlayable->pointerDown(lastWorldMouse);
                 break;
             case GLFW_RELEASE:
-                stateMachineInstance->pointerUp(lastWorldMouse);
+                currentPlayable->pointerUp(lastWorldMouse);
                 break;
         }
     }
@@ -271,14 +271,8 @@
         paint.setColor(SK_ColorDKGRAY);
         canvas->drawPaint(paint);
 
-        if (artboardInstance != nullptr) {
-            if (animationInstance != nullptr) {
-                animationInstance->advance(elapsed);
-                animationInstance->apply();
-            } else if (stateMachineInstance != nullptr) {
-                stateMachineInstance->advance(elapsed);
-            }
-            artboardInstance->advance(elapsed);
+        if (currentPlayable) {
+            currentPlayable->advanceAndApply(elapsed);
 
             rive::SkiaRenderer renderer(canvas);
             renderer.save();
@@ -286,16 +280,15 @@
             auto viewTransform = rive::computeAlignment(rive::Fit::contain,
                                                         rive::Alignment::center,
                                                         rive::AABB(0, 0, width, height),
-                                                        artboardInstance->bounds());
+                                                        currentPlayable->bounds());
             renderer.transform(viewTransform);
             // Store the inverse view so we can later go from screen to world.
             gInverseViewTransform = viewTransform.invertOrIdentity();
             // post_mouse_event(artboard.get(), canvas->getTotalMatrix());
 
-            artboardInstance->draw(&renderer);
+            currentPlayable->draw(&renderer);
             renderer.restore();
         }
-
         context->flush();
 
         ImGui_ImplOpenGL3_NewFrame();
@@ -332,13 +325,13 @@
                 animationIndex = -1;
                 initStateMachine(stateMachineIndex);
             }
-            if (stateMachineInstance != nullptr) {
+            if (currentPlayable != nullptr) {
 
                 ImGui::Columns(2);
                 ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.6666);
 
-                for (int i = 0; i < stateMachineInstance->inputCount(); i++) {
-                    auto inputInstance = stateMachineInstance->input(i);
+                for (int i = 0; i < currentPlayable->inputCount(); i++) {
+                    auto inputInstance = currentPlayable->input(i);
 
                     if (inputInstance->input()->is<rive::StateMachineNumber>()) {
                         // ImGui requires names as id's, use ## to hide the
diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp
index 1c8badb..9819657 100644
--- a/src/animation/linear_animation_instance.cpp
+++ b/src/animation/linear_animation_instance.cpp
@@ -7,14 +7,21 @@
 
 LinearAnimationInstance::LinearAnimationInstance(const LinearAnimation* animation,
                                                  ArtboardInstance* instance) :
+    Scene(instance),
     m_Animation(animation),
-    m_ArtboardInstance(instance),
     m_Time(animation->enableWorkArea() ? (float)animation->workStart() / animation->fps() : 0),
     m_TotalTime(0.0f),
     m_LastTotalTime(0.0f),
     m_SpilledTime(0.0f),
     m_Direction(1) {}
 
+bool LinearAnimationInstance::advanceAndApply(float seconds) {
+    bool more = this->advance(seconds);
+    this->apply();
+    m_ArtboardInstance->advance(seconds);
+    return more;
+}
+
 bool LinearAnimationInstance::advance(float elapsedSeconds) {
     const LinearAnimation& animation = *m_Animation;
     m_Time += elapsedSeconds * animation.speed() * m_Direction;
@@ -126,8 +133,12 @@
     return m_Animation->name();
 }
 
+bool LinearAnimationInstance::isTranslucent() const {
+    return m_ArtboardInstance->isTranslucent(this);
+}
+
 // Returns either the animation's default or overridden loop values
-int LinearAnimationInstance::loopValue() {
+int LinearAnimationInstance::loopValue() const {
     if (m_LoopValue != -1) {
         return m_LoopValue;
     }
@@ -145,4 +156,4 @@
     m_LoopValue = value;
 }
 
-float LinearAnimationInstance::durationSeconds() const { return m_Animation->durationSeconds(); }
\ No newline at end of file
+float LinearAnimationInstance::durationSeconds() const { return m_Animation->durationSeconds(); }
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index fee23a4..0447d10 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -284,9 +284,10 @@
 }
 
 StateMachineInstance::StateMachineInstance(const StateMachine* machine,
-                                           ArtboardInstance* instance) :
-    m_Machine(machine), m_ArtboardInstance(instance) {
-    assert(instance->isInstance());
+                                           ArtboardInstance* instance)
+    : Scene(instance)
+    , m_Machine(machine)
+{
     const auto count = machine->inputCount();
     m_InputInstances.resize(count);
     for (size_t i = 0; i < count; i++) {
@@ -368,6 +369,12 @@
     return m_NeedsAdvance;
 }
 
+bool StateMachineInstance::advanceAndApply(float seconds) {
+    bool more = this->advance(seconds);
+    m_ArtboardInstance->advance(seconds);
+    return more;
+}
+
 void StateMachineInstance::markNeedsAdvance() { m_NeedsAdvance = true; }
 bool StateMachineInstance::needsAdvance() const { return m_NeedsAdvance; }
 
diff --git a/src/scene.cpp b/src/scene.cpp
new file mode 100644
index 0000000..d46302d
--- /dev/null
+++ b/src/scene.cpp
@@ -0,0 +1,30 @@
+#include "rive/artboard.hpp"
+#include "rive/scene.hpp"
+
+using namespace rive;
+
+Scene::Scene(ArtboardInstance* abi) : m_ArtboardInstance(abi) {
+    assert(m_ArtboardInstance->isInstance());
+}
+
+float Scene::width() const {
+    return m_ArtboardInstance->width();
+}
+
+float Scene::height() const {
+    return m_ArtboardInstance->height();
+}
+
+void Scene::draw(Renderer* renderer) {
+    m_ArtboardInstance->draw(renderer);
+}
+
+void Scene::pointerDown(Vec2D) {}
+void Scene::pointerMove(Vec2D) {}
+void Scene::pointerUp(Vec2D) {}
+
+size_t Scene::inputCount() const { return 0; }
+SMIInput* Scene::input(size_t index) const { return nullptr; }
+SMIBool* Scene::getBool(const std::string&) const { return nullptr; }
+SMINumber* Scene::getNumber(const std::string&) const { return nullptr; }
+SMITrigger* Scene::getTrigger(const std::string&) const { return nullptr; }