Browse Source

Added explicit color matrix struct

CPKreuz 1 year ago
parent
commit
4e1a7b5200

+ 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));
+    }
+}

+ 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();
+    }
+}
+

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

@@ -5,11 +5,12 @@
                              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>
+    <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock ui:Translator.Key="{Binding DisplayName}" />
-        <Grid ColumnDefinitions="Auto,*,*,*,*,*" RowDefinitions="Auto, Auto, Auto, Auto, Auto">
+        <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" />

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

@@ -6,7 +6,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 public class MatrixTransformNode : Node
 {
-    private Matrix4x5F previousMatrix = new(
+    private ColorMatrix previousMatrix = new(
         (1, 0, 0, 0, 0),
         (0, 1, 0, 0, 0),
         (0, 0, 1, 0, 0),
@@ -18,7 +18,7 @@ public class MatrixTransformNode : Node
     
     public InputProperty<Surface?> Input { get; }
     
-    public InputProperty<Matrix4x5F> Matrix { get; }
+    public InputProperty<ColorMatrix> Matrix { get; }
 
     public MatrixTransformNode()
     {

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

@@ -28,9 +28,9 @@ public class ColorFilter : NativeObject
         return new ColorFilter(DrawingBackendApi.Current.ColorFilterImplementation.CreateColorMatrix(matrix));
     }
 
-    public static ColorFilter CreateColorMatrix(Matrix4x5F matrix)
+    public static ColorFilter CreateColorMatrix(ColorMatrix matrix)
     {
-        float[] values = new float[Matrix4x5F.Width * Matrix4x5F.Height];
+        float[] values = new float[ColorMatrix.Width * ColorMatrix.Height];
         matrix.TryGetMembers(values);
 
         return CreateColorMatrix(values);

+ 262 - 13
src/PixiEditor.Numerics/ColorMatrix.cs

@@ -3,13 +3,13 @@
 /// <summary>
 /// A helper class for creating 4x5 color matrices
 /// </summary>
-public static class ColorMatrix
+public record struct ColorMatrix
 {
     /// <summary>
     /// All values are set to 0. <br/>
     /// (_, _, _, _) => (0, 0, 0, 0)
     /// </summary>
-    public static Matrix4x5F Zero => new(
+    public static ColorMatrix Zero => new(
         (0, 0, 0, 0, 0),
         (0, 0, 0, 0, 0),
         (0, 0, 0, 0, 0),
@@ -20,7 +20,7 @@ public static class ColorMatrix
     /// All values stay the same. <br/>
     /// (x, y, z, w) => (x, y, z, w)
     /// </summary>
-    public static Matrix4x5F Identity => new(
+    public static ColorMatrix Identity => new(
         (1, 0, 0, 0, 0),
         (0, 1, 0, 0, 0),
         (0, 0, 1, 0, 0),
@@ -31,7 +31,7 @@ public static class ColorMatrix
     /// 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 Matrix4x5F Offset(float r, float g, float b, float a) => new(
+    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),
@@ -44,7 +44,7 @@ public static class ColorMatrix
     /// Adding UseRed + UseAlpha will result in grayscale <br/>
     /// (x, _, _, _) => (0, x, x, 0)
     /// </summary>
-    public static Matrix4x5F MapRedToGreenBlue => new(
+    public static ColorMatrix MapRedToGreenBlue => new(
         (0, 0, 0, 0, 0),
         (1, 0, 0, 0, 0),
         (1, 0, 0, 0, 0),
@@ -57,7 +57,7 @@ public static class ColorMatrix
     /// Adding UseGreen + UseAlpha will result in grayscale <br/>
     /// (_, y, _, _) => (y, 0, y, 0)
     /// </summary>
-    public static Matrix4x5F MapGreenToRedBlue => new(
+    public static ColorMatrix MapGreenToRedBlue => new(
         (0, 1, 0, 0, 0),
         (0, 0, 0, 0, 0),
         (0, 1, 0, 0, 0),
@@ -70,7 +70,7 @@ public static class ColorMatrix
     /// Adding UseBlue + UseAlpha will result in grayscale <br/>
     /// (_, _, z, _) => (z, z, 0, 0)
     /// </summary>
-    public static Matrix4x5F MapBlueToRedGreen => new(
+    public static ColorMatrix MapBlueToRedGreen => new(
         (0, 0, 1, 0, 0),
         (0, 0, 1, 0, 0),
         (0, 0, 0, 0, 0),
@@ -82,7 +82,7 @@ public static class ColorMatrix
     /// 
     /// (_, _, _, w) => (w, w, w, 0)
     /// </summary>
-    public static Matrix4x5F MapAlphaToRedGreenBlue => new(
+    public static ColorMatrix MapAlphaToRedGreenBlue => new(
         (0, 0, 0, 1, 0),
         (0, 0, 0, 1, 0),
         (0, 0, 0, 1, 0),
@@ -93,7 +93,7 @@ public static class ColorMatrix
     /// The red value will stay the red value <br/>
     /// (x, _, _, _) => (x, 0, 0, 0)
     /// </summary>
-    public static Matrix4x5F UseRed => new(
+    public static ColorMatrix UseRed => new(
         (1, 0, 0, 0, 0),
         (0, 0, 0, 0, 0),
         (0, 0, 0, 0, 0),
@@ -104,7 +104,7 @@ public static class ColorMatrix
     /// The green value will stay the green value <br/>
     /// (_, y, _, _) => (0, y, 0, 0)
     /// </summary>
-    public static Matrix4x5F UseGreen => new(
+    public static ColorMatrix UseGreen => new(
         (0, 0, 0, 0, 0),
         (0, 1, 0, 0, 0),
         (0, 0, 0, 0, 0),
@@ -115,7 +115,7 @@ public static class ColorMatrix
     /// The blue value will stay the blue value <br/>
     /// (_, _, z, _) => (0, 0, z, 0)
     /// </summary>
-    public static Matrix4x5F UseBlue => new(
+    public static ColorMatrix UseBlue => new(
         (0, 0, 0, 0, 0),
         (0, 0, 0, 0, 0),
         (0, 0, 1, 0, 0),
@@ -126,7 +126,7 @@ public static class ColorMatrix
     /// The alpha value will stay the alpha value <br/>
     /// (_, _, _, w) => (0, 0, 0, w)
     /// </summary>
-    public static Matrix4x5F UseAlpha => new(
+    public static ColorMatrix UseAlpha => new(
         (0, 0, 0, 0, 0),
         (0, 0, 0, 0, 0),
         (0, 0, 0, 0, 0),
@@ -137,5 +137,254 @@ public static class ColorMatrix
     /// The alpha value will be offset by 1 <br/>
     /// (_, _, _, w) => (0, 0, 0, w + 1)
     /// </summary>
-    public static Matrix4x5F OpaqueAlphaOffset => Offset(0, 0, 0, 1);
+    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; }
 }

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

@@ -36,6 +36,28 @@ public record struct Matrix4x5F
         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)