Browse Source

Fixed input funcs cast error and made default animation length

flabbet 1 year ago
parent
commit
06629c71f8
40 changed files with 179 additions and 62 deletions
  1. 1 1
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/ActionAccumulator.cs
  2. 4 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/IAnimationHandler.cs
  3. 7 6
      src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs
  4. 1 1
      src/PixiEditor.AvaloniaUI/Models/Rendering/CanvasUpdater.cs
  5. 1 1
      src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs
  6. 11 2
      src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs
  7. 9 6
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  8. 10 1
      src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs
  9. 1 0
      src/PixiEditor.AvaloniaUI/Views/Dock/TimelineDockView.axaml
  10. 8 6
      src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameTime.cs
  11. 6 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs
  12. 42 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  13. 8 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  15. 38 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs
  16. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CircleNode.cs
  17. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs
  19. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs
  21. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs
  23. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs
  24. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs
  25. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs
  26. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs
  27. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  29. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs
  30. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/KernelNode.cs
  31. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  32. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MatrixTransformNode.cs
  33. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  34. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  35. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  36. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  37. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  38. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  39. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  40. 6 5
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

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

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

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

@@ -1,9 +1,12 @@
-namespace PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+
+namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
 
 internal interface IAnimationHandler
 internal interface IAnimationHandler
 {
 {
     public IReadOnlyCollection<IKeyFrameHandler> KeyFrames { get; }
     public IReadOnlyCollection<IKeyFrameHandler> KeyFrames { get; }
     public int ActiveFrameBindable { get; set; }
     public int ActiveFrameBindable { get; set; }
+    public KeyFrameTime ActiveFrameTime { get; }
     public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null);
     public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null);
     public void SetActiveFrame(int newFrame);
     public void SetActiveFrame(int newFrame);
     public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration);
     public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration);

+ 7 - 6
src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs

@@ -4,6 +4,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos;
@@ -26,9 +27,9 @@ internal class AffectedAreasGatherer
     public Dictionary<Guid, AffectedArea> ImagePreviewAreas { get; private set; } = new();
     public Dictionary<Guid, AffectedArea> ImagePreviewAreas { get; private set; } = new();
     public Dictionary<Guid, AffectedArea> MaskPreviewAreas { get; private set; } = new();
     public Dictionary<Guid, AffectedArea> MaskPreviewAreas { get; private set; } = new();
     
     
-    private int ActiveFrame { get; set; }
+    private KeyFrameTime ActiveFrame { get; set; }
 
 
-    public AffectedAreasGatherer(int activeFrame, DocumentChangeTracker tracker,
+    public AffectedAreasGatherer(KeyFrameTime activeFrame, DocumentChangeTracker tracker,
         IReadOnlyList<IChangeInfo> changes)
         IReadOnlyList<IChangeInfo> changes)
     {
     {
         this.tracker = tracker;
         this.tracker = tracker;
@@ -140,12 +141,12 @@ internal class AffectedAreasGatherer
         }
         }
     }
     }
 
 
-    private void AddAllToImagePreviews(Guid memberGuid, int frame, bool ignoreSelf = false)
+    private void AddAllToImagePreviews(Guid memberGuid, KeyFrameTime frame, bool ignoreSelf = false)
     {
     {
         var member = tracker.Document.FindMember(memberGuid);
         var member = tracker.Document.FindMember(memberGuid);
         if (member is IReadOnlyImageNode layer)
         if (member is IReadOnlyImageNode layer)
         {
         {
-            var result = layer.GetLayerImageAtFrame(frame);
+            var result = layer.GetLayerImageAtFrame(frame.Frame);
             if (result == null)
             if (result == null)
             {
             {
                 AddWholeCanvasToImagePreviews(memberGuid, ignoreSelf);
                 AddWholeCanvasToImagePreviews(memberGuid, ignoreSelf);
@@ -163,12 +164,12 @@ internal class AffectedAreasGatherer
         }
         }
     }
     }
 
 
-    private void AddAllToMainImage(Guid memberGuid, int frame, bool useMask = true)
+    private void AddAllToMainImage(Guid memberGuid, KeyFrameTime frame, bool useMask = true)
     {
     {
         var member = tracker.Document.FindMember(memberGuid);
         var member = tracker.Document.FindMember(memberGuid);
         if (member is IReadOnlyImageNode layer)
         if (member is IReadOnlyImageNode layer)
         {
         {
-            var result = layer.GetLayerImageAtFrame(frame);
+            var result = layer.GetLayerImageAtFrame(frame.Frame);
             if (result == null)
             if (result == null)
             {
             {
                 AddWholeCanvasToMainImage();
                 AddWholeCanvasToMainImage();

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

@@ -198,7 +198,7 @@ internal class CanvasUpdater
             screenSurface.DrawingSurface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
             screenSurface.DrawingSurface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
         }
         }
 
 
-        doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameBindable, globalClippingRectangle).Switch(
+        doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime, globalClippingRectangle).Switch(
             (Chunk chunk) =>
             (Chunk chunk) =>
             {
             {
                 screenSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
                 screenSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);

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

@@ -438,7 +438,7 @@ internal class MemberPreviewUpdater
                 _ => ChunkResolution.Eighth,
                 _ => ChunkResolution.Eighth,
             };
             };
             var pos = chunkPos * resolution.PixelSize();
             var pos = chunkPos * resolution.PixelSize();
-            var rendered = doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameBindable);
+            var rendered = doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime);
             doc.PreviewSurface.DrawingSurface.Canvas.Save();
             doc.PreviewSurface.DrawingSurface.Canvas.Save();
             doc.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
             doc.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
             doc.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
             doc.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)cumulative.GlobalArea);

+ 11 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs

@@ -6,6 +6,7 @@ using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
@@ -41,12 +42,18 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         {
         {
             _frameRate = value;
             _frameRate = value;
             OnPropertyChanged(nameof(FrameRate));
             OnPropertyChanged(nameof(FrameRate));
+            OnPropertyChanged(nameof(DefaultEndFrame));
         }
         }
     }
     }
 
 
     public int FirstFrame => keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 0;
     public int FirstFrame => keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 0;
-    public int LastFrame => keyFrames.Count > 0 ? keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable) : 0;
-    public int FramesCount => LastFrame - FirstFrame + 1; 
+    public int LastFrame => keyFrames.Count > 0 ? keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable) 
+        : DefaultEndFrame;
+    public int FramesCount => LastFrame - FirstFrame + 1;
+    
+    private double ActiveNormalizedTime => (double)(ActiveFrameBindable - FirstFrame) / FramesCount;
+
+    private int DefaultEndFrame => FrameRate; // 1 second
 
 
     public AnimationDataViewModel(DocumentViewModel document, DocumentInternalParts internals)
     public AnimationDataViewModel(DocumentViewModel document, DocumentInternalParts internals)
     {
     {
@@ -54,6 +61,8 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         Internals = internals;
         Internals = internals;
     }
     }
 
 
+    public KeyFrameTime ActiveFrameTime => new KeyFrameTime(ActiveFrameBindable, ActiveNormalizedTime);
+
     public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null)
     public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null)
     {
     {
         if (!Document.UpdateableChangeActive)
         if (!Document.UpdateableChangeActive)

+ 9 - 6
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -26,6 +26,7 @@ using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Actions.Undo;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -408,7 +409,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// Tries rendering the whole document
     /// Tries rendering the whole document
     /// </summary>
     /// </summary>
     /// <returns><see cref="Error"/> if the ChunkyImage was disposed, otherwise a <see cref="Surface"/> of the rendered document</returns>
     /// <returns><see cref="Error"/> if the ChunkyImage was disposed, otherwise a <see cref="Surface"/> of the rendered document</returns>
-    public OneOf<Error, Surface> TryRenderWholeImage(int frame)
+    public OneOf<Error, Surface> TryRenderWholeImage(KeyFrameTime frameTime)
     {
     {
         try
         try
         {
         {
@@ -419,7 +420,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 for (int j = 0; j < sizeInChunks.Y; j++)
                 for (int j = 0; j < sizeInChunks.Y; j++)
                 {
                 {
                     // TODO: Implement this
                     // TODO: Implement this
-                    var maybeChunk = Renderer.RenderChunk(new(i, j), ChunkResolution.Full, frame);
+                    var maybeChunk = Renderer.RenderChunk(new(i, j), ChunkResolution.Full, frameTime);
                     if (maybeChunk.IsT1)
                     if (maybeChunk.IsT1)
                         continue;
                         continue;
                     using Chunk chunk = maybeChunk.AsT0;
                     using Chunk chunk = maybeChunk.AsT0;
@@ -552,7 +553,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         return bitmap.GetSRGBPixel(new VecI((int)transformed.X, (int)transformed.Y));
         return bitmap.GetSRGBPixel(new VecI((int)transformed.X, (int)transformed.Y));
     }
     }
 
 
-    public Color PickColorFromCanvas(VecI pos, DocumentScope scope, int frame)
+    public Color PickColorFromCanvas(VecI pos, DocumentScope scope, KeyFrameTime frameTime)
     {
     {
         // there is a tiny chance that the image might get disposed by another thread
         // there is a tiny chance that the image might get disposed by another thread
         try
         try
@@ -563,7 +564,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             {
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
                 return Renderer.RenderChunk(chunkPos, ChunkResolution.Full,
                 return Renderer.RenderChunk(chunkPos, ChunkResolution.Full,
-                        frame)
+                        frameTime)
                     .Match(
                     .Match(
                         chunk =>
                         chunk =>
                         {
                         {
@@ -580,7 +581,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             IReadOnlyStructureNode? maybeMember = Internals.Tracker.Document.FindMember(layerVm.Id);
             IReadOnlyStructureNode? maybeMember = Internals.Tracker.Document.FindMember(layerVm.Id);
             if (maybeMember is not IReadOnlyImageNode layer)
             if (maybeMember is not IReadOnlyImageNode layer)
                 return Colors.Transparent;
                 return Colors.Transparent;
-            return layer.GetLayerImageAtFrame(frame).GetMostUpToDatePixel(pos);
+            return layer.GetLayerImageAtFrame(frameTime.Frame).GetMostUpToDatePixel(pos);
         }
         }
         catch (ObjectDisposedException)
         catch (ObjectDisposedException)
         {
         {
@@ -751,7 +752,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         Image[] images = new Image[framesCount];
         Image[] images = new Image[framesCount];
         for (int i = firstFrame; i < lastFrame; i++)
         for (int i = firstFrame; i < lastFrame; i++)
         {
         {
-            var surface = TryRenderWholeImage(i);
+            double normalizedTime = (double)(i - firstFrame) / framesCount;
+            KeyFrameTime frameTime = new KeyFrameTime(i, normalizedTime);
+            var surface = TryRenderWholeImage(frameTime);
             if (surface.IsT0)
             if (surface.IsT0)
             {
             {
                 continue;
                 continue;

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

@@ -82,6 +82,15 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
     public static readonly StyledProperty<ICommand> ChangeKeyFramesLengthCommandProperty = AvaloniaProperty.Register<Timeline, ICommand>(
     public static readonly StyledProperty<ICommand> ChangeKeyFramesLengthCommandProperty = AvaloniaProperty.Register<Timeline, ICommand>(
         nameof(ChangeKeyFramesLengthCommand));
         nameof(ChangeKeyFramesLengthCommand));
 
 
+    public static readonly StyledProperty<int> DefaultEndFrameProperty = AvaloniaProperty.Register<Timeline, int>(
+        nameof(DefaultEndFrame));
+
+    public int DefaultEndFrame
+    {
+        get => GetValue(DefaultEndFrameProperty);
+        set => SetValue(DefaultEndFrameProperty, value);
+    }
+
     public ICommand ChangeKeyFramesLengthCommand
     public ICommand ChangeKeyFramesLengthCommand
     {
     {
         get => GetValue(ChangeKeyFramesLengthCommandProperty);
         get => GetValue(ChangeKeyFramesLengthCommandProperty);
@@ -323,7 +332,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
 
     private void PlayTimerOnTick(object? sender, EventArgs e)
     private void PlayTimerOnTick(object? sender, EventArgs e)
     {
     {
-        if (ActiveFrame >= KeyFrames.FrameCount)
+        if (ActiveFrame >= (KeyFrames.Count > 0 ? KeyFrames.FrameCount : DefaultEndFrame))
         {
         {
             ActiveFrame = 1;
             ActiveFrame = 1;
         }
         }

+ 1 - 0
src/PixiEditor.AvaloniaUI/Views/Dock/TimelineDockView.axaml

@@ -17,6 +17,7 @@
         ActiveFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Mode=TwoWay}"
         ActiveFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Mode=TwoWay}"
         NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateRasterKeyFrame}"
         NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateRasterKeyFrame}"
         Fps="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.FrameRate, Mode=TwoWay}"
         Fps="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.FrameRate, Mode=TwoWay}"
+        DefaultEndFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.DefaultEndFrame}"
         DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateRasterKeyFrame}"
         DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateRasterKeyFrame}"
         DeleteKeyFrameCommand="{xaml:Command PixiEditor.Animation.DeleteKeyFrames, UseProvided=True}"
         DeleteKeyFrameCommand="{xaml:Command PixiEditor.Animation.DeleteKeyFrames, UseProvided=True}"
         ChangeKeyFramesLengthCommand="{xaml:Command PixiEditor.Animation.ChangeKeyFramesStartPos, UseProvided=True}"/>
         ChangeKeyFramesLengthCommand="{xaml:Command PixiEditor.Animation.ChangeKeyFramesStartPos, UseProvided=True}"/>

+ 8 - 6
src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameTime.cs

@@ -1,18 +1,20 @@
 namespace PixiEditor.ChangeableDocument.Changeables.Animations;
 namespace PixiEditor.ChangeableDocument.Changeables.Animations;
 
 
-public record KeyFrameTime
+public struct KeyFrameTime
 {
 {
-    public KeyFrameTime(int Frame)
+    public KeyFrameTime(int frame, double normalizedTime)
     {
     {
-        this.Frame = Frame;
+        this.Frame = frame;
+        this.NormalizedTime = normalizedTime;
     }
     }
 
 
     public int Frame { get; init; }
     public int Frame { get; init; }
+    public double NormalizedTime { get; init; }
     
     
-    public static implicit operator KeyFrameTime(int frame) => new KeyFrameTime(frame);
+    public static implicit operator KeyFrameTime(int frame) => new KeyFrameTime(frame, 0);
 
 
-    public virtual bool Equals(KeyFrameTime? other)
+    public bool Equals(KeyFrameTime other)
     {
     {
-        return other != null && Frame == other.Frame;
+        return Frame == other.Frame && NormalizedTime.Equals(other.NormalizedTime);
     }
     }
 }
 }

+ 6 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs

@@ -14,6 +14,12 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         NonOverridenValue = _ => constantNonOverrideValue;
         NonOverridenValue = _ => constantNonOverrideValue;
     }
     }
 
 
+    protected override object FuncFactory(object toReturn)
+    {
+        Func<FuncContext, T> func = _ => (T)toReturn;
+        return func;
+    }
+
     object? IFuncInputProperty.GetFuncConstantValue() => constantNonOverrideValue;
     object? IFuncInputProperty.GetFuncConstantValue() => constantNonOverrideValue;
 
 
     void IFuncInputProperty.SetFuncConstantValue(object? value)
     void IFuncInputProperty.SetFuncConstantValue(object? value)

+ 42 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using System.Reflection;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
@@ -29,8 +30,7 @@ public class InputProperty : IInputProperty
 
 
             if (ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is not Delegate)
             if (ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is not Delegate)
             {
             {
-                Func<FuncContext, object> field = _ => connectionValue;
-                return field;
+                return FuncFactory(connectionValue);
             }
             }
 
 
             return connectionValue;
             return connectionValue;
@@ -46,6 +46,12 @@ public class InputProperty : IInputProperty
         }
         }
     }
     }
 
 
+    protected virtual object FuncFactory(object toReturn)
+    {
+        Func<FuncContext, object> func = _ => toReturn;
+        return func;
+    }
+
     public Node Node { get; }
     public Node Node { get; }
     public Type ValueType { get; } 
     public Type ValueType { get; } 
     internal bool CacheChanged
     internal bool CacheChanged
@@ -133,14 +139,45 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
 {
 {
     public new T Value
     public new T Value
     {
     {
-        get => (T)(base.Value ?? default);
+        get
+        {
+            object value = base.Value;
+            if (value is null) return default(T);
+
+            return (T)value;
+        }
     }
     }
 
 
     public T NonOverridenValue
     public T NonOverridenValue
     {
     {
-        get => (T)(base.NonOverridenValue ?? default);
+        get => (T)(base.NonOverridenValue ?? default(T));
         set => base.NonOverridenValue = value;
         set => base.NonOverridenValue = value;
     }
     }
+
+    /*
+    private T CastFunc(Func<FuncContext, object> func)
+    {
+        Type targetReturnType = Connection.ValueType;
+        Type funcType = typeof(Func<,>).MakeGenericType(typeof(FuncContext), targetReturnType);
+
+        var methodInfo = func.Method;
+        
+        // methodInfo returns Object, we need to wrap it so it returns targetReturnType
+        
+        MethodInfo castMethod = typeof(InputProperty<T>).GetMethod(nameof(Cast), BindingFlags.NonPublic | BindingFlags.Static)!;
+        
+        MethodInfo genericCastMethod = castMethod.MakeGenericMethod(targetReturnType);
+        
+        
+        
+        // T i Func<FuncContext, targetReturnType> so we need to return it
+        return (T)(object)Delegate.CreateDelegate(funcType, methodInfo);
+    }*/
+    
+    private static Func<FuncContext, T> Cast<T>(Func<FuncContext, object> func)
+    {
+        return context => (T)func(context);
+    }
     
     
     internal InputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, defaultValue, typeof(T))
     internal InputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, defaultValue, typeof(T))
     {
     {

+ 8 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs

@@ -17,7 +17,14 @@ public interface IReadOnlyNode
     string DisplayName { get; }
     string DisplayName { get; }
 
 
     public Surface? Execute(RenderingContext context);
     public Surface? Execute(RenderingContext context);
-    public bool Validate();
+    
+    /// <summary>
+    ///     Checks if the inputs are legal. If they are not, the node should not be executed.
+    /// Note that all nodes connected to any output of this node won't be executed either.
+    /// </summary>
+    /// <example>Divide node has two inputs, if the second input is 0, the node should not be executed. Since division by 0 is illegal</example>
+    /// <returns>True if the inputs are legal, false otherwise.</returns>
+    public bool AreInputsLegal();
     
     
     /// <summary>
     /// <summary>
     ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.
     ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -47,7 +47,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         while (queueNodes.Count > 0)
         while (queueNodes.Count > 0)
         {
         {
             var node = queueNodes.Dequeue();
             var node = queueNodes.Dequeue();
-            if (!visited.Add(node) || (validate && !node.Validate()))
+            if (!visited.Add(node) || (validate && !node.AreInputsLegal()))
             {
             {
                 continue;
                 continue;
             }
             }

+ 38 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs

@@ -0,0 +1,38 @@
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
+
+public class TimeNode : Node
+{
+    protected override string NodeUniqueName { get; } = "Time";
+    public override string DisplayName { get; set; } = "TIME_NODE";
+    
+    public OutputProperty<int> ActiveFrame { get; set; }
+    public OutputProperty<double> NormalizedTime { get; set; }
+
+    protected override bool AffectedByAnimation => true;
+
+    public TimeNode()
+    {
+        ActiveFrame = CreateOutput("ActiveFrame", "ACTIVE_FRAME", 0);
+        NormalizedTime = CreateOutput("NormalizedTime", "NORMALIZED_TIME", 0.0);
+    }
+    
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        ActiveFrame.Value = context.FrameTime.Frame;
+        NormalizedTime.Value = context.FrameTime.NormalizedTime;
+        
+        return null;
+    }
+
+    public override bool AreInputsLegal()
+    {
+        return true;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new TimeNode();
+    }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CircleNode.cs

@@ -53,7 +53,7 @@ public class CircleNode : Node
 
 
     public override string DisplayName { get; set; } = "CIRCLE_NODE";
     public override string DisplayName { get; set; } = "CIRCLE_NODE";
 
 
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
     {
         return Radius.Value is { X: > 0, Y: > 0 } && StrokeWidth.Value > 0;
         return Radius.Value is { X: > 0, Y: > 0 } && StrokeWidth.Value > 0;
     }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs

@@ -113,7 +113,7 @@ public class CombineChannelsNode : Node
     }
     }
 
 
     public override string DisplayName { get; set; } = "COMBINE_CHANNELS_NODE";
     public override string DisplayName { get; set; } = "COMBINE_CHANNELS_NODE";
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new CombineChannelsNode();
     public override Node CreateCopy() => new CombineChannelsNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs

@@ -45,7 +45,7 @@ public class CombineColorNode : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new CombineColorNode();
     public override Node CreateCopy() => new CombineColorNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs

@@ -39,7 +39,7 @@ public class CombineVecD : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new CombineVecD();
     public override Node CreateCopy() => new CombineVecD();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs

@@ -37,7 +37,7 @@ public class CombineVecI : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new CombineVecI();
     public override Node CreateCopy() => new CombineVecI();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -91,7 +91,7 @@ public class SeparateChannelsNode : Node
         return imageSurface;
         return imageSurface;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new SeparateChannelsNode();
     public override Node CreateCopy() => new SeparateChannelsNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs

@@ -33,7 +33,7 @@ public class SeparateColorNode : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new SeparateColorNode();
     public override Node CreateCopy() => new SeparateColorNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs

@@ -27,7 +27,7 @@ public class SeparateVecDNode : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new SeparateVecDNode();
     public override Node CreateCopy() => new SeparateVecDNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs

@@ -27,7 +27,7 @@ public class SeparateVecINode : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new SeparateVecINode();
     public override Node CreateCopy() => new SeparateVecINode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs

@@ -50,7 +50,7 @@ public class DebugBlendModeNode : Node
         return workingSurface;
         return workingSurface;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new DebugBlendModeNode();
     public override Node CreateCopy() => new DebugBlendModeNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs

@@ -39,7 +39,7 @@ public class CreateImageNode : Node
     }
     }
  
  
     public override string DisplayName { get; set; } = "CREATE_IMAGE_NODE";
     public override string DisplayName { get; set; } = "CREATE_IMAGE_NODE";
-    public override bool Validate() => Size.Value is { X: > 0, Y: > 0 };
+    public override bool AreInputsLegal() => Size.Value is { X: > 0, Y: > 0 };
 
 
     public override Node CreateCopy() => new CreateImageNode();
     public override Node CreateCopy() => new CreateImageNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -16,7 +16,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
         Content = CreateInput<Surface?>("Content", "CONTENT", null);
         Content = CreateInput<Surface?>("Content", "CONTENT", null);
     }
     }
 
 
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
     {
         return true;
         return true;
     }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -36,7 +36,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
         return GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
     }
     }
 
 
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
     {
         return true;
         return true;
     }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs

@@ -26,7 +26,7 @@ public class ImageSpaceNode : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new ImageSpaceNode();
     public override Node CreateCopy() => new ImageSpaceNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/KernelNode.cs

@@ -59,7 +59,7 @@ public class KernelFilterNode : Node
         return workingSurface;
         return workingSurface;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new KernelFilterNode();
     public override Node CreateCopy() => new KernelFilterNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs

@@ -60,7 +60,7 @@ public class MathNode : Node
         return null;
         return null;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new MathNode();
     public override Node CreateCopy() => new MathNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MatrixTransformNode.cs

@@ -54,7 +54,7 @@ public class MatrixTransformNode : Node
         return Transformed.Value;
         return Transformed.Value;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new MatrixTransformNode();
     public override Node CreateCopy() => new MatrixTransformNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -21,7 +21,7 @@ public class MergeNode : Node, IBackgroundInput
 
 
     public override string DisplayName { get; set; } = "MERGE_NODE";
     public override string DisplayName { get; set; } = "MERGE_NODE";
 
 
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
     {
         return Top.Connection != null || Bottom.Connection != null;
         return Top.Connection != null || Bottom.Connection != null;
     }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -54,7 +54,7 @@ public class ModifyImageLeftNode : Node
         return Image.Value;
         return Image.Value;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => new ModifyImageLeftNode();
     public override Node CreateCopy() => new ModifyImageLeftNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -63,7 +63,7 @@ public class ModifyImageRightNode : Node
         return Output.Value;
         return Output.Value;
     }
     }
 
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
 
     public override Node CreateCopy() => throw new NotImplementedException();
     public override Node CreateCopy() => throw new NotImplementedException();
 }
 }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -52,7 +52,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public VecD Position { get; set; }
     public VecD Position { get; set; }
     public abstract string DisplayName { get; set; }
     public abstract string DisplayName { get; set; }
 
 
-    private KeyFrameTime _lastFrameTime = new KeyFrameTime(-1);
+    private KeyFrameTime _lastFrameTime = new KeyFrameTime(-1, 0);
     private ChunkResolution? _lastResolution;
     private ChunkResolution? _lastResolution;
     private VecI? _lastChunkPos;
     private VecI? _lastChunkPos;
     private bool _keyFramesDirty;
     private bool _keyFramesDirty;
@@ -81,7 +81,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     }
     }
 
 
     protected abstract Surface? OnExecute(RenderingContext context);
     protected abstract Surface? OnExecute(RenderingContext context);
-    public abstract bool Validate();
+    public abstract bool AreInputsLegal();
 
 
     protected virtual bool CacheChanged(RenderingContext context)
     protected virtual bool CacheChanged(RenderingContext context)
     {
     {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -52,7 +52,7 @@ public class NoiseNode : Node
     }
     }
 
 
     public override string DisplayName { get; set; } = "NOISE_NODE";
     public override string DisplayName { get; set; } = "NOISE_NODE";
-    public override bool Validate() => Size.Value is { X: > 0, Y: > 0 };
+    public override bool AreInputsLegal() => Size.Value is { X: > 0, Y: > 0 }; 
 
 
     public override Node CreateCopy() => new NoiseNode();
     public override Node CreateCopy() => new NoiseNode();
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -14,7 +14,7 @@ public class OutputNode : Node, IBackgroundInput
         Input = CreateInput<Surface>("Background", "INPUT", null);
         Input = CreateInput<Surface>("Background", "INPUT", null);
     }
     }
     
     
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
     {
         return Input.Connection != null;
         return Input.Connection != null;
     }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -47,7 +47,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
     }
     }
 
 
     protected abstract override Surface? OnExecute(RenderingContext context);
     protected abstract override Surface? OnExecute(RenderingContext context);
-    public abstract override bool Validate();
+    public abstract override bool AreInputsLegal();
 
 
     protected Surface TryInitWorkingSurface(VecI imageSize, RenderingContext context)
     protected Surface TryInitWorkingSurface(VecI imageSize, RenderingContext context)
     {
     {

+ 6 - 5
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -16,10 +17,10 @@ public class DocumentRenderer
 
 
     private IReadOnlyDocument Document { get; }
     private IReadOnlyDocument Document { get; }
 
 
-    public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, int frame,
+    public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
         RectI? globalClippingRect = null)
         RectI? globalClippingRect = null)
     {
     {
-        using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
+        using RenderingContext context = new(frameTime, chunkPos, resolution, Document.Size);
         try
         try
         {
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
@@ -63,9 +64,9 @@ public class DocumentRenderer
     }
     }
 
 
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution,
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution,
-        IReadOnlyNode node, int frame, RectI? globalClippingRect = null)
+        IReadOnlyNode node, KeyFrameTime frameTime, RectI? globalClippingRect = null)
     {
     {
-        using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
+        using RenderingContext context = new(frameTime, chunkPos, resolution, Document.Size);
         try
         try
         {
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);