diff --git a/Pinta.Core/Classes/Point.cs b/Pinta.Core/Classes/Point.cs index 91ba89e9d..461886577 100644 --- a/Pinta.Core/Classes/Point.cs +++ b/Pinta.Core/Classes/Point.cs @@ -34,6 +34,8 @@ public readonly record struct PointI (int X, int Y) public static PointI Zero { get; } = new (0, 0); public override readonly string ToString () => $"{X}, {Y}"; + public readonly PointD ToDouble () => new (X, Y); + public PointI Rotated90CCW () // Counterclockwise => new (-Y, X); diff --git a/Pinta.Effects/Effects/VoronoiDiagramEffect.cs b/Pinta.Effects/Effects/VoronoiDiagramEffect.cs index e9805dd6b..9491d61fc 100644 --- a/Pinta.Effects/Effects/VoronoiDiagramEffect.cs +++ b/Pinta.Effects/Effects/VoronoiDiagramEffect.cs @@ -13,10 +13,10 @@ namespace Pinta.Effects; public sealed class VoronoiDiagramEffect : BaseEffect { public override string Icon - => Pinta.Resources.Icons.EffectsRenderVoronoiDiagram; + => Resources.Icons.EffectsRenderVoronoiDiagram; public override bool IsTileable - => false; + => true; public override string Name => Translations.GetString ("Voronoi Diagram"); @@ -31,9 +31,11 @@ public VoronoiDiagramData Data => (VoronoiDiagramData) EffectData!; // NRT - Set in constructor private readonly IChromeService chrome; + private readonly ILivePreview live_preview; public VoronoiDiagramEffect (IServiceProvider services) { chrome = services.GetService (); + live_preview = services.GetService (); EffectData = new VoronoiDiagramData (); } @@ -42,60 +44,77 @@ public override Task LaunchConfiguration () private sealed record VoronoiSettings ( Size size, - bool showPoints, - ImmutableArray points, + ImmutableArray controlPoints, + ImmutableArray samplingLocations, ImmutableArray colors, - Func distanceCalculator); + Func distanceCalculator); - private VoronoiSettings CreateSettings (ImageSurface dst, RectangleI roi) + private VoronoiSettings CreateSettings (ImageSurface dst) { - ColorSorting colorSorting = Data.ColorSorting; + VoronoiDiagramData data = Data; + + RectangleI roi = live_preview.RenderBounds; + + ColorSorting colorSorting = data.ColorSorting; - IEnumerable basePoints = CreatePoints (roi, Data.NumberOfCells, Data.RandomPointLocations); - ImmutableArray points = SortPoints (basePoints, colorSorting).ToImmutableArray (); + PointD locationOffset = new (0.5, 0.5); - IEnumerable baseColors = CreateColors (points.Length, Data.RandomColors); + IEnumerable basePoints = CreatePoints (roi, data.NumberOfCells, data.RandomPointLocations); + IEnumerable pointCorners = SortPoints (basePoints, colorSorting).ToImmutableArray (); + ImmutableArray controlPoints = pointCorners.Select (p => p.ToDouble () + locationOffset).ToImmutableArray (); + + IEnumerable baseColors = CreateColors (controlPoints.Length, data.RandomColors); IEnumerable positionSortedColors = SortColors (baseColors, colorSorting); - IEnumerable reversedSortingColors = Data.ReverseColorSorting ? positionSortedColors.Reverse () : positionSortedColors; + IEnumerable reversedSortingColors = data.ReverseColorSorting ? positionSortedColors.Reverse () : positionSortedColors; return new ( size: dst.GetSize (), - showPoints: Data.ShowPoints, - points: points, + controlPoints: controlPoints, + samplingLocations: CreateSamplingLocations (data.Quality), colors: reversedSortingColors.ToImmutableArray (), - distanceCalculator: GetDistanceCalculator (Data.DistanceMetric) + distanceCalculator: GetDistanceCalculator (data.DistanceMetric) ); } - protected override void Render (ImageSurface src, ImageSurface dst, RectangleI roi) + protected override void Render ( + ImageSurface src, + ImageSurface dst, + RectangleI roi) { - VoronoiSettings settings = CreateSettings (dst, roi); + VoronoiSettings settings = CreateSettings (dst); Span dst_data = dst.GetPixelData (); - foreach (var kvp in roi.GeneratePixelOffsets (settings.size).AsParallel ().Select (CreateColor)) + foreach (var kvp in roi.GeneratePixelOffsets (settings.size).Select (CreateColor)) dst_data[kvp.Key] = kvp.Value; KeyValuePair CreateColor (PixelOffset pixel) + { + int sampleCount = settings.samplingLocations.Length; + Span samples = stackalloc ColorBgra[sampleCount]; + for (int i = 0; i < sampleCount; i++) { + PointD sampleLocation = pixel.coordinates.ToDouble () + settings.samplingLocations[i]; + ColorBgra sample = GetColorForLocation (sampleLocation); + samples[i] = sample; + } + return KeyValuePair.Create ( + pixel.memoryOffset, + ColorBgra.Blend (samples)); + } + + ColorBgra GetColorForLocation (PointD location) { double shortestDistance = double.MaxValue; int closestIndex = 0; - - for (var i = 0; i < settings.points.Length; i++) { + for (var i = 0; i < settings.controlPoints.Length; i++) { // TODO: Acceleration structure that limits the search // to a relevant subset of points, for better performance. // Some ideas to consider: quadtree, spatial hashing - var point = settings.points[i]; - double distance = settings.distanceCalculator (point, pixel.coordinates); + PointD controlPoint = settings.controlPoints[i]; + double distance = settings.distanceCalculator (location, controlPoint); if (distance > shortestDistance) continue; shortestDistance = distance; closestIndex = i; } - - ColorBgra finalColor = - settings.showPoints && shortestDistance == 0 - ? ColorBgra.Black - : settings.colors[closestIndex]; - - return KeyValuePair.Create (pixel.memoryOffset, finalColor); + return settings.colors[closestIndex]; } } @@ -135,7 +154,7 @@ or ColorSorting.VerticalG typeof (ColorSorting)), }; - private static Func GetDistanceCalculator (DistanceMetric distanceCalculationMethod) + private static Func GetDistanceCalculator (DistanceMetric distanceCalculationMethod) { return distanceCalculationMethod switch { DistanceMetric.Euclidean => Euclidean, @@ -147,21 +166,21 @@ private static Func GetDistanceCalculator (DistanceMetri typeof (DistanceMetric)), }; - static double Euclidean (PointI targetPoint, PointI pixelLocation) + static double Euclidean (PointD targetPoint, PointD pixelLocation) { - PointI difference = pixelLocation - targetPoint; + PointD difference = pixelLocation - targetPoint; return difference.Magnitude (); } - static double Manhattan (PointI targetPoint, PointI pixelLocation) + static double Manhattan (PointD targetPoint, PointD pixelLocation) { - PointI difference = pixelLocation - targetPoint; + PointD difference = pixelLocation - targetPoint; return Math.Abs (difference.X) + Math.Abs (difference.Y); } - static double Chebyshev (PointI targetPoint, PointI pixelLocation) + static double Chebyshev (PointD targetPoint, PointD pixelLocation) { - PointI difference = pixelLocation - targetPoint; + PointD difference = pixelLocation - targetPoint; return Math.Max (Math.Abs (difference.X), Math.Abs (difference.Y)); } } @@ -198,6 +217,34 @@ private static IEnumerable CreateColors (int colorCount, RandomSeed c } } + /// + /// Offsets, from top left corner of points, + /// where samples should be taken. + /// + /// + /// The resulting colors are intended to be blended. + /// + private static ImmutableArray CreateSamplingLocations (int quality) + { + var builder = ImmutableArray.CreateBuilder (); + builder.Capacity = quality * quality; + double sectionSize = 1.0 / quality; + double initial = sectionSize / 2; + + for (int h = 0; h < quality; h++) { + for (int v = 0; v < quality; v++) { + double hOffset = sectionSize * h; + double vOffset = sectionSize * v; + PointD currentPoint = new ( + X: initial + hOffset, + Y: initial + vOffset); + builder.Add (currentPoint); + } + } + + return builder.MoveToImmutable (); + } + public sealed class VoronoiDiagramData : EffectData { [Caption ("Distance Metric")] @@ -206,9 +253,7 @@ public sealed class VoronoiDiagramData : EffectData [Caption ("Number of Cells"), MinimumValue (1), MaximumValue (1024)] public int NumberOfCells { get; set; } = 100; - // Translators: The user can choose whether or not to render the points used in the calculation of a Voronoi diagram - [Caption ("Show Points")] - public bool ShowPoints { get; set; } = false; + // TODO: Show points [Caption ("Color Sorting")] public ColorSorting ColorSorting { get; set; } = ColorSorting.Random; @@ -222,6 +267,10 @@ public sealed class VoronoiDiagramData : EffectData [Caption ("Random Point Locations")] public RandomSeed RandomPointLocations { get; set; } = new (0); + + [Caption ("Quality")] + [MinimumValue (1), MaximumValue (4)] + public int Quality { get; set; } = 3; } public enum DistanceMetric @@ -235,6 +284,7 @@ public enum ColorSorting { [Caption ("Random")] Random, + // Translators: Horizontal color sorting with blue (B) as the leading term [Caption ("Horizontal blue (B)")] HorizontalB, diff --git a/tests/Pinta.Effects.Tests/Assets/voronoi1.png b/tests/Pinta.Effects.Tests/Assets/voronoi1.png index 9789618e9..8ade5d2c3 100644 Binary files a/tests/Pinta.Effects.Tests/Assets/voronoi1.png and b/tests/Pinta.Effects.Tests/Assets/voronoi1.png differ diff --git a/tests/Pinta.Effects.Tests/Assets/voronoi2.png b/tests/Pinta.Effects.Tests/Assets/voronoi2.png index f0b82b8c4..d4ae39df8 100644 Binary files a/tests/Pinta.Effects.Tests/Assets/voronoi2.png and b/tests/Pinta.Effects.Tests/Assets/voronoi2.png differ diff --git a/tests/Pinta.Effects.Tests/Assets/voronoi3.png b/tests/Pinta.Effects.Tests/Assets/voronoi3.png index acbe0e16d..42604be4e 100644 Binary files a/tests/Pinta.Effects.Tests/Assets/voronoi3.png and b/tests/Pinta.Effects.Tests/Assets/voronoi3.png differ diff --git a/tests/Pinta.Effects.Tests/Assets/voronoi4.png b/tests/Pinta.Effects.Tests/Assets/voronoi4.png index a004103a3..72f2028dc 100644 Binary files a/tests/Pinta.Effects.Tests/Assets/voronoi4.png and b/tests/Pinta.Effects.Tests/Assets/voronoi4.png differ diff --git a/tests/Pinta.Effects.Tests/Assets/voronoi5.png b/tests/Pinta.Effects.Tests/Assets/voronoi5.png index be7ddf786..8ccdc0e4f 100644 Binary files a/tests/Pinta.Effects.Tests/Assets/voronoi5.png and b/tests/Pinta.Effects.Tests/Assets/voronoi5.png differ