Browse Source

Merge branch 'funcy-nodes' into node-backend

flabbet 1 year ago
parent
commit
70ea498338
20 changed files with 447 additions and 39 deletions
  1. 8 1
      src/PixiEditor.AvaloniaUI/Data/Localization/Languages/en.json
  2. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodePropertyViewModel.cs
  3. 68 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/KernelPropertyViewModel.cs
  4. 3 0
      src/PixiEditor.AvaloniaUI/Views/Main/Navigation.axaml.cs
  5. 1 1
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/DoublePropertyView.axaml
  6. 51 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/KernelPropertyView.axaml
  7. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/KernelPropertyView.axaml.cs
  8. 0 30
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSizeNode.cs
  9. 58 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/KernelNode.cs
  10. 2 1
      src/PixiEditor.DrawingApi.Core/Bridge/IDrawingBackend.cs
  11. 14 0
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IImageFilterImplementation.cs
  12. 5 1
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IPaintImplementation.cs
  13. 40 0
      src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/ImageFilter.cs
  14. 11 0
      src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/Paint.cs
  15. 16 0
      src/PixiEditor.DrawingApi.Core/Surface/TileMode.cs
  16. 30 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageFilterImplementation.cs
  17. 16 2
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPaintImplementation.cs
  18. 6 2
      src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs
  19. 61 0
      src/PixiEditor.Numerics/Kernel.cs
  20. 42 0
      src/PixiEditor.Numerics/KernelArray.cs

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

@@ -633,5 +633,12 @@
   "EMPTY_IMAGE": "Empty image",
   "NOISE": "Noise",
   "SCALE": "Scale",
-  "SEED": "Seed"
+  "SEED": "Seed",
+  "KERNEL": "Kernel",
+  "KERNEL_VIEW_SUM": "Sum:",
+  "KERNEL_VIEW_SUM_TOOLTIP": "The sum of all values. You likely want to aim for a value of 1 or 0",
+  "GAIN": "Gain",
+  "BIAS": "Bias",
+  "TILE_MODE": "Tile Mode",
+  "ON_ALPHA": "On Alpha"
 }

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodePropertyViewModel.cs

@@ -139,7 +139,7 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
         return (NodePropertyViewModel)Activator.CreateInstance(viewModelType, node, type);
     }
 
-    public void InternalSetValue(object? value) => SetProperty(ref _value, value);
+    public void InternalSetValue(object? value) => SetProperty(ref _value, value, nameof(Value));
 }
 
 internal abstract class NodePropertyViewModel<T> : NodePropertyViewModel

+ 68 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/KernelPropertyViewModel.cs

@@ -0,0 +1,68 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class KernelPropertyViewModel : NodePropertyViewModel<Kernel?>
+{
+    public ObservableCollection<KernelVmReference> ReferenceCollections { get; }
+    
+    public RelayCommand<int> AdjustSizeCommand { get; }
+    
+    public KernelPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+        ReferenceCollections = new ObservableCollection<KernelVmReference>();
+        PropertyChanged += OnPropertyChanged;
+        AdjustSizeCommand = new RelayCommand<int>(Execute, i => i > 0 && Width < 9 || i < 0 && Width > 3);
+    }
+
+    private void Execute(int by)
+    {
+        Value.Resize(Width + by * 2, Height + by * 2);
+        OnPropertyChanged(nameof(Value));
+        AdjustSizeCommand.NotifyCanExecuteChanged();
+    }
+
+    public int Width => Value.Width;
+    
+    public int Height => Value.Height;
+
+    public float Sum => Value.Sum;
+
+    private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName != nameof(Value) || Value == null)
+            return;
+
+        ReferenceCollections.Clear();
+        
+        for (int y = -Value.RadiusY; y <= Value.RadiusY; y++)
+        {
+            for (int x = -Value.RadiusX; x <= Value.RadiusX; x++)
+            {
+                ReferenceCollections.Add(new KernelVmReference(this, x, y));
+            }
+        }
+        
+        OnPropertyChanged(nameof(Width));
+        OnPropertyChanged(nameof(Height));
+        OnPropertyChanged(nameof(Sum));
+    }
+
+    public class KernelVmReference(KernelPropertyViewModel viewModel, int x, int y) : PixiObservableObject
+    {
+        public float Value
+        {
+            get => viewModel.Value[x, y];
+            set
+            {
+                viewModel.Value[x, y] = value;
+                ViewModelMain.Current.NodeGraphManager.UpdatePropertyValue((viewModel.Node, viewModel.PropertyName, viewModel.Value));
+                viewModel.OnPropertyChanged(nameof(Sum));
+                OnPropertyChanged();
+            }
+        }
+    }
+}

+ 3 - 0
src/PixiEditor.AvaloniaUI/Views/Main/Navigation.axaml.cs

@@ -150,6 +150,9 @@ internal partial class Navigation : UserControl
         int x = (int)mousePosConverted.X;
         int y = (int)mousePosConverted.Y;
 
+        if (x < 0 || x > Document.Width || y < 0 || y > Document.Height)
+            return;
+        
         Thickness newPos = new Thickness(x, y, 0, 0);
 
         if (ColorCursorPosition == newPos)

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

@@ -11,6 +11,6 @@
                              x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.DoublePropertyView">
     <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}" />
+        <input:NumberInput HorizontalAlignment="Right" MinWidth="100" Decimals="6" IsVisible="{Binding IsInput}" Value="{Binding Value, Mode=TwoWay}" />
     </Grid>
 </properties:NodePropertyView>

+ 51 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/KernelPropertyView.axaml

@@ -0,0 +1,51 @@
+<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:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:system="clr-namespace:System;assembly=System.Runtime"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.KernelPropertyView">
+
+    <StackPanel Margin="0,2">
+        <Grid ColumnDefinitions="*,*,*" Margin="0,0,0,2">
+            <TextBlock ui:Translator.Key="{Binding DisplayName}" VerticalAlignment="Center"/>
+            <StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
+                <Button Padding="0" Content="-" Width="35" Command="{Binding AdjustSizeCommand}">
+                    <Button.CommandParameter>
+                        <system:Int32>-1</system:Int32>
+                    </Button.CommandParameter>
+                </Button>
+                <Button Padding="0" Content="+" Width="35" Command="{Binding AdjustSizeCommand}">
+                    <Button.CommandParameter>
+                        <system:Int32>1</system:Int32>
+                    </Button.CommandParameter>
+                </Button>
+            </StackPanel>
+            <StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
+                <TextBlock ui:Translator.Key="KERNEL_VIEW_SUM" Padding="0,0,2,0"
+                           ui:Translator.TooltipKey="KERNEL_VIEW_SUM_TOOLTIP"
+                           TextAlignment="Right"/>
+                <TextBlock Text="{Binding Sum, StringFormat='0.###'}"
+                           ui:Translator.TooltipKey="KERNEL_VIEW_SUM_TOOLTIP"
+                           TextAlignment="Right"/>
+            </StackPanel>
+        </Grid>
+
+        <ItemsControl ItemsSource="{Binding ReferenceCollections}" Margin="0,1">
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <input:NumberInput Value="{Binding Value, Mode=TwoWay}" Decimals="4" />
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+            <ItemsControl.ItemsPanel>
+                <ItemsPanelTemplate>
+                    <UniformGrid Columns="{Binding Width}" Rows="{Binding Height}" />
+                </ItemsPanelTemplate>
+            </ItemsControl.ItemsPanel>
+        </ItemsControl>
+    </StackPanel>
+</properties:NodePropertyView>

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

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

+ 0 - 30
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSizeNode.cs

@@ -1,30 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Rendering;
-using PixiEditor.DrawingApi.Core.Surface.ImageData;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-public class ImageSizeNode : Node
-{
-    public InputProperty<Surface?> Image { get; }
-    
-    public OutputProperty<VecI> Size { get; }
-    
-    public ImageSizeNode()
-    {
-        Image = CreateInput<Surface>(nameof(Image), "IMAGE", null);
-        Size = CreateOutput(nameof(Size), "SIZE", new VecI());
-    }
-    
-    protected override Surface? OnExecute(RenderingContext context)
-    {
-        Size.Value = Image.Value?.Size ?? new VecI();
-
-        return null;
-    }
-
-    public override bool Validate() => Image.Value != null;
-
-    public override Node CreateCopy() => new ImageSizeNode();
-}

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

@@ -0,0 +1,58 @@
+using PixiEditor.ChangeableDocument.Rendering;
+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 KernelFilterNode : Node
+{
+    private readonly Paint _paint = new();
+    
+    public OutputProperty<Surface> Transformed { get; }
+    
+    public InputProperty<Surface?> Image { get; }
+    
+    public InputProperty<Kernel> Kernel { get; }
+    
+    public InputProperty<double> Gain { get; }
+
+    public InputProperty<double> Bias { get; }
+
+    public InputProperty<TileMode> Tile { get; }
+
+    public InputProperty<bool> OnAlpha { get; }
+
+    public KernelFilterNode()
+    {
+        Transformed = CreateOutput<Surface>(nameof(Transformed), "TRANSFORMED", null);
+        Image = CreateInput<Surface>(nameof(Image), "IMAGE", null);
+        Kernel = CreateInput(nameof(Kernel), "KERNEL", Numerics.Kernel.Identity(3, 3));
+        Gain = CreateInput(nameof(Gain), "GAIN", 1d);
+        Bias = CreateInput(nameof(Bias), "BIAS", 0d);
+        Tile = CreateInput(nameof(Tile), "TILE_MODE", TileMode.Clamp);
+        OnAlpha = CreateInput(nameof(OnAlpha), "ON_ALPHA", false);
+    }
+    
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        var input = Image.Value;
+        var kernel = Kernel.Value;
+        var workingSurface = new Surface(input.Size);
+
+        var kernelOffset = new VecI(kernel.RadiusX, kernel.RadiusY);
+        using var imageFilter = ImageFilter.CreateMatrixConvolution(kernel, (float)Gain.Value, (float)Bias.Value, kernelOffset, Tile.Value, OnAlpha.Value);
+
+        _paint.ImageFilter = imageFilter;
+        workingSurface.DrawingSurface.Canvas.DrawSurface(Image.Value.DrawingSurface, 0, 0, _paint);
+        
+        Transformed.Value = workingSurface;
+        
+        return workingSurface;
+    }
+
+    public override bool Validate() => Image.Value != null;
+
+    public override Node CreateCopy() => new KernelFilterNode();
+}

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

@@ -17,7 +17,8 @@ namespace PixiEditor.DrawingApi.Core.Bridge
         public IColorSpaceImplementation ColorSpaceImplementation { get; }
         public IImgDataImplementation ImgDataImplementation { get; }
         public IBitmapImplementation BitmapImplementation { get; }
-        public IColorFilterImplementation ColorFilterImplementation { get; set; }
+        public IColorFilterImplementation ColorFilterImplementation { get; }
+        public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
     }
 }

+ 14 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IImageFilterImplementation.cs

@@ -0,0 +1,14 @@
+using System;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
+
+public interface IImageFilterImplementation
+{
+    IntPtr CreateMatrixConvolution(VecI size, ReadOnlySpan<float> kernel, float gain, float bias, VecI kernelOffset, TileMode mode, bool convolveAlpha);
+
+    object GetNativeImageFilter(IntPtr objPtr);
+    
+    void DisposeObject(IntPtr objPtr);
+}

+ 5 - 1
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IPaintImplementation.cs

@@ -24,9 +24,13 @@ namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl
         public void SetStrokeCap(Paint paint, StrokeCap value);
         public float GetStrokeWidth(Paint paint);
         public void SetStrokeWidth(Paint paint, float value);
+        
         public ColorFilter GetColorFilter(Paint paint);
-
         public void SetColorFilter(Paint paint, ColorFilter value);
+        
+        public ImageFilter GetImageFilter(Paint paint);
+        public void SetImageFilter(Paint paint, ImageFilter value);
+        
         public object GetNativePaint(IntPtr objectPointer);
         public Shader GetShader(Paint paint);
         public void SetShader(Paint paint, Shader shader);

+ 40 - 0
src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/ImageFilter.cs

@@ -0,0 +1,40 @@
+using System;
+using PixiEditor.DrawingApi.Core.Bridge;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+
+public class ImageFilter : NativeObject
+{
+    public ImageFilter(IntPtr objPtr) : base(objPtr)
+    {
+    }
+
+    public static ImageFilter CreateMatrixConvolution(VecI size, ReadOnlySpan<float> kernel, float gain, float bias, VecI kernelOffset,
+        TileMode tileMode, bool convolveAlpha)
+    {
+        var filter = new ImageFilter(DrawingBackendApi.Current.ImageFilterImplementation.CreateMatrixConvolution(
+            size,
+            kernel,
+            gain,
+            bias,
+            kernelOffset,
+            tileMode,
+            convolveAlpha));
+
+        return filter;
+    }
+
+    public static ImageFilter CreateMatrixConvolution(Kernel kernel, float gain, float bias, VecI kernelOffset, TileMode tileMode, bool convolveAlpha) =>
+        CreateMatrixConvolution(new VecI(kernel.Width, kernel.Height), kernel.AsSpan(), gain, bias, kernelOffset, tileMode, convolveAlpha);
+
+    public static ImageFilter CreateMatrixConvolution(KernelArray kernel, float gain, float bias, VecI kernelOffset, TileMode tileMode, bool convolveAlpha) =>
+        CreateMatrixConvolution(new VecI(kernel.Width, kernel.Height), kernel.AsSpan(), gain, bias, kernelOffset, tileMode, convolveAlpha);
+
+    public override object Native => DrawingBackendApi.Current.ImageFilterImplementation.GetNativeImageFilter(ObjectPointer);
+    
+    public override void Dispose()
+    {
+        DrawingBackendApi.Current.ImageFilterImplementation.DisposeObject(ObjectPointer);
+    }
+}

+ 11 - 0
src/PixiEditor.DrawingApi.Core/Surface/PaintImpl/Paint.cs

@@ -9,6 +9,7 @@ namespace PixiEditor.DrawingApi.Core.Surface.PaintImpl
     /// </summary>
     public class Paint : NativeObject
     {
+        private ImageFilter? imageFilter;
         private ColorFilter? colorFilter;
         private Shader? shader;
         
@@ -66,6 +67,16 @@ namespace PixiEditor.DrawingApi.Core.Surface.PaintImpl
             }
         }
 
+        public ImageFilter ImageFilter
+        {
+            get => imageFilter ??= DrawingBackendApi.Current.PaintImplementation.GetImageFilter(this);
+            set
+            {
+                DrawingBackendApi.Current.PaintImplementation.SetImageFilter(this, value);
+                imageFilter = value;
+            }
+        }
+
         public Shader Shader
         {
             get => shader ??= DrawingBackendApi.Current.PaintImplementation.GetShader(this);

+ 16 - 0
src/PixiEditor.DrawingApi.Core/Surface/TileMode.cs

@@ -0,0 +1,16 @@
+namespace PixiEditor.DrawingApi.Core.Surface;
+
+public enum TileMode
+{
+    /// <summary>Replicate the edge color.</summary>
+    Clamp,
+    
+    /// <summary>Repeat the shader's image horizontally and vertically.</summary>
+    Repeat,
+    
+    /// <summary>Repeat the shader's image horizontally and vertically, alternating mirror images so that adjacent images always seam.</summary>
+    Mirror,
+    
+    /// <summary>To be added.</summary>
+    Decal,
+}

+ 30 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageFilterImplementation.cs

@@ -0,0 +1,30 @@
+using System;
+using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+using SkiaSharp;
+
+namespace PixiEditor.DrawingApi.Skia.Implementations
+{
+    public class SkiaImageFilterImplementation : SkObjectImplementation<SKImageFilter>, IImageFilterImplementation
+    {
+        public IntPtr CreateMatrixConvolution(VecI size, ReadOnlySpan<float> kernel, float gain, float bias, VecI kernelOffset, TileMode mode, bool convolveAlpha)
+        {
+            var skImageFilter = SKImageFilter.CreateMatrixConvolution(
+                new SKSizeI(size.X, size.Y),
+                kernel,
+                gain,
+                bias,
+                new SKPointI(kernelOffset.X, kernelOffset.Y),
+                (SKShaderTileMode)mode,
+                convolveAlpha);
+
+            ManagedInstances[skImageFilter.Handle] = skImageFilter;
+            return skImageFilter.Handle;
+        }
+
+        public object GetNativeImageFilter(IntPtr objPtr) => ManagedInstances[objPtr];
+    }
+}

+ 16 - 2
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPaintImplementation.cs

@@ -10,11 +10,13 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
     public class SkiaPaintImplementation : SkObjectImplementation<SKPaint>, IPaintImplementation
     {
         private readonly SkiaColorFilterImplementation colorFilterImplementation;
+        private readonly SkiaImageFilterImplementation imageFilterImplementation;
         private readonly SkiaShaderImplementation shaderImplementation;
  
-        public SkiaPaintImplementation(SkiaColorFilterImplementation colorFilterImpl, SkiaShaderImplementation shaderImpl)
+        public SkiaPaintImplementation(SkiaColorFilterImplementation colorFilterImpl, SkiaImageFilterImplementation imageFilterImpl, SkiaShaderImplementation shaderImpl)
         {
             colorFilterImplementation = colorFilterImpl;
+            imageFilterImplementation = imageFilterImpl;
             shaderImplementation = shaderImpl;
         }
 
@@ -148,7 +150,19 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             SKPaint skPaint = ManagedInstances[paint.ObjectPointer];
             skPaint.ColorFilter = colorFilterImplementation[value.ObjectPointer];
         }
-        
+
+        public ImageFilter GetImageFilter(Paint paint)
+        {
+            SKPaint skPaint = ManagedInstances[paint.ObjectPointer];
+            return new ImageFilter(skPaint.ColorFilter.Handle);
+        }
+
+        public void SetImageFilter(Paint paint, ImageFilter value)
+        {
+            SKPaint skPaint = ManagedInstances[paint.ObjectPointer];
+            skPaint.ImageFilter = imageFilterImplementation[value.ObjectPointer];
+        }
+
         public Shader GetShader(Paint paint)
         {
             SKPaint skPaint = ManagedInstances[paint.ObjectPointer];

+ 6 - 2
src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs

@@ -18,7 +18,8 @@ namespace PixiEditor.DrawingApi.Skia
         public ISurfaceImplementation SurfaceImplementation { get; }
         public IColorSpaceImplementation ColorSpaceImplementation { get; }
         public IBitmapImplementation BitmapImplementation { get; }
-        public IColorFilterImplementation ColorFilterImplementation { get; set; }
+        public IColorFilterImplementation ColorFilterImplementation { get; }
+        public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
 
         public SkiaDrawingBackend()
@@ -30,11 +31,14 @@ namespace PixiEditor.DrawingApi.Skia
             
             SkiaColorFilterImplementation colorFilterImpl = new SkiaColorFilterImplementation();
             ColorFilterImplementation = colorFilterImpl;
+
+            SkiaImageFilterImplementation imageFilterImpl = new SkiaImageFilterImplementation();
+            ImageFilterImplementation = imageFilterImpl;
             
             SkiaShaderImplementation shader = new SkiaShaderImplementation();
             ShaderImplementation = shader;
             
-            SkiaPaintImplementation paintImpl = new SkiaPaintImplementation(colorFilterImpl, shader);
+            SkiaPaintImplementation paintImpl = new SkiaPaintImplementation(colorFilterImpl, imageFilterImpl, shader);
             PaintImplementation = paintImpl;
             
             SkiaPathImplementation pathImpl = new SkiaPathImplementation();

+ 61 - 0
src/PixiEditor.Numerics/Kernel.cs

@@ -0,0 +1,61 @@
+namespace PixiEditor.Numerics;
+
+public class Kernel
+{
+    private KernelArray _buffer;
+
+    public int Width { get; private set; }
+
+    public int Height { get; private set; }
+
+    public int RadiusX => Width / 2;
+    
+    public int RadiusY => Height / 2;
+
+    public float this[int x, int y]
+    {
+        get => _buffer[x, y];
+        set => _buffer[x, y] = value;
+    }
+
+    public float Sum => _buffer.Sum;
+    
+    public Kernel(int width, int height)
+    {
+        if (width % 2 == 0)
+            throw new ArgumentException($"{width} must be odd", nameof(width));
+        
+        Width = width;
+        Height = height;
+        _buffer = new KernelArray(width, height);
+    }
+
+    public static Kernel Identity(int width, int height) =>
+        new(width, height) { [0, 0] = 1 };
+
+    public void Resize(int width, int height)
+    {
+        var old = _buffer;
+
+        _buffer = new KernelArray(width, height);
+        Width = width;
+        Height = height;
+
+        var oldRadiusX = old.RadiusX;
+        var oldRadiusY = old.RadiusY;
+        var newRadiusX = _buffer.RadiusX;
+        var newRadiusY = _buffer.RadiusY;
+        for (int y = -newRadiusY; y <= newRadiusY; y++)
+        {
+            for (int x = -newRadiusX; x <= newRadiusX; x++)
+            {
+                if (x < -oldRadiusX || x > oldRadiusX || y < -oldRadiusY || y > oldRadiusY)
+                    continue;
+
+                _buffer[x, y] = old[x, y];
+            }
+        }
+    }
+
+    public ReadOnlySpan<float> AsSpan() => _buffer.AsSpan();
+}

+ 42 - 0
src/PixiEditor.Numerics/KernelArray.cs

@@ -0,0 +1,42 @@
+namespace PixiEditor.Numerics;
+
+public class KernelArray
+{
+    private readonly float[] _buffer;
+    
+    public int Width { get; }
+    
+    public int Height { get; }
+
+    public int RadiusX => Width / 2;
+    
+    public int RadiusY => Height / 2;
+
+    public float Sum => _buffer.Sum();
+    
+    public KernelArray(int width, int height)
+    {
+        if (width % 2 == 0)
+            throw new ArgumentException($"{width} must be odd", nameof(width));
+        
+        Width = width;
+        Height = height;
+        _buffer = new float[width * height];
+    }
+
+    public float this[int x, int y]
+    {
+        get => _buffer[GetBufferIndex(x, y)];
+        set => _buffer[GetBufferIndex(x, y)] = value;
+    }
+
+    private int GetBufferIndex(int x, int y)
+    {
+        x += RadiusX;
+        y += RadiusY;
+
+        return y * Width + x;
+    }
+
+    public ReadOnlySpan<float> AsSpan() => _buffer.AsSpan();
+}