[gtk/path-work-rebased: 76/118] Ottie: Add




commit e502de6712cb006f3b38bcf21053cd1ca1fc4830
Author: Benjamin Otte <otte redhat com>
Date:   Sat Dec 12 03:38:10 2020 +0100

    Ottie: Add

 meson.build                          |   1 +
 ottie/meson.build                    |  71 ++++
 ottie/ottie.h                        |  31 ++
 ottie/ottiecolorvalue.c              | 149 +++++++
 ottie/ottiecolorvalueprivate.h       |  54 +++
 ottie/ottiecomposition.c             | 268 +++++++++++++
 ottie/ottiecompositionlayer.c        | 153 +++++++
 ottie/ottiecompositionlayerprivate.h |  49 +++
 ottie/ottiecompositionprivate.h      |  47 +++
 ottie/ottiecreation.c                | 746 +++++++++++++++++++++++++++++++++++
 ottie/ottiecreation.h                |  83 ++++
 ottie/ottiecreationprivate.h         |  40 ++
 ottie/ottiedoublevalue.c             | 121 ++++++
 ottie/ottiedoublevalueprivate.h      |  58 +++
 ottie/ottieellipseshape.c            | 138 +++++++
 ottie/ottieellipseshapeprivate.h     |  45 +++
 ottie/ottiefillshape.c               | 132 +++++++
 ottie/ottiefillshapeprivate.h        |  45 +++
 ottie/ottiegroupshape.c              | 247 ++++++++++++
 ottie/ottiegroupshapeprivate.h       |  50 +++
 ottie/ottieintl.h                    |  15 +
 ottie/ottiekeyframesimpl.c           | 382 ++++++++++++++++++
 ottie/ottielayer.c                   |  96 +++++
 ottie/ottielayerprivate.h            |  93 +++++
 ottie/ottienulllayer.c               |  66 ++++
 ottie/ottienulllayerprivate.h        |  45 +++
 ottie/ottieobject.c                  | 175 ++++++++
 ottie/ottieobjectprivate.h           |  68 ++++
 ottie/ottiepaintable.c               | 381 ++++++++++++++++++
 ottie/ottiepaintable.h               |  54 +++
 ottie/ottieparser.c                  | 592 +++++++++++++++++++++++++++
 ottie/ottieparserprivate.h           | 115 ++++++
 ottie/ottiepathshape.c               | 105 +++++
 ottie/ottiepathshapeprivate.h        |  45 +++
 ottie/ottiepathvalue.c               | 402 +++++++++++++++++++
 ottie/ottiepathvalueprivate.h        |  53 +++
 ottie/ottieplayer.c                  | 490 +++++++++++++++++++++++
 ottie/ottieplayer.h                  |  59 +++
 ottie/ottiepoint3dvalue.c            | 154 ++++++++
 ottie/ottiepoint3dvalueprivate.h     |  54 +++
 ottie/ottiepointvalue.c              | 140 +++++++
 ottie/ottiepointvalueprivate.h       |  53 +++
 ottie/ottierectshape.c               | 219 ++++++++++
 ottie/ottierectshapeprivate.h        |  45 +++
 ottie/ottierender.c                  | 276 +++++++++++++
 ottie/ottierenderprivate.h           |  91 +++++
 ottie/ottieshape.c                   |  56 +++
 ottie/ottieshapelayer.c              | 124 ++++++
 ottie/ottieshapelayerprivate.h       |  47 +++
 ottie/ottieshapeprivate.h            |  69 ++++
 ottie/ottiestrokeshape.c             | 152 +++++++
 ottie/ottiestrokeshapeprivate.h      |  45 +++
 ottie/ottietransform.c               | 187 +++++++++
 ottie/ottietransformprivate.h        |  48 +++
 ottie/ottietrimshape.c               | 220 +++++++++++
 ottie/ottietrimshapeprivate.h        |  45 +++
 ottie/ottievalueimpl.c               | 133 +++++++
 tests/meson.build                    |  10 +-
 58 files changed, 7927 insertions(+), 5 deletions(-)
---
diff --git a/meson.build b/meson.build
index 630ac99854..5d3376199e 100644
--- a/meson.build
+++ b/meson.build
@@ -783,6 +783,7 @@ project_build_root = meson.current_build_dir()
 subdir('gtk/css')
 subdir('gdk')
 subdir('gsk')
+subdir('ottie')
 subdir('gtk')
 subdir('modules')
 if get_option('demos')
diff --git a/ottie/meson.build b/ottie/meson.build
new file mode 100644
index 0000000000..fd2a8a7b22
--- /dev/null
+++ b/ottie/meson.build
@@ -0,0 +1,71 @@
+ottie_public_sources = files([
+  'ottiecreation.c',
+  'ottiepaintable.c',
+  'ottieplayer.c',
+])
+
+ottie_private_sources = files([
+  'ottiecolorvalue.c',
+  'ottiecomposition.c',
+  'ottiecompositionlayer.c',
+  'ottiedoublevalue.c',
+  'ottieellipseshape.c',
+  'ottiefillshape.c',
+  'ottiegroupshape.c',
+  'ottielayer.c',
+  'ottienulllayer.c',
+  'ottieobject.c',
+  'ottieparser.c',
+  'ottiepathshape.c',
+  'ottiepathvalue.c',
+  'ottiepointvalue.c',
+  'ottiepoint3dvalue.c',
+  'ottierectshape.c',
+  'ottierender.c',
+  'ottieshape.c',
+  'ottieshapelayer.c',
+  'ottiestrokeshape.c',
+  'ottietransform.c',
+  'ottietrimshape.c',
+])
+
+ottie_public_headers = files([
+  'ottie.h',
+  'ottiecreation.h',
+  'ottiepaintable.h',
+  'ottieplayer.h',
+])
+
+install_headers(ottie_public_headers, 'ottie.h', subdir: 'gtk-4.0/ottie')
+
+json_glib_dep = dependency('json-glib-1.0', required: true)
+ottie_deps = [
+  libm,
+  glib_dep,
+  gobject_dep,
+  platform_gio_dep,
+  libgdk_dep,
+  libgsk_dep,
+  json_glib_dep
+]
+
+libottie = static_library('ottie',
+                            sources: [
+                              ottie_public_sources,
+                              ottie_private_sources,
+                            ],
+                            dependencies: ottie_deps,
+                            include_directories: [ confinc, ],
+                            c_args: [
+                              '-DGTK_COMPILATION',
+                              '-DG_LOG_DOMAIN="Ottie"',
+                            ] + common_cflags,
+                            link_with: [libgdk, libgsk ],
+                            link_args: common_ldflags)
+
+# We don't have link_with: to internal static libs here on purpose, just
+# list the dependencies and generated headers and such, for use in the
+# "public" libgtk_dep used by internal executables.
+libottie_dep = declare_dependency(include_directories: [ confinc, ],
+                                  dependencies: ottie_deps)
+
diff --git a/ottie/ottie.h b/ottie/ottie.h
new file mode 100644
index 0000000000..c2da341e41
--- /dev/null
+++ b/ottie/ottie.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_H__
+#define __OTTIE_H__
+
+#define __OTTIE_H_INSIDE__
+
+#include <ottie/ottiecreation.h>
+#include <ottie/ottiepaintable.h>
+#include <ottie/ottieplayer.h>
+
+#undef __OTTIE_H_INSIDE__
+
+#endif /* __OTTIE_H__ */
diff --git a/ottie/ottiecolorvalue.c b/ottie/ottiecolorvalue.c
new file mode 100644
index 0000000000..7b81afd1ee
--- /dev/null
+++ b/ottie/ottiecolorvalue.c
@@ -0,0 +1,149 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiecolorvalueprivate.h"
+
+#include "ottieparserprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+static gboolean
+ottie_color_value_parse_one (JsonReader *reader,
+                             gsize       offset,
+                             gpointer    data)
+{
+  GdkRGBA *rgba = (GdkRGBA *) ((guint8 *) data + offset);
+  double d[3];
+
+  if (!ottie_parser_parse_array (reader, "color value",
+                                 3, 3, NULL,
+                                 0, sizeof (double),
+                                 ottie_parser_option_double,
+                                 d))
+    {
+      d[0] = d[1] = d[2] = 0;
+    }
+
+  rgba->red = d[0];
+  rgba->green = d[1];
+  rgba->blue = d[2];
+  rgba->alpha = 1;
+
+  return TRUE;
+}
+
+static void
+ottie_color_value_interpolate (const GdkRGBA *start,
+                               const GdkRGBA *end,
+                               double         progress,
+                               GdkRGBA       *result)
+{
+  result->red = start->red + progress * (end->red - start->red);
+  result->green = start->green + progress * (end->green - start->green);
+  result->blue = start->blue + progress * (end->blue - start->blue);
+  result->alpha = start->alpha + progress * (end->alpha - start->alpha);
+}
+
+#define OTTIE_KEYFRAMES_NAME ottie_color_keyframes
+#define OTTIE_KEYFRAMES_TYPE_NAME OttieColorKeyframes
+#define OTTIE_KEYFRAMES_ELEMENT_TYPE GdkRGBA
+#define OTTIE_KEYFRAMES_BY_VALUE 1
+#define OTTIE_KEYFRAMES_DIMENSIONS 4
+#define OTTIE_KEYFRAMES_PARSE_FUNC ottie_color_value_parse_one
+#define OTTIE_KEYFRAMES_INTERPOLATE_FUNC ottie_color_value_interpolate
+#include "ottiekeyframesimpl.c"
+
+void
+ottie_color_value_init (OttieColorValue *self,
+                        const GdkRGBA   *value)
+{
+  self->is_static = TRUE;
+  self->static_value = *value;
+}
+
+void
+ottie_color_value_clear (OttieColorValue *self)
+{
+  if (!self->is_static)
+    g_clear_pointer (&self->keyframes, ottie_color_keyframes_free);
+}
+
+void
+ottie_color_value_get (OttieColorValue *self,
+                       double           timestamp,
+                       GdkRGBA         *rgba)
+{
+  if (self->is_static)
+    {
+      *rgba = self->static_value;
+      return;
+    }
+  
+  ottie_color_keyframes_get (self->keyframes, timestamp, rgba);
+}
+
+gboolean
+ottie_color_value_parse (JsonReader *reader,
+                         gsize       offset,
+                         gpointer    data)
+{
+  OttieColorValue *self = (OttieColorValue *) ((guint8 *) data + offset);
+
+  if (json_reader_read_member (reader, "k"))
+    {
+      gboolean is_static;
+
+      if (!json_reader_is_array (reader))
+        is_static = TRUE;
+      else
+        {
+          if (json_reader_read_element (reader, 0))
+            is_static = !json_reader_is_object (reader);
+          else
+            is_static = TRUE;
+          json_reader_end_element (reader);
+        }
+
+      if (is_static)
+        {
+          self->is_static = TRUE;
+          ottie_color_value_parse_one (reader, 0, &self->static_value);
+        }
+      else
+        {
+          self->is_static = FALSE;
+          self->keyframes = ottie_color_keyframes_parse (reader);
+          if (self->keyframes == NULL)
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+        }
+    }
+  else
+    {
+      ottie_parser_error_syntax (reader, "Property is not a color value");
+    }
+  json_reader_end_member (reader);
+
+  return TRUE;
+}
+
diff --git a/ottie/ottiecolorvalueprivate.h b/ottie/ottiecolorvalueprivate.h
new file mode 100644
index 0000000000..48998d807a
--- /dev/null
+++ b/ottie/ottiecolorvalueprivate.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_COLOR_VALUE_PRIVATE_H__
+#define __OTTIE_COLOR_VALUE_PRIVATE_H__
+
+#include <json-glib/json-glib.h>
+
+#include <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+typedef struct _OttieColorValue OttieColorValue;
+
+struct _OttieColorValue
+{
+  gboolean is_static;
+  union {
+    GdkRGBA static_value;
+    gpointer keyframes;
+  };
+};
+
+void                      ottie_color_value_init                (OttieColorValue        *self,
+                                                                 const GdkRGBA          *rgba);
+void                      ottie_color_value_clear               (OttieColorValue        *self);
+
+void                      ottie_color_value_get                 (OttieColorValue        *self,
+                                                                 double                  timestamp,
+                                                                 GdkRGBA                *rgba);
+
+gboolean                  ottie_color_value_parse               (JsonReader             *reader,
+                                                                 gsize                   offset,
+                                                                 gpointer                data);
+
+G_END_DECLS
+
+#endif /* __OTTIE_COLOR_VALUE_PRIVATE_H__ */
diff --git a/ottie/ottiecomposition.c b/ottie/ottiecomposition.c
new file mode 100644
index 0000000000..623a52d10b
--- /dev/null
+++ b/ottie/ottiecomposition.c
@@ -0,0 +1,268 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiecompositionprivate.h"
+
+#include "ottieparserprivate.h"
+#include "ottiecompositionlayerprivate.h"
+#include "ottienulllayerprivate.h"
+#include "ottieshapelayerprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+#define GDK_ARRAY_ELEMENT_TYPE OttieLayer *
+#define GDK_ARRAY_FREE_FUNC g_object_unref
+#define GDK_ARRAY_TYPE_NAME OttieLayerList
+#define GDK_ARRAY_NAME ottie_layer_list
+#define GDK_ARRAY_PREALLOC 4
+#include "gdk/gdkarrayimpl.c"
+
+struct _OttieComposition
+{
+  OttieLayer parent;
+
+  OttieLayerList layers;
+  GHashTable *layers_by_index;
+};
+
+struct _OttieCompositionClass
+{
+  OttieLayerClass parent_class;
+};
+
+static GType
+ottie_composition_get_item_type (GListModel *list)
+{
+  return OTTIE_TYPE_LAYER;
+}
+
+static guint
+ottie_composition_get_n_items (GListModel *list)
+{
+  OttieComposition *self = OTTIE_COMPOSITION (list);
+
+  return ottie_layer_list_get_size (&self->layers);
+}
+
+static gpointer
+ottie_composition_get_item (GListModel *list,
+                             guint       position)
+{
+  OttieComposition *self = OTTIE_COMPOSITION (list);
+
+  if (position >= ottie_layer_list_get_size (&self->layers))
+    return NULL;
+
+  return g_object_ref (ottie_layer_list_get (&self->layers, position));
+}
+
+static void
+ottie_composition_list_model_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ottie_composition_get_item_type;
+  iface->get_n_items = ottie_composition_get_n_items;
+  iface->get_item = ottie_composition_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (OttieComposition, ottie_composition, OTTIE_TYPE_LAYER,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, ottie_composition_list_model_init))
+
+static void
+ottie_composition_update (OttieLayer *layer,
+                          GHashTable *compositions)
+{
+  OttieComposition *self = OTTIE_COMPOSITION (layer);
+
+  for (gsize i = ottie_layer_list_get_size (&self->layers); i-- > 0; )
+    {
+      ottie_layer_update (ottie_layer_list_get (&self->layers, i), compositions);
+    }
+}
+
+static void
+ottie_composition_render (OttieLayer  *layer,
+                          OttieRender *render,
+                          double       timestamp)
+{
+  OttieComposition *self = OTTIE_COMPOSITION (layer);
+  OttieRender child_render;
+
+  ottie_render_init (&child_render);
+
+  for (gsize i = 0; i < ottie_layer_list_get_size (&self->layers); i++)
+    {
+      OttieLayer *child = ottie_layer_list_get (&self->layers, i);
+
+      ottie_layer_render (child, &child_render, timestamp);
+      /* XXX: Should we clear paths here because they're not needed anymore? */
+
+      /* Use a counter here to avoid inflooping */
+      for (gsize j = 0; j < ottie_layer_list_get_size (&self->layers); j++)
+        {
+          if (child->transform)
+            ottie_shape_render (OTTIE_SHAPE (child->transform), &child_render, timestamp);
+          if (child->parent_index == OTTIE_INT_UNSET)
+            break;
+          child = g_hash_table_lookup (self->layers_by_index, GINT_TO_POINTER (child->parent_index));
+          if (child == NULL)
+            break;
+        }
+
+      ottie_render_merge (render, &child_render);
+    }
+
+  ottie_render_clear (&child_render);
+}
+
+static void
+ottie_composition_dispose (GObject *object)
+{
+  OttieComposition *self = OTTIE_COMPOSITION (object);
+
+  ottie_layer_list_clear (&self->layers);
+  g_hash_table_remove_all (self->layers_by_index);
+
+  G_OBJECT_CLASS (ottie_composition_parent_class)->dispose (object);
+}
+
+static void
+ottie_composition_finalize (GObject *object)
+{
+  OttieComposition *self = OTTIE_COMPOSITION (object);
+
+  g_hash_table_unref (self->layers_by_index);
+
+  G_OBJECT_CLASS (ottie_composition_parent_class)->finalize (object);
+}
+
+static void
+ottie_composition_class_init (OttieCompositionClass *klass)
+{
+  OttieLayerClass *layer_class = OTTIE_LAYER_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  layer_class->update = ottie_composition_update;
+  layer_class->render = ottie_composition_render;
+
+  gobject_class->dispose = ottie_composition_dispose;
+  gobject_class->finalize = ottie_composition_finalize;
+}
+
+static void
+ottie_composition_init (OttieComposition *self)
+{
+  ottie_layer_list_init (&self->layers);
+
+  self->layers_by_index = g_hash_table_new (g_direct_hash, g_direct_equal);
+}
+
+static void
+ottie_composition_append (OttieComposition *self,
+                          OttieLayer       *layer)
+{
+  ottie_layer_list_append (&self->layers, layer);
+  if (layer->index != OTTIE_INT_UNSET)
+    g_hash_table_insert (self->layers_by_index, GINT_TO_POINTER (layer->index), layer);
+  g_list_model_items_changed (G_LIST_MODEL (self), ottie_layer_list_get_size (&self->layers), 0, 1);
+}
+
+static gboolean
+ottie_composition_parse_layer (JsonReader *reader,
+                               gsize       offset,
+                               gpointer    data)
+{
+  OttieComposition *self = data;
+  OttieLayer *layer;
+  int type;
+
+  if (!json_reader_is_object (reader))
+    {
+      ottie_parser_error_syntax (reader, "Layer %zu is not an object",
+                                 ottie_layer_list_get_size (&self->layers));
+      return FALSE;
+    }
+
+  if (!json_reader_read_member (reader, "ty"))
+    {
+      ottie_parser_error_syntax (reader, "Layer %zu has no type",
+                                 ottie_layer_list_get_size (&self->layers));
+      json_reader_end_member (reader);
+      return FALSE;
+    }
+
+  type = json_reader_get_int_value (reader);
+  json_reader_end_member (reader);
+
+  switch (type)
+  {
+    case 0:
+      layer = ottie_composition_layer_parse (reader);
+      break;
+
+    case 3:
+      layer = ottie_null_layer_parse (reader);
+      break;
+
+    case 4:
+      layer = ottie_shape_layer_parse (reader);
+      break;
+
+    default:
+      ottie_parser_error_value (reader, "Layer %zu has unknown type %d",
+                                ottie_layer_list_get_size (&self->layers),
+                                type);
+      layer = NULL;
+      break;
+  }
+
+  if (layer)
+    ottie_composition_append (self, layer);
+
+  return TRUE;
+}
+
+gboolean
+ottie_composition_parse_layers (JsonReader *reader,
+                                gsize       offset,
+                                gpointer    data)
+{
+  OttieComposition **target = (OttieComposition **) ((guint8 *) data + offset);
+  OttieComposition *self;
+
+  self = g_object_new (OTTIE_TYPE_COMPOSITION, NULL);
+
+  if (!ottie_parser_parse_array (reader, "layers",
+                                 0, G_MAXUINT, NULL,
+                                 0, 0,
+                                 ottie_composition_parse_layer,
+                                 self))
+    {
+      g_object_unref (self);
+      return FALSE;
+    }
+
+  g_clear_object (target);
+  *target = self;
+
+  return TRUE;
+}
+
diff --git a/ottie/ottiecompositionlayer.c b/ottie/ottiecompositionlayer.c
new file mode 100644
index 0000000000..c3efb98a6b
--- /dev/null
+++ b/ottie/ottiecompositionlayer.c
@@ -0,0 +1,153 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiecompositionlayerprivate.h"
+
+#include "ottiedoublevalueprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+struct _OttieCompositionLayer
+{
+  OttieLayer parent;
+
+  OttieDoubleValue time_map;
+  double width;
+  double height;
+  char *ref_id;
+  OttieComposition *composition;
+};
+
+struct _OttieCompositionLayerClass
+{
+  OttieLayerClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieCompositionLayer, ottie_composition_layer, OTTIE_TYPE_LAYER)
+
+static void
+ottie_composition_layer_update (OttieLayer *layer,
+                                GHashTable *compositions)
+{
+  OttieCompositionLayer *self = OTTIE_COMPOSITION_LAYER (layer);
+
+  g_clear_object (&self->composition);
+
+  if (self->ref_id)
+    self->composition = g_object_ref (g_hash_table_lookup (compositions, self->ref_id));
+}
+
+static void
+ottie_composition_layer_render (OttieLayer  *layer,
+                                OttieRender *render,
+                                double       timestamp)
+{
+  OttieCompositionLayer *self = OTTIE_COMPOSITION_LAYER (layer);
+  GskRenderNode *node;
+  double time_map;
+
+  if (self->composition == NULL)
+    return;
+
+  if (ottie_double_value_is_static (&self->time_map))
+    time_map = timestamp;
+  else
+    time_map = ottie_double_value_get (&self->time_map, timestamp);
+
+  ottie_layer_render (OTTIE_LAYER (self->composition),
+                      render,
+                      time_map);
+
+  node = ottie_render_get_node (render);
+  ottie_render_clear_nodes (render);
+  if (node)
+    {
+      ottie_render_add_node (render,
+                             gsk_clip_node_new (node,
+                                                &GRAPHENE_RECT_INIT (
+                                                  0, 0,
+                                                  self->width, self->height
+                                                )));
+      gsk_render_node_unref (node);
+    }
+}
+
+static void
+ottie_composition_layer_dispose (GObject *object)
+{
+  OttieCompositionLayer *self = OTTIE_COMPOSITION_LAYER (object);
+
+  g_clear_object (&self->composition);
+  g_clear_pointer (&self->ref_id, g_free);
+  ottie_double_value_clear (&self->time_map);
+
+  G_OBJECT_CLASS (ottie_composition_layer_parent_class)->dispose (object);
+}
+
+static void
+ottie_composition_layer_class_init (OttieCompositionLayerClass *klass)
+{
+  OttieLayerClass *layer_class = OTTIE_LAYER_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  layer_class->update = ottie_composition_layer_update;
+  layer_class->render = ottie_composition_layer_render;
+
+  gobject_class->dispose = ottie_composition_layer_dispose;
+}
+
+static void
+ottie_composition_layer_init (OttieCompositionLayer *self)
+{
+  ottie_double_value_init (&self->time_map, 0);
+}
+
+OttieLayer *
+ottie_composition_layer_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_LAYER,
+    { "refId", ottie_parser_option_string, G_STRUCT_OFFSET (OttieCompositionLayer, ref_id) },
+    { "tm", ottie_double_value_parse, 0 },
+    { "w", ottie_parser_option_double, G_STRUCT_OFFSET (OttieCompositionLayer, width)  },
+    { "h", ottie_parser_option_double, G_STRUCT_OFFSET (OttieCompositionLayer, height)  },
+  };
+  OttieCompositionLayer *self;
+
+  self = g_object_new (OTTIE_TYPE_COMPOSITION_LAYER, NULL);
+
+  if (!ottie_parser_parse_object (reader, "composition layer", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_LAYER (self);
+}
+
+OttieComposition *
+ottie_composition_layer_get_composition (OttieCompositionLayer *self)
+{
+  g_return_val_if_fail (OTTIE_IS_COMPOSITION_LAYER (self), NULL);
+
+  return self->composition;
+}
diff --git a/ottie/ottiecompositionlayerprivate.h b/ottie/ottiecompositionlayerprivate.h
new file mode 100644
index 0000000000..5d5b44e521
--- /dev/null
+++ b/ottie/ottiecompositionlayerprivate.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_COMPOSITION_LAYER_PRIVATE_H__
+#define __OTTIE_COMPOSITION_LAYER_PRIVATE_H__
+
+#include "ottielayerprivate.h"
+
+#include "ottiecompositionprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_COMPOSITION_LAYER         (ottie_composition_layer_get_type ())
+#define OTTIE_COMPOSITION_LAYER(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_COMPOSITION_LAYER, 
OttieCompositionLayer))
+#define OTTIE_COMPOSITION_LAYER_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_COMPOSITION_LAYER, 
OttieCompositionLayerClass))
+#define OTTIE_IS_COMPOSITION_LAYER(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_COMPOSITION_LAYER))
+#define OTTIE_IS_COMPOSITION_LAYER_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_COMPOSITION_LAYER))
+#define OTTIE_COMPOSITION_LAYER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_COMPOSITION_LAYER, 
OttieCompositionLayerClass))
+
+typedef struct _OttieCompositionLayer OttieCompositionLayer;
+typedef struct _OttieCompositionLayerClass OttieCompositionLayerClass;
+
+GType                   ottie_composition_layer_get_type        (void) G_GNUC_CONST;
+
+OttieComposition *      ottie_composition_layer_get_composition (OttieCompositionLayer  *self);
+
+OttieLayer *            ottie_composition_layer_parse           (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_COMPOSITION_LAYER_PRIVATE_H__ */
diff --git a/ottie/ottiecompositionprivate.h b/ottie/ottiecompositionprivate.h
new file mode 100644
index 0000000000..883440f69f
--- /dev/null
+++ b/ottie/ottiecompositionprivate.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_COMPOSITION_PRIVATE_H__
+#define __OTTIE_COMPOSITION_PRIVATE_H__
+
+#include "ottielayerprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_COMPOSITION         (ottie_composition_get_type ())
+#define OTTIE_COMPOSITION(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_COMPOSITION, 
OttieComposition))
+#define OTTIE_COMPOSITION_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_COMPOSITION, 
OttieCompositionClass))
+#define OTTIE_IS_COMPOSITION(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_COMPOSITION))
+#define OTTIE_IS_COMPOSITION_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_COMPOSITION))
+#define OTTIE_COMPOSITION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_COMPOSITION, 
OttieCompositionClass))
+
+typedef struct _OttieComposition OttieComposition;
+typedef struct _OttieCompositionClass OttieCompositionClass;
+
+GType                   ottie_composition_get_type          (void) G_GNUC_CONST;
+
+gboolean                ottie_composition_parse_layers      (JsonReader                 *reader,
+                                                             gsize                       offset,
+                                                             gpointer                    data);
+
+G_END_DECLS
+
+#endif /* __OTTIE_COMPOSITION_PRIVATE_H__ */
diff --git a/ottie/ottiecreation.c b/ottie/ottiecreation.c
new file mode 100644
index 0000000000..3b7f1564dd
--- /dev/null
+++ b/ottie/ottiecreation.c
@@ -0,0 +1,746 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiecreationprivate.h"
+
+#include "ottielayerprivate.h"
+#include "ottieparserprivate.h"
+#include "ottiecompositionprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <json-glib/json-glib.h>
+
+struct _OttieCreation
+{
+  GObject parent;
+
+  char *name;
+  double frame_rate;
+  double start_frame;
+  double end_frame;
+  double width;
+  double height;
+
+  OttieComposition *layers;
+  GHashTable *composition_assets;
+
+  GCancellable *cancellable;
+};
+
+struct _OttieCreationClass
+{
+  GObjectClass parent_class;
+};
+
+enum {
+  PROP_0,
+  PROP_END_FRAME,
+  PROP_FRAME_RATE,
+  PROP_HEIGHT,
+  PROP_LOADING,
+  PROP_NAME,
+  PROP_PREPARED,
+  PROP_START_FRAME,
+  PROP_WIDTH,
+
+  N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+G_DEFINE_TYPE (OttieCreation, ottie_creation, G_TYPE_OBJECT)
+
+static void
+ottie_creation_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+
+{
+  //OttieCreation *self = OTTIE_CREATION (object);
+
+  switch (prop_id)
+    {
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_creation_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  OttieCreation *self = OTTIE_CREATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_END_FRAME:
+      g_value_set_double (value, self->end_frame);
+      break;
+
+    case PROP_FRAME_RATE:
+      g_value_set_double (value, self->frame_rate);
+      break;
+
+    case PROP_HEIGHT:
+      g_value_set_double (value, self->height);
+      break;
+
+    case PROP_PREPARED:
+      g_value_set_boolean (value, self->cancellable != NULL);
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, self->name);
+      break;
+
+    case PROP_START_FRAME:
+      g_value_set_double (value, self->start_frame);
+      break;
+
+    case PROP_WIDTH:
+      g_value_set_double (value, self->width);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_creation_stop_loading (OttieCreation *self,
+                             gboolean       emit)
+{
+  if (self->cancellable == NULL)
+    return;
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  if (emit)
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOADING]);
+}
+
+static void
+ottie_creation_reset (OttieCreation *self)
+{
+  g_clear_object (&self->layers);
+  g_hash_table_remove_all (self->composition_assets);
+
+  g_clear_pointer (&self->name, g_free);
+  self->frame_rate = 0;
+  self->start_frame = 0;
+  self->end_frame = 0;
+  self->width = 0;
+  self->height = 0;
+}
+
+static void
+ottie_creation_dispose (GObject *object)
+{
+  OttieCreation *self = OTTIE_CREATION (object);
+
+  ottie_creation_stop_loading (self, FALSE);
+  ottie_creation_reset (self);
+
+  G_OBJECT_CLASS (ottie_creation_parent_class)->dispose (object);
+}
+
+static void
+ottie_creation_finalize (GObject *object)
+{
+  OttieCreation *self = OTTIE_CREATION (object);
+
+  g_hash_table_unref (self->composition_assets);
+
+  G_OBJECT_CLASS (ottie_creation_parent_class)->finalize (object);
+}
+
+static void
+ottie_creation_class_init (OttieCreationClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+
+  gobject_class->set_property = ottie_creation_set_property;
+  gobject_class->get_property = ottie_creation_get_property;
+  gobject_class->finalize = ottie_creation_finalize;
+  gobject_class->dispose = ottie_creation_dispose;
+
+  /**
+   * OttieCreation:end-frame:
+   *
+   * End frame of the creation
+   */
+  properties[PROP_END_FRAME] =
+    g_param_spec_double ("end-frame",
+                         "End frame",
+                         "End frame of the creation",
+                         0.0, G_MAXDOUBLE, 0.0,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttieCreation:loading:
+   *
+   * Whether the creation is currently loading.
+   */
+  properties[PROP_LOADING] =
+    g_param_spec_boolean ("loading",
+                          "Loading",
+                          "Whether the creation is currently loading",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttieCreation:frame-rate:
+   *
+   * Frame rate of this creation
+   */
+  properties[PROP_FRAME_RATE] =
+    g_param_spec_double ("frame-rate",
+                         "Frame rate",
+                         "Frame rate of this creation",
+                         0.0, G_MAXDOUBLE, 0.0,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttieCreation:height:
+   *
+   * Height of this creation
+   */
+  properties[PROP_HEIGHT] =
+    g_param_spec_double ("height",
+                         "Height",
+                         "Height of this creation",
+                         0.0, G_MAXDOUBLE, 0.0,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttieCreation:name:
+   *
+   * The name of the creation.
+   */
+  properties[PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The name of the creation",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttieCreation:prepared:
+   *
+   * Whether the creation is prepared to render
+   */
+  properties[PROP_PREPARED] =
+    g_param_spec_boolean ("prepared",
+                          "Prepared",
+                          "Whether the creation is prepared to render",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttieCreation:start-frame:
+   *
+   * Start frame of the creation
+   */
+  properties[PROP_START_FRAME] =
+    g_param_spec_double ("start-frame",
+                         "Start frame",
+                         "Start frame of the creation",
+                         0.0, G_MAXDOUBLE, 0.0,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttieCreation:width:
+   *
+   * Width of this creation
+   */
+  properties[PROP_WIDTH] =
+    g_param_spec_double ("width",
+                         "Width",
+                         "Width of this creation",
+                         0.0, G_MAXDOUBLE, 0.0,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, properties);
+}
+
+static void
+ottie_creation_init (OttieCreation *self)
+{
+  self->composition_assets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+}
+
+/**
+ * ottie_creation_is_loading:
+ * @self: a #OttieCreation
+ *
+ * Returns whether @self is still in the process of loading. This may not just involve
+ * the creation itself, but also any assets that are a part of the creation.
+ *
+ * Returns: %TRUE if the creation is loading
+ */
+gboolean
+ottie_creation_is_loading (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), FALSE);
+
+  return self->cancellable != NULL;
+}
+
+/**
+ * ottie_creation_is_prepared:
+ * @self: a #OttieCreation
+ *
+ * Returns whether @self has successfully loaded a document that it can display.
+ *
+ * Returns: %TRUE if the creation can be used
+ */
+gboolean
+ottie_creation_is_prepared (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), FALSE);
+
+  return self->frame_rate > 0;
+}
+
+/**
+ * ottie_creation_get_name:
+ * @self: a #OttieCreation
+ *
+ * Returns the name of the current creation or %NULL if the creation is unnamed.
+ *
+ * Returns: (allow-none): The name of the creation
+ */
+const char *
+ottie_creation_get_name (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), FALSE);
+
+  return self->name;
+}
+
+static void
+ottie_creation_emit_error (OttieCreation *self,
+                           const GError  *error)
+{
+  g_print ("Ottie is sad: %s\n", error->message);
+}
+
+typedef struct {
+  char *id;
+  OttieComposition *composition;
+} OttieParserAsset;
+
+static gboolean
+ottie_creation_parse_asset (JsonReader *reader,
+                            gsize       offset,
+                            gpointer    data)
+{
+  OttieParserOption options[] = {
+    { "id", ottie_parser_option_string, G_STRUCT_OFFSET (OttieParserAsset, id) },
+    { "layers", ottie_composition_parse_layers, G_STRUCT_OFFSET (OttieParserAsset, composition) },
+  };
+  OttieCreation *self = data;
+  OttieParserAsset asset = { };
+  gboolean result;
+
+  result = ottie_parser_parse_object (reader, "asset", options, G_N_ELEMENTS (options), &asset);
+
+  if (result)
+    {
+      if (asset.id == NULL)
+        ottie_parser_error_syntax (reader, "No name given to asset");
+      else if (asset.composition == NULL)
+        ottie_parser_error_syntax (reader, "No composition layer or image asset defined for name %s", 
asset.id);
+      else
+        g_hash_table_insert (self->composition_assets, g_strdup (asset.id), g_object_ref 
(asset.composition));
+    }
+  
+  g_clear_pointer (&asset.id, g_free);
+  g_clear_object (&asset.composition);
+
+  return result;
+}
+
+static gboolean
+ottie_creation_parse_assets (JsonReader *reader,
+                             gsize       offset,
+                             gpointer    data)
+{
+  return ottie_parser_parse_array (reader, "assets",
+                                   0, G_MAXUINT, NULL,
+                                   offset, 0,
+                                   ottie_creation_parse_asset,
+                                   data);
+}
+
+static gboolean
+ottie_creation_parse_marker (JsonReader *reader,
+                             gsize       offset,
+                             gpointer    data)
+{
+  ottie_parser_error_unsupported (reader, "Markers are not implemented yet.");
+
+  return TRUE;
+}
+
+static gboolean
+ottie_creation_parse_markers (JsonReader *reader,
+                              gsize       offset,
+                              gpointer    data)
+{
+  return ottie_parser_parse_array (reader, "markers",
+                                   0, G_MAXUINT, NULL,
+                                   offset, 0,
+                                   ottie_creation_parse_marker,
+                                   data);
+}
+
+static gboolean
+ottie_creation_load_from_reader (OttieCreation *self,
+                                 JsonReader    *reader)
+{
+  OttieParserOption options[] = {
+    { "fr", ottie_parser_option_double, G_STRUCT_OFFSET (OttieCreation, frame_rate) },
+    { "w", ottie_parser_option_double, G_STRUCT_OFFSET (OttieCreation, width) },
+    { "h", ottie_parser_option_double, G_STRUCT_OFFSET (OttieCreation, height) },
+    { "nm", ottie_parser_option_string, G_STRUCT_OFFSET (OttieCreation, name) },
+    { "ip", ottie_parser_option_double, G_STRUCT_OFFSET (OttieCreation, start_frame) },
+    { "op", ottie_parser_option_double, G_STRUCT_OFFSET (OttieCreation, end_frame) },
+    { "ddd", ottie_parser_option_3d, 0 },
+    { "v", ottie_parser_option_skip, 0 },
+    { "layers", ottie_composition_parse_layers, G_STRUCT_OFFSET (OttieCreation, layers) },
+    { "assets", ottie_creation_parse_assets, 0 },
+    { "markers", ottie_creation_parse_markers, 0 },
+  };
+
+  return ottie_parser_parse_object (reader, "toplevel", options, G_N_ELEMENTS (options), self);
+}
+
+static void
+ottie_creation_update_layers (OttieCreation *self)
+{
+  GHashTableIter iter;
+  gpointer layer;
+
+  g_hash_table_iter_init (&iter, self->composition_assets);
+
+  while (g_hash_table_iter_next (&iter, NULL, &layer))
+    ottie_layer_update (layer, self->composition_assets);
+
+  ottie_layer_update (OTTIE_LAYER (self->layers), self->composition_assets);
+}
+
+static void
+ottie_creation_notify_prepared (OttieCreation *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PREPARED]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FRAME_RATE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WIDTH]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HEIGHT]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_START_FRAME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_END_FRAME]);
+}
+
+static gboolean
+ottie_creation_load_from_node (OttieCreation *self, 
+                               JsonNode      *root)
+{
+  JsonReader *reader = json_reader_new (root);
+  gboolean result;
+
+  result = ottie_creation_load_from_reader (self, reader);
+
+  g_object_unref (reader);
+
+  return result;
+}
+
+static void
+ottie_creation_load_file_parsed (GObject      *parser,
+                                 GAsyncResult *res,
+                                 gpointer      data)
+{
+  OttieCreation *self = data;
+  GError *error = NULL;
+
+  if (!json_parser_load_from_stream_finish (JSON_PARSER (parser), res, &error))
+    {
+      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+      ottie_creation_emit_error (self, error);
+      g_error_free (error);
+      ottie_creation_stop_loading (self, TRUE);
+      return;
+    }
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  if (ottie_creation_load_from_node (self, json_parser_get_root (JSON_PARSER (parser))))
+    {
+      ottie_creation_update_layers (self);
+    }
+  else
+    {
+      ottie_creation_reset (self);
+    }
+
+  ottie_creation_stop_loading (self, TRUE);
+  ottie_creation_notify_prepared (self);
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+ottie_creation_load_file_open (GObject      *file,
+                               GAsyncResult *res,
+                               gpointer      data)
+{
+  OttieCreation *self = data;
+  GFileInputStream *stream;
+  GError *error = NULL;
+  JsonParser *parser;
+
+  stream = g_file_read_finish (G_FILE (file), res, &error);
+  if (stream == NULL)
+    {
+      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+      ottie_creation_emit_error (self, error);
+      g_error_free (error);
+      ottie_creation_stop_loading (self, TRUE);
+      return;
+    }
+
+  parser = json_parser_new ();
+  json_parser_load_from_stream_async (parser,
+                                      G_INPUT_STREAM (stream), 
+                                      self->cancellable,
+                                      ottie_creation_load_file_parsed,
+                                      self);
+  g_object_unref (parser);
+}
+
+void
+ottie_creation_load_bytes (OttieCreation *self,
+                           GBytes        *bytes)
+{
+  GError *error = NULL;
+  JsonParser *parser;
+
+  g_return_if_fail (OTTIE_IS_CREATION (self));
+  g_return_if_fail (bytes != NULL);
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  ottie_creation_stop_loading (self, FALSE);
+  ottie_creation_reset (self);
+
+  parser = json_parser_new ();
+  if (json_parser_load_from_data (parser,
+                                  g_bytes_get_data (bytes, NULL),
+                                  g_bytes_get_size (bytes),
+                                  &error))
+    {
+      if (ottie_creation_load_from_node (self, json_parser_get_root (JSON_PARSER (parser))))
+        {
+          ottie_creation_update_layers (self);
+        }
+      else
+        {
+          ottie_creation_reset (self);
+        }
+    }
+  else
+    {
+      ottie_creation_emit_error (self, error);
+      g_error_free (error);
+    }
+
+  g_object_unref (parser);
+
+  ottie_creation_notify_prepared (self);
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+void
+ottie_creation_load_file (OttieCreation *self,
+                          GFile         *file)
+{
+  g_return_if_fail (OTTIE_IS_CREATION (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  ottie_creation_stop_loading (self, FALSE);
+  if (self->frame_rate)
+    {
+      ottie_creation_reset (self);
+      ottie_creation_notify_prepared (self);
+    }
+
+  self->cancellable = g_cancellable_new ();
+
+  g_file_read_async (file,
+                     G_PRIORITY_DEFAULT,
+                     self->cancellable,
+                     ottie_creation_load_file_open,
+                     self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOADING]);
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+void
+ottie_creation_load_filename (OttieCreation *self,
+                              const char    *filename)
+{
+  GFile *file;
+
+  g_return_if_fail (OTTIE_IS_CREATION (self));
+  g_return_if_fail (filename != NULL);
+
+  file = g_file_new_for_path (filename);
+
+  ottie_creation_load_file (self, file);
+
+  g_clear_object (&file);
+}
+
+OttieCreation *
+ottie_creation_new (void)
+{
+  return g_object_new (OTTIE_TYPE_CREATION, NULL);
+}
+
+OttieCreation *
+ottie_creation_new_for_file (GFile *file)
+{
+  OttieCreation *self;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  self = g_object_new (OTTIE_TYPE_CREATION, NULL);
+
+  ottie_creation_load_file (self, file);
+
+  return self;
+}
+
+OttieCreation *
+ottie_creation_new_for_filename (const char *filename)
+{
+  OttieCreation *self;
+  GFile *file;
+
+  g_return_val_if_fail (filename != NULL, NULL);
+
+  file = g_file_new_for_path (filename);
+
+  self = ottie_creation_new_for_file (file);
+
+  g_clear_object (&file);
+
+  return self;
+}
+
+double
+ottie_creation_get_frame_rate (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), 0);
+
+  return self->frame_rate;
+}
+
+double
+ottie_creation_get_start_frame (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), 0);
+
+  return self->start_frame;
+}
+
+double
+ottie_creation_get_end_frame (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), 0);
+
+  return self->end_frame;
+}
+
+double
+ottie_creation_get_width (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), 0);
+
+  return self->width;
+}
+
+double
+ottie_creation_get_height (OttieCreation *self)
+{
+  g_return_val_if_fail (OTTIE_IS_CREATION (self), 0);
+
+  return self->height;
+}
+
+void
+ottie_creation_snapshot (OttieCreation *self,
+                         GtkSnapshot   *snapshot,
+                         double         timestamp)
+{
+  GskRenderNode *node;
+  OttieRender render;
+
+  if (self->layers == NULL)
+    return;
+
+  timestamp = timestamp * self->frame_rate;
+
+  ottie_render_init (&render);
+
+  ottie_layer_render (OTTIE_LAYER (self->layers), &render, timestamp);
+  node = ottie_render_get_node (&render);
+  if (node)
+    {
+      gtk_snapshot_append_node (snapshot, node);
+      gsk_render_node_unref (node);
+    }
+
+  ottie_render_clear (&render);
+}
+
+OttieComposition *
+ottie_creation_get_composition (OttieCreation *self)
+{
+  return self->layers;
+}
+
diff --git a/ottie/ottiecreation.h b/ottie/ottiecreation.h
new file mode 100644
index 0000000000..ad795ed8ee
--- /dev/null
+++ b/ottie/ottiecreation.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_CREATION_H__
+#define __OTTIE_CREATION_H__
+
+#if !defined (__OTTIE_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <ottie/ottie.h> can be included directly."
+#endif
+
+#include <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_CREATION         (ottie_creation_get_type ())
+#define OTTIE_CREATION(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_CREATION, OttieCreation))
+#define OTTIE_CREATION_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_CREATION, OttieCreationClass))
+#define OTTIE_IS_CREATION(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_CREATION))
+#define OTTIE_IS_CREATION_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_CREATION))
+#define OTTIE_CREATION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_CREATION, 
OttieCreationClass))
+
+typedef struct _OttieCreation OttieCreation;
+typedef struct _OttieCreationClass OttieCreationClass;
+
+GDK_AVAILABLE_IN_ALL
+GType                   ottie_creation_get_type                 (void) G_GNUC_CONST;
+
+GDK_AVAILABLE_IN_ALL
+OttieCreation *         ottie_creation_new                      (void);
+GDK_AVAILABLE_IN_ALL
+OttieCreation *         ottie_creation_new_for_file             (GFile                  *file);
+GDK_AVAILABLE_IN_ALL
+OttieCreation *         ottie_creation_new_for_filename         (const char             *filename);
+
+GDK_AVAILABLE_IN_ALL
+void                    ottie_creation_load_file                (OttieCreation          *self,
+                                                                 GFile                  *file);
+GDK_AVAILABLE_IN_ALL
+void                    ottie_creation_load_filename            (OttieCreation          *self,
+                                                                 const char             *filename);
+GDK_AVAILABLE_IN_ALL
+void                    ottie_creation_load_bytes               (OttieCreation          *self,
+                                                                 GBytes                 *bytes);
+
+GDK_AVAILABLE_IN_ALL
+gboolean                ottie_creation_is_loading               (OttieCreation          *self);
+GDK_AVAILABLE_IN_ALL
+gboolean                ottie_creation_is_prepared              (OttieCreation          *self);
+
+GDK_AVAILABLE_IN_ALL
+const char *            ottie_creation_get_name                 (OttieCreation          *self);
+GDK_AVAILABLE_IN_ALL
+double                  ottie_creation_get_frame_rate           (OttieCreation          *self);
+GDK_AVAILABLE_IN_ALL
+double                  ottie_creation_get_start_frame          (OttieCreation          *self);
+GDK_AVAILABLE_IN_ALL
+double                  ottie_creation_get_end_frame            (OttieCreation          *self);
+GDK_AVAILABLE_IN_ALL
+double                  ottie_creation_get_width                (OttieCreation          *self);
+GDK_AVAILABLE_IN_ALL
+double                  ottie_creation_get_height               (OttieCreation          *self);
+
+
+
+G_END_DECLS
+
+#endif /* __OTTIE_CREATION_H__ */
diff --git a/ottie/ottiecreationprivate.h b/ottie/ottiecreationprivate.h
new file mode 100644
index 0000000000..c59091b723
--- /dev/null
+++ b/ottie/ottiecreationprivate.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_CREATION_PRIVATE_H__
+#define __OTTIE_CREATION_PRIVATE_H__
+
+#include "ottiecreation.h"
+
+#include "ottiecompositionprivate.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+
+void                    ottie_creation_snapshot                 (OttieCreation          *self,
+                                                                 GtkSnapshot            *snapshot,
+                                                                 double                  timestamp);
+
+OttieComposition *      ottie_creation_get_composition          (OttieCreation          *self);
+
+G_END_DECLS
+
+#endif /* __OTTIE_CREATION_PRIVATE_H__ */
diff --git a/ottie/ottiedoublevalue.c b/ottie/ottiedoublevalue.c
new file mode 100644
index 0000000000..e3897ea033
--- /dev/null
+++ b/ottie/ottiedoublevalue.c
@@ -0,0 +1,121 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiedoublevalueprivate.h"
+
+#include "ottieparserprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+static gboolean
+ottie_double_value_parse_value (JsonReader *reader,
+                                gsize       offset,
+                                gpointer    data)
+{
+  gboolean result, array;
+
+  /* Lottie being Lottie, single values may get dumped into arrays. */
+  array = json_reader_is_array (reader);
+  if (array)
+    json_reader_read_element (reader, 0);
+
+  result = ottie_parser_option_double (reader, offset, data);
+
+  if (array)
+    json_reader_end_element (reader);
+
+  return result;
+}
+
+static double
+ottie_double_value_interpolate (double start,
+                                double end,
+                                double progress)
+{
+  return start + (end - start) * progress;
+}
+
+#define OTTIE_KEYFRAMES_NAME ottie_double_keyframes
+#define OTTIE_KEYFRAMES_TYPE_NAME OttieDoubleKeyframes
+#define OTTIE_KEYFRAMES_ELEMENT_TYPE double
+#define OTTIE_KEYFRAMES_PARSE_FUNC ottie_double_value_parse_value
+#define OTTIE_KEYFRAMES_INTERPOLATE_FUNC ottie_double_value_interpolate
+#include "ottiekeyframesimpl.c"
+
+void
+ottie_double_value_init (OttieDoubleValue *self,
+                         double            value)
+{
+  self->is_static = TRUE;
+  self->static_value = value;
+}
+
+void
+ottie_double_value_clear (OttieDoubleValue *self)
+{
+  if (!self->is_static)
+    g_clear_pointer (&self->keyframes, ottie_double_keyframes_free);
+}
+
+double
+ottie_double_value_get (OttieDoubleValue *self,
+                        double            timestamp)
+{
+  if (self->is_static)
+    return self->static_value;
+  
+  return ottie_double_keyframes_get (self->keyframes, timestamp);
+}
+
+gboolean
+ottie_double_value_parse (JsonReader *reader,
+                          gsize       offset,
+                          gpointer    data)
+{
+  OttieDoubleValue *self = (OttieDoubleValue *) ((guint8 *) data + GPOINTER_TO_SIZE (offset));
+
+  if (json_reader_read_member (reader, "k"))
+    {
+      if (!json_reader_is_array (reader))
+        {
+          self->is_static = TRUE;
+          self->static_value = json_reader_get_double_value (reader);
+        }
+      else
+        {
+          self->is_static = FALSE;
+          self->keyframes = ottie_double_keyframes_parse (reader);
+          if (self->keyframes == NULL)
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+        }
+    }
+  else
+    {
+      ottie_parser_error_syntax (reader, "Property is not a number");
+    }
+  json_reader_end_member (reader);
+
+  return TRUE;
+}
+
diff --git a/ottie/ottiedoublevalueprivate.h b/ottie/ottiedoublevalueprivate.h
new file mode 100644
index 0000000000..ed33e9227b
--- /dev/null
+++ b/ottie/ottiedoublevalueprivate.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_DOUBLE_VALUE_PRIVATE_H__
+#define __OTTIE_DOUBLE_VALUE_PRIVATE_H__
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct _OttieDoubleValue OttieDoubleValue;
+
+struct _OttieDoubleValue
+{
+  gboolean is_static;
+  union {
+    double static_value;
+    gpointer keyframes;
+  };
+};
+
+void                      ottie_double_value_init               (OttieDoubleValue       *self,
+                                                                 double                  value);
+void                      ottie_double_value_clear              (OttieDoubleValue       *self);
+
+static inline gboolean    ottie_double_value_is_static          (OttieDoubleValue       *self);
+double                    ottie_double_value_get                (OttieDoubleValue       *self,
+                                                                 double                  timestamp);
+
+gboolean                  ottie_double_value_parse              (JsonReader             *reader,
+                                                                 gsize                   offset,
+                                                                 gpointer                data);
+
+static inline gboolean
+ottie_double_value_is_static (OttieDoubleValue *self)
+{
+  return self->is_static;
+}
+
+G_END_DECLS
+
+#endif /* __OTTIE_DOUBLE_VALUE_PRIVATE_H__ */
diff --git a/ottie/ottieellipseshape.c b/ottie/ottieellipseshape.c
new file mode 100644
index 0000000000..e26e96b02b
--- /dev/null
+++ b/ottie/ottieellipseshape.c
@@ -0,0 +1,138 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottieellipseshapeprivate.h"
+
+#include "ottiedoublevalueprivate.h"
+#include "ottiepointvalueprivate.h"
+#include "ottieparserprivate.h"
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+struct _OttieEllipseShape
+{
+  OttieShape parent;
+
+  double diellipseion;
+  OttiePointValue position;
+  OttiePointValue size;
+};
+
+struct _OttieEllipseShapeClass
+{
+  OttieShapeClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieEllipseShape, ottie_ellipse_shape, OTTIE_TYPE_SHAPE)
+
+static void
+ottie_ellipse_shape_render (OttieShape  *shape,
+                         OttieRender *render,
+                         double       timestamp)
+{
+  OttieEllipseShape *self = OTTIE_ELLIPSE_SHAPE (shape);
+  graphene_point_t p, s;
+  GskPathBuilder *builder;
+  const float weight = sqrt(0.5f);
+
+  ottie_point_value_get (&self->position, timestamp, &p);
+  ottie_point_value_get (&self->size, timestamp, &s);
+  s.x /= 2;
+  s.y /= 2;
+
+  builder = gsk_path_builder_new ();
+
+  gsk_path_builder_move_to (builder,
+                            p.x, p.y - s.y);
+  gsk_path_builder_conic_to (builder,
+                             p.x + s.x, p.y - s.y,
+                             p.x + s.x, p.y,
+                             weight);
+  gsk_path_builder_conic_to (builder,
+                             p.x + s.x, p.y + s.y,
+                             p.x, p.y + s.y,
+                             weight);
+  gsk_path_builder_conic_to (builder,
+                             p.x - s.x, p.y + s.y,
+                             p.x - s.x, p.y,
+                             weight);
+  gsk_path_builder_conic_to (builder,
+                             p.x - s.x, p.y - s.y,
+                             p.x, p.y - s.y,
+                             weight);
+  gsk_path_builder_close (builder);
+
+  ottie_render_add_path (render,
+                         gsk_path_builder_free_to_path (builder));
+}
+
+static void
+ottie_ellipse_shape_dispose (GObject *object)
+{
+  OttieEllipseShape *self = OTTIE_ELLIPSE_SHAPE (object);
+
+  ottie_point_value_clear (&self->position);
+  ottie_point_value_clear (&self->size);
+
+  G_OBJECT_CLASS (ottie_ellipse_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_ellipse_shape_class_init (OttieEllipseShapeClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_ellipse_shape_render;
+
+  gobject_class->dispose = ottie_ellipse_shape_dispose;
+}
+
+static void
+ottie_ellipse_shape_init (OttieEllipseShape *self)
+{
+  ottie_point_value_init (&self->position, &GRAPHENE_POINT_INIT (0, 0));
+  ottie_point_value_init (&self->size, &GRAPHENE_POINT_INIT (0, 0));
+}
+
+OttieShape *
+ottie_ellipse_shape_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "d", ottie_parser_option_double, G_STRUCT_OFFSET (OttieEllipseShape, diellipseion) },
+    { "p", ottie_point_value_parse, G_STRUCT_OFFSET (OttieEllipseShape, position) },
+    { "s", ottie_point_value_parse, G_STRUCT_OFFSET (OttieEllipseShape, size) },
+  };
+  OttieEllipseShape *self;
+
+  self = g_object_new (OTTIE_TYPE_ELLIPSE_SHAPE, NULL);
+
+  if (!ottie_parser_parse_object (reader, "ellipse shape", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_SHAPE (self);
+}
+
diff --git a/ottie/ottieellipseshapeprivate.h b/ottie/ottieellipseshapeprivate.h
new file mode 100644
index 0000000000..0ec6b8ca28
--- /dev/null
+++ b/ottie/ottieellipseshapeprivate.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_ELLIPSE_SHAPE_PRIVATE_H__
+#define __OTTIE_ELLIPSE_SHAPE_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_ELLIPSE_SHAPE         (ottie_ellipse_shape_get_type ())
+#define OTTIE_ELLIPSE_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_ELLIPSE_SHAPE, 
OttieEllipseShape))
+#define OTTIE_ELLIPSE_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_ELLIPSE_SHAPE, 
OttieEllipseShapeClass))
+#define OTTIE_IS_ELLIPSE_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_ELLIPSE_SHAPE))
+#define OTTIE_IS_ELLIPSE_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_ELLIPSE_SHAPE))
+#define OTTIE_ELLIPSE_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_ELLIPSE_SHAPE, 
OttieEllipseShapeClass))
+
+typedef struct _OttieEllipseShape OttieEllipseShape;
+typedef struct _OttieEllipseShapeClass OttieEllipseShapeClass;
+
+GType                   ottie_ellipse_shape_get_type            (void) G_GNUC_CONST;
+
+OttieShape *            ottie_ellipse_shape_parse               (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_ELLIPSE_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottiefillshape.c b/ottie/ottiefillshape.c
new file mode 100644
index 0000000000..f26ca9ee53
--- /dev/null
+++ b/ottie/ottiefillshape.c
@@ -0,0 +1,132 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiefillshapeprivate.h"
+
+#include "ottiecolorvalueprivate.h"
+#include "ottiedoublevalueprivate.h"
+#include "ottieparserprivate.h"
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+struct _OttieFillShape
+{
+  OttieShape parent;
+
+  OttieDoubleValue opacity;
+  OttieColorValue color;
+  GskBlendMode blend_mode;
+  GskFillRule fill_rule;
+};
+
+struct _OttieFillShapeClass
+{
+  OttieShapeClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieFillShape, ottie_fill_shape, OTTIE_TYPE_SHAPE)
+
+static void
+ottie_fill_shape_render (OttieShape  *shape,
+                         OttieRender *render,
+                         double       timestamp)
+{
+  OttieFillShape *self = OTTIE_FILL_SHAPE (shape);
+  GskPath *path;
+  graphene_rect_t bounds;
+  GdkRGBA color;
+  double opacity;
+  GskRenderNode *color_node;
+
+  opacity = ottie_double_value_get (&self->opacity, timestamp);
+  opacity = CLAMP (opacity, 0, 100);
+  ottie_color_value_get (&self->color, timestamp, &color);
+  color.alpha = color.alpha * opacity / 100.f;
+  if (gdk_rgba_is_clear (&color))
+    return;
+
+  path = ottie_render_get_path (render);
+  if (gsk_path_is_empty (path))
+    return;
+
+  gsk_path_get_bounds (path, &bounds);
+  color_node = gsk_color_node_new (&color, &bounds);
+
+  ottie_render_add_node (render, gsk_fill_node_new (color_node, path, self->fill_rule));
+
+  gsk_render_node_unref (color_node);
+}
+
+static void
+ottie_fill_shape_dispose (GObject *object)
+{
+  OttieFillShape *self = OTTIE_FILL_SHAPE (object);
+
+  ottie_double_value_clear (&self->opacity);
+  ottie_color_value_clear (&self->color);
+
+  G_OBJECT_CLASS (ottie_fill_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_fill_shape_class_init (OttieFillShapeClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_fill_shape_render;
+
+  gobject_class->dispose = ottie_fill_shape_dispose;
+}
+
+static void
+ottie_fill_shape_init (OttieFillShape *self)
+{
+  ottie_double_value_init (&self->opacity, 100);
+  ottie_color_value_init (&self->color, &(GdkRGBA) { 0, 0, 0, 1 });
+  self->fill_rule = GSK_FILL_RULE_WINDING;
+}
+
+OttieShape *
+ottie_fill_shape_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "o", ottie_double_value_parse, G_STRUCT_OFFSET (OttieFillShape, opacity) },
+    { "c", ottie_color_value_parse, G_STRUCT_OFFSET (OttieFillShape, color) },
+    { "bm", ottie_parser_option_blend_mode, G_STRUCT_OFFSET (OttieFillShape, blend_mode) },
+    { "r", ottie_parser_option_fill_rule, G_STRUCT_OFFSET (OttieFillShape, fill_rule) },
+  };
+  OttieFillShape *self;
+
+  self = g_object_new (OTTIE_TYPE_FILL_SHAPE, NULL);
+
+  if (!ottie_parser_parse_object (reader, "fill shape", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_SHAPE (self);
+}
+
diff --git a/ottie/ottiefillshapeprivate.h b/ottie/ottiefillshapeprivate.h
new file mode 100644
index 0000000000..ea8889f2eb
--- /dev/null
+++ b/ottie/ottiefillshapeprivate.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_FILL_SHAPE_PRIVATE_H__
+#define __OTTIE_FILL_SHAPE_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_FILL_SHAPE         (ottie_fill_shape_get_type ())
+#define OTTIE_FILL_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_FILL_SHAPE, 
OttieFillShape))
+#define OTTIE_FILL_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_FILL_SHAPE, 
OttieFillShapeClass))
+#define OTTIE_IS_FILL_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_FILL_SHAPE))
+#define OTTIE_IS_FILL_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_FILL_SHAPE))
+#define OTTIE_FILL_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_FILL_SHAPE, 
OttieFillShapeClass))
+
+typedef struct _OttieFillShape OttieFillShape;
+typedef struct _OttieFillShapeClass OttieFillShapeClass;
+
+GType                   ottie_fill_shape_get_type               (void) G_GNUC_CONST;
+
+OttieShape *            ottie_fill_shape_parse                  (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_FILL_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottiegroupshape.c b/ottie/ottiegroupshape.c
new file mode 100644
index 0000000000..22cec48f6c
--- /dev/null
+++ b/ottie/ottiegroupshape.c
@@ -0,0 +1,247 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiegroupshapeprivate.h"
+
+#include "ottieellipseshapeprivate.h"
+#include "ottiefillshapeprivate.h"
+#include "ottieparserprivate.h"
+#include "ottiepathshapeprivate.h"
+#include "ottierectshapeprivate.h"
+#include "ottieshapeprivate.h"
+#include "ottiestrokeshapeprivate.h"
+#include "ottietransformprivate.h"
+#include "ottietrimshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+#define GDK_ARRAY_ELEMENT_TYPE OttieShape *
+#define GDK_ARRAY_FREE_FUNC g_object_unref
+#define GDK_ARRAY_TYPE_NAME OttieShapeList
+#define GDK_ARRAY_NAME ottie_shape_list
+#define GDK_ARRAY_PREALLOC 4
+#include "gdk/gdkarrayimpl.c"
+
+struct _OttieGroupShape
+{
+  OttieShape parent;
+
+  OttieShapeList shapes;
+  GskBlendMode blend_mode;
+};
+
+struct _OttieGroupShapeClass
+{
+  OttieShapeClass parent_class;
+};
+
+static GType
+ottie_group_shape_get_item_type (GListModel *list)
+{
+  return OTTIE_TYPE_SHAPE;
+}
+
+static guint
+ottie_group_shape_get_n_items (GListModel *list)
+{
+  OttieGroupShape *self = OTTIE_GROUP_SHAPE (list);
+
+  return ottie_shape_list_get_size (&self->shapes);
+}
+
+static gpointer
+ottie_group_shape_get_item (GListModel *list,
+                            guint       position)
+{
+  OttieGroupShape *self = OTTIE_GROUP_SHAPE (list);
+
+  if (position >= ottie_shape_list_get_size (&self->shapes))
+    return NULL;
+
+  return g_object_ref (ottie_shape_list_get (&self->shapes, position));
+}
+
+static void
+ottie_group_shape_list_model_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ottie_group_shape_get_item_type;
+  iface->get_n_items = ottie_group_shape_get_n_items;
+  iface->get_item = ottie_group_shape_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (OttieGroupShape, ottie_group_shape, OTTIE_TYPE_SHAPE,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, ottie_group_shape_list_model_init))
+
+static void
+ottie_group_shape_render (OttieShape  *shape,
+                          OttieRender *render,
+                          double       timestamp)
+{
+  OttieGroupShape *self = OTTIE_GROUP_SHAPE (shape);
+  OttieRender child_render;
+
+  ottie_render_init (&child_render);
+
+  for (gsize i = 0; i < ottie_shape_list_get_size (&self->shapes); i++)
+    {
+      ottie_shape_render (ottie_shape_list_get (&self->shapes, i),
+                          &child_render,
+                          timestamp);
+    }
+
+  ottie_render_merge (render, &child_render);
+
+  ottie_render_clear (&child_render);
+}
+
+static void
+ottie_group_shape_dispose (GObject *object)
+{
+  OttieGroupShape *self = OTTIE_GROUP_SHAPE (object);
+
+  ottie_shape_list_clear (&self->shapes);
+
+  G_OBJECT_CLASS (ottie_group_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_group_shape_class_init (OttieGroupShapeClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_group_shape_render;
+
+  gobject_class->dispose = ottie_group_shape_dispose;
+}
+
+static void
+ottie_group_shape_init (OttieGroupShape *self)
+{
+}
+
+gboolean
+ottie_group_shape_parse_shapes (JsonReader *reader,
+                                gsize       offset,
+                                gpointer    data)
+{
+  OttieGroupShape *self = data;
+
+  if (!json_reader_is_array (reader))
+    {
+      ottie_parser_error_syntax (reader, "Shapes are not an array.");
+      return FALSE;
+    }
+
+  for (int i = 0; ; i++)
+    {
+      OttieShape *shape;
+      const char *type;
+
+      if (!json_reader_read_element (reader, i))
+        break;
+
+      if (!json_reader_is_object (reader))
+        {
+          ottie_parser_error_syntax (reader, "Shape %d is not an object", i);
+          continue;
+        }
+
+      if (!json_reader_read_member (reader, "ty"))
+        {
+          ottie_parser_error_syntax (reader, "Shape %d has no type", i);
+          json_reader_end_member (reader);
+          json_reader_end_element (reader);
+          continue;
+        }
+
+      type = json_reader_get_string_value (reader);
+      if (type == NULL || json_reader_get_error (reader))
+        {
+          ottie_parser_emit_error (reader, json_reader_get_error (reader));
+          json_reader_end_member (reader);
+          json_reader_end_element (reader);
+          continue;
+        }
+      json_reader_end_member (reader);
+
+      if (g_str_equal (type, "el"))
+        shape = ottie_ellipse_shape_parse (reader);
+      else if (g_str_equal (type, "fl"))
+        shape = ottie_fill_shape_parse (reader);
+      else if (g_str_equal (type, "gr"))
+        shape = ottie_group_shape_parse (reader);
+      else if (g_str_equal (type, "rc"))
+        shape = ottie_rect_shape_parse (reader);
+      else if (g_str_equal (type, "sh"))
+        shape = ottie_path_shape_parse (reader);
+      else if (g_str_equal (type, "st"))
+        shape = ottie_stroke_shape_parse (reader);
+      else if (g_str_equal (type, "tm"))
+        shape = ottie_trim_shape_parse (reader);
+      else if (g_str_equal (type, "tr"))
+        shape = ottie_transform_parse (reader);
+      else
+        {
+          ottie_parser_error_value (reader, "Shape %d has unknown type \"%s\"", i, type);
+          shape = NULL;
+        }
+
+      if (shape)
+        ottie_shape_list_append (&self->shapes, shape);
+      json_reader_end_element (reader);
+    }
+
+  json_reader_end_element (reader);
+
+  return TRUE;
+}
+
+OttieShape *
+ottie_group_shape_new (void)
+{
+  return g_object_new (OTTIE_TYPE_GROUP_SHAPE, NULL);
+}
+
+OttieShape *
+ottie_group_shape_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "bm", ottie_parser_option_blend_mode, G_STRUCT_OFFSET (OttieGroupShape, blend_mode) },
+    { "np", ottie_parser_option_skip_expression, 0 },
+    { "cix", ottie_parser_option_skip_index, 0 },
+    { "it", ottie_group_shape_parse_shapes, 0 },
+  };
+  OttieShape *self;
+
+  self = ottie_group_shape_new ();
+
+  if (!ottie_parser_parse_object (reader, "group shape", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return self;
+}
+
diff --git a/ottie/ottiegroupshapeprivate.h b/ottie/ottiegroupshapeprivate.h
new file mode 100644
index 0000000000..d0f9b02e88
--- /dev/null
+++ b/ottie/ottiegroupshapeprivate.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_GROUP_SHAPE_PRIVATE_H__
+#define __OTTIE_GROUP_SHAPE_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_GROUP_SHAPE         (ottie_group_shape_get_type ())
+#define OTTIE_GROUP_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_GROUP_SHAPE, 
OttieGroupShape))
+#define OTTIE_GROUP_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_GROUP_SHAPE, 
OttieGroupShapeClass))
+#define OTTIE_IS_GROUP_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_GROUP_SHAPE))
+#define OTTIE_IS_GROUP_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_GROUP_SHAPE))
+#define OTTIE_GROUP_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_GROUP_SHAPE, 
OttieGroupShapeClass))
+
+typedef struct _OttieGroupShape OttieGroupShape;
+typedef struct _OttieGroupShapeClass OttieGroupShapeClass;
+
+GType                   ottie_group_shape_get_type              (void) G_GNUC_CONST;
+
+OttieShape *            ottie_group_shape_new                   (void);
+
+OttieShape *            ottie_group_shape_parse                 (JsonReader             *reader);
+gboolean                ottie_group_shape_parse_shapes          (JsonReader             *reader,
+                                                                 gsize                   offset,
+                                                                 gpointer                data);
+
+G_END_DECLS
+
+#endif /* __OTTIE_GROUP_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottieintl.h b/ottie/ottieintl.h
new file mode 100644
index 0000000000..310b720660
--- /dev/null
+++ b/ottie/ottieintl.h
@@ -0,0 +1,15 @@
+#ifndef __OTTIE_INTL_H__
+#define __OTTIE_INTL_H__
+
+#include <glib/gi18n-lib.h>
+
+#ifdef ENABLE_NLS
+#define P_(String) g_dgettext(GETTEXT_PACKAGE "-properties",String)
+#else 
+#define P_(String) (String)
+#endif
+
+/* not really I18N-related, but also a string marker macro */
+#define I_(string) g_intern_static_string (string)
+
+#endif
diff --git a/ottie/ottiekeyframesimpl.c b/ottie/ottiekeyframesimpl.c
new file mode 100644
index 0000000000..255d17e3a4
--- /dev/null
+++ b/ottie/ottiekeyframesimpl.c
@@ -0,0 +1,382 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#ifndef OTTIE_KEYFRAMES_TYPE_NAME
+#define OTTIE_KEYFRAMES_TYPE_NAME OttieKeyframes
+#endif
+
+#ifndef OTTIE_KEYFRAMES_NAME
+#define OTTIE_KEYFRAMES_NAME ottie_keyframes
+#endif
+
+#ifndef OTTIE_KEYFRAMES_ELEMENT_TYPE
+#define OTTIE_KEYFRAMES_ELEMENT_TYPE gpointer
+#endif
+
+#ifndef OTTIE_KEYFRAMES_DIMENSIONS
+#define OTTIE_KEYFRAMES_DIMENSIONS 1
+#endif
+
+/* make this readable */
+#define _T_ OTTIE_KEYFRAMES_ELEMENT_TYPE
+#define OttieKeyframes OTTIE_KEYFRAMES_TYPE_NAME
+#define OttieKeyframe OTTIE_KEYFRAMES_TYPE_NAME ## Keyframe
+#define OttieControlPoint OTTIE_KEYFRAMES_TYPE_NAME ## ControlPoint
+#define ottie_keyframes_paste_more(OTTIE_KEYFRAMES_NAME, func_name) OTTIE_KEYFRAMES_NAME ## _ ## func_name
+#define ottie_keyframes_paste(OTTIE_KEYFRAMES_NAME, func_name) ottie_keyframes_paste_more 
(OTTIE_KEYFRAMES_NAME, func_name)
+#define ottie_keyframes(func_name) ottie_keyframes_paste (OTTIE_KEYFRAMES_NAME, func_name)
+
+typedef struct OttieControlPoint OttieControlPoint;
+typedef struct OttieKeyframe OttieKeyframe;
+typedef struct OttieKeyframes OttieKeyframes;
+
+struct OttieControlPoint
+{
+  double x[OTTIE_KEYFRAMES_DIMENSIONS];
+  double y[OTTIE_KEYFRAMES_DIMENSIONS];
+};
+
+struct OttieKeyframe
+{
+  /* Cubic control points, but Lottie names them in and out points */
+  OttieControlPoint in;
+  OttieControlPoint out;
+  double start_time;
+  _T_ start_value;
+  _T_ end_value;
+};
+
+struct OttieKeyframes
+{
+  gsize n_items;
+  OttieKeyframe items[];
+};
+
+static inline OttieKeyframes *
+ottie_keyframes(new) (gsize n_items)
+{
+  OttieKeyframes *self;
+
+  self = g_malloc0 (sizeof (OttieKeyframes) + n_items * sizeof (OttieKeyframe));
+  self->n_items = n_items;
+
+  return self;
+}
+
+static inline void
+ottie_keyframes(free_value) (_T_ *item)
+{
+#ifdef OTTIE_KEYFRAMES_FREE_FUNC
+#ifdef OTTIE_KEYFRAMES_BY_VALUE
+    OTTIE_KEYFRAMES_FREE_FUNC (item);
+#else
+    OTTIE_KEYFRAMES_FREE_FUNC (*item);
+#endif
+#endif
+}
+
+static inline void
+ottie_keyframes(copy_value) (_T_ *dest,
+                             _T_ *src)
+{
+#ifdef OTTIE_KEYFRAMES_COPY_FUNC
+#  ifdef OTTIE_KEYFRAMES_BY_VALUE
+  OTTIE_KEYFRAMES_COPY_FUNC (dest, src);
+#  else
+  *dest = OTTIE_KEYFRAMES_COPY_FUNC (*src);
+#  endif
+#else
+    *dest = *src;
+#endif
+}
+
+/* no G_GNUC_UNUSED here */
+static inline void
+ottie_keyframes(free) (OttieKeyframes *self)
+{
+#ifdef OTTIE_KEYFRAMES_FREE_FUNC
+  gsize i;
+
+  for (i = 0; i < self->n_items; i++)
+    {
+      ottie_keyframes(free_value) (&self->items[i].start_value);
+      ottie_keyframes(free_value) (&self->items[i].end_value);
+    }
+#endif
+}
+
+static
+#ifdef OTTIE_KEYFRAMES_BY_VALUE
+void
+#else
+_T_
+#endif
+ottie_keyframes(get) (const OttieKeyframes *self,
+                      double                timestamp
+#ifdef OTTIE_KEYFRAMES_BY_VALUE
+                      , _T_                *out_result
+#endif
+                      )
+{
+  const OttieKeyframe *keyframe;
+  gsize i;
+
+  for (i = 0; i < self->n_items; i++)
+    {
+      if (self->items[i].start_time > timestamp)
+        break;
+    }
+
+  if (i == 0 || i >= self->n_items)
+    {
+      keyframe = &self->items[i == 0 ? 0 : self->n_items - 1];
+#ifdef OTTIE_KEYFRAMES_BY_VALUE
+      *out_result = keyframe->start_value;
+      return;
+#else
+      return keyframe->start_value;
+#endif
+    }
+
+  keyframe = &self->items[i - 1];
+
+  double progress = (timestamp - keyframe->start_time) / (self->items[i].start_time - keyframe->start_time);
+#ifdef OTTIE_KEYFRAMES_BY_VALUE
+  OTTIE_KEYFRAMES_INTERPOLATE_FUNC (&keyframe->start_value, &keyframe->end_value, progress, out_result);
+#else
+  return OTTIE_KEYFRAMES_INTERPOLATE_FUNC (keyframe->start_value, keyframe->end_value, progress);
+#endif
+}
+
+static gboolean
+ottie_keyframes(parse_control_point_dimension) (JsonReader *reader,
+                                                gsize       offset,
+                                                gpointer    data)
+{
+  double d[OTTIE_KEYFRAMES_DIMENSIONS];
+
+  if (json_reader_is_array (reader))
+    {
+      if (json_reader_count_elements (reader) != OTTIE_KEYFRAMES_DIMENSIONS)
+        ottie_parser_error_value (reader, "control point has %d dimension, not %u", 
json_reader_count_elements (reader), OTTIE_KEYFRAMES_DIMENSIONS);
+
+      for (int i = 0; i < OTTIE_KEYFRAMES_DIMENSIONS; i++)
+        {
+          if (!json_reader_read_element (reader, i))
+            {
+              ottie_parser_emit_error (reader, json_reader_get_error (reader));
+            }
+          else
+            {
+              if (!ottie_parser_option_double (reader, 0, &d[i]))
+                d[i] = 0;
+            }
+          json_reader_end_element (reader);
+        }
+    }
+  else
+    {
+      if (!ottie_parser_option_double (reader, 0, &d[0]))
+        return FALSE;
+
+      for (gsize i = 1; i < OTTIE_KEYFRAMES_DIMENSIONS; i++)
+        d[i] = d[0];
+    }
+
+  memcpy ((guint8 *) data + GPOINTER_TO_SIZE (offset), d, sizeof (double) * OTTIE_KEYFRAMES_DIMENSIONS);
+  return TRUE;
+}
+
+static gboolean
+ottie_keyframes(parse_control_point) (JsonReader *reader,
+                                      gsize       offset,
+                                      gpointer    data)
+{
+  OttieParserOption options[] = {
+    { "x", ottie_keyframes(parse_control_point_dimension), G_STRUCT_OFFSET (OttieControlPoint, x) },
+    { "y", ottie_keyframes(parse_control_point_dimension), G_STRUCT_OFFSET (OttieControlPoint, y) },
+  };
+  OttieControlPoint cp;
+  OttieControlPoint *target = (OttieControlPoint *) ((guint8 *) data + offset);
+
+  if (!ottie_parser_parse_object (reader, "control point", options, G_N_ELEMENTS (options), &cp))
+    return FALSE;
+
+  *target = cp;
+  return TRUE;
+}
+
+typedef struct
+{
+  OttieKeyframe keyframe;
+  gboolean has_start_value;
+  gboolean has_end_value;
+} OttieKeyframeParse;
+
+static gboolean
+ottie_keyframes(parse_start_value) (JsonReader *reader,
+                                    gsize       pos,
+                                    gpointer    data)
+{
+  OttieKeyframeParse *parse = data;
+
+  if (parse->has_start_value)
+    ottie_keyframes(free_value) (&parse->keyframe.start_value);
+
+  parse->has_start_value = OTTIE_KEYFRAMES_PARSE_FUNC (reader, 0, &parse->keyframe.start_value);
+  return parse->has_start_value;
+}
+
+static gboolean
+ottie_keyframes(parse_end_value) (JsonReader *reader,
+                                  gsize       pos,
+                                  gpointer    data)
+{
+  OttieKeyframeParse *parse = data;
+
+  if (parse->has_end_value)
+    ottie_keyframes(free_value) (&parse->keyframe.end_value);
+
+  parse->has_end_value = OTTIE_KEYFRAMES_PARSE_FUNC (reader, 0, &parse->keyframe.end_value);
+  return parse->has_end_value;
+}
+
+typedef struct
+{
+  OttieKeyframes *keyframes;
+  gboolean has_end_value;
+} OttieKeyframesParse;
+
+static gboolean
+ottie_keyframes(parse_keyframe) (JsonReader *reader,
+                                 gsize       pos,
+                                 gpointer    data)
+{
+  OttieParserOption options[] = {
+    { "s", ottie_keyframes(parse_start_value), 0, },
+    { "e", ottie_keyframes(parse_end_value), 0, },
+    { "t", ottie_parser_option_double, G_STRUCT_OFFSET (OttieKeyframe, start_time) },
+    { "i", ottie_keyframes(parse_control_point), G_STRUCT_OFFSET (OttieKeyframe, in) },
+    { "o", ottie_keyframes(parse_control_point), G_STRUCT_OFFSET (OttieKeyframe, out) },
+    { "ix", ottie_parser_option_skip_index, 0 },
+  };
+  OttieKeyframesParse *self = data;
+  OttieKeyframeParse parse = { 0, };
+
+  if (!ottie_parser_parse_object (reader, "keyframe", options, G_N_ELEMENTS (options), &parse))
+    goto fail;
+
+  if (pos == 0)
+    {
+      if (!parse.has_start_value)
+        {
+          ottie_parser_error_syntax (reader, "First keyframe must have a start value");
+          return FALSE;
+        }
+    }
+  else
+    {
+      if (parse.keyframe.start_time <= self->keyframes->items[pos - 1].start_time)
+        goto fail;
+
+      if (!parse.has_start_value)
+        {
+          if (self->has_end_value)
+            {
+              ottie_keyframes(copy_value) (&parse.keyframe.start_value, &self->keyframes->items[pos - 
1].end_value);
+            }
+          else
+            {
+              ottie_parser_error_syntax (reader, "Keyframe %zu has no end value and %zu has no start 
value.", pos - 1, pos);
+              goto fail;
+            }
+        }
+
+      if (!self->has_end_value)
+        ottie_keyframes(copy_value) (&self->keyframes->items[pos - 1].end_value, 
&parse.keyframe.start_value);
+    }
+
+  self->has_end_value = parse.has_end_value;
+  self->keyframes->items[pos] = parse.keyframe;
+
+  return TRUE;
+
+fail:
+  self->keyframes->n_items = pos;
+  if (parse.has_start_value)
+    ottie_keyframes(free_value) (&parse.keyframe.start_value);
+  if (parse.has_end_value)
+    ottie_keyframes(free_value) (&parse.keyframe.end_value);
+  return FALSE;
+}
+
+/* no G_GNUC_UNUSED here, if you don't use a type, remove it. */
+static inline OttieKeyframes *
+ottie_keyframes(parse) (JsonReader *reader)
+{
+  OttieKeyframesParse parse;
+  OttieKeyframes *self;
+
+  self = ottie_keyframes(new) (json_reader_count_elements (reader));
+
+  parse.keyframes = self;
+  parse.has_end_value = FALSE;
+
+  if (!ottie_parser_parse_array (reader, "keyframes",
+                                 self->n_items, self->n_items,
+                                 NULL,
+                                 0, 1,
+                                 ottie_keyframes(parse_keyframe),
+                                 &parse))
+    {
+      /* do a dumb copy so the free has something to free */
+      if (!parse.has_end_value && self->n_items > 0)
+        ottie_keyframes(copy_value) (&self->items[self->n_items - 1].end_value,
+                                     &self->items[self->n_items - 1].start_value);
+      ottie_keyframes(free) (self);
+      return NULL;
+    }
+
+  if (!parse.has_end_value)
+    ottie_keyframes(copy_value) (&self->items[self->n_items - 1].end_value,
+                                 &self->items[self->n_items - 1].start_value);
+
+  return parse.keyframes;
+}
+
+#ifndef OTTIE_KEYFRAMES_NO_UNDEF
+
+#undef _T_
+#undef OttieKeyframes
+#undef ottie_keyframes_paste_more
+#undef ottie_keyframes_paste
+#undef ottie_keyframes
+
+#undef OTTIE_KEYFRAMES_COPY_FUNC
+#undef OTTIE_KEYFRAMES_PARSE_FUNC
+#undef OTTIE_KEYFRAMES_BY_VALUE
+#undef OTTIE_KEYFRAMES_ELEMENT_TYPE
+#undef OTTIE_KEYFRAMES_FREE_FUNC
+#undef OTTIE_KEYFRAMES_NAME
+#undef OTTIE_KEYFRAMES_TYPE_NAME
+#endif
diff --git a/ottie/ottielayer.c b/ottie/ottielayer.c
new file mode 100644
index 0000000000..0d88102150
--- /dev/null
+++ b/ottie/ottielayer.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottielayerprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <json-glib/json-glib.h>
+
+G_DEFINE_TYPE (OttieLayer, ottie_layer, OTTIE_TYPE_OBJECT)
+
+static void
+ottie_layer_default_update (OttieLayer *self,
+                            GHashTable *compositions)
+{
+}
+
+static void
+ottie_layer_default_render (OttieLayer  *self,
+                            OttieRender *render,
+                            double       timestamp)
+{
+}
+
+static void
+ottie_layer_dispose (GObject *object)
+{
+  OttieLayer *self = OTTIE_LAYER (object);
+
+  g_clear_object (&self->transform);
+  g_clear_pointer (&self->layer_name, g_free);
+
+  G_OBJECT_CLASS (ottie_layer_parent_class)->dispose (object);
+}
+
+static void
+ottie_layer_class_init (OttieLayerClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  klass->update = ottie_layer_default_update;
+  klass->render = ottie_layer_default_render;
+
+  gobject_class->dispose = ottie_layer_dispose;
+}
+
+static void
+ottie_layer_init (OttieLayer *self)
+{
+  self->start_frame = -G_MAXDOUBLE;
+  self->end_frame = G_MAXDOUBLE;
+  self->stretch = 1;
+  self->blend_mode = GSK_BLEND_MODE_DEFAULT;
+  self->parent_index = OTTIE_INT_UNSET;
+  self->index = OTTIE_INT_UNSET;
+}
+
+void
+ottie_layer_update (OttieLayer *self,
+                    GHashTable *compositions)
+{
+  OTTIE_LAYER_GET_CLASS (self)->update (self, compositions);
+}
+
+void
+ottie_layer_render (OttieLayer  *self,
+                    OttieRender *render,
+                    double       timestamp)
+{
+  if (timestamp < self->start_frame ||
+      timestamp > self->end_frame)
+    return;
+
+  timestamp -= self->start_time;
+  timestamp /= self->stretch;
+
+  OTTIE_LAYER_GET_CLASS (self)->render (self, render, timestamp);
+}
+
diff --git a/ottie/ottielayerprivate.h b/ottie/ottielayerprivate.h
new file mode 100644
index 0000000000..6d5007f4ea
--- /dev/null
+++ b/ottie/ottielayerprivate.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_LAYER_PRIVATE_H__
+#define __OTTIE_LAYER_PRIVATE_H__
+
+#include "ottie/ottietransformprivate.h"
+#include "ottie/ottieobjectprivate.h"
+#include "ottie/ottieparserprivate.h"
+#include "ottie/ottierenderprivate.h"
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_LAYER         (ottie_layer_get_type ())
+#define OTTIE_LAYER(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_LAYER, OttieLayer))
+#define OTTIE_LAYER_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_LAYER, OttieLayerClass))
+#define OTTIE_IS_LAYER(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_LAYER))
+#define OTTIE_IS_LAYER_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_LAYER))
+#define OTTIE_LAYER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_LAYER, OttieLayerClass))
+
+typedef struct _OttieLayer OttieLayer;
+typedef struct _OttieLayerClass OttieLayerClass;
+
+struct _OttieLayer
+{
+  OttieObject parent;
+
+  OttieTransform *transform;
+  gboolean auto_orient;
+  GskBlendMode blend_mode;
+  int index;
+  int parent_index;
+  char *layer_name;
+  double start_frame;
+  double end_frame;
+  double start_time;
+  double stretch;
+};
+
+struct _OttieLayerClass
+{
+  OttieObjectClass parent_class;
+
+  void                  (* update)                           (OttieLayer                *layer,
+                                                              GHashTable                *compositions);
+  void                  (* render)                           (OttieLayer                *layer,
+                                                              OttieRender               *render,
+                                                              double                     timestamp);
+};
+
+GType                   ottie_layer_get_type                 (void) G_GNUC_CONST;
+
+void                    ottie_layer_update                   (OttieLayer                *self,
+                                                              GHashTable                *compositions);
+void                    ottie_layer_render                   (OttieLayer                *self,
+                                                              OttieRender               *render,
+                                                              double                     timestamp);
+
+#define OTTIE_PARSE_OPTIONS_LAYER \
+    OTTIE_PARSE_OPTIONS_OBJECT, \
+    { "ao", ottie_parser_option_boolean, G_STRUCT_OFFSET (OttieLayer, auto_orient) }, \
+    { "bm", ottie_parser_option_blend_mode, G_STRUCT_OFFSET (OttieLayer, blend_mode) }, \
+    { "ln", ottie_parser_option_string, G_STRUCT_OFFSET (OttieLayer, layer_name) }, \
+    { "ks", ottie_parser_option_transform, G_STRUCT_OFFSET (OttieLayer, transform) }, \
+    { "ip", ottie_parser_option_double, G_STRUCT_OFFSET (OttieLayer, start_frame) }, \
+    { "ind", ottie_parser_option_int, G_STRUCT_OFFSET (OttieLayer, index) }, \
+    { "parent", ottie_parser_option_int, G_STRUCT_OFFSET (OttieLayer, parent_index) }, \
+    { "op", ottie_parser_option_double, G_STRUCT_OFFSET (OttieLayer, end_frame) }, \
+    { "st", ottie_parser_option_double, G_STRUCT_OFFSET (OttieLayer, start_time) }, \
+    { "sr", ottie_parser_option_double, G_STRUCT_OFFSET (OttieLayer, stretch) }, \
+    { "ddd", ottie_parser_option_3d, 0 }, \
+    { "ix",  ottie_parser_option_skip_index, 0 }, \
+    { "ty", ottie_parser_option_skip, 0 }
+
+G_END_DECLS
+
+#endif /* __OTTIE_LAYER_PRIVATE_H__ */
diff --git a/ottie/ottienulllayer.c b/ottie/ottienulllayer.c
new file mode 100644
index 0000000000..baf0f599ee
--- /dev/null
+++ b/ottie/ottienulllayer.c
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottienulllayerprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+struct _OttieNullLayer
+{
+  OttieLayer parent;
+};
+
+struct _OttieNullLayerClass
+{
+  OttieLayerClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieNullLayer, ottie_null_layer, OTTIE_TYPE_LAYER)
+
+static void
+ottie_null_layer_class_init (OttieNullLayerClass *klass)
+{
+}
+
+static void
+ottie_null_layer_init (OttieNullLayer *self)
+{
+}
+
+OttieLayer *
+ottie_null_layer_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_LAYER,
+  };
+  OttieNullLayer *self;
+
+  self = g_object_new (OTTIE_TYPE_NULL_LAYER, NULL);
+
+  if (!ottie_parser_parse_object (reader, "null layer", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_LAYER (self);
+}
+
diff --git a/ottie/ottienulllayerprivate.h b/ottie/ottienulllayerprivate.h
new file mode 100644
index 0000000000..40347a2741
--- /dev/null
+++ b/ottie/ottienulllayerprivate.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_NULL_LAYER_PRIVATE_H__
+#define __OTTIE_NULL_LAYER_PRIVATE_H__
+
+#include "ottielayerprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_NULL_LAYER         (ottie_null_layer_get_type ())
+#define OTTIE_NULL_LAYER(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_NULL_LAYER, 
OttieNullLayer))
+#define OTTIE_NULL_LAYER_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_NULL_LAYER, 
OttieNullLayerClass))
+#define OTTIE_IS_NULL_LAYER(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_NULL_LAYER))
+#define OTTIE_IS_NULL_LAYER_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_NULL_LAYER))
+#define OTTIE_NULL_LAYER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_NULL_LAYER, 
OttieNullLayerClass))
+
+typedef struct _OttieNullLayer OttieNullLayer;
+typedef struct _OttieNullLayerClass OttieNullLayerClass;
+
+GType                   ottie_null_layer_get_type               (void) G_GNUC_CONST;
+
+OttieLayer *            ottie_null_layer_parse                  (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_NULL_LAYER_PRIVATE_H__ */
diff --git a/ottie/ottieobject.c b/ottie/ottieobject.c
new file mode 100644
index 0000000000..5720555b77
--- /dev/null
+++ b/ottie/ottieobject.c
@@ -0,0 +1,175 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottieobjectprivate.h"
+
+#include "ottieintl.h"
+
+enum {
+  PROP_0,
+  PROP_MATCH_NAME,
+  PROP_NAME,
+
+  N_PROPS,
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+G_DEFINE_TYPE (OttieObject, ottie_object, G_TYPE_OBJECT)
+
+static void
+ottie_object_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+
+{
+  OttieObject *self = OTTIE_OBJECT (object);
+
+  switch (prop_id)
+    {
+    case PROP_MATCH_NAME:
+      ottie_object_set_match_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_NAME:
+      ottie_object_set_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_object_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  OttieObject *self = OTTIE_OBJECT (object);
+
+  switch (prop_id)
+    {
+    case PROP_MATCH_NAME:
+      g_value_set_string (value, self->match_name);
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, self->name);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_object_dispose (GObject *object)
+{
+  OttieObject *self = OTTIE_OBJECT (object);
+
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->match_name, g_free);
+
+  G_OBJECT_CLASS (ottie_object_parent_class)->dispose (object);
+}
+
+static void
+ottie_object_class_init (OttieObjectClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+
+  gobject_class->set_property = ottie_object_set_property;
+  gobject_class->get_property = ottie_object_get_property;
+  gobject_class->dispose = ottie_object_dispose;
+
+  properties[PROP_NAME] =
+    g_param_spec_string ("name",
+                         P_("Name"),
+                         P_("User-given name"),
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  properties[PROP_MATCH_NAME] =
+    g_param_spec_string ("match-name",
+                         P_("Match name"),
+                         P_("Name for matching in scripts"),
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, properties);
+}
+
+static void
+ottie_object_init (OttieObject *self)
+{
+}
+
+void
+ottie_object_set_name (OttieObject *self,
+                       const char  *name)
+{
+  g_return_if_fail (OTTIE_IS_OBJECT (self));
+
+  if (g_strcmp0 (self->name, name) == 0)
+    return;
+
+  g_free (self->name);
+  self->name = g_strdup (name);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+}
+
+const char *
+ottie_object_get_name (OttieObject *self)
+{
+  g_return_val_if_fail (OTTIE_IS_OBJECT (self), NULL);
+
+  return self->name;
+}
+
+void
+ottie_object_set_match_name (OttieObject *self,
+                             const char  *match_name)
+{
+  g_return_if_fail (OTTIE_IS_OBJECT (self));
+
+  if (g_strcmp0 (self->match_name, match_name) == 0)
+    return;
+
+  g_free (self->match_name);
+  self->match_name = g_strdup (match_name);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MATCH_NAME]);
+}
+
+const char *
+ottie_object_get_match_name (OttieObject *self)
+{
+  g_return_val_if_fail (OTTIE_IS_OBJECT (self), NULL);
+
+  return self->match_name;
+}
+
+
diff --git a/ottie/ottieobjectprivate.h b/ottie/ottieobjectprivate.h
new file mode 100644
index 0000000000..1ea562afb9
--- /dev/null
+++ b/ottie/ottieobjectprivate.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_OBJECT_PRIVATE_H__
+#define __OTTIE_OBJECT_PRIVATE_H__
+
+#include <glib-object.h>
+
+#include "ottie/ottierenderprivate.h"
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_OBJECT         (ottie_object_get_type ())
+#define OTTIE_OBJECT(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_OBJECT, OttieObject))
+#define OTTIE_OBJECT_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_OBJECT, OttieObjectClass))
+#define OTTIE_IS_OBJECT(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_OBJECT))
+#define OTTIE_IS_OBJECT_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_OBJECT))
+#define OTTIE_OBJECT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_OBJECT, OttieObjectClass))
+
+typedef struct _OttieObject OttieObject;
+typedef struct _OttieObjectClass OttieObjectClass;
+
+struct _OttieObject
+{
+  GObject parent;
+
+  char *name;
+  char *match_name;
+};
+
+struct _OttieObjectClass
+{
+  GObjectClass parent_class;
+};
+
+GType                   ottie_object_get_type                   (void) G_GNUC_CONST;
+
+void                    ottie_object_set_name                   (OttieObject            *self,
+                                                                 const char             *name);
+const char *            ottie_object_get_name                   (OttieObject            *self);
+
+void                    ottie_object_set_match_name             (OttieObject            *self,
+                                                                 const char             *match_name);
+const char *            ottie_object_get_match_name             (OttieObject            *self);
+
+#define OTTIE_PARSE_OPTIONS_OBJECT \
+    { "nm", ottie_parser_option_string, G_STRUCT_OFFSET (OttieObject, name) }, \
+    { "mn", ottie_parser_option_string, G_STRUCT_OFFSET (OttieObject, match_name) }
+
+G_END_DECLS
+
+#endif /* __OTTIE_OBJECT_PRIVATE_H__ */
diff --git a/ottie/ottiepaintable.c b/ottie/ottiepaintable.c
new file mode 100644
index 0000000000..27b273300b
--- /dev/null
+++ b/ottie/ottiepaintable.c
@@ -0,0 +1,381 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiepaintable.h"
+
+#include "ottiecreationprivate.h"
+
+#include <math.h>
+#include <glib/gi18n.h>
+
+struct _OttiePaintable
+{
+  GObject parent_instance;
+
+  OttieCreation *creation;
+  gint64 timestamp;
+};
+
+struct _OttiePaintableClass
+{
+  GObjectClass parent_class;
+};
+
+enum {
+  PROP_0,
+  PROP_CREATION,
+  PROP_DURATION,
+  PROP_TIMESTAMP,
+
+  N_PROPS,
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+static void
+ottie_paintable_paintable_snapshot (GdkPaintable *paintable,
+                                    GdkSnapshot  *snapshot,
+                                    double        width,
+                                    double        height)
+{
+  OttiePaintable *self = OTTIE_PAINTABLE (paintable);
+  double w, h, timestamp;
+
+  if (!self->creation)
+    return;
+
+  w = ottie_creation_get_width (self->creation);
+  h = ottie_creation_get_height (self->creation);
+  timestamp = (double) self->timestamp / G_USEC_PER_SEC;
+
+  if (w != width || h != height)
+    {
+      gtk_snapshot_save (snapshot);
+      gtk_snapshot_scale (snapshot, width / w, height / h);
+    }
+
+  gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT (0, 0, w, h));
+  ottie_creation_snapshot (self->creation, snapshot, timestamp);
+  gtk_snapshot_pop (snapshot);
+
+  if (w != width || h != height)
+    gtk_snapshot_restore (snapshot);
+}
+
+static int
+ottie_paintable_paintable_get_intrinsic_width (GdkPaintable *paintable)
+{
+  OttiePaintable *self = OTTIE_PAINTABLE (paintable);
+
+  if (!self->creation)
+    return 0;
+
+  return ceil (ottie_creation_get_width (self->creation));
+}
+
+static int
+ottie_paintable_paintable_get_intrinsic_height (GdkPaintable *paintable)
+{
+  OttiePaintable *self = OTTIE_PAINTABLE (paintable);
+
+  if (!self->creation)
+    return 0;
+
+  return ceil (ottie_creation_get_height (self->creation));
+
+}
+
+static void
+ottie_paintable_paintable_init (GdkPaintableInterface *iface)
+{
+  iface->snapshot = ottie_paintable_paintable_snapshot;
+  iface->get_intrinsic_width = ottie_paintable_paintable_get_intrinsic_width;
+  iface->get_intrinsic_height = ottie_paintable_paintable_get_intrinsic_height;
+}
+
+G_DEFINE_TYPE_EXTENDED (OttiePaintable, ottie_paintable, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
+                                               ottie_paintable_paintable_init))
+
+static void
+ottie_paintable_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+
+{
+  OttiePaintable *self = OTTIE_PAINTABLE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CREATION:
+      ottie_paintable_set_creation (self, g_value_get_object (value));
+      break;
+
+    case PROP_TIMESTAMP:
+      ottie_paintable_set_timestamp (self, g_value_get_int64 (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_paintable_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  OttiePaintable *self = OTTIE_PAINTABLE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CREATION:
+      g_value_set_object (value, self->creation);
+      break;
+
+    case PROP_DURATION:
+      g_value_set_int64 (value, ottie_paintable_get_duration (self));
+      break;
+
+    case PROP_TIMESTAMP:
+      g_value_set_int64 (value, self->timestamp);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_paintable_prepared_cb (OttieCreation  *creation,
+                             GParamSpec     *pspec,
+                             OttiePaintable *self)
+{
+  gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DURATION]);
+}
+
+static void
+ottie_paintable_unset_creation (OttiePaintable *self)
+{
+  if (self->creation == NULL)
+    return;
+
+  g_signal_handlers_disconnect_by_func (self->creation, ottie_paintable_prepared_cb, self);
+  g_clear_object (&self->creation);
+}
+
+static void
+ottie_paintable_dispose (GObject *object)
+{
+  OttiePaintable *self = OTTIE_PAINTABLE (object);
+
+  ottie_paintable_unset_creation (self);
+
+  G_OBJECT_CLASS (ottie_paintable_parent_class)->dispose (object);
+}
+
+static void
+ottie_paintable_class_init (OttiePaintableClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gobject_class->get_property = ottie_paintable_get_property;
+  gobject_class->set_property = ottie_paintable_set_property;
+  gobject_class->dispose = ottie_paintable_dispose;
+
+  /**
+   * OttiePaintable:creation
+   *
+   * The displayed creation or %NULL.
+   */
+  properties[PROP_CREATION] =
+    g_param_spec_object ("creation",
+                         _("Creation"),
+                         _("The displayed creation"),
+                         OTTIE_TYPE_CREATION,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttiePaintable:duration
+   *
+   * Duration of the displayed creation
+   */
+  properties[PROP_DURATION] =
+    g_param_spec_int64 ("duration",
+                        _("Duration"),
+                        _("Duration of the displayed creation"),
+                        0, G_MAXINT64, 0,
+                        G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * OttiePaintable:timestamp
+   *
+   * At what timestamp to display the creation.
+   */
+  properties[PROP_TIMESTAMP] =
+    g_param_spec_int64 ("timestmp",
+                        _("Timestamp"),
+                        _("At what timestamp to display the creation"),
+                        0, G_MAXINT64, 0,
+                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, properties);
+}
+
+static void
+ottie_paintable_init (OttiePaintable *self)
+{
+}
+
+/**
+ * ottie_paintable_new:
+ * @creation: (allow-none) (transfer full): an #OttiePaintable or %NULL
+ *
+ * Creates a new Ottie paintable for the given @creation
+ *
+ * Returns: (transfer full) (type OttiePaintable): a new #OttiePaintable
+ **/
+OttiePaintable *
+ottie_paintable_new (OttieCreation *creation)
+{
+  OttiePaintable *self;
+
+  g_return_val_if_fail (creation == creation || OTTIE_IS_CREATION (creation), NULL);
+
+  self = g_object_new (OTTIE_TYPE_PAINTABLE,
+                       "creation", creation,
+                       NULL);
+
+  g_clear_object (&creation);
+
+  return self;
+}
+
+/**
+ * ottie_paintable_get_creation:
+ * @self: an #OttiePaintable
+ *
+ * Returns the creation that shown or %NULL
+ * if none.
+ *
+ * Returns: (transfer none) (nullable): the observed creation.
+ **/
+OttieCreation *
+ottie_paintable_get_creation (OttiePaintable *self)
+{
+  g_return_val_if_fail (OTTIE_IS_PAINTABLE (self), NULL);
+
+  return self->creation;
+}
+
+/**
+ * ottie_paintable_set_creation:
+ * @self: an #OttiePaintable
+ * @creation: (allow-none): the creation to show or %NULL
+ *
+ * Sets the creation that should be shown.
+ **/
+void
+ottie_paintable_set_creation (OttiePaintable *self,
+                              OttieCreation  *creation)
+{
+  g_return_if_fail (OTTIE_IS_PAINTABLE (self));
+  g_return_if_fail (creation == NULL || OTTIE_IS_CREATION (creation));
+
+  if (self->creation == creation)
+    return;
+
+  ottie_paintable_unset_creation (self);
+
+  self->creation = g_object_ref (creation);
+  g_signal_connect (creation, "notify::prepared", G_CALLBACK (ottie_paintable_prepared_cb), self);
+
+  gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CREATION]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DURATION]);
+}
+
+/**
+ * ottie_paintable_get_timestamp:
+ * @self: an #OttiePaintable
+ *
+ * Gets the timestamp for the currently displayed image. 
+ *
+ * Returns: the timestamp
+ **/
+gint64
+ottie_paintable_get_timestamp (OttiePaintable *self)
+{
+  g_return_val_if_fail (OTTIE_IS_PAINTABLE (self), 0);
+
+  return self->timestamp;
+}
+
+/**
+ * ottie_paintable_set_timestamp:
+ * @self: an #OttiePaintable
+ * @timestamp: the timestamp to display
+ *
+ * Sets the timestamp to display the creation at.
+ **/
+void
+ottie_paintable_set_timestamp (OttiePaintable *self,
+                               gint64          timestamp)
+{
+  g_return_if_fail (OTTIE_IS_PAINTABLE (self));
+  g_return_if_fail (timestamp >= 0);
+
+  if (self->timestamp == timestamp)
+    return;
+
+  self->timestamp = timestamp;
+
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TIMESTAMP]);
+}
+
+/**
+ * ottie_paintable_get_duration:
+ * @self: an #OttiePaintable
+ *
+ * Gets the duration of the currently playing creation.
+ *
+ * Returns: The duration in usec.
+ **/
+gint64
+ottie_paintable_get_duration (OttiePaintable *self)
+{
+  g_return_val_if_fail (OTTIE_IS_PAINTABLE (self), 0);
+
+  if (self->creation == NULL)
+    return 0;
+
+  return ceil (G_USEC_PER_SEC * ottie_creation_get_end_frame (self->creation)
+               / ottie_creation_get_frame_rate (self->creation));
+}
+
diff --git a/ottie/ottiepaintable.h b/ottie/ottiepaintable.h
new file mode 100644
index 0000000000..6c818194a1
--- /dev/null
+++ b/ottie/ottiepaintable.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_PAINTABLE_H__
+#define __OTTIE_PAINTABLE_H__
+
+#if !defined (__OTTIE_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <ottie/ottie.h> can be included directly."
+#endif
+
+#include <ottie/ottiecreation.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_PAINTABLE (ottie_paintable_get_type ())
+
+GDK_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (OttiePaintable, ottie_paintable, OTTIE, PAINTABLE, GObject)
+
+GDK_AVAILABLE_IN_ALL
+OttiePaintable *        ottie_paintable_new                (OttieCreation       *creation);
+
+GDK_AVAILABLE_IN_ALL
+OttieCreation *         ottie_paintable_get_creation       (OttiePaintable      *self);
+GDK_AVAILABLE_IN_ALL
+void                    ottie_paintable_set_creation       (OttiePaintable      *self,
+                                                            OttieCreation       *creation);
+GDK_AVAILABLE_IN_ALL
+gint64                  ottie_paintable_get_timestamp      (OttiePaintable      *self);
+GDK_AVAILABLE_IN_ALL
+void                    ottie_paintable_set_timestamp      (OttiePaintable      *self,
+                                                            gint64               timestamp);
+GDK_AVAILABLE_IN_ALL
+gint64                  ottie_paintable_get_duration       (OttiePaintable      *self);
+
+G_END_DECLS
+
+#endif /* __OTTIE_PAINTABLE_H__ */
diff --git a/ottie/ottieparser.c b/ottie/ottieparser.c
new file mode 100644
index 0000000000..c0be81c951
--- /dev/null
+++ b/ottie/ottieparser.c
@@ -0,0 +1,592 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottieparserprivate.h"
+
+#include "ottietransformprivate.h"
+
+#include <gsk/gsk.h>
+
+void
+ottie_parser_emit_error (JsonReader    *reader,
+                         const GError  *error)
+{
+  g_printerr ("Ottie is sad: %s\n", error->message);
+}
+
+void
+ottie_parser_error_syntax (JsonReader *reader,
+                           const char *format,
+                           ...)
+{
+  va_list args;
+  GError *error;
+
+  va_start (args, format);
+  error = g_error_new_valist (JSON_PARSER_ERROR,
+                              JSON_PARSER_ERROR_INVALID_DATA,
+                              format, args);
+  va_end (args);
+
+  ottie_parser_emit_error (reader, error);
+
+  g_error_free (error);
+}
+
+void
+ottie_parser_error_value (JsonReader *reader,
+                          const char *format,
+                          ...)
+{
+  va_list args;
+  GError *error;
+
+  va_start (args, format);
+  error = g_error_new_valist (JSON_PARSER_ERROR,
+                              JSON_PARSER_ERROR_INVALID_DATA,
+                              format, args);
+  va_end (args);
+
+  ottie_parser_emit_error (reader, error);
+
+  g_error_free (error);
+}
+
+void
+ottie_parser_error_unsupported (JsonReader *reader,
+                                const char *format,
+                                ...)
+{
+  va_list args;
+  GError *error;
+
+  va_start (args, format);
+  error = g_error_new_valist (JSON_PARSER_ERROR,
+                              JSON_PARSER_ERROR_INVALID_DATA,
+                              format, args);
+  va_end (args);
+
+  ottie_parser_emit_error (reader, error);
+
+  g_error_free (error);
+}
+
+gboolean
+ottie_parser_parse_array (JsonReader     *reader,
+                          const char     *debug_name,
+                          guint           min_items,
+                          guint           max_items,
+                          guint          *out_n_items,
+                          gsize           start_offset,
+                          gsize           offset_multiplier,
+                          OttieParseFunc  func,
+                          gpointer        data)
+{
+  guint i;
+
+  if (!json_reader_is_array (reader))
+    {
+      if (min_items > 1)
+        {
+          ottie_parser_error_syntax (reader, "Expected an array when parsing %s", debug_name);
+          if (out_n_items)
+            *out_n_items = 0;
+          return FALSE;
+        }
+      else
+        {
+          if (!func (reader, start_offset, data))
+            {
+              if (out_n_items)
+                *out_n_items = 0;
+              return FALSE;
+            }
+          if (out_n_items)
+            *out_n_items = 1;
+          return TRUE;
+        }
+    }
+
+  if (json_reader_count_elements (reader) < min_items)
+    {
+      ottie_parser_error_syntax (reader, "%s needs %u items, but only %u given",
+                                 debug_name, min_items, json_reader_count_elements (reader));
+      return FALSE;
+    }
+  max_items = MIN (max_items, json_reader_count_elements (reader));
+
+  for (i = 0; i < max_items; i++)
+    {
+      if (!json_reader_read_element (reader, i) || 
+          !func (reader, start_offset + offset_multiplier * i, data))
+        {
+          json_reader_end_element (reader);
+          if (out_n_items)
+            *out_n_items = i;
+          return FALSE;
+        }
+
+      json_reader_end_element (reader);
+    }
+
+  if (out_n_items)
+    *out_n_items = i;
+  return TRUE;
+}
+
+gboolean
+ottie_parser_parse_object (JsonReader              *reader,
+                           const char              *debug_name,
+                           const OttieParserOption *options,
+                           gsize                    n_options,
+                           gpointer                 data)
+{
+  if (!json_reader_is_object (reader))
+    {
+      ottie_parser_error_syntax (reader, "Expected an object when parsing %s", debug_name);
+      return FALSE;
+    }
+
+  for (int i = 0; ; i++)
+    {
+      const OttieParserOption *o = NULL;
+      const char *name;
+
+      if (!json_reader_read_element (reader, i))
+        break;
+
+      name = json_reader_get_member_name (reader);
+
+      for (gsize j = 0; j < n_options; j++)
+        {
+          o = &options[j];
+          if (g_str_equal (o->name, name))
+            break;
+          o = NULL;
+        }
+
+      if (o)
+        {
+          if (!o->parse_func (reader, o->option_data, data))
+            {
+              json_reader_end_element (reader);
+              return FALSE;
+            }
+        }
+      else
+        {
+          ottie_parser_error_unsupported (reader, "Unsupported %s property \"%s\"", debug_name, 
json_reader_get_member_name (reader));
+        }
+
+      json_reader_end_element (reader);
+    }
+
+  json_reader_end_element (reader);
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_skip (JsonReader *reader,
+                          gsize       offset,
+                          gpointer    data)
+{
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_boolean (JsonReader *reader,
+                             gsize       offset,
+                             gpointer    data)
+{
+  gboolean b;
+
+  b = json_reader_get_boolean_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      json_reader_end_element (reader);
+      return FALSE;
+    }
+  
+  *(gboolean *) ((guint8 *) data + offset) = b;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_double (JsonReader *reader,
+                            gsize       offset,
+                            gpointer    data)
+{
+  double d;
+
+  d = json_reader_get_double_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  *(double *) ((guint8 *) data + offset) = d;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_int (JsonReader *reader,
+                         gsize       offset,
+                         gpointer    data)
+{
+  gint64 i;
+
+  i = json_reader_get_int_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  if (i > G_MAXINT || i < G_MININT)
+    {
+      ottie_parser_error_value (reader, "Integer value %"G_GINT64_FORMAT" out of range", i);
+      return FALSE;
+    }
+  if (i == OTTIE_INT_UNSET)
+    {
+      ottie_parser_error_unsupported (reader, "The Integer value %d is a magic internal value of Ottie, file 
a bug", OTTIE_INT_UNSET);
+      return FALSE;
+    }
+
+  *(int *) ((guint8 *) data + offset) = i;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_string (JsonReader *reader,
+                            gsize       offset,
+                            gpointer    data)
+{
+  char **target;
+  const char *s;
+
+  s = json_reader_get_string_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  target = (char **) ((guint8 *) data + offset);
+
+  g_clear_pointer (target, g_free);
+  *target = g_strdup (s);
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_blend_mode (JsonReader *reader,
+                                gsize       offset,
+                                gpointer    data)
+{
+  GskBlendMode blend_mode;
+  gint64 i;
+
+  i = json_reader_get_int_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  switch (i)
+  {
+    case 0:
+      blend_mode = GSK_BLEND_MODE_DEFAULT;
+      break;
+
+    case 1:
+      blend_mode = GSK_BLEND_MODE_MULTIPLY;
+      break;
+
+    case 2:
+      blend_mode = GSK_BLEND_MODE_SCREEN;
+      break;
+
+    case 3:
+      blend_mode = GSK_BLEND_MODE_OVERLAY;
+      break;
+
+    case 4:
+      blend_mode = GSK_BLEND_MODE_DARKEN;
+      break;
+
+    case 5:
+      blend_mode = GSK_BLEND_MODE_LIGHTEN;
+      break;
+
+    case 6:
+      blend_mode = GSK_BLEND_MODE_COLOR_DODGE;
+      break;
+
+    case 7:
+      blend_mode = GSK_BLEND_MODE_COLOR_BURN;
+      break;
+
+    case 8:
+      blend_mode = GSK_BLEND_MODE_HARD_LIGHT;
+      break;
+
+    case 9:
+      blend_mode = GSK_BLEND_MODE_SOFT_LIGHT;
+      break;
+
+    case 10:
+      blend_mode = GSK_BLEND_MODE_DIFFERENCE;
+      break;
+
+    case 11:
+      blend_mode = GSK_BLEND_MODE_EXCLUSION;
+      break;
+
+    case 12:
+      blend_mode = GSK_BLEND_MODE_HUE;
+      break;
+
+    case 13:
+      blend_mode = GSK_BLEND_MODE_SATURATION;
+      break;
+
+    case 14:
+      blend_mode = GSK_BLEND_MODE_COLOR;
+      break;
+
+    case 15:
+      blend_mode = GSK_BLEND_MODE_LUMINOSITY;
+      break;
+
+    default:
+      ottie_parser_error_value (reader, "%"G_GINT64_FORMAT" is not a known blend mode", i);
+      return FALSE;
+  }
+
+  if (blend_mode != GSK_BLEND_MODE_DEFAULT)
+    ottie_parser_error_value (reader, "Blend modes are not implemented yet.");
+    
+  *(GskBlendMode *) ((guint8 *) data + offset) = blend_mode;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_3d (JsonReader *reader,
+                        gsize       offset,
+                        gpointer    data)
+{
+  double d;
+
+  d = json_reader_get_double_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  if (d != 0)
+    {
+      ottie_parser_error_value (reader, "3D is not supported.");
+    }
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_direction (JsonReader *reader,
+                               gsize       offset,
+                               gpointer    data)
+{
+  OttieDirection direction;
+  gint64 i;
+
+  i = json_reader_get_int_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  switch (i)
+  {
+    default:
+      ottie_parser_error_value (reader, "%"G_GINT64_FORMAT" is not a known direction", i);
+      G_GNUC_FALLTHROUGH;
+    case 0:
+      direction = OTTIE_DIRECTION_FORWARD;
+      break;
+
+    case 1:
+    case 2:
+      direction = OTTIE_DIRECTION_BACKWARD;
+      break;
+  }
+
+  *(OttieDirection *) ((guint8 *) data + offset) = direction;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_line_cap (JsonReader *reader,
+                              gsize       offset,
+                              gpointer    data)
+{
+  GskLineCap line_cap;
+  gint64 i;
+
+  i = json_reader_get_int_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  switch (i)
+  {
+    case 1:
+      line_cap = GSK_LINE_CAP_BUTT;
+      break;
+
+    case 2:
+      line_cap = GSK_LINE_CAP_ROUND;
+      break;
+
+    case 3:
+      line_cap = GSK_LINE_CAP_SQUARE;
+      break;
+
+    default:
+      ottie_parser_error_value (reader, "%"G_GINT64_FORMAT" is not a known line cap", i);
+      return FALSE;
+  }
+
+  *(GskLineCap *) ((guint8 *) data + offset) = line_cap;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_line_join (JsonReader *reader,
+                               gsize       offset,
+                               gpointer    data)
+{
+  GskLineJoin line_join;
+  gint64 i;
+
+  i = json_reader_get_int_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  switch (i)
+  {
+    case 1:
+      line_join = GSK_LINE_JOIN_MITER;
+      break;
+
+    case 2:
+      line_join = GSK_LINE_JOIN_ROUND;
+      break;
+
+    case 3:
+      line_join = GSK_LINE_JOIN_BEVEL;
+      break;
+
+    default:
+      ottie_parser_error_value (reader, "%"G_GINT64_FORMAT" is not a known line join", i);
+      return FALSE;
+  }
+
+  *(GskLineJoin *) ((guint8 *) data + offset) = line_join;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_fill_rule (JsonReader *reader,
+                               gsize       offset,
+                               gpointer    data)
+{
+  GskFillRule fill_rule;
+  gint64 i;
+
+  i = json_reader_get_int_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  switch (i)
+  {
+    case 1:
+      fill_rule = GSK_FILL_RULE_WINDING;
+      break;
+
+    case 2:
+      fill_rule = GSK_FILL_RULE_EVEN_ODD;
+      break;
+
+    default:
+      ottie_parser_error_value (reader, "%"G_GINT64_FORMAT" is not a known fill rule", i);
+      /* XXX: really? */
+      fill_rule = GSK_FILL_RULE_EVEN_ODD;
+      break;
+  }
+
+  *(GskFillRule *) ((guint8 *) data + offset) = fill_rule;
+
+  return TRUE;
+}
+
+gboolean
+ottie_parser_option_transform (JsonReader *reader,
+                               gsize       offset,
+                               gpointer    data)
+{
+  OttieShape **target;
+  OttieShape *t;
+
+  t = ottie_transform_parse (reader);
+  if (t == NULL)
+    return FALSE;
+  
+  target = (OttieShape **) ((guint8 *) data + offset);
+
+  g_clear_object (target);
+  *target = t;
+
+  return TRUE;
+}
+
diff --git a/ottie/ottieparserprivate.h b/ottie/ottieparserprivate.h
new file mode 100644
index 0000000000..27fc41acdc
--- /dev/null
+++ b/ottie/ottieparserprivate.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_PARSER_PRIVATE_H__
+#define __OTTIE_PARSER_PRIVATE_H__
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+/* for integers where we want to track that nobody has assigned a value to them */
+#define OTTIE_INT_UNSET G_MININT
+
+typedef enum
+{
+  OTTIE_DIRECTION_FORWARD,
+  OTTIE_DIRECTION_BACKWARD
+} OttieDirection;
+
+typedef struct _OttieParserOption OttieParserOption;
+
+typedef gboolean (* OttieParseFunc) (JsonReader *reader, gsize offset, gpointer data);
+
+struct _OttieParserOption
+{
+  const char *name;
+  OttieParseFunc parse_func;
+  gsize option_data;
+};
+
+void                    ottie_parser_emit_error                 (JsonReader             *reader,
+                                                                 const GError           *error);
+void                    ottie_parser_error_syntax               (JsonReader             *reader,
+                                                                 const char             *format,
+                                                                 ...) G_GNUC_PRINTF (2, 3);
+void                    ottie_parser_error_value                (JsonReader             *reader,
+                                                                 const char             *format,
+                                                                 ...) G_GNUC_PRINTF (2, 3);
+void                    ottie_parser_error_unsupported          (JsonReader             *reader,
+                                                                 const char             *format,
+                                                                 ...) G_GNUC_PRINTF (2, 3);
+
+gboolean                ottie_parser_parse_array               (JsonReader              *reader,
+                                                                const char              *debug_name,
+                                                                guint                    min_items,
+                                                                guint                    max_items,
+                                                                guint                   *out_n_items,
+                                                                gsize                    start_offset,
+                                                                gsize                    offset_multiplier,
+                                                                OttieParseFunc           func,
+                                                                gpointer                 data);
+
+gboolean                ottie_parser_parse_object              (JsonReader              *reader,
+                                                                const char              *debug_name,
+                                                                const OttieParserOption *options,
+                                                                gsize                    n_options,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_skip               (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+#define ottie_parser_option_skip_index ottie_parser_option_skip
+#define ottie_parser_option_skip_expression ottie_parser_option_skip
+gboolean                ottie_parser_option_boolean            (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_int                (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_double             (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_string             (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_3d                 (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_direction          (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_blend_mode         (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_line_cap           (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_line_join          (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_fill_rule          (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+gboolean                ottie_parser_option_transform          (JsonReader              *reader,
+                                                                gsize                    offset,
+                                                                gpointer                 data);
+
+G_END_DECLS
+
+#endif /* __OTTIE_PARSER_PRIVATE_H__ */
diff --git a/ottie/ottiepathshape.c b/ottie/ottiepathshape.c
new file mode 100644
index 0000000000..b8e9d04de2
--- /dev/null
+++ b/ottie/ottiepathshape.c
@@ -0,0 +1,105 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiepathshapeprivate.h"
+
+#include "ottiepathvalueprivate.h"
+#include "ottieparserprivate.h"
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+struct _OttiePathShape
+{
+  OttieShape parent;
+
+  double direction;
+  OttiePathValue path;
+};
+
+struct _OttiePathShapeClass
+{
+  OttieShapeClass parent_class;
+};
+
+G_DEFINE_TYPE (OttiePathShape, ottie_path_shape, OTTIE_TYPE_SHAPE)
+
+static void
+ottie_path_shape_render (OttieShape  *shape,
+                         OttieRender *render,
+                         double       timestamp)
+{
+  OttiePathShape *self = OTTIE_PATH_SHAPE (shape);
+
+  ottie_render_add_path (render,
+                         ottie_path_value_get (&self->path,
+                                               timestamp,
+                                               self->direction));
+}
+
+static void
+ottie_path_shape_dispose (GObject *object)
+{
+  OttiePathShape *self = OTTIE_PATH_SHAPE (object);
+
+  ottie_path_value_clear (&self->path);
+
+  G_OBJECT_CLASS (ottie_path_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_path_shape_class_init (OttiePathShapeClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_path_shape_render;
+
+  gobject_class->dispose = ottie_path_shape_dispose;
+}
+
+static void
+ottie_path_shape_init (OttiePathShape *self)
+{
+  ottie_path_value_init (&self->path);
+}
+
+OttieShape *
+ottie_path_shape_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "d", ottie_parser_option_double, G_STRUCT_OFFSET (OttiePathShape, direction) },
+    { "ks", ottie_path_value_parse, G_STRUCT_OFFSET (OttiePathShape, path) },
+  };
+  OttiePathShape *self;
+
+  self = g_object_new (OTTIE_TYPE_PATH_SHAPE, NULL);
+
+  if (!ottie_parser_parse_object (reader, "path shape", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_SHAPE (self);
+}
+
diff --git a/ottie/ottiepathshapeprivate.h b/ottie/ottiepathshapeprivate.h
new file mode 100644
index 0000000000..827e3cf3b1
--- /dev/null
+++ b/ottie/ottiepathshapeprivate.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_PATH_SHAPE_PRIVATE_H__
+#define __OTTIE_PATH_SHAPE_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_PATH_SHAPE         (ottie_path_shape_get_type ())
+#define OTTIE_PATH_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_PATH_SHAPE, 
OttiePathShape))
+#define OTTIE_PATH_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_PATH_SHAPE, 
OttiePathShapeClass))
+#define OTTIE_IS_PATH_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_PATH_SHAPE))
+#define OTTIE_IS_PATH_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_PATH_SHAPE))
+#define OTTIE_PATH_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_PATH_SHAPE, 
OttiePathShapeClass))
+
+typedef struct _OttiePathShape OttiePathShape;
+typedef struct _OttiePathShapeClass OttiePathShapeClass;
+
+GType                   ottie_path_shape_get_type               (void) G_GNUC_CONST;
+
+OttieShape *            ottie_path_shape_parse                  (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_PATH_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottiepathvalue.c b/ottie/ottiepathvalue.c
new file mode 100644
index 0000000000..7099ea26c7
--- /dev/null
+++ b/ottie/ottiepathvalue.c
@@ -0,0 +1,402 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiepathvalueprivate.h"
+
+#include "ottieparserprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+typedef struct _OttieContour OttieContour;
+typedef struct _OttieCurve OttieCurve;
+typedef struct _OttiePath OttiePath;
+
+struct _OttieCurve {
+  double point[2];
+  double in[2];
+  double out[2];
+};
+
+struct _OttieContour {
+  gboolean closed;
+  guint n_curves;
+  OttieCurve curves[0];
+};
+
+struct _OttiePath {
+  guint ref_count;
+  guint n_contours;
+  OttieContour *contours[];
+};
+
+static OttieContour *
+ottie_contour_renew (OttieContour *path,
+                     guint         n_curves)
+{
+  OttieContour *self;
+
+  self = g_realloc (path, sizeof (OttieContour) + n_curves * sizeof (OttieCurve));
+  self->n_curves = n_curves;
+
+  return self;
+}
+
+static OttieContour *
+ottie_contour_new (gboolean closed,
+                   guint    n_curves)
+{
+  OttieContour *self;
+
+  self = g_malloc0 (sizeof (OttieContour) + n_curves * sizeof (OttieCurve));
+  self->closed = closed;
+  self->n_curves = n_curves;
+
+  return self;
+}
+
+static void
+ottie_contour_free (OttieContour *path)
+{
+  g_free (path);
+}
+
+static gboolean
+ottie_parse_value_parse_numbers (JsonReader *reader,
+                                 gsize       offset,
+                                 gpointer    data)
+{
+  return ottie_parser_parse_array (reader, "number",
+                                   2, 2, NULL,
+                                   offset, sizeof (double),
+                                   ottie_parser_option_double, data);
+}
+
+#define MAKE_OPEN_CONTOUR (NULL)
+#define MAKE_CLOSED_CONTOUR GSIZE_TO_POINTER(1)
+static gboolean
+ottie_parse_value_parse_curve_array (JsonReader *reader,
+                                     gsize       offset,
+                                     gpointer    data)
+{
+  /* Attention: The offset value here is to the point in the curve, not 
+   * to the target */
+  OttieContour **target = (OttieContour **) data;
+  OttieContour *path = *target;
+  guint n_curves;
+
+  n_curves = json_reader_count_elements (reader);
+  if (path == MAKE_OPEN_CONTOUR)
+    path = ottie_contour_new (FALSE, n_curves);
+  else if (path == MAKE_CLOSED_CONTOUR)
+    path = ottie_contour_new (TRUE, n_curves);
+  else if (n_curves < path->n_curves)
+    path = ottie_contour_renew (path, n_curves);
+  else if (n_curves > path->n_curves)
+    n_curves = path->n_curves;
+
+  *target = path;
+
+  return ottie_parser_parse_array (reader, "path array",
+                                   0, path->n_curves, NULL,
+                                   offset, sizeof (OttieCurve),
+                                   ottie_parse_value_parse_numbers, &path->curves[0]);
+}
+
+static gboolean
+ottie_parser_value_parse_closed (JsonReader *reader,
+                                 gsize       offset,
+                                 gpointer    data)
+{
+  OttieContour **target = (OttieContour **) data;
+  gboolean b;
+
+  b = json_reader_get_boolean_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  if (*target == MAKE_OPEN_CONTOUR || *target == MAKE_CLOSED_CONTOUR)
+    {
+      if (b)
+        *target = MAKE_CLOSED_CONTOUR;
+      else
+        *target = MAKE_OPEN_CONTOUR;
+    }
+  else
+    (*target)->closed = b;
+
+  return TRUE;
+}
+
+static gboolean
+ottie_path_value_parse_contour (JsonReader *reader,
+                                gsize       offset,
+                                gpointer    data)
+{
+  OttieContour **target = (OttieContour **) ((guint8 *) data + offset);
+  OttieParserOption options[] = {
+    { "c", ottie_parser_value_parse_closed, 0 },
+    { "i", ottie_parse_value_parse_curve_array, G_STRUCT_OFFSET (OttieCurve, in) },
+    { "o", ottie_parse_value_parse_curve_array, G_STRUCT_OFFSET (OttieCurve, out) },
+    { "v", ottie_parse_value_parse_curve_array, G_STRUCT_OFFSET (OttieCurve, point) },
+  };
+
+  g_assert (*target == NULL);
+  *target = MAKE_CLOSED_CONTOUR;
+
+  if (!ottie_parser_parse_object (reader, "contour", options, G_N_ELEMENTS (options), target))
+    {
+      if (*target != MAKE_OPEN_CONTOUR && *target != MAKE_CLOSED_CONTOUR)
+        g_clear_pointer (target, ottie_contour_free);
+      *target = NULL;
+      return FALSE;
+    }
+
+  if (*target == MAKE_OPEN_CONTOUR)
+    *target = ottie_contour_new (FALSE, 0);
+  else if (*target == MAKE_CLOSED_CONTOUR)
+    *target = ottie_contour_new (TRUE, 0);
+
+  return TRUE;
+}
+
+static OttiePath *
+ottie_path_new (gsize n_contours)
+{
+  OttiePath *self;
+
+  self = g_malloc0 (sizeof (OttiePath) + sizeof (OttieContour *) * n_contours);
+  self->ref_count = 1;
+  self->n_contours = n_contours;
+
+  return self;
+}
+
+static OttiePath *
+ottie_path_ref (OttiePath *self)
+{
+  self->ref_count++;
+
+  return self;
+}
+
+static void
+ottie_path_unref (OttiePath *self)
+{
+  self->ref_count--;
+  if (self->ref_count > 0)
+    return;
+
+  for (guint i = 0; i < self->n_contours; i++)
+    {
+      g_clear_pointer (&self->contours[i], ottie_contour_free);
+    }
+
+  g_free (self);
+}
+
+static gboolean
+ottie_path_value_parse_one (JsonReader *reader,
+                            gsize       offset,
+                            gpointer    data)
+{
+  OttiePath **target = (OttiePath **) ((guint8 *) data + offset);
+  OttiePath *path;
+
+  if (json_reader_is_array (reader))
+    path = ottie_path_new (json_reader_count_elements (reader));
+  else
+    path = ottie_path_new (1);
+
+  if (!ottie_parser_parse_array (reader, "path",
+                                 path->n_contours, path->n_contours, NULL,
+                                 G_STRUCT_OFFSET (OttiePath, contours),
+                                 sizeof (OttieContour *),
+                                 ottie_path_value_parse_contour, path))
+    {
+      ottie_path_unref (path);
+      return FALSE;
+    }
+
+  g_clear_pointer (target, ottie_path_unref);
+  *target = path;
+
+  return TRUE;
+}
+
+static OttieContour *
+ottie_contour_interpolate (const OttieContour *start,
+                           const OttieContour *end,
+                           double              progress)
+{
+  OttieContour *self = ottie_contour_new (start->closed || end->closed,
+                                          MIN (start->n_curves, end->n_curves));
+
+  for (gsize i = 0; i < self->n_curves; i++)
+    {
+      self->curves[i].point[0] = start->curves[i].point[0] + progress * (end->curves[i].point[0] - 
start->curves[i].point[0]);
+      self->curves[i].point[1] = start->curves[i].point[1] + progress * (end->curves[i].point[1] - 
start->curves[i].point[1]);
+      self->curves[i].in[0] = start->curves[i].in[0] + progress * (end->curves[i].in[0] - 
start->curves[i].in[0]);
+      self->curves[i].in[1] = start->curves[i].in[1] + progress * (end->curves[i].in[1] - 
start->curves[i].in[1]);
+      self->curves[i].out[0] = start->curves[i].out[0] + progress * (end->curves[i].out[0] - 
start->curves[i].out[0]);
+      self->curves[i].out[1] = start->curves[i].out[1] + progress * (end->curves[i].out[1] - 
start->curves[i].out[1]);
+    }
+
+  return self;
+}
+
+static OttiePath *
+ottie_path_interpolate (const OttiePath *start,
+                        const OttiePath *end,
+                        double           progress)
+{
+  OttiePath *self = ottie_path_new (MIN (start->n_contours, end->n_contours));
+
+  for (gsize i = 0; i < self->n_contours; i++)
+    {
+      self->contours[i] = ottie_contour_interpolate (start->contours[i],
+                                                     end->contours[i],
+                                                     progress);
+    }
+
+  return self;
+}
+
+#define OTTIE_KEYFRAMES_NAME ottie_path_keyframes
+#define OTTIE_KEYFRAMES_TYPE_NAME OttieContourKeyframes
+#define OTTIE_KEYFRAMES_ELEMENT_TYPE OttiePath *
+#define OTTIE_KEYFRAMES_COPY_FUNC ottie_path_ref
+#define OTTIE_KEYFRAMES_FREE_FUNC ottie_path_unref
+#define OTTIE_KEYFRAMES_PARSE_FUNC ottie_path_value_parse_one
+#define OTTIE_KEYFRAMES_INTERPOLATE_FUNC ottie_path_interpolate
+#include "ottiekeyframesimpl.c"
+
+void
+ottie_path_value_init (OttiePathValue *self)
+{
+  self->is_static = TRUE;
+  self->static_value = NULL;
+}
+
+void
+ottie_path_value_clear (OttiePathValue *self)
+{
+  if (self->is_static)
+    g_clear_pointer (&self->static_value, ottie_path_unref);
+  else
+    g_clear_pointer (&self->keyframes, ottie_path_keyframes_free);
+}
+
+static GskPath *
+ottie_path_build (OttiePath *self,
+                  gboolean   reverse)
+{
+  GskPathBuilder *builder;
+
+  if (reverse)
+    g_warning ("FIXME: Make paths reversible");
+
+  builder = gsk_path_builder_new ();
+  for (gsize i = 0; i < self->n_contours; i++)
+    {
+      OttieContour *contour = self->contours[i];
+      if (contour->n_curves == 0)
+        continue;
+
+      gsk_path_builder_move_to (builder,
+                                contour->curves[0].point[0], contour->curves[0].point[1]);
+      for (guint j = 1; j < contour->n_curves; j++)
+        {
+          gsk_path_builder_curve_to (builder,
+                                     contour->curves[j-1].point[0] + contour->curves[j-1].out[0],
+                                     contour->curves[j-1].point[1] + contour->curves[j-1].out[1],
+                                     contour->curves[j].point[0] + contour->curves[j].in[0],
+                                     contour->curves[j].point[1] + contour->curves[j].in[1],
+                                     contour->curves[j].point[0],
+                                     contour->curves[j].point[1]);
+        }
+      if (contour->closed)
+        {
+          gsk_path_builder_curve_to (builder,
+                                     contour->curves[contour->n_curves-1].point[0] + 
contour->curves[contour->n_curves-1].out[0],
+                                     contour->curves[contour->n_curves-1].point[1] + 
contour->curves[contour->n_curves-1].out[1],
+                                     contour->curves[0].point[0] + contour->curves[0].in[0],
+                                     contour->curves[0].point[1] + contour->curves[0].in[1],
+                                     contour->curves[0].point[0],
+                                     contour->curves[0].point[1]);
+          gsk_path_builder_close (builder);
+        }
+    }
+
+  return gsk_path_builder_free_to_path (builder);
+}
+
+GskPath *
+ottie_path_value_get (OttiePathValue *self,
+                      double          timestamp,
+                      gboolean        reverse)
+{
+  if (self->is_static)
+    return ottie_path_build (self->static_value, reverse);
+  
+  return ottie_path_build (ottie_path_keyframes_get (self->keyframes, timestamp), reverse);
+}
+
+gboolean
+ottie_path_value_parse (JsonReader *reader,
+                        gsize       offset,
+                        gpointer    data)
+{
+  OttiePathValue *self = (OttiePathValue *) ((guint8 *) data + GPOINTER_TO_SIZE (offset));
+
+  if (json_reader_read_member (reader, "k"))
+    {
+      if (!json_reader_is_array (reader))
+        {
+          if (!ottie_path_value_parse_one (reader, 0, &self->static_value))
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+          self->is_static = TRUE;
+        }
+      else
+        {
+          self->keyframes = ottie_path_keyframes_parse (reader);
+          if (self->keyframes == NULL)
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+          self->is_static = FALSE;
+        }
+    }
+  else
+    {
+      ottie_parser_error_syntax (reader, "Property is not a path value");
+    }
+  json_reader_end_member (reader);
+
+  return TRUE;
+}
+
diff --git a/ottie/ottiepathvalueprivate.h b/ottie/ottiepathvalueprivate.h
new file mode 100644
index 0000000000..a7035cc783
--- /dev/null
+++ b/ottie/ottiepathvalueprivate.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_PATH_VALUE_PRIVATE_H__
+#define __OTTIE_PATH_VALUE_PRIVATE_H__
+
+#include <json-glib/json-glib.h>
+
+#include <gsk/gsk.h>
+
+G_BEGIN_DECLS
+
+typedef struct _OttiePathValue OttiePathValue;
+
+struct _OttiePathValue
+{
+  gboolean is_static;
+  union {
+    gpointer static_value;
+    gpointer keyframes;
+  };
+};
+
+void                      ottie_path_value_init                 (OttiePathValue         *self);
+void                      ottie_path_value_clear                (OttiePathValue         *self);
+
+GskPath *                 ottie_path_value_get                  (OttiePathValue         *self,
+                                                                 double                  timestamp,
+                                                                 gboolean                reverse);
+
+gboolean                  ottie_path_value_parse                (JsonReader             *reader,
+                                                                 gsize                   offset,
+                                                                 gpointer                data);
+
+G_END_DECLS
+
+#endif /* __OTTIE_PATH_VALUE_PRIVATE_H__ */
diff --git a/ottie/ottieplayer.c b/ottie/ottieplayer.c
new file mode 100644
index 0000000000..5685701d7d
--- /dev/null
+++ b/ottie/ottieplayer.c
@@ -0,0 +1,490 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottieplayer.h"
+
+#include "ottiecreation.h"
+#include "ottiepaintable.h"
+
+#include <glib/gi18n.h>
+
+struct _OttiePlayer
+{
+  GObject parent_instance;
+
+  GFile *file;
+
+  OttieCreation *creation;
+  OttiePaintable *paintable;
+  gint64 time_offset;
+  guint timer_cb;
+};
+
+struct _OttiePlayerClass
+{
+  GObjectClass parent_class;
+};
+
+enum {
+  PROP_0,
+  PROP_FILE,
+
+  N_PROPS,
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+static void
+ottie_player_paintable_snapshot (GdkPaintable *paintable,
+                                 GdkSnapshot  *snapshot,
+                                 double        width,
+                                 double        height)
+{
+  OttiePlayer *self = OTTIE_PLAYER (paintable);
+
+  gdk_paintable_snapshot (GDK_PAINTABLE (self->paintable), snapshot, width, height);
+}
+
+static int
+ottie_player_paintable_get_intrinsic_width (GdkPaintable *paintable)
+{
+  OttiePlayer *self = OTTIE_PLAYER (paintable);
+
+  return gdk_paintable_get_intrinsic_width (GDK_PAINTABLE (self->paintable));
+}
+
+static int
+ottie_player_paintable_get_intrinsic_height (GdkPaintable *paintable)
+{
+  OttiePlayer *self = OTTIE_PLAYER (paintable);
+
+  return gdk_paintable_get_intrinsic_height (GDK_PAINTABLE (self->paintable));
+}
+
+static void
+ottie_player_paintable_init (GdkPaintableInterface *iface)
+{
+  iface->snapshot = ottie_player_paintable_snapshot;
+  iface->get_intrinsic_width = ottie_player_paintable_get_intrinsic_width;
+  iface->get_intrinsic_height = ottie_player_paintable_get_intrinsic_height;
+}
+
+G_DEFINE_TYPE_EXTENDED (OttiePlayer, ottie_player, GTK_TYPE_MEDIA_STREAM, 0,
+                        G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
+                                               ottie_player_paintable_init))
+
+static gboolean
+ottie_player_timer_cb (gpointer data)
+{
+  OttiePlayer *self = OTTIE_PLAYER (data);
+  gint64 timestamp;
+
+  timestamp = g_get_monotonic_time () - self->time_offset;
+  if (timestamp > ottie_paintable_get_duration (self->paintable))
+    {
+      if (gtk_media_stream_get_loop (GTK_MEDIA_STREAM (self)))
+        {
+          timestamp %= ottie_paintable_get_duration (self->paintable);
+        }
+      else
+        {
+          timestamp = ottie_paintable_get_duration (self->paintable);
+          gtk_media_stream_stream_ended (GTK_MEDIA_STREAM (self));
+        }
+    }
+  ottie_paintable_set_timestamp (self->paintable, timestamp);
+  gtk_media_stream_update (GTK_MEDIA_STREAM (self), timestamp);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+ottie_player_play (GtkMediaStream *stream)
+{
+  OttiePlayer *self = OTTIE_PLAYER (stream);
+  double frame_rate;
+
+  frame_rate = ottie_creation_get_frame_rate (self->creation);
+  if (frame_rate <= 0)
+    return FALSE;
+
+  self->time_offset = g_get_monotonic_time () - ottie_paintable_get_timestamp (self->paintable);
+  self->timer_cb = g_timeout_add (1000 / frame_rate, ottie_player_timer_cb, self);
+
+  return TRUE;
+}
+
+static void
+ottie_player_pause (GtkMediaStream *stream)
+{
+  OttiePlayer *self = OTTIE_PLAYER (stream);
+
+  g_clear_handle_id (&self->timer_cb, g_source_remove);
+}
+
+static void
+ottie_player_seek (GtkMediaStream *stream,
+                   gint64          timestamp)
+{
+  OttiePlayer *self = OTTIE_PLAYER (stream);
+
+  if (!ottie_creation_is_prepared (self->creation))
+    gtk_media_stream_seek_failed (stream);
+
+  ottie_paintable_set_timestamp (self->paintable, timestamp);
+  self->time_offset = g_get_monotonic_time () - timestamp;
+
+  gtk_media_stream_seek_success (stream);
+  gtk_media_stream_update (stream, timestamp);
+}
+
+static void
+ottie_player_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+
+{
+  OttiePlayer *self = OTTIE_PLAYER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      ottie_player_set_file (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_player_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  OttiePlayer *self = OTTIE_PLAYER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      g_value_set_object (value, self->file);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ottie_player_dispose (GObject *object)
+{
+  OttiePlayer *self = OTTIE_PLAYER (object);
+
+  if (self->paintable)
+    {
+      g_signal_handlers_disconnect_by_func (self->paintable, gdk_paintable_invalidate_contents, self);
+      g_signal_handlers_disconnect_by_func (self->paintable, gdk_paintable_invalidate_size, self);
+      g_clear_object (&self->paintable);
+    }
+  g_clear_object (&self->creation);
+
+  G_OBJECT_CLASS (ottie_player_parent_class)->dispose (object);
+}
+
+static void
+ottie_player_class_init (OttiePlayerClass *klass)
+{
+  GtkMediaStreamClass *stream_class = GTK_MEDIA_STREAM_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  stream_class->play = ottie_player_play;
+  stream_class->pause = ottie_player_pause;
+  stream_class->seek = ottie_player_seek;
+
+  gobject_class->get_property = ottie_player_get_property;
+  gobject_class->set_property = ottie_player_set_property;
+  gobject_class->dispose = ottie_player_dispose;
+
+  /**
+   * OttiePlayer:file
+   *
+   * The played file or %NULL.
+   */
+  properties[PROP_FILE] =
+    g_param_spec_object ("file",
+                         _("File"),
+                         _("The played file"),
+                         G_TYPE_FILE,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, properties);
+}
+
+static void
+ottie_player_prepared_cb (OttieCreation *creation,
+                          GParamSpec    *pspec,
+                          OttiePlayer   *self)
+{
+  if (ottie_creation_is_prepared (creation))
+    gtk_media_stream_stream_prepared (GTK_MEDIA_STREAM (self),
+                                      FALSE,
+                                      TRUE,
+                                      TRUE,
+                                      ottie_paintable_get_duration (self->paintable));
+  else
+    gtk_media_stream_stream_unprepared (GTK_MEDIA_STREAM (self));
+
+  ottie_paintable_set_timestamp (self->paintable, 0);
+}
+
+static void
+ottie_player_init (OttiePlayer *self)
+{
+  self->creation = ottie_creation_new ();
+  g_signal_connect (self->creation, "notify::prepared", G_CALLBACK (ottie_player_prepared_cb), self);
+  self->paintable = ottie_paintable_new (self->creation);
+  g_signal_connect_swapped (self->paintable, "invalidate-contents", G_CALLBACK 
(gdk_paintable_invalidate_contents), self);
+  g_signal_connect_swapped (self->paintable, "invalidate-size", G_CALLBACK (gdk_paintable_invalidate_size), 
self);
+}
+
+/**
+ * ottie_player_new:
+ *
+ * Creates a new Ottie player.
+ *
+ * Returns: (transfer full): a new #OttiePlayer
+ **/
+OttiePlayer *
+ottie_player_new (void)
+{
+  return g_object_new (OTTIE_TYPE_PLAYER, NULL);
+}
+
+/**
+ * ottie_player_new_for_file:
+ * @file: (nullable): a #GFile
+ *
+ * Creates a new #OttiePlayer playing the given @file. If the file
+ * isn’t found or can’t be loaded, the resulting #OttiePlayer be empty.
+ *
+ * Returns: a new #OttiePlayer
+ **/
+OttiePlayer*
+ottie_player_new_for_file (GFile *file)
+{
+  g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL);
+
+  return g_object_new (OTTIE_TYPE_PLAYER,
+                       "file", file,
+                       NULL);
+}
+
+/**
+ * ottie_player_new_for_filename:
+ * @filename: (type filename) (nullable): a filename
+ *
+ * Creates a new #OttiePlayer displaying the file @filename.
+ *
+ * This is a utility function that calls ottie_player_new_for_file().
+ * See that function for details.
+ *
+ * Returns: a new #OttiePlayer
+ **/
+OttiePlayer*
+ottie_player_new_for_filename (const char *filename)
+{
+  OttiePlayer *result;
+  GFile *file;
+
+  if (filename)
+    file = g_file_new_for_path (filename);
+  else
+    file = NULL;
+
+  result = ottie_player_new_for_file (file);
+
+  if (file)
+    g_object_unref (file);
+
+  return result;
+}
+
+/**
+ * ottie_player_new_for_resource:
+ * @resource_path: (nullable): resource path to play back
+ *
+ * Creates a new #OttiePlayer displaying the file @resource_path.
+ *
+ * This is a utility function that calls ottie_player_new_for_file().
+ * See that function for details.
+ *
+ * Returns: a new #OttiePlayer
+ **/
+OttiePlayer *
+ottie_player_new_for_resource (const char *resource_path)
+{
+  OttiePlayer *result;
+  GFile *file;
+
+  if (resource_path)
+    {
+      char *uri, *escaped;
+
+      escaped = g_uri_escape_string (resource_path,
+                                     G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
+      uri = g_strconcat ("resource://", escaped, NULL);
+      g_free (escaped);
+
+      file = g_file_new_for_uri (uri);
+      g_free (uri);
+    }
+  else
+    {
+      file = NULL;
+    }
+
+  result = ottie_player_new_for_file (file);
+
+  if (file)
+    g_object_unref (file);
+
+  return result;
+}
+
+/**
+ * ottie_player_set_file:
+ * @self: a #OttiePlayer
+ * @file: (nullable): a %GFile or %NULL
+ *
+ * Makes @self load and display @file.
+ *
+ * See ottie_player_new_for_file() for details.
+ **/
+void
+ottie_player_set_file (OttiePlayer *self,
+                       GFile      *file)
+{
+  g_return_if_fail (OTTIE_IS_PLAYER (self));
+  g_return_if_fail (file == NULL || G_IS_FILE (file));
+
+  if (self->file == file)
+    return;
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  g_set_object (&self->file, file);
+  ottie_creation_load_file (self->creation, file);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FILE]);
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * ottie_player_get_file:
+ * @self: a #OttiePlayer
+ *
+ * Gets the #GFile currently displayed if @self is displaying a file.
+ * If @self is not displaying a file, then %NULL is returned.
+ *
+ * Returns: (nullable) (transfer none): The #GFile displayed by @self.
+ **/
+GFile *
+ottie_player_get_file (OttiePlayer *self)
+{
+  g_return_val_if_fail (OTTIE_IS_PLAYER (self), FALSE);
+
+  return self->file;
+}
+
+/**
+ * ottie_player_set_filename:
+ * @self: a #OttiePlayer
+ * @filename: (nullable): the filename to play
+ *
+ * Makes @self load and display the given @filename.
+ *
+ * This is a utility function that calls ottie_player_set_file().
+ **/
+void
+ottie_player_set_filename (OttiePlayer *self,
+                          const char *filename)
+{
+  GFile *file;
+
+  g_return_if_fail (OTTIE_IS_PLAYER (self));
+
+  if (filename)
+    file = g_file_new_for_path (filename);
+  else
+    file = NULL;
+
+  ottie_player_set_file (self, file);
+
+  if (file)
+    g_object_unref (file);
+}
+
+/**
+ * ottie_player_set_resource:
+ * @self: a #OttiePlayer
+ * @resource_path: (nullable): the resource to set
+ *
+ * Makes @self load and display the resource at the given
+ * @resource_path.
+ *
+ * This is a utility function that calls ottie_player_set_file(),
+ **/
+void
+ottie_player_set_resource (OttiePlayer *self,
+                          const char *resource_path)
+{
+  GFile *file;
+
+  g_return_if_fail (OTTIE_IS_PLAYER (self));
+
+  if (resource_path)
+    {
+      char *uri, *escaped;
+
+      escaped = g_uri_escape_string (resource_path,
+                                     G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
+      uri = g_strconcat ("resource://", escaped, NULL);
+      g_free (escaped);
+
+      file = g_file_new_for_uri (uri);
+      g_free (uri);
+    }
+  else
+    {
+      file = NULL;
+    }
+
+  ottie_player_set_file (self, file);
+
+  if (file)
+    g_object_unref (file);
+}
+
diff --git a/ottie/ottieplayer.h b/ottie/ottieplayer.h
new file mode 100644
index 0000000000..10891523d6
--- /dev/null
+++ b/ottie/ottieplayer.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_PLAYER_H__
+#define __OTTIE_PLAYER_H__
+
+#if !defined (__OTTIE_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <ottie/ottie.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_PLAYER (ottie_player_get_type ())
+
+GDK_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (OttiePlayer, ottie_player, OTTIE, PLAYER, GtkMediaStream)
+
+GDK_AVAILABLE_IN_ALL
+OttiePlayer *           ottie_player_new                        (void);
+GDK_AVAILABLE_IN_ALL
+OttiePlayer *           ottie_player_new_for_file               (GFile                  *file);
+GDK_AVAILABLE_IN_ALL
+OttiePlayer *           ottie_player_new_for_filename           (const char             *filename);
+GDK_AVAILABLE_IN_ALL
+OttiePlayer *           ottie_player_new_for_resource           (const char             *resource_path);
+
+GDK_AVAILABLE_IN_ALL
+void                    ottie_player_set_file                   (OttiePlayer            *self,
+                                                                 GFile                  *file);
+GDK_AVAILABLE_IN_ALL
+GFile *                 ottie_player_get_file                   (OttiePlayer            *self);
+GDK_AVAILABLE_IN_ALL
+void                    ottie_player_set_filename               (OttiePlayer            *self,
+                                                                 const char             *filename);
+GDK_AVAILABLE_IN_ALL
+void                    ottie_player_set_resource               (OttiePlayer            *self,
+                                                                 const char             *resource_path);
+
+G_END_DECLS
+
+#endif /* __OTTIE_PLAYER_H__ */
diff --git a/ottie/ottiepoint3dvalue.c b/ottie/ottiepoint3dvalue.c
new file mode 100644
index 0000000000..37ee149e90
--- /dev/null
+++ b/ottie/ottiepoint3dvalue.c
@@ -0,0 +1,154 @@
+/**
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiepoint3dvalueprivate.h"
+
+#include "ottieparserprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+static gboolean
+ottie_point3d_value_parse_value (JsonReader *reader,
+                                 gsize       offset,
+                                 gpointer    data)
+{
+  double d[3];
+  guint n_items;
+
+  if (!ottie_parser_parse_array (reader, "point",
+                                 2, 3, &n_items,
+                                 0, sizeof (double),
+                                 ottie_parser_option_double,
+                                 &d))
+    return FALSE;
+
+  if (n_items == 2)
+    d[2] = NAN; /* We do fixup below */
+
+  *(graphene_point3d_t *) ((guint8 *) data + offset) = GRAPHENE_POINT3D_INIT (d[0], d[1], d[2]);
+  return TRUE;
+}
+
+#define OTTIE_KEYFRAMES_NAME ottie_point_keyframes
+#define OTTIE_KEYFRAMES_TYPE_NAME OttiePointKeyframes
+#define OTTIE_KEYFRAMES_ELEMENT_TYPE graphene_point3d_t
+#define OTTIE_KEYFRAMES_BY_VALUE 1
+#define OTTIE_KEYFRAMES_DIMENSIONS 3
+#define OTTIE_KEYFRAMES_PARSE_FUNC ottie_point3d_value_parse_value
+#define OTTIE_KEYFRAMES_INTERPOLATE_FUNC graphene_point3d_interpolate
+#include "ottiekeyframesimpl.c"
+
+void
+ottie_point3d_value_init (OttiePoint3DValue        *self,
+                          const graphene_point3d_t *value)
+{
+  self->is_static = TRUE;
+  self->static_value = *value;
+}
+
+void
+ottie_point3d_value_clear (OttiePoint3DValue *self)
+{
+  if (!self->is_static)
+    g_clear_pointer (&self->keyframes, ottie_point_keyframes_free);
+}
+
+void
+ottie_point3d_value_get (OttiePoint3DValue  *self,
+                         double              timestamp,
+                         graphene_point3d_t *value)
+{
+  if (self->is_static)
+    {
+      *value = self->static_value;
+      return;
+    }
+  
+  ottie_point_keyframes_get (self->keyframes, timestamp, value);
+}
+
+gboolean
+ottie_point3d_value_parse (JsonReader *reader,
+                           float       default_value,
+                           gsize       offset,
+                           gpointer    data)
+{
+  OttiePoint3DValue *self = (OttiePoint3DValue *) ((guint8 *) data + offset);
+
+  if (json_reader_read_member (reader, "k"))
+    {
+      gboolean is_static;
+
+      if (!json_reader_is_array (reader))
+        {
+          ottie_parser_error_syntax (reader, "Point value needs an array for its value");
+          return FALSE;
+        }
+
+      if (!json_reader_read_element (reader, 0))
+        {
+          ottie_parser_emit_error (reader, json_reader_get_error (reader));
+          json_reader_end_element (reader);
+          return FALSE;
+        }
+
+      is_static = !json_reader_is_object (reader);
+      json_reader_end_element (reader);
+
+      if (is_static)
+        {
+          if (!ottie_point3d_value_parse_value (reader, 0, &self->static_value))
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+          if (isnan (self->static_value.z))
+            self->static_value.z = default_value;
+          self->is_static = TRUE;
+        }
+      else
+        {
+          OttiePointKeyframes *keyframes = ottie_point_keyframes_parse (reader);
+          if (keyframes == NULL)
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+          for (int i = 0; i < keyframes->n_items; i++)
+            {
+              if (isnan (keyframes->items[i].start_value.z))
+                keyframes->items[i].start_value.z = default_value;
+              if (isnan (keyframes->items[i].end_value.z))
+                keyframes->items[i].end_value.z = default_value;
+            }
+          self->is_static = FALSE;
+          self->keyframes = keyframes;
+        }
+    }
+  else
+    {
+      ottie_parser_error_syntax (reader, "Point value has no value");
+    }
+  json_reader_end_member (reader);
+
+  return TRUE;
+}
+
diff --git a/ottie/ottiepoint3dvalueprivate.h b/ottie/ottiepoint3dvalueprivate.h
new file mode 100644
index 0000000000..f735f06b95
--- /dev/null
+++ b/ottie/ottiepoint3dvalueprivate.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_POINT3D_VALUE_PRIVATE_H__
+#define __OTTIE_POINT3D_VALUE_PRIVATE_H__
+
+#include <json-glib/json-glib.h>
+#include <graphene.h>
+
+G_BEGIN_DECLS
+
+typedef struct _OttiePoint3DValue OttiePoint3DValue;
+
+struct _OttiePoint3DValue
+{
+  gboolean is_static;
+  union {
+    graphene_point3d_t static_value;
+    gpointer keyframes;
+  };
+};
+
+void                    ottie_point3d_value_init                (OttiePoint3DValue                *self,
+                                                                 const graphene_point3d_t       *value);
+void                    ottie_point3d_value_clear               (OttiePoint3DValue                *self);
+
+void                    ottie_point3d_value_get                 (OttiePoint3DValue                *self,
+                                                                 double                          timestamp,
+                                                                 graphene_point3d_t             *value);
+
+gboolean                ottie_point3d_value_parse               (JsonReader                     *reader,
+                                                                 float                           
default_value,
+                                                                 gsize                           offset,
+                                                                 gpointer                        data);
+
+G_END_DECLS
+
+#endif /* __OTTIE_POINT3D_VALUE_PRIVATE_H__ */
diff --git a/ottie/ottiepointvalue.c b/ottie/ottiepointvalue.c
new file mode 100644
index 0000000000..875c4c1a24
--- /dev/null
+++ b/ottie/ottiepointvalue.c
@@ -0,0 +1,140 @@
+/**
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiepointvalueprivate.h"
+
+#include "ottieparserprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+static gboolean
+ottie_point_value_parse_value (JsonReader *reader,
+                               gsize       offset,
+                               gpointer    data)
+{
+  double d[2];
+
+  if (!ottie_parser_parse_array (reader, "point",
+                                 2, 2, NULL,
+                                 0, sizeof (double),
+                                 ottie_parser_option_double,
+                                 &d))
+    return FALSE;
+
+  *(graphene_point_t *) ((guint8 *) data + offset) = GRAPHENE_POINT_INIT (d[0], d[1]);
+  return TRUE;
+}
+
+#define OTTIE_KEYFRAMES_NAME ottie_point_keyframes
+#define OTTIE_KEYFRAMES_TYPE_NAME OttiePointKeyframes
+#define OTTIE_KEYFRAMES_ELEMENT_TYPE graphene_point_t
+#define OTTIE_KEYFRAMES_BY_VALUE 1
+#define OTTIE_KEYFRAMES_DIMENSIONS 2
+#define OTTIE_KEYFRAMES_PARSE_FUNC ottie_point_value_parse_value
+#define OTTIE_KEYFRAMES_INTERPOLATE_FUNC graphene_point_interpolate
+#include "ottiekeyframesimpl.c"
+
+void
+ottie_point_value_init (OttiePointValue        *self,
+                        const graphene_point_t *value)
+{
+  self->is_static = TRUE;
+  self->static_value = *value;
+}
+
+void
+ottie_point_value_clear (OttiePointValue *self)
+{
+  if (!self->is_static)
+    g_clear_pointer (&self->keyframes, ottie_point_keyframes_free);
+}
+
+void
+ottie_point_value_get (OttiePointValue  *self,
+                       double            timestamp,
+                       graphene_point_t *value)
+{
+  if (self->is_static)
+    {
+      *value = self->static_value;
+      return;
+    }
+  
+  ottie_point_keyframes_get (self->keyframes, timestamp, value);
+}
+
+gboolean
+ottie_point_value_parse (JsonReader *reader,
+                         gsize       offset,
+                         gpointer    data)
+{
+  OttiePointValue *self = (OttiePointValue *) ((guint8 *) data + offset);
+
+  if (json_reader_read_member (reader, "k"))
+    {
+      gboolean is_static;
+
+      if (!json_reader_is_array (reader))
+        {
+          ottie_parser_error_syntax (reader, "Point value needs an array for its value");
+          return FALSE;
+        }
+
+      if (!json_reader_read_element (reader, 0))
+        {
+          ottie_parser_emit_error (reader, json_reader_get_error (reader));
+          json_reader_end_element (reader);
+          return FALSE;
+        }
+
+      is_static = !json_reader_is_object (reader);
+      json_reader_end_element (reader);
+
+      if (is_static)
+        {
+          if (!ottie_point_value_parse_value (reader, 0, &self->static_value))
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+          self->is_static = TRUE;
+        }
+      else
+        {
+          OttiePointKeyframes *keyframes = ottie_point_keyframes_parse (reader);
+          if (keyframes == NULL)
+            {
+              json_reader_end_member (reader);
+              return FALSE;
+            }
+          self->is_static = FALSE;
+          self->keyframes = keyframes;
+        }
+    }
+  else
+    {
+      ottie_parser_error_syntax (reader, "Point value has no value");
+    }
+  json_reader_end_member (reader);
+
+  return TRUE;
+}
+
diff --git a/ottie/ottiepointvalueprivate.h b/ottie/ottiepointvalueprivate.h
new file mode 100644
index 0000000000..635fa78d6d
--- /dev/null
+++ b/ottie/ottiepointvalueprivate.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_POINT_VALUE_PRIVATE_H__
+#define __OTTIE_POINT_VALUE_PRIVATE_H__
+
+#include <json-glib/json-glib.h>
+#include <graphene.h>
+
+G_BEGIN_DECLS
+
+typedef struct _OttiePointValue OttiePointValue;
+
+struct _OttiePointValue
+{
+  gboolean is_static;
+  union {
+    graphene_point_t static_value;
+    gpointer keyframes;
+  };
+};
+
+void                    ottie_point_value_init                  (OttiePointValue                *self,
+                                                                 const graphene_point_t         *value);
+void                    ottie_point_value_clear                 (OttiePointValue                *self);
+
+void                    ottie_point_value_get                   (OttiePointValue                *self,
+                                                                 double                          timestamp,
+                                                                 graphene_point_t               *value);
+
+gboolean                ottie_point_value_parse                 (JsonReader                     *reader,
+                                                                 gsize                           offset,
+                                                                 gpointer                        data);
+
+G_END_DECLS
+
+#endif /* __OTTIE_POINT_VALUE_PRIVATE_H__ */
diff --git a/ottie/ottierectshape.c b/ottie/ottierectshape.c
new file mode 100644
index 0000000000..8301ea775c
--- /dev/null
+++ b/ottie/ottierectshape.c
@@ -0,0 +1,219 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottierectshapeprivate.h"
+
+#include "ottiedoublevalueprivate.h"
+#include "ottiepointvalueprivate.h"
+#include "ottieparserprivate.h"
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+struct _OttieRectShape
+{
+  OttieShape parent;
+
+  OttieDirection direction;
+  OttiePointValue position;
+  OttiePointValue size;
+  OttieDoubleValue rounded;
+};
+
+struct _OttieRectShapeClass
+{
+  OttieShapeClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieRectShape, ottie_rect_shape, OTTIE_TYPE_SHAPE)
+
+static void
+ottie_rect_shape_render (OttieShape  *shape,
+                         OttieRender *render,
+                         double       timestamp)
+{
+  OttieRectShape *self = OTTIE_RECT_SHAPE (shape);
+  graphene_point_t p, s;
+  double r;
+  GskPathBuilder *builder;
+
+  ottie_point_value_get (&self->position, timestamp, &p);
+  ottie_point_value_get (&self->size, timestamp, &s);
+  r = ottie_double_value_get (&self->rounded, timestamp);
+  s.x /= 2;
+  s.y /= 2;
+  r = MIN (r, MIN (s.x, s.y));
+
+  builder = gsk_path_builder_new ();
+
+  switch (self->direction)
+  {
+    case OTTIE_DIRECTION_FORWARD:
+      if (r <= 0)
+        {
+          gsk_path_builder_move_to (builder, p.x + s.x, p.y - s.y);
+          gsk_path_builder_line_to (builder, p.x - s.x, p.y - s.y);
+          gsk_path_builder_line_to (builder, p.x - s.x, p.y + s.y);
+          gsk_path_builder_line_to (builder, p.x + s.x, p.y + s.y);
+          gsk_path_builder_line_to (builder, p.x + s.x, p.y - s.y);
+          gsk_path_builder_close (builder);
+        }
+      else
+        {
+          const float weight = sqrt(0.5f);
+
+          gsk_path_builder_move_to (builder,
+                                    p.x + s.x, p.y - s.y + r);
+          gsk_path_builder_conic_to (builder,
+                                     p.x + s.x, p.y - s.y,
+                                     p.x + s.x - r, p.y - s.y,
+                                     weight);
+          gsk_path_builder_line_to (builder,
+                                    p.x - s.x + r, p.y - s.y);
+          gsk_path_builder_conic_to (builder,
+                                     p.x - s.x, p.y - s.y,
+                                     p.x - s.x, p.y - s.y + r,
+                                     weight);
+          gsk_path_builder_line_to (builder,
+                                    p.x - s.x, p.y + s.y - r);
+          gsk_path_builder_conic_to (builder,
+                                     p.x - s.x, p.y + s.y,
+                                     p.x - s.x + r, p.y + s.y,
+                                     weight);
+          gsk_path_builder_line_to (builder,
+                                    p.x + s.x - r, p.y + s.y);
+          gsk_path_builder_conic_to (builder, 
+                                     p.x + s.x, p.y + s.y,
+                                     p.x + s.x, p.y + s.y - r,
+                                     weight);
+          gsk_path_builder_line_to (builder,
+                                    p.x + s.x, p.y - s.y + r);
+          gsk_path_builder_close (builder);
+        }
+      break;
+
+    case OTTIE_DIRECTION_BACKWARD:
+      if (r <= 0)
+        {
+          gsk_path_builder_move_to (builder, p.x + s.x, p.y - s.y);
+          gsk_path_builder_line_to (builder, p.x + s.x, p.y + s.y);
+          gsk_path_builder_line_to (builder, p.x - s.x, p.y + s.y);
+          gsk_path_builder_line_to (builder, p.x - s.x, p.y - s.y);
+          gsk_path_builder_line_to (builder, p.x + s.x, p.y - s.y);
+          gsk_path_builder_close (builder);
+        }
+      else
+        {
+          const float weight = sqrt(0.5f);
+
+          gsk_path_builder_move_to (builder,
+                                    p.x + s.x, p.y - s.y + r);
+          gsk_path_builder_line_to (builder,
+                                    p.x + s.x, p.y + s.y - r);
+          gsk_path_builder_conic_to (builder, 
+                                     p.x + s.x, p.y + s.y,
+                                     p.x + s.x - r, p.y + s.y,
+                                     weight);
+          gsk_path_builder_line_to (builder,
+                                    p.x - s.x + r, p.y + s.y);
+          gsk_path_builder_conic_to (builder,
+                                     p.x - s.x, p.y + s.y,
+                                     p.x - s.x, p.y + s.y - r,
+                                     weight);
+          gsk_path_builder_line_to (builder,
+                                    p.x - s.x, p.y - s.y + r);
+          gsk_path_builder_conic_to (builder,
+                                     p.x - s.x, p.y - s.y,
+                                     p.x - s.x + r, p.y - s.y,
+                                     weight);
+          gsk_path_builder_line_to (builder,
+                                    p.x + s.x - r, p.y - s.y);
+          gsk_path_builder_conic_to (builder,
+                                     p.x + s.x, p.y - s.y,
+                                     p.x + s.x, p.y - s.y + r,
+                                     weight);
+          gsk_path_builder_close (builder);
+        }
+      break;
+
+    default:
+      g_assert_not_reached();
+      break;
+  }
+
+  ottie_render_add_path (render,
+                         gsk_path_builder_free_to_path (builder));
+}
+
+static void
+ottie_rect_shape_dispose (GObject *object)
+{
+  OttieRectShape *self = OTTIE_RECT_SHAPE (object);
+
+  ottie_point_value_clear (&self->position);
+  ottie_point_value_clear (&self->size);
+  ottie_double_value_clear (&self->rounded);
+
+  G_OBJECT_CLASS (ottie_rect_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_rect_shape_class_init (OttieRectShapeClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_rect_shape_render;
+
+  gobject_class->dispose = ottie_rect_shape_dispose;
+}
+
+static void
+ottie_rect_shape_init (OttieRectShape *self)
+{
+  ottie_point_value_init (&self->position, &GRAPHENE_POINT_INIT (0, 0));
+  ottie_point_value_init (&self->size, &GRAPHENE_POINT_INIT (0, 0));
+  ottie_double_value_init (&self->rounded, 0);
+}
+
+OttieShape *
+ottie_rect_shape_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "d", ottie_parser_option_direction, G_STRUCT_OFFSET (OttieRectShape, direction) },
+    { "p", ottie_point_value_parse, G_STRUCT_OFFSET (OttieRectShape, position) },
+    { "s", ottie_point_value_parse, G_STRUCT_OFFSET (OttieRectShape, size) },
+    { "r", ottie_double_value_parse, G_STRUCT_OFFSET (OttieRectShape, rounded) },
+  };
+  OttieRectShape *self;
+
+  self = g_object_new (OTTIE_TYPE_RECT_SHAPE, NULL);
+
+  if (!ottie_parser_parse_object (reader, "rect shape", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_SHAPE (self);
+}
+
diff --git a/ottie/ottierectshapeprivate.h b/ottie/ottierectshapeprivate.h
new file mode 100644
index 0000000000..75875ccf6d
--- /dev/null
+++ b/ottie/ottierectshapeprivate.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_RECT_SHAPE_PRIVATE_H__
+#define __OTTIE_RECT_SHAPE_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_RECT_SHAPE         (ottie_rect_shape_get_type ())
+#define OTTIE_RECT_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_RECT_SHAPE, 
OttieRectShape))
+#define OTTIE_RECT_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_RECT_SHAPE, 
OttieRectShapeClass))
+#define OTTIE_IS_RECT_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_RECT_SHAPE))
+#define OTTIE_IS_RECT_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_RECT_SHAPE))
+#define OTTIE_RECT_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_RECT_SHAPE, 
OttieRectShapeClass))
+
+typedef struct _OttieRectShape OttieRectShape;
+typedef struct _OttieRectShapeClass OttieRectShapeClass;
+
+GType                   ottie_rect_shape_get_type               (void) G_GNUC_CONST;
+
+OttieShape *            ottie_rect_shape_parse                  (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_RECT_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottierender.c b/ottie/ottierender.c
new file mode 100644
index 0000000000..0fca70301b
--- /dev/null
+++ b/ottie/ottierender.c
@@ -0,0 +1,276 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottierenderprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+void
+ottie_render_init (OttieRender *self)
+{
+  memset (self, 0, sizeof (OttieRender));
+
+  ottie_render_paths_init (&self->paths);
+  ottie_render_nodes_init (&self->nodes);
+}
+
+void
+ottie_render_clear_path (OttieRender *self)
+{
+  ottie_render_paths_set_size (&self->paths, 0);
+  g_clear_pointer (&self->cached_path, gsk_path_unref);
+}
+
+void
+ottie_render_clear_nodes (OttieRender *self)
+{
+  ottie_render_nodes_set_size (&self->nodes, 0);
+}
+
+void
+ottie_render_clear (OttieRender *self)
+{
+  ottie_render_nodes_clear (&self->nodes);
+
+  ottie_render_paths_clear (&self->paths);
+  g_clear_pointer (&self->cached_path, gsk_path_unref);
+}
+
+void
+ottie_render_merge (OttieRender *self,
+                    OttieRender *source)
+{
+  /* prepend all the nodes from source */
+  ottie_render_nodes_splice (&self->nodes,
+                             0,
+                             0, FALSE,
+                             ottie_render_nodes_index (&source->nodes, 0),
+                             ottie_render_nodes_get_size (&source->nodes));
+  /* steal all the nodes from source because refcounting */
+  ottie_render_nodes_splice (&source->nodes,
+                             0,
+                             ottie_render_nodes_get_size (&source->nodes), TRUE,
+                             NULL, 0);
+
+  /* append all the paths from source */
+  ottie_render_paths_splice (&self->paths,
+                             ottie_render_paths_get_size (&self->paths),
+                             0, FALSE,
+                             ottie_render_paths_index (&source->paths, 0),
+                             ottie_render_paths_get_size (&source->paths));
+  /* steal all the paths from source because refcounting */
+  ottie_render_paths_splice (&source->paths,
+                             0,
+                             ottie_render_paths_get_size (&source->paths), TRUE,
+                             NULL, 0);
+
+  g_clear_pointer (&self->cached_path, gsk_path_unref);
+  g_clear_pointer (&source->cached_path, gsk_path_unref);
+}
+
+void
+ottie_render_add_path (OttieRender *self,
+                       GskPath     *path)
+{
+  g_clear_pointer (&self->cached_path, gsk_path_unref);
+
+  if (gsk_path_is_empty (path))
+    {
+      gsk_path_unref (path);
+      return;
+    }
+
+  ottie_render_paths_append (&self->paths, &(OttieRenderPath) { path, NULL });
+}
+
+typedef struct 
+{
+  GskPathBuilder *builder;
+  GskTransform *transform;
+} TransformForeach;
+
+static gboolean
+ottie_render_path_transform_foreach (GskPathOperation        op,
+                                     const graphene_point_t *pts,
+                                     gsize                   n_pts,
+                                     float                   weight,
+                                     gpointer                data)
+{
+  TransformForeach *tf = data;
+  graphene_point_t p[3];
+
+  switch (op)
+  {
+    case GSK_PATH_MOVE:
+      gsk_transform_transform_point (tf->transform, &pts[0], &p[0]);
+      gsk_path_builder_move_to (tf->builder, p[0].x, p[0].y);
+      break;
+
+    case GSK_PATH_CLOSE:
+      gsk_path_builder_close (tf->builder);
+      break;
+
+    case GSK_PATH_LINE:
+      gsk_transform_transform_point (tf->transform, &pts[1], &p[0]);
+      gsk_path_builder_line_to (tf->builder, p[0].x, p[0].y);
+      break;
+
+    case GSK_PATH_CURVE:
+      gsk_transform_transform_point (tf->transform, &pts[1], &p[0]);
+      gsk_transform_transform_point (tf->transform, &pts[2], &p[1]);
+      gsk_transform_transform_point (tf->transform, &pts[3], &p[2]);
+      gsk_path_builder_curve_to (tf->builder, p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y);
+      break;
+
+    case GSK_PATH_CONIC:
+      gsk_transform_transform_point (tf->transform, &pts[1], &p[0]);
+      gsk_transform_transform_point (tf->transform, &pts[2], &p[1]);
+      gsk_path_builder_conic_to (tf->builder, p[0].x, p[0].y, p[1].x, p[1].y, weight);
+      break;
+
+    default:
+      g_assert_not_reached ();
+      break;
+  }
+
+  return TRUE;
+}
+
+GskPath *
+ottie_render_get_path (OttieRender *self)
+{
+  GskPathBuilder *builder;
+
+  if (self->cached_path)
+    return self->cached_path;
+
+  builder = gsk_path_builder_new ();
+  for (gsize i = 0; i < ottie_render_paths_get_size (&self->paths); i++)
+    {
+      OttieRenderPath *path = ottie_render_paths_get (&self->paths, i);
+
+      switch (gsk_transform_get_category (path->transform))
+        {
+        case GSK_TRANSFORM_CATEGORY_IDENTITY:
+          gsk_path_builder_add_path (builder, path->path);
+          break;
+
+        case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE:
+        case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
+        case GSK_TRANSFORM_CATEGORY_2D:
+          {
+            TransformForeach tf = { builder, path->transform };
+            gsk_path_foreach (path->path, -1, ottie_render_path_transform_foreach, &tf);
+          }
+          break;
+
+        case GSK_TRANSFORM_CATEGORY_3D:
+        case GSK_TRANSFORM_CATEGORY_ANY:
+        case GSK_TRANSFORM_CATEGORY_UNKNOWN:
+          g_critical ("How did we get a 3D matrix?!");
+          gsk_path_builder_add_path (builder, path->path);
+          break;
+
+        default:
+          g_assert_not_reached();
+          break;
+        }
+    }
+  self->cached_path = gsk_path_builder_free_to_path (builder);
+
+  return self->cached_path;
+}
+
+gsize
+ottie_render_get_n_subpaths (OttieRender *self)
+{
+  return ottie_render_paths_get_size (&self->paths);
+}
+
+GskPath *
+ottie_render_get_subpath (OttieRender *self,
+                          gsize        i)
+{
+  OttieRenderPath *path = ottie_render_paths_get (&self->paths, i);
+
+  return path->path;
+}
+
+void
+ottie_render_replace_subpath (OttieRender *self,
+                              gsize        i,
+                              GskPath     *path)
+{
+  OttieRenderPath *rpath = ottie_render_paths_get (&self->paths, i);
+
+  gsk_path_unref (rpath->path);
+  rpath->path = path;
+
+  g_clear_pointer (&self->cached_path, gsk_path_unref);
+}
+
+void
+ottie_render_add_node (OttieRender   *self,
+                       GskRenderNode *node)
+{
+  ottie_render_nodes_splice (&self->nodes, 0, 0, FALSE, &node, 1);
+}
+
+GskRenderNode *
+ottie_render_get_node (OttieRender *self)
+{
+  if (ottie_render_nodes_get_size (&self->nodes) == 0)
+    return NULL;
+
+  if (ottie_render_nodes_get_size (&self->nodes) == 1)
+    return gsk_render_node_ref (ottie_render_nodes_get (&self->nodes, 0));
+
+  return gsk_container_node_new (ottie_render_nodes_index (&self->nodes, 0),
+                                 ottie_render_nodes_get_size (&self->nodes));
+}
+
+void
+ottie_render_transform (OttieRender  *self,
+                        GskTransform *transform)
+{
+  GskRenderNode *node;
+
+  if (gsk_transform_get_category (transform) == GSK_TRANSFORM_CATEGORY_IDENTITY)
+    return;
+
+  for (gsize i = 0; i < ottie_render_paths_get_size (&self->paths); i++)
+    {
+      OttieRenderPath *path = ottie_render_paths_get (&self->paths, i);
+
+      path->transform = gsk_transform_transform (path->transform, transform);
+    }
+
+  node = ottie_render_get_node (self);
+  if (node)
+    {
+      GskRenderNode *transform_node = gsk_transform_node_new (node, transform);
+
+      ottie_render_clear_nodes (self);
+      ottie_render_add_node (self, transform_node);
+
+      gsk_render_node_unref (node);
+    }
+}
diff --git a/ottie/ottierenderprivate.h b/ottie/ottierenderprivate.h
new file mode 100644
index 0000000000..8ee089c6f5
--- /dev/null
+++ b/ottie/ottierenderprivate.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_RENDER_PRIVATE_H__
+#define __OTTIE_RENDER_PRIVATE_H__
+
+#include <gsk/gsk.h>
+
+G_BEGIN_DECLS
+
+typedef struct _OttieRender OttieRender;
+typedef struct _OttieRenderPath OttieRenderPath;
+
+struct _OttieRenderPath
+{
+  GskPath *path;
+  GskTransform *transform;
+};
+
+static inline void
+ottie_render_path_clear (OttieRenderPath *path)
+{
+  gsk_path_unref (path->path);
+  gsk_transform_unref (path->transform);
+}
+
+#define GDK_ARRAY_ELEMENT_TYPE OttieRenderPath
+#define GDK_ARRAY_TYPE_NAME OttieRenderPaths
+#define GDK_ARRAY_NAME ottie_render_paths
+#define GDK_ARRAY_FREE_FUNC ottie_render_path_clear
+#define GDK_ARRAY_BY_VALUE 1
+#define GDK_ARRAY_PREALLOC 8
+#include "gdk/gdkarrayimpl.c"
+
+#define GDK_ARRAY_ELEMENT_TYPE GskRenderNode *
+#define GDK_ARRAY_TYPE_NAME OttieRenderNodes
+#define GDK_ARRAY_NAME ottie_render_nodes
+#define GDK_ARRAY_FREE_FUNC gsk_render_node_unref
+#define GDK_ARRAY_PREALLOC 8
+#include "gdk/gdkarrayimpl.c"
+
+struct _OttieRender
+{
+  OttieRenderPaths paths;
+  GskPath *cached_path;
+  OttieRenderNodes nodes;
+};
+
+void                    ottie_render_init                       (OttieRender            *self);
+void                    ottie_render_clear                      (OttieRender            *self);
+
+void                    ottie_render_merge                      (OttieRender            *self,
+                                                                 OttieRender            *source);
+
+void                    ottie_render_add_path                   (OttieRender            *self,
+                                                                 GskPath                *path);
+GskPath *               ottie_render_get_path                   (OttieRender            *self);
+void                    ottie_render_clear_path                 (OttieRender            *self);
+gsize                   ottie_render_get_n_subpaths             (OttieRender            *self);
+GskPath *               ottie_render_get_subpath                (OttieRender            *self,
+                                                                 gsize                   i);
+void                    ottie_render_replace_subpath            (OttieRender            *self,
+                                                                 gsize                   i,
+                                                                 GskPath                *path);
+
+void                    ottie_render_add_node                   (OttieRender            *self,
+                                                                 GskRenderNode          *node);
+GskRenderNode *         ottie_render_get_node                   (OttieRender            *self);
+void                    ottie_render_clear_nodes                (OttieRender            *self);
+
+void                    ottie_render_transform                  (OttieRender            *self,
+                                                                 GskTransform           *transform);
+G_END_DECLS
+
+#endif /* __OTTIE_RENDER_PRIVATE_H__ */
diff --git a/ottie/ottieshape.c b/ottie/ottieshape.c
new file mode 100644
index 0000000000..408b307ed2
--- /dev/null
+++ b/ottie/ottieshape.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+
+G_DEFINE_TYPE (OttieShape, ottie_shape, OTTIE_TYPE_OBJECT)
+
+static void
+ottie_shape_dispose (GObject *object)
+{
+  //OttieShape *self = OTTIE_SHAPE (object);
+
+  G_OBJECT_CLASS (ottie_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_shape_class_init (OttieShapeClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+
+  gobject_class->dispose = ottie_shape_dispose;
+}
+
+static void
+ottie_shape_init (OttieShape *self)
+{
+}
+
+void
+ottie_shape_render (OttieShape  *self,
+                    OttieRender *render,
+                    double       timestamp)
+{
+  OTTIE_SHAPE_GET_CLASS (self)->render (self, render, timestamp);
+}
+
diff --git a/ottie/ottieshapelayer.c b/ottie/ottieshapelayer.c
new file mode 100644
index 0000000000..2d76fb048d
--- /dev/null
+++ b/ottie/ottieshapelayer.c
@@ -0,0 +1,124 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottieshapelayerprivate.h"
+
+#include "ottiefillshapeprivate.h"
+#include "ottiegroupshapeprivate.h"
+#include "ottieparserprivate.h"
+#include "ottiepathshapeprivate.h"
+#include "ottieshapeprivate.h"
+#include "ottiestrokeshapeprivate.h"
+#include "ottietransformprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+struct _OttieShapeLayer
+{
+  OttieLayer parent;
+
+  OttieShape *shapes;
+};
+
+struct _OttieShapeLayerClass
+{
+  OttieLayerClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieShapeLayer, ottie_shape_layer, OTTIE_TYPE_LAYER)
+
+static void
+ottie_shape_layer_render (OttieLayer  *layer,
+                          OttieRender *render,
+                          double       timestamp)
+{
+  OttieShapeLayer *self = OTTIE_SHAPE_LAYER (layer);
+
+  ottie_shape_render (self->shapes,
+                      render,
+                      timestamp);
+}
+
+static void
+ottie_shape_layer_dispose (GObject *object)
+{
+  OttieShapeLayer *self = OTTIE_SHAPE_LAYER (object);
+
+  g_clear_object (&self->shapes);
+
+  G_OBJECT_CLASS (ottie_shape_layer_parent_class)->dispose (object);
+}
+
+static void
+ottie_shape_layer_class_init (OttieShapeLayerClass *klass)
+{
+  OttieLayerClass *layer_class = OTTIE_LAYER_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  layer_class->render = ottie_shape_layer_render;
+
+  gobject_class->dispose = ottie_shape_layer_dispose;
+}
+
+static void
+ottie_shape_layer_init (OttieShapeLayer *self)
+{
+  self->shapes = ottie_group_shape_new ();
+}
+
+static gboolean
+ottie_shape_layer_parse_shapes (JsonReader *reader,
+                                gsize       offset,
+                                gpointer    data)
+{
+  OttieShapeLayer *self = data;
+
+  return ottie_group_shape_parse_shapes (reader, 0, self->shapes);
+}
+
+OttieLayer *
+ottie_shape_layer_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_LAYER,
+    { "shapes", ottie_shape_layer_parse_shapes, 0 },
+  };
+  OttieShapeLayer *self;
+
+  self = g_object_new (OTTIE_TYPE_SHAPE_LAYER, NULL);
+
+  if (!ottie_parser_parse_object (reader, "shape layer", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_LAYER (self);
+}
+
+OttieShape *
+ottie_shape_layer_get_shape (OttieShapeLayer *self)
+{
+  g_return_val_if_fail (OTTIE_IS_SHAPE_LAYER (self), NULL);
+
+  return self->shapes;
+}
diff --git a/ottie/ottieshapelayerprivate.h b/ottie/ottieshapelayerprivate.h
new file mode 100644
index 0000000000..fb4707c849
--- /dev/null
+++ b/ottie/ottieshapelayerprivate.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_SHAPE_LAYER_PRIVATE_H__
+#define __OTTIE_SHAPE_LAYER_PRIVATE_H__
+
+#include "ottielayerprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_SHAPE_LAYER         (ottie_shape_layer_get_type ())
+#define OTTIE_SHAPE_LAYER(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_SHAPE_LAYER, 
OttieShapeLayer))
+#define OTTIE_SHAPE_LAYER_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_SHAPE_LAYER, 
OttieShapeLayerClass))
+#define OTTIE_IS_SHAPE_LAYER(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_SHAPE_LAYER))
+#define OTTIE_IS_SHAPE_LAYER_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_SHAPE_LAYER))
+#define OTTIE_SHAPE_LAYER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_SHAPE_LAYER, 
OttieShapeLayerClass))
+
+typedef struct _OttieShapeLayer OttieShapeLayer;
+typedef struct _OttieShapeLayerClass OttieShapeLayerClass;
+
+GType                   ottie_shape_layer_get_type              (void) G_GNUC_CONST;
+
+OttieShape *            ottie_shape_layer_get_shape             (OttieShapeLayer        *self);
+
+OttieLayer *            ottie_shape_layer_parse                 (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_SHAPE_LAYER_PRIVATE_H__ */
diff --git a/ottie/ottieshapeprivate.h b/ottie/ottieshapeprivate.h
new file mode 100644
index 0000000000..d58a6ee036
--- /dev/null
+++ b/ottie/ottieshapeprivate.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_SHAPE_PRIVATE_H__
+#define __OTTIE_SHAPE_PRIVATE_H__
+
+#include "ottie/ottieobjectprivate.h"
+#include "ottie/ottierenderprivate.h"
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_SHAPE         (ottie_shape_get_type ())
+#define OTTIE_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_SHAPE, OttieShape))
+#define OTTIE_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_SHAPE, OttieShapeClass))
+#define OTTIE_IS_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_SHAPE))
+#define OTTIE_IS_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_SHAPE))
+#define OTTIE_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_SHAPE, OttieShapeClass))
+
+typedef struct _OttieShape OttieShape;
+typedef struct _OttieShapeClass OttieShapeClass;
+
+struct _OttieShape
+{
+  OttieObject parent;
+
+  gboolean hidden;
+};
+
+struct _OttieShapeClass
+{
+  OttieObjectClass parent_class;
+
+  void                  (* render)                             (OttieShape              *self,
+                                                                OttieRender             *render,
+                                                                double                   timestamp);
+};
+
+GType                   ottie_shape_get_type                   (void) G_GNUC_CONST;
+
+void                    ottie_shape_render                     (OttieShape              *self,
+                                                                OttieRender             *render,
+                                                                double                   timestamp);
+
+
+#define OTTIE_PARSE_OPTIONS_SHAPE \
+    OTTIE_PARSE_OPTIONS_OBJECT, \
+    { "hd", ottie_parser_option_boolean, G_STRUCT_OFFSET (OttieShape, hidden) }, \
+    { "ix", ottie_parser_option_skip_index, 0 }, \
+    { "ty", ottie_parser_option_skip, 0 }
+
+G_END_DECLS
+
+#endif /* __OTTIE_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottiestrokeshape.c b/ottie/ottiestrokeshape.c
new file mode 100644
index 0000000000..38de557823
--- /dev/null
+++ b/ottie/ottiestrokeshape.c
@@ -0,0 +1,152 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottiestrokeshapeprivate.h"
+
+#include "ottiecolorvalueprivate.h"
+#include "ottiedoublevalueprivate.h"
+#include "ottieparserprivate.h"
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+struct _OttieStrokeShape
+{
+  OttieShape parent;
+
+  OttieDoubleValue opacity;
+  OttieColorValue color;
+  OttieDoubleValue line_width;
+  GskLineCap line_cap;
+  GskLineJoin line_join;
+  double miter_limit;
+  GskBlendMode blend_mode;
+};
+
+struct _OttieStrokeShapeClass
+{
+  OttieShapeClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieStrokeShape, ottie_stroke_shape, OTTIE_TYPE_SHAPE)
+
+static void
+ottie_stroke_shape_render (OttieShape  *shape,
+                           OttieRender *render,
+                           double       timestamp)
+{
+  OttieStrokeShape *self = OTTIE_STROKE_SHAPE (shape);
+  GskPath *path;
+  graphene_rect_t bounds;
+  GdkRGBA color;
+  GskStroke *stroke;
+  double opacity, line_width;
+  GskRenderNode *color_node;
+
+  line_width = ottie_double_value_get (&self->line_width, timestamp);
+  if (line_width <= 0)
+    return;
+
+  opacity = ottie_double_value_get (&self->opacity, timestamp);
+  opacity = CLAMP (opacity, 0, 100);
+  ottie_color_value_get (&self->color, timestamp, &color);
+  color.alpha = color.alpha * opacity / 100.f;
+  if (gdk_rgba_is_clear (&color))
+    return;
+
+  path = ottie_render_get_path (render);
+  if (gsk_path_is_empty (path))
+    return;
+
+  stroke = gsk_stroke_new (line_width);
+  gsk_stroke_set_line_cap (stroke, self->line_cap);
+  gsk_stroke_set_line_join (stroke, self->line_join);
+  gsk_stroke_set_miter_limit (stroke, self->miter_limit);
+
+  gsk_path_get_stroke_bounds (path, stroke, &bounds);
+  color_node = gsk_color_node_new (&color, &bounds);
+
+  ottie_render_add_node (render, gsk_stroke_node_new (color_node, path, stroke));
+
+  gsk_render_node_unref (color_node);
+  gsk_stroke_free (stroke);
+}
+
+static void
+ottie_stroke_shape_dispose (GObject *object)
+{
+  OttieStrokeShape *self = OTTIE_STROKE_SHAPE (object);
+
+  ottie_double_value_clear (&self->opacity);
+  ottie_color_value_clear (&self->color);
+  ottie_double_value_clear (&self->line_width);
+
+  G_OBJECT_CLASS (ottie_stroke_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_stroke_shape_class_init (OttieStrokeShapeClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_stroke_shape_render;
+
+  gobject_class->dispose = ottie_stroke_shape_dispose;
+}
+
+static void
+ottie_stroke_shape_init (OttieStrokeShape *self)
+{
+  ottie_double_value_init (&self->opacity, 100);
+  ottie_color_value_init (&self->color, &(GdkRGBA) { 0, 0, 0, 1 });
+  ottie_double_value_init (&self->line_width, 1);
+
+  self->miter_limit = 10;
+}
+
+OttieShape *
+ottie_stroke_shape_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "w", ottie_double_value_parse, G_STRUCT_OFFSET (OttieStrokeShape, line_width) },
+    { "o", ottie_double_value_parse, G_STRUCT_OFFSET (OttieStrokeShape, opacity) },
+    { "c", ottie_color_value_parse, G_STRUCT_OFFSET (OttieStrokeShape, color) },
+    { "lc", ottie_parser_option_line_cap, G_STRUCT_OFFSET (OttieStrokeShape, line_cap) },
+    { "lj", ottie_parser_option_line_join, G_STRUCT_OFFSET (OttieStrokeShape, line_join) },
+    { "ml", ottie_parser_option_double, G_STRUCT_OFFSET (OttieStrokeShape, miter_limit) },
+    { "bm", ottie_parser_option_blend_mode, G_STRUCT_OFFSET (OttieStrokeShape, blend_mode) },
+  };
+  OttieStrokeShape *self;
+
+  self = g_object_new (OTTIE_TYPE_STROKE_SHAPE, NULL);
+
+  if (!ottie_parser_parse_object (reader, "stroke shape", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_SHAPE (self);
+}
+
diff --git a/ottie/ottiestrokeshapeprivate.h b/ottie/ottiestrokeshapeprivate.h
new file mode 100644
index 0000000000..2160ec77cd
--- /dev/null
+++ b/ottie/ottiestrokeshapeprivate.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_STROKE_SHAPE_PRIVATE_H__
+#define __OTTIE_STROKE_SHAPE_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_STROKE_SHAPE         (ottie_stroke_shape_get_type ())
+#define OTTIE_STROKE_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_STROKE_SHAPE, 
OttieStrokeShape))
+#define OTTIE_STROKE_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_STROKE_SHAPE, 
OttieStrokeShapeClass))
+#define OTTIE_IS_STROKE_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_STROKE_SHAPE))
+#define OTTIE_IS_STROKE_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_STROKE_SHAPE))
+#define OTTIE_STROKE_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_STROKE_SHAPE, 
OttieStrokeShapeClass))
+
+typedef struct _OttieStrokeShape OttieStrokeShape;
+typedef struct _OttieStrokeShapeClass OttieStrokeShapeClass;
+
+GType                   ottie_stroke_shape_get_type             (void) G_GNUC_CONST;
+
+OttieShape *            ottie_stroke_shape_parse                (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_STROKE_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottietransform.c b/ottie/ottietransform.c
new file mode 100644
index 0000000000..cfd8c2aeb2
--- /dev/null
+++ b/ottie/ottietransform.c
@@ -0,0 +1,187 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottietransformprivate.h"
+
+#include "ottiedoublevalueprivate.h"
+#include "ottieparserprivate.h"
+#include "ottiepoint3dvalueprivate.h"
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+struct _OttieTransform
+{
+  OttieShape parent;
+
+  OttieDoubleValue opacity;
+  OttieDoubleValue rotation;
+  OttieDoubleValue skew;
+  OttieDoubleValue skew_angle;
+  OttiePoint3DValue anchor;
+  OttiePoint3DValue position;
+  OttiePoint3DValue scale;
+};
+
+struct _OttieTransformClass
+{
+  OttieShapeClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieTransform, ottie_transform, OTTIE_TYPE_SHAPE)
+
+static void
+ottie_transform_render (OttieShape  *shape,
+                        OttieRender *render,
+                        double       timestamp)
+{
+  OttieTransform *self = OTTIE_TRANSFORM (shape);
+  GskTransform *transform;
+
+  transform = ottie_transform_get_transform (self, timestamp);
+  ottie_render_transform (render, transform);
+  gsk_transform_unref (transform);
+}
+
+static void
+ottie_transform_dispose (GObject *object)
+{
+  OttieTransform *self = OTTIE_TRANSFORM (object);
+
+  ottie_double_value_clear (&self->opacity);
+  ottie_double_value_clear (&self->rotation);
+  ottie_double_value_clear (&self->skew);
+  ottie_double_value_clear (&self->skew_angle);
+  ottie_point3d_value_clear (&self->anchor);
+  ottie_point3d_value_clear (&self->position);
+  ottie_point3d_value_clear (&self->scale);
+
+  G_OBJECT_CLASS (ottie_transform_parent_class)->dispose (object);
+}
+
+static void
+ottie_transform_finalize (GObject *object)
+{
+  //OttieTransform *self = OTTIE_TRANSFORM (object);
+
+  G_OBJECT_CLASS (ottie_transform_parent_class)->finalize (object);
+}
+
+static void
+ottie_transform_class_init (OttieTransformClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_transform_render;
+
+  gobject_class->finalize = ottie_transform_finalize;
+  gobject_class->dispose = ottie_transform_dispose;
+}
+
+static void
+ottie_transform_init (OttieTransform *self)
+{
+  ottie_double_value_init (&self->opacity, 100);
+  ottie_double_value_init (&self->rotation, 0);
+  ottie_double_value_init (&self->skew, 0);
+  ottie_double_value_init (&self->skew_angle, 0);
+  ottie_point3d_value_init (&self->anchor, &GRAPHENE_POINT3D_INIT (0, 0, 0));
+  ottie_point3d_value_init (&self->position, &GRAPHENE_POINT3D_INIT (0, 0, 0));
+  ottie_point3d_value_init (&self->scale, &GRAPHENE_POINT3D_INIT (100, 100, 100));
+}
+
+static gboolean
+ottie_transform_value_parse_point (JsonReader *reader,
+                                   gsize       offset,
+                                   gpointer    data)
+{
+  return ottie_point3d_value_parse (reader, 0, offset, data);
+}
+
+static gboolean
+ottie_transform_value_parse_scale (JsonReader *reader,
+                                   gsize       offset,
+                                   gpointer    data)
+{
+  return ottie_point3d_value_parse (reader, 100, offset, data);
+}
+
+
+OttieShape *
+ottie_transform_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "o", ottie_double_value_parse, G_STRUCT_OFFSET (OttieTransform, opacity) },
+    { "r", ottie_double_value_parse, G_STRUCT_OFFSET (OttieTransform, rotation) },
+    { "a", ottie_transform_value_parse_point, G_STRUCT_OFFSET (OttieTransform, anchor) },
+    { "p", ottie_transform_value_parse_point, G_STRUCT_OFFSET (OttieTransform, position) },
+    { "s", ottie_transform_value_parse_scale, G_STRUCT_OFFSET (OttieTransform, scale) },
+    { "sk", ottie_double_value_parse, G_STRUCT_OFFSET (OttieTransform, skew) },
+    { "sa", ottie_double_value_parse, G_STRUCT_OFFSET (OttieTransform, skew_angle) },
+  };
+  OttieTransform *self;
+
+  self = g_object_new (OTTIE_TYPE_TRANSFORM, NULL);
+
+  if (!ottie_parser_parse_object (reader, "transform", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_SHAPE (self);
+}
+
+GskTransform *
+ottie_transform_get_transform (OttieTransform *self,
+                               double          timestamp)
+{
+  graphene_point3d_t anchor, position, scale;
+  GskTransform *transform;
+  double skew, skew_angle;
+
+  ottie_point3d_value_get (&self->anchor, timestamp, &anchor);
+  ottie_point3d_value_get (&self->position, timestamp, &position);
+  ottie_point3d_value_get (&self->scale, timestamp, &scale);
+
+  transform = NULL;
+  transform = gsk_transform_translate_3d (transform, &position);
+  transform = gsk_transform_rotate (transform, ottie_double_value_get (&self->rotation, timestamp));
+  skew = ottie_double_value_get (&self->skew, timestamp);
+  if (skew)
+    {
+      graphene_matrix_t matrix;
+      skew_angle = ottie_double_value_get (&self->skew_angle, timestamp);
+      transform = gsk_transform_rotate (transform, -skew_angle);
+      graphene_matrix_init_skew (&matrix, -skew / 180.0 * G_PI, 0);
+      transform = gsk_transform_matrix (transform, &matrix);
+      transform = gsk_transform_rotate (transform, skew_angle);
+    }
+  transform = gsk_transform_scale_3d (transform, scale.x / 100, scale.y / 100, scale.z / 100);
+  graphene_point3d_scale (&anchor, -1, &anchor);
+  transform = gsk_transform_translate_3d (transform, &anchor);
+
+  return transform;
+}
+
diff --git a/ottie/ottietransformprivate.h b/ottie/ottietransformprivate.h
new file mode 100644
index 0000000000..0cd80a3054
--- /dev/null
+++ b/ottie/ottietransformprivate.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_TRANSFORM_PRIVATE_H__
+#define __OTTIE_TRANSFORM_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_TRANSFORM         (ottie_transform_get_type ())
+#define OTTIE_TRANSFORM(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_TRANSFORM, OttieTransform))
+#define OTTIE_TRANSFORM_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_TRANSFORM, 
OttieTransformClass))
+#define OTTIE_IS_TRANSFORM(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_TRANSFORM))
+#define OTTIE_IS_TRANSFORM_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_TRANSFORM))
+#define OTTIE_TRANSFORM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_TRANSFORM, 
OttieTransformClass))
+
+typedef struct _OttieTransform OttieTransform;
+typedef struct _OttieTransformClass OttieTransformClass;
+
+GType                   ottie_transform_get_type              (void) G_GNUC_CONST;
+
+OttieShape *            ottie_transform_parse                 (JsonReader             *reader);
+
+GskTransform *          ottie_transform_get_transform         (OttieTransform         *self,
+                                                               double                  timestamp);
+
+G_END_DECLS
+
+#endif /* __OTTIE_TRANSFORM_PRIVATE_H__ */
diff --git a/ottie/ottietrimshape.c b/ottie/ottietrimshape.c
new file mode 100644
index 0000000000..7791e2a121
--- /dev/null
+++ b/ottie/ottietrimshape.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include "config.h"
+
+#include "ottietrimshapeprivate.h"
+
+#include "ottiedoublevalueprivate.h"
+#include "ottieparserprivate.h"
+#include "ottieshapeprivate.h"
+
+#include <glib/gi18n-lib.h>
+#include <gsk/gsk.h>
+
+/*
+ * OttieTrimMode:
+ * @OTTIE_TRIM_SIMULTANEOUSLY: Treat each contour as a custom path
+ * @OTTIE_TRIM_INDIVIDUALLY: Treat the path as one whole path
+ *
+ * Names taken from the spec / After Effects. Don't blame me.
+ */
+typedef enum
+{
+  OTTIE_TRIM_SIMULTANEOUSLY,
+  OTTIE_TRIM_INDIVIDUALLY,
+} OttieTrimMode;
+
+struct _OttieTrimShape
+{
+  OttieShape parent;
+
+  OttieTrimMode mode;
+  OttieDoubleValue start;
+  OttieDoubleValue end;
+  OttieDoubleValue offset;
+};
+
+struct _OttieTrimShapeClass
+{
+  OttieShapeClass parent_class;
+};
+
+G_DEFINE_TYPE (OttieTrimShape, ottie_trim_shape, OTTIE_TYPE_SHAPE)
+
+static void
+ottie_trim_shape_render (OttieShape  *shape,
+                         OttieRender *render,
+                         double       timestamp)
+{
+  OttieTrimShape *self = OTTIE_TRIM_SHAPE (shape);
+  GskPathMeasure *measure;
+  GskPath *path;
+  GskPathBuilder *builder;
+  double start, end, offset;
+
+  start = ottie_double_value_get (&self->start, timestamp);
+  start = CLAMP (start, 0, 100) / 100.f;
+  end = ottie_double_value_get (&self->end, timestamp);
+  end = CLAMP (end, 0, 100) / 100.f;
+  if (start != end)
+    {
+      if (start > end)
+        {
+          double swap = end;
+          end = start;
+          start = swap;
+        }
+      offset = ottie_double_value_get (&self->offset, timestamp) / 360.f;
+      start += offset;
+      start = start - floor (start);
+      end += offset;
+      end = end - floor (end);
+
+      switch (self->mode)
+      {
+        case OTTIE_TRIM_SIMULTANEOUSLY:
+          for (gsize i = 0; i < ottie_render_get_n_subpaths (render); i++)
+            {
+              builder = gsk_path_builder_new ();
+              path = ottie_render_get_subpath (render, i);
+              measure = gsk_path_measure_new (path);
+              gsk_path_measure_restrict_to_contour (measure, i);
+              gsk_path_builder_add_segment (builder,
+                                            measure,
+                                            start * gsk_path_measure_get_length (measure),
+                                            end * gsk_path_measure_get_length (measure));
+              gsk_path_measure_unref (measure);
+              ottie_render_replace_subpath (render, i, gsk_path_builder_free_to_path (builder));
+            }
+          break;
+
+        case OTTIE_TRIM_INDIVIDUALLY:
+          builder = gsk_path_builder_new ();
+          path = ottie_render_get_path (render);
+          measure = gsk_path_measure_new (path);
+          gsk_path_builder_add_segment (builder,
+                                        measure,
+                                        start * gsk_path_measure_get_length (measure),
+                                        end * gsk_path_measure_get_length (measure));
+          ottie_render_clear_path (render);
+          gsk_path_measure_unref (measure);
+          ottie_render_add_path (render, gsk_path_builder_free_to_path (builder));
+          break;
+
+        default:
+          g_assert_not_reached ();
+          break;
+      }
+    }
+  else
+    {
+      ottie_render_clear_path (render);
+    }
+}
+
+static void
+ottie_trim_shape_dispose (GObject *object)
+{
+  OttieTrimShape *self = OTTIE_TRIM_SHAPE (object);
+
+  ottie_double_value_clear (&self->start);
+  ottie_double_value_clear (&self->end);
+  ottie_double_value_clear (&self->offset);
+
+  G_OBJECT_CLASS (ottie_trim_shape_parent_class)->dispose (object);
+}
+
+static void
+ottie_trim_shape_class_init (OttieTrimShapeClass *klass)
+{
+  OttieShapeClass *shape_class = OTTIE_SHAPE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  shape_class->render = ottie_trim_shape_render;
+
+  gobject_class->dispose = ottie_trim_shape_dispose;
+}
+
+static void
+ottie_trim_shape_init (OttieTrimShape *self)
+{
+  ottie_double_value_init (&self->start, 0);
+  ottie_double_value_init (&self->end, 100);
+  ottie_double_value_init (&self->offset, 0);
+}
+
+static gboolean
+ottie_trim_mode_parse (JsonReader *reader,
+                       gsize       offset,
+                       gpointer    data)
+{
+  OttieTrimMode trim_mode;
+  gint64 i;
+
+  i = json_reader_get_int_value (reader);
+  if (json_reader_get_error (reader))
+    {
+      ottie_parser_emit_error (reader, json_reader_get_error (reader));
+      return FALSE;
+    }
+  
+  switch (i)
+  {
+    case 1:
+      trim_mode = OTTIE_TRIM_SIMULTANEOUSLY;
+      break;
+
+    case 2:
+      trim_mode = OTTIE_TRIM_INDIVIDUALLY;
+      break;
+
+    default:
+      ottie_parser_error_value (reader, "%"G_GINT64_FORMAT" is not a known trim mode", i);
+      return FALSE;
+  }
+
+  *(OttieTrimMode *) ((guint8 *) data + offset) = trim_mode;
+
+  return TRUE;
+}
+
+OttieShape *
+ottie_trim_shape_parse (JsonReader *reader)
+{
+  OttieParserOption options[] = {
+    OTTIE_PARSE_OPTIONS_SHAPE,
+    { "s", ottie_double_value_parse, G_STRUCT_OFFSET (OttieTrimShape, start) },
+    { "e", ottie_double_value_parse, G_STRUCT_OFFSET (OttieTrimShape, end) },
+    { "o", ottie_double_value_parse, G_STRUCT_OFFSET (OttieTrimShape, offset) },
+    { "m", ottie_trim_mode_parse, G_STRUCT_OFFSET (OttieTrimShape, mode) },
+  };
+  OttieTrimShape *self;
+
+  self = g_object_new (OTTIE_TYPE_TRIM_SHAPE, NULL);
+
+  if (!ottie_parser_parse_object (reader, "trim shape", options, G_N_ELEMENTS (options), self))
+    {
+      g_object_unref (self);
+      return NULL;
+    }
+
+  return OTTIE_SHAPE (self);
+}
+
diff --git a/ottie/ottietrimshapeprivate.h b/ottie/ottietrimshapeprivate.h
new file mode 100644
index 0000000000..cd41479bbc
--- /dev/null
+++ b/ottie/ottietrimshapeprivate.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __OTTIE_TRIM_SHAPE_PRIVATE_H__
+#define __OTTIE_TRIM_SHAPE_PRIVATE_H__
+
+#include "ottieshapeprivate.h"
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define OTTIE_TYPE_TRIM_SHAPE         (ottie_trim_shape_get_type ())
+#define OTTIE_TRIM_SHAPE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), OTTIE_TYPE_TRIM_SHAPE, 
OttieTrimShape))
+#define OTTIE_TRIM_SHAPE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), OTTIE_TYPE_TRIM_SHAPE, 
OttieTrimShapeClass))
+#define OTTIE_IS_TRIM_SHAPE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), OTTIE_TYPE_TRIM_SHAPE))
+#define OTTIE_IS_TRIM_SHAPE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), OTTIE_TYPE_TRIM_SHAPE))
+#define OTTIE_TRIM_SHAPE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), OTTIE_TYPE_TRIM_SHAPE, 
OttieTrimShapeClass))
+
+typedef struct _OttieTrimShape OttieTrimShape;
+typedef struct _OttieTrimShapeClass OttieTrimShapeClass;
+
+GType                   ottie_trim_shape_get_type               (void) G_GNUC_CONST;
+
+OttieShape *            ottie_trim_shape_parse                  (JsonReader             *reader);
+
+G_END_DECLS
+
+#endif /* __OTTIE_TRIM_SHAPE_PRIVATE_H__ */
diff --git a/ottie/ottievalueimpl.c b/ottie/ottievalueimpl.c
new file mode 100644
index 0000000000..12bcede664
--- /dev/null
+++ b/ottie/ottievalueimpl.c
@@ -0,0 +1,133 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte gnome org>
+ */
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#ifndef OTTIE_VALUE_TYPE_NAME
+#define OTTIE_VALUE_TYPE_NAME OttieValue
+#endif
+
+#ifndef OTTIE_VALUE_NAME
+#define OTTIE_VALUE_NAME ottie_value
+#endif
+
+#ifndef OTTIE_VALUE_ELEMENT_TYPE
+#define OTTIE_VALUE_ELEMENT_TYPE gpointer
+#endif
+
+/* make this readable */
+#define _T_ OTTIE_VALUE_ELEMENT_TYPE
+#define OttieValue OTTIE_VALUE_TYPE_NAME
+#define ottie_value_paste_more(OTTIE_VALUE_NAME, func_name) OTTIE_VALUE_NAME ## _ ## func_name
+#define ottie_value_paste(OTTIE_VALUE_NAME, func_name) ottie_value_paste_more (OTTIE_VALUE_NAME, func_name)
+#define ottie_value(func_name) ottie_value_paste (OTTIE_VALUE_NAME, func_name)
+
+typedef struct OttieValue OttieValue;
+
+struct OttieValue
+{
+  enum {
+    STATIC,
+    KEYFRAMES
+  } type;
+  union {
+    _T_ static_value;
+    struct {
+      _T_ *values;
+      gsize n_frames;
+    } keyframes;
+};
+
+void
+ottie_value(init) (OttieValue *self)
+{
+  memset (self, 0, sizeof (OttieValue));
+}
+
+static inline void
+ottie_value(free_item) (_T_ *item)
+{
+#ifdef OTTIE_VALUE_FREE_FUNC
+#ifdef OTTIE_VALUE_BY_VALUE
+    OTTIE_VALUE_FREE_FUNC (item);
+#else
+    OTTIE_VALUE_FREE_FUNC (*item);
+#endif
+#endif
+}
+
+void
+ottie_value(clear) (OttieValue *self)
+{
+#ifdef OTTIE_VALUE_FREE_FUNC
+  gsize i;
+
+  if (self->type == STATIC)
+    {
+      ottie_value(free_item) (&self->static_value);
+    }
+  else
+    {
+      for (i = 0; i < self->n_values, i++)
+        ottie_value(free_item) (&self->values[i]);
+    }
+}
+
+#ifdef OTTIE_VALUE_BY_VALUE
+_T_ *
+#else
+_T_
+#endif
+ottie_value(get) (const OttieValue *self,
+                  double            progress)
+{
+  _T_ * result;
+
+  if (self->type == STATIC)
+    {
+      result = &self->static_value;
+    }
+  else
+    {
+      result = &self->values[progress * self->n_values];
+    }
+
+#ifdef OTTIE_VALUE_BY_VALUE
+  return result;
+#else
+  return *result;
+#endif
+}
+
+#ifndef OTTIE_VALUE_NO_UNDEF
+
+#undef _T_
+#undef OttieValue
+#undef ottie_value_paste_more
+#undef ottie_value_paste
+#undef ottie_value
+
+#undef OTTIE_VALUE_BY_VALUE
+#undef OTTIE_VALUE_ELEMENT_TYPE
+#undef OTTIE_VALUE_FREE_FUNC
+#undef OTTIE_VALUE_NAME
+#undef OTTIE_VALUE_TYPE_NAME
+#endif
diff --git a/tests/meson.build b/tests/meson.build
index d837b5af6d..94be834b35 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -2,18 +2,18 @@ gtk_tests = [
   # testname, optional extra sources
   ['input'],
   ['testpopup'],
+  ['animated-resizing', ['frame-stats.c', 'variable.c']],
+  ['animated-revealing', ['frame-stats.c', 'variable.c']],
+  ['blur-performance', ['../gsk/gskcairoblur.c']],
+  ['motion-compression'],
+  ['overlayscroll'],
   ['testupload'],
   ['testtransform'],
   ['testdropdown'],
   ['rendernode'],
   ['rendernode-create-tests'],
-  ['overlayscroll'],
   ['syncscroll'],
-  ['animated-resizing', ['frame-stats.c', 'variable.c']],
-  ['animated-revealing', ['frame-stats.c', 'variable.c']],
-  ['motion-compression'],
   ['scrolling-performance', ['frame-stats.c', 'variable.c']],
-  ['blur-performance', ['../gsk/gskcairoblur.c']],
   ['simple'],
   ['video-timer', ['variable.c']],
   ['testaccel'],


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]