diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3d47f986c41db29ec6dc0d5036bf760b3a1cf366
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.idea/
+target/
+.settings/
+*.iml
+.project
+.classpath
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2429c1dbb53dd77b6dcaec5b05769b583fb6dd8b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <!-- Inherited Icy Parent POM -->
+    <parent>
+        <groupId>org.bioimageanalysis.icy</groupId>
+        <artifactId>parent-pom-plugin</artifactId>
+        <version>1.0.3</version>
+    </parent>
+
+    <!-- Project Information -->
+    <artifactId>animation-3d</artifactId>
+    <version>1.3.1</version>
+
+    <packaging>jar</packaging>
+
+    <name>Animation 3D</name>
+    <description>Generate nice dataset animations with interpolated camera points.</description>
+    <url>http://icy.bioimageanalysis.org/plugin/animation-3d/</url>
+    <inceptionYear>2020</inceptionYear>
+
+    <organization>
+        <name>Institut Pasteur</name>
+        <url>https://pasteur.fr</url>
+    </organization>
+
+    <licenses>
+        <license>
+            <name>GNU GPLv3</name>
+            <url>https://www.gnu.org/licenses/gpl-3.0.en.html</url>
+            <distribution>repo</distribution>
+        </license>
+    </licenses>
+
+    <developers>
+        <developer>
+            <id>tprovoost</id>
+            <name>Thomas Provoost</name>
+            <url>http://icy.bioimageanalysis.org/author/tprovoost/</url>
+            <roles>
+                <role>founder</role>
+                <role>developer</role>
+            </roles>
+        </developer>
+        <developer>
+            <id>sdallongeville</id>
+            <name>Stéphane Dallongeville</name>
+            <url>https://research.pasteur.fr/fr/member/stephane-dallongeville/</url>
+            <roles>
+                <role>founder</role>
+                <role>lead</role>
+                <role>architect</role>
+                <role>developer</role>
+                <role>debugger</role>
+                <role>tester</role>
+                <role>maintainer</role>
+                <role>support</role>
+            </roles>
+        </developer>
+    </developers>
+
+    <!-- Project properties -->
+    <properties>
+
+    </properties>
+
+    <!-- Project build configuration -->
+    <build>
+
+    </build>
+
+    <!-- List of project's dependencies -->
+    <dependencies>
+        <!-- The core of Icy -->
+        <dependency>
+            <groupId>org.bioimageanalysis.icy</groupId>
+            <artifactId>icy-kernel</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bioimageanalysis.icy</groupId>
+            <artifactId>flying-camera</artifactId>
+            <version>1.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bioimageanalysis.icy</groupId>
+            <artifactId>xuggler</artifactId>
+            <version>5.5.2</version>
+        </dependency>
+    </dependencies>
+
+    <!-- Icy Maven repository (to find parent POM) -->
+    <repositories>
+        <repository>
+            <id>icy</id>
+            <name>Icy's Nexus</name>
+            <url>https://icy-nexus.pasteur.fr/repository/Icy/</url>
+        </repository>
+    </repositories>
+</project>
diff --git a/src/main/java/plugins/tprovoost/animation3d/Animation3D.java b/src/main/java/plugins/tprovoost/animation3d/Animation3D.java
new file mode 100644
index 0000000000000000000000000000000000000000..c6255f34bcaac6cff381b815aba0d77cca5c5059
--- /dev/null
+++ b/src/main/java/plugins/tprovoost/animation3d/Animation3D.java
@@ -0,0 +1,94 @@
+package plugins.tprovoost.animation3d;
+
+import icy.gui.dialog.MessageDialog;
+import icy.gui.frame.IcyFrame;
+import icy.plugin.PluginLauncher;
+import icy.plugin.abstract_.PluginActionable;
+import icy.sequence.Sequence;
+import icy.system.thread.ThreadUtil;
+
+import java.util.ArrayList;
+
+import plugins.tprovoost.flycam.FlyCam;
+import plugins.tprovoost.flycam.FlyCamOverlay;
+
+public class Animation3D extends PluginActionable
+{
+    private Kinematics3D mainFrame;
+
+    @Override
+    public void run()
+    {
+        Sequence seq = getActiveSequence();
+        if (seq == null)
+        {
+            MessageDialog.showDialog("You must have a sequence opened for this operation.");
+            return;
+        }
+
+        // FlyCam is not running on this sequence ?
+        if (!seq.hasOverlay(FlyCamOverlay.class))
+        {
+            // start FlyCam
+            ThreadUtil.invokeNow(new Runnable()
+            {
+                @Override
+                public void run()
+                {
+                    new FlyCam().run();
+                }
+            });
+        }
+
+        // get the FlyCam overlay
+        final FlyCamOverlay overlay = (FlyCamOverlay) seq.getOverlays(FlyCamOverlay.class).get(0);
+
+        // create the frame now
+        mainFrame = new Kinematics3D(overlay);
+        mainFrame.setVisible(true);
+        mainFrame.addToDesktopPane();
+    }
+
+    public Kinematics3D getKinematics3D()
+    {
+        return mainFrame;
+    }
+
+    /**
+     * Returns the first Animation3D in the list of IcyFrames. If it doesn't
+     * exist, create an instance first.
+     * 
+     * @return Kinematics3D
+     */
+    public static Kinematics3D getAnimation3D()
+    {
+        ArrayList<IcyFrame> frames = IcyFrame.getAllFrames(Kinematics3D.class);
+        if (frames.isEmpty())
+        {
+            Animation3D p = (Animation3D) PluginLauncher.start(Animation3D.class.getName());
+            return p.getKinematics3D();
+        }
+        else
+            return (Kinematics3D) frames.get(0);
+    }
+
+    /**
+     * Returns the Animation3D of index <code>idx</code> in the list of
+     * IcyFrames. If it doesn't exist, returns null;
+     * 
+     * @param idx int
+     * @return Kinematics3D
+     */
+    public static Kinematics3D getAnimation3D(int idx)
+    {
+        ArrayList<IcyFrame> frames = IcyFrame.getAllFrames(Kinematics3D.class);
+        if (frames.isEmpty())
+        {
+            return null;
+        }
+        else
+        {
+            return (Kinematics3D) frames.get(idx);
+        }
+    }
+}
diff --git a/src/main/java/plugins/tprovoost/animation3d/AnimationPanel.java b/src/main/java/plugins/tprovoost/animation3d/AnimationPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..ead606adf8e3f909423e4b7f65e099bdbcf8c260
--- /dev/null
+++ b/src/main/java/plugins/tprovoost/animation3d/AnimationPanel.java
@@ -0,0 +1,661 @@
+package plugins.tprovoost.animation3d;
+
+import icy.gui.component.button.IcyButton;
+import icy.main.Icy;
+import icy.preferences.XMLPreferences;
+import icy.preferences.XMLPreferences.XMLPreferencesRoot;
+import icy.resource.icon.IcyIcon;
+import icy.util.XMLUtil;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.ChangeListener;
+import javax.swing.filechooser.FileNameExtensionFilter;
+
+import org.w3c.dom.Element;
+
+import plugins.tprovoost.flycam.CameraPosition;
+
+public class AnimationPanel extends JPanel
+{
+    private static final long serialVersionUID = 1L;
+
+    private static final String PREF_CAMERAS = "cameras";
+    private static final String PREF_CAM_LOCATION_DATA = "CamLocationData";
+
+    private static final String PREF_FPS = "fps";
+    private static final String PREF_TIME = "time";
+    private static final String PREF_SMOOTH = "smooth";
+
+    private KeySlider timeSlider;
+    private IcyButton gotostart;
+    private IcyButton gotoend;
+    private JButton playbackward;
+    private IcyButton playforward;
+    private IcyButton stop;
+    private JButton previousframe;
+    private JButton nextframe;
+    private JButton record;
+    private JButton recordCancel;
+    private JButton previouskey;
+    private JButton nextkey;
+    private JButton addkey;
+    private JButton removekey;
+    private JButton resetkey;
+    private JButton rescale;
+    private JTextField rescalefps;
+    private JTextField rescaletime;
+    private JTextField smoothpathfactor;
+    private JProgressBar recordProgressBar;
+    private JCheckBox checkBoxAutoUpdate;
+    private JButton openanimation;
+    private JButton saveanimation;
+    private ActionListener actionListener;
+    private ChangeListener changeListener;
+    private JComboBox comboPresets;
+    private JCheckBox cboxAutoChangeSeqT;
+    private JPanel panelLoadSave;
+    private JButton btnLoad;
+    private JButton btnSave;
+
+    private enum AnimationPreset
+    {
+        CUSTOM
+        {
+            @Override
+            public String toString()
+            {
+                return "Custom";
+            }
+        },
+        ROTATION_X
+        {
+            @Override
+            public String toString()
+            {
+                return "Rotation on X";
+            }
+        },
+        ROTATION_Y
+        {
+            @Override
+            public String toString()
+            {
+                return "Rotation on Y";
+            }
+        },
+        ROTATION_Z
+        {
+            @Override
+            public String toString()
+            {
+                return "Rotation on Z";
+            }
+        },
+        GO_INTO
+        {
+            @Override
+            public String toString()
+            {
+                return "Go Into";
+            }
+        }
+    };
+
+    public AnimationPanel(ActionListener actionListener, ChangeListener changeListener)
+    {
+        this.actionListener = actionListener;
+        this.changeListener = changeListener;
+        this.setPreferredSize(new Dimension(200, 465));
+
+        timeSlider = new KeySlider(0, 5 * 15, 15);
+        timeSlider.setBorder(new EmptyBorder(4, 4, 4, 4));
+        timeSlider.setSnapToTicks(false);
+        timeSlider.setValue(0);
+        timeSlider.addChangeListener(changeListener);
+        timeSlider.setPreferredSize(new Dimension(290, 60));
+
+        playbackward = new JButton("<");
+        playbackward.setActionCommand("playbackward");
+        playbackward.addActionListener(actionListener);
+
+        record = new JButton("Render");
+        record.setActionCommand("record");
+        record.addActionListener(actionListener);
+
+        recordCancel = new JButton("Cancel");
+        // recordCancel.setEnabled(false);
+        recordCancel.setActionCommand("recordCancel");
+        recordCancel.addActionListener(actionListener);
+
+        recordProgressBar = new JProgressBar();
+        recordProgressBar.setMaximum(100);
+        recordProgressBar.setValue(0);
+
+        openanimation = new JButton("Open...");
+        openanimation.setActionCommand("openanimation");
+        openanimation.addActionListener(actionListener);
+
+        saveanimation = new JButton("Save...");
+        saveanimation.setActionCommand("saveanimation");
+        saveanimation.addActionListener(actionListener);
+
+        JPanel rescalePanel = new JPanel();
+        rescalePanel.setPreferredSize(new Dimension(300, 80));
+        rescalePanel.setBorder(BorderFactory.createTitledBorder("Rescale tool"));
+        rescalePanel.setLayout(new BoxLayout(rescalePanel, BoxLayout.Y_AXIS));
+
+        JPanel panel_1 = new JPanel();
+        panel_1.setBorder(new EmptyBorder(4, 4, 4, 4));
+        rescalePanel.add(panel_1);
+        panel_1.setLayout(new GridLayout(0, 2, 0, 0));
+
+        JLabel label = new JLabel("FPS:");
+        panel_1.add(label);
+
+        rescalefps = new JTextField();
+        panel_1.add(rescalefps);
+        rescalefps.setPreferredSize(new Dimension(30, 20));
+        rescalefps.setText("" + getTimeSlider().getFps());
+
+        JPanel panel_2 = new JPanel();
+        panel_2.setBorder(new EmptyBorder(4, 4, 4, 4));
+        rescalePanel.add(panel_2);
+        panel_2.setLayout(new GridLayout(0, 2, 0, 0));
+        JLabel label_1 = new JLabel("Seconds :");
+        panel_2.add(label_1);
+        rescaletime = new JTextField("5");
+        panel_2.add(rescaletime);
+        rescaletime.setPreferredSize(new Dimension(30, 20));
+
+        JPanel panelRescaleButton = new JPanel();
+        rescalePanel.add(panelRescaleButton);
+        panelRescaleButton.setLayout(new BoxLayout(panelRescaleButton, BoxLayout.X_AXIS));
+
+        panelRescaleButton.add(Box.createHorizontalGlue());
+
+        rescale = new JButton("Rescale");
+        panelRescaleButton.add(rescale);
+        rescale.setActionCommand("rescale");
+
+        panelRescaleButton.add(Box.createHorizontalGlue());
+        rescale.addActionListener(actionListener);
+
+        JPanel smoothingPanel = new JPanel();
+        smoothingPanel.setBorder(BorderFactory.createTitledBorder("Smooth Camera path"));
+        smoothingPanel.setLayout(new GridLayout(0, 2, 0, 0));
+        smoothingPanel.add(new JLabel("Smooth factor: "));
+        setSmoothpathfactor(new JTextField("0"));
+        getSmoothpathfactor().setPreferredSize(new Dimension(190, 20));
+        smoothingPanel.add(getSmoothpathfactor());
+
+        JPanel p8 = new JPanel();
+        p8.setBorder(BorderFactory.createTitledBorder("Record"));
+        p8.setLayout(new GridLayout(4, 1));
+
+        cboxAutoChangeSeqT = new JCheckBox("Auto Change Time Position");
+        cboxAutoChangeSeqT.setHorizontalAlignment(SwingConstants.CENTER);
+        cboxAutoChangeSeqT.setToolTipText("Also modifies the position of the current T in the Sequence.");
+        p8.add(cboxAutoChangeSeqT);
+        p8.add(record);
+        p8.add(recordCancel);
+        p8.add(getRecordProgressBar());
+        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+
+        // JPanel p9 = new JPanel();
+        // p9.setBorder( BorderFactory.createTitledBorder("File"));
+        // p9.add( openanimation );
+        // p9.add( saveanimation );
+
+        this.add(getTimeSlider());
+
+        comboPresets = new JComboBox();
+        comboPresets.setModel(new DefaultComboBoxModel(AnimationPreset.values()));
+        comboPresets.addItemListener(new ItemListener()
+        {
+
+            @Override
+            public void itemStateChanged(ItemEvent e)
+            {
+                if (e.getStateChange() == ItemEvent.SELECTED)
+                {
+                    switch (AnimationPreset.valueOf(comboPresets.getSelectedItem().toString()))
+                    {
+                        case ROTATION_X:
+                        {
+                            // TODO
+                        }
+                            break;
+
+                        case ROTATION_Y:
+                        {
+                            // TODO
+                        }
+                            break;
+
+                        case ROTATION_Z:
+                        {
+                            // TODO
+                        }
+                            break;
+
+                        case CUSTOM:
+                        default:
+                            break;
+                    }
+                }
+
+            }
+        });
+
+        panelLoadSave = new JPanel();
+        add(panelLoadSave);
+        panelLoadSave.setLayout(new GridLayout(1, 0, 0, 0));
+
+        btnLoad = new JButton("Load");
+        btnLoad.addActionListener(new ActionListener()
+        {
+
+            @Override
+            public void actionPerformed(ActionEvent e)
+            {
+                String filename = animFileChooser();
+                if (filename == null)
+                    return;
+                XMLPreferencesRoot root = new XMLPreferencesRoot(filename);
+                loadXMLFile(root.getPreferences());
+            }
+        });
+        panelLoadSave.add(btnLoad);
+
+        btnSave = new JButton("Save");
+        btnSave.addActionListener(new ActionListener()
+        {
+
+            @Override
+            public void actionPerformed(ActionEvent e)
+            {
+                String filename = animFileChooser();
+                if (filename == null)
+                    return;
+                XMLPreferencesRoot root = new XMLPreferencesRoot(filename);
+                saveToXML(root.getPreferences());
+                root.save();
+            }
+        });
+        panelLoadSave.add(btnSave);
+        // add(comboPresets); // TODO
+
+        JPanel panel = new JPanel();
+        add(panel);
+
+        playforward = new IcyButton(new IcyIcon("sq_next.png"));
+        playforward.setActionCommand("playforward");
+        playforward.addActionListener(actionListener);
+
+        previousframe = new IcyButton(new IcyIcon("sq_br_prev.png"));
+        previousframe.setActionCommand("previousframe");
+        previousframe.addActionListener(actionListener);
+        panel.setLayout(new GridLayout(0, 2, 0, 0));
+
+        JPanel p1 = new JPanel();
+        p1.setBorder(new TitledBorder(null, "Frames", TitledBorder.LEADING, TitledBorder.TOP, null, null));
+        panel.add(p1);
+        p1.setPreferredSize(new Dimension(200, 50));
+        p1.setLayout(new GridLayout(2, 3));
+        p1.add(previousframe);
+        p1.add(getPlayforward());
+
+        gotostart = new IcyButton(new IcyIcon("sq_br_first.png"));
+        gotostart.setActionCommand("gotostart");
+        gotostart.addActionListener(actionListener);
+
+        nextframe = new IcyButton(new IcyIcon("sq_br_next.png"));
+        nextframe.setActionCommand("nextframe");
+        nextframe.addActionListener(actionListener);
+        p1.add(nextframe);
+
+        p1.add(gotostart);
+
+        stop = new IcyButton(new IcyIcon("square_shape.png"));
+        stop.setActionCommand("stop");
+        stop.addActionListener(actionListener);
+        p1.add(stop);
+
+        gotoend = new IcyButton(new IcyIcon("sq_br_last.png"));
+        gotoend.setActionCommand("gotoend");
+        gotoend.addActionListener(actionListener);
+        p1.add(gotoend);
+
+        previouskey = new IcyButton(new IcyIcon("arrow_left.png"));
+        previouskey.setActionCommand("previouskey");
+        previouskey.addActionListener(actionListener);
+
+        nextkey = new IcyButton(new IcyIcon("arrow_right.png"));
+        nextkey.setActionCommand("nextkey");
+        nextkey.addActionListener(actionListener);
+
+        addkey = new IcyButton(new IcyIcon("pin_map.png"));
+        addkey.setActionCommand("addkey");
+        addkey.addActionListener(actionListener);
+
+        removekey = new IcyButton(new IcyIcon("delete.png"));
+        removekey.setActionCommand("removekey");
+        removekey.addActionListener(actionListener);
+
+        resetkey = new IcyButton(new IcyIcon("playback_reload.png"));
+        resetkey.setActionCommand("resetkey");
+        resetkey.addActionListener(actionListener);
+
+        JPanel keyPanel = new JPanel();
+        panel.add(keyPanel);
+        keyPanel.setLayout(new GridLayout(2, 3));
+        keyPanel.setBorder(BorderFactory.createTitledBorder("Keys"));
+        keyPanel.setPreferredSize(new Dimension(300, 75));
+
+        keyPanel.add(addkey);
+        keyPanel.add(previouskey);
+        keyPanel.add(nextkey);
+        keyPanel.add(removekey);
+        keyPanel.add(resetkey);
+
+        checkBoxAutoUpdate = new JCheckBox("Auto Update");
+        // panel.add(checkBoxAutoUpdate);
+        checkBoxAutoUpdate.setSelected(true);
+
+        this.add(rescalePanel);
+        this.add(smoothingPanel);
+        this.add(p8);
+        // this.add(p9);
+    }
+
+    public void removeListeners()
+    {
+        timeSlider.removeChangeListener(changeListener);
+        gotostart.removeActionListener(actionListener);
+        gotoend.removeActionListener(actionListener);
+        playbackward.removeActionListener(actionListener);
+        playforward.removeActionListener(actionListener);
+        stop.removeActionListener(actionListener);
+        previousframe.removeActionListener(actionListener);
+        nextframe.removeActionListener(actionListener);
+        record.removeActionListener(actionListener);
+        recordCancel.removeActionListener(actionListener);
+        previouskey.removeActionListener(actionListener);
+        nextkey.removeActionListener(actionListener);
+        addkey.removeActionListener(actionListener);
+        removekey.removeActionListener(actionListener);
+        resetkey.removeActionListener(actionListener);
+        rescale.removeActionListener(actionListener);
+        rescalefps.removeActionListener(actionListener);
+        rescaletime.removeActionListener(actionListener);
+        smoothpathfactor.removeActionListener(actionListener);
+        checkBoxAutoUpdate.removeActionListener(actionListener);
+        openanimation.removeActionListener(actionListener);
+        saveanimation.removeActionListener(actionListener);
+        actionListener = null;
+        changeListener = null;
+        timeSlider = null;
+    }
+
+    public void setEnabledRecursive(JPanel parent, boolean enabled)
+    {
+        parent.setEnabled(enabled);
+        Component[] components = parent.getComponents();
+        if (components != null && components.length > 0)
+        {
+            int count = components.length;
+            for (int i = 0; i < count; i++)
+            {
+                components[i].setEnabled(enabled);
+                if (components[i].getClass().getName() == "javax.swing.JPanel")
+                {
+                    setEnabledRecursive((JPanel) components[i], enabled);
+                }
+            }
+        }
+    }
+
+    /**
+     * @return the timeSlider
+     */
+    public KeySlider getTimeSlider()
+    {
+        return timeSlider;
+    }
+
+    /**
+     * @param timeSlider
+     *        the timeSlider to set
+     */
+    public void setTimeSlider(KeySlider timeSlider)
+    {
+        this.timeSlider = timeSlider;
+    }
+
+    /**
+     * @return the stop
+     */
+    public JButton getStop()
+    {
+        return stop;
+    }
+
+    /**
+     * @param stop
+     *        the stop to set
+     */
+    public void setStop(IcyButton stop)
+    {
+        this.stop = stop;
+    }
+
+    /**
+     * @return the smoothpathfactor
+     */
+    public JTextField getSmoothpathfactor()
+    {
+        return smoothpathfactor;
+    }
+
+    /**
+     * @param smoothpathfactor
+     *        the smoothpathfactor to set
+     */
+    public void setSmoothpathfactor(JTextField smoothpathfactor)
+    {
+        this.smoothpathfactor = smoothpathfactor;
+    }
+
+    /**
+     * @return the recordProgressBar
+     */
+    public JProgressBar getRecordProgressBar()
+    {
+        return recordProgressBar;
+    }
+
+    /**
+     * @param recordProgressBar
+     *        the recordProgressBar to set
+     */
+    public void setRecordProgressBar(JProgressBar recordProgressBar)
+    {
+        this.recordProgressBar = recordProgressBar;
+    }
+
+    /**
+     * @return the playforward
+     */
+    public JButton getPlayforward()
+    {
+        return playforward;
+    }
+
+    /**
+     * @param playforward
+     *        the playforward to set
+     */
+    public void setPlayforward(IcyButton playforward)
+    {
+        this.playforward = playforward;
+    }
+
+    /**
+     * @return the rescalefps
+     */
+    public JTextField getRescalefps()
+    {
+        return rescalefps;
+    }
+
+    /**
+     * @param rescalefps
+     *        the rescalefps to set
+     */
+    public void setRescalefps(JTextField rescalefps)
+    {
+        this.rescalefps = rescalefps;
+    }
+
+    /**
+     * @return the rescaletime
+     */
+    public JTextField getRescaletime()
+    {
+        return rescaletime;
+    }
+
+    /**
+     * @param rescaletime
+     *        the rescaletime to set
+     */
+    public void setRescaletime(JTextField rescaletime)
+    {
+        this.rescaletime = rescaletime;
+    }
+
+    /**
+     * @return the checkBoxAutoUpdate
+     */
+    public JCheckBox getCheckBoxAutoUpdate()
+    {
+        return checkBoxAutoUpdate;
+    }
+
+    /**
+     * @param checkBoxAutoUpdate
+     *        the checkBoxAutoUpdate to set
+     */
+    public void setCheckBoxAutoUpdate(JCheckBox checkBoxAutoUpdate)
+    {
+        this.checkBoxAutoUpdate = checkBoxAutoUpdate;
+    }
+
+    public JButton getCancel()
+    {
+        return recordCancel;
+    }
+
+    public boolean isAutoChangeSeqT()
+    {
+        return cboxAutoChangeSeqT.isSelected();
+    }
+
+    /**
+     * Used to load XML Files
+     * 
+     * @return Returns null if Dialog exited.
+     */
+    private String animFileChooser()
+    {
+        String filename = null;
+        JFileChooser fc = new JFileChooser();
+        fc.setFileFilter(new FileNameExtensionFilter("Animation Files (.anim)", "anim"));
+        int returnVal = fc.showDialog(Icy.getMainInterface().getMainFrame(), "Save File");
+        if (returnVal == JFileChooser.APPROVE_OPTION)
+        {
+            filename = fc.getSelectedFile().getAbsolutePath();
+            if (!filename.endsWith(".anim"))
+                filename += ".anim";
+        }
+        else
+        {
+            filename = null;
+        }
+        
+        return filename;
+    }
+
+    /**
+     * Save all the properties into an XML file.
+     * 
+     * @param root
+     *        : file and node where data is saved.
+     */
+    private void saveToXML(XMLPreferences root)
+    {
+        root.removeChildren();
+
+        XMLPreferences cameras = root.node(PREF_CAMERAS);
+        cameras.removeChildren();
+
+        root.put(PREF_FPS, rescalefps.getText());
+        root.put(PREF_TIME, rescaletime.getText());
+        root.put(PREF_SMOOTH, smoothpathfactor.getText());
+
+        // save camera positions into XML
+        for (CameraPosition cam : timeSlider.getCameraPositions())
+            cam.saveCamera(XMLUtil.addElement(cameras.getXMLNode(), PREF_CAM_LOCATION_DATA));
+    }
+
+    /**
+     * Load all the properties into a file.
+     * 
+     * @param root
+     *        : file and node where data is saved.
+     */
+    private void loadXMLFile(XMLPreferences root)
+    {
+        if (!root.nodeExists(PREF_CAMERAS))
+            return;
+
+        rescalefps.setText(root.get(PREF_FPS, "15"));
+        rescaletime.setText(root.get(PREF_TIME, "5"));
+        smoothpathfactor.setText(root.get(PREF_SMOOTH, "0"));
+        XMLPreferences cameras = root.node(PREF_CAMERAS);
+
+        List<CameraPosition> cams = timeSlider.getCameraPositions();
+        cams.clear();
+        for (Element e : XMLUtil.getElements(cameras.getXMLNode(), PREF_CAM_LOCATION_DATA))
+        {
+            CameraPosition cam = new CameraPosition();
+            cam.loadCamera(e);
+            cams.add(cam);
+        }
+
+        timeSlider.repaint();
+    }
+}
diff --git a/src/main/java/plugins/tprovoost/animation3d/KeySlider.java b/src/main/java/plugins/tprovoost/animation3d/KeySlider.java
new file mode 100644
index 0000000000000000000000000000000000000000..461429f9fc5a85110c0e26e0de3c0b20391f3a49
--- /dev/null
+++ b/src/main/java/plugins/tprovoost/animation3d/KeySlider.java
@@ -0,0 +1,274 @@
+package plugins.tprovoost.animation3d;
+
+import java.awt.Color;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JSlider;
+
+import plugins.tprovoost.flycam.CameraPosition;
+
+public class KeySlider extends JSlider implements MouseListener, MouseMotionListener
+{
+    private static final long serialVersionUID = 1L;
+    // private ArrayList<Integer> key = new ArrayList<Integer>();
+    private int fps = 15;
+    private int timelength;
+    private Point currentPoint = null;
+    private CameraPosition currentCamera;
+    private List<CameraPosition> cameraPositions = new ArrayList<CameraPosition>();
+
+    public KeySlider(int min, int max, int value)
+    {
+        super(min, max, value);
+        setPaintTicks(true);
+        setPaintLabels(true);
+        addMouseListener(this);
+        addMouseMotionListener(this);
+    }
+
+    @Override
+    protected void paintComponent(Graphics g)
+    {
+        super.paintComponent(g);
+        Insets ins = getInsets();
+        int w = getWidth();
+        int h = getHeight();
+        Graphics2D g2 = (Graphics2D) g.create();
+        // g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+        // RenderingHints.VALUE_ANTIALIAS_ON);
+        // g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+        // display red keys in component.
+        g2.setColor(new Color(210, 27, 58));
+        for (CameraPosition cam : cameraPositions)
+        {
+            float step = ((w - 31) / (float) getMaximum());
+            int positionx = Math.round(ins.left + 11 + cam.timeIndex * step);
+            g2.fillRect(positionx - 1, h - 12, 3, 6);
+        }
+        // // display Seconds:Frame.
+        g2.setColor(Color.black);
+        drawCenteredString(g2, "Frame: #" + getValue() + " / " + timeFormat(getValue()) + " s", getMaximum() / 2, 10,
+                false);
+        g2.dispose();
+    }
+
+    private String timeFormat(int t)
+    {
+        String modulo;
+        if (t % getFps() < 10)
+        {
+            modulo = "0" + t % getFps();
+        }
+        else
+        {
+            modulo = "" + t % getFps();
+        }
+        String SecondFrame;
+        SecondFrame = "" + (t / getFps()) + ":" + modulo + "0";
+        return SecondFrame;
+    }
+
+    /**
+     * @param g
+     * @param s
+     * @param v
+     *        Tick corresponding to the center of the string.
+     */
+    private void drawCenteredString(Graphics g, String s, int v, int y, boolean ShowTick)
+    {
+        FontMetrics metrics = getFontMetrics(g.getFont());
+        float step = ((getWidth() - 16.0f) / (float) getMaximum());
+        int positionx = Math.round(6 + v * step);
+        g.drawString(s, positionx - metrics.stringWidth(s) / 2, y);
+        if (ShowTick)
+            g.drawLine(positionx, 35, positionx, 30);
+    }
+
+    // public void addKey(int timeKey)
+    // {
+    // key.add(timeKey);
+    // repaint();
+    // }
+    //
+    // public void removeKey(int TimeKey)
+    // {
+    // key.remove((Integer) TimeKey);
+    // repaint();
+    // }
+    //
+    // public void removeAllKey()
+    // {
+    // key.clear();
+    // repaint();
+    // }
+
+    // public ArrayList<Integer> getKeys()
+    // {
+    // return key;
+    // }
+
+    /**
+     * @return the timelength
+     */
+    public int getTimelength()
+    {
+        return timelength;
+    }
+
+    /**
+     * @param timelength
+     *        the timelength to set
+     */
+    public void setTimelength(int timelength)
+    {
+        this.timelength = timelength;
+    }
+
+    /**
+     * @return the fps
+     */
+    public int getFps()
+    {
+        return fps;
+    }
+
+    /**
+     * @param fps
+     *        the fps to set
+     */
+    public void setFps(int fps)
+    {
+        this.fps = fps;
+    }
+
+    @Override
+    public void setMaximum(int maximum)
+    {
+        super.setMaximum(maximum);
+        if (maximum < 5)
+        {
+            setMinorTickSpacing(maximum);
+            setLabelTable(createStandardLabels(maximum));
+        }
+        else
+        {
+            setMajorTickSpacing(maximum / 5);
+            setMinorTickSpacing(maximum / 10);
+            setLabelTable(createStandardLabels(maximum / 5));
+        }
+    }
+
+    @Override
+    public void mouseDragged(MouseEvent e)
+    {
+        if (currentPoint != null && currentCamera != null)
+        {
+            int xmov = e.getPoint().x - currentPoint.x;
+            int max = getMaximum();
+            float step = (getWidth()) / (float) max;
+            int timeIndex = (int) (e.getPoint().x / step);
+
+            if (timeIndex == 0)
+            {
+                timeIndex = 1;
+            }
+            else if (timeIndex >= max)
+            {
+                timeIndex = max;
+            }
+            else if (cameraExists(timeIndex))
+            {
+                if (Math.signum(xmov) == -1.0d)
+                {
+                    timeIndex--;
+                }
+                else if (Math.signum(xmov) == 1.0d)
+                {
+                    timeIndex++;
+                }
+            }
+            System.out.println(timeIndex);
+            currentCamera.timeIndex = timeIndex;
+            currentPoint = e.getPoint();
+        }
+        repaint();
+    }
+
+    private boolean cameraExists(int timeIndex)
+    {
+        for (CameraPosition cam : cameraPositions)
+        {
+            if (cam.timeIndex == timeIndex)
+                return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e)
+    {
+    }
+
+    @Override
+    public void mouseClicked(MouseEvent e)
+    {
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e)
+    {
+        Insets ins = getInsets();
+        int w = getWidth();
+        int h = getHeight();
+        for (CameraPosition cam : cameraPositions)
+        {
+            float step = ((w - 31) / (float) getMaximum());
+            int positionx = Math.round(ins.left + 11 + cam.timeIndex * step);
+            Rectangle rect = new Rectangle(positionx - 1, h - 12, 3, 6);
+            if (rect.contains(e.getPoint()))
+            {
+                currentCamera = cam;
+                currentPoint = e.getPoint();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void mouseReleased(MouseEvent e)
+    {
+        currentCamera = null;
+        currentPoint = e.getPoint();
+    }
+
+    @Override
+    public void mouseEntered(MouseEvent e)
+    {
+    }
+
+    @Override
+    public void mouseExited(MouseEvent e)
+    {
+    }
+
+    public void setCameraPositions(List<CameraPosition> value)
+    {
+        cameraPositions = value;
+    }
+
+    public List<CameraPosition> getCameraPositions()
+    {
+        return cameraPositions;
+    }
+}
diff --git a/src/main/java/plugins/tprovoost/animation3d/Kinematics3D.java b/src/main/java/plugins/tprovoost/animation3d/Kinematics3D.java
new file mode 100644
index 0000000000000000000000000000000000000000..788ecbcfb12bad2ede742c6af92024b42792430b
--- /dev/null
+++ b/src/main/java/plugins/tprovoost/animation3d/Kinematics3D.java
@@ -0,0 +1,878 @@
+package plugins.tprovoost.animation3d;
+
+// Management of generality
+import icy.file.FileUtil;
+import icy.gui.dialog.MessageDialog;
+import icy.gui.frame.IcyFrame;
+import icy.image.ImageUtil;
+import icy.main.Icy;
+import icy.preferences.PluginsPreferences;
+import icy.preferences.XMLPreferences;
+import icy.system.thread.ThreadUtil;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.JFileChooser;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.filechooser.FileNameExtensionFilter;
+
+import plugins.kernel.canvas.VtkCanvas;
+import plugins.tprovoost.flycam.CameraPosition;
+import plugins.tprovoost.flycam.FlyCamOverlay;
+
+import com.xuggle.mediatool.IMediaWriter;
+import com.xuggle.mediatool.ToolFactory;
+import com.xuggle.xuggler.ICodec;
+import com.xuggle.xuggler.IPixelFormat;
+import com.xuggle.xuggler.IRational;
+import com.xuggle.xuggler.IStreamCoder;
+
+public class Kinematics3D extends IcyFrame
+{
+    private static final String LAST_DIR = "last_directory";
+    private XMLPreferences prefs = PluginsPreferences.root(Animation3D.class);
+
+    /**
+     * Original animation user's key.
+     */
+    private List<CameraPosition> cameraPositions;
+    private AnimationPanel animPanel = null;
+    private PanelKinematics kinematicPanel = null;
+    private FlyCamOverlay flyCam;
+    private KinematicsThread kinemAnim;
+
+    public Kinematics3D(FlyCamOverlay flyCam)
+    {
+        super("Animation 3D", true);
+
+        cameraPositions = new ArrayList<CameraPosition>();
+        kinematicPanel = new PanelKinematics(this);
+        this.flyCam = flyCam;
+
+        rescaleTimeLine();
+        setContentPane(getPanel());
+        pack();
+    }
+
+    // private float[] linearInterp(float[] v1, float[] v2, float h)
+    // {
+    // float[] toBeReturn = new float[3];
+    // toBeReturn[0] = v1[0] * (1 - h) + v2[0] * h;
+    // toBeReturn[1] = v1[1] * (1 - h) + v2[1] * h;
+    // toBeReturn[2] = v1[2] * (1 - h) + v2[2] * h;
+    // return toBeReturn;
+    //
+    // }
+
+    public List<CameraPosition> getCameraPositions()
+    {
+        return cameraPositions;
+    }
+
+    public void setCameraPositions(List<CameraPosition> value)
+    {
+        if (value != null)
+            cameraPositions = value;
+    }
+
+    public void seekAndDestroyKeyAt(int t)
+    {
+        synchronized (cameraPositions)
+        {
+            for (int i = cameraPositions.size() - 1; i >= 0; i--)
+            {
+                if (cameraPositions.get(i).timeIndex == t)
+                    cameraPositions.remove(i);
+            }
+        }
+    }
+
+    public int getNumberOfUserKey()
+    {
+        return getCameraPositions().size();
+    }
+
+    /**
+     * @return the first key available.
+     * 
+     * @author Fab
+     */
+    public CameraPosition getFirstCamKey()
+    {
+        if (getCameraPositions().isEmpty())
+        {
+            System.out.println("Kinematic3D : Problem : GetFirstCamKey should be able to get a valid key.");
+            return null;
+        }
+
+        return getCameraPositions().get(0);
+    }
+
+    /**
+     * @return the last key available.
+     * 
+     * @author Fab
+     */
+    public CameraPosition getLastCamKey()
+    {
+        if (getCameraPositions().isEmpty())
+        {
+            System.out.println("Kinematic3D : Problem : GetLastCamKey should be able to get a valid key.");
+            return null;
+        }
+        return getCameraPositions().get(getNumberOfUserKey() - 1);
+    }
+
+    /**
+     * @param t int
+     * @param CreateKeyIfDoNotExists boolean
+     * @return Get Previous Key &lt;=t.
+     * 
+     * @author Fab
+     */
+    public CameraPosition getPreviousFlyCamKey(int t, boolean CreateKeyIfDoNotExists)
+    {
+        if (t < 0)
+            t = 0;
+
+        CameraPosition Previous = null; // Previous detected key.
+        int previousbest = animPanel.getTimeSlider().getMaximum();
+
+        // find previous
+        for (CameraPosition camPos : getCameraPositions())
+        {
+            // if (povk.CameraType == canvas.getCameraType())
+            if (camPos.timeIndex <= t)
+            {
+                if ((t - camPos.timeIndex) < previousbest)
+                {
+                    previousbest = t - camPos.timeIndex;
+                    Previous = camPos;
+                }
+            }
+        }
+
+        if (Previous == null && CreateKeyIfDoNotExists == true)
+            return getFirstCamKey();
+
+        return Previous;
+    }
+
+    /**
+     * @param t int
+     * @param CreateKeyIfDoNotExists boolean
+     * @return Get Previous Key &gt;=t.
+     * 
+     * @author Fab
+     */
+    public CameraPosition getNextFlyCamKey(int t, boolean CreateKeyIfDoNotExists)
+    {
+        if (t > animPanel.getTimeSlider().getMaximum())
+            t = animPanel.getTimeSlider().getMaximum();
+
+        CameraPosition Next = null; // Previous detected key.
+        int nextbest = animPanel.getTimeSlider().getMaximum();
+
+        // find best
+        for (CameraPosition camPos : getCameraPositions())
+        {
+            // if (povk.CameraType == canvas.getCameraType())
+            if (camPos.timeIndex >= t)
+            {
+                if ((camPos.timeIndex - t) < nextbest)
+                {
+                    nextbest = camPos.timeIndex - t;
+                    Next = camPos;
+                }
+            }
+        }
+
+        if (Next == null && CreateKeyIfDoNotExists == true)
+            return getLastCamKey();
+
+        return Next;
+    }
+
+    /**
+     * @param t int
+     * @param smooth int
+     * @return CameraPosition
+     */
+    public CameraPosition computeSmoothInterpolatedCamKey(int t, int smooth)
+    {
+        CameraPosition cumul = new CameraPosition();
+
+        int iter = 0;
+        for (int i = t - smooth; i <= t + smooth; i++)
+        {
+            iter++;
+            CameraPosition tmppovd = computeInterpolatedCamKey(i);
+            for (int j = 0; j < 3; j++)
+            {
+                cumul.position[j] += tmppovd.position[j];
+                cumul.focal[j] += tmppovd.focal[j];
+                cumul.viewUp[j] += tmppovd.viewUp[j];
+            }
+            cumul.viewAngle += tmppovd.viewAngle;
+        }
+
+        for (int j = 0; j < 3; j++)
+        {
+            cumul.position[j] /= iter;
+            cumul.focal[j] /= iter;
+            cumul.viewUp[j] /= iter;
+        }
+        cumul.viewAngle /= iter;
+
+        return cumul;
+    }
+
+    /**
+     * Creates an interpolated FlyCamKey.
+     * 
+     * @param t
+     *        Time index in animation's sequence ( frame # )
+     * @return Interpolated FlyCamKey as CamPointOfViewData
+     * @author Fab 27/11/06
+     */
+    public CameraPosition computeInterpolatedCamKey(int t)
+    {
+        CameraPosition povd = new CameraPosition();
+        CameraPosition step = new CameraPosition();
+        CameraPosition Previous = getPreviousFlyCamKey(t, true);
+        CameraPosition Next = getNextFlyCamKey(t, true);
+
+        if (Previous.timeIndex == Next.timeIndex)
+            return Previous; // We are on the key. No need to interpolate.
+
+        final int delta = Next.timeIndex - Previous.timeIndex;
+        final int delta2 = t - Previous.timeIndex;
+
+        for (int i = 0; i < 3; i++)
+        {
+            step.position[i] = (Next.position[i] - Previous.position[i]) / delta;
+            step.focal[i] = (Next.focal[i] - Previous.focal[i]) / delta;
+            step.viewUp[i] = (Next.viewUp[i] - Previous.viewUp[i]) / delta;
+            povd.position[i] = Previous.position[i] + step.position[i] * delta2;
+            povd.focal[i] = Previous.focal[i] + step.focal[i] * delta2;
+            povd.viewUp[i] = Previous.viewUp[i] + step.viewUp[i] * delta2;
+        }
+        step.viewAngle = (Next.viewAngle - Previous.viewAngle) / delta;
+        povd.viewAngle = Previous.viewAngle + step.viewAngle * delta2;
+
+        return povd;
+    }
+
+    /**
+     * @return Get the kinematic interaction panel
+     */
+    public JPanel getPanel()
+    {
+        return kinematicPanel.getPanel();
+    }
+
+    /**
+     * needed when extends Object3D
+     */
+    public void display()
+    {
+        // nothing to do here
+
+    }
+
+    /**
+     * clean up the object
+     */
+    public void unload()
+    {
+
+    }
+
+    public void refreshAnimPanel()
+    {
+        // Put new computed key in Animation's Slider.
+        // animPanel.getTimeSlider().removeAllKey();
+        for (CameraPosition camPos : getCameraPositions())
+        {
+            // CameraPosition povk = e.nextElement();
+            // if (canvas.getCameraType() == povk.CameraType)
+            // animPanel.getTimeSlider().addKey(povk.timeIndex);
+
+            // Pour forcer un rafraichissement.
+            int i = animPanel.getTimeSlider().getValue();
+            animPanel.getTimeSlider().setValue(i++);
+            animPanel.getTimeSlider().setValue(i);
+
+        }
+    }
+
+    /**
+     * Set a new CameraPosition at the specific time position.
+     * 
+     * @param cam CameraPosition
+     * @param pos int
+     */
+    public void addKey(CameraPosition cam, int pos)
+    {
+        kinematicPanel.addKey(pos);
+    }
+
+    public class PanelKinematics implements ActionListener, ChangeListener
+
+    {
+        // to define the panel :
+        private JPanel panel = null;
+        private Recorder recorder;
+
+        /**
+         * Constructor
+         * @param kine Kinematics3D
+         */
+        public PanelKinematics(Kinematics3D kine)
+        {
+            kinemAnim = new KinematicsThread();
+            createPanel();
+        }
+
+        /**
+         * Build the panel.
+         */
+        private void createPanel()
+        {
+            panel = new JPanel();
+            panel.setLayout(new BorderLayout());
+
+            animPanel = new AnimationPanel(this, this);
+            animPanel.getTimeSlider().setCameraPositions(getCameraPositions());
+            panel.add(animPanel);
+        }
+
+        /**
+         * Get the panel to be used in the GUI for the list of marching cube.
+         * Build it on the first time.
+         * 
+         * @return the panel to be used
+         */
+        public JPanel getPanel()
+        {
+            if (panel != null)
+                return panel;
+
+            createPanel();
+            return panel;
+        }
+
+        public void addKey(int t)
+        {
+            addKey(null, t);
+        }
+
+        public void addKey(CameraPosition cam, int t)
+        {
+            seekAndDestroyKeyAt(t);
+
+            // Sequence s = canvas.getSequence();
+            // s.setSelectedT( s.getSelectedT()+1 );
+
+            // animPanel.getTimeSlider().addKey(t);
+
+            if (cam == null)
+                cam = flyCam.getCameraPosition();
+            cam.timeIndex = t;
+            // povd.CameraType = canvas.getCameraType();
+            System.out.println(cam);
+            getCameraPositions().add(cam);
+
+        }
+
+        public void actionPerformed(ActionEvent event)
+        {
+
+            String action = event.getActionCommand();
+
+            if (action == "gotostart")
+            {
+                animPanel.getTimeSlider().setValue(0);
+            }
+
+            if (action == "gotoend")
+            {
+                animPanel.getTimeSlider().setValue(animPanel.getTimeSlider().getMaximum());
+            }
+
+            if (action == "playbackward")
+            {
+                // AnimPanel.getTimeSlider().setValue(
+                // AnimPanel.getTimeSlider().getMaximum() );
+            }
+
+            if (action == "playforward")
+            {
+                if (getNumberOfUserKey() < 2)
+                {
+                    System.out.println("Must have at least 2 keys to create animation.");
+                    // animPanel.setEnabledRecursive(animPanel, true);
+                    return;
+                }
+
+                animPanel.setEnabledRecursive(animPanel, false);
+                animPanel.getStop().setEnabled(true);
+                animPanel.getStop().requestFocus();
+
+                if (kinemAnim != null)
+                {
+                    kinemAnim.stop = true;
+                    kinemAnim = new KinematicsThread();
+                }
+                ThreadUtil.bgRun(kinemAnim);
+            }
+
+            if (action == "stop")
+            {
+                if (kinemAnim != null)
+                    kinemAnim.stop = true;
+                animPanel.setEnabledRecursive(animPanel, true);
+                animPanel.getPlayforward().requestFocus();
+            }
+
+            if (action == "previousframe")
+            {
+                int v = animPanel.getTimeSlider().getValue();
+                v--;
+                if (v < 0)
+                    v = 0;
+                animPanel.getTimeSlider().setValue(v);
+            }
+
+            if (action == "nextframe")
+            {
+                int v = animPanel.getTimeSlider().getValue();
+                v++;
+                if (v > animPanel.getTimeSlider().getMaximum())
+                    v = animPanel.getTimeSlider().getMaximum();
+                animPanel.getTimeSlider().setValue(v);
+            }
+
+            if (action == "recordCancel")
+            {
+                if (recorder != null)
+                    recorder.recording = false;
+            }
+
+            if (action == "record")
+            {
+                if (recorder != null)
+                {
+                    recorder.recording = false;
+                }
+                if (getNumberOfUserKey() < 2)
+                {
+                    MessageDialog.showDialog("You should at least have 2 keys to start rendering.");
+                    return;
+                }
+
+                final JFileChooser fc = new JFileChooser(prefs.get(LAST_DIR, ""));
+                fc.setFileFilter(new FileNameExtensionFilter("AVI Files", "avi", "AVI"));
+                fc.addChoosableFileFilter(new FileNameExtensionFilter("MP4 Files", "mp4", "MP4"));
+                fc.addChoosableFileFilter(new FileNameExtensionFilter("MOV Files", "mov", "MOV"));
+                fc.setMultiSelectionEnabled(false);
+                int res = fc.showSaveDialog(Icy.getMainInterface().getMainFrame());
+                if (res == JFileChooser.APPROVE_OPTION)
+                {
+                    File file = fc.getSelectedFile();
+                    if (file == null)
+                        return;
+                    prefs.put(LAST_DIR, file.getParent());
+                    if (FileUtil.getFileExtension(file.getAbsolutePath(), false).isEmpty())
+                    {
+                        String desc = fc.getFileFilter().getDescription();
+                        if (desc.contentEquals("MP4 Files"))
+                            file = new File(file.getAbsoluteFile() + ".mp4");
+                        else if (desc.contentEquals("MOV Files"))
+                            file = new File(file.getAbsoluteFile() + ".mov");
+                        else
+                            file = new File(file.getAbsoluteFile() + ".avi");
+                    }
+                    recorder = new Recorder(file);
+                    ThreadUtil.bgRun(recorder);
+                }
+            }
+
+            if (action == "previouskey")
+            {
+                CameraPosition previous = getPreviousFlyCamKey(animPanel.getTimeSlider().getValue() - 1, false);
+                if (previous != null)
+                {
+                    animPanel.getTimeSlider().setValue(previous.timeIndex);
+                    flyCam.setCameraPosition(previous, true);
+                }
+
+            }
+            if (action == "nextkey")
+            {
+                CameraPosition next = getNextFlyCamKey(animPanel.getTimeSlider().getValue() + 1, false);
+                if (next != null)
+                {
+                    animPanel.getTimeSlider().setValue(next.timeIndex);
+                    flyCam.setCameraPosition(next, true);
+                }
+            }
+
+            if (action == "removekey")
+            {
+                if (getNumberOfUserKey() > 1)
+                {
+                    int t = animPanel.getTimeSlider().getValue();
+                    seekAndDestroyKeyAt(t);
+                    // animPanel.getTimeSlider().removeKey(t);
+                }
+                else
+                {
+                    System.out.println("Animator > Last remaining animation's key cannot be removed.");
+                }
+
+            }
+
+            if (action == "addkey")
+            {
+                // Destroy potential key at time t.
+                int t = animPanel.getTimeSlider().getValue();
+                // animPanel.getTimeSlider().removeKey(t); // same for time
+                // slider
+                addKey(t);
+            }
+            if (action == "resetkey")
+            {
+                // animPanel.getTimeSlider().removeAllKey();
+                getCameraPositions().clear();
+
+                // Ajoute une clef de chaque camera.
+                // int cameratype = canvas.getCameraType();
+                // canvas.setCameraType(CanvasViewPort.CAMERA_FLYCAM);
+                addKey(0);
+                // canvas.setCameraType(CanvasViewPort.CAMERA_ROTATION);
+                // AddKey(0);
+                // canvas.setCameraType(cameratype);
+
+                animPanel.getTimeSlider().setValue(0);
+                animPanel.getTimeSlider().repaint();
+            }
+
+            if (action == "rescale")
+            {
+                // Rescale TimeLine.
+
+                rescaleTimeLine();
+
+            }
+
+            // if (action == "saveanimation") {
+            // // Create a file chooser
+            // JFileChooser fc = new JFileChooser();
+            // int returnVal = fc.showSaveDialog(null);
+            //
+            // if (returnVal == JFileChooser.APPROVE_OPTION) {
+            // File file = fc.getSelectedFile();
+            //
+            // try {
+            // FileOutputStream fos;
+            // fos = new FileOutputStream( file );
+            // ObjectOutputStream oos = new ObjectOutputStream(fos);
+            //
+            // AnimationSaveData d = new AnimationSaveData();
+            // d.CampovData = CamPointOfViewKey;
+            // d.fps = AnimPanel.getTimeSlider().fps;
+            // d.smoothfactor = AnimPanel.smoothpathfactor.getText();
+            // d.timelenght = AnimPanel.getTimeSlider().getTimelength();
+            // oos.writeObject( d );
+            // fos.close();
+            //
+            // }
+            // catch (FileNotFoundException e) { e.printStackTrace(); }
+            // catch (IOException e) { e.printStackTrace(); }
+            // }
+            // }
+
+            // if (action == "openanimation") {
+            // // Create a file chooser
+            // JFileChooser fc = new JFileChooser();
+            // int returnVal = fc.showOpenDialog(null);
+            //
+            // if (returnVal == JFileChooser.APPROVE_OPTION) {
+            // File file = fc.getSelectedFile();
+            // try {
+            //
+            // FileInputStream fis = new FileInputStream( file );
+            // ObjectInputStream ois = new ObjectInputStream(fis);
+            // AnimationSaveData d =
+            // (AnimationSaveData)ois.readObject();
+            // CamPointOfViewKey = d.CampovData;
+            // AnimPanel.getTimeSlider().setFps(d.fps);
+            // AnimPanel.getTimeSlider().setTimelength( d.timelenght);
+            // AnimPanel.getTimeSlider().setMaximum( d.fps * d.timelenght );
+            // AnimPanel.rescalefps.setText( ""+d.fps );
+            // AnimPanel.rescaletime.setText( ""+d.timelenght );
+            // AnimPanel.smoothpathfactor.setText( "" + d.smoothfactor
+            // );
+            // fis.close();
+            // }
+            // catch (FileNotFoundException e) { e.printStackTrace(); }
+            // catch (IOException e) { e.printStackTrace(); }
+            // catch (ClassNotFoundException e) { e.printStackTrace(); }
+            //
+            // RefreshAnimPanel();
+            // }
+            // }
+
+        }
+
+        public void stateChanged(ChangeEvent e)
+        {
+            // Listening to Animation's Panel TimeSlider.
+            if (animPanel.getCheckBoxAutoUpdate().isSelected() || (kinemAnim != null && !kinemAnim.stop))
+            {
+                if (getNumberOfUserKey() == 0)
+                    addKey(0); // Prevent system from having no key to read at
+                // all
+                JSlider slider = (JSlider) e.getSource();
+                CameraPosition d = new CameraPosition();
+                d = computeSmoothInterpolatedCamKey(slider.getValue(),
+                        Integer.parseInt(animPanel.getSmoothpathfactor().getText()));
+
+                if (d != null)
+                    flyCam.setCameraPosition(d, true);
+            }
+
+        }
+
+    }
+
+    public void actionPerformed(ActionEvent e)
+    {
+
+    }
+
+    public void rescaleTimeLine()
+    {
+        int oldfps = animPanel.getTimeSlider().getFps();
+        int oldlength = animPanel.getTimeSlider().getTimelength();
+        int newfps = Integer.parseInt(animPanel.getRescalefps().getText());
+        int newlength = Integer.parseInt(animPanel.getRescaletime().getText());
+
+        animPanel.getTimeSlider().setFps(newfps);
+        animPanel.getTimeSlider().setTimelength(newlength);
+        // animPanel.getTimeSlider().removeAllKey();
+        animPanel.getTimeSlider().setMaximum(newfps * newlength);
+        animPanel.getRescalefps().setText("" + newfps);
+        animPanel.getRescaletime().setText("" + newlength);
+
+        // change towards length
+        double timeratio = newlength / (double) oldlength;
+        timeratio *= newfps / (double) oldfps;
+        for (CameraPosition cam : getCameraPositions())
+            cam.timeIndex *= timeratio;
+
+        animPanel.getTimeSlider().setValue((int) (animPanel.getTimeSlider().getValue() * timeratio));
+
+        // Put new computed key in Animation's Slider.
+        // for (Enumeration<CameraPosition> e = cameraPositions.elements();
+        // e.hasMoreElements();)
+        // {
+        // CameraPosition povk = e.nextElement();
+        // if (canvas.getCameraType() == povk.CameraType)
+        // animPanel.getTimeSlider().addKey(povk.timeIndex);
+        // }
+    }
+
+    private class KinematicsThread implements Runnable
+    {
+
+        private boolean stop = false;
+
+        /**
+         * Play the animation // And has record capability
+         */
+        @Override
+        public void run()
+        {
+            int idx = animPanel.getTimeSlider().getValue();
+            int size = animPanel.getTimeSlider().getTimelength();
+            int fps = animPanel.getTimeSlider().getFps();
+            // long time = 0;
+
+            idx = animPanel.getTimeSlider().getValue();
+            size = animPanel.getTimeSlider().getTimelength() * animPanel.getTimeSlider().getFps();
+            fps = animPanel.getTimeSlider().getFps();
+            // time = System.nanoTime();
+
+            while (!stop)
+            {
+                if (idx >= size)
+                    idx = 0;
+
+                ThreadUtil.sleep((int) (1d / fps * 1000));
+                // long delay;
+                // delay = (System.nanoTime() - time) * 1000000L;
+
+                // if (animPanel.getCheckBoxSmooth().isSelected())
+                // {
+                // float step;
+                // step = delay / (float) (1000 / fps);
+                // idx += step;
+                // }
+                // else
+                // {
+                idx += 1;
+                // }
+
+                animPanel.getTimeSlider().setValue(idx);
+
+                // time = System.nanoTime();
+
+            }
+        }
+    }
+
+    class Recorder implements Runnable
+    {
+        private boolean recording;
+        private IRational frameRate;
+        private long nextTimeStamp;
+        private File f;
+        private IMediaWriter writer;
+
+        public Recorder(File f)
+        {
+            this.f = f;
+        }
+
+        @Override
+        public void run()
+        {
+            VtkCanvas canvas = flyCam.getCanvas();
+
+            if (canvas == null)
+                return;
+
+            int width = canvas.getVtkPanel().getWidth();
+            int height = canvas.getVtkPanel().getHeight();
+            int fps = Integer.valueOf(animPanel.getRescalefps().getText());
+
+            frameRate = IRational.make(fps, 1);
+            writer = ToolFactory.makeWriter(f.getAbsolutePath());
+            writer.addVideoStream(0, 0, frameRate, width, height);
+
+            String ext = FileUtil.getFileExtension(f.getAbsolutePath(), false).toLowerCase();
+            if (ext.contentEquals("mp4") || ext.contentEquals("mov"))
+            {
+                IStreamCoder coder = writer.getContainer().getStream(0).getStreamCoder();
+                coder.setCodec(ICodec.ID.CODEC_ID_H264);
+                coder.setPixelType(IPixelFormat.Type.YUV422P);
+            }
+
+            recording = true;
+
+            nextTimeStamp = 0;
+
+            // final int nbframe = animPanel.getTimeSlider().getFps() *
+            // animPanel.getTimeSlider().getTimelength();
+            final int nbframe = animPanel.getTimeSlider().getMaximum();
+
+            ThreadUtil.invokeLater(new Runnable()
+            {
+                @Override
+                public void run()
+                {
+                    animPanel.getRecordProgressBar().setMaximum(nbframe);
+                    animPanel.getRecordProgressBar().setValue(0);
+                }
+            });
+            try
+            {
+                for (int i = 0; i < nbframe && recording; i++)
+                {
+                    final CameraPosition d = computeSmoothInterpolatedCamKey(i,
+                            Integer.parseInt(animPanel.getSmoothpathfactor().getText()));
+                    final int tPos = i;
+                    int sequenceT = canvas.getPositionT();
+                    if (animPanel.isAutoChangeSeqT())
+                        sequenceT = (sequenceT + i) % canvas.getSequence().getSizeT();
+
+                    canvas = flyCam.getCanvas();
+                    if ((canvas == null) || (canvas.getRenderer() == null))
+                        return;
+
+                    // set camera position
+                    flyCam.setCameraPosition(d, false);
+
+                    BufferedImage originalImage = canvas.getRenderedImage(sequenceT, -1);
+                    BufferedImage worksWithXugglerBufferedImage = ImageUtil.convert(originalImage, new BufferedImage(
+                            originalImage.getWidth(), originalImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR));
+
+                    writer.encodeVideo(0, worksWithXugglerBufferedImage, nextTimeStamp, TimeUnit.MICROSECONDS);
+
+                    nextTimeStamp += (1e6 / frameRate.getNumerator());
+                    // recordedSequence.setImage(tPos, 0,
+                    // canvas.getRenderedImage(sequenceT, -1));
+                    ThreadUtil.invokeLater(new Runnable()
+                    {
+                        @Override
+                        public void run()
+                        {
+                            animPanel.getTimeSlider().setValue(tPos);
+                            animPanel.getRecordProgressBar().setValue(tPos);
+                        }
+                    });
+                }
+            }
+            finally
+            {
+                writer.close();
+                ThreadUtil.invokeLater(new Runnable()
+                {
+                    @Override
+                    public void run()
+                    {
+                        animPanel.getRecordProgressBar().setValue(0);
+                    }
+                });
+            }
+        }
+    }
+
+    public void clearKeys()
+    {
+        getCameraPositions().clear();
+    }
+
+    public void close()
+    {
+        flyCam = null;
+        if (kinemAnim != null)
+        {
+            kinemAnim.stop = true;
+            kinemAnim = null;
+        }
+        if (animPanel != null)
+        {
+            animPanel.removeListeners();
+            animPanel.removeAll();
+            animPanel = null;
+        }
+    }
+
+    public void setFlyCamOverlay(FlyCamOverlay flyCam)
+    {
+        this.flyCam = flyCam;
+    }
+}
diff --git a/src/main/java/plugins/tprovoost/animation3d/TablePanel.java b/src/main/java/plugins/tprovoost/animation3d/TablePanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..36abac6720335c23e74dcdf977bf7cc53f7c4af6
--- /dev/null
+++ b/src/main/java/plugins/tprovoost/animation3d/TablePanel.java
@@ -0,0 +1,115 @@
+package plugins.tprovoost.animation3d;
+
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.util.Hashtable;
+
+import javax.swing.JPanel;
+
+public class TablePanel extends JPanel
+{
+
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    private Hashtable<String, Boolean> busy = new Hashtable<String, Boolean>();
+
+    private GridBagLayout gridbag;
+    private GridBagConstraints c;
+
+    public final static int WEST = GridBagConstraints.WEST;
+    public final static int EAST = GridBagConstraints.EAST;
+    public final static int CENTER = GridBagConstraints.CENTER;
+
+    private final Insets DEFAULT_INSETS = new Insets(2, 2, 2, 2);
+    private final int DEFAULT_ANCHOR = CENTER;
+
+    public TablePanel()
+    {
+        super();
+        gridbag = new GridBagLayout();
+        setLayout(gridbag);
+        c = new GridBagConstraints();
+        c.fill = GridBagConstraints.BOTH;
+        // c.fill = GridBagConstraints.NONE;
+
+        c.gridx = -1;
+        c.gridy = -1;
+        c.gridwidth = 1;
+        c.gridheight = 1;
+
+        c.weightx = 0.0; // see later
+        c.weighty = 0.0; // see later
+
+    }
+
+    public void newLine()
+    {
+        c.gridx = -1;
+        c.gridy++;
+    }
+
+    public Component add(Component component)
+    {
+        return add(component, 1, 1, null, -1);
+    }
+
+    public Component add(Component component, int colspan, int rowspan)
+    {
+        return add(component, colspan, rowspan, null, -1);
+    }
+
+    public Component add(Component component, int colspan, int rowspan, int anchor)
+    {
+        return add(component, colspan, rowspan, null, anchor);
+    }
+
+    public Component add(Component component, int colspan, int rowspan, Insets insets, int anchor)
+    {
+        c.gridx++;
+        while (isBusy(c.gridx, c.gridy))
+            c.gridx++;
+        c.gridwidth = colspan;
+        c.gridheight = rowspan;
+        for (int i = 0; i < colspan; i++)
+        {
+            for (int j = 0; j < rowspan; j++)
+            {
+                setBusy(c.gridx + i, c.gridy + j);
+            }
+        }
+        if (anchor != -1)
+        {
+            c.anchor = anchor;
+        }
+        else
+        {
+            c.anchor = DEFAULT_ANCHOR;
+        }
+        if (insets != null)
+        {
+            c.insets = insets;
+        }
+        else
+        {
+            c.insets = DEFAULT_INSETS;
+        }
+
+        gridbag.setConstraints(component, c);
+        return super.add(component);
+    }
+
+    private void setBusy(int x, int y)
+    {
+        busy.put(new String(y + "-" + x), new Boolean(true));
+    }
+
+    private boolean isBusy(int x, int y)
+    {
+        return busy.get(new String(c.gridy + "-" + c.gridx)) != null;
+    }
+}