-
Notifications
You must be signed in to change notification settings - Fork 275
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Made fractal algorithms more flexible so that colors can be customized (
#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
Showing
6 changed files
with
623 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}; | ||
} | ||
} |
Oops, something went wrong.