mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'development'
This commit is contained in:
commit
fd22176dbe
@ -44,9 +44,9 @@
|
|||||||
<PackageReference Include="Ninject" Version="3.3.6" />
|
<PackageReference Include="Ninject" Version="3.3.6" />
|
||||||
<PackageReference Include="Ninject.Extensions.ChildKernel" Version="3.3.0" />
|
<PackageReference Include="Ninject.Extensions.ChildKernel" Version="3.3.0" />
|
||||||
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
|
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
|
||||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease.46" />
|
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
||||||
<PackageReference Include="RGB.NET.Layout" Version="1.0.0-prerelease.46" />
|
<PackageReference Include="RGB.NET.Layout" Version="1.0.0" />
|
||||||
<PackageReference Include="RGB.NET.Presets" Version="1.0.0-prerelease.46" />
|
<PackageReference Include="RGB.NET.Presets" Version="1.0.0" />
|
||||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=colorscience_005Cquantization/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=colorscience_005Csorting/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=defaulttypes/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=defaulttypes/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cnodes/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cnodes/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofileconfiguration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofileconfiguration/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|||||||
167
src/Artemis.Core/ColorScience/Quantization/ColorCube.cs
Normal file
167
src/Artemis.Core/ColorScience/Quantization/ColorCube.cs
Normal file
@ -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<byte>.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<SKColor> fullColorList, int from, int length, SortTarget preOrdered)
|
||||||
|
{
|
||||||
|
this._from = from;
|
||||||
|
this._length = length;
|
||||||
|
|
||||||
|
if (length < 2) return;
|
||||||
|
|
||||||
|
Span<SKColor> 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<SKColor> colors)
|
||||||
|
{
|
||||||
|
if (Vector.IsHardwareAccelerated && (colors.Length >= Vector<byte>.Count))
|
||||||
|
{
|
||||||
|
int chunks = colors.Length / ELEMENTS_PER_VECTOR;
|
||||||
|
int vectorElements = (chunks * ELEMENTS_PER_VECTOR);
|
||||||
|
int missingElements = colors.Length - vectorElements;
|
||||||
|
|
||||||
|
Vector<byte> max = Vector<byte>.Zero;
|
||||||
|
Vector<byte> min = new(byte.MaxValue);
|
||||||
|
foreach (Vector<byte> currentVector in MemoryMarshal.Cast<SKColor, Vector<byte>>(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<SKColor> fullColorList, out ColorCube a, out ColorCube b)
|
||||||
|
{
|
||||||
|
Span<SKColor> 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<SKColor> fullColorList)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<SKColor> 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
285
src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs
Normal file
285
src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Artemis.Core.ColorScience;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for color quantization.
|
||||||
|
/// </summary>
|
||||||
|
public static class ColorQuantizer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Quantizes a span of colors into the desired amount of representative colors.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="colors">The colors to quantize</param>
|
||||||
|
/// <param name="amount">How many colors to return. Must be a power of two.</param>
|
||||||
|
/// <returns><paramref name="amount"/> colors.</returns>
|
||||||
|
public static SKColor[] Quantize(in Span<SKColor> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quantizes a span of colors, splitting the average <paramref name="splits"/> number of times.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="colors">The colors to quantize</param>
|
||||||
|
/// <param name="splits">How many splits to execute. Each split doubles the number of colors returned.</param>
|
||||||
|
/// <returns>Up to (2 ^ <paramref name="splits"/>) number of colors.</returns>
|
||||||
|
public static SKColor[] QuantizeSplit(in Span<SKColor> 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<ColorCube> 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<ColorCube> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds colors with certain characteristics in a given <see cref="IEnumerable{SKColor}" />.
|
||||||
|
/// <para />
|
||||||
|
/// Vibrant variants are more saturated, while Muted colors are less.
|
||||||
|
/// <para />
|
||||||
|
/// Light and Dark colors have higher and lower lightness values, respectively.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="colors">The colors to find the variations in</param>
|
||||||
|
/// <param name="type">Which type of color to find</param>
|
||||||
|
/// <param name="ignoreLimits">
|
||||||
|
/// Ignore hard limits on whether a color is considered for each category. Result may be
|
||||||
|
/// <see cref="SKColor.Empty" /> if this is false
|
||||||
|
/// </param>
|
||||||
|
/// <returns>The color found</returns>
|
||||||
|
public static SKColor FindColorVariation(IEnumerable<SKColor> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds all the color variations available and returns a struct containing them all.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="colors">The colors to find the variations in</param>
|
||||||
|
/// <param name="ignoreLimits">
|
||||||
|
/// Ignore hard limits on whether a color is considered for each category. Some colors may be
|
||||||
|
/// <see cref="SKColor.Empty" /> if this is false
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A swatch containing all color variations</returns>
|
||||||
|
public static ColorSwatch FindAllColorVariations(IEnumerable<SKColor> 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a gradient from a given image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bitmap">The image to process</param>
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.ColorScience;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Swatch containing the known useful color variations.
|
/// Swatch containing the known useful color variations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct ColorSwatch
|
public readonly record struct ColorSwatch
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="ColorType.Vibrant" /> component.
|
/// The <see cref="ColorType.Vibrant" /> component.
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.ColorScience;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The types of relevant colors in an image.
|
/// The types of relevant colors in an image.
|
||||||
121
src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs
Normal file
121
src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs
Normal file
@ -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<SKColor> colors)
|
||||||
|
{
|
||||||
|
Span<int> counts = stackalloc int[256];
|
||||||
|
foreach (SKColor t in colors)
|
||||||
|
counts[t.Red]++;
|
||||||
|
|
||||||
|
SKColor[] bucketsArray = ArrayPool<SKColor>.Shared.Rent(colors.Length);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Span<SKColor> buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
|
||||||
|
Span<int> 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<SKColor>.Shared.Return(bucketsArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SortGreen(in Span<SKColor> colors)
|
||||||
|
{
|
||||||
|
Span<int> counts = stackalloc int[256];
|
||||||
|
foreach (SKColor t in colors)
|
||||||
|
counts[t.Green]++;
|
||||||
|
|
||||||
|
SKColor[] bucketsArray = ArrayPool<SKColor>.Shared.Rent(colors.Length);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Span<SKColor> buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
|
||||||
|
Span<int> 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<SKColor>.Shared.Return(bucketsArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SortBlue(in Span<SKColor> colors)
|
||||||
|
{
|
||||||
|
Span<int> counts = stackalloc int[256];
|
||||||
|
foreach (SKColor t in colors)
|
||||||
|
counts[t.Blue]++;
|
||||||
|
|
||||||
|
SKColor[] bucketsArray = ArrayPool<SKColor>.Shared.Rent(colors.Length);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Span<SKColor> buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
|
||||||
|
Span<int> 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<SKColor>.Shared.Return(bucketsArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
6
src/Artemis.Core/ColorScience/Quantization/SortTarget.cs
Normal file
6
src/Artemis.Core/ColorScience/Quantization/SortTarget.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Artemis.Core.ColorScience;
|
||||||
|
|
||||||
|
internal enum SortTarget
|
||||||
|
{
|
||||||
|
None, Red, Green, Blue
|
||||||
|
}
|
||||||
40
src/Artemis.Core/ColorScience/Sorting/Cie94.cs
Normal file
40
src/Artemis.Core/ColorScience/Sorting/Cie94.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
89
src/Artemis.Core/ColorScience/Sorting/ColorSorter.cs
Normal file
89
src/Artemis.Core/ColorScience/Sorting/ColorSorter.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
|
||||||
|
namespace Artemis.Core.ColorScience
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Methods for sorting colors.
|
||||||
|
/// </summary>
|
||||||
|
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<SKColor> colors, Span<SortColor> 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorts the given colors in place, starting by the closest to the reference color.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="colors">The span of colors to sort.</param>
|
||||||
|
/// <param name="startColor">The reference color to sort from.</param>
|
||||||
|
public static void Sort(in Span<SKColor> colors, SKColor startColor = new())
|
||||||
|
{
|
||||||
|
LabColor referenceColor = new(startColor);
|
||||||
|
|
||||||
|
if (colors.Length < STACK_ALLOC_LIMIT)
|
||||||
|
{
|
||||||
|
Span<SortColor> sortColors = stackalloc SortColor[colors.Length];
|
||||||
|
Sort(colors, sortColors, referenceColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SortColor[] sortColorArray = ArrayPool<SortColor>.Shared.Rent(colors.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Span<SortColor> sortColors = sortColorArray.AsSpan(0, colors.Length);
|
||||||
|
Sort(colors, sortColors, referenceColor);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<SortColor>.Shared.Return(sortColorArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Cie94 difference between two colors.
|
||||||
|
/// </summary>
|
||||||
|
public static float GetColorDifference(in SKColor color1, in SKColor color2)
|
||||||
|
{
|
||||||
|
return Cie94.ComputeDifference(new LabColor(color1), new LabColor(color2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/Artemis.Core/ColorScience/Sorting/LabColor.cs
Normal file
106
src/Artemis.Core/ColorScience/Sorting/LabColor.cs
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -62,8 +62,8 @@ public static class Constants
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current API version for plugins
|
/// The current API version for plugins
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly int PluginApiVersion = int.Parse(CoreAssembly.GetCustomAttributes<AssemblyMetadataAttribute>()
|
public static readonly int PluginApiVersion = int.Parse(CoreAssembly.GetCustomAttributes<AssemblyMetadataAttribute>().First(a => a.Key == "PluginApiVersion").Value ??
|
||||||
.First(a => a.Key == "PluginApiVersion").Value);
|
throw new InvalidOperationException("Cannot find PluginApiVersion metadata in assembly"));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The plugin info used by core components of Artemis
|
/// The plugin info used by core components of Artemis
|
||||||
|
|||||||
@ -10,9 +10,9 @@ namespace Artemis.Core.JsonConverters
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ForgivingVersionConverter : VersionConverter
|
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)
|
if (obj is not Version v)
|
||||||
return obj;
|
return obj;
|
||||||
|
|
||||||
|
|||||||
@ -99,19 +99,26 @@ public sealed class Folder : RenderProfileElement
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDisplayCondition();
|
try
|
||||||
UpdateTimeline(deltaTime);
|
{
|
||||||
|
UpdateDisplayCondition();
|
||||||
|
UpdateTimeline(deltaTime);
|
||||||
|
|
||||||
if (ShouldBeEnabled)
|
if (ShouldBeEnabled)
|
||||||
Enable();
|
Enable();
|
||||||
else if (Timeline.IsFinished)
|
else if (Timeline.IsFinished)
|
||||||
Disable();
|
Disable();
|
||||||
|
|
||||||
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
|
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
|
||||||
baseLayerEffect.InternalUpdate(Timeline);
|
baseLayerEffect.InternalUpdate(Timeline);
|
||||||
|
|
||||||
foreach (ProfileElement child in Children)
|
foreach (ProfileElement child in Children)
|
||||||
child.Update(deltaTime);
|
child.Update(deltaTime);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Timeline.ClearDelta();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -224,8 +231,6 @@ public sealed class Folder : RenderProfileElement
|
|||||||
canvas.Restore();
|
canvas.Restore();
|
||||||
layerPaint.DisposeSelfAndProperties();
|
layerPaint.DisposeSelfAndProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
Timeline.ClearDelta();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -233,8 +238,10 @@ public sealed class Folder : RenderProfileElement
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Enable()
|
public override void Enable()
|
||||||
{
|
{
|
||||||
// No checks here, effects will do their own checks to ensure they never enable twice
|
if (Enabled)
|
||||||
// Also not enabling children, they'll enable themselves during their own Update
|
return;
|
||||||
|
|
||||||
|
// Not enabling children, they'll enable themselves during their own Update
|
||||||
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
||||||
baseLayerEffect.InternalEnable();
|
baseLayerEffect.InternalEnable();
|
||||||
|
|
||||||
@ -244,7 +251,9 @@ public sealed class Folder : RenderProfileElement
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Disable()
|
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)
|
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
||||||
baseLayerEffect.InternalDisable();
|
baseLayerEffect.InternalDisable();
|
||||||
|
|
||||||
|
|||||||
@ -370,41 +370,48 @@ public sealed class Layer : RenderProfileElement
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDisplayCondition();
|
try
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (!baseLayerEffect.Suspended)
|
UpdateDisplayCondition();
|
||||||
baseLayerEffect.InternalUpdate(Timeline);
|
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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
// Remove render copies that finished their timeline and update the rest
|
|
||||||
for (int index = 0; index < _renderCopies.Count; index++)
|
|
||||||
{
|
{
|
||||||
Layer child = _renderCopies[index];
|
Timeline.ClearDelta();
|
||||||
if (!child.Timeline.IsFinished)
|
|
||||||
{
|
|
||||||
child.Update(deltaTime);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_renderCopies.Remove(child);
|
|
||||||
child.Dispose();
|
|
||||||
index--;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,8 +492,6 @@ public sealed class Layer : RenderProfileElement
|
|||||||
{
|
{
|
||||||
layerPaint.DisposeSelfAndProperties();
|
layerPaint.DisposeSelfAndProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
Timeline.ClearDelta();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RenderCopies(SKCanvas canvas, SKPointI basePosition)
|
private void RenderCopies(SKCanvas canvas, SKPointI basePosition)
|
||||||
@ -498,7 +503,9 @@ public sealed class Layer : RenderProfileElement
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Enable()
|
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");
|
bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush");
|
||||||
if (!tryOrBreak)
|
if (!tryOrBreak)
|
||||||
return;
|
return;
|
||||||
@ -517,7 +524,9 @@ public sealed class Layer : RenderProfileElement
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Disable()
|
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();
|
LayerBrush?.InternalDisable();
|
||||||
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
||||||
baseLayerEffect.InternalDisable();
|
baseLayerEffect.InternalDisable();
|
||||||
@ -830,11 +839,10 @@ public sealed class Layer : RenderProfileElement
|
|||||||
General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
|
General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
|
||||||
General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
|
General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
|
||||||
Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
|
Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
|
||||||
if (LayerBrush != null)
|
if (LayerBrush != null && Enabled)
|
||||||
{
|
{
|
||||||
if (!LayerBrush.Enabled)
|
LayerBrush.InternalEnable();
|
||||||
LayerBrush.InternalEnable();
|
LayerBrush.Update(0);
|
||||||
LayerBrush?.Update(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OnLayerBrushUpdated();
|
OnLayerBrushUpdated();
|
||||||
|
|||||||
@ -24,6 +24,8 @@ public sealed class Profile : ProfileElement
|
|||||||
_scripts = new ObservableCollection<ProfileScript>();
|
_scripts = new ObservableCollection<ProfileScript>();
|
||||||
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
|
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
|
||||||
|
|
||||||
|
Opacity = 0d;
|
||||||
|
ShouldDisplay = true;
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
Profile = this;
|
Profile = this;
|
||||||
ProfileEntity = profileEntity;
|
ProfileEntity = profileEntity;
|
||||||
@ -81,6 +83,10 @@ public sealed class Profile : ProfileElement
|
|||||||
|
|
||||||
internal List<Exception> Exceptions { get; }
|
internal List<Exception> Exceptions { get; }
|
||||||
|
|
||||||
|
internal bool ShouldDisplay { get; set; }
|
||||||
|
|
||||||
|
internal double Opacity { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Update(double deltaTime)
|
public override void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
@ -97,6 +103,13 @@ public sealed class Profile : ProfileElement
|
|||||||
|
|
||||||
foreach (ProfileScript profileScript in Scripts)
|
foreach (ProfileScript profileScript in Scripts)
|
||||||
profileScript.OnProfileUpdated(deltaTime);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,9 +124,25 @@ public sealed class Profile : ProfileElement
|
|||||||
foreach (ProfileScript profileScript in Scripts)
|
foreach (ProfileScript profileScript in Scripts)
|
||||||
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
|
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)
|
foreach (ProfileElement profileElement in Children)
|
||||||
profileElement.Render(canvas, basePosition, editorFocus);
|
profileElement.Render(canvas, basePosition, editorFocus);
|
||||||
|
|
||||||
|
if (applyOpacityLayer)
|
||||||
|
{
|
||||||
|
canvas.Restore();
|
||||||
|
opacityPaint?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (ProfileScript profileScript in Scripts)
|
foreach (ProfileScript profileScript in Scripts)
|
||||||
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
|
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
|
||||||
|
|
||||||
|
|||||||
@ -223,6 +223,14 @@ public abstract class RenderProfileElement : ProfileElement
|
|||||||
|
|
||||||
// Make sure the layer effect is tied to this element
|
// Make sure the layer effect is tied to this element
|
||||||
layerEffect.ProfileElement = this;
|
layerEffect.ProfileElement = this;
|
||||||
|
|
||||||
|
// If the element is enabled, enable the effect before adding it
|
||||||
|
if (Enabled)
|
||||||
|
{
|
||||||
|
layerEffect.InternalEnable();
|
||||||
|
layerEffect.Update(0);
|
||||||
|
}
|
||||||
|
|
||||||
_layerEffects.Add(layerEffect);
|
_layerEffects.Add(layerEffect);
|
||||||
|
|
||||||
// Update the order on the effects
|
// Update the order on the effects
|
||||||
@ -246,6 +254,9 @@ public abstract class RenderProfileElement : ProfileElement
|
|||||||
// Remove the effect from the layer
|
// Remove the effect from the layer
|
||||||
_layerEffects.Remove(layerEffect);
|
_layerEffects.Remove(layerEffect);
|
||||||
|
|
||||||
|
// Disable the effect after removing it
|
||||||
|
layerEffect.InternalDisable();
|
||||||
|
|
||||||
// Update the order on the remaining effects
|
// Update the order on the remaining effects
|
||||||
OrderEffects();
|
OrderEffects();
|
||||||
OnLayerEffectsUpdated();
|
OnLayerEffectsUpdated();
|
||||||
|
|||||||
@ -21,6 +21,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
|||||||
private bool _isBeingEdited;
|
private bool _isBeingEdited;
|
||||||
private bool _isMissingModule;
|
private bool _isMissingModule;
|
||||||
private bool _isSuspended;
|
private bool _isSuspended;
|
||||||
|
private bool _fadeInAndOut;
|
||||||
private Module? _module;
|
private Module? _module;
|
||||||
|
|
||||||
private string _name;
|
private string _name;
|
||||||
@ -160,6 +161,15 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
|||||||
internal set => SetAndNotify(ref _profile, value);
|
internal set => SetAndNotify(ref _profile, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a boolean indicating whether this profile should fade in and out when enabling or disabling
|
||||||
|
/// </summary>
|
||||||
|
public bool FadeInAndOut
|
||||||
|
{
|
||||||
|
get => _fadeInAndOut;
|
||||||
|
set => SetAndNotify(ref _fadeInAndOut, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the module this profile uses
|
/// Gets or sets the module this profile uses
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -272,6 +282,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
|||||||
IsSuspended = Entity.IsSuspended;
|
IsSuspended = Entity.IsSuspended;
|
||||||
ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour;
|
ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour;
|
||||||
HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode;
|
HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode;
|
||||||
|
FadeInAndOut = Entity.FadeInAndOut;
|
||||||
Order = Entity.Order;
|
Order = Entity.Order;
|
||||||
|
|
||||||
Icon.Load();
|
Icon.Load();
|
||||||
@ -294,6 +305,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
|||||||
Entity.ActivationBehaviour = (int) ActivationBehaviour;
|
Entity.ActivationBehaviour = (int) ActivationBehaviour;
|
||||||
Entity.HotkeyMode = (int) HotkeyMode;
|
Entity.HotkeyMode = (int) HotkeyMode;
|
||||||
Entity.ProfileCategoryId = Category.Entity.Id;
|
Entity.ProfileCategoryId = Category.Entity.Id;
|
||||||
|
Entity.FadeInAndOut = FadeInAndOut;
|
||||||
Entity.Order = Order;
|
Entity.Order = Order;
|
||||||
|
|
||||||
Icon.Save();
|
Icon.Save();
|
||||||
|
|||||||
@ -63,7 +63,7 @@ public class ArtemisLedLayout
|
|||||||
|
|
||||||
// Prefer a matching layout or else a default layout (that has no name)
|
// Prefer a matching layout or else a default layout (that has no name)
|
||||||
LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts
|
LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts
|
||||||
.OrderBy(l => l.Name == artemisDevice.LogicalLayout)
|
.OrderByDescending(l => l.Name == artemisDevice.LogicalLayout)
|
||||||
.ThenBy(l => l.Name == null)
|
.ThenBy(l => l.Name == null)
|
||||||
.First();
|
.First();
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,6 @@ namespace Artemis.Core;
|
|||||||
public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
||||||
{
|
{
|
||||||
private string? _description;
|
private string? _description;
|
||||||
private string? _icon;
|
|
||||||
private PluginFeature? _instance;
|
private PluginFeature? _instance;
|
||||||
private Exception? _loadException;
|
private Exception? _loadException;
|
||||||
private string _name = null!;
|
private string _name = 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<SKColor> _colors;
|
|
||||||
|
|
||||||
internal ColorCube(IEnumerable<SKColor> 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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,227 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public SKColor[] Quantize(IEnumerable<SKColor> colors, int amount)
|
|
||||||
{
|
|
||||||
if ((amount & (amount - 1)) != 0)
|
|
||||||
throw new ArgumentException("Must be power of two", nameof(amount));
|
|
||||||
|
|
||||||
Queue<ColorCube> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public SKColor FindColorVariation(IEnumerable<SKColor> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public ColorSwatch FindAllColorVariations(IEnumerable<SKColor> 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
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A service providing a pallette of colors in a bitmap based on vibrant.js
|
|
||||||
/// </summary>
|
|
||||||
public interface IColorQuantizerService : IArtemisService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Reduces an <see cref="IEnumerable{SKColor}" /> to a given amount of relevant colors. Based on the Median Cut
|
|
||||||
/// algorithm
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="colors">The colors to quantize.</param>
|
|
||||||
/// <param name="amount">The number of colors that should be calculated. Must be a power of two.</param>
|
|
||||||
/// <returns>The quantized colors.</returns>
|
|
||||||
public SKColor[] Quantize(IEnumerable<SKColor> colors, int amount);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds colors with certain characteristics in a given <see cref="IEnumerable{SKColor}" />.
|
|
||||||
/// <para />
|
|
||||||
/// Vibrant variants are more saturated, while Muted colors are less.
|
|
||||||
/// <para />
|
|
||||||
/// Light and Dark colors have higher and lower lightness values, respectively.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="colors">The colors to find the variations in</param>
|
|
||||||
/// <param name="type">Which type of color to find</param>
|
|
||||||
/// <param name="ignoreLimits">
|
|
||||||
/// Ignore hard limits on whether a color is considered for each category. Result may be
|
|
||||||
/// <see cref="SKColor.Empty" /> if this is false
|
|
||||||
/// </param>
|
|
||||||
/// <returns>The color found</returns>
|
|
||||||
public SKColor FindColorVariation(IEnumerable<SKColor> colors, ColorType type, bool ignoreLimits = false);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds all the color variations available and returns a struct containing them all.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="colors">The colors to find the variations in</param>
|
|
||||||
/// <param name="ignoreLimits">
|
|
||||||
/// Ignore hard limits on whether a color is considered for each category. Some colors may be
|
|
||||||
/// <see cref="SKColor.Empty" /> if this is false
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A swatch containing all color variations</returns>
|
|
||||||
public ColorSwatch FindAllColorVariations(IEnumerable<SKColor> colors, bool ignoreLimits = false);
|
|
||||||
}
|
|
||||||
@ -8,7 +8,10 @@ namespace Artemis.Core.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class InputProvider : IDisposable
|
public abstract class InputProvider : IDisposable
|
||||||
{
|
{
|
||||||
public InputProvider()
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="InputProvider"/> class.
|
||||||
|
/// </summary>
|
||||||
|
protected InputProvider()
|
||||||
{
|
{
|
||||||
ProviderName = GetType().FullName ?? throw new InvalidOperationException("Input provider must have a type with a name");
|
ProviderName = GetType().FullName ?? throw new InvalidOperationException("Input provider must have a type with a name");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -213,8 +213,17 @@ internal class ProfileService : IProfileService
|
|||||||
// Make sure the profile is active or inactive according to the parameters above
|
// Make sure the profile is active or inactive according to the parameters above
|
||||||
if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile")
|
if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile")
|
||||||
profileConfiguration.TryOrBreak(() => ActivateProfile(profileConfiguration), "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)
|
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);
|
profileConfiguration.Profile?.Update(deltaTime);
|
||||||
}
|
}
|
||||||
@ -254,7 +263,8 @@ internal class ProfileService : IProfileService
|
|||||||
{
|
{
|
||||||
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
|
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
|
||||||
// Ensure all criteria are met before rendering
|
// 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);
|
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty, null);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -316,7 +326,10 @@ internal class ProfileService : IProfileService
|
|||||||
public Profile ActivateProfile(ProfileConfiguration profileConfiguration)
|
public Profile ActivateProfile(ProfileConfiguration profileConfiguration)
|
||||||
{
|
{
|
||||||
if (profileConfiguration.Profile != null)
|
if (profileConfiguration.Profile != null)
|
||||||
|
{
|
||||||
|
profileConfiguration.Profile.ShouldDisplay = true;
|
||||||
return profileConfiguration.Profile;
|
return profileConfiguration.Profile;
|
||||||
|
}
|
||||||
|
|
||||||
ProfileEntity profileEntity;
|
ProfileEntity profileEntity;
|
||||||
try
|
try
|
||||||
@ -361,6 +374,16 @@ internal class ProfileService : IProfileService
|
|||||||
OnProfileDeactivated(new ProfileConfigurationEventArgs(profileConfiguration));
|
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)
|
public void DeleteProfile(ProfileConfiguration profileConfiguration)
|
||||||
{
|
{
|
||||||
DeactivateProfile(profileConfiguration);
|
DeactivateProfile(profileConfiguration);
|
||||||
|
|||||||
@ -97,7 +97,7 @@ public interface INode : INotifyPropertyChanged, IBreakableModel
|
|||||||
void TryEvaluate();
|
void TryEvaluate();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets the node causing all pins to re-evaluate the next time <see cref="Evaluate" /> is called
|
/// Resets the node causing all pins to re-evaluate the next time <see cref="TryEvaluate" /> is called
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Reset();
|
void Reset();
|
||||||
}
|
}
|
||||||
@ -25,4 +25,6 @@ public class ProfileConfigurationEntity
|
|||||||
|
|
||||||
public Guid ProfileCategoryId { get; set; }
|
public Guid ProfileCategoryId { get; set; }
|
||||||
public Guid ProfileId { get; set; }
|
public Guid ProfileId { get; set; }
|
||||||
|
|
||||||
|
public bool FadeInAndOut { get; set; }
|
||||||
}
|
}
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
||||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease.46" />
|
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -317,7 +317,7 @@ public class DeviceVisualizer : Control
|
|||||||
|
|
||||||
Dispatcher.UIThread.Post(InvalidateMeasure);
|
Dispatcher.UIThread.Post(InvalidateMeasure);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable
|
public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable
|
||||||
{
|
{
|
||||||
private const int MaxDepth = 4;
|
private const int MAX_DEPTH = 4;
|
||||||
private ObservableCollection<DataModelVisualizationViewModel> _children;
|
private ObservableCollection<DataModelVisualizationViewModel> _children;
|
||||||
private DataModel? _dataModel;
|
private DataModel? _dataModel;
|
||||||
private bool _isMatchingFilteredTypes;
|
private bool _isMatchingFilteredTypes;
|
||||||
@ -47,6 +47,9 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
|
|||||||
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription;
|
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the path of the data model to the clipboard.
|
||||||
|
/// </summary>
|
||||||
public ReactiveCommand<Unit, Unit> CopyPath { get; }
|
public ReactiveCommand<Unit, Unit> CopyPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -337,7 +340,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
|
|||||||
{
|
{
|
||||||
if (DataModel == null)
|
if (DataModel == null)
|
||||||
throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model");
|
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;
|
return null;
|
||||||
|
|
||||||
DataModelPath dataModelPath = new(DataModel, path);
|
DataModelPath dataModelPath = new(DataModel, path);
|
||||||
|
|||||||
@ -19,6 +19,9 @@ namespace Artemis.UI.Shared;
|
|||||||
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
|
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
|
||||||
public class ReactiveCoreWindow<TViewModel> : CoreWindow, IViewFor<TViewModel> where TViewModel : class
|
public class ReactiveCoreWindow<TViewModel> : CoreWindow, IViewFor<TViewModel> where TViewModel : class
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ViewModel.
|
||||||
|
/// </summary>
|
||||||
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
|
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
|
||||||
.Register<ReactiveCoreWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
|
.Register<ReactiveCoreWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ using Artemis.Core;
|
|||||||
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a profile editor command that can be used to update a layer property of type <typeparamref name="T" />.
|
/// Represents a profile editor command that can be used to update a color gradient.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateColorGradient : IProfileEditorCommand
|
public class UpdateColorGradient : IProfileEditorCommand
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,8 +29,8 @@
|
|||||||
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
||||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease.46" />
|
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
||||||
<PackageReference Include="RGB.NET.Layout" Version="1.0.0-prerelease.46" />
|
<PackageReference Include="RGB.NET.Layout" Version="1.0.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
||||||
<PackageReference Include="Splat.Ninject" Version="14.4.1" />
|
<PackageReference Include="Splat.Ninject" Version="14.4.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -13,8 +13,8 @@
|
|||||||
Value="{CompiledBinding Start}"
|
Value="{CompiledBinding Start}"
|
||||||
Prefix="{CompiledBinding Prefix}"
|
Prefix="{CompiledBinding Prefix}"
|
||||||
Suffix="{CompiledBinding Affix}"
|
Suffix="{CompiledBinding Affix}"
|
||||||
Minimum="{CompiledBinding End}"
|
Minimum="{CompiledBinding Min}"
|
||||||
Maximum="{CompiledBinding Max}"
|
Maximum="{CompiledBinding End}"
|
||||||
LargeChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
|
LargeChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
|
||||||
SimpleNumberFormat="F3"
|
SimpleNumberFormat="F3"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|||||||
@ -13,8 +13,8 @@
|
|||||||
Value="{CompiledBinding Start}"
|
Value="{CompiledBinding Start}"
|
||||||
Prefix="{CompiledBinding Prefix}"
|
Prefix="{CompiledBinding Prefix}"
|
||||||
Suffix="{CompiledBinding Affix}"
|
Suffix="{CompiledBinding Affix}"
|
||||||
Minimum="{CompiledBinding End}"
|
Minimum="{CompiledBinding Min}"
|
||||||
Maximum="{CompiledBinding Max}"
|
Maximum="{CompiledBinding End}"
|
||||||
LargeChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
|
LargeChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
|
||||||
SimpleNumberFormat="F3"
|
SimpleNumberFormat="F3"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|||||||
@ -92,7 +92,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
|||||||
|
|
||||||
private void CurrentMainWindowOnClosing(object? sender, EventArgs e)
|
private void CurrentMainWindowOnClosing(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
WindowSizeSetting.Save();
|
WindowSizeSetting?.Save();
|
||||||
_lifeTime.MainWindow = null;
|
_lifeTime.MainWindow = null;
|
||||||
SidebarViewModel = null;
|
SidebarViewModel = null;
|
||||||
Router.NavigateAndReset.Execute(new EmptyViewModel(this, "blank")).Subscribe();
|
Router.NavigateAndReset.Execute(new EmptyViewModel(this, "blank")).Subscribe();
|
||||||
|
|||||||
@ -127,6 +127,9 @@
|
|||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<CheckBox VerticalAlignment="Bottom" IsChecked="{CompiledBinding FadeInAndOut}" ToolTip.Tip="Smoothly animates in and out when the profile activation conditions change.">Fade when enabling and disabling</CheckBox>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
|||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private Hotkey? _disableHotkey;
|
private Hotkey? _disableHotkey;
|
||||||
private Hotkey? _enableHotkey;
|
private Hotkey? _enableHotkey;
|
||||||
|
private bool _fadeInAndOut;
|
||||||
private ProfileConfigurationHotkeyMode _hotkeyMode;
|
private ProfileConfigurationHotkeyMode _hotkeyMode;
|
||||||
private ProfileConfigurationIconType _iconType;
|
private ProfileConfigurationIconType _iconType;
|
||||||
private ObservableCollection<ProfileIconViewModel>? _materialIcons;
|
private ObservableCollection<ProfileIconViewModel>? _materialIcons;
|
||||||
@ -57,6 +58,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
|||||||
_profileName = _profileConfiguration.Name;
|
_profileName = _profileConfiguration.Name;
|
||||||
_iconType = _profileConfiguration.Icon.IconType;
|
_iconType = _profileConfiguration.Icon.IconType;
|
||||||
_hotkeyMode = _profileConfiguration.HotkeyMode;
|
_hotkeyMode = _profileConfiguration.HotkeyMode;
|
||||||
|
_fadeInAndOut = _profileConfiguration.FadeInAndOut;
|
||||||
if (_profileConfiguration.EnableHotkey != null)
|
if (_profileConfiguration.EnableHotkey != null)
|
||||||
_enableHotkey = new Hotkey {Key = _profileConfiguration.EnableHotkey.Key, Modifiers = _profileConfiguration.EnableHotkey.Modifiers};
|
_enableHotkey = new Hotkey {Key = _profileConfiguration.EnableHotkey.Key, Modifiers = _profileConfiguration.EnableHotkey.Modifiers};
|
||||||
if (_profileConfiguration.DisableHotkey != null)
|
if (_profileConfiguration.DisableHotkey != null)
|
||||||
@ -117,6 +119,12 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
|||||||
set => RaiseAndSetIfChanged(ref _disableHotkey, value);
|
set => RaiseAndSetIfChanged(ref _disableHotkey, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool FadeInAndOut
|
||||||
|
{
|
||||||
|
get => _fadeInAndOut;
|
||||||
|
set => RaiseAndSetIfChanged(ref _fadeInAndOut, value);
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<ProfileModuleViewModel?> Modules { get; }
|
public ObservableCollection<ProfileModuleViewModel?> Modules { get; }
|
||||||
|
|
||||||
public ProfileModuleViewModel? SelectedModule
|
public ProfileModuleViewModel? SelectedModule
|
||||||
@ -131,7 +139,6 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
|||||||
public ReactiveCommand<Unit, Unit> OpenConditionEditor { get; }
|
public ReactiveCommand<Unit, Unit> OpenConditionEditor { get; }
|
||||||
public ReactiveCommand<Unit, Unit> BrowseBitmapFile { get; }
|
public ReactiveCommand<Unit, Unit> BrowseBitmapFile { get; }
|
||||||
public ReactiveCommand<Unit, Unit> Confirm { get; }
|
public ReactiveCommand<Unit, Unit> Confirm { get; }
|
||||||
public ReactiveCommand<Unit, Unit> Import { get; }
|
|
||||||
public ReactiveCommand<Unit, Unit> Delete { get; }
|
public ReactiveCommand<Unit, Unit> Delete { get; }
|
||||||
public ReactiveCommand<Unit, Unit> Cancel { get; }
|
public ReactiveCommand<Unit, Unit> Cancel { get; }
|
||||||
|
|
||||||
@ -155,6 +162,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
|||||||
ProfileConfiguration.HotkeyMode = HotkeyMode;
|
ProfileConfiguration.HotkeyMode = HotkeyMode;
|
||||||
ProfileConfiguration.EnableHotkey = EnableHotkey;
|
ProfileConfiguration.EnableHotkey = EnableHotkey;
|
||||||
ProfileConfiguration.DisableHotkey = DisableHotkey;
|
ProfileConfiguration.DisableHotkey = DisableHotkey;
|
||||||
|
ProfileConfiguration.FadeInAndOut = FadeInAndOut;
|
||||||
|
|
||||||
await SaveIcon();
|
await SaveIcon();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.Color
|
||||||
|
{
|
||||||
|
[Node("Gradient Builder", "Generates a gradient based on some values", "Color", OutputType = typeof(ColorGradient), HelpUrl = "https://krazydad.com/tutorials/makecolors.php")]
|
||||||
|
public class GradientBuilderNode : Node
|
||||||
|
{
|
||||||
|
public OutputPin<ColorGradient> Output { get; }
|
||||||
|
|
||||||
|
public InputPin<Numeric> Frequency1 { get; }
|
||||||
|
public InputPin<Numeric> Frequency2 { get; }
|
||||||
|
public InputPin<Numeric> Frequency3 { get; }
|
||||||
|
public InputPin<Numeric> Phase1 { get; }
|
||||||
|
public InputPin<Numeric> Phase2 { get; }
|
||||||
|
public InputPin<Numeric> Phase3 { get; }
|
||||||
|
public InputPin<Numeric> Center { get; }
|
||||||
|
public InputPin<Numeric> Width { get; }
|
||||||
|
public InputPin<Numeric> Length { get; }
|
||||||
|
|
||||||
|
public GradientBuilderNode()
|
||||||
|
{
|
||||||
|
Output = CreateOutputPin<ColorGradient>();
|
||||||
|
Frequency1 = CreateInputPin<Numeric>("Frequency 1");
|
||||||
|
Frequency2 = CreateInputPin<Numeric>("Frequency 2");
|
||||||
|
Frequency3 = CreateInputPin<Numeric>("Frequency 3");
|
||||||
|
Phase1 = CreateInputPin<Numeric>("Phase 1");
|
||||||
|
Phase2 = CreateInputPin<Numeric>("Phase 2");
|
||||||
|
Phase3 = CreateInputPin<Numeric>("Phase 3");
|
||||||
|
Center = CreateInputPin<Numeric>("Center");
|
||||||
|
Width = CreateInputPin<Numeric>("Width");
|
||||||
|
Length = CreateInputPin<Numeric>("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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs
Normal file
45
src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs
Normal file
@ -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<SKColor> Inputs { get; }
|
||||||
|
public OutputPin<ColorGradient> Output { get; }
|
||||||
|
|
||||||
|
public SortedGradientNode()
|
||||||
|
{
|
||||||
|
Inputs = CreateInputPinCollection<SKColor>();
|
||||||
|
Output = CreateOutputPin<ColorGradient>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -117,7 +117,7 @@ public class LayerPropertyNode : Node<LayerPropertyNodeEntity, LayerPropertyNode
|
|||||||
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
|
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
|
||||||
/// editor.
|
/// editor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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
|
// Grab the first pin from the bucket that isn't on the node yet
|
||||||
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
||||||
|
|||||||
@ -9,7 +9,7 @@ namespace Artemis.VisualScripting.Nodes.List;
|
|||||||
public class ListOperatorPredicateNode : Node<ListOperatorEntity, ListOperatorPredicateNodeCustomViewModel>, IDisposable
|
public class ListOperatorPredicateNode : Node<ListOperatorEntity, ListOperatorPredicateNodeCustomViewModel>, IDisposable
|
||||||
{
|
{
|
||||||
private readonly object _scriptLock = new();
|
private readonly object _scriptLock = new();
|
||||||
private ListOperatorPredicateStartNode _startNode;
|
private readonly ListOperatorPredicateStartNode _startNode;
|
||||||
|
|
||||||
public ListOperatorPredicateNode()
|
public ListOperatorPredicateNode()
|
||||||
{
|
{
|
||||||
@ -65,7 +65,7 @@ public class ListOperatorPredicateNode : Node<ListOperatorEntity, ListOperatorPr
|
|||||||
|
|
||||||
private bool EvaluateItem(object item)
|
private bool EvaluateItem(object item)
|
||||||
{
|
{
|
||||||
if (Script == null || _startNode == null)
|
if (Script == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_startNode.Item = item;
|
_startNode.Item = item;
|
||||||
@ -100,7 +100,6 @@ public class ListOperatorPredicateNode : Node<ListOperatorEntity, ListOperatorPr
|
|||||||
{
|
{
|
||||||
Script?.Dispose();
|
Script?.Dispose();
|
||||||
Script = null;
|
Script = null;
|
||||||
_startNode = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user