Skip to content

Commit

Permalink
Made fractal algorithms more flexible so that colors can be customized (
Browse files Browse the repository at this point in the history
#578)

* Improved Mandelbrot algorithm so that color can be customized

* Moved gradient to `Pinta.Effects`

* Added some default color schemes and an enum property that will be used to make a combobox

* Colors are now customizable in Julia Fractal, too

* Added `Resize` method to `ColorGradient`

* Moved `ColorGradient` to `Pinta.Effects`

* Changed names of gradients and left notes for translators

* Created `InvLerp` method

* Added documentation to `InvLerp` method

* `sorted_stops` is now an `ImmutableArray`, which is an important step as it allows us to use more library methods like `BinarySearch`

* Implemented search method with standard library methods

* Separated list of positions and list of colors, otherwise it's awkward

---------

Co-authored-by: Lehonti Ramos <lehonti@ramos>
  • Loading branch information
Lehonti and Lehonti Ramos authored Jan 9, 2024
1 parent df346c6 commit e108a40
Show file tree
Hide file tree
Showing 6 changed files with 623 additions and 20 deletions.
13 changes: 13 additions & 0 deletions Pinta.Core/Effects/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/////////////////////////////////////////////////////////////////////////////////

using System;
using System.Numerics;
using System.Reflection;

namespace Pinta.Core;
Expand All @@ -30,6 +31,18 @@ public static float Lerp (float from, float to, float frac)
public static double Lerp (double from, double to, double frac)
=> from + frac * (to - from);

/// <exception cref="ArgumentException">
/// Difference between upper and lower bounds is zero
/// </exception>
public static double InvLerp (double from, double to, double value)
{
double valueSpan = to - from;
if (valueSpan == 0)
throw new ArgumentException ("Difference between upper and lower bounds cannot be zero", $"{nameof (from)}, {nameof (to)}");
double offset = value - from;
return offset / valueSpan;
}

public static PointD Lerp (PointD from, PointD to, float frac)
=> new (
X: Lerp (from.X, to.X, frac),
Expand Down
201 changes: 201 additions & 0 deletions Pinta.Effects/Effects/ColorGradient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Pinta.Core;

namespace Pinta.Effects;

/// <summary>
/// Helps obtain intermediate colors at a certain position,
/// based on the start and end colors, and any additional color stops
/// </summary>
internal sealed class ColorGradient
{
/// <summary>
/// Color at the initial position in the gradient
/// </summary>
public ColorBgra StartColor { get; }

/// <summary>
/// Color at the end position in the gradient
/// </summary>
public ColorBgra EndColor { get; }

/// <summary>
/// Represents initial position in the gradient
/// </summary>
public double StartPosition { get; }

/// <summary>
/// Represents end position in the gradient
/// </summary>
public double EndPosition { get; }

private readonly ImmutableArray<double> sorted_positions;
private readonly ImmutableArray<ColorBgra> sorted_colors;

internal ColorGradient (
ColorBgra startColor,
ColorBgra endColor,
double minPosition,
double maxPosition,
IEnumerable<KeyValuePair<double, ColorBgra>> gradientStops)
{
CheckBoundsConsistency (minPosition, maxPosition);

var sortedStops = gradientStops.OrderBy (stop => stop.Key).ToArray ();
var sortedPositions = sortedStops.Select (stop => stop.Key).ToImmutableArray ();
var sortedColors = sortedStops.Select (stop => stop.Value).ToImmutableArray ();
CheckStopsBounds (sortedPositions, minPosition, maxPosition);
CheckUniqueness (sortedPositions);

StartColor = startColor;
EndColor = endColor;
StartPosition = minPosition;
EndPosition = maxPosition;
sorted_positions = sortedPositions;
sorted_colors = sortedColors;
}

private static void CheckStopsBounds (ImmutableArray<double> sortedPositions, double minPosition, double maxPosition)
{
if (sortedPositions.Length == 0) return;
if (sortedPositions[0] <= minPosition) throw new ArgumentException ($"Lowest key in gradient stops has to be greater than {nameof (minPosition)}");
if (sortedPositions[^1] >= maxPosition) throw new ArgumentException ($"Greatest key in gradient stops has to be lower than {nameof (maxPosition)}");
}

private static void CheckUniqueness (ImmutableArray<double> sortedPositions)
{
var distinctPositions = sortedPositions.GroupBy (s => s).Count ();
if (distinctPositions != sortedPositions.Length) throw new ArgumentException ("Cannot have more than one stop in the same position");
}

private static void CheckBoundsConsistency (double minPosition, double maxPosition)
{
if (minPosition >= maxPosition) throw new ArgumentException ($"{nameof (minPosition)} has to be lower than {nameof (maxPosition)}");
}

/// <summary>
/// Creates new gradient object with the lower and upper bounds
/// (along with all of its stops) adjusted, proportionally,
/// to the provided lower and upper bounds.
/// </summary>
public ColorGradient Resized (double minPosition, double maxPosition)
{
CheckBoundsConsistency (minPosition, maxPosition);

double newSpan = maxPosition - minPosition;
double currentSpan = EndPosition - StartPosition;
double newProportion = newSpan / currentSpan;
double newMinRelativeOffset = minPosition - StartPosition;

KeyValuePair<double, ColorBgra> ToNewStop (KeyValuePair<double, ColorBgra> stop)
{
double stopToMinOffset = stop.Key - StartPosition;
double adjustedOffset = stopToMinOffset * newProportion;
double newPosition = minPosition + adjustedOffset;
return KeyValuePair.Create (newPosition, stop.Value);
}

return new (
StartColor,
EndColor,
minPosition,
maxPosition,
sorted_positions.Zip (sorted_colors, KeyValuePair.Create).Select (ToNewStop)
);
}

/// <returns>
/// Intermediate color, according to start and end colors,
/// and gradient stops.
/// No overflow occurs as such;
/// if the target position is lower than the start position,
/// the start color will be returned, and if it's higher than
/// the end position, the end color will be returned.
/// </returns>
public ColorBgra GetColor (double position)
{
if (position <= StartPosition) return StartColor;
if (position >= EndPosition) return EndColor;
if (sorted_positions.Length == 0) return HandleNoStops (position);
return HandleWithStops (position);
}

private ColorBgra HandleNoStops (double position)
{
double fraction = Utility.InvLerp (StartPosition, EndPosition, position);
return ColorBgra.Lerp (StartColor, EndColor, fraction);
}

private ColorBgra HandleWithStops (double position)
{
int immediatelyHigherIndex = BinarySearchHigherOrEqual (sorted_positions, position);

if (immediatelyHigherIndex < 0)
return ColorBgra.Lerp (
sorted_colors[^1],
EndColor,
Utility.InvLerp (sorted_positions[^1], EndPosition, position));

var immediatelyHigher = KeyValuePair.Create (sorted_positions[immediatelyHigherIndex], sorted_colors[immediatelyHigherIndex]);

if (immediatelyHigher.Key == position)
return immediatelyHigher.Value;

int immediatelyLowerIndex = immediatelyHigherIndex - 1;

if (immediatelyLowerIndex < 0)
return ColorBgra.Lerp (
StartColor,
immediatelyHigher.Value,
Utility.InvLerp (StartPosition, immediatelyHigher.Key, position));

var immediatelyLower = KeyValuePair.Create (sorted_positions[immediatelyLowerIndex], sorted_colors[immediatelyLowerIndex]);

return ColorBgra.Lerp (
immediatelyLower.Value,
immediatelyHigher.Value,
Utility.InvLerp (immediatelyLower.Key, immediatelyHigher.Key, position));
}

private static int BinarySearchHigherOrEqual (ImmutableArray<double> sortedPositions, double target)
{
if (sortedPositions.Length == 0) return -1;
int found = sortedPositions.BinarySearch (target);
if (found >= 0) return found; // Exact match
int foundComplement = ~found;
if (foundComplement == sortedPositions.Length) return -1; // Not found
return foundComplement; // Found larger
}

private static IEnumerable<KeyValuePair<double, ColorBgra>> EmptyStops ()
=> Enumerable.Empty<KeyValuePair<double, ColorBgra>> ();

/// <summary>
/// Creates gradient mapping based on start and end color,
/// and the provided lower and upper bounds
/// </summary>
public static ColorGradient Create (ColorBgra start, ColorBgra end, double minimum, double maximum)
=> new (
start,
end,
minimum,
maximum,
EmptyStops ()
);

/// <summary>
/// Creates gradient mapping based on start and end color,
/// and the provided lower and upper bounds, and color stops
/// </summary>
public static ColorGradient Create (ColorBgra start, ColorBgra end, double minimum, double maximum, IEnumerable<KeyValuePair<double, ColorBgra>> stops)
=> new (
start,
end,
minimum,
maximum,
stops
);
}
132 changes: 132 additions & 0 deletions Pinta.Effects/Effects/GradientHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System.Collections.Generic;
using Pinta.Core;
using Pinta.Gui.Widgets;

namespace Pinta.Effects;

public enum PredefinedGradients
{
// Translators: Gradient with the colors of the flag of Italy: red, white, and green
[Caption ("Beautiful Italy")]
BeautifulItaly,

// Translators: Simple gradient that goes from black to white
[Caption ("Black and White")]
BlackAndWhite,

// Translators: Gradient that starts out white, like the core of a raging fire, and then goes through yellow, red, and black (like visible black smoke), and finally transparent, blending with the background
[Caption ("Bonfire")]
Bonfire,

// Translators: Gradient that starts out off-white, like cherry blossoms against sunlight, then goes through pink, then light blue (like the sky) and finally transparent, blending with the background
[Caption ("Cherry Blossom")]
CherryBlossom,

// Translators: Gradient with the colors of blue and pink cotton candy
[Caption ("Cotton Candy")]
CottonCandy,

// Translators: Gradient that starts out white, like the the inner part of a spark, and goes through progressively dark shades of blue until it reaches black, and finally transparent, blending with the background
[Caption ("Electric")]
Electric,

// Translators: Gradient with a citrusy vibe that starts out white, goes through light yellow, several shades of green, and then transparent, blending with the background
[Caption ("Lime Lemon")]
LimeLemon,

// Translators: Gradient with different shades of brownish yellow
[Caption ("Piña Colada")]
PinaColada,
}

internal static class GradientHelper
{
private const double DefaultMinimumValue = 0;
private const double DefaultMaximumValue = 1;

public static ColorGradient CreateColorGradient (PredefinedGradients scheme)
{
return scheme switch {

PredefinedGradients.BeautifulItaly => ColorGradient.Create (
ColorBgra.FromBgr (70, 146, 0),
ColorBgra.FromBgr (55, 43, 206),
DefaultMinimumValue,
DefaultMaximumValue,
new Dictionary<double, ColorBgra> {
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.25)] = ColorBgra.White,
}),

PredefinedGradients.BlackAndWhite => ColorGradient.Create (
ColorBgra.White,
ColorBgra.Black,
DefaultMinimumValue,
DefaultMaximumValue),

PredefinedGradients.Bonfire => ColorGradient.Create (
ColorBgra.Transparent,
ColorBgra.White,
DefaultMinimumValue,
DefaultMaximumValue,
new Dictionary<double, ColorBgra> {
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.25)] = ColorBgra.Black,
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.50)] = ColorBgra.Red,
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.75)] = ColorBgra.Yellow,
}),

PredefinedGradients.CherryBlossom => ColorGradient.Create (
ColorBgra.Transparent,
ColorBgra.FromBgr (240, 255, 255),
DefaultMinimumValue,
DefaultMaximumValue,
new Dictionary<double, ColorBgra> {
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.25)] = ColorBgra.FromBgr (235, 206, 135),
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.75)] = ColorBgra.FromBgr (193, 182, 255),
}),

PredefinedGradients.CottonCandy => ColorGradient.Create (
ColorBgra.White,
ColorBgra.FromBgr (242, 235, 214),
DefaultMinimumValue,
DefaultMaximumValue,
new Dictionary<double, ColorBgra> {
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.25)] = ColorBgra.FromBgr (180, 105, 255),
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.50)] = ColorBgra.FromBgr (219, 112, 219),
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.75)] = ColorBgra.FromBgr (230, 216, 173),
}),

PredefinedGradients.Electric => ColorGradient.Create (
ColorBgra.Transparent,
ColorBgra.White,
DefaultMinimumValue,
DefaultMaximumValue,
new Dictionary<double, ColorBgra> {
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.25)] = ColorBgra.Black,
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.50)] = ColorBgra.Blue,
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.75)] = ColorBgra.Cyan,
}),

PredefinedGradients.LimeLemon => ColorGradient.Create (
ColorBgra.Transparent,
ColorBgra.White,
DefaultMinimumValue,
DefaultMaximumValue,
new Dictionary<double, ColorBgra> {
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.25)] = ColorBgra.FromBgr (0, 128, 0),
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.50)] = ColorBgra.FromBgr (0, 255, 0),
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.75)] = ColorBgra.FromBgr (0, 255, 255),
}),

PredefinedGradients.PinaColada => ColorGradient.Create (
ColorBgra.FromBgr (0, 128, 128),
ColorBgra.FromBgr (196, 245, 253),
DefaultMinimumValue,
DefaultMaximumValue,
new Dictionary<double, ColorBgra> {
[Utility.Lerp (DefaultMinimumValue, DefaultMaximumValue, 0.25)] = ColorBgra.Yellow,
}),

_ => CreateColorGradient (PredefinedGradients.Electric),
};
}
}
Loading

0 comments on commit e108a40

Please sign in to comment.