[gimp/wip/animation: 54/197] plug-ins: the legacy animation becomes "animatic" animation.



commit 39a6e2e179619687fd2075c5f1b1785c81fdea47
Author: Jehan <jehan girinstud io>
Date:   Sat Apr 9 13:45:16 2016 +0200

    plug-ins: the legacy animation becomes "animatic" animation.
    
    We show all layers as panels of a storyboard view, where one can choose
    each panel's duration, and whether it combines with the previous one.
    Layer name tagging will still be processed, for backward compatibility,
    but everything will be doable through a much nicer UI now.
    Changes of logics:
    - There aren't 2 modes now. By default, all panels just replace the
    previous one. Note that the button to combine exists but does not work
    yet.
    - Panels can now be commented. Current version has the UI, but neither
    comments nor any other settings are saved at this point.
    - Time is not given in ms as for tagging. Each panel duration is in
    frames, with a global framerate. Keeping irregular frame duration is
    not how animation works.

 plug-ins/animation-play/Makefile.am                |    6 +-
 plug-ins/animation-play/core/animation.c           |  134 +++--
 plug-ins/animation-play/core/animation.h           |   30 +-
 plug-ins/animation-play/core/animationanimatic.c   |  709 ++++++++++++++++++++
 plug-ins/animation-play/core/animationanimatic.h   |   66 ++
 plug-ins/animation-play/core/animationlegacy.c     |  473 -------------
 plug-ins/animation-play/core/animationlegacy.h     |   48 --
 plug-ins/animation-play/widgets/animation-dialog.c |  241 +++----
 .../animation-play/widgets/animation-storyboard.c  |  334 +++++++++
 .../animation-play/widgets/animation-storyboard.h  |   52 ++
 10 files changed, 1365 insertions(+), 728 deletions(-)
---
diff --git a/plug-ins/animation-play/Makefile.am b/plug-ins/animation-play/Makefile.am
index 763be80..944432e 100644
--- a/plug-ins/animation-play/Makefile.am
+++ b/plug-ins/animation-play/Makefile.am
@@ -45,10 +45,12 @@ LDADD = \
 animation_play_SOURCES = \
        core/animation.h                                \
        core/animation.c                                \
-       core/animationlegacy.h                          \
-       core/animationlegacy.c                          \
+       core/animationanimatic.h                \
+       core/animationanimatic.c                \
        widgets/animation-dialog.h              \
        widgets/animation-dialog.c              \
+       widgets/animation-storyboard.h  \
+       widgets/animation-storyboard.c  \
        animation-utils.h                               \
        animation-utils.c                               \
        animation-play.c
diff --git a/plug-ins/animation-play/core/animation.c b/plug-ins/animation-play/core/animation.c
index 6fe1758..3faefcd 100644
--- a/plug-ins/animation-play/core/animation.c
+++ b/plug-ins/animation-play/core/animation.c
@@ -28,7 +28,7 @@
 #include "animation-utils.h"
 
 #include "animation.h"
-#include "animationlegacy.h"
+#include "animationanimatic.h"
 
 enum
 {
@@ -63,7 +63,6 @@ struct _AnimationPrivate
 
   /* State of the currently loaded animation. */
   gint         position;
-  gint32       length;
   /* We want to allow animator to set any number as first frame,
    * for frame export capability. */
   guint        start_pos;
@@ -74,8 +73,10 @@ struct _AnimationPrivate
   /* Proxy settings generates a reload. */
   gdouble      proxy_ratio;
 
-  /* Frames are associated to an unused image (used as backend for GEGL buffers). */
   gboolean     loaded;
+
+  gint64       playback_start_time;
+  gint64       frames_played;
 };
 
 
@@ -95,7 +96,7 @@ static void       animation_get_property           (GObject      *object,
 
 /* Base implementation of virtual methods. */
 static gint       animation_real_get_start_position (Animation *animation);
-static gboolean   animation_real_identical_frames   (Animation *animation,
+static gboolean   animation_real_same               (Animation *animation,
                                                      gint       prev_pos,
                                                      gint       next_pos);
 
@@ -253,6 +254,8 @@ animation_class_init (AnimationClass *klass)
    * @animation: the animation.
    * @playback_start: the playback start frame.
    * @playback_stop: the playback last frame.
+   * @first_frame: the first frame.
+   * @length: the full length.
    *
    * The ::playback-range signal is emitted when the playback range is
    * updated.
@@ -265,7 +268,9 @@ animation_class_init (AnimationClass *klass)
                   NULL, NULL,
                   NULL,
                   G_TYPE_NONE,
-                  2,
+                  4,
+                  G_TYPE_INT,
+                  G_TYPE_INT,
                   G_TYPE_INT,
                   G_TYPE_INT);
   /**
@@ -310,7 +315,7 @@ animation_class_init (AnimationClass *klass)
   object_class->get_property = animation_get_property;
 
   klass->get_start_position  = animation_real_get_start_position;
-  klass->identical_frames    = animation_real_identical_frames;
+  klass->same                = animation_real_same;
 
   /**
    * Animation:image:
@@ -339,15 +344,13 @@ animation_init (Animation *animation)
 /************ Public Functions ****************/
 
 Animation *
-animation_new (gint32      image_id,
-               DisposeType disposal)
+animation_new (gint32   image_id,
+               gboolean xsheet)
 {
   Animation *animation;
 
-  /* Right now, only legacy animation type exists. */
-  animation = g_object_new (ANIMATION_TYPE_ANIMATION_LEGACY,
+  animation = g_object_new (ANIMATION_TYPE_ANIMATIC,
                             "image", image_id,
-                            "disposal", disposal,
                             NULL);
   return animation;
 }
@@ -367,6 +370,7 @@ animation_load (Animation *animation,
   AnimationPrivate *priv = ANIMATION_GET_PRIVATE (animation);
   GeglBuffer       *buffer;
   gint              width, height;
+  gint              length;
 
   priv->loaded = FALSE;
   g_signal_emit (animation, animation_signals[LOADING_START], 0);
@@ -378,17 +382,17 @@ animation_load (Animation *animation,
 
   priv->start_pos = ANIMATION_GET_CLASS (animation)->get_start_position (animation);
   priv->position  = priv->start_pos;
-  priv->length    = ANIMATION_GET_CLASS (animation)->get_length (animation);
+  length    = ANIMATION_GET_CLASS (animation)->get_length (animation);
 
   /* Default playback is the full range of frames. */
   priv->playback_start = priv->position;
-  priv->playback_stop  = priv->position + priv->length - 1;
+  priv->playback_stop  = priv->position + length - 1;
 
   priv->loaded = TRUE;
 
   animation_get_size (animation, &width, &height);
   g_signal_emit (animation, animation_signals[LOADED], 0,
-                 1, priv->length,
+                 1, length,
                  priv->playback_start,
                  priv->playback_stop,
                  width,
@@ -403,13 +407,14 @@ animation_load (Animation *animation,
     g_object_unref (buffer);
 }
 
-DisposeType
-animation_get_disposal (Animation *animation)
+gchar *
+animation_serialize (Animation   *animation)
 {
-  return ANIMATION_GET_CLASS (animation)->get_disposal (animation);
+  return ANIMATION_GET_CLASS (animation)->serialize (animation);
 }
 
-gint animation_get_start_position (Animation *animation)
+gint
+animation_get_start_position (Animation *animation)
 {
   return ANIMATION_GET_CLASS (animation)->get_start_position (animation);
 }
@@ -465,7 +470,7 @@ animation_play (Animation *animation)
   AnimationPrivate *priv = ANIMATION_GET_PRIVATE (animation);
   gint              duration;
 
-  duration = ANIMATION_GET_CLASS (animation)->time_to_next (animation);
+  duration = (gint) (1000.0 / animation_get_framerate (animation));
 
   if (priv->playback_timer)
     {
@@ -476,11 +481,8 @@ animation_play (Animation *animation)
       g_source_remove (priv->playback_timer);
     }
 
-  if (duration <= 0)
-    {
-      gdouble fps = animation_get_framerate (animation);
-      duration = (gint) (1000.0 / fps);
-    }
+  priv->playback_start_time = g_get_monotonic_time ();
+  priv->frames_played = 1;
 
   priv->playback_timer = g_timeout_add ((guint) duration,
                                         (GSourceFunc) animation_advance_frame_callback,
@@ -508,19 +510,21 @@ animation_next (Animation *animation)
   AnimationPrivate *priv         = ANIMATION_GET_PRIVATE (animation);
   GeglBuffer       *buffer       = NULL;
   gint              previous_pos = priv->position;
+  gboolean          identical;
 
   priv->position = animation_get_playback_start (animation) +
                    ((priv->position - animation_get_playback_start (animation) + 1) %
                     (animation_get_playback_stop (animation) - animation_get_playback_start (animation) + 
1));
 
-  if (! ANIMATION_GET_CLASS (animation)->identical_frames (animation,
-                                                           previous_pos,
-                                                           priv->position))
+  identical = ANIMATION_GET_CLASS (animation)->same (animation,
+                                                     previous_pos,
+                                                     priv->position);
+  if (! identical)
     {
       buffer = animation_get_frame (animation, priv->position);
     }
   g_signal_emit (animation, animation_signals[RENDER], 0,
-                 priv->position, buffer, FALSE);
+                 priv->position, buffer, ! identical);
   if (buffer != NULL)
     {
       g_object_unref (buffer);
@@ -533,6 +537,7 @@ animation_prev (Animation *animation)
   AnimationPrivate *priv     = ANIMATION_GET_PRIVATE (animation);
   GeglBuffer       *buffer   = NULL;
   gint              prev_pos = priv->position;
+  gboolean          identical;
 
   if (priv->position == animation_get_playback_start (animation))
     {
@@ -543,14 +548,15 @@ animation_prev (Animation *animation)
       --priv->position;
     }
 
-  if (! ANIMATION_GET_CLASS (animation)->identical_frames (animation,
-                                                           prev_pos,
-                                                           priv->position))
+  identical = ANIMATION_GET_CLASS (animation)->same (animation,
+                                                     prev_pos,
+                                                     priv->position);
+  if (! identical)
     {
       buffer = animation_get_frame (animation, priv->position);
     }
   g_signal_emit (animation, animation_signals[RENDER], 0,
-                 priv->position, buffer, FALSE);
+                 priv->position, buffer, ! identical);
   if (buffer)
     g_object_unref (buffer);
 }
@@ -562,6 +568,7 @@ animation_jump (Animation *animation,
   AnimationPrivate *priv           = ANIMATION_GET_PRIVATE (animation);
   GeglBuffer       *buffer         = NULL;
   gint              prev_pos = priv->position;
+  gboolean          identical;
 
   if (index < priv->playback_start ||
       index > priv->playback_stop)
@@ -569,14 +576,15 @@ animation_jump (Animation *animation,
   else
     priv->position = index;
 
-  if (! ANIMATION_GET_CLASS (animation)->identical_frames (animation,
-                                                           prev_pos,
-                                                           priv->position))
+  identical = ANIMATION_GET_CLASS (animation)->same (animation,
+                                                     prev_pos,
+                                                     priv->position);
+  if (! identical)
     {
       buffer = animation_get_frame (animation, priv->position);
     }
   g_signal_emit (animation, animation_signals[RENDER], 0,
-                 priv->position, buffer, FALSE);
+                 priv->position, buffer, ! identical);
   if (buffer)
     g_object_unref (buffer);
 }
@@ -586,9 +594,12 @@ animation_set_playback_start (Animation *animation,
                               gint       frame_number)
 {
   AnimationPrivate *priv = ANIMATION_GET_PRIVATE (animation);
+  gint              length;
+
+  length = animation_get_length (animation);
 
   if (frame_number < priv->start_pos ||
-      frame_number >= priv->start_pos + priv->length)
+      frame_number >= priv->start_pos + length)
     {
       priv->playback_start = priv->start_pos;
     }
@@ -598,11 +609,12 @@ animation_set_playback_start (Animation *animation,
     }
   if (priv->playback_stop < priv->playback_start)
     {
-      priv->playback_stop = priv->start_pos + priv->length - 1;
+      priv->playback_stop = priv->start_pos + length - 1;
     }
 
   g_signal_emit (animation, animation_signals[PLAYBACK_RANGE], 0,
-                 priv->playback_start, priv->playback_stop);
+                 priv->playback_start, priv->playback_stop,
+                 priv->start_pos, length);
 
   if (priv->position < priv->playback_start ||
       priv->position > priv->playback_stop)
@@ -624,11 +636,14 @@ animation_set_playback_stop (Animation *animation,
                              gint       frame_number)
 {
   AnimationPrivate *priv = ANIMATION_GET_PRIVATE (animation);
+  gint              length;
+
+  length = animation_get_length (animation);
 
   if (frame_number < priv->start_pos ||
-      frame_number >= priv->start_pos + priv->length)
+      frame_number >= priv->start_pos + length)
     {
-      priv->playback_stop = priv->start_pos + priv->length - 1;
+      priv->playback_stop = priv->start_pos + length - 1;
     }
   else
     {
@@ -639,7 +654,8 @@ animation_set_playback_stop (Animation *animation,
       priv->playback_start = priv->start_pos;
     }
   g_signal_emit (animation, animation_signals[PLAYBACK_RANGE], 0,
-                 priv->playback_start, priv->playback_stop);
+                 priv->playback_start, priv->playback_stop,
+                 priv->start_pos, length);
 
   if (priv->position < priv->playback_start ||
       priv->position > priv->playback_stop)
@@ -738,9 +754,9 @@ gint animation_real_get_start_position (Animation *animation)
 }
 
 static gboolean
-animation_real_identical_frames (Animation *animation,
-                                 gint       previous_pos,
-                                 gint       next_pos)
+animation_real_same (Animation *animation,
+                     gint       previous_pos,
+                     gint       next_pos)
 {
   /* By default all frames are supposed different. */
   return (previous_pos == next_pos);
@@ -750,16 +766,34 @@ static gboolean
 animation_advance_frame_callback (Animation *animation)
 {
   AnimationPrivate *priv = ANIMATION_GET_PRIVATE (animation);
-  gint              duration;
+  gint64            duration;
+  gint64            duration_since_start;
+  static gboolean   prev_low_framerate = FALSE;
 
   animation_next (animation);
-  duration = ANIMATION_GET_CLASS (animation)->time_to_next (animation);
+  duration = (gint) (1000.0 / animation_get_framerate (animation));
+
+  duration_since_start = (g_get_monotonic_time () - priv->playback_start_time) / 1000;
+  duration = duration - (duration_since_start - priv->frames_played * duration);
 
-  if (duration <= 0)
+  if (duration < 1)
+    {
+      if (prev_low_framerate)
+        {
+          /* Let's only warn the user for several subsequent slow frames. */
+          gdouble real_framerate = (gdouble) priv->frames_played * 1000.0 / duration_since_start;
+          if (real_framerate < priv->framerate)
+            g_signal_emit (animation, animation_signals[LOW_FRAMERATE], 0,
+                           real_framerate);
+        }
+      duration = 1;
+      prev_low_framerate = TRUE;
+    }
+  else
     {
-      gdouble fps = animation_get_framerate (animation);
-      duration = (gint) (1000.0 / fps);
+      prev_low_framerate = FALSE;
     }
+  priv->frames_played++;
 
   priv->playback_timer = g_timeout_add ((guint) duration,
                                         (GSourceFunc) animation_advance_frame_callback,
diff --git a/plug-ins/animation-play/core/animation.h b/plug-ins/animation-play/core/animation.h
index 9b701f8..21d9418 100644
--- a/plug-ins/animation-play/core/animation.h
+++ b/plug-ins/animation-play/core/animation.h
@@ -31,18 +31,6 @@
 typedef struct _Animation      Animation;
 typedef struct _AnimationClass AnimationClass;
 
-typedef enum
-{
-  /* Each layer is combined over the previous frame. */
-  DISPOSE_COMBINE  = 0x00,
-  /* Each layer is a frame. */
-  DISPOSE_REPLACE  = 0x01,
-  /* Custom disposal through timeline. */
-  DISPOSE_XSHEET   = 0x02,
-  /* Keep the current disposal */
-  DISPOSE_KEEP     = 0x03,
-} DisposeType;
-
 struct _Animation
 {
   GObject      parent_instance;
@@ -56,38 +44,36 @@ struct _AnimationClass
   gint         (*get_start_position) (Animation *animation);
 
   /* Defaults to returning FALSE for any different position. */
-  gboolean     (*identical_frames) (Animation *animation,
-                                    gint       prev_pos,
-                                    gint       next_pos);
+  gboolean     (*same)               (Animation *animation,
+                                      gint       prev_pos,
+                                      gint       next_pos);
 
   /* These virtual methods must be implemented by any subclass. */
   void         (*load)          (Animation  *animation,
                                  gdouble     proxy_ratio);
-  DisposeType  (*get_disposal)  (Animation   *animation);
   gint         (*get_length)    (Animation   *animation);
-  gint         (*next)          (Animation   *animation);
-  gint         (*prev)          (Animation   *animation);
 
 
   void         (*get_size)      (Animation   *animation,
                                  gint        *width,
                                  gint        *height);
 
-  gint         (*time_to_next)  (Animation   *animation);
-
   GeglBuffer * (*get_frame)     (Animation   *animation,
                                  gint         pos);
+
+  gchar      * (*serialize)     (Animation   *animation);
 };
 
 GType         animation_get_type (void);
 
 Animation   * animation_new                (gint32       image_id,
-                                            DisposeType  disposal);
+                                            gboolean     xsheet);
 gint32        animation_get_image_id       (Animation   *animation);
 
 void          animation_load               (Animation   *animation,
                                             gdouble      proxy_ratio);
-DisposeType   animation_get_disposal       (Animation   *animation);
+
+gchar       * animation_serialize          (Animation   *animation);
 
 gint          animation_get_start_position (Animation   *animation);
 gint          animation_get_position       (Animation   *animation);
diff --git a/plug-ins/animation-play/core/animationanimatic.c 
b/plug-ins/animation-play/core/animationanimatic.c
new file mode 100644
index 0000000..060aecf
--- /dev/null
+++ b/plug-ins/animation-play/core/animationanimatic.c
@@ -0,0 +1,709 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation.c
+ * Copyright (C) 2016 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 "animationanimatic.h"
+
+enum
+{
+  IMAGE_DURATION,
+  LAST_SIGNAL
+};
+
+typedef struct _AnimationAnimaticPrivate AnimationAnimaticPrivate;
+
+struct _AnimationAnimaticPrivate
+{
+  gint         preview_width;
+  gint         preview_height;
+
+  /* Panel images are associated to an unused image (used as backend
+   * for GEGL buffers). */
+  gint32       panels;
+  /* The number of panels. */
+  gint         n_panels;
+  /* Layers associated to each panel. For serialization. */
+  gint        *tattoos;
+  /* The duration of each panel in frames. */
+  gint        *durations;
+  /* Whether a panel should get blended together with previous panel. */
+  gboolean    *combine;
+  /* Panel comments. */
+  gchar      **comments;
+};
+
+#define GET_PRIVATE(animation) \
+        G_TYPE_INSTANCE_GET_PRIVATE (animation, \
+                                     ANIMATION_TYPE_ANIMATIC, \
+                                     AnimationAnimaticPrivate)
+
+static void         animation_animatic_finalize   (GObject           *object);
+
+/* Virtual methods */
+
+static gint         animation_animatic_get_length (Animation         *animation);
+static void         animation_animatic_get_size   (Animation         *animation,
+                                                   gint              *width,
+                                                   gint              *height);
+
+static void         animation_animatic_load       (Animation         *animation,
+                                                   gdouble            proxy_ratio);
+static GeglBuffer * animation_animatic_get_frame  (Animation         *animation,
+                                                   gint               pos);
+static gchar      * animation_animatic_serialize  (Animation         *animation);
+static gboolean     animation_animatic_same       (Animation         *animation,
+                                                   gint               previous_pos,
+                                                   gint               next_pos);
+
+/* Utils */
+
+static gint         animation_animatic_get_layer  (AnimationAnimatic *animation,
+                                                   gint               pos);
+
+/* Tag handling (from layer names) */
+
+static gint         parse_ms_tag                  (Animation         *animation,
+                                                   const gchar       *str);
+static gboolean     parse_combine_tag             (const gchar       *str);
+
+static gboolean     is_ms_tag                     (const gchar       *str,
+                                                   gint              *duration,
+                                                   gint              *taglength);
+
+G_DEFINE_TYPE (AnimationAnimatic, animation_animatic, ANIMATION_TYPE_ANIMATION)
+
+#define parent_class animation_animatic_parent_class
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static void
+animation_animatic_class_init (AnimationAnimaticClass *klass)
+{
+  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
+  AnimationClass *anim_class   = ANIMATION_CLASS (klass);
+
+  /**
+   * AnimationAnimatic::image-duration:
+   * @animatic: the #AnimationAnimatic.
+   * @layer_id: the #GimpLayer id.
+   * @duration: the new duration for @layer_id (in number of panels).
+   *
+   * The ::image-duration will be emitted when the duration of a layer
+   * changes. It can be %0 meaning that this layer should not be shown
+   * in the reel.
+   */
+  signals[IMAGE_DURATION] =
+    g_signal_new ("image-duration",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_INT,
+                  G_TYPE_INT);
+
+  object_class->finalize = animation_animatic_finalize;
+
+  anim_class->get_length = animation_animatic_get_length;
+  anim_class->get_size   = animation_animatic_get_size;
+  anim_class->load       = animation_animatic_load;
+  anim_class->get_frame  = animation_animatic_get_frame;
+  anim_class->serialize  = animation_animatic_serialize;
+  anim_class->same       = animation_animatic_same;
+
+  g_type_class_add_private (klass, sizeof (AnimationAnimaticPrivate));
+}
+
+static void
+animation_animatic_init (AnimationAnimatic *animation)
+{
+}
+
+static void
+animation_animatic_finalize (GObject *object)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (object);
+
+  gimp_image_delete (priv->panels);
+  if (priv->tattoos)
+    g_free (priv->tattoos);
+  if (priv->durations)
+    g_free (priv->durations);
+  if (priv->combine)
+    g_free (priv->combine);
+  if (priv->comments)
+    {
+      gint i;
+
+      for (i = 0; i < priv->n_panels; i++)
+        {
+          g_free (priv->comments[i]);
+        }
+      g_free (priv->comments);
+    }
+
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+/**** Public Functions ****/
+
+void
+animation_animatic_set_duration (AnimationAnimatic *animatic,
+                                 gint               panel_num,
+                                 gint               duration)
+{
+  AnimationAnimaticPrivate *priv           = GET_PRIVATE (animatic);
+  Animation                *animation      = ANIMATION (animatic);
+  gint                      prev_length    = animation_get_length (animation);
+  gint                      playback_start = animation_get_playback_start (animation);
+  gint                      playback_stop  = animation_get_playback_stop (animation);
+  gint                      position       = animation_get_position (animation);
+  gint                      layer_id;
+  gint                      length;
+
+  g_return_if_fail (duration >= 0  &&
+                    panel_num > 0 &&
+                    panel_num <= priv->n_panels);
+
+  layer_id = animation_animatic_get_layer (animatic, position);
+
+  priv->durations[panel_num - 1] = duration;
+  length = animation_get_length (animation);
+
+  if (playback_start > length)
+    {
+      playback_start = animation_get_start_position (animation);
+    }
+  if (playback_stop > length ||
+      playback_stop == prev_length)
+    {
+      playback_stop = length;
+    }
+  g_signal_emit (animatic, signals[IMAGE_DURATION], 0,
+                 panel_num, duration);
+  g_signal_emit_by_name (animatic, "playback-range",
+                         playback_start, playback_stop,
+                         animation_get_start_position (animation),
+                         animation_get_length (animation));
+  if (position > length)
+    {
+      animation_jump (animation, length);
+    }
+  else if (layer_id != animation_animatic_get_layer (animatic, position))
+    {
+      GeglBuffer *buffer;
+
+      buffer = animation_get_frame (animation, position);
+      g_signal_emit_by_name (animation, "render",
+                             position, buffer, TRUE);
+      if (buffer)
+        g_object_unref (buffer);
+    }
+}
+
+gint
+animation_animatic_get_duration (AnimationAnimatic *animatic,
+                                 gint               panel_num)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animatic);
+
+  g_return_val_if_fail (panel_num > 0 &&
+                        panel_num <= priv->n_panels,
+                        0);
+
+  return priv->durations[panel_num - 1];
+}
+
+void
+animation_animatic_set_comment (AnimationAnimatic *animatic,
+                                gint               panel_num,
+                                const gchar       *comment)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animatic);
+
+  g_return_if_fail (panel_num > 0 &&
+                    panel_num <= priv->n_panels);
+
+  if (priv->comments[panel_num - 1])
+    g_free (priv->comments[panel_num - 1]);
+
+  priv->comments[panel_num - 1] = g_strdup (comment);
+}
+
+const gchar *
+animation_animatic_get_comment (AnimationAnimatic *animatic,
+                                gint               panel_num)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animatic);
+
+  g_return_val_if_fail (panel_num > 0 &&
+                        panel_num <= priv->n_panels,
+                        0);
+  return priv->comments[panel_num - 1];
+}
+
+void
+animation_animatic_set_combine (AnimationAnimatic *animatic,
+                                gint               panel_num,
+                                gboolean           combine)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animatic);
+
+  g_return_if_fail (panel_num > 0 &&
+                    panel_num <= priv->n_panels);
+
+  priv->combine[panel_num] = combine;
+}
+
+const gboolean
+animation_animatic_get_combine (AnimationAnimatic *animatic,
+                                gint               panel_num)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animatic);
+
+  g_return_val_if_fail (panel_num > 0 &&
+                        panel_num <= priv->n_panels,
+                        0);
+  return priv->combine[panel_num - 1];
+}
+
+/**** Virtual methods ****/
+
+static gint
+animation_animatic_get_length (Animation *animation)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animation);
+  gint                      count = 0;
+  gint                      i ;
+
+  for (i = 0; i < priv->n_panels; i++)
+    {
+      count += priv->durations[i];
+    }
+
+  return count;
+}
+
+static void
+animation_animatic_get_size (Animation *animation,
+                             gint      *width,
+                             gint      *height)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animation);
+
+  *width  = priv->preview_width;
+  *height = priv->preview_height;
+}
+
+static void
+animation_animatic_load (Animation *animation,
+                         gdouble    proxy_ratio)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animation);
+  gint32                   *layers;
+  gint32                    image_id;
+  gint32                    new_frame;
+  gint32                    previous_frame = 0;
+  gint                      image_width;
+  gint                      image_height;
+  gint                      i;
+
+  g_return_if_fail (proxy_ratio > 0.0 && proxy_ratio <= 1.0);
+
+  /* Cleaning. */
+  if (gimp_image_is_valid (priv->panels))
+    {
+      gimp_image_delete (priv->panels);
+      g_free (priv->tattoos);
+      g_free (priv->durations);
+      g_free (priv->combine);
+
+      for (i = 0; i < priv->n_panels; i++)
+        {
+          g_free (priv->comments[i]);
+        }
+      g_free (priv->comments);
+    }
+
+  image_id = animation_get_image_id (animation);
+  layers   = gimp_image_get_layers (image_id, &priv->n_panels);
+
+  priv->tattoos   = g_try_malloc0_n (priv->n_panels, sizeof (gint));
+  priv->durations = g_try_malloc0_n (priv->n_panels, sizeof (gint));
+  priv->combine   = g_try_malloc0_n (priv->n_panels, sizeof (gboolean));
+  priv->comments  = g_try_malloc0_n (priv->n_panels, sizeof (gchar*));
+  if (! priv->tattoos || ! priv->durations ||
+      ! priv->combine || ! priv->comments)
+    {
+      gimp_message (_("Memory could not be allocated to the animatic."));
+      gimp_quit ();
+      return;
+    }
+
+  /* We default at full preview size. */
+  image_width  = gimp_image_width (image_id);
+  image_height = gimp_image_height (image_id);
+
+  priv->preview_width = image_width;
+  priv->preview_height = image_height;
+
+  /* Apply proxy ratio. */
+  priv->preview_width  *= proxy_ratio;
+  priv->preview_height *= proxy_ratio;
+
+  priv->panels = gimp_image_new (priv->preview_width,
+                                 priv->preview_width,
+                                 GIMP_RGB);
+
+  gimp_image_undo_disable (priv->panels);
+
+  for (i = 0; i < priv->n_panels; i++)
+    {
+      gchar       *layer_name;
+      gint         duration;
+      gboolean     combine;
+      gint         layer_offx;
+      gint         layer_offy;
+
+      g_signal_emit_by_name (animation, "loading",
+                             (gdouble) i / ((gdouble) priv->n_panels - 0.999));
+
+      layer_name = gimp_item_get_name (layers[priv->n_panels - (i + 1)]);
+
+      duration = parse_ms_tag (animation, layer_name);
+      combine  = parse_combine_tag (layer_name);
+
+      /* Frame duration. */
+      priv->tattoos[i]   = gimp_item_get_tattoo (layers[priv->n_panels - (i + 1)]);
+      priv->durations[i] = duration;
+      priv->combine[i]   = combine;
+      /* Layer names are used as default comments. */
+      priv->comments[i]  = layer_name;
+
+      /* Frame disposal. */
+      if (i > 0 && combine)
+        {
+          previous_frame = gimp_layer_copy (previous_frame);
+          gimp_image_insert_layer (priv->panels, previous_frame, 0, 0);
+          gimp_item_set_visible (previous_frame, TRUE);
+        }
+
+      new_frame = gimp_layer_new_from_drawable (layers[priv->n_panels - (i + 1)],
+                                                priv->panels);
+      gimp_image_insert_layer (priv->panels, new_frame, 0, 0);
+      gimp_layer_scale (new_frame,
+                        (gimp_drawable_width (layers[priv->n_panels - (i + 1)]) * (gint) 
priv->preview_width) / image_width,
+                        (gimp_drawable_height (layers[priv->n_panels - (i + 1)]) * (gint) 
priv->preview_height) / image_height,
+                        FALSE);
+      gimp_drawable_offsets (layers[priv->n_panels - (i + 1)], &layer_offx, &layer_offy);
+      gimp_layer_set_offsets (new_frame,
+                              (layer_offx * (gint) priv->preview_width) / image_width,
+                              (layer_offy * (gint) priv->preview_height) / image_height);
+      gimp_layer_resize_to_image_size (new_frame);
+      gimp_item_set_visible (new_frame, TRUE);
+      new_frame = gimp_image_merge_visible_layers (priv->panels, GIMP_CLIP_TO_IMAGE);
+      gimp_item_set_visible (new_frame, FALSE);
+
+      previous_frame = new_frame;
+    }
+  g_free (layers);
+}
+
+static GeglBuffer *
+animation_animatic_get_frame (Animation *animation,
+                              gint       pos)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animation);
+  GeglBuffer               *buffer = NULL;
+
+  if (priv->panels)
+    {
+      gint32 *layers;
+      gint32  num_layers;
+      gint    count = 0;
+
+      layers = gimp_image_get_layers (priv->panels, &num_layers);
+
+      if (num_layers > 0 &&
+          pos >= 1       &&
+          pos <= animation_animatic_get_length (animation))
+        {
+          gint i ;
+
+          for (i = 0; i < num_layers; i++)
+            {
+              count += priv->durations[i];
+              if (count >= pos)
+                break;
+            }
+
+          buffer = gimp_drawable_get_buffer (layers[num_layers - i - 1]);
+        }
+
+      g_free (layers);
+    }
+
+  return buffer;
+}
+
+static gchar *
+animation_animatic_serialize (Animation *animation)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animation);
+  gchar                    *text;
+  gchar                    *tmp;
+  gint                      i;
+
+  text = g_strdup_printf ("<animation framerate=\"%f\" type=\"animatic\"><sequence>",
+                          animation_get_framerate (animation));
+  for (i = 0; i < priv->n_panels; i++)
+    {
+      gchar  *panel;
+      gchar  *layer_name;
+      gint32 *panels;
+      gint    n_panels;
+
+      panels = gimp_image_get_layers (priv->panels, &n_panels);
+      layer_name = gimp_item_get_name (panels[n_panels - (i + 1)]);
+
+      panel = g_markup_printf_escaped ("<panel title=\"%s\" duration=\"%d\" layer=\"%d\">",
+                                       layer_name, priv->durations[i],
+                                       priv->tattoos[i]);
+      g_free (layer_name);
+
+      tmp = text;
+      text = g_strconcat (text, panel, NULL);
+      g_free (tmp);
+      g_free (panel);
+
+      if (priv->comments[i])
+        {
+          gchar *comment;
+
+          comment = g_markup_printf_escaped ("<notes title=\"Notes\">%s</notes>",
+                                             priv->comments[i]);
+          tmp = text;
+          text = g_strconcat (text, comment, NULL);
+          g_free (tmp);
+          g_free (comment);
+        }
+      tmp = text;
+      text = g_strconcat (text, "</panel>", NULL);
+      g_free (tmp);
+    }
+  tmp = text;
+  text = g_strconcat (text, "</sequence></animation>", NULL);
+  g_free (tmp);
+
+  return text;
+}
+
+static gboolean
+animation_animatic_same (Animation *animation,
+                         gint       previous_pos,
+                         gint       next_pos)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animation);
+  gint                      count = 0;
+  gboolean                  identical = FALSE;
+  gint                      i ;
+
+  for (i = 0; i < priv->n_panels; i++)
+    {
+      count += priv->durations[i];
+      if (count >= previous_pos && count >= next_pos)
+        {
+          identical = TRUE;
+          break;
+        }
+      else if (count >= previous_pos || count >= next_pos)
+        {
+          identical = FALSE;
+          break;
+        }
+    }
+
+  return identical;
+}
+
+/**** Utils ****/
+
+static gint
+animation_animatic_get_layer (AnimationAnimatic *animation,
+                              gint               pos)
+{
+  AnimationAnimaticPrivate *priv = GET_PRIVATE (animation);
+  gint                      i = -1;
+
+  if (priv->panels)
+    {
+      gint32 *layers;
+      gint32  num_layers;
+      gint    count = 0;
+
+      layers = gimp_image_get_layers (priv->panels, &num_layers);
+
+      if (num_layers > 0 &&
+          pos >= 1       &&
+          pos <= animation_animatic_get_length (ANIMATION (animation)))
+        {
+          for (i = num_layers - 1; i >= 0; i--)
+            {
+              count += priv->durations[i];
+              if (count >= pos)
+                break;
+            }
+        }
+
+      g_free (layers);
+    }
+
+  return i;
+}
+
+/**** TAG UTILS ****/
+
+static gint
+parse_ms_tag (Animation   *animation,
+              const gchar *str)
+{
+  if (str != NULL)
+    {
+      gint length = strlen (str);
+      gint i;
+
+      for (i = 0; i < length; i++)
+        {
+          gint duration;
+          gint dummy;
+
+          if (is_ms_tag (&str[i], &duration, &dummy))
+            {
+              gdouble fps = animation_get_framerate (animation);
+
+              /* Get frame duration in frame numbers, not millisecond. */
+              duration = (gint) ((fps * (gdouble) duration) / 1000.0);
+
+              return duration;
+            }
+        }
+    }
+
+  /* Default to 6 frames per panel.
+   * Storyboard-type animations are rarely detailed. */
+  return 6;
+}
+
+static gboolean
+parse_combine_tag (const gchar *str)
+{
+  gboolean combine = FALSE;
+
+  if (str != NULL)
+    {
+      gint length = strlen (str);
+      gint i;
+
+      for (i = 0; i < length; i++)
+        {
+          if (strlen (str) != 9)
+            continue;
+
+          if (strncmp (str, "(combine)", 9) == 0)
+            {
+              combine = TRUE;
+              break;
+            }
+          else if (strncmp (str, "(replace)", 9) == 0)
+            {
+              combine = FALSE;
+              break;
+            }
+        }
+    }
+
+  return combine;
+}
+
+static gboolean
+is_ms_tag (const gchar *str,
+           gint        *duration,
+           gint        *taglength)
+{
+  gint sum = 0;
+  gint offset;
+  gint length;
+
+  length = strlen(str);
+
+  if (str[0] != '(')
+    return FALSE;
+
+  offset = 1;
+
+  /* eat any spaces between open-parenthesis and number */
+  while ((offset < length) && (str[offset] == ' '))
+    offset++;
+
+  if ((offset>=length) || (!g_ascii_isdigit (str[offset])))
+    return FALSE;
+
+  do
+    {
+      sum *= 10;
+      sum += str[offset] - '0';
+      offset++;
+    }
+  while ((offset<length) && (g_ascii_isdigit (str[offset])));
+
+  if (length - offset <= 2)
+    return FALSE;
+
+  /* eat any spaces between number and 'ms' */
+  while ((offset < length) && (str[offset] == ' '))
+    offset++;
+
+  if (length - offset <= 2                     ||
+      g_ascii_toupper (str[offset])     != 'M' ||
+      g_ascii_toupper (str[offset + 1]) != 'S')
+    return FALSE;
+
+  offset += 2;
+
+  /* eat any spaces between 'ms' and close-parenthesis */
+  while ((offset < length) && (str[offset] == ' '))
+    offset++;
+
+  if ((length - offset < 1) || (str[offset] != ')'))
+    return FALSE;
+
+  offset++;
+
+  *duration = sum;
+  *taglength = offset;
+
+  return TRUE;
+}
diff --git a/plug-ins/animation-play/core/animationanimatic.h 
b/plug-ins/animation-play/core/animationanimatic.h
new file mode 100644
index 0000000..f2e084c
--- /dev/null
+++ b/plug-ins/animation-play/core/animationanimatic.h
@@ -0,0 +1,66 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation.h
+ * Copyright (C) 2016 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_ANIMATIC_H__
+#define __ANIMATION_ANIMATIC_H__
+
+#include "animation.h"
+
+#define ANIMATION_TYPE_ANIMATIC            (animation_animatic_get_type ())
+#define ANIMATION_ANIMATIC(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), ANIMATION_TYPE_ANIMATIC, 
AnimationAnimatic))
+#define ANIMATION_ANIMATIC_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), ANIMATION_TYPE_ANIMATIC, 
AnimationAnimaticClass))
+#define ANIMATION_IS_ANIMATIC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ANIMATION_TYPE_ANIMATIC))
+#define ANIMATION_IS_ANIMATIC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ANIMATION_TYPE_ANIMATIC))
+#define ANIMATION_ANIMATIC_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), ANIMATION_TYPE_ANIMATIC, 
AnimationAnimaticClass))
+
+typedef struct _AnimationAnimatic      AnimationAnimatic;
+typedef struct _AnimationAnimaticClass AnimationAnimaticClass;
+
+struct _AnimationAnimatic
+{
+  Animation       parent_instance;
+};
+
+struct _AnimationAnimaticClass
+{
+  AnimationClass  parent_class;
+};
+
+GType            animation_animatic_get_type (void);
+
+void             animation_animatic_set_duration (AnimationAnimatic *animatic,
+                                                  gint               panel_num,
+                                                  gint               duration);
+gint             animation_animatic_get_duration (AnimationAnimatic *animatic,
+                                                  gint               panel_num);
+
+void             animation_animatic_set_comment  (AnimationAnimatic *animatic,
+                                                  gint               panel_num,
+                                                  const gchar       *comment);
+const gchar    * animation_animatic_get_comment  (AnimationAnimatic *animatic,
+                                                  gint               panel_num);
+
+void             animation_animatic_set_combine  (AnimationAnimatic *animatic,
+                                                  gint               panel_num,
+                                                  gboolean           combine);
+const gboolean   animation_animatic_get_combine  (AnimationAnimatic *animatic,
+                                                  gint               panel_num);
+
+#endif  /*  __ANIMATION_ANIMATIC_H__  */
diff --git a/plug-ins/animation-play/widgets/animation-dialog.c 
b/plug-ins/animation-play/widgets/animation-dialog.c
index ea1ba62..416ee73 100755
--- a/plug-ins/animation-play/widgets/animation-dialog.c
+++ b/plug-ins/animation-play/widgets/animation-dialog.c
@@ -31,8 +31,12 @@
 #include <libgimp/gimpui.h>
 
 #include "animation-utils.h"
+
 #include "core/animation.h"
+#include "core/animationanimatic.h"
+
 #include "animation-dialog.h"
+#include "animation-storyboard.h"
 
 #include "libgimp/stdplugins-intl.h"
 
@@ -40,11 +44,10 @@
 #define DITHERTYPE     GDK_RGB_DITHER_NORMAL
 
 /* Settings we cache assuming they may be the user's
- * favorite, like a framerate, or a type of frame disposal.
+ * favorite, like a framerate.
  * These will be used only for image without stored animation. */
 typedef struct
 {
-  DisposeType disposal;
   gdouble     framerate;
 }
 CachedSettings;
@@ -75,7 +78,6 @@ struct _AnimationDialogPrivate
   GtkWidget      *progress_bar;
   GtkWidget      *settings_bar;
 
-  GtkWidget      *disposalcombo;
   GtkWidget      *fpscombo;
   GtkWidget      *zoomcombo;
   GtkWidget      *proxycheckbox;
@@ -99,6 +101,9 @@ struct _AnimationDialogPrivate
   guint           shape_drawing_area_width;
   guint           shape_drawing_area_height;
 
+  /* The hpaned (left is preview, right is layer list. */
+  GtkWidget      *hpaned;
+
   /* The vpaned (bottom is timeline, above is preview). */
   GtkWidget      *vpaned;
 
@@ -148,8 +153,6 @@ static void        close_callback            (GtkAction        *action,
 static void        help_callback             (GtkAction        *action,
                                               AnimationDialog  *dialog);
 
-static void        disposalcombo_changed     (GtkWidget        *combo,
-                                              AnimationDialog  *dialog);
 static void        fpscombo_activated        (GtkEntry         *combo,
                                               AnimationDialog  *dialog);
 static void        fpscombo_changed          (GtkWidget        *combo,
@@ -206,6 +209,12 @@ static gboolean    popup_menu                (GtkWidget        *widget,
 static void        show_loading_progress     (Animation        *animation,
                                               gdouble           load_rate,
                                               AnimationDialog  *dialog);
+static void        animation_update_progress (Animation        *animation,
+                                              gint              first_frame,
+                                              gint              num_frames,
+                                              gint              playback_start,
+                                              gint              playback_stop,
+                                              AnimationDialog  *dialog);
 static void        block_ui                  (Animation        *animation,
                                               AnimationDialog  *dialog);
 static void        unblock_ui                (Animation        *animation,
@@ -219,6 +228,8 @@ static void        unblock_ui                (Animation        *animation,
 static void        playback_range_changed    (Animation        *animation,
                                               gint              playback_start,
                                               gint              playback_stop,
+                                              gint              start_pos,
+                                              gint              length,
                                               AnimationDialog  *dialog);
 static void        proxy_changed             (Animation        *animation,
                                               gdouble           fps,
@@ -324,7 +335,7 @@ animation_dialog_init (AnimationDialog *dialog)
   priv->shape_drawing_area_height = -1;
 }
 
-/************ Public Functions ****************/
+/**** Public Functions ****/
 
 GtkWidget *
 animation_dialog_new (gint32 image_id)
@@ -335,21 +346,12 @@ animation_dialog_new (gint32 image_id)
   CachedSettings  settings;
 
   /* Acceptable default settings. */
-  settings.disposal  = DISPOSE_COMBINE;
   settings.framerate = 24.0;
 
   /* If we saved any settings globally, use the one from the last run. */
   gimp_get_data (PLUG_IN_PROC, &settings);
 
   /* If this animation has specific settings already, override the global ones. */
-  parasite = gimp_image_get_parasite (image_id, PLUG_IN_PROC "/frame-disposal");
-  if (parasite)
-    {
-      const DisposeType *mode = gimp_parasite_data (parasite);
-
-      settings.disposal = *mode;
-      gimp_parasite_free (parasite);
-    }
   parasite = gimp_image_get_parasite (image_id,
                                       PLUG_IN_PROC "/framerate");
   if (parasite)
@@ -360,7 +362,7 @@ animation_dialog_new (gint32 image_id)
       gimp_parasite_free (parasite);
     }
 
-  animation = animation_new (image_id, settings.disposal);
+  animation = animation_new (image_id, FALSE);
 
   dialog = g_object_new (ANIMATION_TYPE_DIALOG,
                          "type",  GTK_WINDOW_TOPLEVEL,
@@ -375,14 +377,13 @@ animation_dialog_new (gint32 image_id)
   return dialog;
 }
 
-/************ Private Functions ****************/
+/**** Private Functions ****/
 
 static void
 animation_dialog_constructed (GObject *object)
 {
   AnimationDialog        *dialog = ANIMATION_DIALOG (object);
   AnimationDialogPrivate *priv   = GET_PRIVATE (dialog);
-  GtkWidget              *hpaned;
   GtkAdjustment          *adjust;
   GtkWidget              *main_vbox;
   GtkWidget              *upper_bar;
@@ -422,13 +423,13 @@ animation_dialog_constructed (GObject *object)
   gtk_container_add (GTK_CONTAINER (dialog), priv->vpaned);
   gtk_widget_show (priv->vpaned);
 
-  hpaned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL);
-  gtk_paned_pack1 (GTK_PANED (priv->vpaned), hpaned, TRUE, TRUE);
-  gtk_widget_show (hpaned);
+  priv->hpaned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL);
+  gtk_paned_pack1 (GTK_PANED (priv->vpaned), priv->hpaned, TRUE, TRUE);
+  gtk_widget_show (priv->hpaned);
 
   /* Playback vertical box. */
   main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
-  gtk_paned_pack1 (GTK_PANED (hpaned), main_vbox, TRUE, TRUE);
+  gtk_paned_pack1 (GTK_PANED (priv->hpaned), main_vbox, TRUE, TRUE);
   gtk_widget_show (main_vbox);
 
   /* Upper Bar */
@@ -444,6 +445,7 @@ animation_dialog_constructed (GObject *object)
   /*****************/
   /* Settings box. */
   /*****************/
+
   priv->settings_bar = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
   gtk_paned_add1 (GTK_PANED (hbox), priv->settings_bar);
   gtk_widget_show (priv->settings_bar);
@@ -519,27 +521,6 @@ animation_dialog_constructed (GObject *object)
   gtk_box_pack_end (GTK_BOX (priv->settings_bar), priv->fpscombo, FALSE, FALSE, 0);
   gtk_widget_show (priv->fpscombo);
 
-  /* Settings: frame mode. */
-  priv->disposalcombo = gtk_combo_box_text_new ();
-
-  text = g_strdup (_("Cumulative layers (combine)"));
-  gtk_combo_box_text_insert_text (GTK_COMBO_BOX_TEXT (priv->disposalcombo),
-                                  DISPOSE_COMBINE, text);
-  g_free (text);
-
-  text = g_strdup (_("One frame per layer (replace)"));
-  gtk_combo_box_text_insert_text (GTK_COMBO_BOX_TEXT (priv->disposalcombo),
-                                  DISPOSE_REPLACE, text);
-  g_free (text);
-
-  g_signal_connect (priv->disposalcombo, "changed",
-                    G_CALLBACK (disposalcombo_changed),
-                    dialog);
-
-  gtk_box_pack_end (GTK_BOX (priv->settings_bar),
-                    priv->disposalcombo, FALSE, FALSE, 0);
-  gtk_widget_show (priv->disposalcombo);
-
   /*************/
   /* View box. */
   /*************/
@@ -762,7 +743,6 @@ animation_dialog_constructed (GObject *object)
 
   preview_width = gimp_image_width (priv->image_id);
   preview_height = gimp_image_height (priv->image_id);
-  /*animation_get_size (priv->animation, &preview_width, &preview_height);*/
   gtk_window_set_default_size (GTK_WINDOW (dialog),
                                MAX (preview_width + 20,
                                     screen_width - 20),
@@ -1145,10 +1125,6 @@ animation_dialog_set_animation (AnimationDialog *dialog,
                                    G_CALLBACK (fpscombo_activated),
                                    dialog);
 
-  g_signal_handlers_block_by_func (priv->disposalcombo,
-                                   G_CALLBACK (disposalcombo_changed),
-                                   dialog);
-
   g_signal_handlers_block_by_func (gtk_bin_get_child (GTK_BIN (priv->zoomcombo)),
                                    G_CALLBACK (zoomcombo_activated),
                                    dialog);
@@ -1211,14 +1187,6 @@ animation_dialog_set_animation (AnimationDialog *dialog,
                                      G_CALLBACK (fpscombo_activated),
                                      dialog);
 
-  /* Settings: frame mode. */
-  gtk_combo_box_set_active (GTK_COMBO_BOX (priv->disposalcombo),
-                            animation_get_disposal (priv->animation));
-
-  g_signal_handlers_unblock_by_func (priv->disposalcombo,
-                                     G_CALLBACK (disposalcombo_changed),
-                                     dialog);
-
   /* View: zoom. */
   g_signal_handlers_unblock_by_func (gtk_bin_get_child (GTK_BIN (priv->zoomcombo)),
                                      G_CALLBACK (zoomcombo_activated),
@@ -1242,6 +1210,33 @@ animation_dialog_set_animation (AnimationDialog *dialog,
                                      G_CALLBACK (progress_button),
                                      dialog);
 
+  /* The layer list. */
+  if (ANIMATION_IS_ANIMATIC (animation))
+    {
+      GtkWidget *scrolled_win;
+      GtkWidget *layer_list;
+      GtkWidget *frame;
+
+      layer_list = gtk_paned_get_child2 (GTK_PANED (priv->hpaned));
+      if (layer_list)
+        gtk_widget_destroy (layer_list);
+
+      frame = gtk_frame_new (_("Storyboard"));
+      gtk_paned_pack2 (GTK_PANED (priv->hpaned), frame,
+                       TRUE, TRUE);
+      gtk_widget_show (frame);
+
+      scrolled_win = gtk_scrolled_window_new (NULL, NULL);
+      gtk_container_add (GTK_CONTAINER (frame), scrolled_win);
+      gtk_widget_show (scrolled_win);
+
+      layer_list = animation_storyboard_new (ANIMATION_ANIMATIC (animation));
+      gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled_win),
+                                             layer_list);
+
+      gtk_widget_show (layer_list);
+    }
+
   /* Animation. */
   g_signal_connect (priv->animation, "proxy",
                     G_CALLBACK (proxy_changed),
@@ -1281,7 +1276,6 @@ animation_dialog_save_settings (AnimationDialog *dialog)
   CachedSettings          cached_settings;
 
   /* First saving in cache for any image. */
-  cached_settings.disposal  = animation_get_disposal (priv->animation);
   cached_settings.framerate = animation_get_framerate (priv->animation);
 
   gimp_set_data (PLUG_IN_PROC, &cached_settings, sizeof (&cached_settings));
@@ -1291,25 +1285,6 @@ animation_dialog_save_settings (AnimationDialog *dialog)
    * current state, do not resave them.
    * This prevents setting the image in a dirty state while it stayed
    * the same. */
-  old_parasite = gimp_image_get_parasite (priv->image_id,
-                                          PLUG_IN_PROC "/frame-disposal");
-  if (! old_parasite ||
-      *(DisposeType *) gimp_parasite_data (old_parasite) != cached_settings.disposal)
-    {
-      GimpParasite *parasite;
-
-      gimp_image_undo_group_start (priv->image_id);
-      undo_step_started = TRUE;
-
-      parasite = gimp_parasite_new (PLUG_IN_PROC "/frame-disposal",
-                                    GIMP_PARASITE_PERSISTENT | GIMP_PARASITE_UNDOABLE,
-                                    sizeof (cached_settings.disposal),
-                                    &cached_settings.disposal);
-      gimp_image_attach_parasite (priv->image_id, parasite);
-      gimp_parasite_free (parasite);
-    }
-  gimp_parasite_free (old_parasite);
-
   old_parasite = gimp_image_get_parasite (priv->image_id, PLUG_IN_PROC "/framerate");
   if (! old_parasite ||
       *(gdouble *) gimp_parasite_data (old_parasite) != cached_settings.framerate)
@@ -1419,38 +1394,6 @@ help_callback (GtkAction           *action,
 }
 
 /*
- * Callback emitted when the user changes the disposal in UI.
- */
-static void
-disposalcombo_changed (GtkWidget       *combo,
-                       AnimationDialog *dialog)
-{
-  AnimationDialogPrivate *priv = GET_PRIVATE (dialog);
-  Animation              *animation;
-  DisposeType             disposal;
-
-  disposal = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
-
-  if (! priv->animation ||
-      animation_get_disposal (priv->animation) != disposal)
-    {
-      gdouble proxy;
-
-      if (priv->animation &&
-          animation_is_playing (priv->animation))
-        animation_stop (priv->animation);
-
-      /* Keep same proxy ratio as previously. */
-      proxy = animation_get_proxy (priv->animation);
-
-      animation = animation_new (priv->image_id, disposal);
-      animation_dialog_set_animation (dialog,
-                                      animation);
-      animation_load (animation, proxy);
-    }
-}
-
-/*
  * Callback emitted when the user hits the Enter key of the fps combo.
  */
 static void
@@ -1971,6 +1914,36 @@ show_loading_progress (Animation       *animation,
 }
 
 static void
+animation_update_progress (Animation      *animation,
+                           gint            first_frame,
+                           gint            num_frames,
+                           gint            playback_start,
+                           gint            playback_stop,
+                           AnimationDialog *dialog)
+{
+  AnimationDialogPrivate *priv = GET_PRIVATE (dialog);
+  gint frame_spin_size;
+  gint last_frame = num_frames + first_frame - 1;
+
+  frame_spin_size = (gint) (log10 (last_frame - (last_frame % 10))) + 1;
+  gtk_entry_set_width_chars (GTK_ENTRY (priv->startframe_spin), frame_spin_size);
+  gtk_entry_set_width_chars (GTK_ENTRY (priv->endframe_spin), frame_spin_size);
+
+  gtk_adjustment_configure (gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (priv->startframe_spin)),
+                            playback_start,
+                            first_frame,
+                            last_frame,
+                            1.0, 5.0, 0.0);
+  gtk_adjustment_configure (gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (priv->endframe_spin)),
+                            playback_stop,
+                            playback_start,
+                            last_frame,
+                            1.0, 5.0, 0.0);
+
+  update_ui_sensitivity (dialog);
+}
+
+static void
 block_ui (Animation       *animation,
           AnimationDialog *dialog)
 {
@@ -1998,27 +1971,12 @@ unblock_ui (Animation      *animation,
             gint            preview_height,
             AnimationDialog *dialog)
 {
-  AnimationDialogPrivate *priv = GET_PRIVATE (dialog);
-  gint frame_spin_size;
-  gint last_frame = num_frames + first_frame - 1;
-
-  frame_spin_size = (gint) (log10 (last_frame - (last_frame % 10))) + 1;
-  gtk_entry_set_width_chars (GTK_ENTRY (priv->startframe_spin), frame_spin_size);
-  gtk_entry_set_width_chars (GTK_ENTRY (priv->endframe_spin), frame_spin_size);
-
-  gtk_adjustment_configure (gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (priv->startframe_spin)),
-                            playback_start,
-                            first_frame,
-                            last_frame,
-                            1.0, 5.0, 0.0);
-  gtk_adjustment_configure (gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (priv->endframe_spin)),
-                            playback_stop,
-                            playback_start,
-                            last_frame,
-                            1.0, 5.0, 0.0);
-
-  update_ui_sensitivity (dialog);
-
+  animation_update_progress (animation,
+                             first_frame,
+                             num_frames,
+                             playback_start,
+                             playback_stop,
+                             dialog);
   animation_dialog_refresh (dialog);
 }
 
@@ -2026,12 +1984,21 @@ static void
 playback_range_changed (Animation       *animation,
                         gint             playback_start,
                         gint             playback_stop,
+                        gint             start_pos,
+                        gint             length,
                         AnimationDialog *dialog)
 {
   AnimationDialogPrivate *priv = GET_PRIVATE (dialog);
   GtkAdjustment *startframe_adjust;
   GtkAdjustment *stopframe_adjust;
 
+  animation_update_progress (animation,
+                             start_pos,
+                             length,
+                             playback_start,
+                             playback_stop,
+                             dialog);
+
   startframe_adjust = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (priv->startframe_spin));
   stopframe_adjust  = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (priv->endframe_spin));
 
@@ -2049,6 +2016,8 @@ playback_range_changed (Animation       *animation,
   g_signal_handlers_unblock_by_func (stopframe_adjust,
                                      G_CALLBACK (endframe_changed),
                                      dialog);
+
+  show_playing_progress (dialog);
 }
 
 static void
@@ -2261,7 +2230,7 @@ da_size_callback (GtkWidget       *drawing_area,
                            allocation->height);
       repaint_da (priv->drawing_area, NULL, dialog);
     }
-  else 
+  else
     {
       /* Update the zoom information. */
       AnimationDialogPrivate *priv = GET_PRIVATE (dialog);
@@ -2590,7 +2559,9 @@ progress_button (GtkWidget       *widget,
       gtk_widget_get_allocation (widget, &allocation);
 
       goto_frame = animation_get_start_position (priv->animation) +
-                  (gint) (event->x / (allocation.width / animation_get_length (priv->animation)));
+        (gint) (event->x /
+                ((gdouble) allocation.width /
+                 (gdouble) animation_get_length (priv->animation)));
 
       animation_jump (priv->animation, goto_frame);
     }
@@ -2610,7 +2581,9 @@ progress_entered (GtkWidget        *widget,
   gtk_widget_get_allocation (widget, &allocation);
 
   goto_frame = animation_get_start_position (priv->animation) +
-               (gint) (event->x / (allocation.width / animation_get_length (priv->animation)));
+               (gint) (event->x /
+                       ((gdouble) allocation.width /
+                        (gdouble) animation_get_length (priv->animation)));
 
   show_goto_progress (dialog, goto_frame);
 
@@ -2629,7 +2602,9 @@ progress_motion (GtkWidget       *widget,
   gtk_widget_get_allocation (widget, &allocation);
 
   goto_frame = animation_get_start_position (priv->animation) +
-               (gint) (event->x / (allocation.width / animation_get_length (priv->animation)));
+               (gint) (event->x /
+                       ((gdouble) allocation.width /
+                        (gdouble) animation_get_length (priv->animation)));
 
   show_goto_progress (dialog, goto_frame);
 
diff --git a/plug-ins/animation-play/widgets/animation-storyboard.c 
b/plug-ins/animation-play/widgets/animation-storyboard.c
new file mode 100644
index 0000000..ee5c528
--- /dev/null
+++ b/plug-ins/animation-play/widgets/animation-storyboard.c
@@ -0,0 +1,334 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation-layer_view.c
+ * Copyright (C) 2016 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/animationanimatic.h"
+
+#include "animation-storyboard.h"
+
+/* Properties. */
+enum
+{
+  PROP_0,
+  PROP_ANIMATION
+};
+
+struct _AnimationStoryboardPrivate
+{
+  AnimationAnimatic *animation;
+};
+
+/* GObject handlers */
+static void animation_storyboard_constructed           (GObject             *object);
+static void animation_storyboard_set_property          (GObject             *object,
+                                                        guint                property_id,
+                                                        const GValue        *value,
+                                                        GParamSpec          *pspec);
+static void animation_storyboard_get_property          (GObject             *object,
+                                                        guint                property_id,
+                                                        GValue              *value,
+                                                        GParamSpec          *pspec);
+static void animation_storyboard_finalize              (GObject             *object);
+
+/* Callbacks on animation */
+static void animation_storyboard_load                  (Animation           *animation,
+                                                        G_GNUC_UNUSED gint   first_frame,
+                                                        G_GNUC_UNUSED gint   num_frames,
+                                                        G_GNUC_UNUSED gint   playback_start,
+                                                        G_GNUC_UNUSED gint   playback_stop,
+                                                        G_GNUC_UNUSED gint   preview_width,
+                                                        G_GNUC_UNUSED gint   preview_height,
+                                                        AnimationStoryboard *view);
+
+static void animation_storyboard_duration_spin_changed (GtkSpinButton       *spinbutton,
+                                                        AnimationAnimatic   *animation);
+
+G_DEFINE_TYPE (AnimationStoryboard, animation_storyboard, GTK_TYPE_TABLE)
+
+#define parent_class animation_storyboard_parent_class
+
+static void
+animation_storyboard_class_init (AnimationStoryboardClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed  = animation_storyboard_constructed;
+  object_class->finalize     = animation_storyboard_finalize;
+  object_class->get_property = animation_storyboard_get_property;
+  object_class->set_property = animation_storyboard_set_property;
+
+  /**
+   * AnimationStoryboard:animation:
+   *
+   * The associated #AnimationAnimatic.
+   */
+  g_object_class_install_property (object_class, PROP_ANIMATION,
+                                   g_param_spec_object ("animation",
+                                                        NULL, NULL,
+                                                        ANIMATION_TYPE_ANIMATIC,
+                                                        G_PARAM_READWRITE |
+                                                        G_PARAM_CONSTRUCT_ONLY));
+
+  g_type_class_add_private (klass, sizeof (AnimationStoryboardPrivate));
+}
+
+static void
+animation_storyboard_init (AnimationStoryboard *view)
+{
+  view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
+                                            ANIMATION_TYPE_STORYBOARD,
+                                            AnimationStoryboardPrivate);
+
+  gtk_table_set_homogeneous (GTK_TABLE (view), TRUE);
+}
+
+/**** Public Functions ****/
+
+/**
+ * animation_storyboard_new:
+ * @animation: the #AnimationAnimatic for this storyboard.
+ *
+ * Creates a new layer view tied to @animation, ready to be displayed.
+ */
+GtkWidget *
+animation_storyboard_new (AnimationAnimatic *animation)
+{
+  GtkWidget *layer_view;
+
+  layer_view = g_object_new (ANIMATION_TYPE_STORYBOARD,
+                             "animation", animation,
+                             NULL);
+  return layer_view;
+}
+
+/**** Private Functions ****/
+
+static void
+animation_storyboard_constructed (GObject *object)
+{
+  AnimationStoryboard *view = ANIMATION_STORYBOARD (object);
+
+  g_signal_connect (view->priv->animation, "loaded",
+                    (GCallback) animation_storyboard_load,
+                    view);
+}
+
+static void
+animation_storyboard_set_property (GObject      *object,
+                                        guint         property_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  AnimationStoryboard *view = ANIMATION_STORYBOARD (object);
+
+  switch (property_id)
+    {
+    case PROP_ANIMATION:
+      view->priv->animation = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+animation_storyboard_get_property (GObject    *object,
+                                        guint       property_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  AnimationStoryboard *view = ANIMATION_STORYBOARD (object);
+
+  switch (property_id)
+    {
+    case PROP_ANIMATION:
+      g_value_set_object (value, view->priv->animation);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+animation_storyboard_finalize (GObject *object)
+{
+  AnimationStoryboard *view = ANIMATION_STORYBOARD (object);
+
+  g_object_unref (view->priv->animation);
+
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+/* animation_storyboard_load:
+ * @view: the #AnimationStoryboard.
+ *
+ * Recursively fills @store with the #GimpLayers data of the #GimpImage
+ * tied to @view.
+ */
+static void
+animation_storyboard_load (Animation           *animation,
+                           gint                 first_frame,
+                           gint                 num_frames,
+                           gint                 playback_start,
+                           gint                 playback_stop,
+                           gint                 preview_width,
+                           gint                 preview_height,
+                           AnimationStoryboard *view)
+{
+  gint   *layers;
+  gint32  image_id;
+  gint    n_images;
+  gint    i;
+
+  image_id = animation_get_image_id (animation);
+
+  /* Cleaning previous loads. */
+  gtk_container_foreach (GTK_CONTAINER (view),
+                         (GtkCallback) gtk_widget_destroy,
+                         NULL);
+
+  /* Setting new values. */
+  layers = gimp_image_get_layers (image_id,
+                                  &n_images);
+
+  gtk_table_resize (GTK_TABLE (view),
+                    5 * n_images,
+                    10);
+  for (i = 0; i < n_images; i++)
+    {
+      GdkPixbuf *thumbnail;
+      GtkWidget *image;
+      GtkWidget *comment;
+      GtkWidget *duration;
+      GtkWidget *disposal;
+      gchar     *image_name;
+      gint       panel_num = n_images - i;
+
+      thumbnail = gimp_drawable_get_thumbnail (layers[i], 250, 250,
+                                               GIMP_PIXBUF_SMALL_CHECKS);
+      image = gtk_image_new_from_pixbuf (thumbnail);
+      g_object_unref (thumbnail);
+
+      /* Let's align top-right, in case the storyboard gets resized
+       * and the image grows (the thumbnail right now stays as fixed size). */
+      gtk_misc_set_alignment (GTK_MISC (image), 1.0, 0.0);
+
+      gtk_table_attach (GTK_TABLE (view),
+                        image, 0, 4,
+                        5 * n_images - 5 * i - 5,
+                        5 * n_images - 5 * i,
+                        GTK_EXPAND | GTK_FILL,
+                        GTK_EXPAND | GTK_FILL,
+                        1, 1);
+      gtk_widget_show (image);
+
+      comment = gtk_text_view_new ();
+      gtk_table_attach (GTK_TABLE (view),
+                        comment, 5, 10,
+                        5 * n_images - 5 * i - 5,
+                        5 * n_images - 5 * i,
+                        GTK_EXPAND | GTK_FILL,
+                        GTK_EXPAND | GTK_FILL,
+                        0, 1);
+      gtk_widget_show (comment);
+
+      image_name = gimp_item_get_name (layers[i]);
+      if (image_name)
+        {
+          GtkTextBuffer *buffer;
+          const gchar   *comment_contents;
+
+          /* Layer name is shown as a tooltip. */
+          gtk_widget_set_tooltip_text (image, image_name);
+
+          buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (comment));
+
+          comment_contents = animation_animatic_get_comment (view->priv->animation,
+                                                             panel_num);
+          if (comment_contents != NULL)
+            gtk_text_buffer_insert_at_cursor (buffer, comment_contents, -1);
+
+          g_free (image_name);
+        }
+
+      duration = gtk_spin_button_new_with_range (0.0, G_MAXINT, 1.0);
+      gtk_spin_button_set_digits (GTK_SPIN_BUTTON (duration), 0);
+      gtk_spin_button_set_increments (GTK_SPIN_BUTTON (duration), 1.0, 10.0);
+      gtk_spin_button_set_snap_to_ticks (GTK_SPIN_BUTTON (duration), TRUE);
+      gtk_spin_button_set_value (GTK_SPIN_BUTTON (duration),
+                                 animation_animatic_get_duration (ANIMATION_ANIMATIC (animation),
+                                                                  panel_num));
+      gtk_entry_set_width_chars (GTK_ENTRY (duration), 2);
+      /* Allowing non-numeric text to type "ms" or "s". */
+      gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (duration), FALSE);
+
+      gtk_table_attach (GTK_TABLE (view),
+                        duration, 4, 5,
+                        5 * n_images - 5 * i - 5,
+                        5 * n_images - 5 * i - 4,
+                        0, /* Do not expand nor fill, nor shrink. */
+                        0, /* Do not expand nor fill, nor shrink. */
+                        0, 1);
+      g_object_set_data (G_OBJECT (duration), "layer-position",
+                         GINT_TO_POINTER (panel_num));
+      g_signal_connect (duration, "value-changed",
+                        (GCallback) animation_storyboard_duration_spin_changed,
+                        animation);
+      gtk_widget_show (duration);
+
+      disposal = gtk_toggle_button_new ();
+      image = gtk_image_new_from_icon_name (GIMP_STOCK_TRANSPARENCY,
+                                            GTK_ICON_SIZE_MENU);
+      gtk_container_add (GTK_CONTAINER (disposal), image);
+      gtk_widget_show (image);
+      gtk_table_attach (GTK_TABLE (view),
+                        disposal, 4, 5,
+                        5 * n_images - 5 * i - 3,
+                        5 * n_images - 5 * i - 2,
+                        GTK_EXPAND, GTK_EXPAND,
+                        0, 1);
+      gtk_widget_show (disposal);
+    }
+}
+
+static void
+animation_storyboard_duration_spin_changed (GtkSpinButton     *spinbutton,
+                                            AnimationAnimatic *animation)
+{
+  gpointer layer_pos;
+  gint     duration;
+
+  layer_pos = g_object_get_data (G_OBJECT (spinbutton), "layer-position");
+  duration = gtk_spin_button_get_value_as_int (spinbutton);
+
+  animation_animatic_set_duration (animation,
+                                   GPOINTER_TO_INT (layer_pos),
+                                   duration);
+}
diff --git a/plug-ins/animation-play/widgets/animation-storyboard.h 
b/plug-ins/animation-play/widgets/animation-storyboard.h
new file mode 100644
index 0000000..f92a23e
--- /dev/null
+++ b/plug-ins/animation-play/widgets/animation-storyboard.h
@@ -0,0 +1,52 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * animation-layer_view.h
+ * Copyright (C) 2016 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_STORYBOARD_H__
+#define __ANIMATION_STORYBOARD_H__
+
+#define ANIMATION_TYPE_STORYBOARD            (animation_storyboard_get_type ())
+#define ANIMATION_STORYBOARD(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), ANIMATION_TYPE_STORYBOARD, 
AnimationStoryboard))
+#define ANIMATION_STORYBOARD_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), ANIMATION_TYPE_STORYBOARD, 
AnimationStoryboardClass))
+#define ANIMATION_IS_STORYBOARD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ANIMATION_TYPE_STORYBOARD))
+#define ANIMATION_IS_STORYBOARD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ANIMATION_TYPE_STORYBOARD))
+#define ANIMATION_STORYBOARD_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), ANIMATION_TYPE_STORYBOARD, 
AnimationStoryboardClass))
+
+typedef struct _AnimationStoryboard        AnimationStoryboard;
+typedef struct _AnimationStoryboardClass   AnimationStoryboardClass;
+typedef struct _AnimationStoryboardPrivate AnimationStoryboardPrivate;
+
+struct _AnimationStoryboard
+{
+  GtkTable          parent_instance;
+
+  AnimationStoryboardPrivate *priv;
+};
+
+struct _AnimationStoryboardClass
+{
+  GtkTableClass     parent_class;
+};
+
+GType       animation_storyboard_get_type (void) G_GNUC_CONST;
+
+GtkWidget * animation_storyboard_new      (AnimationAnimatic *animation);
+
+#endif  /*  __ANIMATION_STORYBOARD_H__  */
+


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