Browse Source

Merge branch 'refs/heads/avalonia-rewrite' into funcy-nodes

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

+ 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(document.AnimationHandler.ActiveFrameBindable, internals.Tracker, optimizedChanges);
+            var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime, internals.Tracker, optimizedChanges);
             List<IRenderInfo> renderResult = new();
             renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed || viewportRefreshRequest));
             renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));

+ 3 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentStructureHelper.cs

@@ -77,6 +77,9 @@ internal class DocumentStructureHelper
             Guid guid = Guid.NewGuid();
             //put member above the layer
             INodeHandler parent = doc.StructureHelper.GetFirstForwardNode(layer);
+            if(parent is null)
+                parent = doc.NodeGraphHandler.OutputNode;
+            
             internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(parent.Id, guid, type));
             name ??= GetUniqueName(
                 type == StructureMemberType.Layer

+ 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
 {
     public IReadOnlyCollection<IKeyFrameHandler> KeyFrames { get; }
     public int ActiveFrameBindable { get; set; }
+    public KeyFrameTime ActiveFrameTime { get; }
     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);

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

@@ -4,6 +4,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos;
@@ -26,9 +27,9 @@ internal class AffectedAreasGatherer
     public Dictionary<Guid, AffectedArea> ImagePreviewAreas { 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)
     {
         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);
         if (member is IReadOnlyImageNode layer)
         {
-            var result = layer.GetLayerImageAtFrame(frame);
+            var result = layer.GetLayerImageAtFrame(frame.Frame);
             if (result == null)
             {
                 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);
         if (member is IReadOnlyImageNode layer)
         {
-            var result = layer.GetLayerImageAtFrame(frame);
+            var result = layer.GetLayerImageAtFrame(frame.Frame);
             if (result == null)
             {
                 AddWholeCanvasToMainImage();

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

@@ -198,7 +198,7 @@ internal class CanvasUpdater
             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) =>
             {
                 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,
             };
             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.Scale(scaling);
             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.Handlers;
 using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
@@ -41,12 +42,18 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         {
             _frameRate = value;
             OnPropertyChanged(nameof(FrameRate));
+            OnPropertyChanged(nameof(DefaultEndFrame));
         }
     }
 
     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)
     {
@@ -54,6 +61,8 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         Internals = internals;
     }
 
+    public KeyFrameTime ActiveFrameTime => new KeyFrameTime(ActiveFrameBindable, ActiveNormalizedTime);
+
     public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null)
     {
         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.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -408,7 +409,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// Tries rendering the whole document
     /// </summary>
     /// <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
         {
@@ -419,7 +420,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 for (int j = 0; j < sizeInChunks.Y; j++)
                 {
                     // 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)
                         continue;
                     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));
     }
 
-    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
         try
@@ -563,7 +564,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
                 return Renderer.RenderChunk(chunkPos, ChunkResolution.Full,
-                        frame)
+                        frameTime)
                     .Match(
                         chunk =>
                         {
@@ -580,7 +581,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             IReadOnlyStructureNode? maybeMember = Internals.Tracker.Document.FindMember(layerVm.Id);
             if (maybeMember is not IReadOnlyImageNode layer)
                 return Colors.Transparent;
-            return layer.GetLayerImageAtFrame(frame).GetMostUpToDatePixel(pos);
+            return layer.GetLayerImageAtFrame(frameTime.Frame).GetMostUpToDatePixel(pos);
         }
         catch (ObjectDisposedException)
         {
@@ -751,7 +752,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         Image[] images = new Image[framesCount];
         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)
             {
                 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>(
         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
     {
         get => GetValue(ChangeKeyFramesLengthCommandProperty);
@@ -323,7 +332,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
     private void PlayTimerOnTick(object? sender, EventArgs e)
     {
-        if (ActiveFrame >= KeyFrames.FrameCount)
+        if (ActiveFrame >= (KeyFrames.Count > 0 ? KeyFrames.FrameCount : DefaultEndFrame))
         {
             ActiveFrame = 1;
         }

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

@@ -17,6 +17,7 @@
         ActiveFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Mode=TwoWay}"
         NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateRasterKeyFrame}"
         Fps="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.FrameRate, Mode=TwoWay}"
+        DefaultEndFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.DefaultEndFrame}"
         DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateRasterKeyFrame}"
         DeleteKeyFrameCommand="{xaml:Command PixiEditor.Animation.DeleteKeyFrames, 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;
 
-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 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;
     }
 
+    protected override object FuncFactory(object toReturn)
+    {
+        Func<FuncContext, T> func = _ => (T)toReturn;
+        return func;
+    }
+
     object? IFuncInputProperty.GetFuncConstantValue() => constantNonOverrideValue;
 
     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.Nodes;
 
@@ -29,8 +30,7 @@ public class InputProperty : IInputProperty
 
             if (ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is not Delegate)
             {
-                Func<FuncContext, object> field = _ => connectionValue;
-                return field;
+                return FuncFactory(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 Type ValueType { get; } 
     internal bool CacheChanged
@@ -133,14 +139,45 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
 {
     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
     {
-        get => (T)(base.NonOverridenValue ?? default);
+        get => (T)(base.NonOverridenValue ?? default(T));
         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))
     {

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

@@ -17,7 +17,14 @@ public interface IReadOnlyNode
     string DisplayName { get; }
 
     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>
     ///     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)
         {
             var node = queueNodes.Dequeue();
-            if (!visited.Add(node) || (validate && !node.Validate()))
+            if (!visited.Add(node) || (validate && !node.AreInputsLegal()))
             {
                 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 bool Validate()
+    public override bool AreInputsLegal()
     {
         return Radius.Value is { X: > 0, Y: > 0 } && StrokeWidth.Value > 0;
     }

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

@@ -13,12 +13,6 @@ public class CombineChannelsNode : Node
     private readonly ColorFilter _redFilter = ColorFilter.CreateColorMatrix(ColorMatrix.UseRed + ColorMatrix.OpaqueAlphaOffset);
     private readonly ColorFilter _greenFilter = ColorFilter.CreateColorMatrix(ColorMatrix.UseGreen + ColorMatrix.OpaqueAlphaOffset);
     private readonly ColorFilter _blueFilter = ColorFilter.CreateColorMatrix(ColorMatrix.UseBlue + ColorMatrix.OpaqueAlphaOffset);
-    
-    private readonly ColorFilter _alphaGrayscaleFilter = ColorFilter.CreateColorMatrix(new ColorMatrix(
-        (0, 0, 0, 0, 0),
-        (0, 0, 0, 0, 0),
-        (0, 0, 0, 0, 0),
-        (1, 0, 0, 0, 0)));
 
     public InputProperty<Surface> Red { get; }
     
@@ -75,7 +69,7 @@ public class CombineChannelsNode : Node
 
         if (Alpha.Value is { } alpha)
         {
-            _clearPaint.ColorFilter = Grayscale.Value ? _alphaGrayscaleFilter : null;
+            _clearPaint.ColorFilter = Grayscale.Value ? Filters.AlphaGrayscaleFilter : null;
 
             workingSurface.DrawingSurface.Canvas.DrawSurface(alpha.DrawingSurface, 0, 0, _clearPaint);
         }
@@ -113,7 +107,7 @@ public class CombineChannelsNode : 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();
 }

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

@@ -45,7 +45,7 @@ public class CombineColorNode : Node
         return null;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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 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();
 }

+ 22 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Filters.cs

@@ -0,0 +1,22 @@
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public static class Filters
+{
+    public static readonly ColorFilter RedGrayscaleFilter =
+        ColorFilter.CreateColorMatrix(
+            ColorMatrix.UseRed + ColorMatrix.MapRedToGreenBlue + ColorMatrix.OpaqueAlphaOffset);
+
+    public static readonly ColorFilter GreenGrayscaleFilter =
+        ColorFilter.CreateColorMatrix(ColorMatrix.UseGreen + ColorMatrix.MapGreenToRedBlue +
+                                      ColorMatrix.OpaqueAlphaOffset);
+
+    public static readonly ColorFilter BlueGrayscaleFilter =
+        ColorFilter.CreateColorMatrix(ColorMatrix.UseBlue + ColorMatrix.MapBlueToRedGreen +
+                                      ColorMatrix.OpaqueAlphaOffset);
+
+    public static readonly ColorFilter AlphaGrayscaleFilter =
+        ColorFilter.CreateColorMatrix(ColorMatrix.MapAlphaToRedGreenBlue + ColorMatrix.OpaqueAlphaOffset);
+}

+ 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);
     }
 
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
         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();
     }
 
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
         return true;
     }

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

@@ -26,7 +26,7 @@ public class ImageSpaceNode : Node
         return null;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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 bool Validate()
+    public override bool AreInputsLegal()
     {
         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;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     public override Node CreateCopy() => new ModifyImageLeftNode();
 }

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

@@ -65,7 +65,7 @@ public class ModifyImageRightNode : Node
         return Output.Value;
     }
 
-    public override bool Validate() => true;
+    public override bool AreInputsLegal() => true;
 
     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 abstract string DisplayName { get; set; }
 
-    private KeyFrameTime _lastFrameTime = new KeyFrameTime(-1);
+    private KeyFrameTime _lastFrameTime = new KeyFrameTime(-1, 0);
     private ChunkResolution? _lastResolution;
     private VecI? _lastChunkPos;
     private bool _keyFramesDirty;
@@ -81,7 +81,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     }
 
     protected abstract Surface? OnExecute(RenderingContext context);
-    public abstract bool Validate();
+    public abstract bool AreInputsLegal();
 
     protected virtual bool CacheChanged(RenderingContext context)
     {

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

@@ -1,6 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.Numerics;
@@ -10,21 +11,33 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 public class NoiseNode : Node
 {
     private double previousScale = double.NaN;
+    private double previousSeed = double.NaN;
+    private NoiseType previousNoiseType = Nodes.NoiseType.TurbulencePerlin;
+    private int previousOctaves = -1;
+    
     private Paint paint = new();
     
+    private static readonly ColorFilter grayscaleFilter = ColorFilter.CreateColorMatrix(
+        ColorMatrix.MapAlphaToRedGreenBlue + ColorMatrix.OpaqueAlphaOffset);
+    
     public OutputProperty<Surface> Noise { get; }
-
+    
+    public InputProperty<NoiseType> NoiseType { get; }
     public InputProperty<VecI> Size { get; }
     
     public InputProperty<double> Scale { get; }
     
+    public InputProperty<int> Octaves { get; }
+    
     public InputProperty<double> Seed { get; }
 
     public NoiseNode()
     {
         Noise = CreateOutput<Surface>(nameof(Noise), "NOISE", null);
-        Size = CreateInput(nameof(Size), "SIZE", new VecI());
-        Scale = CreateInput(nameof(Scale), "SCALE", 0d);
+        NoiseType = CreateInput(nameof(NoiseType), "NOISE_TYPE", Nodes.NoiseType.TurbulencePerlin);
+        Size = CreateInput(nameof(Size), "SIZE", new VecI(64, 64));
+        Scale = CreateInput(nameof(Scale), "SCALE", 10d);
+        Octaves = CreateInput(nameof(Octaves), "OCTAVES", 1);
         Seed = CreateInput(nameof(Seed), "SEED", 0d);
     }
 
@@ -32,18 +45,34 @@ public class NoiseNode : Node
 
     protected override Surface OnExecute(RenderingContext context)
     {
-        if (Math.Abs(previousScale - Scale.Value) > 0.000001 || double.IsNaN(previousScale))
+        if (Math.Abs(previousScale - Scale.Value) > 0.000001 
+            || previousSeed != Seed.Value
+            || previousOctaves != Octaves.Value
+            || previousNoiseType != NoiseType.Value
+            || double.IsNaN(previousScale))
         {
-            var shader = Shader.CreatePerlinNoiseTurbulence((float)(1d / Scale.Value), (float)(1d / Scale.Value), 4, (float)Seed.Value);
+            var shader = SelectShader();
+            if (shader == null)
+            {
+                Noise.Value = null;
+                return null;
+            }
+            
             paint.Shader = shader;
-
+            
+            // Define a grayscale color filter to apply to the image
+            paint.ColorFilter = grayscaleFilter; 
+            
             previousScale = Scale.Value;
+            previousSeed = Seed.Value;
+            previousOctaves = Octaves.Value;
+            previousNoiseType = NoiseType.Value;
         }
         
         var size = Size.Value;
         
         var workingSurface = new Surface(size);
-        
+       
         workingSurface.DrawingSurface.Canvas.DrawPaint(paint);
 
         Noise.Value = workingSurface;
@@ -51,8 +80,30 @@ public class NoiseNode : Node
         return Noise.Value;
     }
 
+    private Shader SelectShader()
+    {
+        Shader shader = NoiseType.Value switch
+        {
+            Nodes.NoiseType.TurbulencePerlin => Shader.CreatePerlinNoiseTurbulence(
+                (float)(1d / Scale.Value),
+                (float)(1d / Scale.Value), Octaves.Value, (float)Seed.Value),
+            Nodes.NoiseType.FractalPerlin => Shader.CreatePerlinFractalNoise(
+                (float)(1d / Scale.Value),
+                (float)(1d / Scale.Value), Octaves.Value, (float)Seed.Value),
+            _ => null
+        };
+
+        return shader;
+    }
+
     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 enum NoiseType
+{
+    TurbulencePerlin,
+    FractalPerlin
+}

+ 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);
     }
     
-    public override bool Validate()
+    public override bool AreInputsLegal()
     {
         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);
-    public abstract override bool Validate();
+    public abstract override bool AreInputsLegal();
 
     protected Surface TryInitWorkingSurface(VecI imageSize, RenderingContext context)
     {

+ 16 - 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.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -16,10 +17,10 @@ public class DocumentRenderer
 
     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)
     {
-        using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
+        using RenderingContext context = new(frameTime, chunkPos, resolution, Document.Size);
         try
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
@@ -47,6 +48,16 @@ public class DocumentRenderer
             int height = (int)(ChunkyImage.FullChunkSize * resolution.Multiplier());
 
             RectD sourceRect = new(x, y, width, height);
+            
+            RectD availableRect = new(0, 0, evaluated.Size.X, evaluated.Size.Y);
+            
+            sourceRect = sourceRect.Intersect(availableRect);
+            
+            if (sourceRect.IsZeroOrNegativeArea)
+            {
+                chunk.Dispose();
+                return new EmptyChunk();
+            }
 
             using var chunkSnapshot = evaluated.DrawingSurface.Snapshot((RectI)sourceRect);
 
@@ -63,9 +74,9 @@ public class DocumentRenderer
     }
 
     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
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);

+ 1 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IShaderImplementation.cs

@@ -12,5 +12,6 @@ public interface IShaderImplementation
     public Shader? CreateFromSksl(string sksl, bool isOpaque, out string errors);
     public Shader CreateLinearGradient(VecI p1, VecI p2, Color[] colors);
     public Shader CreatePerlinNoiseTurbulence(float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed);
+    public Shader CreatePerlinFractalNoise(float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed);
     public object GetNativeShader(IntPtr objectPointer);
 }

+ 5 - 0
src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/Shader.cs

@@ -32,4 +32,9 @@ public class Shader : NativeObject
     {
         return DrawingBackendApi.Current.ShaderImplementation.CreatePerlinNoiseTurbulence(baseFrequencyX, baseFrequencyY, numOctaves, seed);
     }
+
+    public static Shader CreatePerlinFractalNoise(float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed)
+    {
+        return DrawingBackendApi.Current.ShaderImplementation.CreatePerlinFractalNoise(baseFrequencyX, baseFrequencyY, numOctaves, seed);
+    }
 }

+ 12 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaShaderImplementation.cs

@@ -57,6 +57,18 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             ManagedInstances[shader.Handle] = shader;
             return new Shader(shader.Handle);
         }
+        
+        public Shader CreatePerlinFractalNoise(float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed)
+        {
+            SKShader shader = SKShader.CreatePerlinNoiseFractalNoise(
+                baseFrequencyX,
+                baseFrequencyY,
+                numOctaves,
+                seed);
+
+            ManagedInstances[shader.Handle] = shader;
+            return new Shader(shader.Handle);
+        }
 
         public object GetNativeShader(IntPtr objectPointer)
         {