Browse Source

Merge branch 'funcy-nodes' into node-backend

flabbet 1 year ago
parent
commit
202cf12871
56 changed files with 1957 additions and 61 deletions
  1. 18 1
      src/PixiEditor.AvaloniaUI/Data/Localization/Languages/en.json
  2. 10 0
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/ColorChannel.cs
  3. 14 0
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/ColorChannelMode.cs
  4. 1 0
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs
  5. 154 0
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/ViewportColorChannels.cs
  6. 9 10
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml
  7. 2 2
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml
  8. 143 0
      src/PixiEditor.AvaloniaUI/ViewModels/Dock/ChannelsDockViewModel.cs
  9. 4 1
      src/PixiEditor.AvaloniaUI/ViewModels/Dock/LayoutManager.cs
  10. 170 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/ColorMatrixPropertyViewModel.cs
  11. 8 1
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/ColorPropertyViewModel.cs
  12. 8 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/Int32PropertyViewModel.cs
  13. 170 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/Matrix4x5FPropertyViewModel.cs
  14. 10 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/VecDPropertyViewModel.cs
  15. 9 0
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ViewportWindowViewModel.cs
  16. 48 0
      src/PixiEditor.AvaloniaUI/Views/Dock/ChannelsDockView.axaml
  17. 14 0
      src/PixiEditor.AvaloniaUI/Views/Dock/ChannelsDockView.axaml.cs
  18. 1 0
      src/PixiEditor.AvaloniaUI/Views/Dock/DocumentTemplate.axaml
  19. 1 0
      src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml
  20. 9 0
      src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml.cs
  21. 49 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorMatrixPropertyView.axaml
  22. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorMatrixPropertyView.axaml.cs
  23. 5 1
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorPropertyView.axaml
  24. 15 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Int32PropertyView.axaml
  25. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Int32PropertyView.axaml.cs
  26. 47 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Matrix4x5FPropertyView.axaml
  27. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Matrix4x5FPropertyView.axaml.cs
  28. 3 12
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs
  29. 18 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecDPropertyView.axaml
  30. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecDPropertyView.axaml.cs
  31. 2 2
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecIPropertyView.axaml
  32. 26 2
      src/PixiEditor.AvaloniaUI/Views/Rendering/Scene.cs
  33. 9 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs
  34. 4 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodePropertyInfo.cs
  35. 19 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FieldContext.cs
  36. 7 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/NoNodeFieldContextException.cs
  37. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldInputProperty.cs
  38. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldOutputProperty.cs
  39. 24 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  40. 0 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/FieldContext.cs
  41. 19 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CircleNode.cs
  42. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineColorNode.cs
  43. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  44. 53 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MatrixTransformNode.cs
  45. 3 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  46. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  47. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  48. 92 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SeparateChannelsNode.cs
  49. 1 0
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IColorFilterImplementation.cs
  50. 2 1
      src/PixiEditor.DrawingApi.Core/Bridge/Operations/ICanvasImplementation.cs
  51. 15 3
      src/PixiEditor.DrawingApi.Core/Surface/Canvas.cs
  52. 18 0
      src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/ColorFilter.cs
  53. 8 2
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs
  54. 8 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaColorFilterImplementation.cs
  55. 390 0
      src/PixiEditor.Numerics/ColorMatrix.cs
  56. 253 0
      src/PixiEditor.Numerics/Matrix4x5F.cs

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

@@ -616,5 +616,22 @@
   "STROKE_WIDTH": "Stroke width",
   "FILL_COLOR": "Fill color",
   "TOP": "Top",
-  "BOTTOM": "Bottom"
+  "BOTTOM": "Bottom",
+  "CHANNELS_DOCK_TITLE": "Channels",
+  "RED": "Red",
+  "GREEN": "Green",
+  "BLUE": "Blue",
+  "ALPHA": "Alpha",
+  "COLOR": "Color",
+  "COORDINATE": "Coordinate",
+  "VECTOR": "Vector",
+  "MATRIX": "Matrix",
+  "TRANSFORMED": "Transformed",
+  "GRAYSCALE": "Grayscale",
+  "CLAMP": "Clamp",
+  "SIZE": "Size",
+  "EMPTY_IMAGE": "Empty image",
+  "NOISE": "Noise",
+  "SCALE": "Scale",
+  "SEED": "Seed"
 }

+ 10 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ColorChannel.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.AvaloniaUI.Models.DocumentModels;
+
+public enum ColorChannel
+{
+    None = -1,
+    Red,
+    Green,
+    Blue,
+    Alpha
+}

+ 14 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ColorChannelMode.cs

@@ -0,0 +1,14 @@
+using System.Diagnostics.Contracts;
+
+namespace PixiEditor.AvaloniaUI.Models.DocumentModels;
+
+internal record struct ColorChannelMode(bool IsVisible, bool IsSolo)
+{
+    [Pure]
+    public ColorChannelMode WithVisible(bool visible) => this with { IsVisible = visible };
+    
+    [Pure]
+    public ColorChannelMode WithSolo(bool solo) => this with { IsSolo = solo };
+
+    public static ColorChannelMode Default => new(true, false);
+}

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -517,6 +517,7 @@ internal class DocumentUpdater
             prop.PropertyName = input.PropertyName;
             prop.IsInput = isInput;
             prop.IsFunc = input.ValueType.IsAssignableTo(typeof(Delegate));
+            prop.InternalSetValue(input.InputValue);
             inputs.Add(prop);
         }
         

+ 154 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ViewportColorChannels.cs

@@ -0,0 +1,154 @@
+using System.ComponentModel;
+using System.Diagnostics.Contracts;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Models.DocumentModels;
+
+internal record struct ViewportColorChannels
+{
+    public ColorChannelMode Red { get; }
+
+    public ColorChannelMode Green { get; }
+
+    public ColorChannelMode Blue { get; }
+
+    public ColorChannelMode Alpha { get; }
+
+    public ViewportColorChannels(ColorChannelMode red, ColorChannelMode green, ColorChannelMode blue, ColorChannelMode alpha)
+    {
+        ReadOnlySpan<ColorChannelMode> modes = [red, green, blue, alpha];
+        int solos = 0;
+
+        for (int i = 0; i < modes.Length; i++)
+        {
+            if (!modes[i].IsSolo)
+            {
+                continue;
+            }
+
+            solos++;
+
+            if (solos > 1)
+            {
+                throw new ArgumentException("Can't have more than one channel solo");
+            }
+        }
+
+        Red = red;
+        Green = green;
+        Blue = blue;
+        Alpha = alpha;
+    }
+
+    public static ViewportColorChannels Default => new(ColorChannelMode.Default, ColorChannelMode.Default, ColorChannelMode.Default, ColorChannelMode.Default);
+
+    public override string ToString() => $"Red: {Red}; Green: {Green}; Blue: {Blue}; Alpha: {Alpha}";
+
+    public bool IsVisiblyVisible(ColorChannel channel) =>
+        GetModeForChannel(channel).IsVisible || GetModeForChannel(channel).IsSolo;
+
+    public bool IsSolo(ColorChannel channel) => GetModeForChannel(channel).IsSolo;
+
+    [Pure]
+    public ViewportColorChannels WithModeForChannel(ColorChannel channel, Func<ColorChannelMode, ColorChannelMode> mode, bool otherNonSolo)
+    {
+        switch (channel)
+        {
+            case ColorChannel.Red:
+                return new ViewportColorChannels(mode(Red), MON(Green), MON(Blue), MON(Alpha));
+            case ColorChannel.Green:
+                return new ViewportColorChannels(MON(Red), mode(Green), MON(Blue), MON(Alpha));
+            case ColorChannel.Blue:
+                return new ViewportColorChannels(MON(Red), MON(Green), mode(Blue), MON(Alpha));
+            case ColorChannel.Alpha:
+                return new ViewportColorChannels(MON(Red), MON(Green), MON(Blue), mode(Alpha));
+            case ColorChannel.None:
+                throw new InvalidEnumArgumentException(nameof(channel), (int)channel, typeof(ColorChannel));
+            default:
+                throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
+        }
+
+        // Modify Other Node
+        ColorChannelMode MON(ColorChannelMode otherMode)
+        {
+            if (otherNonSolo && otherMode.IsSolo)
+            {
+                return otherMode.WithSolo(false);
+            }
+
+            return otherMode;
+        }
+    }
+    
+    public ColorChannelMode GetModeForChannel(ColorChannel channel) => channel switch
+    {
+        ColorChannel.Red => Red,
+        ColorChannel.Green => Green,
+        ColorChannel.Blue => Blue,
+        ColorChannel.Alpha => Alpha
+    };
+
+    public ColorMatrix GetColorMatrix()
+    {
+        var solo = GetSoloChannel();
+
+        var (otherToRed, redToRed) = GetTarget(Red, solo, ColorChannel.Red);
+        var (otherToGreen, greenToGreen) = GetTarget(Green, solo, ColorChannel.Green);
+        var (otherToBlue, blueToBlue) = GetTarget(Blue, solo, ColorChannel.Blue);
+        
+        var opaque = solo is not ColorChannel.None || !Alpha.IsVisible;
+
+        var alphaToOther = Alpha.IsSolo;
+        var alphaToAlpha = !alphaToOther && !opaque;
+
+        var o2r = otherToRed ? 1 : 0;
+        var r2r = redToRed ? 1 : 0;
+
+        var o2g = otherToGreen ? 1 : 0;
+        var g2g = greenToGreen ? 1 : 0;
+
+        var o2b = otherToBlue ? 1 : 0;
+        var b2b = blueToBlue ? 1 : 0;
+
+        var a2o = alphaToOther ? 1 : 0;
+        var a2a = alphaToAlpha ? 1 : 0;
+
+        var o = opaque ? 1 : 0;
+
+        return new ColorMatrix(
+            (r2r, o2g, o2b, a2o, 0),
+            (o2r, g2g, o2b, a2o, 0),
+            (o2r, o2g, b2b, a2o, 0),
+            (0, 0, 0, a2a, o)
+        );
+    }
+
+    private static (bool otherToRed, bool targetToTarget) GetTarget(ColorChannelMode mode, ColorChannel solo, ColorChannel target)
+    {
+        var otherToTarget = solo == target;
+        var targetToTarget = solo == target || (mode.IsVisible && solo == ColorChannel.None);
+
+        return (otherToTarget, targetToTarget);
+    }
+
+    public ColorChannel GetSoloChannel()
+    {
+        ReadOnlySpan<(ColorChannel channel, ColorChannelMode mode)> modes = [
+            (ColorChannel.Red, Red),
+            (ColorChannel.Green, Green),
+            (ColorChannel.Blue, Blue),
+            (ColorChannel.Alpha, Alpha)
+        ];
+    
+        for (int i = 0; i < modes.Length; i++)
+        {
+            var mode = modes[i];
+            if (modes[i].mode.IsSolo)
+            {
+                return mode.channel;
+            }
+        }
+    
+        return ColorChannel.None;
+    }
+}

+ 9 - 10
src/PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml

@@ -8,23 +8,22 @@
             <ControlTemplate>
                 <Grid Margin="-5, 2" ColumnDefinitions="15, *, 15" MinHeight="18">
                     <properties:NodeSocket Name="PART_InputSocket"
+                                           ClipToBounds="False"
                                            Node="{Binding DataContext.Node, RelativeSource={RelativeSource TemplatedParent}}"
-                                           Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
                                            SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}"
-                                           IsFunc="{Binding DataContext.IsFunc, RelativeSource={RelativeSource TemplatedParent}}"
-                                           IsVisible="{Binding DataContext.IsInput, 
-                    RelativeSource={RelativeSource TemplatedParent}}">
+                                           IsVisible="{Binding DataContext.IsInput, RelativeSource={RelativeSource TemplatedParent}}"
+                                           IsFunc="{Binding DataContext.IsFunc, RelativeSource={RelativeSource TemplatedParent}}">
                         <properties:NodeSocket.IsInput>
                             <x:Boolean>True</x:Boolean>
                         </properties:NodeSocket.IsInput>
                     </properties:NodeSocket>
-                    <ContentPresenter Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Top" Content="{TemplateBinding Content}" />
-                    <properties:NodeSocket Grid.Column="2" Name="PART_OutputSocket"
-                                           Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
-                                           HorizontalAlignment="Right"
-                                           IsFunc="{Binding DataContext.IsFunc, RelativeSource={RelativeSource TemplatedParent}}"
+                    <ContentPresenter Grid.Column="1" VerticalAlignment="Top" Content="{TemplateBinding Content}" />
+                    <properties:NodeSocket Name="PART_OutputSocket"
+                                           ClipToBounds="False" HorizontalAlignment="Right"  Grid.Column="2"
+                                           Node="{Binding DataContext.Node, RelativeSource={RelativeSource TemplatedParent}}"
+                                           SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}"
                                            IsVisible="{Binding !DataContext.IsInput,RelativeSource={RelativeSource TemplatedParent}}"
-                                           SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}">
+                                           IsFunc="{Binding DataContext.IsFunc, RelativeSource={RelativeSource TemplatedParent}}">
                         <properties:NodeSocket.IsInput>
                             <x:Boolean>False</x:Boolean>
                         </properties:NodeSocket.IsInput>

+ 2 - 2
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml

@@ -10,9 +10,9 @@
                                  Fill="{TemplateBinding SocketBrush}" 
                                  IsVisible="{Binding !IsFunc, RelativeSource={RelativeSource TemplatedParent}}"/>
                         <Rectangle Width="10" Height="10"
-                                   RadiusX="1" RadiusY="1"
+                                   RadiusX="2" RadiusY="2"
                                    Fill="{TemplateBinding SocketBrush}"
-                                   RenderTransform="rotate(45deg)"
+                                   RenderTransform="rotate(45deg) scale(0.89)"
                                    IsVisible="{Binding IsFunc, RelativeSource={RelativeSource TemplatedParent}}"/>
                     </Grid>
                 </StackPanel>

+ 143 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Dock/ChannelsDockViewModel.cs

@@ -0,0 +1,143 @@
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
+using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Dock;
+
+internal class ChannelsDockViewModel : DockableViewModel
+{
+    public const string TabId = "ChannelsDock";
+
+    public override string Id => TabId;
+    public override string Title => new LocalizedString("CHANNELS_DOCK_TITLE");
+    public override bool CanFloat => true;
+    public override bool CanClose => true;
+
+    public WindowViewModel WindowViewModel { get; }
+
+    private ViewportWindowViewModel? _activeViewport;
+
+    public ViewportWindowViewModel? ActiveViewport
+    {
+        get => _activeViewport;
+        set => SetProperty(ref _activeViewport, value);
+    }
+
+    private ViewportColorChannels Channels
+    {
+        get => ActiveViewport?.Channels ?? ViewportColorChannels.Default;
+        set
+        {
+            if (ActiveViewport != null)
+            {
+                ActiveViewport.Channels = value;
+            }
+        }
+    }
+
+    public ChannelsDockViewModel(WindowViewModel windowViewModel)
+    {
+        WindowViewModel = windowViewModel;
+        windowViewModel.ActiveViewportChanged += WindowViewModelOnActiveViewportChanged;
+    }
+
+    private void WindowViewModelOnActiveViewportChanged(object? sender, ViewportWindowViewModel e)
+    {
+        if (ActiveViewport != null)
+        {
+            ActiveViewport.PropertyChanged -= ActiveViewportOnPropertyChanged;
+        }
+
+        ActiveViewport = e;
+        ActiveViewport.PropertyChanged += ActiveViewportOnPropertyChanged;
+    }
+
+    private void ActiveViewportOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName != nameof(ViewportWindowViewModel.Channels))
+        {
+            return;
+        }
+
+        OnPropertyChanged(nameof(IsRedVisible));
+        OnPropertyChanged(nameof(IsGreenVisible));
+        OnPropertyChanged(nameof(IsBlueVisible));
+        OnPropertyChanged(nameof(IsAlphaVisible));
+        
+        OnPropertyChanged(nameof(IsRedSolo));
+        OnPropertyChanged(nameof(IsGreenSolo));
+        OnPropertyChanged(nameof(IsBlueSolo));
+        OnPropertyChanged(nameof(IsAlphaSolo));
+    }
+
+    public bool IsRedVisible
+    {
+        get => Channels.IsVisiblyVisible(ColorChannel.Red);
+        set => SetVisible(ColorChannel.Red, value);
+    }
+
+    public bool IsRedSolo
+    {
+        get => Channels.IsSolo(ColorChannel.Red);
+        set => SetSolo(ColorChannel.Red, value);
+    }
+
+    public bool IsGreenVisible
+    {
+        get => Channels.IsVisiblyVisible(ColorChannel.Green);
+        set => SetVisible(ColorChannel.Green, value);
+    }
+
+    public bool IsGreenSolo
+    {
+        get => Channels.IsSolo(ColorChannel.Green);
+        set => SetSolo(ColorChannel.Green, value);
+    }
+
+    public bool IsBlueVisible
+    {
+        get => Channels.IsVisiblyVisible(ColorChannel.Blue);
+        set => SetVisible(ColorChannel.Blue, value);
+    }
+
+    public bool IsBlueSolo
+    {
+        get => Channels.IsSolo(ColorChannel.Blue);
+        set => SetSolo(ColorChannel.Blue, value);
+    }
+
+    public bool IsAlphaVisible
+    {
+        get => Channels.IsVisiblyVisible(ColorChannel.Alpha);
+        set => SetVisible(ColorChannel.Alpha, value);
+    }
+
+    public bool IsAlphaSolo
+    {
+        get => Channels.IsSolo(ColorChannel.Alpha);
+        set => SetSolo(ColorChannel.Alpha, value);
+    }
+
+    private void SetVisible(ColorChannel channel, bool value)
+    {
+        var mode = Channels.GetModeForChannel(channel);
+
+        if (mode.IsSolo && !value)
+        {
+            Channels = Channels.WithModeForChannel(channel, _ => new ColorChannelMode(), false);
+        }
+        else
+        {
+            Channels = Channels.WithModeForChannel(channel, x => x.WithVisible(value), value);
+        }
+    }
+
+    private void SetSolo(ColorChannel channel, bool value)
+    {
+        var mode = Channels.GetModeForChannel(channel);
+
+        Channels = Channels.WithModeForChannel(channel, x => x.WithSolo(value), value);
+    }
+}

+ 4 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Dock/LayoutManager.cs

@@ -40,6 +40,7 @@ internal class LayoutManager
         TimelineDockViewModel timelineDockViewModel = new(mainViewModel.DocumentManagerSubViewModel);
         
         NodeGraphDockViewModel nodeGraphDockViewModel = new(mainViewModel.DocumentManagerSubViewModel);
+        ChannelsDockViewModel channelsDockDockViewModel = new(mainViewModel.WindowSubViewModel);
 
         RegisterDockable(layersDockViewModel);
         RegisterDockable(colorPickerDockViewModel);
@@ -49,6 +50,7 @@ internal class LayoutManager
         RegisterDockable(paletteViewerDockViewModel);
         RegisterDockable(timelineDockViewModel);
         RegisterDockable(nodeGraphDockViewModel);
+        RegisterDockable(channelsDockDockViewModel);
         
         DefaultLayout = new LayoutTree
         {
@@ -91,7 +93,8 @@ internal class LayoutManager
                         SplitDirection = DockingDirection.Bottom,
                         Second = new DockableArea
                         {
-                            Id = "LayersArea", ActiveDockable = DockContext.CreateDockable(layersDockViewModel)
+                            Id = "LayersArea",
+                            Dockables = [ DockContext.CreateDockable(layersDockViewModel), DockContext.CreateDockable(channelsDockDockViewModel) ]
                         },
                     },
                     FirstSize = 0.66,

+ 170 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/ColorMatrixPropertyViewModel.cs

@@ -0,0 +1,170 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class ColorMatrixPropertyViewModel : NodePropertyViewModel<ColorMatrix>
+{
+    public ColorMatrixPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+    
+    public float M11
+    {
+        get => Value.M11;
+        set => Value = new ColorMatrix(
+            (value, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M12
+    {
+        get => Value.M12;
+        set => Value = new ColorMatrix(
+            (M11, value, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M13
+    {
+        get => Value.M13;
+        set => Value = new ColorMatrix(
+            (M11, M12, value, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M14
+    {
+        get => Value.M14;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, value, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M15
+    {
+        get => Value.M15;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, value), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M21
+    {
+        get => Value.M21;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (value, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M22
+    {
+        get => Value.M22;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, value, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M23
+    {
+        get => Value.M23;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, value, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M24
+    {
+        get => Value.M24;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, value, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M25
+    {
+        get => Value.M25;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, value),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M31
+    {
+        get => Value.M31;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (value, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M32
+    {
+        get => Value.M32;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, value, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M33
+    {
+        get => Value.M33;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, value, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M34
+    {
+        get => Value.M34;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, value, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M35
+    {
+        get => Value.M35;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, value), (M41, M42, M43, M44, M45));
+    }
+
+    public float M41
+    {
+        get => Value.M41;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (value, M42, M43, M44, M45));
+    }
+
+    public float M42
+    {
+        get => Value.M42;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, value, M43, M44, M45));
+    }
+
+    public float M43
+    {
+        get => Value.M43;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, value, M44, M45));
+    }
+
+    public float M44
+    {
+        get => Value.M44;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, value, M45));
+    }
+
+    public float M45
+    {
+        get => Value.M45;
+        set => Value = new ColorMatrix(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, value));
+    }
+}

+ 8 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/ColorPropertyViewModel.cs

@@ -1,4 +1,5 @@
-using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.AvaloniaUI.Helpers.Extensions;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
 
@@ -7,4 +8,10 @@ internal class ColorPropertyViewModel : NodePropertyViewModel<Color>
     public ColorPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
     {
     }
+
+    public new Avalonia.Media.Color Value
+    {
+        get => base.Value.ToColor();
+        set => base.Value = value.ToColor();
+    }
 }

+ 8 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/Int32PropertyViewModel.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class Int32PropertyViewModel : NodePropertyViewModel<int>
+{
+    public Int32PropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+}

+ 170 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/Matrix4x5FPropertyViewModel.cs

@@ -0,0 +1,170 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class Matrix4x5FPropertyViewModel : NodePropertyViewModel<Matrix4x5F>
+{
+    public Matrix4x5FPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+
+    public float M11
+    {
+        get => Value.M11;
+        set => Value = new Matrix4x5F(
+            (value, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M12
+    {
+        get => Value.M12;
+        set => Value = new Matrix4x5F(
+            (M11, value, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M13
+    {
+        get => Value.M13;
+        set => Value = new Matrix4x5F(
+            (M11, M12, value, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M14
+    {
+        get => Value.M14;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, value, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M15
+    {
+        get => Value.M15;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, value), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M21
+    {
+        get => Value.M21;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (value, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M22
+    {
+        get => Value.M22;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, value, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M23
+    {
+        get => Value.M23;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, value, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M24
+    {
+        get => Value.M24;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, value, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M25
+    {
+        get => Value.M25;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, value),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M31
+    {
+        get => Value.M31;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (value, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M32
+    {
+        get => Value.M32;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, value, M33, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M33
+    {
+        get => Value.M33;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, value, M34, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M34
+    {
+        get => Value.M34;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, value, M35), (M41, M42, M43, M44, M45));
+    }
+
+    public float M35
+    {
+        get => Value.M35;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, value), (M41, M42, M43, M44, M45));
+    }
+
+    public float M41
+    {
+        get => Value.M41;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (value, M42, M43, M44, M45));
+    }
+
+    public float M42
+    {
+        get => Value.M42;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, value, M43, M44, M45));
+    }
+
+    public float M43
+    {
+        get => Value.M43;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, value, M44, M45));
+    }
+
+    public float M44
+    {
+        get => Value.M44;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, value, M45));
+    }
+
+    public float M45
+    {
+        get => Value.M45;
+        set => Value = new Matrix4x5F(
+            (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, value));
+    }
+}

+ 10 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/VecDPropertyViewModel.cs

@@ -0,0 +1,10 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class VecDPropertyViewModel : NodePropertyViewModel<VecD>
+{
+    public VecDPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+}

+ 9 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ViewportWindowViewModel.cs

@@ -20,6 +20,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
     public ExecutionTrigger<VecI> CenterViewportTrigger { get; } = new ExecutionTrigger<VecI>();
     public ExecutionTrigger<double> ZoomViewportTrigger { get; } = new ExecutionTrigger<double>();
 
+    
     public string Index => _index;
 
     public string Id => id;
@@ -58,6 +59,14 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         }
     }
 
+    private ViewportColorChannels _channels = ViewportColorChannels.Default;
+    
+    public ViewportColorChannels Channels
+    {
+        get => _channels;
+        set => SetProperty(ref _channels, value);
+    }
+
     public void IndexChanged()
     {
         _index = Owner.CalculateViewportIndex(this) ?? "";

+ 48 - 0
src/PixiEditor.AvaloniaUI/Views/Dock/ChannelsDockView.axaml

@@ -0,0 +1,48 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:dock="clr-namespace:PixiEditor.AvaloniaUI.ViewModels.Dock"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PixiEditor.AvaloniaUI.Views.Dock.ChannelsDockView"
+             x:DataType="dock:ChannelsDockViewModel">
+    <Design.DataContext>
+        <dock:ChannelsDockViewModel/>
+    </Design.DataContext>
+    
+    <StackPanel>
+        <!-- TODO: Improve this UI -->
+        <Grid ColumnDefinitions="*,Auto">
+            <TextBlock ui:Translator.Key="RED" />
+            <StackPanel Grid.Column="1" Orientation="Horizontal">
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-eye}" IsChecked="{Binding IsRedVisible}" />
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-trash}" IsChecked="{Binding IsRedSolo}" />
+            </StackPanel>
+        </Grid>
+        
+        <Grid ColumnDefinitions="*,Auto">
+            <TextBlock ui:Translator.Key="GREEN" />
+            <StackPanel Grid.Column="1" Orientation="Horizontal">
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-eye}" IsChecked="{Binding IsGreenVisible}" />
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-trash}" IsChecked="{Binding IsGreenSolo}" />
+            </StackPanel>
+        </Grid>
+
+        <Grid ColumnDefinitions="*,Auto">
+            <TextBlock ui:Translator.Key="BLUE" />
+            <StackPanel Grid.Column="1" Orientation="Horizontal">
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-eye}" IsChecked="{Binding IsBlueVisible}" />
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-trash}" IsChecked="{Binding IsBlueSolo}" />
+            </StackPanel>
+        </Grid>
+
+        <Grid ColumnDefinitions="*,Auto">
+            <TextBlock ui:Translator.Key="ALPHA" />
+            <StackPanel Grid.Column="1" Orientation="Horizontal">
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-eye}" IsChecked="{Binding IsAlphaVisible}" />
+                <ToggleButton Classes="pixi-icon" Content="{DynamicResource icon-trash}" IsChecked="{Binding IsAlphaSolo}" />
+            </StackPanel>
+        </Grid>
+    </StackPanel>
+</UserControl>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Dock/ChannelsDockView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Dock;
+
+public partial class ChannelsDockView : UserControl
+{
+    public ChannelsDockView()
+    {
+        InitializeComponent();
+    }
+}
+

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

@@ -33,6 +33,7 @@
         UseTouchGestures="{Binding StylusSubViewModel.UseTouchGestures, Source={viewModels1:MainVM}}"
         FlipX="{Binding FlipX, Mode=TwoWay}"
         FlipY="{Binding FlipY, Mode=TwoWay}"
+        Channels="{Binding Channels, Mode=TwoWay}"
         ContextRequested="Viewport_OnContextMenuOpening"
         Document="{Binding Document}">
         <viewportControls:Viewport.ContextFlyout>

+ 1 - 0
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml

@@ -112,6 +112,7 @@
             ZoomOutOnClick="{Binding ZoomOutOnClick, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
             FlipX="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
             FlipY="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
+            Channels="{Binding Channels, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
             AllOverlays="{Binding ElementName=vpUc, Path=ActiveOverlays}"
             FadeOut="{Binding Source={viewModels:ToolVM ColorPickerToolViewModel}, Path=PickOnlyFromReferenceLayer, Mode=OneWay}"
             DefaultCursor="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ToolCursor, Mode=OneWay}"

+ 9 - 0
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -84,6 +84,9 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<ICommand> MiddleMouseClickedCommandProperty =
         AvaloniaProperty.Register<Viewport, ICommand>(nameof(MiddleMouseClickedCommand), null);
 
+    public static readonly StyledProperty<ViewportColorChannels> ChannelsProperty = AvaloniaProperty.Register<Viewport, ViewportColorChannels>(
+        nameof(Channels));
+
     public ICommand? MiddleMouseClickedCommand
     {
         get => (ICommand?)GetValue(MiddleMouseClickedCommandProperty);
@@ -193,6 +196,12 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         set => SetValue(FlipYProperty, value);
     }
 
+    public ViewportColorChannels Channels
+    {
+        get => GetValue(ChannelsProperty);
+        set => SetValue(ChannelsProperty, value);
+    }
+
     private double angleRadians = 0;
 
     public double AngleRadians

+ 49 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorMatrixPropertyView.axaml

@@ -0,0 +1,49 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.ColorMatrixPropertyView">
+    <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock ui:Translator.Key="{Binding DisplayName}" />
+        <Grid IsVisible="{Binding IsInput}" ColumnDefinitions="Auto,*,*,*,*,*" RowDefinitions="Auto, Auto, Auto, Auto, Auto">
+            <TextBlock Grid.Row="0" Grid.Column="0" Text="T\F" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"  />
+            
+            <TextBlock Grid.Row="1" Grid.Column="0" Text="R" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            <TextBlock Grid.Row="2" Grid.Column="0" Text="G" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            <TextBlock Grid.Row="3" Grid.Column="0" Text="B" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            <TextBlock Grid.Row="4" Grid.Column="0" Text="A" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            
+            <TextBlock Grid.Row="0" Grid.Column="1" Text="R" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            <TextBlock Grid.Row="0" Grid.Column="2" Text="G" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            <TextBlock Grid.Row="0" Grid.Column="3" Text="B" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            <TextBlock Grid.Row="0" Grid.Column="4" Text="A" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            <TextBlock Grid.Row="0" Grid.Column="5" Text="+" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
+            
+            <input:NumberInput Grid.Row="1" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M11, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M12, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M13, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M14, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M15, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M21, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M22, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M23, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M24, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M25, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M31, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M32, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M33, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M34, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M35, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M41, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M42, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M43, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M44, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M45, Mode=TwoWay}" />
+        </Grid>
+    </StackPanel>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorMatrixPropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class ColorMatrixPropertyView : NodePropertyView
+{
+    public ColorMatrixPropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 5 - 1
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorPropertyView.axaml

@@ -11,6 +11,10 @@
                              x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.ColorPropertyView">
     <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
-        <colorPicker:PortableColorPicker PointerPressed="InputElement_OnPointerPressed" Width="40" Height="20" IsVisible="{Binding IsInput}" SelectedColor="{Binding Value, Mode=TwoWay}" />
+        <colorPicker:PortableColorPicker
+            PointerPressed="InputElement_OnPointerPressed"
+            Width="40" Height="20"
+            IsVisible="{Binding IsInput}"
+            SelectedColor="{Binding Value, Mode=TwoWay}" />
     </Grid>
 </properties:NodePropertyView>

+ 15 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Int32PropertyView.axaml

@@ -0,0 +1,15 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.Int32PropertyView">
+    <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <input:NumberInput HorizontalAlignment="Right" MinWidth="100" IsVisible="{Binding IsInput}" Value="{Binding Value, Mode=TwoWay}" />
+    </Grid>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Int32PropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class Int32PropertyView : NodePropertyView
+{
+    public Int32PropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 47 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Matrix4x5FPropertyView.axaml

@@ -0,0 +1,47 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.Matrix4x5FPropertyView">
+    <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock ui:Translator.Key="{Binding DisplayName}" />
+        <Grid IsVisible="{Binding IsInput}" ColumnDefinitions="Auto,*,*,*,*,*" RowDefinitions="Auto, Auto, Auto, Auto, Auto">
+            <TextBlock Grid.Row="1" Grid.Column="0" Text="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            <TextBlock Grid.Row="2" Grid.Column="0" Text="2" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            <TextBlock Grid.Row="3" Grid.Column="0" Text="3" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            <TextBlock Grid.Row="4" Grid.Column="0" Text="4" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            
+            <TextBlock Grid.Row="0" Grid.Column="1" Text="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            <TextBlock Grid.Row="0" Grid.Column="2" Text="2" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            <TextBlock Grid.Row="0" Grid.Column="3" Text="3" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            <TextBlock Grid.Row="0" Grid.Column="4" Text="4" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            <TextBlock Grid.Row="0" Grid.Column="5" Text="5" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
+            
+            <input:NumberInput Grid.Row="1" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M11, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M12, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M13, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M14, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="1" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M15, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M21, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M22, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M23, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M24, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="2" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M25, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M31, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M32, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M33, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M34, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="3" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M35, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M41, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M42, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M43, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M44, Mode=TwoWay}" />
+            <input:NumberInput Grid.Row="4" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M45, Mode=TwoWay}" />
+        </Grid>
+    </StackPanel>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/Matrix4x5FPropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class Matrix4x5FPropertyView : NodePropertyView
+{
+    public Matrix4x5FPropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 3 - 12
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs

@@ -10,12 +10,10 @@ namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
 
 public class NodeSocket : TemplatedControl
 {
-    public static readonly StyledProperty<bool> IsInputProperty = AvaloniaProperty.Register<NodeSocket, bool>("IsInput");
+    public static readonly StyledProperty<bool> IsInputProperty = AvaloniaProperty.Register<NodeSocket, bool>(nameof(IsInput));
     public static readonly StyledProperty<bool> IsFuncProperty = AvaloniaProperty.Register<NodeSocket, bool>(nameof(IsFunc));
-    public static readonly StyledProperty<string> LabelProperty = AvaloniaProperty.Register<NodeSocket, string>("Label");
 
-    public static readonly StyledProperty<IBrush> SocketBrushProperty = AvaloniaProperty.Register<NodeSocket, IBrush>(
-        "SocketBrush");
+    public static readonly StyledProperty<IBrush> SocketBrushProperty = AvaloniaProperty.Register<NodeSocket, IBrush>(nameof(SocketBrush));
 
     public IBrush SocketBrush
     {
@@ -23,8 +21,7 @@ public class NodeSocket : TemplatedControl
         set => SetValue(SocketBrushProperty, value);
     }
 
-    public static readonly StyledProperty<INodeHandler> NodeProperty = AvaloniaProperty.Register<NodeSocket, INodeHandler>(
-        "Node");
+    public static readonly StyledProperty<INodeHandler> NodeProperty = AvaloniaProperty.Register<NodeSocket, INodeHandler>(nameof(Node));
 
     public INodeHandler Node
     {
@@ -43,12 +40,6 @@ public class NodeSocket : TemplatedControl
         get => GetValue(IsFuncProperty);
         set => SetValue(IsFuncProperty, value);
     }
-
-    public string Label
-    {
-        get { return (string)GetValue(LabelProperty); }
-        set { SetValue(LabelProperty, value); }
-    }
     
     public Control ConnectPort { get; set; }
 

+ 18 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecDPropertyView.axaml

@@ -0,0 +1,18 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.VecDPropertyView">
+    <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <StackPanel IsVisible="{Binding IsInput}">
+            <input:NumberInput MinWidth="100" Value="{Binding XValue, Mode=TwoWay}" Margin="0,2" />
+            <input:NumberInput MinWidth="100" Value="{Binding YValue, Mode=TwoWay}" />
+        </StackPanel>
+    </StackPanel>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecDPropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class VecDPropertyView : NodePropertyView
+{
+    public VecDPropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecIPropertyView.axaml

@@ -11,8 +11,8 @@
     <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
         <StackPanel IsVisible="{Binding IsInput}">
-            <input:NumberInput MinWidth="100" Value="{Binding XValue, Mode=TwoWay}" Margin="0,2" />
-            <input:NumberInput MinWidth="100" Value="{Binding YValue, Mode=TwoWay}" />
+            <input:NumberInput MinWidth="100" Decimals="0" Value="{Binding XValue, Mode=TwoWay}" Margin="0,2" />
+            <input:NumberInput MinWidth="100" Decimals="0" Value="{Binding YValue, Mode=TwoWay}" />
         </StackPanel>
     </StackPanel>
 </properties:NodePropertyView>

+ 26 - 2
src/PixiEditor.AvaloniaUI/Views/Rendering/Scene.cs

@@ -14,6 +14,7 @@ using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers.Converters;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.Views.Overlays;
 using PixiEditor.AvaloniaUI.Views.Overlays.Pointers;
@@ -50,6 +51,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     public static readonly StyledProperty<Cursor> DefaultCursorProperty = AvaloniaProperty.Register<Scene, Cursor>(
         nameof(DefaultCursor));
 
+    public static readonly StyledProperty<ViewportColorChannels> ChannelsProperty = AvaloniaProperty.Register<Scene, ViewportColorChannels>(
+        nameof(Channels));
+
     public Cursor DefaultCursor
     {
         get => GetValue(DefaultCursorProperty);
@@ -86,6 +90,12 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(SurfaceProperty, value);
     }
 
+    public ViewportColorChannels Channels
+    {
+        get => GetValue(ChannelsProperty);
+        set => SetValue(ChannelsProperty, value);
+    }
+
     private Bitmap? checkerBitmap;
 
     private Overlay? capturedOverlay;
@@ -104,6 +114,12 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         CheckerImagePathProperty.Changed.AddClassHandler<Scene>(CheckerImagePathChanged);
         AllOverlaysProperty.Changed.AddClassHandler<Scene>(ActiveOverlaysChanged);
         DefaultCursorProperty.Changed.AddClassHandler<Scene>(DefaultCursorChanged);
+        ChannelsProperty.Changed.AddClassHandler<Scene>(ChannelsChanged);
+    }
+
+    private static void ChannelsChanged(Scene scene, AvaloniaPropertyChangedEventArgs args)
+    {
+        scene.InvalidateVisual();
     }
 
     public Scene()
@@ -130,7 +146,8 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         using var operation = new DrawSceneOperation(Surface, Document, CanvasPos, Scale * resolutionScale, angle, FlipX, FlipY,
             dirtyRect,
             Bounds,
-            sceneOpacity);
+            sceneOpacity,
+            Channels.GetColorMatrix());
 
         var matrix = CalculateTransformMatrix();
         context.PushTransform(matrix);
@@ -454,13 +471,14 @@ internal class DrawSceneOperation : SkiaDrawOperation
     public bool FlipX { get; set; }
     public bool FlipY { get; set; }
     public Rect ViewportBounds { get; }
+    public ColorMatrix ColorMatrix { get; }
 
     public RectI SurfaceRectToRender { get; }
 
     private SKPaint _paint = new SKPaint();
 
     public DrawSceneOperation(Surface surface, DocumentViewModel document, VecD contentPosition, double scale,
-        double angle, bool flipX, bool flipY, Rect dirtyBounds, Rect viewportBounds, double opacity) : base(dirtyBounds)
+        double angle, bool flipX, bool flipY, Rect dirtyBounds, Rect viewportBounds, double opacity, ColorMatrix colorMatrix) : base(dirtyBounds)
     {
         Surface = surface;
         Document = document;
@@ -469,6 +487,7 @@ internal class DrawSceneOperation : SkiaDrawOperation
         Angle = angle;
         FlipX = flipX;
         FlipY = flipY;
+        ColorMatrix = colorMatrix;
         ViewportBounds = viewportBounds;
         _paint.Color = _paint.Color.WithAlpha((byte)(opacity * 255));
         SurfaceRectToRender = FindRectToRender((float)scale);
@@ -489,6 +508,11 @@ internal class DrawSceneOperation : SkiaDrawOperation
         }
         
         using Image snapshot = Surface.DrawingSurface.Snapshot(SurfaceRectToRender);
+
+        var matrixValues = new float[ColorMatrix.Width * ColorMatrix.Height];
+        ColorMatrix.TryGetMembers(matrixValues);
+        
+        _paint.ColorFilter = SKColorFilter.CreateColorMatrix(matrixValues);
         canvas.DrawImage((SKImage)snapshot.Native, SurfaceRectToRender.X, SurfaceRectToRender.Y, _paint);
 
         canvas.Restore();

+ 9 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs

@@ -1,5 +1,6 @@
 using System.Collections;
 using System.Collections.Immutable;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.Numerics;
 
@@ -16,7 +17,7 @@ public record CreateNode_ChangeInfo(
     public static ImmutableArray<NodePropertyInfo> CreatePropertyInfos(IEnumerable<INodeProperty> properties,
         bool isInput, Guid guid)
     {
-        return properties.Select(p => new NodePropertyInfo(p.InternalPropertyName, p.DisplayName, p.ValueType, isInput, guid))
+        return properties.Select(p => new NodePropertyInfo(p.InternalPropertyName, p.DisplayName, p.ValueType, isInput, GetNonOverridenValue(p), guid))
             .ToImmutableArray();
     }
 
@@ -29,4 +30,11 @@ public record CreateNode_ChangeInfo(
             node.Id,
             CreatePropertyInfos(node.InputProperties, true, node.Id), CreatePropertyInfos(node.OutputProperties, false, node.Id));
     }
+
+    private static object? GetNonOverridenValue(INodeProperty property) => property switch
+    {
+        IFieldInputProperty fieldProperty => fieldProperty.GetFieldConstantValue(),
+        IInputProperty inputProperty => inputProperty.NonOverridenValue,
+        _ => null
+    };
 }

+ 4 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodePropertyInfo.cs

@@ -1,8 +1,11 @@
-namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using System.Diagnostics.CodeAnalysis;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 
 public record NodePropertyInfo(
     string PropertyName,
     string DisplayName,
     Type ValueType,
     bool IsInput,
+    object? InputValue,
     Guid NodeId);

+ 19 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FieldContext.cs

@@ -0,0 +1,19 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+
+public record struct FieldContext(VecD Position, VecI Size, bool HasContext)
+{
+    public FieldContext(VecD position, VecI size) : this(position, size, true) { }
+
+    public static FieldContext NoContext => new(VecD.Zero, VecI.Zero, false);
+
+    public void ThrowOnMissingContext()
+    {
+        if (!HasContext)
+        {
+            throw new NoNodeFieldContextException();
+        }
+    }
+}

+ 7 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/NoNodeFieldContextException.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public class NoNodeFieldContextException : Exception
+{
+    public NoNodeFieldContextException() : base("The node field requires context")
+    { }
+}

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldInputProperty.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldOutputProperty.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;

+ 24 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
@@ -12,7 +13,28 @@ public class InputProperty : IInputProperty
 
     public object Value
     {
-        get => Connection != null ? Connection.Value : _internalValue;
+        get
+        {
+            if (Connection == null)
+            {
+                return _internalValue;
+            }
+
+            var connectionValue = Connection.Value;
+            
+            if (!ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is Delegate connectionField)
+            {
+                return connectionField.DynamicInvoke(FieldContext.NoContext);
+            }
+
+            if (ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is not Delegate)
+            {
+                Func<FieldContext, object> field = _ => connectionValue;
+                return field;
+            }
+
+            return connectionValue;
+        }
     }
     
     public object NonOverridenValue

+ 0 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/FieldContext.cs

@@ -1,5 +0,0 @@
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-
-public record struct FieldContext(VecD Position, VecI Size);

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

@@ -1,5 +1,6 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.Numerics;
 
@@ -7,19 +8,15 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 public class CircleNode : Node
 {
-    public InputProperty<int> Radius { get; }
-    public InputProperty<int> X { get; }
-    public InputProperty<int> Y { get; }
+    public InputProperty<VecI> Radius { get; }
     public InputProperty<Color> StrokeColor { get; }
     public InputProperty<Color> FillColor { get; }
     public InputProperty<int> StrokeWidth { get; }
     public OutputProperty<Surface> Output { get; }
     
-    public CircleNode() 
+    public CircleNode()
     {
-        Radius = CreateInput<int>("Radius", "RADIUS", 10);
-        X = CreateInput<int>("X", "X", 0);
-        Y = CreateInput<int>("Y", "Y", 0);
+        Radius = CreateInput<VecI>("Radius", "RADIUS", new VecI(32, 32));
         StrokeColor = CreateInput<Color>("StrokeColor", "STROKE_COLOR", new Color(0, 0, 0, 255));
         FillColor = CreateInput<Color>("FillColor", "FILL_COLOR", new Color(0, 0, 0, 255));
         StrokeWidth = CreateInput<int>("StrokeWidth", "STROKE_WIDTH", 1);
@@ -28,13 +25,23 @@ public class CircleNode : Node
     
     protected override Surface? OnExecute(RenderingContext context)
     {
-        Surface workingSurface = new Surface(new VecI(Radius.Value * 2, Radius.Value * 2));
+        var radius = Radius.Value / 2;
+        var strokeWidth = StrokeWidth.Value;
+        var strokeOffset = new VecI(strokeWidth / 2);
+        
+        Surface workingSurface = new Surface(Radius.Value + strokeOffset * 2);
         
         using Paint paint = new Paint();
-        paint.StrokeWidth = StrokeWidth.Value;
-        paint.Color = StrokeColor.Value;
+        paint.Color = FillColor.Value;
+        paint.Style = PaintStyle.Fill;
+        
+        workingSurface.DrawingSurface.Canvas.DrawOval(radius + strokeOffset, radius, paint);
         
-        workingSurface.DrawingSurface.Canvas.DrawCircle(Radius.Value, Radius.Value, Radius.Value, paint);
+        paint.Color = StrokeColor.Value;
+        paint.StrokeWidth = strokeWidth;
+        paint.Style = PaintStyle.Stroke;
+
+        workingSurface.DrawingSurface.Canvas.DrawOval(radius + strokeOffset, radius, paint);
 
         Output.Value = workingSurface;
         
@@ -43,7 +50,7 @@ public class CircleNode : Node
 
     public override bool Validate()
     {
-        return Radius.Value > 0 && StrokeWidth.Value > 0;
+        return Radius.Value is { X: > 0, Y: > 0 } && StrokeWidth.Value > 0;
     }
 
     public override Node CreateCopy() => new CircleNode();

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

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;

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

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Rendering;

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

@@ -0,0 +1,53 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class MatrixTransformNode : Node
+{
+    private ColorMatrix previousMatrix = new(
+        (1, 0, 0, 0, 0),
+        (0, 1, 0, 0, 0),
+        (0, 0, 1, 0, 0),
+        (0, 0, 0, 1, 0));
+    
+    private Paint paint;
+    
+    public OutputProperty<Surface> Transformed { get; }
+    
+    public InputProperty<Surface?> Input { get; }
+    
+    public InputProperty<ColorMatrix> Matrix { get; }
+
+    public MatrixTransformNode()
+    {
+        Transformed = CreateOutput<Surface>(nameof(Transformed), "TRANSFORMED", null);
+        Input = CreateInput<Surface>(nameof(Input), "INPUT", null);
+        Matrix = CreateInput(nameof(Matrix), "MATRIX", previousMatrix);
+
+        paint = new Paint { ColorFilter = ColorFilter.CreateColorMatrix(previousMatrix) };
+    }
+    
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        var currentMatrix = Matrix.Value;
+        if (previousMatrix != currentMatrix)
+        {
+            paint.ColorFilter = ColorFilter.CreateColorMatrix(Matrix.Value);
+            previousMatrix = currentMatrix;
+        }
+
+        var workingSurface = new Surface(Input.Value.Size);
+        
+        workingSurface.DrawingSurface.Canvas.DrawSurface(Input.Value.DrawingSurface, 0, 0, paint);
+
+        Transformed.Value = workingSurface;
+        
+        return Transformed.Value;
+    }
+
+    public override bool Validate() => Input.Value != null;
+
+    public override Node CreateCopy() => new MatrixTransformNode();
+}

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

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
@@ -27,6 +28,8 @@ public class ModifyImageLeftNode : Node
 
     private Color GetColor(FieldContext context)
     {
+        context.ThrowOnMissingContext();
+        
         if (pixmap == null)
             return new Color();
         

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

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;

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

@@ -1,5 +1,6 @@
 using System.Diagnostics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;

+ 92 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SeparateChannelsNode.cs

@@ -0,0 +1,92 @@
+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;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class SeparateChannelsNode : Node
+{
+    private readonly Paint _paint = new();
+    
+    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 _alphaFilter = ColorFilter.CreateColorMatrix(ColorMatrix.UseAlpha);
+    
+    private readonly ColorFilter _redGrayscaleFilter = ColorFilter.CreateColorMatrix(ColorMatrix.UseRed + ColorMatrix.MapRedToGreenBlue + ColorMatrix.OpaqueAlphaOffset);
+    private readonly ColorFilter _greenGrayscaleFilter = ColorFilter.CreateColorMatrix(ColorMatrix.UseGreen + ColorMatrix.MapGreenToRedBlue + ColorMatrix.OpaqueAlphaOffset);
+    private readonly ColorFilter _blueGrayscaleFilter = ColorFilter.CreateColorMatrix(ColorMatrix.UseBlue + ColorMatrix.MapBlueToRedGreen + ColorMatrix.OpaqueAlphaOffset);
+    private readonly ColorFilter _alphaGrayscaleFilter = ColorFilter.CreateColorMatrix(ColorMatrix.MapAlphaToRedGreenBlue + ColorMatrix.OpaqueAlphaOffset);
+
+    public OutputProperty<Surface?> Red { get; }
+    
+    public OutputProperty<Surface?> Green { get; }
+    
+    public OutputProperty<Surface?> Blue { get; }
+
+    public OutputProperty<Surface?> Alpha { get; }
+    
+    public InputProperty<Surface?> Image { get; }
+    
+    public InputProperty<bool> Grayscale { get; }
+
+    public SeparateChannelsNode()
+    {
+        Red = CreateOutput<Surface>(nameof(Red), "RED", null);
+        Green = CreateOutput<Surface>(nameof(Green), "GREEN", null);
+        Blue = CreateOutput<Surface>(nameof(Blue), "BLUE", null);
+        Alpha = CreateOutput<Surface>(nameof(Alpha), "ALPHA", null);
+        
+        Image = CreateInput<Surface>(nameof(Image), "IMAGE", null);
+        Grayscale = CreateInput(nameof(Grayscale), "GRAYSCALE", false);
+    }
+    
+    protected override Surface OnExecute(RenderingContext context)
+    {
+        var image = Image.Value;
+        var grayscale = Grayscale.Value;
+
+        var red = !grayscale ? _redFilter : _redGrayscaleFilter;
+        var green = !grayscale ? _greenFilter : _greenGrayscaleFilter;
+        var blue = !grayscale ? _blueFilter : _blueGrayscaleFilter;
+        var alpha = !grayscale ? _alphaFilter : _alphaGrayscaleFilter;
+
+        Red.Value = GetImage(image, red);
+        Green.Value = GetImage(image, green);
+        Blue.Value = GetImage(image, blue);
+        Alpha.Value = GetImage(image, alpha);
+
+        var previewSurface = new Surface(image.Size * 2);
+
+        var size = image.Size;
+        
+        var redPos = new VecI();
+        var greenPos = new VecI(size.X, 0);
+        var bluePos = new VecI(0, size.Y);
+        var alphaPos = new VecI(size.X, size.Y);
+        
+        previewSurface.DrawingSurface.Canvas.DrawSurface(Red.Value.DrawingSurface, redPos, context.ReplacingPaintWithOpacity);
+        previewSurface.DrawingSurface.Canvas.DrawSurface(Green.Value.DrawingSurface, greenPos, context.ReplacingPaintWithOpacity);
+        previewSurface.DrawingSurface.Canvas.DrawSurface(Blue.Value.DrawingSurface, bluePos, context.ReplacingPaintWithOpacity);
+        previewSurface.DrawingSurface.Canvas.DrawSurface(Alpha.Value.DrawingSurface, alphaPos, context.ReplacingPaintWithOpacity);
+        
+        return previewSurface;
+    }
+
+    private Surface GetImage(Surface image, ColorFilter filter)
+    {
+        var imageSurface = new Surface(image.Size);
+
+        _paint.ColorFilter = filter;
+        imageSurface.DrawingSurface.Canvas.DrawSurface(image.DrawingSurface, 0, 0, _paint);
+
+        return imageSurface;
+    }
+
+    public override bool Validate() => Image.Value != null;
+
+    public override Node CreateCopy() => new SeparateChannelsNode();
+}

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

@@ -8,6 +8,7 @@ namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 public interface IColorFilterImplementation
 {
     public IntPtr CreateBlendMode(Color color, BlendMode blendMode);
+    public IntPtr CreateColorMatrix(float[] matrix);
     public void Dispose(ColorFilter colorFilter);
     public object GetNativeColorFilter(IntPtr objectPointer);
 }

+ 2 - 1
src/PixiEditor.DrawingApi.Core/Bridge/Operations/ICanvasImplementation.cs

@@ -22,7 +22,8 @@ namespace PixiEditor.DrawingApi.Core.Bridge.Operations
         public void DrawPoint(IntPtr objPtr, VecI pos, Paint paint);
         public void DrawPoints(IntPtr objPtr, PointMode pointMode, Point[] points, Paint paint);
         public void DrawRect(IntPtr objPtr, int x, int y, int width, int height, Paint paint);
-        public void DrawCircle(IntPtr objPtr, int x, int y, int radius, Paint paint);
+        public void DrawCircle(IntPtr objPtr, int cx, int cy, int radius, Paint paint);
+        public void DrawOval(IntPtr objPtr, int cx, int cy, int width, int height, Paint paint);
         public void ClipPath(IntPtr objPtr, VectorPath clipPath, ClipOperation clipOperation, bool antialias);
         public void ClipRect(IntPtr objPtr, RectD rect, ClipOperation clipOperation);
         public void Clear(IntPtr objPtr);

+ 15 - 3
src/PixiEditor.DrawingApi.Core/Surface/Canvas.cs

@@ -120,12 +120,24 @@ namespace PixiEditor.DrawingApi.Core.Surface
             Changed?.Invoke(new RectD(x, y, width, height));
         }
 
-        public void DrawCircle(int x, int y, int radius, Paint paint)
+        public void DrawCircle(int centerX, int centerY, int radius, Paint paint)
         {
-            DrawingBackendApi.Current.CanvasImplementation.DrawCircle(ObjectPointer, x, y, radius, paint);
-            Changed?.Invoke(new RectD(x - radius, y - radius, radius * 2, radius * 2));
+            DrawingBackendApi.Current.CanvasImplementation.DrawCircle(ObjectPointer, centerX, centerY, radius, paint);
+            Changed?.Invoke(new RectD(centerX - radius, centerY - radius, radius * 2, radius * 2));
         }
 
+        public void DrawCircle(VecI center, int radius, Paint paint) =>
+            DrawCircle(center.X, center.Y, radius, paint);
+
+        public void DrawOval(int centerX, int centerY, int radiusX, int radiusY, Paint paint)
+        {
+            DrawingBackendApi.Current.CanvasImplementation.DrawOval(ObjectPointer, centerX, centerY, radiusX, radiusY, paint);
+            Changed?.Invoke(new RectD(centerX - radiusX, centerY - radiusY, radiusX * 2, radiusY * 2));
+        }
+
+        public void DrawOval(VecI center, VecI radius, Paint paint) =>
+            DrawOval(center.X, center.Y, radius.X, radius.Y, paint);
+
         public void DrawRect(RectI rect, Paint paint) => DrawRect(rect.X, rect.Y, rect.Width, rect.Height, paint);
 
         public void ClipPath(VectorPath clipPath) => ClipPath(clipPath, ClipOperation.Intersect);

+ 18 - 0
src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/ColorFilter.cs

@@ -1,6 +1,7 @@
 using System;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Numerics;
 
 namespace PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
@@ -18,6 +19,23 @@ public class ColorFilter : NativeObject
         return filter;
     }
 
+    /// <param name="matrix">An array of <see cref="F:SkiaSharp.SKColorFilter.ColorMatrixSize" /> elements.</param>
+    /// <summary>Creates a new color filter that transforms a color by a 4x5 (row-major) matrix.</summary>
+    /// <returns>Returns the new <see cref="T:PixiEditor.DrawingApi.Core.Surface.PaintImpl.ColorFilter" />.</returns>
+    /// <remarks>The matrix is in row-major order and the translation column is specified in unnormalized, 0...255, space.</remarks>
+    public static ColorFilter CreateColorMatrix(float[] matrix)
+    {
+        return new ColorFilter(DrawingBackendApi.Current.ColorFilterImplementation.CreateColorMatrix(matrix));
+    }
+
+    public static ColorFilter CreateColorMatrix(ColorMatrix matrix)
+    {
+        float[] values = new float[ColorMatrix.Width * ColorMatrix.Height];
+        matrix.TryGetMembers(values);
+
+        return CreateColorMatrix(values);
+    }
+
     public override void Dispose()
     {
         DrawingBackendApi.Current.ColorFilterImplementation.Dispose(this);

+ 8 - 2
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs

@@ -112,10 +112,16 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             canvas.DrawRect(x, y, width, height, skPaint);
         }
 
-        public void DrawCircle(IntPtr objPtr, int x, int y, int radius, Paint paint)
+        public void DrawCircle(IntPtr objPtr, int cx, int cy, int radius, Paint paint)
         {
             var canvas = ManagedInstances[objPtr];
-            canvas.DrawCircle(x, y, radius, _paintImpl[paint.ObjectPointer]);
+            canvas.DrawCircle(cx, cy, radius, _paintImpl[paint.ObjectPointer]);
+        }
+
+        public void DrawOval(IntPtr objPtr, int cx, int cy, int width, int height, Paint paint)
+        {
+            var canvas = ManagedInstances[objPtr];
+            canvas.DrawOval(cx, cy, width, height, _paintImpl[paint.ObjectPointer]);
         }
 
         public void ClipPath(IntPtr objPtr, VectorPath clipPath, ClipOperation clipOperation, bool antialias)

+ 8 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaColorFilterImplementation.cs

@@ -17,6 +17,14 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return skColorFilter.Handle;
         }
 
+        public IntPtr CreateColorMatrix(float[] matrix)
+        {
+            var skColorFilter = SKColorFilter.CreateColorMatrix(matrix);
+            ManagedInstances[skColorFilter.Handle] = skColorFilter;
+
+            return skColorFilter.Handle;
+        }
+
         public void Dispose(ColorFilter colorFilter)
         {
             SKColorFilter skColorFilter = ManagedInstances[colorFilter.ObjectPointer];

+ 390 - 0
src/PixiEditor.Numerics/ColorMatrix.cs

@@ -0,0 +1,390 @@
+namespace PixiEditor.Numerics;
+
+/// <summary>
+/// A helper class for creating 4x5 color matrices
+/// </summary>
+public record struct ColorMatrix
+{
+    /// <summary>
+    /// All values are set to 0. <br/>
+    /// (_, _, _, _) => (0, 0, 0, 0)
+    /// </summary>
+    public static ColorMatrix Zero => new(
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0)
+    );
+
+    /// <summary>
+    /// All values stay the same. <br/>
+    /// (x, y, z, w) => (x, y, z, w)
+    /// </summary>
+    public static ColorMatrix Identity => new(
+        (1, 0, 0, 0, 0),
+        (0, 1, 0, 0, 0),
+        (0, 0, 1, 0, 0),
+        (0, 0, 0, 1, 0)
+    );
+
+    /// <summary>
+    /// Values are offset by r, g, b and a <br/>
+    /// (x, y, z, w) => (x + <paramref name="r"/>, y + <paramref name="g"/>, z + <paramref name="b"/>, w + <paramref name="a"/>)
+    /// </summary>
+    public static ColorMatrix Offset(float r, float g, float b, float a) => new(
+        (0, 0, 0, 0, r),
+        (0, 0, 0, 0, g),
+        (0, 0, 0, 0, b),
+        (0, 0, 0, 0, a)
+    );
+
+    /// <summary>
+    /// The Red value is mapped to the Green and Blue values. The Red and Alpha values become 0. <br/><br/>
+    /// 
+    /// Adding UseRed + UseAlpha will result in grayscale <br/>
+    /// (x, _, _, _) => (0, x, x, 0)
+    /// </summary>
+    public static ColorMatrix MapRedToGreenBlue => new(
+        (0, 0, 0, 0, 0),
+        (1, 0, 0, 0, 0),
+        (1, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0)
+    );
+    
+    /// <summary>
+    /// The Green value is mapped to the Red and Blue values. The Green and Alpha values become 0. <br/><br/>
+    /// 
+    /// Adding UseGreen + UseAlpha will result in grayscale <br/>
+    /// (_, y, _, _) => (y, 0, y, 0)
+    /// </summary>
+    public static ColorMatrix MapGreenToRedBlue => new(
+        (0, 1, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 1, 0, 0, 0),
+        (0, 0, 0, 0, 0)
+    );
+
+    /// <summary>
+    /// The Blue value is mapped to the Red and Green values. The Blue and Alpha values become 0. <br/><br/>
+    /// 
+    /// Adding UseBlue + UseAlpha will result in grayscale <br/>
+    /// (_, _, z, _) => (z, z, 0, 0)
+    /// </summary>
+    public static ColorMatrix MapBlueToRedGreen => new(
+        (0, 0, 1, 0, 0),
+        (0, 0, 1, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0)
+    );
+
+    /// <summary>
+    /// The Alpha value is mapped to the Red, Green and Blue values. The Alpha values becomes 0. <br/>
+    /// 
+    /// (_, _, _, w) => (w, w, w, 0)
+    /// </summary>
+    public static ColorMatrix MapAlphaToRedGreenBlue => new(
+        (0, 0, 0, 1, 0),
+        (0, 0, 0, 1, 0),
+        (0, 0, 0, 1, 0),
+        (0, 0, 0, 0, 0)
+    );
+
+    /// <summary>
+    /// The red value will stay the red value <br/>
+    /// (x, _, _, _) => (x, 0, 0, 0)
+    /// </summary>
+    public static ColorMatrix UseRed => new(
+        (1, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0)
+    );
+    
+    /// <summary>
+    /// The green value will stay the green value <br/>
+    /// (_, y, _, _) => (0, y, 0, 0)
+    /// </summary>
+    public static ColorMatrix UseGreen => new(
+        (0, 0, 0, 0, 0),
+        (0, 1, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0)
+    );
+    
+    /// <summary>
+    /// The blue value will stay the blue value <br/>
+    /// (_, _, z, _) => (0, 0, z, 0)
+    /// </summary>
+    public static ColorMatrix UseBlue => new(
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 1, 0, 0),
+        (0, 0, 0, 0, 0)
+    );
+    
+    /// <summary>
+    /// The alpha value will stay the alpha value <br/>
+    /// (_, _, _, w) => (0, 0, 0, w)
+    /// </summary>
+    public static ColorMatrix UseAlpha => new(
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 0, 0),
+        (0, 0, 0, 1, 0)
+    );
+
+    /// <summary>
+    /// The alpha value will be offset by 1 <br/>
+    /// (_, _, _, w) => (0, 0, 0, w + 1)
+    /// </summary>
+    public static ColorMatrix OpaqueAlphaOffset => Offset(0, 0, 0, 1);
+    
+    public static ColorMatrix operator +(ColorMatrix left, ColorMatrix right) => new(left.M11 + right.M11,
+        left.M12 + right.M12, left.M13 + right.M13, left.M14 + right.M14, left.M15 + right.M15, left.M21 + right.M21,
+        left.M22 + right.M22, left.M23 + right.M23, left.M24 + right.M24, left.M25 + right.M25, left.M31 + right.M31,
+        left.M32 + right.M32, left.M33 + right.M33, left.M34 + right.M34, left.M35 + right.M35, left.M41 + right.M41,
+        left.M42 + right.M42, left.M43 + right.M43, left.M44 + right.M44, left.M45 + right.M45);
+
+    public static ColorMatrix operator -(ColorMatrix left, ColorMatrix right) => new(left.M11 - right.M11,
+        left.M12 - right.M12, left.M13 - right.M13, left.M14 - right.M14, left.M15 - right.M15, left.M21 - right.M21,
+        left.M22 - right.M22, left.M23 - right.M23, left.M24 - right.M24, left.M25 - right.M25, left.M31 - right.M31,
+        left.M32 - right.M32, left.M33 - right.M33, left.M34 - right.M34, left.M35 - right.M35, left.M41 - right.M41,
+        left.M42 - right.M42, left.M43 - right.M43, left.M44 - right.M44, left.M45 - right.M45);
+
+    public static ColorMatrix operator *(ColorMatrix left, ColorMatrix right) => new(
+        left.M11 * right.M11 + left.M12 * right.M21 + left.M13 * right.M31 + left.M14 * right.M41,
+        left.M21 * right.M11 + left.M22 * right.M21 + left.M23 * right.M31 + left.M24 * right.M41,
+        left.M31 * right.M11 + left.M32 * right.M21 + left.M33 * right.M31 + left.M34 * right.M41,
+        left.M41 * right.M11 + left.M42 * right.M21 + left.M43 * right.M31 + left.M44 * right.M41,
+        left.M11 * right.M12 + left.M12 * right.M22 + left.M13 * right.M32 + left.M14 * right.M42,
+        left.M21 * right.M12 + left.M22 * right.M22 + left.M23 * right.M32 + left.M24 * right.M42,
+        left.M31 * right.M12 + left.M32 * right.M22 + left.M33 * right.M32 + left.M34 * right.M42,
+        left.M41 * right.M12 + left.M42 * right.M22 + left.M43 * right.M32 + left.M44 * right.M42,
+        left.M11 * right.M13 + left.M12 * right.M23 + left.M13 * right.M33 + left.M14 * right.M43,
+        left.M21 * right.M13 + left.M22 * right.M23 + left.M23 * right.M33 + left.M24 * right.M43,
+        left.M31 * right.M13 + left.M32 * right.M23 + left.M33 * right.M33 + left.M34 * right.M43,
+        left.M41 * right.M13 + left.M42 * right.M23 + left.M43 * right.M33 + left.M44 * right.M43,
+        left.M11 * right.M14 + left.M12 * right.M24 + left.M13 * right.M34 + left.M14 * right.M44,
+        left.M21 * right.M14 + left.M22 * right.M24 + left.M23 * right.M34 + left.M24 * right.M44,
+        left.M31 * right.M14 + left.M32 * right.M24 + left.M33 * right.M34 + left.M34 * right.M44,
+        left.M41 * right.M14 + left.M42 * right.M24 + left.M43 * right.M34 + left.M44 * right.M44,
+        left.M11 * right.M15 + left.M12 * right.M25 + left.M13 * right.M35 + left.M14 * right.M45,
+        left.M21 * right.M15 + left.M22 * right.M25 + left.M23 * right.M35 + left.M24 * right.M45,
+        left.M31 * right.M15 + left.M32 * right.M25 + left.M33 * right.M35 + left.M34 * right.M45,
+        left.M41 * right.M15 + left.M42 * right.M25 + left.M43 * right.M35 + left.M44 * right.M45);
+
+    public static implicit operator ColorMatrix(Matrix4x5F toConvert) => new(
+        toConvert.M11,
+        toConvert.M12,
+        toConvert.M13,
+        toConvert.M14,
+        toConvert.M15,
+        toConvert.M21,
+        toConvert.M22,
+        toConvert.M23,
+        toConvert.M24,
+        toConvert.M25,
+        toConvert.M31,
+        toConvert.M32,
+        toConvert.M33,
+        toConvert.M34,
+        toConvert.M35,
+        toConvert.M41,
+        toConvert.M42,
+        toConvert.M43,
+        toConvert.M44,
+        toConvert.M45);
+    
+    private ColorMatrix(float m11, float m12, float m13, float m14, float m15, float m21, float m22, float m23, float m24,
+        float m25, float m31, float m32, float m33, float m34, float m35, float m41, float m42, float m43, float m44,
+        float m45)
+    {
+        M11 = m11;
+        M12 = m12;
+        M13 = m13;
+        M14 = m14;
+        M15 = m15;
+        M21 = m21;
+        M22 = m22;
+        M23 = m23;
+        M24 = m24;
+        M25 = m25;
+        M31 = m31;
+        M32 = m32;
+        M33 = m33;
+        M34 = m34;
+        M35 = m35;
+        M41 = m41;
+        M42 = m42;
+        M43 = m43;
+        M44 = m44;
+        M45 = m45;
+    }
+
+    public ColorMatrix(
+        (float m11, float m12, float m13, float m14, float m15) row1, 
+        (float m21, float m22, float m23, float m24, float m25) row2,
+        (float m31, float m32, float m33, float m34, float m35) row3,
+        (float m41, float m42, float m43, float m44, float m45) row4)
+    {
+        (M11, M12, M13, M14, M15) = row1;
+        (M21, M22, M23, M24, M25) = row2;
+        (M31, M32, M33, M34, M35) = row3;
+        (M41, M42, M43, M44, M45) = row4;
+    }
+
+    [System.Runtime.CompilerServices.CompilerGenerated]
+    public bool TryGetMembers(Span<float> members)
+    {
+        if (members.Length < 20)
+            return false;
+        members[0] = M11;
+        members[1] = M12;
+        members[2] = M13;
+        members[3] = M14;
+        members[4] = M15;
+        members[5] = M21;
+        members[6] = M22;
+        members[7] = M23;
+        members[8] = M24;
+        members[9] = M25;
+        members[10] = M31;
+        members[11] = M32;
+        members[12] = M33;
+        members[13] = M34;
+        members[14] = M35;
+        members[15] = M41;
+        members[16] = M42;
+        members[17] = M43;
+        members[18] = M44;
+        members[19] = M45;
+        return true;
+    }
+
+    public bool TryGetRow(int row, Span<float> members)
+    {
+        if (row < 0 || row >= 4)
+            throw new ArgumentOutOfRangeException(nameof(row));
+        if (members.Length < 5)
+            return false;
+        switch (row)
+        {
+            case 0:
+                members[0] = M11;
+                members[1] = M12;
+                members[2] = M13;
+                members[3] = M14;
+                members[4] = M15;
+                break;
+            case 1:
+                members[0] = M21;
+                members[1] = M22;
+                members[2] = M23;
+                members[3] = M24;
+                members[4] = M25;
+                break;
+            case 2:
+                members[0] = M31;
+                members[1] = M32;
+                members[2] = M33;
+                members[3] = M34;
+                members[4] = M35;
+                break;
+            case 3:
+                members[0] = M41;
+                members[1] = M42;
+                members[2] = M43;
+                members[3] = M44;
+                members[4] = M45;
+                break;
+        }
+
+        return true;
+    }
+
+    public bool TryGetColumn(int column, Span<float> members)
+    {
+        if (column < 0 || column >= 5)
+            throw new global::System.ArgumentOutOfRangeException(nameof(column));
+        if (members.Length < 4)
+            return false;
+        switch (column)
+        {
+            case 0:
+                members[0] = M11;
+                members[1] = M21;
+                members[2] = M31;
+                members[3] = M41;
+                break;
+            case 1:
+                members[0] = M12;
+                members[1] = M22;
+                members[2] = M32;
+                members[3] = M42;
+                break;
+            case 2:
+                members[0] = M13;
+                members[1] = M23;
+                members[2] = M33;
+                members[3] = M43;
+                break;
+            case 3:
+                members[0] = M14;
+                members[1] = M24;
+                members[2] = M34;
+                members[3] = M44;
+                break;
+            case 4:
+                members[0] = M15;
+                members[1] = M25;
+                members[2] = M35;
+                members[3] = M45;
+                break;
+        }
+
+        return true;
+    }
+
+    public float M11 { get; }
+
+    public float M12 { get; }
+
+    public float M13 { get; }
+
+    public float M14 { get; }
+
+    public float M15 { get; }
+
+    public float M21 { get; }
+
+    public float M22 { get; }
+
+    public float M23 { get; }
+
+    public float M24 { get; }
+
+    public float M25 { get; }
+
+    public float M31 { get; }
+
+    public float M32 { get; }
+
+    public float M33 { get; }
+
+    public float M34 { get; }
+
+    public float M35 { get; }
+
+    public float M41 { get; }
+
+    public float M42 { get; }
+
+    public float M43 { get; }
+
+    public float M44 { get; }
+
+    public float M45 { get; }
+
+    public static int Width { get => 5; }
+    public static int Height { get => 4; }
+}

+ 253 - 0
src/PixiEditor.Numerics/Matrix4x5F.cs

@@ -0,0 +1,253 @@
+namespace PixiEditor.Numerics;
+
+public record struct Matrix4x5F
+{
+    public static Matrix4x5F operator +(Matrix4x5F left, Matrix4x5F right) => new(left.M11 + right.M11,
+        left.M12 + right.M12, left.M13 + right.M13, left.M14 + right.M14, left.M15 + right.M15, left.M21 + right.M21,
+        left.M22 + right.M22, left.M23 + right.M23, left.M24 + right.M24, left.M25 + right.M25, left.M31 + right.M31,
+        left.M32 + right.M32, left.M33 + right.M33, left.M34 + right.M34, left.M35 + right.M35, left.M41 + right.M41,
+        left.M42 + right.M42, left.M43 + right.M43, left.M44 + right.M44, left.M45 + right.M45);
+
+    public static Matrix4x5F operator -(Matrix4x5F left, Matrix4x5F right) => new(left.M11 - right.M11,
+        left.M12 - right.M12, left.M13 - right.M13, left.M14 - right.M14, left.M15 - right.M15, left.M21 - right.M21,
+        left.M22 - right.M22, left.M23 - right.M23, left.M24 - right.M24, left.M25 - right.M25, left.M31 - right.M31,
+        left.M32 - right.M32, left.M33 - right.M33, left.M34 - right.M34, left.M35 - right.M35, left.M41 - right.M41,
+        left.M42 - right.M42, left.M43 - right.M43, left.M44 - right.M44, left.M45 - right.M45);
+
+    public static Matrix4x5F operator *(Matrix4x5F left, Matrix4x5F right) => new(
+        left.M11 * right.M11 + left.M12 * right.M21 + left.M13 * right.M31 + left.M14 * right.M41,
+        left.M21 * right.M11 + left.M22 * right.M21 + left.M23 * right.M31 + left.M24 * right.M41,
+        left.M31 * right.M11 + left.M32 * right.M21 + left.M33 * right.M31 + left.M34 * right.M41,
+        left.M41 * right.M11 + left.M42 * right.M21 + left.M43 * right.M31 + left.M44 * right.M41,
+        left.M11 * right.M12 + left.M12 * right.M22 + left.M13 * right.M32 + left.M14 * right.M42,
+        left.M21 * right.M12 + left.M22 * right.M22 + left.M23 * right.M32 + left.M24 * right.M42,
+        left.M31 * right.M12 + left.M32 * right.M22 + left.M33 * right.M32 + left.M34 * right.M42,
+        left.M41 * right.M12 + left.M42 * right.M22 + left.M43 * right.M32 + left.M44 * right.M42,
+        left.M11 * right.M13 + left.M12 * right.M23 + left.M13 * right.M33 + left.M14 * right.M43,
+        left.M21 * right.M13 + left.M22 * right.M23 + left.M23 * right.M33 + left.M24 * right.M43,
+        left.M31 * right.M13 + left.M32 * right.M23 + left.M33 * right.M33 + left.M34 * right.M43,
+        left.M41 * right.M13 + left.M42 * right.M23 + left.M43 * right.M33 + left.M44 * right.M43,
+        left.M11 * right.M14 + left.M12 * right.M24 + left.M13 * right.M34 + left.M14 * right.M44,
+        left.M21 * right.M14 + left.M22 * right.M24 + left.M23 * right.M34 + left.M24 * right.M44,
+        left.M31 * right.M14 + left.M32 * right.M24 + left.M33 * right.M34 + left.M34 * right.M44,
+        left.M41 * right.M14 + left.M42 * right.M24 + left.M43 * right.M34 + left.M44 * right.M44,
+        left.M11 * right.M15 + left.M12 * right.M25 + left.M13 * right.M35 + left.M14 * right.M45,
+        left.M21 * right.M15 + left.M22 * right.M25 + left.M23 * right.M35 + left.M24 * right.M45,
+        left.M31 * right.M15 + left.M32 * right.M25 + left.M33 * right.M35 + left.M34 * right.M45,
+        left.M41 * right.M15 + left.M42 * right.M25 + left.M43 * right.M35 + left.M44 * right.M45);
+
+    public static implicit operator Matrix4x5F(ColorMatrix toConvert) => new(
+        toConvert.M11,
+        toConvert.M12,
+        toConvert.M13,
+        toConvert.M14,
+        toConvert.M15,
+        toConvert.M21,
+        toConvert.M22,
+        toConvert.M23,
+        toConvert.M24,
+        toConvert.M25,
+        toConvert.M31,
+        toConvert.M32,
+        toConvert.M33,
+        toConvert.M34,
+        toConvert.M35,
+        toConvert.M41,
+        toConvert.M42,
+        toConvert.M43,
+        toConvert.M44,
+        toConvert.M45);
+
+    private Matrix4x5F(float m11, float m12, float m13, float m14, float m15, float m21, float m22, float m23, float m24,
+        float m25, float m31, float m32, float m33, float m34, float m35, float m41, float m42, float m43, float m44,
+        float m45)
+    {
+        M11 = m11;
+        M12 = m12;
+        M13 = m13;
+        M14 = m14;
+        M15 = m15;
+        M21 = m21;
+        M22 = m22;
+        M23 = m23;
+        M24 = m24;
+        M25 = m25;
+        M31 = m31;
+        M32 = m32;
+        M33 = m33;
+        M34 = m34;
+        M35 = m35;
+        M41 = m41;
+        M42 = m42;
+        M43 = m43;
+        M44 = m44;
+        M45 = m45;
+    }
+
+    public Matrix4x5F(
+        (float m11, float m12, float m13, float m14, float m15) row1, 
+        (float m21, float m22, float m23, float m24, float m25) row2,
+        (float m31, float m32, float m33, float m34, float m35) row3,
+        (float m41, float m42, float m43, float m44, float m45) row4)
+    {
+        (M11, M12, M13, M14, M15) = row1;
+        (M21, M22, M23, M24, M25) = row2;
+        (M31, M32, M33, M34, M35) = row3;
+        (M41, M42, M43, M44, M45) = row4;
+    }
+
+    [System.Runtime.CompilerServices.CompilerGenerated]
+    public bool TryGetMembers(Span<float> members)
+    {
+        if (members.Length < 20)
+            return false;
+        members[0] = M11;
+        members[1] = M12;
+        members[2] = M13;
+        members[3] = M14;
+        members[4] = M15;
+        members[5] = M21;
+        members[6] = M22;
+        members[7] = M23;
+        members[8] = M24;
+        members[9] = M25;
+        members[10] = M31;
+        members[11] = M32;
+        members[12] = M33;
+        members[13] = M34;
+        members[14] = M35;
+        members[15] = M41;
+        members[16] = M42;
+        members[17] = M43;
+        members[18] = M44;
+        members[19] = M45;
+        return true;
+    }
+
+    public bool TryGetRow(int row, Span<float> members)
+    {
+        if (row < 0 || row >= 4)
+            throw new ArgumentOutOfRangeException(nameof(row));
+        if (members.Length < 5)
+            return false;
+        switch (row)
+        {
+            case 0:
+                members[0] = M11;
+                members[1] = M12;
+                members[2] = M13;
+                members[3] = M14;
+                members[4] = M15;
+                break;
+            case 1:
+                members[0] = M21;
+                members[1] = M22;
+                members[2] = M23;
+                members[3] = M24;
+                members[4] = M25;
+                break;
+            case 2:
+                members[0] = M31;
+                members[1] = M32;
+                members[2] = M33;
+                members[3] = M34;
+                members[4] = M35;
+                break;
+            case 3:
+                members[0] = M41;
+                members[1] = M42;
+                members[2] = M43;
+                members[3] = M44;
+                members[4] = M45;
+                break;
+        }
+
+        return true;
+    }
+
+    public bool TryGetColumn(int column, Span<float> members)
+    {
+        if (column < 0 || column >= 5)
+            throw new global::System.ArgumentOutOfRangeException(nameof(column));
+        if (members.Length < 4)
+            return false;
+        switch (column)
+        {
+            case 0:
+                members[0] = M11;
+                members[1] = M21;
+                members[2] = M31;
+                members[3] = M41;
+                break;
+            case 1:
+                members[0] = M12;
+                members[1] = M22;
+                members[2] = M32;
+                members[3] = M42;
+                break;
+            case 2:
+                members[0] = M13;
+                members[1] = M23;
+                members[2] = M33;
+                members[3] = M43;
+                break;
+            case 3:
+                members[0] = M14;
+                members[1] = M24;
+                members[2] = M34;
+                members[3] = M44;
+                break;
+            case 4:
+                members[0] = M15;
+                members[1] = M25;
+                members[2] = M35;
+                members[3] = M45;
+                break;
+        }
+
+        return true;
+    }
+
+    public float M11 { get; }
+
+    public float M12 { get; }
+
+    public float M13 { get; }
+
+    public float M14 { get; }
+
+    public float M15 { get; }
+
+    public float M21 { get; }
+
+    public float M22 { get; }
+
+    public float M23 { get; }
+
+    public float M24 { get; }
+
+    public float M25 { get; }
+
+    public float M31 { get; }
+
+    public float M32 { get; }
+
+    public float M33 { get; }
+
+    public float M34 { get; }
+
+    public float M35 { get; }
+
+    public float M41 { get; }
+
+    public float M42 { get; }
+
+    public float M43 { get; }
+
+    public float M44 { get; }
+
+    public float M45 { get; }
+
+    public static int Width { get => 5; }
+    public static int Height { get => 4; }
+}