[gimp/wip/animation: 118/182] plug-ins: add a start of implementation for camera keyframing.



commit d104c519594fdb56c92ef0011773df869287b400
Author: Jehan <jehan girinstud io>
Date:   Mon Jan 9 01:46:06 2017 +0100

    plug-ins: add a start of implementation for camera keyframing.
    
    It is barely usable at this state since it will recompute too much at
    any update. It also miss a lot of the UI to set or delete a keyframe,
    and navigate through the animation positions.
    It works only for a full frame for the time being (there should also be
    camera works possible on independant levels) and only provides offset
    (i.e. tilting and panning). Zoom or rotation features will be necessary,
    as well as effects (basically animating GEGL operations).
    Finally data persistence will need to be implemented in the
    serialization of the animation.
    Yet that's a start for further development in progress.

 plug-ins/animation-play/Makefile.am                |    8 +-
 plug-ins/animation-play/core/animation-camera.c    |  406 ++++++++++++++++++++
 plug-ins/animation-play/core/animation-camera.h    |   72 ++++
 .../animation-play/core/animation-celanimation.c   |  100 ++++-
 .../animation-play/core/animation-celanimation.h   |    2 +
 plug-ins/animation-play/widgets/animation-dialog.c |   36 ++-
 .../widgets/animation-keyframe-view.c              |  236 ++++++++++++
 .../widgets/animation-keyframe-view.h              |   56 +++
 plug-ins/animation-play/widgets/animation-xsheet.c |  200 +++++++++-
 plug-ins/animation-play/widgets/animation-xsheet.h |    3 +-
 10 files changed, 1069 insertions(+), 50 deletions(-)
---
diff --git a/plug-ins/animation-play/Makefile.am b/plug-ins/animation-play/Makefile.am
index 549e05e..b4f88d4 100644
--- a/plug-ins/animation-play/Makefile.am
+++ b/plug-ins/animation-play/Makefile.am
@@ -47,18 +47,22 @@ animation_play_SOURCES = \
        core/animation.c                                \
        core/animation-animatic.h               \
        core/animation-animatic.c               \
+       core/animation-camera.h                 \
+       core/animation-camera.c                 \
        core/animation-celanimation.h   \
        core/animation-celanimation.c   \
        core/animation-playback.h               \
        core/animation-playback.c               \
        widgets/animation-dialog.h              \
        widgets/animation-dialog.c              \
+       widgets/animation-keyframe-view.h       \
+       widgets/animation-keyframe-view.c       \
+       widgets/animation-layer-view.h  \
+       widgets/animation-layer-view.c  \
        widgets/animation-menus.h               \
        widgets/animation-menus.c               \
        widgets/animation-storyboard.h  \
        widgets/animation-storyboard.c  \
-       widgets/animation-layer-view.h  \
-       widgets/animation-layer-view.c  \
        widgets/animation-xsheet.h              \
        widgets/animation-xsheet.c              \
        animation-utils.h                               \
diff --git a/plug-ins/animation-play/core/animation-camera.c b/plug-ins/animation-play/core/animation-camera.c
new file mode 100644
index 0000000..8376158
--- /dev/null
+++ b/plug-ins/animation-play/core/animation-camera.c
@@ -0,0 +1,406 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation-camera.c
+ * Copyright (C) 2016-2017 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <libgimp/gimp.h>
+#include "libgimp/stdplugins-intl.h"
+
+#include "animation.h"
+
+#include "animation-camera.h"
+
+enum
+{
+  PROP_0,
+  PROP_ANIMATION,
+};
+
+enum
+{
+  OFFSETS_CHANGED,
+  KEYFRAME_SET,
+  KEYFRAME_DELETED,
+  LAST_SIGNAL
+};
+
+typedef struct
+{
+  gint x;
+  gint y;
+}
+Offset;
+
+struct _AnimationCameraPrivate
+{
+  Animation *animation;
+
+  /* Panning and tilting. */
+  GList     *offsets;
+};
+
+static void   animation_camera_finalize             (GObject         *object);
+static void   animation_camera_set_property         (GObject         *object,
+                                                     guint            property_id,
+                                                     const GValue    *value,
+                                                     GParamSpec      *pspec);
+static void   animation_camera_get_property         (GObject         *object,
+                                                     guint            property_id,
+                                                     GValue          *value,
+                                                     GParamSpec      *pspec);
+
+static void   animation_camera_emit_offsets_changed (AnimationCamera *camera,
+                                                     gint             position);
+
+G_DEFINE_TYPE (AnimationCamera, animation_camera, G_TYPE_OBJECT)
+
+#define parent_class animation_camera_parent_class
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static void
+animation_camera_class_init (AnimationCameraClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize     = animation_camera_finalize;
+  object_class->get_property = animation_camera_get_property;
+  object_class->set_property = animation_camera_set_property;
+
+  /**
+   * AnimationCamera::offsets-changed:
+   * @camera: the #AnimationCamera.
+   * @position:
+   * @duration:
+   *
+   * The ::offsets-changed signal will be emitted when camera offsets
+   * were updated between [@position; @position + @duration[.
+   */
+  signals[OFFSETS_CHANGED] =
+    g_signal_new ("offsets-changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (AnimationCameraClass, offsets_changed),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_INT,
+                  G_TYPE_INT);
+
+  /**
+   * AnimationCamera::keyframe-set:
+   * @camera: the #AnimationCamera.
+   * @position:
+   *
+   * The ::keyframe-set signal will be emitted when a keyframe is
+   * created or modified at @position.
+   */
+  signals[KEYFRAME_SET] =
+    g_signal_new ("keyframe-set",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (AnimationCameraClass, offsets_changed),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_INT);
+
+  /**
+   * AnimationCamera::keyframe-deleted:
+   * @camera: the #AnimationCamera.
+   * @position:
+   *
+   * The ::keyframe-set signal will be emitted when a keyframe is
+   * deleted at @position.
+   */
+  signals[KEYFRAME_DELETED] =
+    g_signal_new ("keyframe-deleted",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (AnimationCameraClass, offsets_changed),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_INT);
+
+  g_object_class_install_property (object_class, PROP_ANIMATION,
+                                   g_param_spec_object ("animation",
+                                                        NULL, NULL,
+                                                        ANIMATION_TYPE_ANIMATION,
+                                                        G_PARAM_READWRITE));
+
+  g_type_class_add_private (klass, sizeof (AnimationCameraPrivate));
+}
+
+static void
+animation_camera_init (AnimationCamera *view)
+{
+  view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
+                                            ANIMATION_TYPE_CAMERA,
+                                            AnimationCameraPrivate);
+}
+
+/************ Public Functions ****************/
+
+/**
+ * animation_camera_new:
+ *
+ * Creates a new camera.
+ */
+AnimationCamera *
+animation_camera_new (Animation *animation)
+{
+  AnimationCamera *camera;
+
+  camera = g_object_new (ANIMATION_TYPE_CAMERA,
+                         "animation", animation,
+                         NULL);
+
+  return camera;
+}
+
+void
+animation_camera_set_keyframe (AnimationCamera *camera,
+                               gint             position,
+                               gint             x,
+                               gint             y)
+{
+  Offset *offset;
+
+  g_return_if_fail (position >= 0 &&
+                    position < animation_get_duration (camera->priv->animation));
+
+  offset = g_list_nth_data (camera->priv->offsets, position);
+
+  if (! offset)
+    {
+      gint length = g_list_length (camera->priv->offsets);
+      gint i;
+
+      for (i = length; i < position; i++)
+        {
+          camera->priv->offsets = g_list_append (camera->priv->offsets, NULL);
+        }
+      offset = g_new (Offset, 1);
+      camera->priv->offsets = g_list_append (camera->priv->offsets, offset);
+    }
+
+  offset->x = x;
+  offset->y = y;
+
+  g_signal_emit (camera, signals[KEYFRAME_SET], 0, position);
+  animation_camera_emit_offsets_changed (camera, position);
+}
+
+void
+animation_camera_delete_keyframe (AnimationCamera *camera,
+                                  gint             position)
+{
+  GList *offset;
+
+  g_return_if_fail (position >= 0 &&
+                    position < animation_get_duration (camera->priv->animation));
+
+  offset = g_list_nth (camera->priv->offsets, position);
+
+  if (offset && offset->data)
+    {
+      g_free (offset->data);
+      offset->data = NULL;
+
+      g_signal_emit (camera, signals[KEYFRAME_DELETED], 0, position);
+      animation_camera_emit_offsets_changed (camera, position);
+    }
+}
+
+void
+animation_camera_get (AnimationCamera *camera,
+                      gint             position,
+                      gint            *x_offset,
+                      gint            *y_offset)
+{
+  Offset *keyframe;
+
+  g_return_if_fail (position >= 0 &&
+                    position < animation_get_duration (camera->priv->animation));
+
+  keyframe = g_list_nth_data (camera->priv->offsets, position);
+  if (keyframe)
+    {
+      /* There is a keyframe to this exact position. Use its values. */
+      *x_offset = keyframe->x;
+      *y_offset = keyframe->y;
+    }
+  else
+    {
+      GList  *iter;
+      Offset *prev_keyframe = NULL;
+      Offset *next_keyframe = NULL;
+      gint    prev_keyframe_pos;
+      gint    next_keyframe_pos;
+      gint    i;
+
+      /* This position is not a keyframe. */
+      if (position > 0)
+        {
+          i = MIN (position - 1, g_list_length (camera->priv->offsets) - 1);
+          iter = g_list_nth (camera->priv->offsets, i);
+          for (; iter && ! iter->data; iter = iter->prev, i--)
+            ;
+          if (iter && iter->data)
+            {
+              prev_keyframe_pos = i;
+              prev_keyframe = iter->data;
+            }
+        }
+      if (position < animation_get_duration (camera->priv->animation) - 1)
+        {
+          i = position + 1;
+          iter = g_list_nth (camera->priv->offsets, i);
+          for (; iter && ! iter->data; iter = iter->next, i++)
+            ;
+          if (iter && iter->data)
+            {
+              next_keyframe_pos = i;
+              next_keyframe = iter->data;
+            }
+        }
+
+      if (prev_keyframe == NULL && next_keyframe == NULL)
+        {
+          *x_offset = *y_offset = 0;
+        }
+      else if (prev_keyframe == NULL)
+        {
+          *x_offset = next_keyframe->x;
+          *y_offset = next_keyframe->y;
+        }
+      else if (next_keyframe == NULL)
+        {
+          *x_offset = prev_keyframe->x;
+          *y_offset = prev_keyframe->y;
+        }
+      else
+        {
+          /* XXX No curve editing or anything like this yet.
+           * All keyframing is linear in this first version.
+           */
+          *x_offset = prev_keyframe->x + (position - prev_keyframe_pos) *
+                                         (next_keyframe->x - prev_keyframe->x) /
+                                         (next_keyframe_pos - prev_keyframe_pos);
+          *y_offset = prev_keyframe->y + (position - prev_keyframe_pos) *
+                                         (next_keyframe->y - prev_keyframe->y) /
+                                         (next_keyframe_pos - prev_keyframe_pos);
+        }
+    }
+}
+
+/************ Private Functions ****************/
+
+static void
+animation_camera_finalize (GObject *object)
+{
+  g_list_free_full (ANIMATION_CAMERA (object)->priv->offsets, g_free);
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+animation_camera_set_property (GObject      *object,
+                               guint         property_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  AnimationCamera *camera = ANIMATION_CAMERA (object);
+
+  switch (property_id)
+    {
+    case PROP_ANIMATION:
+      camera->priv->animation = g_value_get_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+
+static void
+animation_camera_get_property (GObject      *object,
+                               guint         property_id,
+                               GValue       *value,
+                               GParamSpec   *pspec)
+{
+  AnimationCamera *camera = ANIMATION_CAMERA (object);
+
+  switch (property_id)
+    {
+    case PROP_ANIMATION:
+      g_value_set_object (value, camera->priv->animation);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+animation_camera_emit_offsets_changed (AnimationCamera *camera,
+                                       gint             position)
+{
+  GList *iter;
+  gint   prev_keyframe;
+  gint   next_keyframe;
+  gint   i;
+
+  if (position > 0)
+    {
+      i = position - 1;
+      iter = g_list_nth (camera->priv->offsets, i);
+      for (; iter && ! iter->data; iter = iter->prev, i--)
+        ;
+      prev_keyframe = i + 1;
+    }
+  else
+    {
+      prev_keyframe = 0;
+    }
+  if (position < animation_get_duration (camera->priv->animation) - 1)
+    {
+      i = position + 1;
+      iter = g_list_nth (camera->priv->offsets, i);
+      for (; iter && ! iter->data; iter = iter->next, i++)
+        ;
+      if (iter && iter->data)
+        next_keyframe = i - 1;
+      else
+        next_keyframe = animation_get_duration (camera->priv->animation) - 1;
+    }
+  else
+    {
+      next_keyframe = animation_get_duration (camera->priv->animation) - 1;
+    }
+  g_signal_emit (camera, signals[OFFSETS_CHANGED], 0,
+                 prev_keyframe, next_keyframe - prev_keyframe + 1);
+}
diff --git a/plug-ins/animation-play/core/animation-camera.h b/plug-ins/animation-play/core/animation-camera.h
new file mode 100644
index 0000000..4f27b19
--- /dev/null
+++ b/plug-ins/animation-play/core/animation-camera.h
@@ -0,0 +1,72 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation-camera.h
+ * Copyright (C) 2016-2017 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __ANIMATION_CAMERA_H__
+#define __ANIMATION_CAMERA_H__
+
+#define ANIMATION_TYPE_CAMERA            (animation_camera_get_type ())
+#define ANIMATION_CAMERA(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), ANIMATION_TYPE_CAMERA, 
AnimationCamera))
+#define ANIMATION_CAMERA_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), ANIMATION_TYPE_CAMERA, 
AnimationCameraClass))
+#define ANIMATION_IS_CAMERA(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ANIMATION_TYPE_CAMERA))
+#define ANIMATION_IS_CAMERA_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ANIMATION_TYPE_CAMERA))
+#define ANIMATION_CAMERA_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), ANIMATION_TYPE_CAMERA, 
AnimationCameraClass))
+
+typedef struct _AnimationCamera        AnimationCamera;
+typedef struct _AnimationCameraClass   AnimationCameraClass;
+typedef struct _AnimationCameraPrivate AnimationCameraPrivate;
+
+struct _AnimationCamera
+{
+  GObject                   parent_instance;
+
+  AnimationCameraPrivate   *priv;
+};
+
+struct _AnimationCameraClass
+{
+  GObjectClass              parent_class;
+
+  /* Signals */
+  void         (*offsets_changed)   (AnimationCamera *camera,
+                                     gint             position,
+                                     gint             duration);
+  void         (*keyframe_set)      (AnimationCamera *camera,
+                                     gint             position);
+  void         (*keyframe_deleted)  (AnimationCamera *camera,
+                                     gint             position);
+};
+
+GType             animation_camera_get_type        (void) G_GNUC_CONST;
+
+AnimationCamera * animation_camera_new             (Animation       *animation);
+
+void              animation_camera_set_keyframe    (AnimationCamera *camera,
+                                                    gint             position,
+                                                    gint             x,
+                                                    gint             y);
+void              animation_camera_delete_keyframe (AnimationCamera *camera,
+                                                    gint             position);
+
+void              animation_camera_get             (AnimationCamera *camera,
+                                                    gint             position,
+                                                    gint            *x_offset,
+                                                    gint            *y_offset);
+
+#endif  /*  __ANIMATION_CAMERA_H__  */
diff --git a/plug-ins/animation-play/core/animation-celanimation.c 
b/plug-ins/animation-play/core/animation-celanimation.c
index 5b08fc7..419bb0a 100644
--- a/plug-ins/animation-play/core/animation-celanimation.c
+++ b/plug-ins/animation-play/core/animation-celanimation.c
@@ -24,6 +24,10 @@
 #include <libgimp/stdplugins-intl.h>
 
 #include "animation-utils.h"
+
+#include "animation.h"
+#include "animation-camera.h"
+
 #include "animation-celanimation.h"
 
 typedef struct _AnimationCelAnimationPrivate AnimationCelAnimationPrivate;
@@ -38,34 +42,45 @@ Track;
 
 typedef struct
 {
+  gint   tattoo;
+  gint   offset_x;
+  gint   offset_y;
+}
+CompLayer;
+
+typedef struct
+{
   GeglBuffer *buffer;
 
   /* The list of layers (identified by their tattoos) composited into
    * this buffer allows to easily compare caches so that to not
    * duplicate them.*/
-  gint *composition;
-  gint  n_sources;
+  CompLayer  *composition;
+  gint        n_sources;
 
-  gint refs;
+  gint        refs;
 }
 Cache;
 
 struct _AnimationCelAnimationPrivate
 {
   /* The number of frames. */
-  gint   duration;
+  gint             duration;
 
   /* Frames are cached as GEGL buffers. */
-  GList *cache;
+  GList           *cache;
 
   /* Panel comments. */
-  GList *comments;
+  GList           *comments;
 
   /* List of tracks/levels.
    * The background is a special-named track, always present
    * and first.
    * There is always at least 1 additional track. */
-  GList *tracks;
+  GList           *tracks;
+
+  /* The globale camera. */
+  AnimationCamera *camera;
 };
 
 typedef enum
@@ -135,6 +150,12 @@ static void      animation_cel_animation_text              (GMarkupParseContext
                                                             gpointer              user_data,
                                                             GError              **error);
 
+/* Signal handling */
+
+static void      on_camera_offsets_changed                 (AnimationCamera        *camera,
+                                                            gint                    position,
+                                                            gint                    duration,
+                                                            AnimationCelAnimation  *animation);
 /* Utils */
 
 static void         animation_cel_animation_cache          (AnimationCelAnimation  *animation,
@@ -350,6 +371,12 @@ animation_cel_animation_set_duration (AnimationCelAnimation *animation,
     }
 }
 
+GObject *
+animation_cel_animation_get_main_camera (AnimationCelAnimation *animation)
+{
+  return G_OBJECT (animation->priv->camera);
+}
+
 gint
 animation_cel_animation_get_levels (AnimationCelAnimation *animation)
 {
@@ -748,6 +775,11 @@ animation_cel_animation_reset_defaults (Animation *animation)
                                           layers);
         }
     }
+
+  priv->camera = animation_camera_new (animation);
+  g_signal_connect (priv->camera, "offsets-changed",
+                    G_CALLBACK (on_camera_offsets_changed),
+                    animation);
 }
 
 static gchar *
@@ -910,6 +942,15 @@ animation_cel_animation_deserialize (Animation    *animation,
       cel_animation = ANIMATION_CEL_ANIMATION (animation);
       /* Reverse track order. */
       cel_animation->priv->tracks = g_list_reverse (cel_animation->priv->tracks);
+
+      /* TODO: just testing right now. I will have to add actual
+       * (de)serialization, otherwise there is no persistency of
+       * camera works.
+       */
+      cel_animation->priv->camera = animation_camera_new (animation);
+      g_signal_connect (cel_animation->priv->camera, "offsets-changed",
+                        G_CALLBACK (on_camera_offsets_changed),
+                        animation);
     }
   g_markup_parse_context_free (context);
 
@@ -1207,6 +1248,23 @@ animation_cel_animation_text (GMarkupParseContext  *context,
     }
 }
 
+/**** Signal handling ****/
+
+static void
+on_camera_offsets_changed (AnimationCamera       *camera,
+                           gint                   position,
+                           gint                   duration,
+                           AnimationCelAnimation *animation)
+{
+  gint i;
+
+  for (i = position; i < position + duration; i++)
+    animation_cel_animation_cache (animation, i);
+
+  g_signal_emit_by_name (animation, "cache-invalidated",
+                         position, duration);
+}
+
 /**** Utils ****/
 
 static void
@@ -1216,13 +1274,15 @@ animation_cel_animation_cache (AnimationCelAnimation *animation,
   GeglBuffer *backdrop = NULL;
   GList      *iter;
   Cache      *cache;
-  gint       *composition;
+  CompLayer  *composition;
   gint        n_sources = 0;
   gint32      image_id;
   gdouble     proxy_ratio;
   gint        preview_width;
   gint        preview_height;
   gint        i;
+  gint        main_offset_x;
+  gint        main_offset_y;
 
   /* Clean out current cache. */
   iter = g_list_nth (animation->priv->cache, pos);
@@ -1266,8 +1326,11 @@ animation_cel_animation_cache (AnimationCelAnimation *animation,
       animation->priv->cache = g_list_reverse (animation->priv->cache);
     }
 
+  animation_camera_get (animation->priv->camera,
+                        pos, &main_offset_x, &main_offset_y);
+
   /* Create the new buffer composition. */
-  composition = g_new0 (int, n_sources);
+  composition = g_new0 (CompLayer, n_sources);
   i = 0;
   for (iter = animation->priv->tracks; iter; iter = iter->next)
     {
@@ -1284,7 +1347,9 @@ animation_cel_animation_cache (AnimationCelAnimation *animation,
           tattoo = GPOINTER_TO_INT (layer->data);
           if (tattoo)
             {
-              composition[i++] = tattoo;
+              composition[i].tattoo = tattoo;
+              composition[i].offset_x = main_offset_x;
+              composition[i++].offset_y = main_offset_y;
             }
         }
     }
@@ -1302,7 +1367,9 @@ animation_cel_animation_cache (AnimationCelAnimation *animation,
               same = TRUE;
               for (i = 0; i < n_sources; i++)
                 {
-                  if (cache->composition[i] != composition[i])
+                  if (cache->composition[i].tattoo   != composition[i].tattoo   ||
+                      cache->composition[i].offset_x != composition[i].offset_x ||
+                      cache->composition[i].offset_y != composition[i].offset_y)
                     {
                       same = FALSE;
                       break;
@@ -1340,7 +1407,7 @@ animation_cel_animation_cache (AnimationCelAnimation *animation,
       gint        layer_offy;
 
       layer = gimp_image_get_layer_by_tattoo (image_id,
-                                              cache->composition[i]);
+                                              cache->composition[i].tattoo);
       if (layer > 0)
         source = gimp_drawable_get_buffer (layer);
       if (layer <= 0 || ! source)
@@ -1353,7 +1420,8 @@ animation_cel_animation_cache (AnimationCelAnimation *animation,
       intermediate = normal_blend (preview_width, preview_height,
                                    backdrop, 1.0, 0, 0,
                                    source, proxy_ratio,
-                                   layer_offx, layer_offy);
+                                   layer_offx + cache->composition[i].offset_x,
+                                   layer_offy + cache->composition[i].offset_y);
       g_object_unref (source);
       if (backdrop)
         {
@@ -1379,6 +1447,8 @@ animation_cel_animation_cleanup (AnimationCelAnimation *animation)
   g_list_free_full (animation->priv->tracks,
                     (GDestroyNotify) animation_cel_animation_clean_track);
   animation->priv->tracks   = NULL;
+
+  g_object_unref (animation->priv->camera);
 }
 
 static gboolean
@@ -1395,7 +1465,9 @@ animation_cel_animation_cache_cmp (Cache *cache1,
       identical = TRUE;
       for (i = 0; i < cache1->n_sources; i++)
         {
-          if (cache1->composition[i] != cache2->composition[i])
+          if (cache1->composition[i].tattoo   != cache2->composition[i].tattoo   ||
+              cache1->composition[i].offset_x != cache2->composition[i].offset_x ||
+              cache1->composition[i].offset_y != cache2->composition[i].offset_y)
             {
               identical = FALSE;
               break;
diff --git a/plug-ins/animation-play/core/animation-celanimation.h 
b/plug-ins/animation-play/core/animation-celanimation.h
index 36205fc..1aa0d00 100644
--- a/plug-ins/animation-play/core/animation-celanimation.h
+++ b/plug-ins/animation-play/core/animation-celanimation.h
@@ -66,6 +66,8 @@ const gchar * animation_cel_animation_get_comment     (AnimationCelAnimation *an
 void          animation_cel_animation_set_duration    (AnimationCelAnimation *animation,
                                                        gint                   duration);
 
+GObject     * animation_cel_animation_get_main_camera (AnimationCelAnimation *animation);
+
 gint          animation_cel_animation_get_levels      (AnimationCelAnimation *animation);
 gint          animation_cel_animation_level_up        (AnimationCelAnimation *animation,
                                                        gint                   level);
diff --git a/plug-ins/animation-play/widgets/animation-dialog.c 
b/plug-ins/animation-play/widgets/animation-dialog.c
index 2b909ed..ed0c2b6 100755
--- a/plug-ins/animation-play/widgets/animation-dialog.c
+++ b/plug-ins/animation-play/widgets/animation-dialog.c
@@ -30,10 +30,12 @@
 
 #include "core/animation.h"
 #include "core/animation-animatic.h"
+#include "core/animation-camera.h"
 #include "core/animation-celanimation.h"
 #include "core/animation-playback.h"
 
 #include "animation-dialog.h"
+#include "animation-keyframe-view.h"
 #include "animation-layer-view.h"
 #include "animation-storyboard.h"
 #include "animation-xsheet.h"
@@ -91,7 +93,9 @@ struct _AnimationDialogPrivate
   guint              shape_drawing_area_height;
 
   /* Notebook on the right (layer list, storyboard, settings). */
+  GtkWidget         *right_pane;
   GtkWidget         *right_notebook;
+  GtkWidget         *keyframe_view;
 
   /* Notebook: settings. */
   GtkWidget         *settings;
@@ -103,8 +107,8 @@ struct _AnimationDialogPrivate
   /* Notebook: layer list. */
   GtkWidget         *layer_list;
 
-  /* The vpaned (bottom is timeline, above is preview). */
-  GtkWidget         *vpaned;
+  /* The left panel (bottom is timeline, above is preview). */
+  GtkWidget         *left_pane;
   GtkWidget         *xsheet;
 
   /* Actions */
@@ -425,15 +429,24 @@ animation_dialog_constructed (GObject *object)
   gtk_container_add (GTK_CONTAINER (dialog), hpaned);
   gtk_widget_show (hpaned);
 
-  priv->vpaned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
-  gtk_paned_pack1 (GTK_PANED (hpaned), priv->vpaned, TRUE, TRUE);
-  gtk_widget_show (priv->vpaned);
+  priv->left_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
+  gtk_paned_pack1 (GTK_PANED (hpaned), priv->left_pane, TRUE, TRUE);
+  gtk_widget_show (priv->left_pane);
 
-  priv->right_notebook = gtk_notebook_new ();
-  gtk_paned_pack2 (GTK_PANED (hpaned), priv->right_notebook,
+  priv->right_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
+  gtk_paned_pack2 (GTK_PANED (hpaned), priv->right_pane,
                    TRUE, TRUE);
+  gtk_widget_show (priv->right_pane);
+
+  priv->right_notebook = gtk_notebook_new ();
+  gtk_paned_pack1 (GTK_PANED (priv->right_pane), priv->right_notebook,
+                   TRUE, FALSE);
   gtk_widget_show (priv->right_notebook);
 
+  priv->keyframe_view = animation_keyframe_view_new ();
+  gtk_paned_pack2 (GTK_PANED (priv->right_pane), priv->keyframe_view,
+                   TRUE, FALSE);
+
   /******************\
   |**** Settings ****|
   \******************/
@@ -579,7 +592,7 @@ animation_dialog_constructed (GObject *object)
 
   /* Playback vertical box. */
   main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
-  gtk_paned_pack1 (GTK_PANED (priv->vpaned), main_vbox, TRUE, TRUE);
+  gtk_paned_pack1 (GTK_PANED (priv->left_pane), main_vbox, TRUE, TRUE);
   gtk_widget_show (main_vbox);
 
   /*************/
@@ -1327,7 +1340,7 @@ animation_dialog_set_animation (AnimationDialog *dialog,
     }
 
   /* The bottom panel. */
-  frame = gtk_paned_get_child2 (GTK_PANED (priv->vpaned));
+  frame = gtk_paned_get_child2 (GTK_PANED (priv->left_pane));
   if (frame)
     {
       gtk_widget_destroy (frame);
@@ -1337,12 +1350,13 @@ animation_dialog_set_animation (AnimationDialog *dialog,
   if (ANIMATION_IS_CEL_ANIMATION (animation))
     {
       frame = gtk_frame_new (_("X-Sheet"));
-      gtk_paned_pack2 (GTK_PANED (priv->vpaned), frame,
+      gtk_paned_pack2 (GTK_PANED (priv->left_pane), frame,
                        TRUE, TRUE);
       gtk_widget_show (frame);
 
       priv->xsheet = animation_xsheet_new (ANIMATION_CEL_ANIMATION (animation),
-                                           priv->playback, priv->layer_list);
+                                           priv->playback, priv->layer_list,
+                                           priv->keyframe_view);
       gtk_container_add (GTK_CONTAINER (frame), priv->xsheet);
       gtk_widget_show (priv->xsheet);
     }
diff --git a/plug-ins/animation-play/widgets/animation-keyframe-view.c 
b/plug-ins/animation-play/widgets/animation-keyframe-view.c
new file mode 100644
index 0000000..810f09c
--- /dev/null
+++ b/plug-ins/animation-play/widgets/animation-keyframe-view.c
@@ -0,0 +1,236 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation-keyframe_view.c
+ * Copyright (C) 2016-2017 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <libgimp/gimp.h>
+#include <libgimp/gimpui.h>
+
+#include "libgimp/stdplugins-intl.h"
+
+#include "core/animation.h"
+#include "core/animation-camera.h"
+#include "core/animation-celanimation.h"
+
+#include "animation-keyframe-view.h"
+
+struct _AnimationKeyFrameViewPrivate
+{
+  AnimationCamera *camera;
+  gint             position;
+
+  GtkWidget       *offset_entry;
+};
+
+/* GObject handlers */
+static void animation_keyframe_view_constructed  (GObject               *object);
+
+static void on_offset_entry_changed              (GimpSizeEntry         *entry,
+                                                  AnimationKeyFrameView *view);
+static void on_offsets_changed                   (AnimationCamera       *camera,
+                                                  gint                   position,
+                                                  gint                   duration,
+                                                  AnimationKeyFrameView *view);
+
+G_DEFINE_TYPE (AnimationKeyFrameView, animation_keyframe_view, GTK_TYPE_NOTEBOOK)
+
+#define parent_class animation_keyframe_view_parent_class
+
+static void
+animation_keyframe_view_class_init (AnimationKeyFrameViewClass *klass)
+{
+  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed  = animation_keyframe_view_constructed;
+
+  g_type_class_add_private (klass, sizeof (AnimationKeyFrameViewPrivate));
+}
+
+static void
+animation_keyframe_view_init (AnimationKeyFrameView *view)
+{
+  view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
+                                            ANIMATION_TYPE_KEYFRAME_VIEW,
+                                            AnimationKeyFrameViewPrivate);
+}
+
+/************ Public Functions ****************/
+
+/**
+ * animation_keyframe_view_new:
+ *
+ * Creates a new layer view. You should not show it with
+ * gtk_widget_show() but with animation_keyframe_view_show() instead.
+ */
+GtkWidget *
+animation_keyframe_view_new ()
+{
+  GtkWidget *view;
+
+  view = g_object_new (ANIMATION_TYPE_KEYFRAME_VIEW,
+                       NULL);
+
+  return view;
+}
+
+/**
+ * animation_keyframe_view_show:
+ * @view: the #AnimationKeyFrameView.
+ * @animation: the #Animation.
+ * @position:
+ *
+ * Show the @view widget, displaying the keyframes set on
+ * @animation at @position.
+ */
+void
+animation_keyframe_view_show (AnimationKeyFrameView *view,
+                              AnimationCelAnimation *animation,
+                              gint                   position)
+{
+  AnimationCamera *camera;
+  gint32           image_id;
+  gint             width;
+  gint             height;
+  gdouble          xres;
+  gdouble          yres;
+  gint             x_offset;
+  gint             y_offset;
+
+  camera = ANIMATION_CAMERA (animation_cel_animation_get_main_camera (animation));
+
+  view->priv->camera   = camera;
+  view->priv->position = position;
+
+  image_id = animation_get_image_id (ANIMATION (animation));
+  gimp_image_get_resolution (image_id, &xres, &yres);
+
+  animation_get_size (ANIMATION (animation), &width, &height);
+  gimp_size_entry_set_refval_boundaries (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                                         0, (gdouble) -width, (gdouble) width);
+  gimp_size_entry_set_refval_boundaries (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                                         1, (gdouble) -height, (gdouble) height);
+  gimp_size_entry_set_size (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                            0, 0.0, (gdouble) width);
+  gimp_size_entry_set_size (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                            1, 0.0, (gdouble) height);
+  gimp_size_entry_set_resolution (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                            0, xres, TRUE);
+  gimp_size_entry_set_resolution (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                            1, yres, TRUE);
+
+  g_signal_handlers_disconnect_by_func (view->priv->offset_entry,
+                                        G_CALLBACK (on_offset_entry_changed),
+                                        view);
+  g_signal_handlers_disconnect_by_func (view->priv->camera,
+                                        G_CALLBACK (on_offsets_changed),
+                                        view);
+  animation_camera_get (camera, position, &x_offset, &y_offset);
+  gimp_size_entry_set_value (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                             0, (gdouble) x_offset);
+  gimp_size_entry_set_value (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                             1, (gdouble) y_offset);
+  g_signal_connect (view->priv->offset_entry, "value-changed",
+                    G_CALLBACK (on_offset_entry_changed),
+                    view);
+  g_signal_connect (camera, "offsets-changed",
+                    G_CALLBACK (on_offsets_changed),
+                    view);
+  gtk_widget_show (GTK_WIDGET (view));
+}
+
+void
+animation_keyframe_view_hide (AnimationKeyFrameView *view)
+{
+  g_signal_handlers_disconnect_by_func (view->priv->offset_entry,
+                                        G_CALLBACK (on_offset_entry_changed),
+                                        view);
+  g_signal_handlers_disconnect_by_func (view->priv->camera,
+                                        G_CALLBACK (on_offsets_changed),
+                                        view);
+}
+
+/************ Private Functions ****************/
+
+static void
+animation_keyframe_view_constructed (GObject *object)
+{
+  AnimationKeyFrameView *view = ANIMATION_KEYFRAME_VIEW (object);
+  GtkWidget             *page;
+  GtkWidget             *label;
+
+  page = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+  label = gtk_image_new_from_icon_name ("camera-video",
+                                        GTK_ICON_SIZE_SMALL_TOOLBAR);
+  gtk_notebook_append_page (GTK_NOTEBOOK (view), page,
+                            label);
+
+  view->priv->offset_entry = gimp_size_entry_new (2, GIMP_UNIT_PIXEL, NULL,
+                                                  TRUE, TRUE, FALSE, 5,
+                                                  GIMP_SIZE_ENTRY_UPDATE_SIZE);
+  gimp_size_entry_attach_label (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                                _("Horizontal offset:"), 0, 1, 0.0);
+  gimp_size_entry_attach_label (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                                _("Vertical offset:"), 0, 2, 0.0);
+  gimp_size_entry_set_pixel_digits (GIMP_SIZE_ENTRY (view->priv->offset_entry), 0);
+  gtk_box_pack_start (GTK_BOX (page), view->priv->offset_entry, FALSE, FALSE, 0);
+  gtk_widget_show (view->priv->offset_entry);
+
+  gtk_widget_show (page);
+}
+
+static void
+on_offset_entry_changed (GimpSizeEntry         *entry,
+                         AnimationKeyFrameView *view)
+{
+  gdouble x_offset;
+  gdouble y_offset;
+
+  x_offset = gimp_size_entry_get_refval (GIMP_SIZE_ENTRY (view->priv->offset_entry), 0);
+  y_offset = gimp_size_entry_get_refval (GIMP_SIZE_ENTRY (view->priv->offset_entry), 1);
+  animation_camera_set_keyframe (view->priv->camera,
+                                 view->priv->position,
+                                 x_offset, y_offset);
+}
+
+static void
+on_offsets_changed (AnimationCamera       *camera,
+                    gint                   position,
+                    gint                   duration,
+                    AnimationKeyFrameView *view)
+{
+  if (view->priv->position >= position &&
+      view->priv->position < position + duration)
+    {
+      gint x_offset;
+      gint y_offset;
+
+      g_signal_handlers_block_by_func (view->priv->offset_entry,
+                                       G_CALLBACK (on_offset_entry_changed),
+                                       view);
+      animation_camera_get (camera, position, &x_offset, &y_offset);
+      gimp_size_entry_set_value (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                                 0, (gdouble) x_offset);
+      gimp_size_entry_set_value (GIMP_SIZE_ENTRY (view->priv->offset_entry),
+                                 1, (gdouble) y_offset);
+      g_signal_handlers_unblock_by_func (view->priv->offset_entry,
+                                         G_CALLBACK (on_offset_entry_changed),
+                                         view);
+    }
+}
diff --git a/plug-ins/animation-play/widgets/animation-keyframe-view.h 
b/plug-ins/animation-play/widgets/animation-keyframe-view.h
new file mode 100644
index 0000000..64f7d12
--- /dev/null
+++ b/plug-ins/animation-play/widgets/animation-keyframe-view.h
@@ -0,0 +1,56 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation-keyframe_view.h
+ * Copyright (C) 2016-2017 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __ANIMATION_KEYFRAME_VIEW_H__
+#define __ANIMATION_KEYFRAME_VIEW_H__
+
+#define ANIMATION_TYPE_KEYFRAME_VIEW            (animation_keyframe_view_get_type ())
+#define ANIMATION_KEYFRAME_VIEW(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
ANIMATION_TYPE_KEYFRAME_VIEW, AnimationKeyFrameView))
+#define ANIMATION_KEYFRAME_VIEW_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), 
ANIMATION_TYPE_KEYFRAME_VIEW, AnimationKeyFrameViewClass))
+#define ANIMATION_IS_KEYFRAME_VIEW(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
ANIMATION_TYPE_KEYFRAME_VIEW))
+#define ANIMATION_IS_KEYFRAME_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), 
ANIMATION_TYPE_KEYFRAME_VIEW))
+#define ANIMATION_KEYFRAME_VIEW_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), 
ANIMATION_TYPE_KEYFRAME_VIEW, AnimationKeyFrameViewClass))
+
+typedef struct _AnimationKeyFrameView        AnimationKeyFrameView;
+typedef struct _AnimationKeyFrameViewClass   AnimationKeyFrameViewClass;
+typedef struct _AnimationKeyFrameViewPrivate AnimationKeyFrameViewPrivate;
+
+struct _AnimationKeyFrameView
+{
+  GtkNotebook                   parent_instance;
+
+  AnimationKeyFrameViewPrivate *priv;
+};
+
+struct _AnimationKeyFrameViewClass
+{
+  GtkNotebookClass              parent_class;
+};
+
+GType       animation_keyframe_view_get_type (void) G_GNUC_CONST;
+
+GtkWidget * animation_keyframe_view_new      (void);
+
+void        animation_keyframe_view_show     (AnimationKeyFrameView *view,
+                                              AnimationCelAnimation *animation,
+                                              gint                   position);
+void        animation_keyframe_view_hide     (AnimationKeyFrameView *view);
+
+#endif  /*  __ANIMATION_KEYFRAME_VIEW_H__  */
diff --git a/plug-ins/animation-play/widgets/animation-xsheet.c 
b/plug-ins/animation-play/widgets/animation-xsheet.c
index 43a5950..a591625 100755
--- a/plug-ins/animation-play/widgets/animation-xsheet.c
+++ b/plug-ins/animation-play/widgets/animation-xsheet.c
@@ -29,9 +29,11 @@
 #include "animation-utils.h"
 
 #include "core/animation.h"
+#include "core/animation-camera.h"
 #include "core/animation-playback.h"
 #include "core/animation-celanimation.h"
 
+#include "animation-keyframe-view.h"
 #include "animation-layer-view.h"
 #include "animation-menus.h"
 
@@ -44,6 +46,7 @@ enum
   PROP_0,
   PROP_ANIMATION,
   PROP_LAYER_VIEW,
+  PROP_KEYFRAME_VIEW,
   PROP_SUITE_CYCLE,
   PROP_SUITE_FPI
 };
@@ -54,6 +57,7 @@ struct _AnimationXSheetPrivate
   AnimationPlayback     *playback;
 
   GtkWidget             *layer_view;
+  GtkWidget             *keyframe_view;
 
   GtkWidget             *track_layout;
 
@@ -67,6 +71,8 @@ struct _AnimationXSheetPrivate
   gint                   suite_cycle;
   gint                   suite_fpi;
 
+  GList                 *effect_buttons;
+
   GList                 *cels;
   gint                   selected_track;
   GQueue                *selected_frames;
@@ -141,10 +147,18 @@ static void     on_animation_loaded            (Animation         *animation,
 static void     on_animation_duration_changed  (Animation         *animation,
                                                 gint               duration,
                                                 AnimationXSheet   *xsheet);
+/* Callbacks on camera. */
+static void     on_camera_keyframe_set         (AnimationCamera   *camera,
+                                                gint               position,
+                                                AnimationXSheet   *xsheet);
+static void     on_camera_keyframe_deleted     (AnimationCamera   *camera,
+                                                gint               position,
+                                                AnimationXSheet   *xsheet);
+
 /* Callbacks on playback. */
-static void     on_animation_stopped           (AnimationPlayback *playback,
+static void     on_playback_stopped            (AnimationPlayback *playback,
                                                 AnimationXSheet   *xsheet);
-static void     on_animation_rendered          (AnimationPlayback *animation,
+static void     on_playback_rendered           (AnimationPlayback *animation,
                                                 gint               frame_number,
                                                 GeglBuffer        *buffer,
                                                 gboolean           must_draw_null,
@@ -160,6 +174,9 @@ static void     animation_xsheet_suite_do            (GtkWidget       *button,
                                                       AnimationXSheet *xsheet);
 static void     animation_xsheet_suite_cancelled     (GtkWidget       *button,
                                                       AnimationXSheet *xsheet);
+static gboolean animation_xsheet_effect_clicked      (GtkWidget       *button,
+                                                      GdkEvent        *event,
+                                                      AnimationXSheet *xsheet);
 static gboolean animation_xsheet_frame_clicked       (GtkWidget       *button,
                                                       GdkEvent        *event,
                                                       AnimationXSheet *xsheet);
@@ -245,6 +262,17 @@ animation_xsheet_class_init (AnimationXSheetClass *klass)
                                                         G_PARAM_READWRITE |
                                                         G_PARAM_CONSTRUCT_ONLY));
   /**
+   * AnimationXSheet:keyframe-view:
+   *
+   * The associated #AnimationLayerView.
+   */
+  g_object_class_install_property (object_class, PROP_KEYFRAME_VIEW,
+                                   g_param_spec_object ("keyframe-view",
+                                                        NULL, NULL,
+                                                        ANIMATION_TYPE_KEYFRAME_VIEW,
+                                                        G_PARAM_READWRITE |
+                                                        G_PARAM_CONSTRUCT_ONLY));
+  /**
    * AnimationXSheet:suite-cycle:
    *
    * The configured cycle repeat. 0 means indefinitely.
@@ -284,20 +312,22 @@ animation_xsheet_init (AnimationXSheet *xsheet)
 GtkWidget *
 animation_xsheet_new (AnimationCelAnimation *animation,
                       AnimationPlayback     *playback,
-                      GtkWidget             *layer_view)
+                      GtkWidget             *layer_view,
+                      GtkWidget             *keyframe_view)
 {
   GtkWidget *xsheet;
 
   xsheet = g_object_new (ANIMATION_TYPE_XSHEET,
                          "animation", animation,
                          "layer-view", layer_view,
+                         "keyframe-view", keyframe_view,
                          NULL);
   ANIMATION_XSHEET (xsheet)->priv->playback = playback;
   g_signal_connect (ANIMATION_XSHEET (xsheet)->priv->playback,
-                    "render", G_CALLBACK (on_animation_rendered),
+                    "render", G_CALLBACK (on_playback_rendered),
                     xsheet);
   g_signal_connect (ANIMATION_XSHEET (xsheet)->priv->playback,
-                    "stop", G_CALLBACK (on_animation_stopped),
+                    "stop", G_CALLBACK (on_playback_stopped),
                     xsheet);
 
   return xsheet;
@@ -352,6 +382,9 @@ animation_xsheet_set_property (GObject      *object,
     case PROP_LAYER_VIEW:
       xsheet->priv->layer_view = g_value_dup_object (value);
       break;
+    case PROP_KEYFRAME_VIEW:
+      xsheet->priv->keyframe_view = g_value_dup_object (value);
+      break;
     case PROP_SUITE_CYCLE:
       xsheet->priv->suite_cycle = g_value_get_int (value);
       break;
@@ -381,6 +414,9 @@ animation_xsheet_get_property (GObject      *object,
     case PROP_LAYER_VIEW:
       g_value_set_object (value, xsheet->priv->layer_view);
       break;
+    case PROP_KEYFRAME_VIEW:
+      g_value_set_object (value, xsheet->priv->keyframe_view);
+      break;
     case PROP_SUITE_CYCLE:
       g_value_set_int (value, xsheet->priv->suite_cycle);
       break;
@@ -400,10 +436,10 @@ animation_xsheet_finalize (GObject *object)
   AnimationXSheet *xsheet = ANIMATION_XSHEET (object);
 
   g_signal_handlers_disconnect_by_func (ANIMATION_XSHEET (xsheet)->priv->playback,
-                                        G_CALLBACK (on_animation_rendered),
+                                        G_CALLBACK (on_playback_rendered),
                                         xsheet);
   g_signal_handlers_disconnect_by_func (ANIMATION_XSHEET (xsheet)->priv->playback,
-                                        G_CALLBACK (on_animation_stopped),
+                                        G_CALLBACK (on_playback_stopped),
                                         xsheet);
   if (xsheet->priv->animation)
     g_object_unref (xsheet->priv->animation);
@@ -413,6 +449,7 @@ animation_xsheet_finalize (GObject *object)
 
   g_list_free_full (xsheet->priv->cels, (GDestroyNotify) g_list_free);
   g_list_free (xsheet->priv->comment_fields);
+  g_list_free (xsheet->priv->effect_buttons);
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
 }
@@ -438,7 +475,7 @@ animation_xsheet_add_headers (AnimationXSheet *xsheet,
                                         frame, level);
   gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_OUT);
   gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout),
-                    frame, level * 9 + 1, level * 9 + 10, 1, 2,
+                    frame, level * 9 + 2, level * 9 + 11, 1, 2,
                     GTK_FILL, GTK_FILL, 0, 0);
   label = gtk_entry_new ();
   gtk_entry_set_text (GTK_ENTRY (label), title);
@@ -493,7 +530,7 @@ animation_xsheet_add_headers (AnimationXSheet *xsheet,
   gtk_widget_show (GTK_WIDGET (item));
 
   gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout),
-                    toolbar, level * 9 + 9, level * 9 + 11, 0, 1,
+                    toolbar, level * 9 + 10, level * 9 + 12, 0, 1,
                     GTK_FILL, GTK_FILL, 0, 0);
   gtk_widget_show (toolbar);
 
@@ -563,7 +600,7 @@ animation_xsheet_add_headers (AnimationXSheet *xsheet,
   gtk_widget_show (GTK_WIDGET (item));
 
   gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout),
-                    toolbar, level * 9 + 2, level * 9 + 9, 0, 1,
+                    toolbar, level * 9 + 3, level * 9 + 10, 0, 1,
                     GTK_FILL, GTK_FILL, 0, 0);
   gtk_widget_show (toolbar);
 }
@@ -735,11 +772,41 @@ animation_xsheet_add_frames (AnimationXSheet *xsheet,
       gtk_widget_show (label);
       gtk_widget_show (frame);
 
+      /* Create effect button. */
+      frame = gtk_frame_new (NULL);
+      gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_OUT);
+      gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout),
+                        frame, 1, 2, i + 2, i + 3,
+                        GTK_FILL, GTK_FILL, 0, 0);
+
+      label = gtk_toggle_button_new ();
+      xsheet->priv->effect_buttons = g_list_append (xsheet->priv->effect_buttons,
+                                                    label);
+      g_object_set_data (G_OBJECT (label), "frame-position",
+                         GINT_TO_POINTER (i));
+      gtk_button_set_relief (GTK_BUTTON (label), GTK_RELIEF_NONE);
+      gtk_button_set_focus_on_click (GTK_BUTTON (label), FALSE);
+      g_signal_connect (label, "button-press-event",
+                        G_CALLBACK (animation_xsheet_effect_clicked),
+                        xsheet);
+      g_signal_connect (animation_cel_animation_get_main_camera (xsheet->priv->animation),
+                        "keyframe-set",
+                        G_CALLBACK (on_camera_keyframe_set),
+                        xsheet);
+      g_signal_connect (animation_cel_animation_get_main_camera (xsheet->priv->animation),
+                        "keyframe-deleted",
+                        G_CALLBACK (on_camera_keyframe_deleted),
+                        xsheet);
+      gtk_container_add (GTK_CONTAINER (frame), label);
+
+      gtk_widget_show (label);
+      gtk_widget_show (frame);
+
       /* Create new comment fields. */
       frame = gtk_frame_new (NULL);
       gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
       gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout),
-                        frame, n_tracks * 9 + 1, n_tracks * 9 + 6, i + 2, i + 3,
+                        frame, n_tracks * 9 + 2, n_tracks * 9 + 7, i + 2, i + 3,
                         GTK_FILL, GTK_FILL, 0, 0);
       comment_field = gtk_text_view_new ();
       xsheet->priv->comment_fields = g_list_append (xsheet->priv->comment_fields,
@@ -836,6 +903,24 @@ animation_xsheet_remove_frames (AnimationXSheet   *xsheet,
   item->prev = NULL;
   g_list_free (item);
 
+  /* Remove the effect button. */
+  item = g_list_nth (xsheet->priv->effect_buttons, position);
+  for (iter = item, i = position; iter && i < position + n_frames; iter = iter->next)
+    {
+      gtk_widget_destroy (gtk_widget_get_parent (iter->data));
+    }
+  if (item->prev)
+    item->prev->next = iter;
+  else
+    xsheet->priv->effect_buttons = iter;
+  if (iter)
+    {
+      iter->prev->next = NULL;
+      iter->prev = item->prev;
+    }
+  item->prev = NULL;
+  g_list_free (item);
+
   /* Remove the comments field. */
   item = g_list_nth (xsheet->priv->comment_fields,
                      position);
@@ -877,6 +962,7 @@ static void
 animation_xsheet_reset_layout (AnimationXSheet *xsheet)
 {
   GtkWidget *frame;
+  GtkWidget *image;
   GtkWidget *label;
   gint       n_tracks;
   gint       n_frames;
@@ -887,6 +973,8 @@ animation_xsheet_reset_layout (AnimationXSheet *xsheet)
                          NULL);
   g_list_free_full (xsheet->priv->cels, (GDestroyNotify) g_list_free);
   xsheet->priv->cels = NULL;
+  g_list_free (xsheet->priv->effect_buttons);
+  xsheet->priv->effect_buttons = NULL;
   g_list_free (xsheet->priv->comment_fields);
   xsheet->priv->comment_fields = NULL;
   g_list_free (xsheet->priv->second_separators);
@@ -926,7 +1014,7 @@ animation_xsheet_reset_layout (AnimationXSheet *xsheet)
   /* Add 4 columns for track names and 1 row for frame numbers. */
   gtk_table_resize (GTK_TABLE (xsheet->priv->track_layout),
                     (guint) (n_frames + 2),
-                    (guint) (n_tracks * 9 + 6));
+                    (guint) (n_tracks * 9 + 7));
   animation_xsheet_add_frames (xsheet, 0, n_frames);
 
   /* Titles. */
@@ -935,13 +1023,20 @@ animation_xsheet_reset_layout (AnimationXSheet *xsheet)
       animation_xsheet_add_headers (xsheet, j);
     }
 
+  image = gtk_image_new_from_icon_name ("camera-video",
+                                        GTK_ICON_SIZE_SMALL_TOOLBAR);
+  gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout), image,
+                    1, 2, 1, 2,
+                    GTK_FILL, GTK_FILL, 0, 0);
+  gtk_widget_show (image);
+
   frame = gtk_frame_new (NULL);
   label = gtk_label_new (_("Comments"));
   gtk_container_add (GTK_CONTAINER (frame), label);
   gtk_widget_show (label);
   gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_OUT);
   gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout),
-                    frame, n_tracks * 9 + 1, n_tracks * 9 + 6, 1, 2,
+                    frame, n_tracks * 9 + 2, n_tracks * 9 + 7, 1, 2,
                     GTK_FILL, GTK_FILL, 0, 0);
   xsheet->priv->comments_title = frame;
   gtk_widget_show (frame);
@@ -1143,19 +1238,62 @@ on_animation_duration_changed (Animation       *animation,
 }
 
 static void
-on_animation_stopped (AnimationPlayback   *playback,
-                      AnimationXSheet     *xsheet)
+on_camera_keyframe_set (AnimationCamera *camera,
+                        gint             position,
+                        AnimationXSheet *xsheet)
+{
+  GtkWidget *button;
+
+  button = g_list_nth_data (xsheet->priv->effect_buttons,
+                            position);
+
+  if (button)
+    {
+      GtkWidget *image;
+
+      if (gtk_bin_get_child (GTK_BIN (button)))
+        {
+          gtk_container_remove (GTK_CONTAINER (button),
+                                gtk_bin_get_child (GTK_BIN (button)));
+        }
+      image = gtk_image_new_from_icon_name ("gtk-ok",
+                                            GTK_ICON_SIZE_SMALL_TOOLBAR);
+      gtk_container_add (GTK_CONTAINER (button), image);
+      gtk_widget_show (image);
+    }
+}
+
+static void
+on_camera_keyframe_deleted (AnimationCamera *camera,
+                            gint             position,
+                            AnimationXSheet *xsheet)
+{
+  GtkWidget *button;
+
+  button = g_list_nth_data (xsheet->priv->effect_buttons,
+                            position);
+
+  if (button && gtk_bin_get_child (GTK_BIN (button)))
+    {
+      gtk_container_remove (GTK_CONTAINER (button),
+                            gtk_bin_get_child (GTK_BIN (button)));
+    }
+}
+
+static void
+on_playback_stopped (AnimationPlayback *playback,
+                     AnimationXSheet   *xsheet)
 {
   animation_xsheet_jump (xsheet,
                          animation_playback_get_position (playback));
 }
 
 static void
-on_animation_rendered (AnimationPlayback *playback,
-                       gint               frame_number,
-                       GeglBuffer        *buffer,
-                       gboolean           must_draw_null,
-                       AnimationXSheet   *xsheet)
+on_playback_rendered (AnimationPlayback *playback,
+                      gint               frame_number,
+                      GeglBuffer        *buffer,
+                      gboolean           must_draw_null,
+                      AnimationXSheet   *xsheet)
 {
   animation_xsheet_jump (xsheet, frame_number);
 }
@@ -1339,6 +1477,23 @@ animation_xsheet_suite_cancelled (GtkWidget       *button,
 }
 
 static gboolean
+animation_xsheet_effect_clicked (GtkWidget       *button,
+                                 GdkEvent        *event,
+                                 AnimationXSheet *xsheet)
+{
+  gpointer position;
+
+  position = g_object_get_data (G_OBJECT (button), "frame-position");
+
+  animation_keyframe_view_show (ANIMATION_KEYFRAME_VIEW (xsheet->priv->keyframe_view),
+                                ANIMATION_CEL_ANIMATION (xsheet->priv->animation),
+                                GPOINTER_TO_INT (position));
+
+  /* All handled here. */
+  return TRUE;
+}
+
+static gboolean
 animation_xsheet_frame_clicked (GtkWidget       *button,
                                 GdkEvent        *event,
                                 AnimationXSheet *xsheet)
@@ -1604,7 +1759,8 @@ animation_xsheet_cel_clicked (GtkWidget       *button,
     }
   else if (event->type == GDK_BUTTON_RELEASE && event->button == 3)
     {
-      animation_menu_cell (xsheet->priv->animation, event,
+      animation_menu_cell (xsheet->priv->animation,
+                           event,
                            GPOINTER_TO_INT (position),
                            GPOINTER_TO_INT (track_num));
     }
@@ -1951,7 +2107,7 @@ animation_xsheet_attach_cel (AnimationXSheet *xsheet,
       gtk_container_add (GTK_CONTAINER (frame), cel);
     }
   gtk_table_attach (GTK_TABLE (xsheet->priv->track_layout),
-                    frame, track * 9 + 1, track * 9 + 10,
+                    frame, track * 9 + 2, track * 9 + 11,
                     pos + 2, pos + 3,
                     GTK_FILL, GTK_FILL, 0, 0);
   gtk_widget_show (frame);
diff --git a/plug-ins/animation-play/widgets/animation-xsheet.h 
b/plug-ins/animation-play/widgets/animation-xsheet.h
index 4a4dfba..1e90e13 100755
--- a/plug-ins/animation-play/widgets/animation-xsheet.h
+++ b/plug-ins/animation-play/widgets/animation-xsheet.h
@@ -48,6 +48,7 @@ GType       animation_xsheet_get_type (void) G_GNUC_CONST;
 
 GtkWidget * animation_xsheet_new      (AnimationCelAnimation *animation,
                                        AnimationPlayback     *playback,
-                                       GtkWidget             *layer_view);
+                                       GtkWidget             *layer_view,
+                                       GtkWidget             *keyframe_view);
 
 #endif  /*  __ANIMATION_XSHEET_H__  */


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