Browse Source

Merge pull request #764 from PixiEditor/more-nodes-vol3

More nodes vol3
Krzysztof Krysiński 5 months ago
parent
commit
a61de89c8d
37 changed files with 888 additions and 79 deletions
  1. 1 1
      src/Drawie
  2. 29 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  3. 23 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  4. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs
  5. 7 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/FilterNode.cs
  6. 11 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs
  7. 37 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/InvertFilterNode.cs
  8. 49 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/SepiaFilterNode.cs
  9. 51 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Image/MaskNode.cs
  10. 4 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  11. 37 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  13. 41 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/PropertyValidator.cs
  14. 5 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs
  15. 14 8
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  16. 1 0
      src/PixiEditor.UI.Common/Controls/Slider.axaml
  17. BIN
      src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf
  18. 1 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  19. 1 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  20. 18 1
      src/PixiEditor/Data/Localization/Languages/en.json
  21. 3 0
      src/PixiEditor/Helpers/ThemeResources.cs
  22. 4 4
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  23. 1 0
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  24. 8 2
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  25. 1 0
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  26. 5 3
      src/PixiEditor/Styles/Templates/NodeView.axaml
  27. 202 0
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ColorAdjustmentsFilterNode.cs
  28. 112 0
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ColorAdjustmentsFilterNodeViewModel.cs
  29. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/InvertFilterNodeViewModel.cs
  30. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/SepiaFilterNodeViewModel.cs
  31. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/Image/MaskNodeViewModel.cs
  32. 14 4
      src/PixiEditor/ViewModels/Document/TransformOverlays/TextOverlayViewModel.cs
  33. 3 0
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  34. 85 1
      src/PixiEditor/ViewModels/Nodes/Properties/DoublePropertyViewModel.cs
  35. 21 4
      src/PixiEditor/Views/Input/NumberInput.cs
  36. 9 0
      src/PixiEditor/Views/Nodes/NodeView.cs
  37. 63 8
      src/PixiEditor/Views/Nodes/Properties/DoublePropertyView.axaml

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 157b999633b2c08dd5bd9bd09e48adea59075481
+Subproject commit 1843368cc81ac00bf5bd9e096a1a1da1c500ebcc

+ 29 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -32,7 +32,7 @@ public class InputProperty : IInputProperty
 
             var connectionValue = Connection.Value;
 
-            if(connectionValue is null)
+            if (connectionValue is null)
             {
                 return null;
             }
@@ -77,9 +77,25 @@ public class InputProperty : IInputProperty
         get => _internalValue;
         set
         {
-            _internalValue = value;
-            NonOverridenValueChanged?.Invoke(value);
-            NonOverridenValueSet(value);
+            object evaluatedValue = value;
+            if (value != null)
+            {
+                if (!value.GetType().IsAssignableTo(ValueType))
+                {
+                    if (!ConversionTable.TryConvert(value, ValueType, out object result))
+                    {
+                        evaluatedValue = null;
+                    }
+                    else
+                    {
+                        evaluatedValue = result;
+                    }
+                }
+            }
+
+            _internalValue = evaluatedValue;
+            NonOverridenValueChanged?.Invoke(evaluatedValue);
+            NonOverridenValueSet(evaluatedValue);
         }
     }
 
@@ -89,7 +105,7 @@ public class InputProperty : IInputProperty
         {
             if (validator is null)
             {
-                validator = new PropertyValidator();
+                validator = new PropertyValidator(this);
             }
 
             return validator;
@@ -209,7 +225,14 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             if (value is T tValue)
                 return tValue;
 
-            return (T)Validator.GetClosestValidValue(value);
+            var validated = Validator.GetClosestValidValue(value);
+
+            if (!ConversionTable.TryConvert(validated, ValueType, out object result))
+            {
+                return default(T);
+            }
+
+            return (T)result;
         }
     }
 

+ 23 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs

@@ -2,7 +2,9 @@
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -29,10 +31,28 @@ public class ApplyFilterNode : RenderNode, IRenderInput
             return;
 
         _paint.SetFilters(Filter.Value);
-        int layer = surface.Canvas.SaveLayer(_paint);
-        Background.Value?.Paint(context, surface);
 
-        surface.Canvas.RestoreToCount(layer);
+        if (!context.ProcessingColorSpace.IsSrgb)
+        {
+            var target = Texture.ForProcessing(surface, ColorSpace.CreateSrgb());
+
+            int saved = surface.Canvas.Save();
+            surface.Canvas.SetMatrix(Matrix3X3.Identity);
+
+            target.DrawingSurface.Canvas.SaveLayer(_paint);
+            Background.Value?.Paint(context, target.DrawingSurface);
+            target.DrawingSurface.Canvas.Restore();
+
+            surface.Canvas.DrawSurface(target.DrawingSurface, 0, 0);
+            surface.Canvas.RestoreToCount(saved);
+            target.Dispose();
+        }
+        else
+        {
+            int layer = surface.Canvas.SaveLayer(_paint);
+            Background.Value?.Paint(context, surface);
+            surface.Canvas.RestoreToCount(layer);
+        }
     }
 
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;

+ 7 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/FilterNode.cs

@@ -1,5 +1,4 @@
 using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
@@ -7,15 +6,18 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 public abstract class FilterNode : Node
 {
     public OutputProperty<Filter> Output { get; }
-    
+
     public InputProperty<Filter?> Input { get; }
-    
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
     public FilterNode()
     {
         Output = CreateOutput<Filter>(nameof(Output), "FILTERS", null);
         Input = CreateInput<Filter>(nameof(Input), "PREVIOUS", null);
     }
-    
+
     protected override void OnExecute(RenderContext context)
     {
         var colorFilter = GetColorFilter();
@@ -33,6 +35,6 @@ public abstract class FilterNode : Node
     }
 
     protected virtual ColorFilter? GetColorFilter() => null;
-    
+
     protected virtual ImageFilter? GetImageFilter() => null;
 }

+ 11 - 9
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
@@ -21,19 +22,19 @@ public class GrayscaleNode : FilterNode
     private double lastFactor;
     private bool lastNormalize;
     private Vec3D lastCustomWeight;
-    
+
     private ColorFilter? filter;
     
     public GrayscaleNode()
     {
         Mode = CreateInput("Mode", "MODE", GrayscaleMode.Weighted);
-        // TODO: Clamp 0 - 1 in UI
-        Factor = CreateInput("Factor", "FACTOR", 1d);
+        Factor = CreateInput("Factor", "FACTOR", 1d)
+            .WithRules(rules => rules.Min(0d).Max(1d));
         Normalize = CreateInput("Normalize", "NORMALIZE", true);
         CustomWeight = CreateInput("CustomWeight", "WEIGHT_FACTOR", new Vec3D(1, 1, 1));
     }
 
-    protected override ColorFilter GetColorFilter()
+    protected override ColorFilter? GetColorFilter()
     {
         if (Mode.Value == lastMode 
             && Factor.Value == lastFactor 
@@ -47,17 +48,18 @@ public class GrayscaleNode : FilterNode
         lastFactor = Factor.Value;
         lastNormalize = Normalize.Value;
         lastCustomWeight = CustomWeight.Value;
-        
+
         filter?.Dispose();
         
-        filter = ColorFilter.CreateColorMatrix(Mode.Value switch
+        var matrix = Mode.Value switch
         {
             GrayscaleMode.Weighted => UseFactor(WeightedMatrix),
             GrayscaleMode.Average => UseFactor(AverageMatrix),
             GrayscaleMode.Custom => UseFactor(ColorMatrix.WeightedGrayscale(GetAdjustedCustomWeight()) +
                                               ColorMatrix.UseAlpha)
-        });
-        
+        };
+
+        filter = ColorFilter.CreateColorMatrix(matrix);
         return filter;
     }
 

+ 37 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/InvertFilterNode.cs

@@ -0,0 +1,37 @@
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+
+[NodeInfo("InvertFilter")]
+public class InvertFilterNode : FilterNode
+{
+    public InputProperty<double> Intensity { get; }
+    private ColorFilter? filter;
+    private ColorMatrix invertedMatrix;
+
+    public InvertFilterNode()
+    {
+        Intensity = CreateInput("Intensity", "INTENSITY", 1.0)
+            .WithRules(rules => rules.Min(0d).Max(1d));
+        invertedMatrix = new ColorMatrix(new float[] { -1, 0, 0, 0, 1, 0, -1, 0, 0, 1, 0, 0, -1, 0, 1, 0, 0, 0, 1, 0 });
+
+        filter = ColorFilter.CreateColorMatrix(invertedMatrix);
+    }
+
+    protected override ColorFilter? GetColorFilter()
+    {
+        filter?.Dispose();
+
+        var lerped = ColorMatrix.Lerp(ColorMatrix.Identity, invertedMatrix, (float)Intensity.Value);
+        filter = ColorFilter.CreateColorMatrix(lerped);
+
+        return filter;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new InvertFilterNode();
+    }
+}

+ 49 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/SepiaFilterNode.cs

@@ -0,0 +1,49 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+
+[NodeInfo("Sepia")]
+public class SepiaFilterNode : FilterNode
+{
+    public InputProperty<double> Intensity { get; }
+
+    private ColorMatrix sepiaMatrix;
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
+    private ColorFilter lastFilter;
+
+    public SepiaFilterNode()
+    {
+        Intensity = CreateInput("Intensity", "INTENSITY", 1d)
+            .WithRules(rules => rules.Min(0d).Max(1d));
+
+        sepiaMatrix = new ColorMatrix(
+            [
+                0.393f, 0.769f, 0.189f, 0.0f, 0.0f,
+                0.349f, 0.686f, 0.168f, 0.0f, 0.0f,
+                0.272f, 0.534f, 0.131f, 0.0f, 0.0f,
+                0.0f, 0.0f, 0.0f, 1.0f, 0.0f
+            ]
+        );
+    }
+
+    protected override ColorFilter? GetColorFilter()
+    {
+        lastFilter?.Dispose();
+
+        var lerped = ColorMatrix.Lerp(ColorMatrix.Identity, sepiaMatrix, (float)Intensity.Value);
+        lastFilter = ColorFilter.CreateColorMatrix(lerped);
+
+        return lastFilter;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new SepiaFilterNode();
+    }
+}

+ 51 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Image/MaskNode.cs

@@ -0,0 +1,51 @@
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Image;
+
+[NodeInfo("Mask")]
+public class MaskNode : RenderNode, IRenderInput
+{
+    public RenderInputProperty Background { get; }
+    public RenderInputProperty Mask { get; }
+
+    protected Paint maskPaint = new Paint()
+    {
+        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn, ColorFilter = Nodes.Filters.MaskFilter
+    };
+
+    public MaskNode()
+    {
+        Background = CreateRenderInput("Background", "INPUT");
+        Mask = CreateRenderInput("Mask", "MASK");
+        AllowHighDpiRendering = true;
+        Output.FirstInChain = null;
+    }
+
+    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    {
+        if (Background.Value == null)
+        {
+            return;
+        }
+
+        Background.Value.Paint(context, surface);
+
+        if (Mask.Value == null)
+        {
+            return;
+        }
+
+        int layer = surface.Canvas.SaveLayer(maskPaint);
+        Mask.Value.Paint(context, surface);
+        surface.Canvas.RestoreToCount(layer);
+    }
+
+
+    public override Node CreateCopy()
+    {
+        return new MaskNode();
+    }
+}

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

@@ -52,7 +52,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return (GetFrameWithImage(ctx.FrameTime).Data as ChunkyImage).LatestSize;
     }
 
-    protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters = true)
     {
         int scaled = workingSurface.Canvas.Save();
@@ -63,7 +64,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         workingSurface.Canvas.RestoreToCount(scaled);
     }
 
-    protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters)
     {
         int scaled = workingSurface.Canvas.Save();

+ 37 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -3,7 +3,9 @@ using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -22,7 +24,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
     {
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         {
-            Output.Value = Background.Value; 
+            Output.Value = Background.Value;
             return;
         }
 
@@ -48,7 +50,8 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             return;
         }
 
-        var outputWorkingSurface = TryInitWorkingSurface(size, context.ChunkResolution, 1);
+        var outputWorkingSurface =
+            TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 1);
         outputWorkingSurface.DrawingSurface.Canvas.Clear();
 
         DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, useFilters);
@@ -57,7 +60,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
         if (Background.Value != null)
         {
-            Texture tempSurface = TryInitWorkingSurface(size, context.ChunkResolution, 4);
+            Texture tempSurface = TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 4);
             tempSurface.DrawingSurface.Canvas.Clear();
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
             {
@@ -68,10 +71,11 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         }
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
-        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution, size);
+        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution);
     }
 
-    protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters)
     {
         int scaled = workingSurface.Canvas.Save();
@@ -82,7 +86,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         workingSurface.Canvas.RestoreToCount(scaled);
     }
 
-    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution, VecI size)
+    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution)
     {
         int scaled = target.Canvas.Save();
         float multiplier = (float)resolution.InvertedMultiplier();
@@ -95,20 +99,42 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
     protected abstract VecI GetTargetSize(RenderContext ctx);
 
-    protected internal virtual void DrawLayerInScene(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal virtual void DrawLayerInScene(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters = true)
     {
         DrawLayerOnto(ctx, workingSurface, useFilters);
     }
 
-    protected void DrawLayerOnto(SceneObjectRenderContext ctx, DrawingSurface workingSurface, bool useFilters)
+    protected void DrawLayerOnto(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+        bool useFilters)
     {
         blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
 
         if (useFilters && Filters.Value != null)
         {
             blendPaint.SetFilters(Filters.Value);
-            DrawWithFilters(ctx, workingSurface, blendPaint);
+
+            var targetSurface = workingSurface;
+            Texture? tex = null;
+            int saved = -1;
+            if (!ctx.ProcessingColorSpace.IsSrgb)
+            {
+                saved = workingSurface.Canvas.Save();
+
+                tex = Texture.ForProcessing(workingSurface, ColorSpace.CreateSrgb()); // filters are meant to be applied in sRGB
+                targetSurface = tex.DrawingSurface;
+                workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            }
+
+            DrawWithFilters(ctx, targetSurface, blendPaint);
+
+            if(targetSurface != workingSurface)
+            {
+                workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+                tex.Dispose();
+                workingSurface.Canvas.RestoreToCount(saved);
+            }
         }
         else
         {
@@ -123,7 +149,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
     protected abstract void DrawWithFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
         Paint paint);
 
-    protected Texture TryInitWorkingSurface(VecI imageSize, ChunkResolution resolution, int id)
+    protected Texture TryInitWorkingSurface(VecI imageSize, ChunkResolution resolution, ColorSpace processingCs, int id)
     {
         ChunkResolution targetResolution = resolution;
         bool hasSurface = workingSurfaces.TryGetValue((targetResolution, id), out Texture workingSurface);
@@ -133,7 +159,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
         if (!hasSurface || workingSurface.Size != targetSize || workingSurface.IsDisposed)
         {
-            workingSurfaces[(targetResolution, id)] = new Texture(targetSize);
+            workingSurfaces[(targetResolution, id)] = Texture.ForProcessing(targetSize, processingCs);
             workingSurface = workingSurfaces[(targetResolution, id)];
         }
 

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

@@ -19,7 +19,7 @@ public class TileNode : RenderNode
     public InputProperty<ShaderTileMode> TileModeY { get; }
     public InputProperty<Matrix3X3> Matrix { get; }
 
-    private Image lastImage;
+    private Drawie.Backend.Core.Surfaces.ImageData.Image lastImage;
     private Shader tileShader;
     private Paint paint;
 

+ 41 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/PropertyValidator.cs

@@ -6,34 +6,70 @@ public delegate ValidatorResult ValidateProperty(object? value);
 
 public class PropertyValidator
 {
+    public InputProperty ForProperty { get; }
     public List<ValidateProperty> Rules { get; } = new();
 
+    public PropertyValidator(InputProperty forProperty)
+    {
+        ForProperty = forProperty;
+    }
+
     public PropertyValidator Min(VecI min)
     {
-       return Min(min, v => new VecI(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y))); 
+        return Min(min, v => new VecI(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y)));
     }
-    
+
     public PropertyValidator Min(VecD min)
     {
-        return Min(min, v => new VecD(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y))); 
+        return Min(min, v => new VecD(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y)));
     }
 
     public PropertyValidator Min<T>(T min, Func<T, T>? adjust = null) where T : IComparable<T>
     {
+        if (!typeof(T).IsAssignableTo(ForProperty.ValueType))
+        {
+            throw new ArgumentException($"Type mismatch. Expected {ForProperty.ValueType}, got {typeof(T)}");
+        }
+
         Rules.Add((value) =>
         {
             if (value is T val)
             {
                 bool isValid = val.CompareTo(min) >= 0;
-                return new (isValid, isValid ? val : GetReturnValue(val, min, adjust));
+                return new(isValid, isValid ? val : GetReturnValue(val, min, adjust));
             }
 
-            return new (false, GetReturnValue(min, min, adjust));
+            return new(false, GetReturnValue(min, min, adjust));
         });
 
         return this;
     }
 
+
+    public void Max(VecI max)
+    {
+        Max(max, v => new VecI(Math.Min(v.X, max.X), Math.Min(v.Y, max.Y)));
+    }
+
+    public void Max(VecD max)
+    {
+        Max(max, v => new VecD(Math.Min(v.X, max.X), Math.Min(v.Y, max.Y)));
+    }
+
+    public void Max<T>(T max, Func<T, T>? adjust = null) where T : IComparable<T>
+    {
+        Rules.Add((value) =>
+        {
+            if (value is T val)
+            {
+                bool isValid = val.CompareTo(max) <= 0;
+                return new(isValid, isValid ? val : GetReturnValue(val, max, adjust));
+            }
+
+            return new(false, GetReturnValue(max, max, adjust));
+        });
+    }
+
     public PropertyValidator Custom(ValidateProperty rule)
     {
         Rules.Add(rule);

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs

@@ -162,4 +162,9 @@ internal class UpdatePropertyValue_Change : Change
 
         return hash.ToHashCode();
     }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        return other is UpdatePropertyValue_Change change && change._nodeId == _nodeId && change._propertyName == _propertyName;
+    }
 }

+ 14 - 8
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -208,7 +208,7 @@ public class DocumentChangeTracker : IDisposable
             Trace.WriteLine($"Attempted to execute make change action {act} while {activeUpdateableChange} is active");
             return new None();
         }
-        
+
         bool ignoreInUndo = false;
         List<IChangeInfo> changeInfos = new();
 
@@ -219,12 +219,12 @@ public class DocumentChangeTracker : IDisposable
                 AddToUndo(interruptable, source);
             else
                 interruptable.Dispose();
-            
+
             applyInfo.Switch(
                 static (None _) => { },
                 (IChangeInfo info) => changeInfos.Add(info),
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
-            
+
             activeUpdateableChange = null;
         }
 
@@ -238,12 +238,12 @@ public class DocumentChangeTracker : IDisposable
         }
 
         var info = change.Apply(document, true, out ignoreInUndo);
-        
+
         info.Switch(
             static (None _) => { },
             (IChangeInfo changeInfo) => changeInfos.Add(changeInfo),
             (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
-        
+
         if (!ignoreInUndo)
             AddToUndo(change, source);
         else
@@ -406,9 +406,15 @@ public class DocumentChangeTracker : IDisposable
         if (running)
             throw new InvalidOperationException("Already currently processing");
         running = true;
-        var result = ProcessActionList(actions);
-        running = false;
-        return result;
+        try
+        {
+            var result = ProcessActionList(actions);
+            return result;
+        }
+        finally
+        {
+            running = false;
+        }
     }
 }
 

+ 1 - 0
src/PixiEditor.UI.Common/Controls/Slider.axaml

@@ -22,6 +22,7 @@
                 <ControlTemplate>
                     <Grid Name="grid">
                         <Border Margin="6, 0" CornerRadius="4" Background="{DynamicResource ThemeControlLowBrush}"
+                                Name="TrackBackground"
                                 Height="6"
                                 VerticalAlignment="Center">
                         </Border>

BIN
src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf


+ 1 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -157,6 +157,7 @@
             <system:String x:Key="icon-outline">&#xE99a;</system:String>
             <system:String x:Key="icon-terminal">&#xE99b;</system:String>
             <system:String x:Key="icon-cone">&#xE99c;</system:String>
+            <system:String x:Key="icon-camera">&#xE99d;</system:String>
 
         </ResourceDictionary>
     </Styles.Resources>

+ 1 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs

@@ -152,6 +152,7 @@ public static class PixiPerfectIcons
     public const string TextUnderline = "\uE998";
     public const string TextRound = "\uE999";
     public const string Cone = "\uE99c";
+    public const string Camera = "\uE99d";
 
     public static Stream GetFontStream()
     {

+ 18 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -888,5 +888,22 @@
   "TRANSFORM_NODE": "Transform",
   "UNIT": "Unit",
   "ANGLE": "Angle",
-  "DOCUMENT_INFO_NODE": "Document Info"
+  "DOCUMENT_INFO_NODE": "Document Info",
+  "MASK_NODE": "Mask",
+  "SEPIA_FILTER_NODE": "Sepia Filter",
+  "INTENSITY": "Intensity",
+  "INVERT_FILTER_NODE": "Invert Filter",
+  "COLOR_ADJUSTMENTS_FILTER": "Color Adjustments Filter",
+  "ADJUST_BRIGHTNESS": "Adjust Brightness",
+  "ADJUST_CONTRAST": "Adjust Contrast",
+  "ADJUST_SATURATION": "Adjust Saturation",
+  "ADJUST_TEMPERATURE": "Adjust Temperature",
+  "ADJUST_TINT": "Adjust Tint",
+  "ADJUST_HUE": "Adjust Hue",
+  "HUE_VALUE": "Hue",
+  "SATURATION_VALUE": "Saturation",
+  "BRIGHTNESS_VALUE": "Brightness",
+  "CONTRAST_VALUE": "Contrast",
+  "TEMPERATURE_VALUE": "Temperature",
+  "TINT_VALUE": "Tint"
 }

+ 3 - 0
src/PixiEditor/Helpers/ThemeResources.cs

@@ -32,4 +32,7 @@ public static class ThemeResources
     public static Color SelectionFillColor =>
         ResourceLoader.GetResource<Avalonia.Media.Color>("SelectionFillColor", Application.Current.ActualThemeVariant)
             .ToColor();
+
+    public static SolidColorBrush ThemeControlLowBrush =>
+        ResourceLoader.GetResource<SolidColorBrush>("ThemeControlLowBrush", Application.Current.ActualThemeVariant);
 }

+ 4 - 4
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -87,13 +87,13 @@ internal class ActionAccumulator
         if (executing || queuedActions.Count == 0)
             return;
         executing = true;
-        DispatcherTimer busyTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(2000) };
+        /*DispatcherTimer busyTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(2000) };
         busyTimer.Tick += (_, _) =>
         {
             busyTimer.Stop();
             document.Busy = true;
         };
-        busyTimer.Start();
+        busyTimer.Start();*/
 
         try
         {
@@ -158,7 +158,7 @@ internal class ActionAccumulator
         }
         catch (Exception e)
         {
-            busyTimer.Stop();
+            //busyTimer.Stop();
             document.Busy = false;
             executing = false;
 #if DEBUG
@@ -168,7 +168,7 @@ internal class ActionAccumulator
             throw;
         }
 
-        busyTimer.Stop();
+        //busyTimer.Stop();
         if (document.Busy)
             document.Busy = false;
         executing = false;

+ 1 - 0
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -23,6 +23,7 @@ public interface INodeHandler : INotifyPropertyChanged
     public PreviewPainter? ResultPainter { get; set; }
     public VecD PositionBindable { get; set; }
     public bool IsNodeSelected { get; set; }
+    public string Icon { get; }
     public void TraverseBackwards(Func<INodeHandler, bool> func);
     public void TraverseBackwards(Func<INodeHandler, INodeHandler, bool> func);
     public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);

+ 8 - 2
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -71,6 +71,11 @@ public class PreviewPainter
 
     public void ChangeRenderTextureSize(int requestId, VecI size)
     {
+        if (size.X <= 0 || size.Y <= 0)
+        {
+            return;
+        }
+
         if (repaintingTextures.Contains(requestId))
         {
             pendingResizes[requestId] = size;
@@ -161,7 +166,8 @@ public class PreviewPainter
                                 try
                                 {
                                     renderTexture.Dispose();
-                                } catch (Exception) { }
+                                }
+                                catch (Exception) { }
                             }
 
                             renderTextures.Remove(texture);
@@ -170,7 +176,7 @@ public class PreviewPainter
                             dirtyTextures.Remove(texture);
                             return;
                         }
-                        
+
                         if (renderTexture is { IsDisposed: false })
                         {
                             try

+ 1 - 0
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -43,6 +43,7 @@
                                 <nodes:NodeView
                                     Node="{Binding}"
                                     DisplayName="{Binding NodeNameBindable}"
+                                    Icon="{Binding Icon}"
                                     CategoryBackgroundBrush="{Binding CategoryBackgroundBrush}"
                                     Inputs="{Binding Inputs}"
                                     ActiveFrame="{Binding ActiveFrame, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"

+ 5 - 3
src/PixiEditor/Styles/Templates/NodeView.axaml

@@ -31,8 +31,10 @@
                         <Border Padding="{TemplateBinding Padding}" Grid.ColumnSpan="3" Grid.Row="0"
                                 CornerRadius="4.5, 4.5, 0 ,0"
                                 Background="{TemplateBinding CategoryBackgroundBrush}">
-                            <TextBlock ui:Translator.Key="{TemplateBinding DisplayName}"
-                                       FontWeight="Bold" />
+                            <TextBlock FontWeight="Bold">
+                                <Run Classes="pixi-icon" BaselineAlignment="Center" Text="{TemplateBinding Icon}"/>
+                                <Run BaselineAlignment="Center" ui:Translator.Key="{TemplateBinding DisplayName}"/>
+                            </TextBlock>
                         </Border>
                         <Border Grid.Row="1" Background="{DynamicResource ThemeControlMidBrush}">
                             <StackPanel>
@@ -60,7 +62,7 @@
                                     <ImageBrush Source="/Images/CheckerTile.png"
                                                 TileMode="Tile" DestinationRect="0, 0, 25, 25" />
                                 </Panel.Background>
-                            <visuals:PreviewPainterControl 
+                            <visuals:PreviewPainterControl
                                                            PreviewPainter="{TemplateBinding ResultPreview}"
                                                            FrameToRender="{TemplateBinding ActiveFrame}"
                                                            RenderOptions.BitmapInterpolationMode="None">

+ 202 - 0
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ColorAdjustmentsFilterNode.cs

@@ -0,0 +1,202 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
+
+[NodeInfo("ColorAdjustmentsFilter")]
+public class ColorAdjustmentsFilterNode : FilterNode
+{
+    public InputProperty<bool> AdjustBrightness { get; }
+    public InputProperty<double> BrightnessValue { get; }
+
+    public InputProperty<bool> AdjustContrast { get; }
+    public InputProperty<double> ContrastValue { get; }
+
+    public InputProperty<bool> AdjustTemperature { get; }
+    public InputProperty<double> TemperatureValue { get; }
+
+    public InputProperty<bool> AdjustTint { get; }
+    public InputProperty<double> TintValue { get; }
+
+    public InputProperty<bool> AdjustSaturation { get; }
+    public InputProperty<double> SaturationValue { get; }
+
+    public InputProperty<bool> AdjustHue { get; }
+    public InputProperty<double> HueValue { get; }
+
+    private List<ColorFilter> filters = new List<ColorFilter>();
+    private ColorFilter lastCombinedFilter;
+
+    public ColorAdjustmentsFilterNode()
+    {
+        AdjustBrightness = CreateInput("AdjustBrightness", "ADJUST_BRIGHTNESS", false);
+        BrightnessValue = CreateInput("BrightnessValue", "BRIGHTNESS_VALUE", 0.0)
+            .WithRules(rules => rules.Min(-1d).Max(1d));
+
+        AdjustContrast = CreateInput("AdjustContrast", "ADJUST_CONTRAST", false);
+        ContrastValue = CreateInput("ContrastValue", "CONTRAST_VALUE", 0.0)
+            .WithRules(rules => rules.Min(-1d).Max(1d));
+
+        AdjustTemperature = CreateInput("AdjustTemperature", "ADJUST_TEMPERATURE", false);
+        TemperatureValue = CreateInput("TemperatureValue", "TEMPERATURE_VALUE", 0.0)
+            .WithRules(rules => rules.Min(-1d).Max(1d));
+
+        AdjustTint = CreateInput("AdjustTint", "ADJUST_TINT", false);
+        TintValue = CreateInput("TintValue", "TINT_VALUE", 0.0)
+            .WithRules(rules => rules.Min(-1d).Max(1d));
+
+        AdjustSaturation = CreateInput("AdjustSaturation", "ADJUST_SATURATION", false);
+        SaturationValue = CreateInput("SaturationValue", "SATURATION_VALUE", 0.0)
+            .WithRules(rules => rules.Min(-1d).Max(1d));
+
+        AdjustHue = CreateInput("AdjustHue", "ADJUST_HUE", false);
+        HueValue = CreateInput("HueValue", "HUE_VALUE", 0.0)
+            .WithRules(rules => rules.Min(-180d).Max(180d));
+    }
+
+    protected override ColorFilter? GetColorFilter()
+    {
+        filters.ForEach(filter => filter.Dispose());
+        filters.Clear();
+
+        CreateBrightnessFilter();
+        CreateContrastFilter();
+        CreateTemperatureFilter();
+        CreateTintFilter();
+        CreateSaturationFilter();
+        CreateHueFilter();
+
+        lastCombinedFilter?.Dispose();
+        lastCombinedFilter = CombineFilters();
+        return lastCombinedFilter;
+    }
+
+    private void CreateBrightnessFilter()
+    {
+        if (AdjustBrightness.Value)
+        {
+            float brightnessValue = (float)BrightnessValue.Value;
+            ColorFilter brightnessFilter = ColorFilter.CreateColorMatrix(
+            [
+                1, 0, 0, 0, brightnessValue,
+                0, 1, 0, 0, brightnessValue,
+                0, 0, 1, 0, brightnessValue,
+                0, 0, 0, 1, 0
+            ]);
+            filters.Add(brightnessFilter);
+        }
+    }
+
+    private void CreateContrastFilter()
+    {
+        if (AdjustContrast.Value)
+        {
+            float contrastValue = (float)ContrastValue.Value;
+            ColorFilter contrastFilter =
+                ColorFilter.CreateHighContrast(false, ContrastInvertMode.InvertBrightness, contrastValue);
+            filters.Add(contrastFilter);
+        }
+    }
+
+    private void CreateTemperatureFilter()
+    {
+        if (AdjustTemperature.Value)
+        {
+            float temperatureValue = (float)TemperatureValue.Value;
+            ColorFilter temperatureFilter = ColorFilter.CreateColorMatrix(
+            [
+                1, 0, 0, 0, temperatureValue,
+                0, 1, 0, 0, 0,
+                0, 0, 1, 0, -temperatureValue,
+                0, 0, 0, 1, 0
+            ]);
+            filters.Add(temperatureFilter);
+        }
+    }
+
+    private void CreateTintFilter()
+    {
+        if (AdjustTint.Value)
+        {
+            float tintValue = (float)TintValue.Value;
+            ColorFilter tintFilter = ColorFilter.CreateColorMatrix(
+            [
+                1, 0, 0, 0, 0,
+                0, 1, 0, 0, tintValue,
+                0, 0, 1, 0, 0,
+                0, 0, 0, 1, 0
+            ]);
+            filters.Add(tintFilter);
+        }
+    }
+
+    private void CreateSaturationFilter()
+    {
+        if (AdjustSaturation.Value)
+        {
+            float saturationValue = (float)SaturationValue.Value + 1;
+            ColorFilter saturationFilter = ColorFilter.CreateColorMatrix(
+            [
+                0.213f + 0.787f * saturationValue, 0.715f - 0.715f * saturationValue, 0.072f - 0.072f * saturationValue,
+                0, 0,
+                0.213f - 0.213f * saturationValue, 0.715f + 0.285f * saturationValue, 0.072f - 0.072f * saturationValue,
+                0, 0,
+                0.213f - 0.213f * saturationValue, 0.715f - 0.715f * saturationValue, 0.072f + 0.928f * saturationValue,
+                0, 0,
+                0, 0, 0, 1, 0
+            ]);
+            filters.Add(saturationFilter);
+        }
+    }
+
+    private void CreateHueFilter()
+    {
+        if (AdjustHue.Value)
+        {
+            float value = (float)-HueValue.Value * (float)Math.PI / 180f;
+            var cosVal = (float)Math.Cos(value);
+            var sinVal = (float)Math.Sin(value);
+            float lumR = 0.213f;
+            float lumG = 0.715f;
+            float lumB = 0.072f;
+
+            ColorFilter hueFilter = ColorFilter.CreateColorMatrix(
+            [
+                lumR + cosVal * (1 - lumR) + sinVal * (-lumR), lumG + cosVal * (-lumG) + sinVal * (-lumG),
+                lumB + cosVal * (-lumB) + sinVal * (1 - lumB), 0, 0,
+                lumR + cosVal * (-lumR) + sinVal * (0.143f), lumG + cosVal * (1 - lumG) + sinVal * (0.140f),
+                lumB + cosVal * (-lumB) + sinVal * (-0.283f), 0, 0,
+                lumR + cosVal * (-lumR) + sinVal * (-(1 - lumR)), lumG + cosVal * (-lumG) + sinVal * (lumG),
+                lumB + cosVal * (1 - lumB) + sinVal * (lumB), 0, 0,
+                0, 0, 0, 1, 0,
+            ]);
+
+            filters.Add(hueFilter);
+        }
+    }
+
+    private ColorFilter? CombineFilters()
+    {
+        if (filters.Count == 0)
+        {
+            return null;
+        }
+
+        ColorFilter combinedFilter = filters[0];
+        for (int i = 1; i < filters.Count; i++)
+        {
+            combinedFilter = ColorFilter.CreateCompose(combinedFilter, filters[i]);
+        }
+
+        return combinedFilter;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new ColorAdjustmentsFilterNode();
+    }
+}

+ 112 - 0
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ColorAdjustmentsFilterNodeViewModel.cs

@@ -0,0 +1,112 @@
+using Avalonia;
+using Avalonia.Media;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Events;
+using PixiEditor.Models.Handlers;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+using PixiEditor.ViewModels.Nodes.Properties;
+
+namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
+
+[NodeViewModel("COLOR_ADJUSTMENTS_FILTER", "FILTERS", PixiPerfectIcons.Sun )]
+internal class ColorAdjustmentsFilterNodeViewModel : NodeViewModel<ColorAdjustmentsFilterNode>
+{
+    private Dictionary<string, List<INodePropertyHandler>> toggleToProperties = new Dictionary<string, List<INodePropertyHandler>>();
+    public override void OnInitialized()
+    {
+        foreach (var input in Inputs)
+        {
+            if(input is BooleanPropertyViewModel booleanProperty)
+            {
+                booleanProperty.ValueChanged += BooleanPropertyOnValueChanged;
+                toggleToProperties.Add(input.PropertyName, new List<INodePropertyHandler>());
+            }
+            if (input is DoublePropertyViewModel doubleProperty)
+            {
+                doubleProperty.NumberPickerMode = NumberPickerMode.Slider;
+                doubleProperty.Min = -1;
+                doubleProperty.Max = 1;
+
+                if(input.PropertyName == "HueValue")
+                {
+                    doubleProperty.Min = -180;
+                    doubleProperty.Max = 180;
+                }
+
+                doubleProperty.IsVisible = false;
+                doubleProperty.SliderSettings.IsColorSlider = true;
+                var background = SolveBackground(doubleProperty.PropertyName);
+
+                doubleProperty.SliderSettings.BackgroundBrush = background;
+                AddToToggleGroup(input);
+            }
+        }
+    }
+
+    private static IBrush SolveBackground(string propertyName)
+    {
+        if (propertyName.Contains("Brightness") || propertyName.Contains("Saturation") || propertyName.Contains("Contrast"))
+        {
+            LinearGradientBrush brush = new LinearGradientBrush();
+            brush.GradientStops.Add(new GradientStop(Colors.Black, 0));
+            brush.GradientStops.Add(new GradientStop(Colors.White, 1));
+
+            return brush;
+        }
+
+        if (propertyName.Contains("Temperature"))
+        {
+            LinearGradientBrush brush = new LinearGradientBrush();
+            brush.GradientStops.Add(new GradientStop(Colors.Blue, 0));
+            brush.GradientStops.Add(new GradientStop(Colors.Red, 1));
+
+            return brush;
+        }
+
+        if (propertyName.Contains("Tint"))
+        {
+            LinearGradientBrush brush = new LinearGradientBrush();
+            brush.GradientStops.Add(new GradientStop(Colors.Green, 0));
+            brush.GradientStops.Add(new GradientStop(Colors.Magenta, 1));
+
+            return brush;
+        }
+
+        if (propertyName.Contains("Hue"))
+        {
+            LinearGradientBrush brush = new LinearGradientBrush();
+            brush.GradientStops.Add(new GradientStop(Colors.Red, 0));
+            brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0.166));
+            brush.GradientStops.Add(new GradientStop(Colors.Green, 0.333));
+            brush.GradientStops.Add(new GradientStop(Colors.Cyan, 0.5));
+            brush.GradientStops.Add(new GradientStop(Colors.Blue, 0.666));
+            brush.GradientStops.Add(new GradientStop(Colors.Magenta, 0.833));
+            brush.GradientStops.Add(new GradientStop(Colors.Red, 1));
+
+            return brush;
+        }
+
+        return ThemeResources.ThemeControlLowBrush;
+    }
+
+    private void BooleanPropertyOnValueChanged(INodePropertyHandler property, NodePropertyValueChangedArgs args)
+    {
+        if (toggleToProperties.TryGetValue(property.PropertyName, out var toProperty))
+        {
+            foreach (var prop in toProperty)
+            {
+                prop.IsVisible = args.NewValue is bool b and true;
+            }
+        }
+    }
+
+    private void AddToToggleGroup(INodePropertyHandler property)
+    {
+        string groupName = "Adjust" + property.PropertyName.Replace("Value", "");
+        if (toggleToProperties.ContainsKey(groupName))
+        {
+            toggleToProperties[groupName].Add(property);
+        }
+    }
+}

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/InvertFilterNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
+
+[NodeViewModel("INVERT_FILTER_NODE", "FILTERS", PixiPerfectIcons.Invert)]
+internal class InvertFilterNodeViewModel : NodeViewModel<InvertFilterNode>;

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/SepiaFilterNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
+
+[NodeViewModel("SEPIA_FILTER_NODE", "FILTERS", PixiPerfectIcons.Camera)]
+internal class SepiaFilterNodeViewModel : NodeViewModel<SepiaFilterNode>;

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/Image/MaskNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Image;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Image;
+
+[NodeViewModel("MASK_NODE", "IMAGE", PixiPerfectIcons.CreateMask)]
+internal class MaskNodeViewModel : NodeViewModel<MaskNode>;

+ 14 - 4
src/PixiEditor/ViewModels/Document/TransformOverlays/TextOverlayViewModel.cs

@@ -57,19 +57,19 @@ internal class TextOverlayViewModel : ObservableObject, ITextOverlayHandler
         get => requestEditTextTrigger;
         set => SetProperty(ref requestEditTextTrigger, value);
     }
-    
+
     public Matrix3X3 Matrix
     {
         get => matrix;
         set => SetProperty(ref matrix, value);
     }
-    
+
     public double? Spacing
     {
         get => spacing;
         set => SetProperty(ref spacing, value);
     }
-    
+
     public int CursorPosition
     {
         get => cursorPosition;
@@ -86,11 +86,21 @@ internal class TextOverlayViewModel : ObservableObject, ITextOverlayHandler
     {
         VecD mapped = Matrix.Invert().MapPoint(closestToPosition);
         RichText richText = new(Text);
+        if (Font == null)
+        {
+            return;
+        }
+
         var positions = richText.GetGlyphPositions(Font);
+        if (positions == null || positions.Length == 0)
+        {
+            return;
+        }
+
         int indexOfClosest = positions.Select((pos, index) => (pos, index))
             .OrderBy(pos => ((pos.pos + Position - new VecD(0, Font.Size / 2f)) - mapped).LengthSquared)
             .First().index;
-        
+
         CursorPosition = indexOfClosest;
         SelectionEnd = indexOfClosest;
     }

+ 3 - 0
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -30,6 +30,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     private ObservableRangeCollection<INodePropertyHandler> outputs = new();
     private PreviewPainter resultPainter;
     private bool isSelected;
+    private string? icon;
 
     protected Guid id;
 
@@ -165,6 +166,8 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         OnPropertyChanged(nameof(NodeNameBindable));
     }
 
+    public string Icon => icon ??= GetType().GetCustomAttribute<NodeViewModelAttribute>().Icon;
+
     public void TraverseBackwards(Func<INodeHandler, bool> func)
     {
         var visited = new HashSet<INodeHandler>();

+ 85 - 1
src/PixiEditor/ViewModels/Nodes/Properties/DoublePropertyViewModel.cs

@@ -1,8 +1,92 @@
-namespace PixiEditor.ViewModels.Nodes.Properties;
+using System.ComponentModel;
+using Avalonia;
+using Avalonia.Media;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace PixiEditor.ViewModels.Nodes.Properties;
 
 internal class DoublePropertyViewModel : NodePropertyViewModel<double>
 {
+    private double min = double.MinValue;
+    private double max = double.MaxValue;
+
+    private NumberPickerMode numberPickerMode = NumberPickerMode.NumberInput;
+
+    private SliderSettings sliderSettings = new SliderSettings();
+
+    public NumberPickerMode NumberPickerMode
+    {
+        get => numberPickerMode;
+        set => SetProperty(ref numberPickerMode, value);
+    }
+
+    public double DoubleValue
+    {
+        get => Value;
+        set => Value = value;
+    }
+
+    public double Min
+    {
+        get => min;
+        set => SetProperty(ref min, value);
+    }
+
+    public double Max
+    {
+        get => max;
+        set => SetProperty(ref max, value);
+    }
+
+    public SliderSettings SliderSettings
+    {
+        get => sliderSettings;
+        set => SetProperty(ref sliderSettings, value);
+    }
+
     public DoublePropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
     {
     }
+
+    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+    {
+        base.OnPropertyChanged(e);
+        if (e.PropertyName == nameof(Value))
+        {
+            OnPropertyChanged(nameof(DoubleValue));
+        }
+    }
+}
+
+class SliderSettings : ObservableObject
+{
+    private bool isColorSlider;
+    private IBrush backgroundBrush;
+    private IBrush borderBrush;
+    private Thickness borderThickness;
+    public double thumbSize;
+    public IBrush thumbBackground;
+
+    public bool IsColorSlider
+    {
+        get => isColorSlider;
+        set => SetProperty(ref isColorSlider, value);
+    }
+
+    public IBrush BackgroundBrush
+    {
+        get => backgroundBrush;
+        set => SetProperty(ref backgroundBrush, value);
+    }
+
+    public SliderSettings()
+    {
+
+    }
+}
+
+public enum NumberPickerMode
+{
+    NumberInput,
+    Slider,
 }

+ 21 - 4
src/PixiEditor/Views/Input/NumberInput.cs

@@ -38,6 +38,15 @@ internal partial class NumberInput : TextBox
         AvaloniaProperty.Register<NumberInput, bool>(
             "EnableScrollChange", true);
 
+    public static readonly StyledProperty<bool> EnableGrabberProperty = AvaloniaProperty.Register<NumberInput, bool>(
+        nameof(EnableGrabber), true);
+
+    public bool EnableGrabber
+    {
+        get => GetValue(EnableGrabberProperty);
+        set => SetValue(EnableGrabberProperty, value);
+    }
+
     public string FormattedValue
     {
         get => GetValue(FormattedValueProperty);
@@ -164,14 +173,22 @@ internal partial class NumberInput : TextBox
     {
         base.OnApplyTemplate(e);
 
-        InnerLeftContent = leftGrabber = CreateMouseGrabber();
-        leftGrabber.HorizontalAlignment = HorizontalAlignment.Left;
-        InnerRightContent = rightGrabber = CreateMouseGrabber();
-        rightGrabber.HorizontalAlignment = HorizontalAlignment.Right;
+        if (EnableGrabber)
+        {
+            InnerLeftContent = leftGrabber = CreateMouseGrabber();
+            leftGrabber.HorizontalAlignment = HorizontalAlignment.Left;
+            InnerRightContent = rightGrabber = CreateMouseGrabber();
+            rightGrabber.HorizontalAlignment = HorizontalAlignment.Right;
+        }
     }
 
     protected override void OnSizeChanged(SizeChangedEventArgs e)
     {
+        if (!EnableGrabber)
+        {
+            return;
+        }
+
         if (e.NewSize.Width < 100)
         {
             rightGrabber.IsVisible = false;

+ 9 - 0
src/PixiEditor/Views/Nodes/NodeView.cs

@@ -59,6 +59,15 @@ public class NodeView : TemplatedControl
     public static readonly StyledProperty<ICommand> EndDragCommandProperty =
         AvaloniaProperty.Register<NodeView, ICommand>("EndDragCommand");
 
+    public static readonly StyledProperty<string> IconProperty = AvaloniaProperty.Register<NodeView, string>(
+        nameof(Icon));
+
+    public string Icon
+    {
+        get => GetValue(IconProperty);
+        set => SetValue(IconProperty, value);
+    }
+
     public INodeHandler Node
     {
         get => GetValue(NodeProperty);

+ 63 - 8
src/PixiEditor/Views/Nodes/Properties/DoublePropertyView.axaml

@@ -8,16 +8,71 @@
                              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
                              xmlns:properties1="clr-namespace:PixiEditor.ViewModels.Nodes.Properties"
+                             xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              x:DataType="properties1:DoublePropertyViewModel"
                              x:Class="PixiEditor.Views.Nodes.Properties.DoublePropertyView">
     <Design.DataContext>
-        <properties1:DoublePropertyViewModel/>
+        <properties1:DoublePropertyViewModel />
     </Design.DataContext>
-    <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
-        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
-        <input:NumberInput EnableScrollChange="False" Name="input"
-                           HorizontalAlignment="Right" MinWidth="100" Decimals="6" IsVisible="{Binding ShowInputField}"
-                           Value="{Binding Value, Mode=TwoWay}" />
-    </Grid>
-</properties:NodePropertyView>
+    <StackPanel Orientation="Vertical"
+                HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}" />
+        <Panel IsVisible="{Binding ShowInputField}">
+            <DockPanel
+                LastChildFill="True"
+                IsVisible="{Binding NumberPickerMode,
+                                            Converter={converters:EnumBooleanConverter}, ConverterParameter=Slider}">
+                <input:NumberInput DockPanel.Dock="Right"
+                                   EnableScrollChange="False" Name="sliderInput"
+                                   Width="45" Decimals="2"
+                                   EnableGrabber="False"
+                                   Min="{Binding Min}" Max="{Binding Max}"
+                                   Value="{Binding DoubleValue, Mode=TwoWay}" />
+
+                <Slider Value="{Binding DoubleValue, Mode=TwoWay}"
+                        Margin="5, 0"
+                        Classes.colorSlider="{Binding SliderSettings.IsColorSlider}"
+                        Minimum="{Binding Min}" Maximum="{Binding Max}">
+                    <Slider.Styles>
+                        <Style Selector="Slider.colorSlider Border#TrackBackground">
+                            <Setter Property="Background" Value="{Binding SliderSettings.BackgroundBrush}" />
+                            <Setter Property="BorderThickness" Value="0" />
+                            <Setter Property="Height" Value="8" />
+                            <Setter Property="Margin" Value="0"/>
+                        </Style>
+                        <Style Selector="Slider.colorSlider Thumb">
+                            <Setter Property="Width" Value="10" />
+                            <Setter Property="Height" Value="10" />
+                            <Setter Property="MinWidth" Value="10" />
+                            <Setter Property="MinHeight" Value="10" />
+                        </Style>
+                        <Style Selector="Slider.colorSlider Thumb Border">
+                            <Setter Property="Width" Value="10" />
+                            <Setter Property="Height" Value="10" />
+                            <Setter Property="CornerRadius" Value="50" />
+                            <Setter Property="Background">
+                                <Setter.Value>
+                                    <VisualBrush>
+                                        <VisualBrush.Visual>
+                                            <Ellipse Width="10" Height="10" Fill="Transparent" Stroke="White"
+                                                     StrokeThickness="1" />
+                                        </VisualBrush.Visual>
+                                    </VisualBrush>
+                                </Setter.Value>
+                            </Setter>
+                        </Style>
+                    </Slider.Styles>
+                </Slider>
+
+
+            </DockPanel>
+            <input:NumberInput EnableScrollChange="False" Name="input"
+                               MinWidth="100" Decimals="6"
+                               IsVisible="{Binding NumberPickerMode,
+                                            Converter={converters:EnumBooleanConverter}, ConverterParameter=NumberInput}"
+                               Min="{Binding Min}" Max="{Binding Max}"
+                               Value="{Binding DoubleValue, Mode=TwoWay}" />
+        </Panel>
+    </StackPanel>
+</properties:NodePropertyView>