Browse Source

Refactored animation backend

flabbet 1 year ago
parent
commit
adb37722cb
60 changed files with 451 additions and 275 deletions
  1. 3 3
      src/PixiEditor.AvaloniaUI/Helpers/Converters/DurationToMarginConverter.cs
  2. 3 3
      src/PixiEditor.AvaloniaUI/Helpers/Converters/TimelineSliderValueToMarginConverter.cs
  3. 1 1
      src/PixiEditor.AvaloniaUI/Models/Controllers/ClipboardController.cs
  4. 1 1
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/ActionAccumulator.cs
  5. 3 3
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs
  6. 26 25
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/Public/DocumentOperationsModule.cs
  7. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/BrightnessToolExecutor.cs
  8. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs
  9. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs
  10. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/FloodFillToolExecutor.cs
  11. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs
  12. 1 1
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs
  13. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  14. 4 4
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs
  15. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/RectangleToolExecutor.cs
  16. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs
  17. 2 2
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs
  18. 1 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/IAnimationHandler.cs
  19. 28 23
      src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs
  20. 1 1
      src/PixiEditor.AvaloniaUI/Models/Rendering/CanvasUpdater.cs
  21. 23 18
      src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs
  22. 3 1
      src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml
  23. 3 1
      src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml
  24. 3 3
      src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs
  25. 5 5
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentManagerViewModel.cs
  26. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs
  27. 7 10
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  28. 11 8
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/AnimationsViewModel.cs
  29. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ClipboardViewModel.cs
  30. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ColorsViewModel.cs
  31. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/LayersViewModel.cs
  32. 5 2
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/SelectionViewModel.cs
  33. 10 0
      src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs
  34. 4 4
      src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs
  35. 1 1
      src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSlider.cs
  36. 2 2
      src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSliderTrack.cs
  37. 10 1
      src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs
  38. 0 3
      src/PixiEditor.ChangeableDocument/ChangeInfos/Animation/ActiveFrame_ChangeInfo.cs
  39. 3 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberBlendMode_ChangeInfo.cs
  40. 3 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberClipToMemberBelow_ChangeInfo.cs
  41. 3 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberIsVisible_ChangeInfo.cs
  42. 3 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberMaskIsVisible_ChangeInfo.cs
  43. 3 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberOpacity_ChangeInfo.cs
  44. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs
  45. 0 5
      src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrame.cs
  46. 13 9
      src/PixiEditor.ChangeableDocument/Changeables/Animations/RasterKeyFrame.cs
  47. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyLayer.cs
  48. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyRasterKeyFrame.cs
  49. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyRasterLayer.cs
  50. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Layer.cs
  51. 39 12
      src/PixiEditor.ChangeableDocument/Changeables/RasterLayer.cs
  52. 8 6
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs
  53. 44 7
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  54. 34 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs
  55. 8 3
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  56. 5 2
      src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs
  57. 16 15
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  58. 9 5
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs
  59. 27 28
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  60. 43 25
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

+ 3 - 3
src/PixiEditor.AvaloniaUI/Helpers/Converters/DurationToMarginConverter.cs

@@ -12,15 +12,15 @@ internal class DurationToMarginConverter : SingleInstanceMultiValueConverter<Dur
 
     public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     {
-        if (values.Count != 2)
+        if (values.Count != 3)
         {
             return 0;
             throw new ArgumentException("DurationToWidthConverter requires 2 values");
         }
         
-        if(values[0] is int startFrame && values[1] is double scale)
+        if(values[0] is int startFrame && values[1] is double min && values[2] is double scale)
         {
-            return new Thickness(startFrame * scale, 0, 0, 0);
+            return new Thickness((startFrame - min) * scale, 0, 0, 0);
         }
         
         return 0;

+ 3 - 3
src/PixiEditor.AvaloniaUI/Helpers/Converters/TimelineSliderValueToMarginConverter.cs

@@ -12,14 +12,14 @@ internal class TimelineSliderValueToMarginConverter : SingleInstanceMultiValueCo
 
     public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     {
-        if (values.Count != 3)
+        if (values.Count != 4)
         {
             throw new ArgumentException("TimelineSliderValueToMarginConverter requires 3 values");
         }
 
-        if (values[0] is int frame && values[1] is double scale && values[2] is Vector offset)
+        if (values[0] is int frame && values[1] is double minimum && values[2] is double scale && values[3] is Vector offset)
         {
-            return new Thickness(frame * scale - offset.X, 0, 0, 0);
+            return new Thickness((frame - minimum) * scale - offset.X, 0, 0, 0);
         }
 
         return new Thickness();

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Controllers/ClipboardController.cs

@@ -128,7 +128,7 @@ internal static class ClipboardController
             return true;
         }
 
-        document.Operations.PasteImagesAsLayers(images);
+        document.Operations.PasteImagesAsLayers(images, document.AnimationDataViewModel.ActiveFrameBindable);
         return true;
     }
 

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ActionAccumulator.cs

@@ -90,7 +90,7 @@ internal class ActionAccumulator
                 internals.Updater.AfterUndoBoundaryPassed();
 
             // update the contents of the bitmaps
-            var affectedAreas = new AffectedAreasGatherer(internals.Tracker, optimizedChanges);
+            var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameBindable, internals.Tracker, optimizedChanges);
             List<IRenderInfo> renderResult = new();
             renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed || viewportRefreshRequest));
             renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));

+ 3 - 3
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -133,7 +133,7 @@ internal class DocumentUpdater
             case DeleteKeyFrame_ChangeInfo info:
                 ProcessDeleteKeyFrame(info);
                 break;
-            case ActiveFrame_ChangeInfo info:
+            case SetActiveFrame_PassthroughAction info:
                 ProcessActiveFrame(info);
                 break;
             case KeyFrameLength_ChangeInfo info:
@@ -429,9 +429,9 @@ internal class DocumentUpdater
         doc.AnimationHandler.RemoveKeyFrame(info.DeletedKeyFrameId);
     }
     
-    private void ProcessActiveFrame(ActiveFrame_ChangeInfo info)
+    private void ProcessActiveFrame(SetActiveFrame_PassthroughAction info)
     {
-        doc.AnimationHandler.SetActiveFrame(info.ActiveFrame);
+        doc.AnimationHandler.SetActiveFrame(info.Frame);
     }
     
     private void ProcessKeyFrameLength(KeyFrameLength_ChangeInfo info)

+ 26 - 25
src/PixiEditor.AvaloniaUI/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -64,15 +64,15 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// Deletes selected pixels
     /// </summary>
     /// <param name="clearSelection">Should the selection be cleared</param>
-    public void DeleteSelectedPixels(bool clearSelection = false)
+    public void DeleteSelectedPixels(int frame, bool clearSelection = false)
     {
         var member = Document.SelectedStructureMember;
         if (Internals.ChangeController.IsChangeActive || member is null)
             return;
-        bool drawOnMask = member is ILayerHandler layer ? layer.ShouldDrawOnMask : true;
+        bool drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
         if (drawOnMask && !member.HasMaskBindable)
             return;
-        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.GuidValue, drawOnMask));
+        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.GuidValue, drawOnMask, frame));
         if (clearSelection)
             Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
         Internals.ActionAccumulator.AddFinishedActions();
@@ -117,7 +117,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// Pastes the <paramref name="images"/> as new layers
     /// </summary>
     /// <param name="images">The images to paste</param>
-    public void PasteImagesAsLayers(List<DataImage> images)
+    public void PasteImagesAsLayers(List<DataImage> images, int frame)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
@@ -134,7 +134,8 @@ internal class DocumentOperationsModule : IDocumentOperations
         foreach (var imageWithName in images)
         {
             var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, Path.GetFileName(imageWithName.name));
-            DrawImage(imageWithName.image, new ShapeCorners(new RectD(imageWithName.position, imageWithName.image.Size)), layerGuid, true, false, false);
+            DrawImage(imageWithName.image, new ShapeCorners(new RectD(imageWithName.position, imageWithName.image.Size)),
+                layerGuid, true, false, frame, false);
         }
         Internals.ActionAccumulator.AddFinishedActions();
     }
@@ -244,12 +245,12 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// </summary>
     /// <param name="oldColor">The color to replace</param>
     /// <param name="newColor">The new color</param>
-    public void ReplaceColor(PaletteColor oldColor, PaletteColor newColor)
+    public void ReplaceColor(PaletteColor oldColor, PaletteColor newColor, int frame)
     {
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
             return;
         
-        Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor.ToColor(), newColor.ToColor()));
+        Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor.ToColor(), newColor.ToColor(), frame));
         ReplaceInPalette(oldColor, newColor);
     }
 
@@ -288,12 +289,12 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <summary>
     /// Applies the mask to the image
     /// </summary>
-    public void ApplyMask(IStructureMemberHandler member)
+    public void ApplyMask(IStructureMemberHandler member, int frame)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
         
-        Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.GuidValue), new DeleteStructureMemberMask_Action(member.GuidValue));
+        Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.GuidValue, frame), new DeleteStructureMemberMask_Action(member.GuidValue));
     }
 
     /// <summary>
@@ -440,8 +441,8 @@ internal class DocumentOperationsModule : IDocumentOperations
             Internals.ChangeController.TryStopActiveExecutor();
     }
 
-    public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
-        DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, true);
+    public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, int frame) =>
+        DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, frame, true);
 
     /// <summary>
     /// Draws a image on the member with the <paramref name="memberGuid"/>
@@ -452,12 +453,12 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="ignoreClipSymmetriesEtc">Ignore selection clipping and symmetry (See DrawingChangeHelper.ApplyClipsSymmetriesEtc of UpdateableDocument)</param>
     /// <param name="drawOnMask">Draw on the mask or on the image</param>
     /// <param name="finish">Is this a finished action</param>
-    private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
+    private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, int atFrame, bool finish)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
         Internals.ActionAccumulator.AddActions(
-            new PasteImage_Action(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask),
+            new PasteImage_Action(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, atFrame, default),
             new EndPasteImage_Action());
         if (finish)
             Internals.ActionAccumulator.AddFinishedActions();
@@ -470,52 +471,52 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
-        Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
+        Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action(Document.AnimationHandler.ActiveFrameBindable));
     }
 
     /// <summary>
     /// Flips the image on the <paramref name="flipType"/> axis
     /// </summary>
-    public void FlipImage(FlipType flipType) => FlipImage(flipType, null);
+    public void FlipImage(FlipType flipType, int frame) => FlipImage(flipType, null, frame);
 
     /// <summary>
     /// Flips the members with the Guids of <paramref name="membersToFlip"/> on the <paramref name="flipType"/> axis
     /// </summary>
-    public void FlipImage(FlipType flipType, List<Guid> membersToFlip)
+    public void FlipImage(FlipType flipType, List<Guid> membersToFlip, int frame)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
         
-        Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, membersToFlip));
+        Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, frame, membersToFlip));
     }
 
     /// <summary>
     /// Rotates the image
     /// </summary>
     /// <param name="rotation">The degrees to rotate the image by</param>
-    public void RotateImage(RotationAngle rotation) => RotateImage(rotation, null);
+    public void RotateImage(RotationAngle rotation) => RotateImage(rotation, null, -1);
 
     /// <summary>
     /// Rotates the members with the Guids of <paramref name="membersToRotate"/>
     /// </summary>
     /// <param name="rotation">The degrees to rotate the members by</param>
-    public void RotateImage(RotationAngle rotation, List<Guid> membersToRotate)
+    public void RotateImage(RotationAngle rotation, List<Guid> membersToRotate, int frame)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
         
-        Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate));
+        Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate, frame));
     }
     
     /// <summary>
     /// Puts the content of the image in the middle of the canvas
     /// </summary>
-    public void CenterContent(IReadOnlyList<Guid> structureMembers)
+    public void CenterContent(IReadOnlyList<Guid> structureMembers, int frame)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
 
-        Internals.ActionAccumulator.AddFinishedActions(new CenterContent_Action(structureMembers.ToList()));
+        Internals.ActionAccumulator.AddFinishedActions(new CenterContent_Action(structureMembers.ToList(), frame));
     }
 
     /// <summary>
@@ -572,7 +573,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             );
     }
 
-    public void SelectionToMask(SelectionMode mode)
+    public void SelectionToMask(SelectionMode mode, int frame)
     {
         if (Document.SelectedStructureMember is not { } member || Document.SelectionPathBindable.IsEmpty)
             return;
@@ -582,10 +583,10 @@ internal class DocumentOperationsModule : IDocumentOperations
             Internals.ActionAccumulator.AddActions(new CreateStructureMemberMask_Action(member.GuidValue));
         }
         
-        Internals.ActionAccumulator.AddFinishedActions(new SelectionToMask_Action(member.GuidValue, mode));
+        Internals.ActionAccumulator.AddFinishedActions(new SelectionToMask_Action(member.GuidValue, mode, frame));
     }
 
-    public void CropToSelection(bool clearSelection = true)
+    public void CropToSelection(int frame, bool clearSelection = true)
     {
         var bounds = Document.SelectionPathBindable.TightBounds;
         if (Document.SelectionPathBindable.IsEmpty || bounds.Width <= 0 || bounds.Height <= 0)

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/BrightnessToolExecutor.cs

@@ -30,7 +30,7 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
         toolSize = toolbar.ToolSize;
         correctionFactor = tool.Darken || tool.UsedWith == MouseButton.Right ? -tool.CorrectionFactor : tool.CorrectionFactor;
 
-        ChangeBrightness_Action action = new(guidValue, controller!.LastPixelPosition, correctionFactor, toolSize, repeat);
+        ChangeBrightness_Action action = new(guidValue, controller!.LastPixelPosition, correctionFactor, toolSize, repeat, document.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -38,7 +38,7 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        ChangeBrightness_Action action = new(guidValue, pos, correctionFactor, toolSize, repeat);
+        ChangeBrightness_Action action = new(guidValue, pos, correctionFactor, toolSize, repeat, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
     }
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs

@@ -22,7 +22,7 @@ internal class EllipseToolExecutor : ShapeToolExecutor<IEllipseToolHandler>
 
         lastRect = rect;
 
-        internals!.ActionAccumulator.AddActions(new DrawEllipse_Action(memberGuid, rect, strokeColor, fillColor, strokeWidth, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new DrawEllipse_Action(memberGuid, rect, strokeColor, fillColor, strokeWidth, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override ExecutorType Type => ExecutorType.ToolLinked;
@@ -31,7 +31,7 @@ internal class EllipseToolExecutor : ShapeToolExecutor<IEllipseToolHandler>
 
     protected override IAction TransformMovedAction(ShapeData data, ShapeCorners corners) =>
         new DrawEllipse_Action(memberGuid, (RectI)RectD.FromCenterAndSize(data.Center, data.Size), strokeColor,
-            fillColor, strokeWidth, drawOnMask);
+            fillColor, strokeWidth, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
 
     protected override IAction EndDrawAction() => new EndDrawEllipse_Action();
 }

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -41,7 +41,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = new LineBasedPen_Action(guidValue, DrawingApi.Core.ColorsImpl.Colors.Transparent, controller!.LastPixelPosition, toolSize, true,
-            drawOnMask);
+            drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -49,7 +49,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        IAction? action = new LineBasedPen_Action(guidValue, DrawingApi.Core.ColorsImpl.Colors.Transparent, pos, toolSize, true, drawOnMask);
+        IAction? action = new LineBasedPen_Action(guidValue, DrawingApi.Core.ColorsImpl.Colors.Transparent, pos, toolSize, true, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
     }
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/FloodFillToolExecutor.cs

@@ -37,14 +37,14 @@ internal class FloodFillToolExecutor : UpdateableChangeExecutor
         color = colorsVM.PrimaryColor;
         var pos = controller!.LastPixelPosition;
 
-        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
 
         return ExecutionState.Success;
     }
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override void OnLeftMouseButtonUp()

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs

@@ -58,7 +58,7 @@ internal class LineToolExecutor : UpdateableChangeExecutor
         if (toolViewModel!.Snap)
             pos = ShapeToolExecutor<IShapeToolHandler>.Get45IncrementedPosition(startPos, pos);
         curPos = pos;
-        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, startPos, pos, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, startPos, pos, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override void OnLeftMouseButtonUp()
@@ -77,7 +77,7 @@ internal class LineToolExecutor : UpdateableChangeExecutor
     {
         if (!transforming)
             return;
-        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, (VecI)start, (VecI)end, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, (VecI)start, (VecI)end, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override void OnSelectedObjectNudged(VecI distance)

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs

@@ -30,7 +30,7 @@ internal class MagicWandToolExecutor : UpdateableChangeExecutor
             memberGuids = document!.StructureHelper.GetAllLayers().Select(x => x.GuidValue).ToList();
         var pos = controller!.LastPixelPosition;
 
-        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuids, pos, mode));
+        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuids, pos, mode, document!.AnimationHandler.ActiveFrameBindable));
 
         return ExecutionState.Success;
     }

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -50,7 +50,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
         }
 
         ShapeCorners corners = new(new RectD(pos, image.Size));
-        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask, document.AnimationHandler.ActiveFrameBindable, default));
         document.TransformHandler.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners, true);
 
         return ExecutionState.Success;
@@ -58,7 +58,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
 
     public override void OnTransformMoved(ShapeCorners corners)
     {
-        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, default));
     }
 
     public override void OnSelectedObjectNudged(VecI distance) => document!.TransformHandler.Nudge(distance);

+ 4 - 4
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -41,8 +41,8 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, toolSize, false, drawOnMask),
-            true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask)
+            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, toolSize, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);
 
@@ -53,8 +53,8 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     {
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, pos, toolSize, false, drawOnMask),
-            true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask)
+            false => new LineBasedPen_Action(guidValue, color, pos, toolSize, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);
     }

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/RectangleToolExecutor.cs

@@ -22,12 +22,12 @@ internal class RectangleToolExecutor : ShapeToolExecutor<IRectangleToolHandler>
             rect = RectI.FromTwoPixels(startPos, curPos);
         lastRect = rect;
 
-        internals!.ActionAccumulator.AddActions(new DrawRectangle_Action(memberGuid, new ShapeData(rect.Center, rect.Size, 0, strokeWidth, strokeColor, fillColor), drawOnMask));
+        internals!.ActionAccumulator.AddActions(new DrawRectangle_Action(memberGuid, new ShapeData(rect.Center, rect.Size, 0, strokeWidth, strokeColor, fillColor), drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     protected override void DrawShape(VecI currentPos, bool first) => DrawRectangle(currentPos, first);
 
-    protected override IAction TransformMovedAction(ShapeData data, ShapeCorners corners) => new DrawRectangle_Action(memberGuid, data, drawOnMask);
+    protected override IAction TransformMovedAction(ShapeData data, ShapeCorners corners) => new DrawRectangle_Action(memberGuid, data, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
 
     protected override IAction EndDrawAction() => new EndDrawRectangle_Action();
 }

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs

@@ -40,7 +40,7 @@ internal class ShiftLayerExecutor : UpdateableChangeExecutor
         
         startPos = controller!.LastPixelPosition;
 
-        ShiftLayer_Action action = new(_affectedMemberGuids, VecI.Zero, tool.KeepOriginalImage);
+        ShiftLayer_Action action = new(_affectedMemberGuids, VecI.Zero, tool.KeepOriginalImage, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -61,7 +61,7 @@ internal class ShiftLayerExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        ShiftLayer_Action action = new(_affectedMemberGuids, pos - startPos, tool!.KeepOriginalImage);
+        ShiftLayer_Action action = new(_affectedMemberGuids, pos - startPos, tool!.KeepOriginalImage, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
     }
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs

@@ -41,14 +41,14 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
         document.TransformHandler.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners, Type == ExecutorType.Regular);
         membersToTransform = members.Select(static a => a.GuidValue).ToArray();
         internals!.ActionAccumulator.AddActions(
-            new TransformSelectedArea_Action(membersToTransform, corners, tool.KeepOriginalImage, false));
+            new TransformSelectedArea_Action(membersToTransform, corners, tool.KeepOriginalImage, false, document.AnimationHandler.ActiveFrameBindable));
         return ExecutionState.Success;
     }
 
     public override void OnTransformMoved(ShapeCorners corners)
     {
         internals!.ActionAccumulator.AddActions(
-            new TransformSelectedArea_Action(membersToTransform!, corners, tool!.KeepOriginalImage, false));
+            new TransformSelectedArea_Action(membersToTransform!, corners, tool!.KeepOriginalImage, false, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override void OnSelectedObjectNudged(VecI distance) => document!.TransformHandler.Nudge(distance);

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/IAnimationHandler.cs

@@ -4,7 +4,7 @@ internal interface IAnimationHandler
 {
     public IReadOnlyCollection<IKeyFrameHandler> KeyFrames { get; }
     public int ActiveFrameBindable { get; set; }
-    public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, bool cloneFromExisting);
+    public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null);
     public void SetActiveFrame(int newFrame);
     public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration);
     public void SetKeyFrameVisibility(Guid infoKeyFrameId, bool infoIsVisible);

+ 28 - 23
src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs

@@ -1,6 +1,7 @@
 using System.Collections.Generic;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos;
@@ -21,10 +22,14 @@ internal class AffectedAreasGatherer
     public AffectedArea MainImageArea { get; private set; } = new();
     public Dictionary<Guid, AffectedArea> ImagePreviewAreas { get; private set; } = new();
     public Dictionary<Guid, AffectedArea> MaskPreviewAreas { get; private set; } = new();
+    
+    private int ActiveFrame { get; set; }
 
-    public AffectedAreasGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo> changes)
+    public AffectedAreasGatherer(int activeFrame, DocumentChangeTracker tracker,
+        IReadOnlyList<IChangeInfo> changes)
     {
         this.tracker = tracker;
+        ActiveFrame = activeFrame;
         ProcessChanges(changes);
     }
 
@@ -48,8 +53,8 @@ internal class AffectedAreasGatherer
                     AddToImagePreviews(info.GuidValue, info.Area);
                     break;
                 case CreateStructureMember_ChangeInfo info:
-                    AddAllToMainImage(info.GuidValue);
-                    AddAllToImagePreviews(info.GuidValue);
+                    AddAllToMainImage(info.GuidValue, 0);
+                    AddAllToImagePreviews(info.GuidValue, 0);
                     AddAllToMaskPreview(info.GuidValue);
                     break;
                 case DeleteStructureMember_ChangeInfo info:
@@ -57,8 +62,8 @@ internal class AffectedAreasGatherer
                     AddWholeCanvasToImagePreviews(info.ParentGuid);
                     break;
                 case MoveStructureMember_ChangeInfo info:
-                    AddAllToMainImage(info.GuidValue);
-                    AddAllToImagePreviews(info.GuidValue, true);
+                    AddAllToMainImage(info.GuidValue, ActiveFrame);
+                    AddAllToImagePreviews(info.GuidValue, ActiveFrame, true);
                     if (info.ParentFromGuid != info.ParentToGuid)
                         AddWholeCanvasToImagePreviews(info.ParentFromGuid);
                     break;
@@ -73,30 +78,30 @@ internal class AffectedAreasGatherer
                     AddWholeCanvasToImagePreviews(info.GuidValue, true);
                     break;
                 case StructureMemberBlendMode_ChangeInfo info:
-                    AddAllToMainImage(info.GuidValue);
-                    AddAllToImagePreviews(info.GuidValue, true);
+                    AddAllToMainImage(info.GuidValue, ActiveFrame);
+                    AddAllToImagePreviews(info.GuidValue, ActiveFrame, true);
                     break;
                 case StructureMemberClipToMemberBelow_ChangeInfo info:
-                    AddAllToMainImage(info.GuidValue);
-                    AddAllToImagePreviews(info.GuidValue, true);
+                    AddAllToMainImage(info.GuidValue, ActiveFrame);
+                    AddAllToImagePreviews(info.GuidValue, ActiveFrame, true);
                     break;
                 case StructureMemberOpacity_ChangeInfo info:
-                    AddAllToMainImage(info.GuidValue);
-                    AddAllToImagePreviews(info.GuidValue, true);
+                    AddAllToMainImage(info.GuidValue, ActiveFrame);
+                    AddAllToImagePreviews(info.GuidValue, ActiveFrame, true);
                     break;
                 case StructureMemberIsVisible_ChangeInfo info:
-                    AddAllToMainImage(info.GuidValue);
-                    AddAllToImagePreviews(info.GuidValue, true);
+                    AddAllToMainImage(info.GuidValue, ActiveFrame);
+                    AddAllToImagePreviews(info.GuidValue, ActiveFrame, true);
                     break;
                 case StructureMemberMaskIsVisible_ChangeInfo info:
-                    AddAllToMainImage(info.GuidValue, false);
-                    AddAllToImagePreviews(info.GuidValue, true);
+                    AddAllToMainImage(info.GuidValue, ActiveFrame, false);
+                    AddAllToImagePreviews(info.GuidValue, ActiveFrame, true);
                     break;
                 case CreateRasterKeyFrame_ChangeInfo info:
                     if (info.CloneFromExisting)
                     {
-                        AddAllToMainImage(info.TargetLayerGuid);
-                        AddAllToImagePreviews(info.TargetLayerGuid);
+                        AddAllToMainImage(info.TargetLayerGuid, info.Frame);
+                        AddAllToImagePreviews(info.TargetLayerGuid, info.Frame);
                     }
                     else
                     {
@@ -104,7 +109,7 @@ internal class AffectedAreasGatherer
                         AddWholeCanvasToImagePreviews(info.TargetLayerGuid);
                     }
                     break;
-                case ActiveFrame_ChangeInfo:
+                case SetActiveFrame_PassthroughAction:
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToEveryImagePreview();
                     break;
@@ -124,28 +129,28 @@ internal class AffectedAreasGatherer
         }
     }
 
-    private void AddAllToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
+    private void AddAllToImagePreviews(Guid memberGuid, int frame, bool ignoreSelf = false)
     {
         var member = tracker.Document.FindMember(memberGuid);
         if (member is IReadOnlyLayer layer)
         {
-            var chunks = layer.Rasterize().FindAllChunks();
+            var chunks = layer.Rasterize(frame).FindAllChunks();
             AddToImagePreviews(memberGuid, new AffectedArea(chunks), ignoreSelf);
         }
         else if (member is IReadOnlyFolder folder)
         {
             AddWholeCanvasToImagePreviews(memberGuid, ignoreSelf);
             foreach (var child in folder.Children)
-                AddAllToImagePreviews(child.GuidValue);
+                AddAllToImagePreviews(child.GuidValue, frame);
         }
     }
 
-    private void AddAllToMainImage(Guid memberGuid, bool useMask = true)
+    private void AddAllToMainImage(Guid memberGuid, int frame, bool useMask = true)
     {
         var member = tracker.Document.FindMember(memberGuid);
         if (member is IReadOnlyLayer layer)
         {
-            var chunks = layer.Rasterize().FindAllChunks();
+            var chunks = layer.Rasterize(frame).FindAllChunks();
             if (layer.Mask is not null && layer.MaskIsVisible && useMask)
                 chunks.IntersectWith(layer.Mask.FindAllChunks());
             AddToMainImage(new AffectedArea(chunks));

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Rendering/CanvasUpdater.cs

@@ -195,7 +195,7 @@ internal class CanvasUpdater
             screenSurface.DrawingSurface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
         }
 
-        ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot, globalClippingRectangle).Switch(
+        ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot, doc.AnimationHandler.ActiveFrameBindable, globalClippingRectangle).Switch(
             (Chunk chunk) =>
             {
                 screenSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);

+ 23 - 18
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -55,10 +55,13 @@ internal class MemberPreviewUpdater
 
         Dictionary<Guid, (VecI previewSize, RectI tightBounds)?>? changedMainPreviewBounds = null;
         Dictionary<Guid, (VecI previewSize, RectI tightBounds)?>? changedMaskPreviewBounds = null;
+        
+        int atFrame = doc.AnimationHandler.ActiveFrameBindable;
+        
         await Task.Run(() =>
         {
-            changedMainPreviewBounds = FindChangedTightBounds(false);
-            changedMaskPreviewBounds = FindChangedTightBounds(true);
+            changedMainPreviewBounds = FindChangedTightBounds(atFrame, false);
+            changedMaskPreviewBounds = FindChangedTightBounds(atFrame, true);
         }).ConfigureAwait(true);
 
         RecreatePreviewBitmaps(changedMainPreviewBounds!, changedMaskPreviewBounds!);
@@ -94,9 +97,11 @@ internal class MemberPreviewUpdater
         AddAreasToAccumulator(chunkGatherer);
         if (!rerenderPreviews)
             return new List<IRenderInfo>();
+        
+        int frame = doc.AnimationHandler.ActiveFrameBindable;
 
-        var changedMainPreviewBounds = FindChangedTightBounds(false);
-        var changedMaskPreviewBounds = FindChangedTightBounds(true);
+        var changedMainPreviewBounds = FindChangedTightBounds(frame, false);
+        var changedMaskPreviewBounds = FindChangedTightBounds(frame, true);
 
         RecreatePreviewBitmaps(changedMainPreviewBounds, changedMaskPreviewBounds);
         var renderInfos = Render(changedMainPreviewBounds, changedMaskPreviewBounds);
@@ -162,7 +167,7 @@ internal class MemberPreviewUpdater
     /// <summary>
     /// Looks at the accumulated areas and determines which members need to have their preview bitmaps resized or deleted
     /// </summary>
-    private Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> FindChangedTightBounds(bool forMasks)
+    private Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> FindChangedTightBounds(int atFrame, bool forMasks)
     {
         // VecI? == null stands for "layer is empty, the preview needs to be deleted"
         Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> newPreviewBitmapSizes = new();
@@ -181,7 +186,7 @@ internal class MemberPreviewUpdater
                 continue;
             }
 
-            RectI? tightBounds = GetOrFindMemberTightBounds(member, area, forMasks);
+            RectI? tightBounds = GetOrFindMemberTightBounds(member, atFrame, area, forMasks);
             RectI? maybeLastBounds = targetLastBounds.TryGetValue(guid, out RectI lastBounds) ? lastBounds : null;
             if (tightBounds == maybeLastBounds)
                 continue;
@@ -271,7 +276,7 @@ internal class MemberPreviewUpdater
     /// Returns the previosly known committed tight bounds if there are no reasons to believe they have changed (based on the passed <paramref name="currentlyAffectedArea"/>).
     /// Otherwise, calculates the new bounds via <see cref="FindLayerTightBounds"/> and returns them.
     /// </summary>
-    private RectI? GetOrFindMemberTightBounds(IReadOnlyStructureMember member, AffectedArea currentlyAffectedArea, bool forMask)
+    private RectI? GetOrFindMemberTightBounds(IReadOnlyStructureMember member, int atFrame, AffectedArea currentlyAffectedArea, bool forMask)
     {
         if (forMask && member.Mask is null)
             throw new InvalidOperationException();
@@ -291,8 +296,8 @@ internal class MemberPreviewUpdater
 
         return member switch
         {
-            IReadOnlyLayer layer => FindLayerTightBounds(layer, forMask),
-            IReadOnlyFolder folder => FindFolderTightBounds(folder, forMask),
+            IReadOnlyLayer layer => FindLayerTightBounds(layer, atFrame, forMask),
+            IReadOnlyFolder folder => FindFolderTightBounds(folder, atFrame, forMask),
             _ => throw new ArgumentOutOfRangeException()
         };
     }
@@ -300,7 +305,7 @@ internal class MemberPreviewUpdater
     /// <summary>
     /// Finds the current committed tight bounds for a layer.
     /// </summary>
-    private RectI? FindLayerTightBounds(IReadOnlyLayer layer, bool forMask)
+    private RectI? FindLayerTightBounds(IReadOnlyLayer layer, int frame, bool forMask)
     {
         if (layer.Mask is null && forMask)
             throw new InvalidOperationException();
@@ -310,16 +315,16 @@ internal class MemberPreviewUpdater
 
         if (layer is IReadOnlyRasterLayer raster)
         {
-            return FindImageTightBoundsFast(raster.LayerImage);
+            return FindImageTightBoundsFast(raster.GetLayerImageAtFrame(frame));
         }
 
-        return layer.GetTightBounds();
+        return layer.GetTightBounds(frame);
     }
 
     /// <summary>
     /// Finds the current committed tight bounds for a folder recursively.
     /// </summary>
-    private RectI? FindFolderTightBounds(IReadOnlyFolder folder, bool forMask)
+    private RectI? FindFolderTightBounds(IReadOnlyFolder folder, int frame, bool forMask)
     {
         if (forMask)
         {
@@ -334,9 +339,9 @@ internal class MemberPreviewUpdater
             RectI? curBounds = null;
             
             if (child is IReadOnlyLayer childLayer)
-                curBounds = FindLayerTightBounds(childLayer, false);
+                curBounds = FindLayerTightBounds(childLayer, frame, false);
             else if (child is IReadOnlyFolder childFolder)
-                curBounds = FindFolderTightBounds(childFolder, false);
+                curBounds = FindFolderTightBounds(childFolder, frame, false);
 
             if (combinedBounds is null)
                 combinedBounds = curBounds;
@@ -428,7 +433,7 @@ internal class MemberPreviewUpdater
                 _ => ChunkResolution.Eighth,
             };
             var pos = chunkPos * resolution.PixelSize();
-            var rendered = ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot);
+            var rendered = ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot, doc.AnimationHandler.ActiveFrameBindable);
             doc.PreviewSurface.DrawingSurface.Canvas.Save();
             doc.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
             doc.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
@@ -534,7 +539,7 @@ internal class MemberPreviewUpdater
             var pos = chunk * ChunkResolution.Full.PixelSize();
             // drawing in full res here is kinda slow
             // we could switch to a lower resolution based on (canvas size / preview size) to make it run faster
-            OneOf<Chunk, EmptyChunk> rendered = ChunkRenderer.MergeWholeStructure(chunk, ChunkResolution.Full, folder);
+            OneOf<Chunk, EmptyChunk> rendered = ChunkRenderer.MergeWholeStructure(chunk, ChunkResolution.Full, folder, doc.AnimationHandler.ActiveFrameBindable);
             if (rendered.IsT0)
             {
                 memberVM.PreviewSurface.DrawingSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
@@ -561,7 +566,7 @@ internal class MemberPreviewUpdater
         foreach (var chunk in area.Chunks)
         {
             var pos = chunk * ChunkResolution.Full.PixelSize();
-            if (!layer.Rasterize().DrawCommittedChunkOn(chunk, ChunkResolution.Full, memberVM.PreviewSurface.DrawingSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
+            if (!layer.Rasterize(doc.AnimationHandler.ActiveFrameBindable).DrawCommittedChunkOn(chunk, ChunkResolution.Full, memberVM.PreviewSurface.DrawingSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
                 memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint);
         }
 

+ 3 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -60,7 +60,7 @@
                             MinLeftOffset="{Binding MinLeftOffset, RelativeSource={RelativeSource TemplatedParent}}"
                             IsSnapToTickEnabled="True"
                             Name="PART_TimelineSlider"
-                            Minimum="0">
+                            Minimum="1">
                             <animations:TimelineSlider.Maximum>
                                 <MultiBinding>
                                     <MultiBinding.Converter>
@@ -92,6 +92,7 @@
                                     <MultiBinding Converter="{converters:TimelineSliderValueToMarginConverter}">
                                         <Binding Path="ActiveFrame"
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
+                                        <Binding Path="Minimum" ElementName="PART_TimelineSlider"/>
                                         <Binding Path="Scale"
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
                                         <Binding Path="ScrollOffset"
@@ -159,6 +160,7 @@
                                                 Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
                                                 IsEnabled="{Binding IsVisible}"
                                                 IsSelected="{Binding IsSelected, Mode=TwoWay}"
+                                                Min="{Binding ElementName=PART_TimelineSlider, Path=Minimum}"
                                                 Item="{Binding}">
                                                 <animations:KeyFrame.Width>
                                                     <MultiBinding Converter="{converters:DurationToWidthConverter}">

+ 3 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml

@@ -37,12 +37,14 @@
                             Scale="{TemplateBinding Scale}"
                             Offset="{TemplateBinding Offset}"
                             MinLeftOffset="{TemplateBinding MinLeftOffset}"
-                            Fill="White" />
+                            MinValue="{TemplateBinding Minimum}"
+                            Fill="{DynamicResource ThemeForegroundBrush}" />
                         <animations:TimelineSliderTrack Name="PART_Track"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
                                Margin="15, 0, 0, 0"
                                ScaleFactor="{TemplateBinding Scale}"
                                Offset="{TemplateBinding Offset}"
+                               Minimum="{TemplateBinding Minimum}"
                                Orientation="Horizontal">
                             <Track.IncreaseButton>
                                 <RepeatButton Name="PART_IncreaseButton"

+ 3 - 3
src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs

@@ -12,7 +12,7 @@ namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
 internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 {
-    private int _activeFrameBindable;
+    private int _activeFrameBindable = 1;
     private int _frameRate = 60;
     public DocumentViewModel Document { get; }
     protected DocumentInternalParts Internals { get; }
@@ -54,12 +54,12 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         Internals = internals;
     }
 
-    public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, bool cloneFromExisting)
+    public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null)
     {
         if (!Document.UpdateableChangeActive)
         {
             Internals.ActionAccumulator.AddFinishedActions(new CreateRasterKeyFrame_Action(targetLayerGuid, Guid.NewGuid(), frame,
-                cloneFromExisting));
+                frameToCopyFrom ?? -1, toCloneFrom ?? Guid.Empty));
         }
     }
 

+ 5 - 5
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentManagerViewModel.cs

@@ -70,7 +70,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         MenuItemPath = "IMAGE/FLIP/FLIP_IMG_HORIZONTALLY", MenuItemOrder = 14, Icon = PixiPerfectIcons.XFlip)]
     [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "FLIP_IMG_VERTICALLY", "FLIP_IMG_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
         MenuItemPath = "IMAGE/FLIP/FLIP_IMG_VERTICALLY", MenuItemOrder = 15, Icon = PixiPerfectIcons.YFlip)]
-    public void FlipImage(FlipType type) => ActiveDocument?.Operations.FlipImage(type);
+    public void FlipImage(FlipType type) => ActiveDocument?.Operations.FlipImage(type, activeDocument.AnimationDataViewModel.ActiveFrameBindable);
 
     [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "FLIP_LAYERS_HORIZONTALLY", "FLIP_LAYERS_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
         MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_HORIZONTALLY", MenuItemOrder = 16, Icon = PixiPerfectIcons.XSelectedFlip)]
@@ -81,7 +81,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
 
-        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers());
+        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
 
     [Command.Basic("PixiEditor.Document.Rotate90Deg", "ROT_IMG_90",
@@ -109,7 +109,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
         
-        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers());
+        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
 
     [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "TOGGLE_VERT_SYMMETRY_AXIS", "TOGGLE_VERT_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument", 
@@ -160,7 +160,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6)]
     public void DeletePixels()
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels();
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
 
 
@@ -199,6 +199,6 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         if(ActiveDocument?.SelectedStructureMember == null)
             return;
         
-        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers());
+        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
 }

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -36,7 +36,7 @@ internal partial class DocumentViewModel
         {
             Width = Width, Height = Height,
             Swatches = ToCollection(Swatches), Palette = ToCollection(Palette),
-            RootFolder = root, PreviewImage = (TryRenderWholeImage(AnimationDataViewModel.ActiveFrameBindable).Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray(),
+            RootFolder = root, PreviewImage = (TryRenderWholeImage(0).Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray(),
             ReferenceLayer = GetReferenceLayer(doc),
             AnimationData = ToAnimationData(doc.AnimationData)
         };

+ 7 - 10
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -295,7 +295,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             if (member is DocumentViewModelBuilder.LayerBuilder layer && layer.Surface is not null)
             {
                 PasteImage(member.GuidValue, layer.Surface, layer.Width, layer.Height, layer.OffsetX, layer.OffsetY,
-                    false);
+                    false, 0);
             }
 
             acc.AddActions(
@@ -311,7 +311,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 if (!member.Mask.IsVisible)
                     acc.AddActions(new StructureMemberMaskIsVisible_Action(member.Mask.IsVisible, member.GuidValue));
 
-                PasteImage(member.GuidValue, member.Mask.Surface, maskSurface.Size.X, maskSurface.Size.Y, 0, 0, true);
+                PasteImage(member.GuidValue, member.Mask.Surface, maskSurface.Size.X, maskSurface.Size.Y, 0, 0, true, 0);
             }
 
             acc.AddFinishedActions();
@@ -323,11 +323,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
 
         void PasteImage(Guid guid, DocumentViewModelBuilder.SurfaceBuilder surface, int width, int height, int offsetX,
-            int offsetY, bool onMask)
+            int offsetY, bool onMask, int frame, Guid? keyFrameGuid = default)
         {
             acc.AddActions(
                 new PasteImage_Action(surface.Surface, new(new RectD(new VecD(offsetX, offsetY), new(width, height))),
-                    guid, true, onMask),
+                    guid, true, onMask, frame, keyFrameGuid ?? default),
                 new EndPasteImage_Action());
         }
 
@@ -359,13 +359,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                         new CreateRasterKeyFrame_Action(
                             rasterKeyFrameBuilder.LayerGuid,
                             rasterKeyFrameBuilder.Id,
-                            rasterKeyFrameBuilder.StartFrame,
-                            false),
+                            rasterKeyFrameBuilder.StartFrame, -1, default),
                         new KeyFrameLength_Action(rasterKeyFrameBuilder.Id, rasterKeyFrameBuilder.StartFrame, rasterKeyFrameBuilder.Duration),
                         new EndKeyFrameLength_Action());
                     
                     PasteImage(rasterKeyFrameBuilder.LayerGuid, rasterKeyFrameBuilder.Surface, rasterKeyFrameBuilder.Surface.Surface.Size.X,
-                        rasterKeyFrameBuilder.Surface.Surface.Size.Y, 0, 0, false);
+                        rasterKeyFrameBuilder.Surface.Surface.Size.Y, 0, 0, false, rasterKeyFrameBuilder.StartFrame, rasterKeyFrameBuilder.Id);
                     
                     acc.AddFinishedActions();
                 }
@@ -732,8 +731,6 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         int framesCount = AnimationDataViewModel.FramesCount;
         int lastFrame = firstFrame + framesCount;
 
-        int activeFrame = AnimationDataViewModel.ActiveFrameBindable;
-
         Image[] images = new Image[framesCount];
         for (int i = firstFrame; i < lastFrame; i++)
         {
@@ -748,7 +745,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 surface = processFrameAction(surface.AsT1);
             }
 
-            images[i] = surface.AsT1.DrawingSurface.Snapshot();
+            images[i - firstFrame] = surface.AsT1.DrawingSurface.Snapshot();
             surface.AsT1.Dispose();
         }
 

+ 11 - 8
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -27,11 +27,14 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
         }
 
         int newFrame = GetActiveFrame(activeDocument, activeDocument.SelectedStructureMember.GuidValue);
-        
+       
+        Guid toCloneFrom = duplicate ? activeDocument.SelectedStructureMember.GuidValue : Guid.Empty;
+        int frameToCopyFrom = duplicate ? activeDocument.AnimationDataViewModel.ActiveFrameBindable : -1;
+
         activeDocument.AnimationDataViewModel.CreateRasterKeyFrame(
-            activeDocument.SelectedStructureMember.GuidValue, 
+            activeDocument.SelectedStructureMember.GuidValue,
             newFrame,
-            duplicate);
+            toCloneFrom);
         
         activeDocument.Operations.SetActiveFrame(newFrame);
     }
@@ -96,8 +99,8 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
             return;
-        
-        Owner.DocumentManagerSubViewModel.ActiveDocument.EventInlet.StartChangeActiveFrame();
+        // TODO: same as below
+        //Owner.DocumentManagerSubViewModel.ActiveDocument.EventInlet.StartChangeActiveFrame();
     }
     
     [Command.Internal("PixiEditor.Document.ChangeActiveFrame", CanExecute = "PixiEditor.HasDocument")]
@@ -107,8 +110,8 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
             return;
         
         int intNewActiveFrame = (int)newActiveFrame;
-        
-        Owner.DocumentManagerSubViewModel.ActiveDocument.EventInlet.ChangeActiveFrame(intNewActiveFrame);
+        // TODO: Check if this should be implemented
+        //Owner.DocumentManagerSubViewModel.ActiveDocument.EventInlet.ChangeActiveFrame(intNewActiveFrame);
     }
 
     [Command.Internal("PixiEditor.Document.EndChangeActiveFrame", CanExecute = "PixiEditor.HasDocument")]
@@ -117,7 +120,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
             return;
         
-        Owner.DocumentManagerSubViewModel.ActiveDocument.EventInlet.EndChangeActiveFrame();
+        //Owner.DocumentManagerSubViewModel.ActiveDocument.EventInlet.EndChangeActiveFrame();
     }
     
     [Command.Internal("PixiEditor.Animation.ActiveFrameSet")]

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -39,7 +39,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         if (doc is null)
             return;
         await Copy();
-        doc.Operations.DeleteSelectedPixels(true);
+        doc.Operations.DeleteSelectedPixels(doc.AnimationDataViewModel.ActiveFrameBindable, true);
     }
 
     [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ColorsViewModel.cs

@@ -110,7 +110,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null || colors.oldColor == colors.newColor)
             return;
-        doc.Operations.ReplaceColor(colors.oldColor, colors.newColor);
+        doc.Operations.ReplaceColor(colors.oldColor, colors.newColor, doc.AnimationDataViewModel.ActiveFrameBindable);
     }
 
     [Command.Basic("PixiEditor.Colors.ReplaceSecondaryByPrimaryColor", false, "REPLACE_SECONDARY_BY_PRIMARY", "REPLACE_SECONDARY_BY_PRIMARY", IconEvaluator = "PixiEditor.Colors.ReplaceColorIcon")]

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/LayersViewModel.cs

@@ -280,7 +280,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         if (member is null || !member.HasMaskBindable)
             return;
         
-        doc!.Operations.ApplyMask(member);
+        doc!.Operations.ApplyMask(member, doc.AnimationDataViewModel.ActiveFrameBindable);
     }
 
     [Command.Basic("PixiEditor.Layer.ToggleVisible", "TOGGLE_VISIBILITY", "TOGGLE_VISIBILITY", CanExecute = "PixiEditor.HasDocument",

+ 5 - 2
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/SelectionViewModel.cs

@@ -84,7 +84,10 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
     [Command.Filter("PixiEditor.Selection.ToMaskMenu", "SELECTION_TO_MASK", "SELECTION_TO_MASK", Key = Key.M, Modifiers = KeyModifiers.Control)]
     public void SelectionToMask(SelectionMode mode)
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.SelectionToMask(mode);
+        if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
+            return;
+        
+        Owner.DocumentManagerSubViewModel.ActiveDocument.Operations.SelectionToMask(mode, Owner.DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
 
     [Command.Basic("PixiEditor.Selection.CropToSelection", "CROP_TO_SELECTION", "CROP_TO_SELECTION_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty",
@@ -93,7 +96,7 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
     {
         var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
         
-        document!.Operations.CropToSelection();
+        document!.Operations.CropToSelection(document.AnimationDataViewModel.ActiveFrameBindable);
     }
 
     [Evaluator.CanExecute("PixiEditor.Selection.CanNudgeSelectedObject")]

+ 10 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs

@@ -25,6 +25,15 @@ internal class KeyFrame : TemplatedControl
     public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<KeyFrame, bool>(
         nameof(IsSelected));
 
+    public static readonly StyledProperty<double> MinProperty = AvaloniaProperty.Register<KeyFrame, double>(
+        nameof(Min), 1);
+
+    public double Min
+    {
+        get => GetValue(MinProperty);
+        set => SetValue(MinProperty, value);
+    }
+
     public bool IsSelected
     {
         get => GetValue(IsSelectedProperty);
@@ -74,6 +83,7 @@ internal class KeyFrame : TemplatedControl
                 Bindings =
                 {
                     new Binding("StartFrameBindable") { Source = Item }, 
+                    new Binding("Min") { Source = this },
                     new Binding("Scale") { Source = this },
                 }
             };

+ 4 - 4
src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs

@@ -33,7 +33,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             nameof(KeyFrames));
 
     public static readonly StyledProperty<int> ActiveFrameProperty =
-        AvaloniaProperty.Register<Timeline, int>(nameof(ActiveFrame));
+        AvaloniaProperty.Register<Timeline, int>(nameof(ActiveFrame), 1);
 
     public static readonly StyledProperty<bool> IsPlayingProperty = AvaloniaProperty.Register<Timeline, bool>(
         nameof(IsPlaying));
@@ -323,9 +323,9 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
     private void PlayTimerOnTick(object? sender, EventArgs e)
     {
-        if (ActiveFrame >= KeyFrames.FrameCount)
+        if (ActiveFrame >= KeyFrames.FrameCount + 1)
         {
-            ActiveFrame = 0;
+            ActiveFrame = 1;
         }
         else
         {
@@ -512,7 +512,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             frame = (int)Math.Floor(x / Scale);
         }
 
-        frame = Math.Max(0, frame);
+        frame = Math.Max(1, frame);
         return frame;
     }
 

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSlider.cs

@@ -103,7 +103,7 @@ public class TimelineSlider : Slider
         const double marginLeft = 15;
         
         double x = point.Position.X - marginLeft + Offset.X;
-        int value = (int)Math.Round(x / Scale);
+        int value = (int)Math.Round(x / Scale) + (int)Minimum;
         
         Value = value;
     }

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineSliderTrack.cs

@@ -29,7 +29,7 @@ internal class TimelineSliderTrack : Track
 
     static TimelineSliderTrack()
     {
-        AffectsArrange<TimelineSliderTrack>(ScaleFactorProperty, OffsetProperty);
+        AffectsArrange<TimelineSliderTrack>(ScaleFactorProperty, OffsetProperty, MinimumProperty);
     }
 
     // Override the ArrangeOverride method
@@ -38,7 +38,7 @@ internal class TimelineSliderTrack : Track
         base.ArrangeOverride(finalSize);
         if (Thumb != null)
         {
-            double scaledValue = Value * ScaleFactor;
+            double scaledValue = (Value - Minimum) * ScaleFactor;
             double thumbLength = Orientation == Orientation.Horizontal ? Thumb.DesiredSize.Width : Thumb.DesiredSize.Height;
             
             double thumbPosition = scaledValue - Offset.X;

+ 10 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs

@@ -17,6 +17,15 @@ public class TimelineTickBar : Control
     
     public static readonly StyledProperty<Vector> OffsetProperty = AvaloniaProperty.Register<TimelineTickBar, Vector>("Offset");
 
+    public static readonly StyledProperty<int> MinValueProperty = AvaloniaProperty.Register<TimelineTickBar, int>(
+        nameof(MinValue), 1);
+
+    public int MinValue
+    {
+        get => GetValue(MinValueProperty);
+        set => SetValue(MinValueProperty, value);
+    }
+
     public double Scale
     {
         get => GetValue(ScaleProperty);
@@ -107,7 +116,7 @@ public class TimelineTickBar : Control
             double x = i * frameWidth - Offset.X + MinLeftOffset;
             context.DrawLine(largeTickPen, new Point(x, height), new Point(x, height * 0.55f));
             
-            var text = new FormattedText(i.ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
+            var text = new FormattedText((i + MinValue).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
                 Typeface.Default, 12, Fill);
             
             double textCenter = text.WidthIncludingTrailingWhitespace / 2;

+ 0 - 3
src/PixiEditor.ChangeableDocument/ChangeInfos/Animation/ActiveFrame_ChangeInfo.cs

@@ -1,3 +0,0 @@
-namespace PixiEditor.ChangeableDocument.ChangeInfos.Animation;
-
-public record ActiveFrame_ChangeInfo(int ActiveFrame) : IChangeInfo;

+ 3 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberBlendMode_ChangeInfo.cs

@@ -1,4 +1,6 @@
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Properties;
-public record class StructureMemberBlendMode_ChangeInfo(Guid GuidValue, BlendMode BlendMode) : IChangeInfo;
+public record class StructureMemberBlendMode_ChangeInfo(Guid GuidValue, BlendMode BlendMode) : IChangeInfo
+{
+}

+ 3 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberClipToMemberBelow_ChangeInfo.cs

@@ -1,2 +1,4 @@
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Properties;
-public record class StructureMemberClipToMemberBelow_ChangeInfo(Guid GuidValue, bool ClipToMemberBelow) : IChangeInfo;
+public record class StructureMemberClipToMemberBelow_ChangeInfo(Guid GuidValue, bool ClipToMemberBelow) : IChangeInfo
+{
+}

+ 3 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberIsVisible_ChangeInfo.cs

@@ -1,3 +1,5 @@
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
-public record class StructureMemberIsVisible_ChangeInfo(Guid GuidValue, bool IsVisible) : IChangeInfo;
+public record class StructureMemberIsVisible_ChangeInfo(Guid GuidValue, bool IsVisible) : IChangeInfo
+{
+}

+ 3 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberMaskIsVisible_ChangeInfo.cs

@@ -1,2 +1,4 @@
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Properties;
-public record class StructureMemberMaskIsVisible_ChangeInfo(Guid GuidValue, bool IsVisible) : IChangeInfo;
+public record class StructureMemberMaskIsVisible_ChangeInfo(Guid GuidValue, bool IsVisible) : IChangeInfo
+{
+}

+ 3 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberOpacity_ChangeInfo.cs

@@ -1,3 +1,5 @@
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
-public record class StructureMemberOpacity_ChangeInfo(Guid GuidValue, float Opacity) : IChangeInfo;
+public record class StructureMemberOpacity_ChangeInfo(Guid GuidValue, float Opacity) : IChangeInfo
+{
+}

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs

@@ -33,6 +33,11 @@ internal class AnimationData : IReadOnlyAnimationData
     {
         TryFindKeyFrameCallback<KeyFrame>(createdKeyFrameId, out _, (frame, parent) =>
         {
+            if (document.TryFindMember<Layer>(frame.LayerGuid, out Layer? layer))
+            {
+                layer.RemoveKeyFrame(frame.Id);
+            }
+            
             parent?.Children.Remove(frame);
         });
     }

+ 0 - 5
src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrame.cs

@@ -8,8 +8,6 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
     private int duration;
     private bool isVisible = true;
     
-    public event Action KeyFrameChanged;
-
     public virtual int StartFrame
     {
         get => startFrame;
@@ -21,7 +19,6 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
             }
 
             startFrame = value;
-            KeyFrameChanged?.Invoke();
         }
     }
 
@@ -36,7 +33,6 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
             }
 
             duration = value;
-            KeyFrameChanged?.Invoke();
         }
     }
     
@@ -52,7 +48,6 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
         {
             isVisible = value;
             OnVisibilityChanged();
-            KeyFrameChanged?.Invoke();
         }
     }
 

+ 13 - 9
src/PixiEditor.ChangeableDocument/Changeables/Animations/RasterKeyFrame.cs

@@ -4,27 +4,31 @@ namespace PixiEditor.ChangeableDocument.Changeables.Animations;
 
 internal class RasterKeyFrame : KeyFrame, IReadOnlyRasterKeyFrame
 {
-    public ChunkyImage Image { get; set; }
     public Document Document { get; set; }
+
+    private RasterLayer targetLayer;
+    private ChunkyImage targetImage;
     
-    IReadOnlyChunkyImage IReadOnlyRasterKeyFrame.Image => Image;
-    
-    public RasterKeyFrame(Guid targetLayerGuid, int startFrame, Document document, ChunkyImage? cloneFrom = null)
-        : base(targetLayerGuid, startFrame)
+    public RasterKeyFrame(Guid id, RasterLayer layer, int startFrame, Document document, ChunkyImage? cloneFrom = null)
+        : base(layer.GuidValue, startFrame)
     {
-        Image = cloneFrom?.CloneFromCommitted() ?? new ChunkyImage(document.Size);
+        Id = id;
+        targetLayer = layer;
+        targetImage = cloneFrom?.CloneFromCommitted() ?? new ChunkyImage(document.Size);
+        layer.AddFrame(Id, startFrame, 1, targetImage);
 
         Document = document;
     }
     
     public override KeyFrame Clone()
     {
-        var image = Image.CloneFromCommitted();
-        return new RasterKeyFrame(LayerGuid, StartFrame, Document, image) { Id = this.Id };
+        var image = targetImage.CloneFromCommitted();
+        return new RasterKeyFrame(Id, targetLayer, StartFrame, Document, image);
     }
 
     public override void Dispose()
     {
-        Image.Dispose();
     }
+
+    public IReadOnlyChunkyImage Image => targetImage;
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyLayer.cs

@@ -6,4 +6,5 @@ namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 public interface IReadOnlyLayer : IReadOnlyStructureMember
 {
     public ChunkyImage Rasterize(KeyFrameTime frameTime);
+    public void RemoveKeyFrame(Guid keyFrameGuid);
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyRasterKeyFrame.cs

@@ -2,5 +2,5 @@
 
 public interface IReadOnlyRasterKeyFrame : IReadOnlyKeyFrame
 {
-    public IReadOnlyChunkyImage Image { get; }
+    IReadOnlyChunkyImage Image { get; }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyRasterLayer.cs

@@ -7,4 +7,5 @@ public interface IReadOnlyRasterLayer : ITransparencyLockable
     /// </summary>
     IReadOnlyChunkyImage GetLayerImageAtFrame(int frame);
     void SetLayerImageAtFrame(int frame, IReadOnlyChunkyImage image);
+    public void ForEveryFrame(Action<IReadOnlyChunkyImage> action);
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Layer.cs

@@ -7,4 +7,5 @@ namespace PixiEditor.ChangeableDocument.Changeables;
 internal abstract class Layer : StructureMember, IReadOnlyLayer
 {
     public abstract ChunkyImage Rasterize(KeyFrameTime frameTime);
+    public abstract void RemoveKeyFrame(Guid keyFrameGuid);
 }

+ 39 - 12
src/PixiEditor.ChangeableDocument/Changeables/RasterLayer.cs

@@ -15,7 +15,7 @@ internal class RasterLayer : Layer, IReadOnlyRasterLayer
 
     public RasterLayer(VecI size)
     {
-        frameImages.Add(new ImageFrame(0, 0, new(size)));
+        frameImages.Add(new ImageFrame(Guid.NewGuid(), 0, 0, new(size)));
     }
 
     public RasterLayer(List<ImageFrame> frames)
@@ -37,15 +37,30 @@ internal class RasterLayer : Layer, IReadOnlyRasterLayer
 
     IReadOnlyChunkyImage IReadOnlyRasterLayer.GetLayerImageAtFrame(int frame) => GetLayerImageAtFrame(frame);
     void IReadOnlyRasterLayer.SetLayerImageAtFrame(int frame, IReadOnlyChunkyImage newLayerImage) => SetLayerImageAtFrame(frame, (ChunkyImage)newLayerImage);
-    
+
+    void IReadOnlyRasterLayer.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
+
+    public void ForEveryFrame(Action<ChunkyImage> action)
+    {
+        foreach (var frame in frameImages)
+        {
+            action(frame.Image);
+        }
+    }
+
     public ChunkyImage GetLayerImageAtFrame(int frame)
     {
         return Rasterize(frame);
     }
+    
+    public ChunkyImage GetLayerImageByKeyFrameGuid(Guid keyFrameGuid)
+    {
+        return frameImages.FirstOrDefault(x => x.KeyFrameGuid == keyFrameGuid)?.Image ?? frameImages[0].Image;
+    }
 
     public override ChunkyImage Rasterize(KeyFrameTime frameTime)
     {
-        if (frameImages.Count == 0)
+        if (frameTime.Frame == 0 || frameImages.Count == 1)
         {
             return frameImages[0].Image;
         }
@@ -55,6 +70,12 @@ internal class RasterLayer : Layer, IReadOnlyRasterLayer
         return frame?.Image ?? frameImages[0].Image;
     }
 
+    public override void RemoveKeyFrame(Guid keyFrameGuid)
+    {
+        // Remove all in case I'm the lucky winner of guid collision
+        frameImages.RemoveAll(x => x.KeyFrameGuid == keyFrameGuid);
+    }
+
     public override RectI? GetTightBounds(int frame)
     {
         return Rasterize(frame).FindTightCommittedBounds();
@@ -68,7 +89,7 @@ internal class RasterLayer : Layer, IReadOnlyRasterLayer
         List<ImageFrame> clonedFrames = new();
         foreach (var frame in frameImages)
         {
-            clonedFrames.Add(new ImageFrame(frame.StartFrame, frame.EndFrame, frame.Image.CloneFromCommitted()));
+            clonedFrames.Add(new ImageFrame(frame.KeyFrameGuid, frame.StartFrame, frame.Duration, frame.Image.CloneFromCommitted()));
         }
         
         return new RasterLayer(clonedFrames)
@@ -93,28 +114,34 @@ internal class RasterLayer : Layer, IReadOnlyRasterLayer
             existingFrame.Image.Dispose();
             existingFrame.Image = newLayerImage;
         }
-        else
-        {
-            frameImages.Add(new ImageFrame(frame, frame, newLayerImage));
-        }
+    }
+
+
+    public void AddFrame(Guid keyFrameGuid, int startFrame, int duration, ChunkyImage frameImg)
+    {
+        ImageFrame newFrame = new(keyFrameGuid, startFrame, duration, frameImg);
+        frameImages.Add(newFrame);
     }
 }
 
 class ImageFrame
 {
     public int StartFrame { get; set; }
-    public int EndFrame { get; set; }
+    public int Duration { get; set; }
     public ChunkyImage Image { get; set; }
+    
+    public Guid KeyFrameGuid { get; set; }
 
-    public ImageFrame(int startFrame, int endFrame, ChunkyImage image)
+    public ImageFrame(Guid keyFrameGuid, int startFrame, int duration, ChunkyImage image)
     {
         StartFrame = startFrame;
-        EndFrame = endFrame;
+        Duration = duration;
         Image = image;
+        KeyFrameGuid = keyFrameGuid;
     }
 
     public bool IsInFrame(int frame)
     {
-        return frame >= StartFrame && frame <= EndFrame;
+        return frame >= StartFrame && frame < StartFrame + Duration;
     }
 }

+ 8 - 6
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs

@@ -14,14 +14,14 @@ internal class CreateRasterKeyFrame_Change : Change
 
     [GenerateMakeChangeAction]
     public CreateRasterKeyFrame_Change(Guid targetLayerGuid, Guid newKeyFrameGuid, int frame,
-        int? cloneFromFrame = null,
-        Guid? cloneFromExisting = null)
+        int cloneFromFrame = -1,
+        Guid cloneFromExisting = default)
     {
         _targetLayerGuid = targetLayerGuid;
         _frame = frame;
-        cloneFrom = cloneFromExisting;
+        cloneFrom = cloneFromExisting != default ? cloneFromExisting : null;
         createdKeyFrameId = newKeyFrameGuid;
-        this.cloneFromFrame = cloneFromFrame;
+        this.cloneFromFrame = cloneFromFrame < 0 ? null : cloneFromFrame;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -35,9 +35,11 @@ internal class CreateRasterKeyFrame_Change : Change
         var cloneFromImage = cloneFrom.HasValue
             ? target.FindMemberOrThrow<RasterLayer>(cloneFrom.Value).GetLayerImageAtFrame(cloneFromFrame ?? 0)
             : null;
+        
+        RasterLayer targetLayer = target.FindMemberOrThrow<RasterLayer>(_targetLayerGuid);
+        
         var keyFrame =
-            new RasterKeyFrame(_targetLayerGuid, _frame, target, cloneFromImage);
-        keyFrame.Id = createdKeyFrameId;
+            new RasterKeyFrame(createdKeyFrameId, targetLayer, _frame, target, cloneFromImage);
         target.AnimationData.AddKeyFrame(keyFrame);
         ignoreInUndo = false;
         return new CreateRasterKeyFrame_ChangeInfo(_targetLayerGuid, _frame, createdKeyFrameId, cloneFrom.HasValue);

+ 44 - 7
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs

@@ -2,15 +2,25 @@
 using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
 internal static class DrawingChangeHelper
 {
-    public static AffectedArea ApplyStoredChunksDisposeAndSetToNull(Document target, Guid memberGuid, bool drawOnMask, int frame, ref CommittedChunkStorage? storage)
+    public static AffectedArea ApplyStoredChunksDisposeAndSetToNull(Document target, Guid memberGuid, bool drawOnMask,
+        int frame, ref CommittedChunkStorage? storage)
     {
         var image = GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
         return ApplyStoredChunksDisposeAndSetToNull(image, ref storage);
     }
 
-    public static AffectedArea ApplyStoredChunksDisposeAndSetToNull(ChunkyImage image, ref CommittedChunkStorage? storage)
+    public static AffectedArea ApplyStoredChunksDisposeAndSetToNull(Document target, Guid memberGuid, bool drawOnMask,
+        Guid targetKeyFrameGuid, ref CommittedChunkStorage? savedChunks)
+    {
+        var image = GetTargetImageOrThrow(target, memberGuid, drawOnMask, targetKeyFrameGuid);
+        return ApplyStoredChunksDisposeAndSetToNull(image, ref savedChunks);
+    }
+
+    public static AffectedArea ApplyStoredChunksDisposeAndSetToNull(ChunkyImage image,
+        ref CommittedChunkStorage? storage)
     {
         if (storage is null)
             throw new InvalidOperationException("No stored chunks to apply");
@@ -22,18 +32,43 @@ internal static class DrawingChangeHelper
         return area;
     }
 
+    public static ChunkyImage GetTargetImageOrThrow(Document target, Guid memberGuid, bool drawOnMask,
+        Guid targetKeyFrameGuid)
+    {
+        var member = target.FindMemberOrThrow(memberGuid);
+
+        if (drawOnMask)
+        {
+            if (member.Mask is null)
+                throw new InvalidOperationException("Trying to draw on a mask that doesn't exist");
+            return member.Mask;
+        }
+
+        if (member is Folder)
+        {
+            throw new InvalidOperationException("Trying to draw on a folder");
+        }
+
+        if (member is not RasterLayer layer)
+        {
+            throw new InvalidOperationException("Trying to draw on a non-raster layer member");
+        }
+
+        return layer.GetLayerImageByKeyFrameGuid(targetKeyFrameGuid);
+    }
+
     public static ChunkyImage GetTargetImageOrThrow(Document target, Guid memberGuid, bool drawOnMask, int frame)
     {
         // TODO: Figure out if this should work only for raster layers or should rasterize any
         var member = target.FindMemberOrThrow(memberGuid);
-        
+
         if (drawOnMask)
         {
             if (member.Mask is null)
                 throw new InvalidOperationException("Trying to draw on a mask that doesn't exist");
             return member.Mask;
         }
-        
+
         if (member is Folder)
         {
             throw new InvalidOperationException("Trying to draw on a folder");
@@ -43,11 +78,12 @@ internal static class DrawingChangeHelper
         {
             throw new InvalidOperationException("Trying to draw on a non-raster layer member");
         }
-        
+
         return layer.GetLayerImageAtFrame(frame);
     }
 
-    public static void ApplyClipsSymmetriesEtc(Document target, ChunkyImage targetImage, Guid targetMemberGuid, bool drawOnMask)
+    public static void ApplyClipsSymmetriesEtc(Document target, ChunkyImage targetImage, Guid targetMemberGuid,
+        bool drawOnMask)
     {
         if (!target.Selection.SelectionPath.IsEmpty)
             targetImage.SetClippingPath(target.Selection.SelectionPath);
@@ -79,7 +115,8 @@ internal static class DrawingChangeHelper
         };
     }
 
-    public static OneOf<None, IChangeInfo, List<IChangeInfo>> CreateAreaChangeInfo(Guid memberGuid, AffectedArea affectedArea, bool drawOnMask) =>
+    public static OneOf<None, IChangeInfo, List<IChangeInfo>> CreateAreaChangeInfo(Guid memberGuid,
+        AffectedArea affectedArea, bool drawOnMask) =>
         drawOnMask switch
         {
             false => new LayerImageArea_ChangeInfo(memberGuid, affectedArea),

+ 34 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs

@@ -11,19 +11,22 @@ internal class PasteImage_UpdateableChange : UpdateableChange
     private readonly bool drawOnMask;
     private readonly Surface imageToPaste;
     private CommittedChunkStorage? savedChunks;
-    private int frame;
+    private int? frame;
+    private Guid? targetKeyFrameGuid;
     private static Paint RegularPaint { get; } = new Paint() { BlendMode = BlendMode.SrcOver };
 
     private bool hasEnqueudImage = false;
 
     [GenerateUpdateableChangeActions]
-    public PasteImage_UpdateableChange(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipsSymmetriesEtc, bool isDrawingOnMask, int frame)
+    public PasteImage_UpdateableChange(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipsSymmetriesEtc, bool isDrawingOnMask, int frame, Guid targetKeyFrameGuid)
     {
         this.corners = corners;
         this.memberGuid = memberGuid;
         this.ignoreClipsSymmetriesEtc = ignoreClipsSymmetriesEtc;
         this.drawOnMask = isDrawingOnMask;
         this.imageToPaste = new Surface(image);
+        this.frame = frame;
+        this.targetKeyFrameGuid = targetKeyFrameGuid;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -54,7 +57,16 @@ internal class PasteImage_UpdateableChange : UpdateableChange
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
+        ChunkyImage targetImage;
+        if (targetKeyFrameGuid.HasValue && targetKeyFrameGuid != Guid.Empty)
+        {
+            targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, targetKeyFrameGuid.Value);
+        }
+        else
+        {
+            targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame.Value);
+        }
+        
         var chunks = DrawImage(target, targetImage);
         savedChunks?.Dispose();
         savedChunks = new(targetImage, targetImage.FindAffectedArea().Chunks);
@@ -66,13 +78,30 @@ internal class PasteImage_UpdateableChange : UpdateableChange
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
+        ChunkyImage targetImage;
+        if (targetKeyFrameGuid.HasValue && targetKeyFrameGuid != Guid.Empty)
+        {
+            targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, targetKeyFrameGuid.Value);
+        }
+        else
+        {
+            targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame.Value);
+        }
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, DrawImage(target, targetImage), drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, frame, ref savedChunks);
+        AffectedArea chunks;
+        if (targetKeyFrameGuid.HasValue && targetKeyFrameGuid != Guid.Empty)
+        {
+            chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, targetKeyFrameGuid.Value, ref savedChunks);
+        }
+        else
+        {
+            chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, frame.Value, ref savedChunks);
+        }
+        
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, chunks, drawOnMask);
     }
 

+ 8 - 3
src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs

@@ -6,9 +6,11 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 
 internal class ClipCanvas_Change : ResizeBasedChangeBase
 {
+    private int frameToClip;
     [GenerateMakeChangeAction]
-    public ClipCanvas_Change(int frame) : base(frame)
+    public ClipCanvas_Change(int clipToFrame)
     {
+        frameToClip = clipToFrame;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -18,7 +20,7 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
         {
             if (member is Layer layer)
             {
-                var layerBounds = layer.GetTightBounds(frame);
+                var layerBounds = layer.GetTightBounds(frameToClip);
                 if (layerBounds.HasValue)
                 {
                     bounds ??= layerBounds.Value;
@@ -43,7 +45,10 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
         {
             if (member is RasterLayer layer)
             {
-                Resize(layer.GetLayerImageAtFrame(frame), layer.GuidValue, newBounds.Size, -newBounds.Pos, deletedChunks);
+                layer.ForEveryFrame(img =>
+                {
+                    Resize(img, layer.GuidValue, newBounds.Size, -newBounds.Pos, deletedChunks);
+                });
             }
             
             if (member.Mask is null)

+ 5 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs

@@ -9,7 +9,7 @@ internal class Crop_Change : ResizeBasedChangeBase
     private RectI rect;
     
     [GenerateMakeChangeAction]
-    public Crop_Change(RectI rect, int frame) : base(frame)
+    public Crop_Change(RectI rect)
     {
         this.rect = rect;
     }
@@ -36,7 +36,10 @@ internal class Crop_Change : ResizeBasedChangeBase
         {
             if (member is RasterLayer layer)
             {
-                Resize(layer.GetLayerImageAtFrame(frame), layer.GuidValue, rect.Size, rect.Pos * -1, deletedChunks);
+                layer.ForEveryFrame(frame =>
+                {
+                    Resize(frame, layer.GuidValue, rect.Size, rect.Pos * -1, deletedChunks);
+                });
             }
             if (member.Mask is null)
                 return;

+ 16 - 15
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -11,13 +11,11 @@ internal abstract class ResizeBasedChangeBase : Change
     protected double _originalVerAxisX;
     protected Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     protected Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
-    protected int frame;
-    
-    public ResizeBasedChangeBase(int frame)
+
+    public ResizeBasedChangeBase()
     {
-        this.frame = frame;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         _originalSize = target.Size;
@@ -25,11 +23,12 @@ internal abstract class ResizeBasedChangeBase : Change
         _originalVerAxisX = target.VerticalSymmetryAxisX;
         return true;
     }
-    
+
     /// <summary>
     /// Notice: this commits image changes, you won't have a chance to revert or set ignoreInUndo to true
     /// </summary>
-    protected virtual void Resize(ChunkyImage img, Guid memberGuid, VecI size, VecI offset, Dictionary<Guid, CommittedChunkStorage> deletedChunksDict)
+    protected virtual void Resize(ChunkyImage img, Guid memberGuid, VecI size, VecI offset,
+        Dictionary<Guid, CommittedChunkStorage> deletedChunksDict)
     {
         img.EnqueueResize(size);
         img.EnqueueClear();
@@ -38,7 +37,7 @@ internal abstract class ResizeBasedChangeBase : Change
         deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, img.FindAffectedArea().Chunks));
         img.CommitChanges();
     }
-    
+
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         target.Size = _originalSize;
@@ -46,14 +45,16 @@ internal abstract class ResizeBasedChangeBase : Change
         {
             if (member is RasterLayer layer)
             {
-                var layerImage = layer.GetLayerImageAtFrame(frame);
-                layerImage.EnqueueResize(_originalSize);
-                deletedChunks[layer.GuidValue].ApplyChunksToImage(layerImage);
-                layerImage.CommitChanges();
+                layer.ForEveryFrame(img =>
+                {
+                    img.EnqueueResize(_originalSize);
+                    deletedChunks[layer.GuidValue].ApplyChunksToImage(img);
+                    img.CommitChanges();
+                });
             }
 
             // TODO: Add support for different Layer types?
-            
+
             if (member.Mask is null)
                 return;
             member.Mask.EnqueueResize(_originalSize);
@@ -68,7 +69,7 @@ internal abstract class ResizeBasedChangeBase : Change
 
         return new Size_ChangeInfo(_originalSize, _originalVerAxisX, _originalHorAxisY);
     }
-    
+
     private void DisposeDeletedChunks()
     {
         foreach (var stored in deletedChunks)
@@ -79,7 +80,7 @@ internal abstract class ResizeBasedChangeBase : Change
             stored.Value.Dispose();
         deletedMaskChunks = new();
     }
-    
+
     public override void Dispose()
     {
         DisposeDeletedChunks();

+ 9 - 5
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -11,7 +11,7 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
     private readonly ResizeAnchor anchor;
 
     [GenerateMakeChangeAction]
-    public ResizeCanvas_Change(VecI size, ResizeAnchor anchor, int frame) : base(frame)
+    public ResizeCanvas_Change(VecI size, ResizeAnchor anchor)
     {
         newSize = size;
         this.anchor = anchor;
@@ -21,11 +21,12 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
     {
         if (newSize.X < 1 || newSize.Y < 1)
             return false;
-        
+
         return base.InitializeAndValidate(target);
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (_originalSize == newSize)
         {
@@ -47,7 +48,10 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
         {
             if (member is RasterLayer layer)
             {
-                Resize(layer.GetLayerImageAtFrame(frame), layer.GuidValue, newSize, offset, deletedChunks);
+                layer.ForEveryFrame(img =>
+                {
+                    Resize(img, layer.GuidValue, newSize, offset, deletedChunks);
+                });
             }
 
             // TODO: Check if adding support for different Layer types is necessary
@@ -57,7 +61,7 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
 
             Resize(member.Mask, member.GuidValue, newSize, offset, deletedMaskChunks);
         });
-        
+
         ignoreInUndo = false;
         return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
     }

+ 27 - 28
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -15,24 +15,22 @@ internal class ResizeImage_Change : Change
     private VecI originalSize;
     private double originalHorAxisY;
     private double originalVerAxisX;
-    private int frame;
-    
+
     private Dictionary<Guid, CommittedChunkStorage> savedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> savedMaskChunks = new();
 
     [GenerateMakeChangeAction]
-    public ResizeImage_Change(VecI size, ResamplingMethod method, int frame)
+    public ResizeImage_Change(VecI size, ResamplingMethod method)
     {
         this.newSize = size;
         this.method = method;
-        this.frame = frame;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         if (newSize.X < 1 || newSize.Y < 1)
             return false;
-        
+
         originalSize = target.Size;
         originalHorAxisY = target.HorizontalSymmetryAxisY;
         originalVerAxisX = target.VerticalSymmetryAxisX;
@@ -53,31 +51,28 @@ internal class ResizeImage_Change : Change
     {
         using Surface originalSurface = new(originalSize);
         image.DrawMostUpToDateRegionOn(
-            new(VecI.Zero, originalSize), 
+            new(VecI.Zero, originalSize),
             ChunkResolution.Full,
             originalSurface.DrawingSurface,
             VecI.Zero);
-        
+
         bool downscaling = newSize.LengthSquared < originalSize.LengthSquared;
         FilterQuality quality = ToFilterQuality(method, downscaling);
-        using Paint paint = new()
-        {
-            FilterQuality = quality, 
-            BlendMode = BlendMode.Src,
-        };
+        using Paint paint = new() { FilterQuality = quality, BlendMode = BlendMode.Src, };
 
         using Surface newSurface = new(newSize);
         newSurface.DrawingSurface.Canvas.Save();
         newSurface.DrawingSurface.Canvas.Scale(newSize.X / (float)originalSize.X, newSize.Y / (float)originalSize.Y);
         newSurface.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
         newSurface.DrawingSurface.Canvas.Restore();
-        
+
         image.EnqueueResize(newSize);
         image.EnqueueClear();
         image.EnqueueDrawImage(VecI.Zero, newSurface);
     }
-    
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (originalSize == newSize)
         {
@@ -93,11 +88,13 @@ internal class ResizeImage_Change : Change
         {
             if (member is RasterLayer layer)
             {
-                var layerImage = layer.GetLayerImageAtFrame(frame);
-                ScaleChunkyImage(layerImage);
-                var affected = layerImage.FindAffectedArea();
-                savedChunks[layer.GuidValue] = new CommittedChunkStorage(layerImage, affected.Chunks);
-                layerImage.CommitChanges();
+                layer.ForEveryFrame(img =>
+                {
+                    ScaleChunkyImage(img);
+                    var affected = img.FindAffectedArea();
+                    savedChunks[layer.GuidValue] = new CommittedChunkStorage(img, affected.Chunks);
+                    img.CommitChanges();
+                });
             }
 
             // Add support for different Layer types
@@ -122,13 +119,15 @@ internal class ResizeImage_Change : Change
         {
             if (member is RasterLayer layer)
             {
-                var layerImage = layer.GetLayerImageAtFrame(frame);
-                layerImage.EnqueueResize(originalSize);
-                layerImage.EnqueueClear();
-                savedChunks[layer.GuidValue].ApplyChunksToImage(layerImage);
-                layerImage.CommitChanges();
+                layer.ForEveryFrame(layerImage =>
+                {
+                    layerImage.EnqueueResize(originalSize);
+                    layerImage.EnqueueClear();
+                    savedChunks[layer.GuidValue].ApplyChunksToImage(layerImage);
+                    layerImage.CommitChanges();
+                });
             }
-            
+
             if (member.Mask is not null)
             {
                 member.Mask.EnqueueResize(originalSize);
@@ -144,7 +143,7 @@ internal class ResizeImage_Change : Change
         foreach (var stored in savedChunks)
             stored.Value.Dispose();
         savedChunks = new();
-        
+
         foreach (var stored in savedMaskChunks)
             stored.Value.Dispose();
         savedMaskChunks = new();

+ 43 - 25
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -13,45 +13,46 @@ internal sealed class RotateImage_Change : Change
 {
     private readonly RotationAngle rotation;
     private List<Guid> membersToRotate;
-    
+
     private VecI originalSize;
     private double originalHorAxisY;
     private double originalVerAxisX;
     private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
-    int frame;
-    
+    private int? frame;
+
     [GenerateMakeChangeAction]
     public RotateImage_Change(RotationAngle rotation, List<Guid>? membersToRotate, int frame)
     {
         this.rotation = rotation;
         membersToRotate ??= new List<Guid>();
         this.membersToRotate = membersToRotate;
-        this.frame = frame;
+        this.frame = frame < 0 ? null : frame;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         if (membersToRotate.Count > 0)
         {
             membersToRotate = target.ExtractLayers(membersToRotate);
-            
+
             foreach (var layer in membersToRotate)
             {
                 if (!target.HasMember(layer)) return false;
-            }  
+            }
         }
-        
+
         originalSize = target.Size;
         originalHorAxisY = target.HorizontalSymmetryAxisY;
         originalVerAxisX = target.VerticalSymmetryAxisX;
         return true;
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         var changes = Rotate(target);
-        
+
         ignoreInUndo = false;
         return changes;
     }
@@ -71,21 +72,18 @@ internal sealed class RotateImage_Change : Change
 
         int originalWidth = bounds.Width;
         int originalHeight = bounds.Height;
-        
+
         int newWidth = rotation == RotationAngle.D180 ? originalWidth : originalHeight;
         int newHeight = rotation == RotationAngle.D180 ? originalHeight : originalWidth;
 
         VecI oldSize = new VecI(originalWidth, originalHeight);
         VecI newSize = new VecI(newWidth, newHeight);
-        
-        using Paint paint = new()
-        {
-            BlendMode = DrawingApi.Core.Surface.BlendMode.Src
-        };
-        
+
+        using Paint paint = new() { BlendMode = DrawingApi.Core.Surface.BlendMode.Src };
+
         using Surface originalSurface = new(oldSize);
         img.DrawMostUpToDateRegionOn(
-            bounds, 
+            bounds,
             ChunkResolution.Full,
             originalSurface.DrawingSurface,
             VecI.Zero);
@@ -103,14 +101,14 @@ internal sealed class RotateImage_Change : Change
                 translationX = 0;
                 break;
         }
-        
+
         flipped.DrawingSurface.Canvas.Save();
         flipped.DrawingSurface.Canvas.Translate(translationX, translationY);
         flipped.DrawingSurface.Canvas.RotateRadians(RotationAngleToRadians(rotation), 0, 0);
         flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
         flipped.DrawingSurface.Canvas.Restore();
 
-        if (membersToRotate.Count == 0) 
+        if (membersToRotate.Count == 0)
         {
             img.EnqueueResize(newSize);
         }
@@ -144,7 +142,17 @@ internal sealed class RotateImage_Change : Change
             {
                 if (member is RasterLayer layer)
                 {
-                    Resize(layer.GetLayerImageAtFrame(frame), layer.GuidValue, deletedChunks, changes);
+                    if (frame != null)
+                    {
+                        Resize(layer.GetLayerImageAtFrame(frame.Value), layer.GuidValue, deletedChunks, changes);
+                    }
+                    else
+                    {
+                        layer.ForEveryFrame(img =>
+                        {
+                            Resize(img, layer.GuidValue, deletedChunks, changes);
+                        });
+                    }
                 }
 
                 // TODO: Add support for different Layer types
@@ -177,7 +185,17 @@ internal sealed class RotateImage_Change : Change
         {
             if (member is RasterLayer layer)
             {
-                Resize(layer.GetLayerImageAtFrame(frame), layer.GuidValue, deletedChunks, null);
+                if (frame != null)
+                {
+                    Resize(layer.GetLayerImageAtFrame(frame.Value), layer.GuidValue, deletedChunks, null);
+                }
+                else
+                {
+                    layer.ForEveryFrame(img =>
+                    {
+                        Resize(img, layer.GuidValue, deletedChunks, null);
+                    });
+                }
             }
 
             if (member.Mask is null)
@@ -188,7 +206,7 @@ internal sealed class RotateImage_Change : Change
 
         return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
     }
-    
+
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         if (membersToRotate.Count == 0)
@@ -215,10 +233,10 @@ internal sealed class RotateImage_Change : Change
         List<IChangeInfo> revertChanges = new List<IChangeInfo>();
         target.ForEveryMember((member) =>
         {
-            if(membersToRotate.Count > 0 && !membersToRotate.Contains(member.GuidValue)) return;
+            if (membersToRotate.Count > 0 && !membersToRotate.Contains(member.GuidValue)) return;
             if (member is RasterLayer layer)
             {
-                var layerImage = layer.GetLayerImageAtFrame(frame);
+                var layerImage = layer.GetLayerImageAtFrame(frame.Value);
                 layerImage.EnqueueResize(originalSize);
                 deletedChunks[layer.GuidValue].ApplyChunksToImage(layerImage);
                 revertChanges.Add(new LayerImageArea_ChangeInfo(layer.GuidValue, layerImage.FindAffectedArea()));