Move nonvirtual helpers into .cpp file
diff --git a/include/rive/renderer.hpp b/include/rive/renderer.hpp
index edced89..1b919ca 100644
--- a/include/rive/renderer.hpp
+++ b/include/rive/renderer.hpp
@@ -68,85 +68,22 @@
         virtual void
         drawImage(RenderImage* image, BlendMode value, float opacity) = 0;
 
+        // helpers
+
+        void translate(float x, float y);
+        void scale(float sx, float sy);
+        void rotate(float radians);
+
         void computeAlignment(Mat2D& result,
                               Fit fit,
                               const Alignment& alignment,
                               const AABB& frame,
-                              const AABB& content) {
-            float contentWidth = content[2] - content[0];
-            float contentHeight = content[3] - content[1];
-            float x = -content[0] - contentWidth / 2.0 -
-                      (alignment.x() * contentWidth / 2.0);
-            float y = -content[1] - contentHeight / 2.0 -
-                      (alignment.y() * contentHeight / 2.0);
-
-            float scaleX = 1.0, scaleY = 1.0;
-
-            switch (fit) {
-                case Fit::fill: {
-                    scaleX = frame.width() / contentWidth;
-                    scaleY = frame.height() / contentHeight;
-                    break;
-                }
-                case Fit::contain: {
-                    float minScale = std::fmin(frame.width() / contentWidth,
-                                               frame.height() / contentHeight);
-                    scaleX = scaleY = minScale;
-                    break;
-                }
-                case Fit::cover: {
-                    float maxScale = std::fmax(frame.width() / contentWidth,
-                                               frame.height() / contentHeight);
-                    scaleX = scaleY = maxScale;
-                    break;
-                }
-                case Fit::fitHeight: {
-                    float minScale = frame.height() / contentHeight;
-                    scaleX = scaleY = minScale;
-                    break;
-                }
-                case Fit::fitWidth: {
-                    float minScale = frame.width() / contentWidth;
-                    scaleX = scaleY = minScale;
-                    break;
-                }
-                case Fit::none: {
-                    scaleX = scaleY = 1.0;
-                    break;
-                }
-                case Fit::scaleDown: {
-                    float minScale = std::fmin(frame.width() / contentWidth,
-                                               frame.height() / contentHeight);
-                    scaleX = scaleY = minScale < 1.0 ? minScale : 1.0;
-                    break;
-                }
-            }
-
-            Mat2D translation;
-            translation[4] = frame[0] + frame.width() / 2.0 +
-                             (alignment.x() * frame.width() / 2.0);
-            translation[5] = frame[1] + frame.height() / 2.0 +
-                             (alignment.y() * frame.height() / 2.0);
-            Mat2D scale;
-            scale[0] = scaleX;
-            scale[3] = scaleY;
-
-            Mat2D translateBack;
-            translateBack[4] = x;
-            translateBack[5] = y;
-
-            Mat2D::multiply(result, translation, scale);
-            Mat2D::multiply(result, result, translateBack);
-        }
+                              const AABB& content);
 
         void align(Fit fit,
                    const Alignment& alignment,
                    const AABB& frame,
-                   const AABB& content) {
-            Mat2D result;
-            computeAlignment(result, fit, alignment, frame, content);
-            transform(result);
-        }
+                   const AABB& content);
     };
 
     extern RenderPath* makeRenderPath();
diff --git a/src/nested_artboard.cpp b/src/nested_artboard.cpp
index e42eb14..bd5c608 100644
--- a/src/nested_artboard.cpp
+++ b/src/nested_artboard.cpp
@@ -34,10 +34,8 @@
     }
     renderer->save();
     renderer->transform(worldTransform());
-    Mat2D translation;
-    translation[4] = -m_NestedInstance->originX() * m_NestedInstance->width();
-    translation[5] = -m_NestedInstance->originY() * m_NestedInstance->height();
-    renderer->transform(translation);
+    renderer->translate(-m_NestedInstance->originX() * m_NestedInstance->width(),
+                        -m_NestedInstance->originY() * m_NestedInstance->height());
     m_NestedInstance->draw(renderer);
     renderer->restore();
 }
@@ -89,4 +87,4 @@
     {
         m_NestedInstance->opacity(renderOpacity());
     }
-}
\ No newline at end of file
+}
diff --git a/src/renderer.cpp b/src/renderer.cpp
new file mode 100644
index 0000000..7153488
--- /dev/null
+++ b/src/renderer.cpp
@@ -0,0 +1,99 @@
+#include "rive/math/mat2d.hpp"
+#include "rive/renderer.hpp"
+
+using namespace rive;
+
+void Renderer::translate(float tx, float ty) {
+    this->transform(Mat2D(1, 0, 0, 1, tx, ty));
+}
+
+void Renderer::scale(float sx, float sy) {
+    this->transform(Mat2D(sx, 0, 0, sy, 0, 0));
+}
+
+void Renderer::rotate(float radians) {
+    const float s = std::sin(radians);
+    const float c = std::cos(radians);
+    this->transform(Mat2D(c, s, -s, c, 0, 0));
+}
+
+void Renderer::computeAlignment(Mat2D& result,
+                                Fit fit,
+                                const Alignment& alignment,
+                                const AABB& frame,
+                                const AABB& content)
+{
+    float contentWidth = content[2] - content[0];
+    float contentHeight = content[3] - content[1];
+    float x = -content[0] - contentWidth / 2.0 -
+            (alignment.x() * contentWidth / 2.0);
+    float y = -content[1] - contentHeight / 2.0 -
+            (alignment.y() * contentHeight / 2.0);
+
+    float scaleX = 1.0, scaleY = 1.0;
+
+    switch (fit) {
+      case Fit::fill: {
+          scaleX = frame.width() / contentWidth;
+          scaleY = frame.height() / contentHeight;
+          break;
+      }
+      case Fit::contain: {
+          float minScale = std::fmin(frame.width() / contentWidth,
+                                     frame.height() / contentHeight);
+          scaleX = scaleY = minScale;
+          break;
+      }
+      case Fit::cover: {
+          float maxScale = std::fmax(frame.width() / contentWidth,
+                                     frame.height() / contentHeight);
+          scaleX = scaleY = maxScale;
+          break;
+      }
+      case Fit::fitHeight: {
+          float minScale = frame.height() / contentHeight;
+          scaleX = scaleY = minScale;
+          break;
+      }
+      case Fit::fitWidth: {
+          float minScale = frame.width() / contentWidth;
+          scaleX = scaleY = minScale;
+          break;
+      }
+      case Fit::none: {
+          scaleX = scaleY = 1.0;
+          break;
+      }
+      case Fit::scaleDown: {
+          float minScale = std::fmin(frame.width() / contentWidth,
+                                     frame.height() / contentHeight);
+          scaleX = scaleY = minScale < 1.0 ? minScale : 1.0;
+          break;
+      }
+    }
+
+    Mat2D translation;
+    translation[4] = frame[0] + frame.width() / 2.0 +
+                   (alignment.x() * frame.width() / 2.0);
+    translation[5] = frame[1] + frame.height() / 2.0 +
+                   (alignment.y() * frame.height() / 2.0);
+    Mat2D scale;
+    scale[0] = scaleX;
+    scale[3] = scaleY;
+
+    Mat2D translateBack;
+    translateBack[4] = x;
+    translateBack[5] = y;
+
+    Mat2D::multiply(result, translation, scale);
+    Mat2D::multiply(result, result, translateBack);
+}
+
+void Renderer::align(Fit fit,
+                     const Alignment& alignment,
+                     const AABB& frame,
+                     const AABB& content) {
+    Mat2D result;
+    computeAlignment(result, fit, alignment, frame, content);
+    transform(result);
+}
diff --git a/src/shapes/image.cpp b/src/shapes/image.cpp
index 4bc4b6a..78d7204 100644
--- a/src/shapes/image.cpp
+++ b/src/shapes/image.cpp
@@ -21,12 +21,8 @@
     auto width = renderImage->width();
     auto height = renderImage->height();
 
-    const Mat2D& transform = worldTransform();
-    renderer->transform(transform);
-
-    Mat2D originTranslation(
-        1.0f, 0.0f, 0.0f, 1.0f, -width / 2.0f, -height / 2.0f);
-    renderer->transform(originTranslation);
+    renderer->transform(worldTransform());
+    renderer->translate(-width / 2.0f, -height / 2.0f);
 
     renderer->drawImage(renderImage, blendMode(), renderOpacity());
 
@@ -58,4 +54,4 @@
     Image* twin = ImageBase::clone()->as<Image>();
     twin->m_ImageAsset = m_ImageAsset;
     return twin;
-}
\ No newline at end of file
+}
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp
index 2ffe390..78fbb30 100644
--- a/src/shapes/shape.cpp
+++ b/src/shapes/shape.cpp
@@ -44,8 +44,7 @@
         bool paintsInLocal =
             (shapePaint->pathSpace() & PathSpace::Local) == PathSpace::Local;
         if (paintsInLocal) {
-            const Mat2D& transform = worldTransform();
-            renderer->transform(transform);
+            renderer->transform(worldTransform());
         }
         shapePaint->draw(renderer,
                          paintsInLocal ? m_PathComposer.localPath()
@@ -84,4 +83,4 @@
     }
     // This ensures context propagates to path composer too.
     return m_PathComposer.onAddedDirty(context);
-}
\ No newline at end of file
+}