diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj
index 9a766c559..0c62553d4 100644
--- a/src/Artemis.Core/Artemis.Core.csproj
+++ b/src/Artemis.Core/Artemis.Core.csproj
@@ -44,9 +44,9 @@
-
-
-
+
+
+
diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
index 57c6a2f5f..6641be310 100644
--- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings
+++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
@@ -1,4 +1,6 @@
+ True
+ True
True
True
True
diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs b/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs
new file mode 100644
index 000000000..5b7b353a0
--- /dev/null
+++ b/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs
@@ -0,0 +1,167 @@
+using SkiaSharp;
+using System;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Artemis.Core.ColorScience;
+
+internal readonly struct ColorRanges
+{
+ public readonly byte RedRange;
+ public readonly byte GreenRange;
+ public readonly byte BlueRange;
+
+ public ColorRanges(byte redRange, byte greenRange, byte blueRange)
+ {
+ this.RedRange = redRange;
+ this.GreenRange = greenRange;
+ this.BlueRange = blueRange;
+ }
+}
+
+internal readonly struct ColorCube
+{
+ private const int BYTES_PER_COLOR = 4;
+ private static readonly int ELEMENTS_PER_VECTOR = Vector.Count / BYTES_PER_COLOR;
+ private static readonly int BYTES_PER_VECTOR = ELEMENTS_PER_VECTOR * BYTES_PER_COLOR;
+
+ private readonly int _from;
+ private readonly int _length;
+ private readonly SortTarget _currentOrder = SortTarget.None;
+
+ public ColorCube(in Span fullColorList, int from, int length, SortTarget preOrdered)
+ {
+ this._from = from;
+ this._length = length;
+
+ if (length < 2) return;
+
+ Span colors = fullColorList.Slice(from, length);
+ ColorRanges colorRanges = GetColorRanges(colors);
+
+ if ((colorRanges.RedRange > colorRanges.GreenRange) && (colorRanges.RedRange > colorRanges.BlueRange))
+ {
+ if (preOrdered != SortTarget.Red)
+ QuantizerSort.SortRed(colors);
+
+ _currentOrder = SortTarget.Red;
+ }
+ else if (colorRanges.GreenRange > colorRanges.BlueRange)
+ {
+ if (preOrdered != SortTarget.Green)
+ QuantizerSort.SortGreen(colors);
+
+ _currentOrder = SortTarget.Green;
+ }
+ else
+ {
+ if (preOrdered != SortTarget.Blue)
+ QuantizerSort.SortBlue(colors);
+
+ _currentOrder = SortTarget.Blue;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private ColorRanges GetColorRanges(in ReadOnlySpan colors)
+ {
+ if (Vector.IsHardwareAccelerated && (colors.Length >= Vector.Count))
+ {
+ int chunks = colors.Length / ELEMENTS_PER_VECTOR;
+ int vectorElements = (chunks * ELEMENTS_PER_VECTOR);
+ int missingElements = colors.Length - vectorElements;
+
+ Vector max = Vector.Zero;
+ Vector min = new(byte.MaxValue);
+ foreach (Vector currentVector in MemoryMarshal.Cast>(colors[..vectorElements]))
+ {
+ max = Vector.Max(max, currentVector);
+ min = Vector.Min(min, currentVector);
+ }
+
+ byte redMin = byte.MaxValue;
+ byte redMax = byte.MinValue;
+ byte greenMin = byte.MaxValue;
+ byte greenMax = byte.MinValue;
+ byte blueMin = byte.MaxValue;
+ byte blueMax = byte.MinValue;
+
+ for (int i = 0; i < BYTES_PER_VECTOR; i += BYTES_PER_COLOR)
+ {
+ if (min[i + 2] < redMin) redMin = min[i + 2];
+ if (max[i + 2] > redMax) redMax = max[i + 2];
+ if (min[i + 1] < greenMin) greenMin = min[i + 1];
+ if (max[i + 1] > greenMax) greenMax = max[i + 1];
+ if (min[i] < blueMin) blueMin = min[i];
+ if (max[i] > blueMax) blueMax = max[i];
+ }
+
+ for (int i = 0; i < missingElements; i++)
+ {
+ SKColor color = colors[^(i + 1)];
+
+ if (color.Red < redMin) redMin = color.Red;
+ if (color.Red > redMax) redMax = color.Red;
+ if (color.Green < greenMin) greenMin = color.Green;
+ if (color.Green > greenMax) greenMax = color.Green;
+ if (color.Blue < blueMin) blueMin = color.Blue;
+ if (color.Blue > blueMax) blueMax = color.Blue;
+ }
+
+ return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin));
+ }
+ else
+ {
+ byte redMin = byte.MaxValue;
+ byte redMax = byte.MinValue;
+ byte greenMin = byte.MaxValue;
+ byte greenMax = byte.MinValue;
+ byte blueMin = byte.MaxValue;
+ byte blueMax = byte.MinValue;
+
+ foreach (SKColor color in colors)
+ {
+ if (color.Red < redMin) redMin = color.Red;
+ if (color.Red > redMax) redMax = color.Red;
+ if (color.Green < greenMin) greenMin = color.Green;
+ if (color.Green > greenMax) greenMax = color.Green;
+ if (color.Blue < blueMin) blueMin = color.Blue;
+ if (color.Blue > blueMax) blueMax = color.Blue;
+ }
+
+ return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void Split(in Span fullColorList, out ColorCube a, out ColorCube b)
+ {
+ Span colors = fullColorList.Slice(_from, _length);
+
+ int median = colors.Length / 2;
+
+ a = new ColorCube(fullColorList, _from, median, _currentOrder);
+ b = new ColorCube(fullColorList, _from + median, colors.Length - median, _currentOrder);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal SKColor GetAverageColor(in ReadOnlySpan fullColorList)
+ {
+ ReadOnlySpan colors = fullColorList.Slice(_from, _length);
+
+ int r = 0, g = 0, b = 0;
+ foreach (SKColor color in colors)
+ {
+ r += color.Red;
+ g += color.Green;
+ b += color.Blue;
+ }
+
+ return new SKColor(
+ (byte)(r / colors.Length),
+ (byte)(g / colors.Length),
+ (byte)(b / colors.Length)
+ );
+ }
+}
diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs
new file mode 100644
index 000000000..1afaa4f3c
--- /dev/null
+++ b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs
@@ -0,0 +1,285 @@
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Artemis.Core.ColorScience;
+
+///
+/// Helper class for color quantization.
+///
+public static class ColorQuantizer
+{
+ ///
+ /// Quantizes a span of colors into the desired amount of representative colors.
+ ///
+ /// The colors to quantize
+ /// How many colors to return. Must be a power of two.
+ /// colors.
+ public static SKColor[] Quantize(in Span colors, int amount)
+ {
+ if (!BitOperations.IsPow2(amount))
+ throw new ArgumentException("Must be power of two", nameof(amount));
+
+ int splits = BitOperations.Log2((uint)amount);
+ return QuantizeSplit(colors, splits);
+ }
+
+ ///
+ /// Quantizes a span of colors, splitting the average number of times.
+ ///
+ /// The colors to quantize
+ /// How many splits to execute. Each split doubles the number of colors returned.
+ /// Up to (2 ^ ) number of colors.
+ public static SKColor[] QuantizeSplit(in Span colors, int splits)
+ {
+ if (colors.Length < (1 << splits)) throw new ArgumentException($"The color array must at least contain ({(1 << splits)}) to perform {splits} splits.");
+
+ Span cubes = new ColorCube[1 << splits];
+ cubes[0] = new ColorCube(colors, 0, colors.Length, SortTarget.None);
+
+ int currentIndex = 0;
+ for (int i = 0; i < splits; i++)
+ {
+ int currentCubeCount = 1 << i;
+ Span currentCubes = cubes.Slice(0, currentCubeCount);
+ for (int j = 0; j < currentCubes.Length; j++)
+ {
+ currentCubes[j].Split(colors, out ColorCube a, out ColorCube b);
+ currentCubes[j] = a;
+ cubes[++currentIndex] = b;
+ }
+ }
+
+ SKColor[] result = new SKColor[cubes.Length];
+ for (int i = 0; i < cubes.Length; i++)
+ result[i] = cubes[i].GetAverageColor(colors);
+
+ return result;
+ }
+
+ ///
+ /// Finds colors with certain characteristics in a given .
+ ///
+ /// Vibrant variants are more saturated, while Muted colors are less.
+ ///
+ /// Light and Dark colors have higher and lower lightness values, respectively.
+ ///
+ /// The colors to find the variations in
+ /// Which type of color to find
+ ///
+ /// Ignore hard limits on whether a color is considered for each category. Result may be
+ /// if this is false
+ ///
+ /// The color found
+ public static SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false)
+ {
+ float bestColorScore = 0;
+ SKColor bestColor = SKColor.Empty;
+
+ foreach (SKColor clr in colors)
+ {
+ float score = GetScore(clr, type, ignoreLimits);
+ if (score > bestColorScore)
+ {
+ bestColorScore = score;
+ bestColor = clr;
+ }
+ }
+
+ return bestColor;
+ }
+
+ ///
+ /// Finds all the color variations available and returns a struct containing them all.
+ ///
+ /// The colors to find the variations in
+ ///
+ /// Ignore hard limits on whether a color is considered for each category. Some colors may be
+ /// if this is false
+ ///
+ /// A swatch containing all color variations
+ public static ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false)
+ {
+ SKColor bestVibrantColor = SKColor.Empty;
+ SKColor bestLightVibrantColor = SKColor.Empty;
+ SKColor bestDarkVibrantColor = SKColor.Empty;
+ SKColor bestMutedColor = SKColor.Empty;
+ SKColor bestLightMutedColor = SKColor.Empty;
+ SKColor bestDarkMutedColor = SKColor.Empty;
+ float bestVibrantScore = float.MinValue;
+ float bestLightVibrantScore = float.MinValue;
+ float bestDarkVibrantScore = float.MinValue;
+ float bestMutedScore = float.MinValue;
+ float bestLightMutedScore = float.MinValue;
+ float bestDarkMutedScore = float.MinValue;
+
+ //ugly but at least we only loop through the enumerable once ¯\_(ツ)_/¯
+ foreach (SKColor color in colors)
+ {
+ static void SetIfBetterScore(ref float bestScore, ref SKColor bestColor, SKColor newColor, ColorType type, bool ignoreLimits)
+ {
+ float newScore = GetScore(newColor, type, ignoreLimits);
+ if (newScore > bestScore)
+ {
+ bestScore = newScore;
+ bestColor = newColor;
+ }
+ }
+
+ SetIfBetterScore(ref bestVibrantScore, ref bestVibrantColor, color, ColorType.Vibrant, ignoreLimits);
+ SetIfBetterScore(ref bestLightVibrantScore, ref bestLightVibrantColor, color, ColorType.LightVibrant, ignoreLimits);
+ SetIfBetterScore(ref bestDarkVibrantScore, ref bestDarkVibrantColor, color, ColorType.DarkVibrant, ignoreLimits);
+ SetIfBetterScore(ref bestMutedScore, ref bestMutedColor, color, ColorType.Muted, ignoreLimits);
+ SetIfBetterScore(ref bestLightMutedScore, ref bestLightMutedColor, color, ColorType.LightMuted, ignoreLimits);
+ SetIfBetterScore(ref bestDarkMutedScore, ref bestDarkMutedColor, color, ColorType.DarkMuted, ignoreLimits);
+ }
+
+ return new ColorSwatch
+ {
+ Vibrant = bestVibrantColor,
+ LightVibrant = bestLightVibrantColor,
+ DarkVibrant = bestDarkVibrantColor,
+ Muted = bestMutedColor,
+ LightMuted = bestLightMutedColor,
+ DarkMuted = bestDarkMutedColor,
+ };
+ }
+
+ ///
+ /// Gets a gradient from a given image.
+ ///
+ /// The image to process
+ public static ColorGradient GetGradientFromImage(SKBitmap bitmap)
+ {
+ SKColor[] colors = QuantizeSplit(bitmap.Pixels, 8);
+ ColorSwatch swatch = FindAllColorVariations(colors);
+ SKColor[] swatchArray = new SKColor[]
+ {
+ swatch.Muted,
+ swatch.Vibrant,
+ swatch.DarkMuted,
+ swatch.DarkVibrant,
+ swatch.LightMuted,
+ swatch.LightVibrant
+ };
+
+ ColorSorter.Sort(swatchArray, SKColors.Black);
+
+ ColorGradient gradient = new();
+
+ for (int i = 0; i < swatchArray.Length; i++)
+ gradient.Add(new ColorGradientStop(swatchArray[i], (float)i / (swatchArray.Length - 1)));
+
+ return gradient;
+ }
+
+ private static float GetScore(SKColor color, ColorType type, bool ignoreLimits = false)
+ {
+ static float InvertDiff(float value, float target)
+ {
+ return 1 - Math.Abs(value - target);
+ }
+
+ color.ToHsl(out float _, out float saturation, out float luma);
+ saturation /= 100f;
+ luma /= 100f;
+
+ if (!ignoreLimits && ((saturation <= GetMinSaturation(type)) || (saturation >= GetMaxSaturation(type)) || (luma <= GetMinLuma(type)) || (luma >= GetMaxLuma(type))))
+ {
+ //if either saturation or luma falls outside the min-max, return the
+ //lowest score possible unless we're ignoring these limits.
+ return float.MinValue;
+ }
+
+ float totalValue = (InvertDiff(saturation, GetTargetSaturation(type)) * WEIGHT_SATURATION) + (InvertDiff(luma, GetTargetLuma(type)) * WEIGHT_LUMA);
+
+ const float TOTAL_WEIGHT = WEIGHT_SATURATION + WEIGHT_LUMA;
+
+ return totalValue / TOTAL_WEIGHT;
+ }
+
+ #region Constants
+
+ private const float TARGET_DARK_LUMA = 0.26f;
+ private const float MAX_DARK_LUMA = 0.45f;
+ private const float MIN_LIGHT_LUMA = 0.55f;
+ private const float TARGET_LIGHT_LUMA = 0.74f;
+ private const float MIN_NORMAL_LUMA = 0.3f;
+ private const float TARGET_NORMAL_LUMA = 0.5f;
+ private const float MAX_NORMAL_LUMA = 0.7f;
+ private const float TARGET_MUTES_SATURATION = 0.3f;
+ private const float MAX_MUTES_SATURATION = 0.3f;
+ private const float TARGET_VIBRANT_SATURATION = 1.0f;
+ private const float MIN_VIBRANT_SATURATION = 0.35f;
+ private const float WEIGHT_SATURATION = 3f;
+ private const float WEIGHT_LUMA = 5f;
+
+ private static float GetTargetLuma(ColorType colorType) => colorType switch
+ {
+ ColorType.Vibrant => TARGET_NORMAL_LUMA,
+ ColorType.LightVibrant => TARGET_LIGHT_LUMA,
+ ColorType.DarkVibrant => TARGET_DARK_LUMA,
+ ColorType.Muted => TARGET_NORMAL_LUMA,
+ ColorType.LightMuted => TARGET_LIGHT_LUMA,
+ ColorType.DarkMuted => TARGET_DARK_LUMA,
+ _ => throw new ArgumentException(nameof(colorType))
+ };
+
+ private static float GetMinLuma(ColorType colorType) => colorType switch
+ {
+ ColorType.Vibrant => MIN_NORMAL_LUMA,
+ ColorType.LightVibrant => MIN_LIGHT_LUMA,
+ ColorType.DarkVibrant => 0f,
+ ColorType.Muted => MIN_NORMAL_LUMA,
+ ColorType.LightMuted => MIN_LIGHT_LUMA,
+ ColorType.DarkMuted => 0,
+ _ => throw new ArgumentException(nameof(colorType))
+ };
+
+ private static float GetMaxLuma(ColorType colorType) => colorType switch
+ {
+ ColorType.Vibrant => MAX_NORMAL_LUMA,
+ ColorType.LightVibrant => 1f,
+ ColorType.DarkVibrant => MAX_DARK_LUMA,
+ ColorType.Muted => MAX_NORMAL_LUMA,
+ ColorType.LightMuted => 1f,
+ ColorType.DarkMuted => MAX_DARK_LUMA,
+ _ => throw new ArgumentException(nameof(colorType))
+ };
+
+ private static float GetTargetSaturation(ColorType colorType) => colorType switch
+ {
+ ColorType.Vibrant => TARGET_VIBRANT_SATURATION,
+ ColorType.LightVibrant => TARGET_VIBRANT_SATURATION,
+ ColorType.DarkVibrant => TARGET_VIBRANT_SATURATION,
+ ColorType.Muted => TARGET_MUTES_SATURATION,
+ ColorType.LightMuted => TARGET_MUTES_SATURATION,
+ ColorType.DarkMuted => TARGET_MUTES_SATURATION,
+ _ => throw new ArgumentException(nameof(colorType))
+ };
+
+ private static float GetMinSaturation(ColorType colorType) => colorType switch
+ {
+ ColorType.Vibrant => MIN_VIBRANT_SATURATION,
+ ColorType.LightVibrant => MIN_VIBRANT_SATURATION,
+ ColorType.DarkVibrant => MIN_VIBRANT_SATURATION,
+ ColorType.Muted => 0,
+ ColorType.LightMuted => 0,
+ ColorType.DarkMuted => 0,
+ _ => throw new ArgumentException(nameof(colorType))
+ };
+
+ private static float GetMaxSaturation(ColorType colorType) => colorType switch
+ {
+ ColorType.Vibrant => 1f,
+ ColorType.LightVibrant => 1f,
+ ColorType.DarkVibrant => 1f,
+ ColorType.Muted => MAX_MUTES_SATURATION,
+ ColorType.LightMuted => MAX_MUTES_SATURATION,
+ ColorType.DarkMuted => MAX_MUTES_SATURATION,
+ _ => throw new ArgumentException(nameof(colorType))
+ };
+
+ #endregion
+}
diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs b/src/Artemis.Core/ColorScience/Quantization/ColorSwatch.cs
similarity index 92%
rename from src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs
rename to src/Artemis.Core/ColorScience/Quantization/ColorSwatch.cs
index b1b4561a5..16dd769b3 100644
--- a/src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs
+++ b/src/Artemis.Core/ColorScience/Quantization/ColorSwatch.cs
@@ -1,11 +1,11 @@
using SkiaSharp;
-namespace Artemis.Core.Services;
+namespace Artemis.Core.ColorScience;
///
/// Swatch containing the known useful color variations.
///
-public struct ColorSwatch
+public readonly record struct ColorSwatch
{
///
/// The component.
diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs b/src/Artemis.Core/ColorScience/Quantization/ColorType.cs
similarity index 94%
rename from src/Artemis.Core/Services/ColorQuantizer/ColorType.cs
rename to src/Artemis.Core/ColorScience/Quantization/ColorType.cs
index 81f28f99c..c3eada53f 100644
--- a/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs
+++ b/src/Artemis.Core/ColorScience/Quantization/ColorType.cs
@@ -1,4 +1,4 @@
-namespace Artemis.Core.Services;
+namespace Artemis.Core.ColorScience;
///
/// The types of relevant colors in an image.
diff --git a/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs b/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs
new file mode 100644
index 000000000..46b593ece
--- /dev/null
+++ b/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs
@@ -0,0 +1,121 @@
+using SkiaSharp;
+using System;
+using System.Buffers;
+
+namespace Artemis.Core.ColorScience;
+
+//HACK DarthAffe 17.11.2022: Sorting is a really hot path in the quantizer, therefore abstracting this into cleaner code (one method with parameter or something like that) sadly has a well measurable performance impact.
+internal static class QuantizerSort
+{
+ #region Methods
+
+ public static void SortRed(in Span colors)
+ {
+ Span counts = stackalloc int[256];
+ foreach (SKColor t in colors)
+ counts[t.Red]++;
+
+ SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length);
+
+ try
+ {
+ Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
+ Span currentBucketIndex = stackalloc int[256];
+
+ int offset = 0;
+ for (int i = 0; i < counts.Length; i++)
+ {
+ currentBucketIndex[i] = offset;
+ offset += counts[i];
+ }
+
+ foreach (SKColor color in colors)
+ {
+ int index = color.Red;
+ int bucketIndex = currentBucketIndex[index];
+ currentBucketIndex[index]++;
+ buckets[bucketIndex] = color;
+ }
+
+ buckets.CopyTo(colors);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(bucketsArray);
+ }
+ }
+
+ public static void SortGreen(in Span colors)
+ {
+ Span counts = stackalloc int[256];
+ foreach (SKColor t in colors)
+ counts[t.Green]++;
+
+ SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length);
+
+ try
+ {
+ Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
+ Span currentBucketIndex = stackalloc int[256];
+
+ int offset = 0;
+ for (int i = 0; i < counts.Length; i++)
+ {
+ currentBucketIndex[i] = offset;
+ offset += counts[i];
+ }
+
+ foreach (SKColor color in colors)
+ {
+ int index = color.Green;
+ int bucketIndex = currentBucketIndex[index];
+ currentBucketIndex[index]++;
+ buckets[bucketIndex] = color;
+ }
+
+ buckets.CopyTo(colors);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(bucketsArray);
+ }
+ }
+
+ public static void SortBlue(in Span colors)
+ {
+ Span counts = stackalloc int[256];
+ foreach (SKColor t in colors)
+ counts[t.Blue]++;
+
+ SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length);
+
+ try
+ {
+ Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
+ Span currentBucketIndex = stackalloc int[256];
+
+ int offset = 0;
+ for (int i = 0; i < counts.Length; i++)
+ {
+ currentBucketIndex[i] = offset;
+ offset += counts[i];
+ }
+
+ foreach (SKColor color in colors)
+ {
+ int index = color.Blue;
+ int bucketIndex = currentBucketIndex[index];
+ currentBucketIndex[index]++;
+ buckets[bucketIndex] = color;
+ }
+
+ buckets.CopyTo(colors);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(bucketsArray);
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs b/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs
new file mode 100644
index 000000000..72f58517d
--- /dev/null
+++ b/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs
@@ -0,0 +1,6 @@
+namespace Artemis.Core.ColorScience;
+
+internal enum SortTarget
+{
+ None, Red, Green, Blue
+}
diff --git a/src/Artemis.Core/ColorScience/Sorting/Cie94.cs b/src/Artemis.Core/ColorScience/Sorting/Cie94.cs
new file mode 100644
index 000000000..ca6ea6644
--- /dev/null
+++ b/src/Artemis.Core/ColorScience/Sorting/Cie94.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Artemis.Core.ColorScience;
+
+//HACK DarthAffe 17.11.2022: Due to the high amount of inlined code this is not supposed to be used outside the ColorSorter!
+internal static class Cie94
+{
+ const float KL = 1.0f;
+ const float K1 = 0.045f;
+ const float K2 = 0.015f;
+ const float SL = 1.0f;
+ const float KC = 1.0f;
+ const float KH = 1.0f;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ComputeDifference(in LabColor x, in LabColor y)
+ {
+ float deltaL = x.L - y.L;
+ float deltaA = x.A - y.A;
+ float deltaB = x.B - y.B;
+
+ float c1 = MathF.Sqrt(Pow2(x.A) + Pow2(x.B));
+ float c2 = MathF.Sqrt(Pow2(y.A) + Pow2(y.B));
+ float deltaC = c1 - c2;
+
+ float deltaH = Pow2(deltaA) + Pow2(deltaB) - Pow2(deltaC);
+ deltaH = deltaH < 0f ? 0f : MathF.Sqrt(deltaH);
+
+ float sc = 1.0f + K1 * c1;
+ float sh = 1.0f + K2 * c1;
+
+ float i = Pow2(deltaL / (KL * SL)) + Pow2(deltaC / (KC * sc)) + Pow2(deltaH / (KH * sh));
+
+ return i < 0f ? 0f : MathF.Sqrt(i);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float Pow2(in float x) => x * x;
+}
diff --git a/src/Artemis.Core/ColorScience/Sorting/ColorSorter.cs b/src/Artemis.Core/ColorScience/Sorting/ColorSorter.cs
new file mode 100644
index 000000000..0ff7c5d6f
--- /dev/null
+++ b/src/Artemis.Core/ColorScience/Sorting/ColorSorter.cs
@@ -0,0 +1,89 @@
+using SkiaSharp;
+using System;
+using System.Buffers;
+
+namespace Artemis.Core.ColorScience
+{
+ ///
+ /// Methods for sorting colors.
+ ///
+ public static class ColorSorter
+ {
+ private const int STACK_ALLOC_LIMIT = 1024;
+ private readonly record struct SortColor(SKColor RGB, LabColor Lab);
+
+ private static void Sort(in Span colors, Span sortColors, LabColor referenceColor)
+ {
+ for (int i = 0; i < colors.Length; i++)
+ {
+ SKColor color = colors[i];
+ sortColors[i] = new SortColor(color, new LabColor(color));
+ }
+
+ for (int i = 0; i < colors.Length; i++)
+ {
+ float closestDistance = float.MaxValue;
+ int closestIndex = -1;
+ for (int j = 0; j < sortColors.Length; j++)
+ {
+ float distance = Cie94.ComputeDifference(sortColors[j].Lab, referenceColor);
+ if (distance == 0f)
+ {
+ closestIndex = j;
+ break;
+ }
+
+ if (distance < closestDistance)
+ {
+ closestIndex = j;
+ closestDistance = distance;
+ }
+ }
+
+ SortColor closestColor = sortColors[closestIndex];
+ colors[i] = closestColor.RGB;
+ referenceColor = closestColor.Lab;
+
+ sortColors[closestIndex] = sortColors[^1];
+ sortColors = sortColors[..^1];
+ }
+ }
+
+ ///
+ /// Sorts the given colors in place, starting by the closest to the reference color.
+ ///
+ /// The span of colors to sort.
+ /// The reference color to sort from.
+ public static void Sort(in Span colors, SKColor startColor = new())
+ {
+ LabColor referenceColor = new(startColor);
+
+ if (colors.Length < STACK_ALLOC_LIMIT)
+ {
+ Span sortColors = stackalloc SortColor[colors.Length];
+ Sort(colors, sortColors, referenceColor);
+ }
+ else
+ {
+ SortColor[] sortColorArray = ArrayPool.Shared.Rent(colors.Length);
+ try
+ {
+ Span sortColors = sortColorArray.AsSpan(0, colors.Length);
+ Sort(colors, sortColors, referenceColor);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(sortColorArray);
+ }
+ }
+ }
+
+ ///
+ /// Gets the Cie94 difference between two colors.
+ ///
+ public static float GetColorDifference(in SKColor color1, in SKColor color2)
+ {
+ return Cie94.ComputeDifference(new LabColor(color1), new LabColor(color2));
+ }
+ }
+}
diff --git a/src/Artemis.Core/ColorScience/Sorting/LabColor.cs b/src/Artemis.Core/ColorScience/Sorting/LabColor.cs
new file mode 100644
index 000000000..e4e17dfde
--- /dev/null
+++ b/src/Artemis.Core/ColorScience/Sorting/LabColor.cs
@@ -0,0 +1,106 @@
+using RGB.NET.Core;
+using SkiaSharp;
+using System.Runtime.CompilerServices;
+using static System.MathF;
+
+namespace Artemis.Core.ColorScience;
+
+internal readonly struct LabColor
+{
+ #region Constants
+
+ private const float LAB_REFERENCE_X = 0.95047f;
+ private const float LAB_REFERENCE_Y = 1.0f;
+ private const float LAB_REFERENCE_Z = 1.08883f;
+
+ #endregion
+
+ #region Properties & Fields
+
+ public readonly float L;
+ public readonly float A;
+ public readonly float B;
+
+ #endregion
+
+ #region Constructors
+
+ public LabColor(SKColor color)
+ : this(color.Red.GetPercentageFromByteValue(),
+ color.Green.GetPercentageFromByteValue(),
+ color.Blue.GetPercentageFromByteValue())
+ { }
+
+ public LabColor(float r, float g, float b)
+ {
+ (L, A, B) = RgbToLab((r, g, b));
+ }
+
+ #endregion
+
+ #region Methods
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static (float x, float y, float z) RgbToXyz((float r, float g, float b) rgb)
+ {
+ (float r, float g, float b) = rgb;
+
+ if (r > 0.04045f)
+ r = Pow((r + 0.055f) / 1.055f, 2.4f);
+ else
+ r /= 12.92f;
+
+ if (g > 0.04045)
+ g = Pow((g + 0.055f) / 1.055f, 2.4f);
+ else
+ g /= 12.92f;
+
+ if (b > 0.04045)
+ b = Pow((b + 0.055f) / 1.055f, 2.4f);
+ else
+ b /= 12.92f;
+
+ float x = (r * 0.4124f) + (g * 0.3576f) + (b * 0.1805f);
+ float y = (r * 0.2126f) + (g * 0.7152f) + (b * 0.0722f);
+ float z = (r * 0.0193f) + (g * 0.1192f) + (b * 0.9505f);
+
+ return (x, y, z);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static (float l, float a, float b) XyzToLab((float x, float y, float z) xyz)
+ {
+ const float POWER = 1.0f / 3.0f;
+ const float OFFSET = 16.0f / 116.0f;
+
+ float x = xyz.x / LAB_REFERENCE_X;
+ float y = xyz.y / LAB_REFERENCE_Y;
+ float z = xyz.z / LAB_REFERENCE_Z;
+
+ if (x > 0.008856f)
+ x = Pow(x, POWER);
+ else
+ x = (7.787f * x) + OFFSET;
+
+ if (y > 0.008856f)
+ y = Pow(y, POWER);
+ else
+ y = (7.787f * y) + OFFSET;
+
+ if (z > 0.008856f)
+ z = Pow(z, POWER);
+ else
+ z = (7.787f * z) + OFFSET;
+
+ float l = (116f * y) - 16f;
+ float a = 500f * (x - y);
+ float b = 200f * (y - z);
+
+ return (l, a, b);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static (float l, float a, float b) RgbToLab((float r, float g, float b) rgb) => XyzToLab(RgbToXyz(rgb));
+
+ #endregion
+}
diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs
index f465f118e..cfb3b0551 100644
--- a/src/Artemis.Core/Constants.cs
+++ b/src/Artemis.Core/Constants.cs
@@ -62,8 +62,8 @@ public static class Constants
///
/// The current API version for plugins
///
- public static readonly int PluginApiVersion = int.Parse(CoreAssembly.GetCustomAttributes()
- .First(a => a.Key == "PluginApiVersion").Value);
+ public static readonly int PluginApiVersion = int.Parse(CoreAssembly.GetCustomAttributes().First(a => a.Key == "PluginApiVersion").Value ??
+ throw new InvalidOperationException("Cannot find PluginApiVersion metadata in assembly"));
///
/// The plugin info used by core components of Artemis
diff --git a/src/Artemis.Core/JsonConverters/ForgivingVersionConverter.cs b/src/Artemis.Core/JsonConverters/ForgivingVersionConverter.cs
index 8cfe43151..8a325f7e0 100644
--- a/src/Artemis.Core/JsonConverters/ForgivingVersionConverter.cs
+++ b/src/Artemis.Core/JsonConverters/ForgivingVersionConverter.cs
@@ -10,9 +10,9 @@ namespace Artemis.Core.JsonConverters
///
internal class ForgivingVersionConverter : VersionConverter
{
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
- object obj = base.ReadJson(reader, objectType, existingValue, serializer);
+ object? obj = base.ReadJson(reader, objectType, existingValue, serializer);
if (obj is not Version v)
return obj;
diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs
index 363a5759b..ec3496955 100644
--- a/src/Artemis.Core/Models/Profile/Folder.cs
+++ b/src/Artemis.Core/Models/Profile/Folder.cs
@@ -99,19 +99,26 @@ public sealed class Folder : RenderProfileElement
return;
}
- UpdateDisplayCondition();
- UpdateTimeline(deltaTime);
+ try
+ {
+ UpdateDisplayCondition();
+ UpdateTimeline(deltaTime);
- if (ShouldBeEnabled)
- Enable();
- else if (Timeline.IsFinished)
- Disable();
+ if (ShouldBeEnabled)
+ Enable();
+ else if (Timeline.IsFinished)
+ Disable();
- foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
- baseLayerEffect.InternalUpdate(Timeline);
+ foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
+ baseLayerEffect.InternalUpdate(Timeline);
- foreach (ProfileElement child in Children)
- child.Update(deltaTime);
+ foreach (ProfileElement child in Children)
+ child.Update(deltaTime);
+ }
+ finally
+ {
+ Timeline.ClearDelta();
+ }
}
///
@@ -224,8 +231,6 @@ public sealed class Folder : RenderProfileElement
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
-
- Timeline.ClearDelta();
}
#endregion
@@ -233,8 +238,10 @@ public sealed class Folder : RenderProfileElement
///
public override void Enable()
{
- // No checks here, effects will do their own checks to ensure they never enable twice
- // Also not enabling children, they'll enable themselves during their own Update
+ if (Enabled)
+ return;
+
+ // Not enabling children, they'll enable themselves during their own Update
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalEnable();
@@ -244,7 +251,9 @@ public sealed class Folder : RenderProfileElement
///
public override void Disable()
{
- // No checks here, effects will do their own checks to ensure they never disable twice
+ if (!Enabled)
+ return;
+
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalDisable();
diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs
index beca25a7d..a92aabc35 100644
--- a/src/Artemis.Core/Models/Profile/Layer.cs
+++ b/src/Artemis.Core/Models/Profile/Layer.cs
@@ -40,7 +40,7 @@ public sealed class Layer : RenderProfileElement
Suspended = false;
Leds = new ReadOnlyCollection(_leds);
Adapter = new LayerAdapter(this);
-
+
Initialize();
}
@@ -60,7 +60,7 @@ public sealed class Layer : RenderProfileElement
Parent = parent;
Leds = new ReadOnlyCollection(_leds);
Adapter = new LayerAdapter(this);
-
+
Load();
Initialize();
if (loadNodeScript)
@@ -370,41 +370,48 @@ public sealed class Layer : RenderProfileElement
return;
}
- UpdateDisplayCondition();
- UpdateTimeline(deltaTime);
-
- if (ShouldBeEnabled)
- Enable();
- else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any()))
- Disable();
-
- if (Timeline.Delta == TimeSpan.Zero)
- return;
-
- General.Update(Timeline);
- Transform.Update(Timeline);
- LayerBrush?.InternalUpdate(Timeline);
-
- foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
+ try
{
- if (!baseLayerEffect.Suspended)
- baseLayerEffect.InternalUpdate(Timeline);
+ UpdateDisplayCondition();
+ UpdateTimeline(deltaTime);
+
+ if (ShouldBeEnabled)
+ Enable();
+ else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any()))
+ Disable();
+
+ if (!Enabled || Timeline.Delta == TimeSpan.Zero)
+ return;
+
+ General.Update(Timeline);
+ Transform.Update(Timeline);
+ LayerBrush?.InternalUpdate(Timeline);
+
+ foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
+ {
+ if (!baseLayerEffect.Suspended)
+ baseLayerEffect.InternalUpdate(Timeline);
+ }
+
+ // Remove render copies that finished their timeline and update the rest
+ for (int index = 0; index < _renderCopies.Count; index++)
+ {
+ Layer child = _renderCopies[index];
+ if (!child.Timeline.IsFinished)
+ {
+ child.Update(deltaTime);
+ }
+ else
+ {
+ _renderCopies.Remove(child);
+ child.Dispose();
+ index--;
+ }
+ }
}
-
- // Remove render copies that finished their timeline and update the rest
- for (int index = 0; index < _renderCopies.Count; index++)
+ finally
{
- Layer child = _renderCopies[index];
- if (!child.Timeline.IsFinished)
- {
- child.Update(deltaTime);
- }
- else
- {
- _renderCopies.Remove(child);
- child.Dispose();
- index--;
- }
+ Timeline.ClearDelta();
}
}
@@ -485,8 +492,6 @@ public sealed class Layer : RenderProfileElement
{
layerPaint.DisposeSelfAndProperties();
}
-
- Timeline.ClearDelta();
}
private void RenderCopies(SKCanvas canvas, SKPointI basePosition)
@@ -498,7 +503,9 @@ public sealed class Layer : RenderProfileElement
///
public override void Enable()
{
- // No checks here, the brush and effects will do their own checks to ensure they never enable twice
+ if (Enabled)
+ return;
+
bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush");
if (!tryOrBreak)
return;
@@ -517,7 +524,9 @@ public sealed class Layer : RenderProfileElement
///
public override void Disable()
{
- // No checks here, the brush and effects will do their own checks to ensure they never disable twice
+ if (!Enabled)
+ return;
+
LayerBrush?.InternalDisable();
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalDisable();
@@ -830,11 +839,10 @@ public sealed class Layer : RenderProfileElement
General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
- if (LayerBrush != null)
+ if (LayerBrush != null && Enabled)
{
- if (!LayerBrush.Enabled)
- LayerBrush.InternalEnable();
- LayerBrush?.Update(0);
+ LayerBrush.InternalEnable();
+ LayerBrush.Update(0);
}
OnLayerBrushUpdated();
diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs
index 5534e686d..c511f9ec9 100644
--- a/src/Artemis.Core/Models/Profile/Profile.cs
+++ b/src/Artemis.Core/Models/Profile/Profile.cs
@@ -23,7 +23,9 @@ public sealed class Profile : ProfileElement
{
_scripts = new ObservableCollection();
_scriptConfigurations = new ObservableCollection();
-
+
+ Opacity = 0d;
+ ShouldDisplay = true;
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
@@ -81,6 +83,10 @@ public sealed class Profile : ProfileElement
internal List Exceptions { get; }
+ internal bool ShouldDisplay { get; set; }
+
+ internal double Opacity { get; private set; }
+
///
public override void Update(double deltaTime)
{
@@ -97,6 +103,13 @@ public sealed class Profile : ProfileElement
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdated(deltaTime);
+
+ const double OPACITY_PER_SECOND = 1;
+
+ if (ShouldDisplay && Opacity < 1)
+ Opacity = Math.Clamp(Opacity + OPACITY_PER_SECOND * deltaTime, 0d, 1d);
+ if (!ShouldDisplay && Opacity > 0)
+ Opacity = Math.Clamp(Opacity - OPACITY_PER_SECOND * deltaTime, 0d, 1d);
}
}
@@ -110,10 +123,26 @@ public sealed class Profile : ProfileElement
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
+
+ SKPaint? opacityPaint = null;
+ bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1;
+
+ if (applyOpacityLayer)
+ {
+ opacityPaint = new SKPaint();
+ opacityPaint.Color = new SKColor(0, 0, 0, (byte)(255d * Easings.CubicEaseInOut(Opacity)));
+ canvas.SaveLayer(opacityPaint);
+ }
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition, editorFocus);
+ if (applyOpacityLayer)
+ {
+ canvas.Restore();
+ opacityPaint?.Dispose();
+ }
+
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
index 02b75ce9b..9a87edc67 100644
--- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
+++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
@@ -223,6 +223,14 @@ public abstract class RenderProfileElement : ProfileElement
// Make sure the layer effect is tied to this element
layerEffect.ProfileElement = this;
+
+ // If the element is enabled, enable the effect before adding it
+ if (Enabled)
+ {
+ layerEffect.InternalEnable();
+ layerEffect.Update(0);
+ }
+
_layerEffects.Add(layerEffect);
// Update the order on the effects
@@ -246,6 +254,9 @@ public abstract class RenderProfileElement : ProfileElement
// Remove the effect from the layer
_layerEffects.Remove(layerEffect);
+ // Disable the effect after removing it
+ layerEffect.InternalDisable();
+
// Update the order on the remaining effects
OrderEffects();
OnLayerEffectsUpdated();
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs
index 3390a2863..b349acfd1 100644
--- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs
@@ -21,6 +21,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
private bool _isBeingEdited;
private bool _isMissingModule;
private bool _isSuspended;
+ private bool _fadeInAndOut;
private Module? _module;
private string _name;
@@ -160,6 +161,15 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
internal set => SetAndNotify(ref _profile, value);
}
+ ///
+ /// Gets or sets a boolean indicating whether this profile should fade in and out when enabling or disabling
+ ///
+ public bool FadeInAndOut
+ {
+ get => _fadeInAndOut;
+ set => SetAndNotify(ref _fadeInAndOut, value);
+ }
+
///
/// Gets or sets the module this profile uses
///
@@ -272,6 +282,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
IsSuspended = Entity.IsSuspended;
ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour;
HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode;
+ FadeInAndOut = Entity.FadeInAndOut;
Order = Entity.Order;
Icon.Load();
@@ -294,6 +305,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
Entity.ActivationBehaviour = (int) ActivationBehaviour;
Entity.HotkeyMode = (int) HotkeyMode;
Entity.ProfileCategoryId = Category.Entity.Id;
+ Entity.FadeInAndOut = FadeInAndOut;
Entity.Order = Order;
Icon.Save();
diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs
index 1baca1d95..025732df5 100644
--- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs
+++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs
@@ -63,7 +63,7 @@ public class ArtemisLedLayout
// Prefer a matching layout or else a default layout (that has no name)
LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts
- .OrderBy(l => l.Name == artemisDevice.LogicalLayout)
+ .OrderByDescending(l => l.Name == artemisDevice.LogicalLayout)
.ThenBy(l => l.Name == null)
.First();
diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
index 32d5775ec..b1dfbdc7b 100644
--- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
@@ -18,7 +18,6 @@ namespace Artemis.Core;
public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
{
private string? _description;
- private string? _icon;
private PluginFeature? _instance;
private Exception? _loadException;
private string _name = null!;
diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs
deleted file mode 100644
index 2e5133f18..000000000
--- a/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using SkiaSharp;
-
-namespace Artemis.Core.Services;
-
-internal class ColorCube
-{
- private readonly List _colors;
-
- internal ColorCube(IEnumerable colors)
- {
- if (colors.Count() < 2)
- {
- _colors = colors.ToList();
- return;
- }
-
- int redRange = colors.Max(c => c.Red) - colors.Min(c => c.Red);
- int greenRange = colors.Max(c => c.Green) - colors.Min(c => c.Green);
- int blueRange = colors.Max(c => c.Blue) - colors.Min(c => c.Blue);
-
- if (redRange > greenRange && redRange > blueRange)
- _colors = colors.OrderBy(a => a.Red).ToList();
- else if (greenRange > blueRange)
- _colors = colors.OrderBy(a => a.Green).ToList();
- else
- _colors = colors.OrderBy(a => a.Blue).ToList();
- }
-
- internal bool TrySplit([NotNullWhen(true)] out ColorCube? a, [NotNullWhen(true)] out ColorCube? b)
- {
- if (_colors.Count < 2)
- {
- a = null;
- b = null;
- return false;
- }
-
- int median = _colors.Count / 2;
-
- a = new ColorCube(_colors.GetRange(0, median));
- b = new ColorCube(_colors.GetRange(median, _colors.Count - median));
-
- return true;
- }
-
- internal SKColor GetAverageColor()
- {
- int r = 0, g = 0, b = 0;
-
- for (int i = 0; i < _colors.Count; i++)
- {
- r += _colors[i].Red;
- g += _colors[i].Green;
- b += _colors[i].Blue;
- }
-
- return new SKColor(
- (byte) (r / _colors.Count),
- (byte) (g / _colors.Count),
- (byte) (b / _colors.Count)
- );
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs
deleted file mode 100644
index b1cfc22d8..000000000
--- a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using SkiaSharp;
-
-namespace Artemis.Core.Services;
-
-///
-internal class ColorQuantizerService : IColorQuantizerService
-{
- private static float GetScore(SKColor color, ColorType type, bool ignoreLimits = false)
- {
- static float InvertDiff(float value, float target)
- {
- return 1 - Math.Abs(value - target);
- }
-
- color.ToHsl(out float _, out float saturation, out float luma);
- saturation /= 100f;
- luma /= 100f;
-
- if (!ignoreLimits &&
- (saturation <= GetMinSaturation(type) || saturation >= GetMaxSaturation(type)
- || luma <= GetMinLuma(type) || luma >= GetMaxLuma(type)))
- //if either saturation or luma falls outside the min-max, return the
- //lowest score possible unless we're ignoring these limits.
- return float.MinValue;
-
- float totalValue = InvertDiff(saturation, GetTargetSaturation(type)) * weightSaturation +
- InvertDiff(luma, GetTargetLuma(type)) * weightLuma;
-
- const float totalWeight = weightSaturation + weightLuma;
-
- return totalValue / totalWeight;
- }
-
- ///
- public SKColor[] Quantize(IEnumerable colors, int amount)
- {
- if ((amount & (amount - 1)) != 0)
- throw new ArgumentException("Must be power of two", nameof(amount));
-
- Queue cubes = new(amount);
- cubes.Enqueue(new ColorCube(colors));
-
- while (cubes.Count < amount)
- {
- ColorCube cube = cubes.Dequeue();
- if (cube.TrySplit(out ColorCube? a, out ColorCube? b))
- {
- cubes.Enqueue(a);
- cubes.Enqueue(b);
- }
- }
-
- return cubes.Select(c => c.GetAverageColor()).ToArray();
- }
-
- ///
- public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false)
- {
- float bestColorScore = 0;
- SKColor bestColor = SKColor.Empty;
-
- foreach (SKColor clr in colors)
- {
- float score = GetScore(clr, type, ignoreLimits);
- if (score > bestColorScore)
- {
- bestColorScore = score;
- bestColor = clr;
- }
- }
-
- return bestColor;
- }
-
- ///
- public ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false)
- {
- SKColor bestVibrantColor = SKColor.Empty;
- SKColor bestLightVibrantColor = SKColor.Empty;
- SKColor bestDarkVibrantColor = SKColor.Empty;
- SKColor bestMutedColor = SKColor.Empty;
- SKColor bestLightMutedColor = SKColor.Empty;
- SKColor bestDarkMutedColor = SKColor.Empty;
- float bestVibrantScore = 0;
- float bestLightVibrantScore = 0;
- float bestDarkVibrantScore = 0;
- float bestMutedScore = 0;
- float bestLightMutedScore = 0;
- float bestDarkMutedScore = 0;
-
- //ugly but at least we only loop through the enumerable once ¯\_(ツ)_/¯
- foreach (SKColor color in colors)
- {
- static void SetIfBetterScore(ref float bestScore, ref SKColor bestColor, SKColor newColor, ColorType type, bool ignoreLimits)
- {
- float newScore = GetScore(newColor, type, ignoreLimits);
- if (newScore > bestScore)
- {
- bestScore = newScore;
- bestColor = newColor;
- }
- }
-
- SetIfBetterScore(ref bestVibrantScore, ref bestVibrantColor, color, ColorType.Vibrant, ignoreLimits);
- SetIfBetterScore(ref bestLightVibrantScore, ref bestLightVibrantColor, color, ColorType.LightVibrant, ignoreLimits);
- SetIfBetterScore(ref bestDarkVibrantScore, ref bestDarkVibrantColor, color, ColorType.DarkVibrant, ignoreLimits);
- SetIfBetterScore(ref bestMutedScore, ref bestMutedColor, color, ColorType.Muted, ignoreLimits);
- SetIfBetterScore(ref bestLightMutedScore, ref bestLightMutedColor, color, ColorType.LightMuted, ignoreLimits);
- SetIfBetterScore(ref bestDarkMutedScore, ref bestDarkMutedColor, color, ColorType.DarkMuted, ignoreLimits);
- }
-
- return new ColorSwatch
- {
- Vibrant = bestVibrantColor,
- LightVibrant = bestLightVibrantColor,
- DarkVibrant = bestDarkVibrantColor,
- Muted = bestMutedColor,
- LightMuted = bestLightMutedColor,
- DarkMuted = bestDarkMutedColor
- };
- }
-
- #region Constants
-
- private const float targetDarkLuma = 0.26f;
- private const float maxDarkLuma = 0.45f;
- private const float minLightLuma = 0.55f;
- private const float targetLightLuma = 0.74f;
- private const float minNormalLuma = 0.3f;
- private const float targetNormalLuma = 0.5f;
- private const float maxNormalLuma = 0.7f;
- private const float targetMutesSaturation = 0.3f;
- private const float maxMutesSaturation = 0.3f;
- private const float targetVibrantSaturation = 1.0f;
- private const float minVibrantSaturation = 0.35f;
- private const float weightSaturation = 3f;
- private const float weightLuma = 5f;
-
- private static float GetTargetLuma(ColorType colorType)
- {
- return colorType switch
- {
- ColorType.Vibrant => targetNormalLuma,
- ColorType.LightVibrant => targetLightLuma,
- ColorType.DarkVibrant => targetDarkLuma,
- ColorType.Muted => targetNormalLuma,
- ColorType.LightMuted => targetLightLuma,
- ColorType.DarkMuted => targetDarkLuma,
- _ => throw new ArgumentException(nameof(colorType))
- };
- }
-
- private static float GetMinLuma(ColorType colorType)
- {
- return colorType switch
- {
- ColorType.Vibrant => minNormalLuma,
- ColorType.LightVibrant => minLightLuma,
- ColorType.DarkVibrant => 0f,
- ColorType.Muted => minNormalLuma,
- ColorType.LightMuted => minLightLuma,
- ColorType.DarkMuted => 0,
- _ => throw new ArgumentException(nameof(colorType))
- };
- }
-
- private static float GetMaxLuma(ColorType colorType)
- {
- return colorType switch
- {
- ColorType.Vibrant => maxNormalLuma,
- ColorType.LightVibrant => 1f,
- ColorType.DarkVibrant => maxDarkLuma,
- ColorType.Muted => maxNormalLuma,
- ColorType.LightMuted => 1f,
- ColorType.DarkMuted => maxDarkLuma,
- _ => throw new ArgumentException(nameof(colorType))
- };
- }
-
- private static float GetTargetSaturation(ColorType colorType)
- {
- return colorType switch
- {
- ColorType.Vibrant => targetVibrantSaturation,
- ColorType.LightVibrant => targetVibrantSaturation,
- ColorType.DarkVibrant => targetVibrantSaturation,
- ColorType.Muted => targetMutesSaturation,
- ColorType.LightMuted => targetMutesSaturation,
- ColorType.DarkMuted => targetMutesSaturation,
- _ => throw new ArgumentException(nameof(colorType))
- };
- }
-
- private static float GetMinSaturation(ColorType colorType)
- {
- return colorType switch
- {
- ColorType.Vibrant => minVibrantSaturation,
- ColorType.LightVibrant => minVibrantSaturation,
- ColorType.DarkVibrant => minVibrantSaturation,
- ColorType.Muted => 0,
- ColorType.LightMuted => 0,
- ColorType.DarkMuted => 0,
- _ => throw new ArgumentException(nameof(colorType))
- };
- }
-
- private static float GetMaxSaturation(ColorType colorType)
- {
- return colorType switch
- {
- ColorType.Vibrant => 1f,
- ColorType.LightVibrant => 1f,
- ColorType.DarkVibrant => 1f,
- ColorType.Muted => maxMutesSaturation,
- ColorType.LightMuted => maxMutesSaturation,
- ColorType.DarkMuted => maxMutesSaturation,
- _ => throw new ArgumentException(nameof(colorType))
- };
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs
deleted file mode 100644
index e363fe6d9..000000000
--- a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Collections.Generic;
-using SkiaSharp;
-
-namespace Artemis.Core.Services;
-
-///
-/// A service providing a pallette of colors in a bitmap based on vibrant.js
-///
-public interface IColorQuantizerService : IArtemisService
-{
- ///
- /// Reduces an to a given amount of relevant colors. Based on the Median Cut
- /// algorithm
- ///
- /// The colors to quantize.
- /// The number of colors that should be calculated. Must be a power of two.
- /// The quantized colors.
- public SKColor[] Quantize(IEnumerable colors, int amount);
-
- ///
- /// Finds colors with certain characteristics in a given .
- ///
- /// Vibrant variants are more saturated, while Muted colors are less.
- ///
- /// Light and Dark colors have higher and lower lightness values, respectively.
- ///
- /// The colors to find the variations in
- /// Which type of color to find
- ///
- /// Ignore hard limits on whether a color is considered for each category. Result may be
- /// if this is false
- ///
- /// The color found
- public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false);
-
- ///
- /// Finds all the color variations available and returns a struct containing them all.
- ///
- /// The colors to find the variations in
- ///
- /// Ignore hard limits on whether a color is considered for each category. Some colors may be
- /// if this is false
- ///
- /// A swatch containing all color variations
- public ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false);
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/Input/InputProvider.cs b/src/Artemis.Core/Services/Input/InputProvider.cs
index c51a25d63..8db4dec76 100644
--- a/src/Artemis.Core/Services/Input/InputProvider.cs
+++ b/src/Artemis.Core/Services/Input/InputProvider.cs
@@ -8,7 +8,10 @@ namespace Artemis.Core.Services;
///
public abstract class InputProvider : IDisposable
{
- public InputProvider()
+ ///
+ /// Creates a new instance of the class.
+ ///
+ protected InputProvider()
{
ProviderName = GetType().FullName ?? throw new InvalidOperationException("Input provider must have a type with a name");
}
diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs
index d5adc7d87..4b86f3d55 100644
--- a/src/Artemis.Core/Services/Storage/ProfileService.cs
+++ b/src/Artemis.Core/Services/Storage/ProfileService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
@@ -213,8 +213,17 @@ internal class ProfileService : IProfileService
// Make sure the profile is active or inactive according to the parameters above
if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile")
profileConfiguration.TryOrBreak(() => ActivateProfile(profileConfiguration), "Failed to activate profile");
+ if (shouldBeActive && profileConfiguration.Profile != null && !profileConfiguration.Profile.ShouldDisplay)
+ profileConfiguration.Profile.ShouldDisplay = true;
else if (!shouldBeActive && profileConfiguration.Profile != null)
- DeactivateProfile(profileConfiguration);
+ {
+ if (!profileConfiguration.FadeInAndOut)
+ DeactivateProfile(profileConfiguration);
+ else if (!profileConfiguration.Profile.ShouldDisplay && profileConfiguration.Profile.Opacity <= 0)
+ DeactivateProfile(profileConfiguration);
+ else if (profileConfiguration.Profile.Opacity > 0)
+ RequestDeactivation(profileConfiguration);
+ }
profileConfiguration.Profile?.Update(deltaTime);
}
@@ -254,7 +263,8 @@ internal class ProfileService : IProfileService
{
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
// Ensure all criteria are met before rendering
- if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && profileConfiguration.ActivationConditionMet)
+ bool fadingOut = profileConfiguration.Profile?.ShouldDisplay == false && profileConfiguration.Profile?.Opacity > 0;
+ if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && (profileConfiguration.ActivationConditionMet || fadingOut))
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty, null);
}
catch (Exception e)
@@ -316,7 +326,10 @@ internal class ProfileService : IProfileService
public Profile ActivateProfile(ProfileConfiguration profileConfiguration)
{
if (profileConfiguration.Profile != null)
+ {
+ profileConfiguration.Profile.ShouldDisplay = true;
return profileConfiguration.Profile;
+ }
ProfileEntity profileEntity;
try
@@ -361,6 +374,16 @@ internal class ProfileService : IProfileService
OnProfileDeactivated(new ProfileConfigurationEventArgs(profileConfiguration));
}
+ public void RequestDeactivation(ProfileConfiguration profileConfiguration)
+ {
+ if (profileConfiguration.IsBeingEdited)
+ throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude");
+ if (profileConfiguration.Profile == null)
+ return;
+
+ profileConfiguration.Profile.ShouldDisplay = false;
+ }
+
public void DeleteProfile(ProfileConfiguration profileConfiguration)
{
DeactivateProfile(profileConfiguration);
diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs
index dce2df631..3bc2521fb 100644
--- a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs
+++ b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs
@@ -97,7 +97,7 @@ public interface INode : INotifyPropertyChanged, IBreakableModel
void TryEvaluate();
///
- /// Resets the node causing all pins to re-evaluate the next time is called
+ /// Resets the node causing all pins to re-evaluate the next time is called
///
void Reset();
}
\ No newline at end of file
diff --git a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs
index 377d78ce7..e91ddcc96 100644
--- a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs
+++ b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs
@@ -25,4 +25,6 @@ public class ProfileConfigurationEntity
public Guid ProfileCategoryId { get; set; }
public Guid ProfileId { get; set; }
+
+ public bool FadeInAndOut { get; set; }
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj
index 66fe3e2c7..09c2d5479 100644
--- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj
+++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs
index c3fd4fc6e..1632f25a7 100644
--- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs
+++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs
@@ -317,7 +317,7 @@ public class DeviceVisualizer : Control
Dispatcher.UIThread.Post(InvalidateMeasure);
}
- catch (Exception e)
+ catch (Exception)
{
// ignored
}
diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs
index a1df2555c..029a6dc18 100644
--- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs
+++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs
@@ -18,7 +18,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared;
///
public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable
{
- private const int MaxDepth = 4;
+ private const int MAX_DEPTH = 4;
private ObservableCollection _children;
private DataModel? _dataModel;
private bool _isMatchingFilteredTypes;
@@ -47,6 +47,9 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription;
}
+ ///
+ /// Copies the path of the data model to the clipboard.
+ ///
public ReactiveCommand CopyPath { get; }
///
@@ -337,7 +340,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
{
if (DataModel == null)
throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model");
- if (depth > MaxDepth)
+ if (depth > MAX_DEPTH)
return null;
DataModelPath dataModelPath = new(DataModel, path);
diff --git a/src/Artemis.UI.Shared/ReactiveCoreWindow.cs b/src/Artemis.UI.Shared/ReactiveCoreWindow.cs
index 60fd8f353..295f4e6bb 100644
--- a/src/Artemis.UI.Shared/ReactiveCoreWindow.cs
+++ b/src/Artemis.UI.Shared/ReactiveCoreWindow.cs
@@ -19,6 +19,9 @@ namespace Artemis.UI.Shared;
/// ViewModel type.
public class ReactiveCoreWindow : CoreWindow, IViewFor where TViewModel : class
{
+ ///
+ /// The ViewModel.
+ ///
public static readonly StyledProperty ViewModelProperty = AvaloniaProperty
.Register, TViewModel?>(nameof(ViewModel));
diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateColorGradient.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateColorGradient.cs
index 5970b652b..58f91f4bd 100644
--- a/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateColorGradient.cs
+++ b/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateColorGradient.cs
@@ -5,7 +5,7 @@ using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
///
-/// Represents a profile editor command that can be used to update a layer property of type .
+/// Represents a profile editor command that can be used to update a color gradient.
///
public class UpdateColorGradient : IProfileEditorCommand
{
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index c568a677c..04e578c41 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -29,8 +29,8 @@
-
-
+
+
diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml
index 07d3faeab..ad358c205 100644
--- a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml
+++ b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml
@@ -13,8 +13,8 @@
Value="{CompiledBinding Start}"
Prefix="{CompiledBinding Prefix}"
Suffix="{CompiledBinding Affix}"
- Minimum="{CompiledBinding End}"
- Maximum="{CompiledBinding Max}"
+ Minimum="{CompiledBinding Min}"
+ Maximum="{CompiledBinding End}"
LargeChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
SimpleNumberFormat="F3"
VerticalAlignment="Center"
diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml b/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml
index 512006830..96ded4c7c 100644
--- a/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml
+++ b/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml
@@ -13,8 +13,8 @@
Value="{CompiledBinding Start}"
Prefix="{CompiledBinding Prefix}"
Suffix="{CompiledBinding Affix}"
- Minimum="{CompiledBinding End}"
- Maximum="{CompiledBinding Max}"
+ Minimum="{CompiledBinding Min}"
+ Maximum="{CompiledBinding End}"
LargeChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
SimpleNumberFormat="F3"
VerticalAlignment="Center"
diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs
index 10481f25c..b9175e9ed 100644
--- a/src/Artemis.UI/Screens/Root/RootViewModel.cs
+++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs
@@ -92,7 +92,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
private void CurrentMainWindowOnClosing(object? sender, EventArgs e)
{
- WindowSizeSetting.Save();
+ WindowSizeSetting?.Save();
_lifeTime.MainWindow = null;
SidebarViewModel = null;
Router.NavigateAndReset.Execute(new EmptyViewModel(this, "blank")).Subscribe();
diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml
index fed01edac..a127cdc14 100644
--- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml
+++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml
@@ -127,6 +127,9 @@
+
+ Fade when enabling and disabling
+
diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs
index ed55d14c3..eaaf00480 100644
--- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs
@@ -30,6 +30,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase? _materialIcons;
@@ -57,6 +58,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase RaiseAndSetIfChanged(ref _disableHotkey, value);
}
+ public bool FadeInAndOut
+ {
+ get => _fadeInAndOut;
+ set => RaiseAndSetIfChanged(ref _fadeInAndOut, value);
+ }
+
public ObservableCollection Modules { get; }
public ProfileModuleViewModel? SelectedModule
@@ -131,7 +139,6 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase OpenConditionEditor { get; }
public ReactiveCommand BrowseBitmapFile { get; }
public ReactiveCommand Confirm { get; }
- public ReactiveCommand Import { get; }
public ReactiveCommand Delete { get; }
public ReactiveCommand Cancel { get; }
@@ -155,6 +162,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase Output { get; }
+
+ public InputPin Frequency1 { get; }
+ public InputPin Frequency2 { get; }
+ public InputPin Frequency3 { get; }
+ public InputPin Phase1 { get; }
+ public InputPin Phase2 { get; }
+ public InputPin Phase3 { get; }
+ public InputPin Center { get; }
+ public InputPin Width { get; }
+ public InputPin Length { get; }
+
+ public GradientBuilderNode()
+ {
+ Output = CreateOutputPin();
+ Frequency1 = CreateInputPin("Frequency 1");
+ Frequency2 = CreateInputPin("Frequency 2");
+ Frequency3 = CreateInputPin("Frequency 3");
+ Phase1 = CreateInputPin("Phase 1");
+ Phase2 = CreateInputPin("Phase 2");
+ Phase3 = CreateInputPin("Phase 3");
+ Center = CreateInputPin("Center");
+ Width = CreateInputPin("Width");
+ Length = CreateInputPin("Length");
+ }
+
+ public override void Evaluate()
+ {
+ ColorGradient gradient = new ColorGradient();
+
+ for (int i = 0; i < Length.Value; i++)
+ {
+ Numeric r = Math.Sin(Frequency1.Value * i + Phase1.Value) * Width.Value + Center.Value;
+ Numeric g = Math.Sin(Frequency2.Value * i + Phase2.Value) * Width.Value + Center.Value;
+ Numeric b = Math.Sin(Frequency3.Value * i + Phase3.Value) * Width.Value + Center.Value;
+ gradient.Add(new ColorGradientStop(new SKColor((byte)r, (byte)g, (byte)b), i / Length.Value));
+ }
+
+ Output.Value = gradient;
+ }
+ }
+}
diff --git a/src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs b/src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs
new file mode 100644
index 000000000..eea7329fe
--- /dev/null
+++ b/src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs
@@ -0,0 +1,45 @@
+using Artemis.Core;
+using Artemis.Core.ColorScience;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Artemis.VisualScripting.Nodes.Color
+{
+ [Node("Sorted Gradient", "Generates a sorted gradient from the given colors", "Color", InputType = typeof(SKColor), OutputType = typeof(ColorGradient))]
+ public class SortedGradientNode : Node
+ {
+ public InputPinCollection Inputs { get; }
+ public OutputPin Output { get; }
+
+ public SortedGradientNode()
+ {
+ Inputs = CreateInputPinCollection();
+ Output = CreateOutputPin();
+ }
+
+ public override void Evaluate()
+ {
+ SKColor[] colors = Inputs.Values.ToArray();
+
+ if (colors.Length == 0)
+ {
+ Output.Value = null;
+ return;
+ }
+
+ ColorSorter.Sort(colors, SKColors.Black);
+
+ ColorGradient gradient = new();
+ for (int i = 0; i < colors.Length; i++)
+ {
+ gradient.Add(new ColorGradientStop(colors[i], (float)i / (colors.Length - 1)));
+ }
+
+ Output.Value = gradient;
+ }
+ }
+}
diff --git a/src/Artemis.VisualScripting/Nodes/External/LayerPropertyNode.cs b/src/Artemis.VisualScripting/Nodes/External/LayerPropertyNode.cs
index 72fff998a..5a2199c05 100644
--- a/src/Artemis.VisualScripting/Nodes/External/LayerPropertyNode.cs
+++ b/src/Artemis.VisualScripting/Nodes/External/LayerPropertyNode.cs
@@ -117,7 +117,7 @@ public class LayerPropertyNode : Node
- private void CreateOrAddOutputPin(Type valueType, string displayName)
+ private new void CreateOrAddOutputPin(Type valueType, string displayName)
{
// Grab the first pin from the bucket that isn't on the node yet
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
diff --git a/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs b/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs
index 90f4f83eb..3631e5934 100644
--- a/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs
+++ b/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs
@@ -9,7 +9,7 @@ namespace Artemis.VisualScripting.Nodes.List;
public class ListOperatorPredicateNode : Node, IDisposable
{
private readonly object _scriptLock = new();
- private ListOperatorPredicateStartNode _startNode;
+ private readonly ListOperatorPredicateStartNode _startNode;
public ListOperatorPredicateNode()
{
@@ -65,7 +65,7 @@ public class ListOperatorPredicateNode : Node