diff --git a/src/Artemis.Core/RGB.NET/SKTexture.cs b/src/Artemis.Core/RGB.NET/SKTexture.cs index 27a9f6d5b..6bb77132a 100644 --- a/src/Artemis.Core/RGB.NET/SKTexture.cs +++ b/src/Artemis.Core/RGB.NET/SKTexture.cs @@ -107,7 +107,7 @@ namespace Artemis.Core GetRegionData(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, buffer); Span pixelData = stackalloc byte[DATA_PER_PIXEL]; - Sampler.SampleColor(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); + Sampler.Sample(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); return GetColor(pixelData); } @@ -119,7 +119,7 @@ namespace Artemis.Core GetRegionData(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, buffer); Span pixelData = stackalloc byte[DATA_PER_PIXEL]; - Sampler.SampleColor(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); + Sampler.Sample(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); ArrayPool.Shared.Return(rent); diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs index 4fe7d22c4..f61b70dd5 100644 --- a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs @@ -8,21 +8,6 @@ namespace Artemis.Core.Services /// internal class ColorQuantizerService : IColorQuantizerService { - private static float GetComparisonValue(float sat, float targetSaturation, float luma, float targetLuma) - { - static float InvertDiff(float value, float target) - { - return 1 - Math.Abs(value - target); - } - - const float totalWeight = weightSaturation + weightLuma; - - float totalValue = InvertDiff(sat, targetSaturation) * weightSaturation + - InvertDiff(luma, targetLuma) * weightLuma; - - return totalValue / totalWeight; - } - /// public SKColor[] Quantize(IEnumerable colors, int amount) { @@ -48,29 +33,12 @@ namespace Artemis.Core.Services /// public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false) { - (float targetLuma, float minLuma, float maxLuma, float targetSaturation, float minSaturation, float maxSaturation) = type switch - { - ColorType.Vibrant => (targetNormalLuma, minNormalLuma, maxNormalLuma, targetVibrantSaturation, minVibrantSaturation, 1f), - ColorType.LightVibrant => (targetLightLuma, minLightLuma, 1f, targetVibrantSaturation, minVibrantSaturation, 1f), - ColorType.DarkVibrant => (targetDarkLuma, 0f, maxDarkLuma, targetVibrantSaturation, minVibrantSaturation, 1f), - ColorType.Muted => (targetNormalLuma, minNormalLuma, maxNormalLuma, targetMutesSaturation, 0, maxMutesSaturation), - ColorType.LightMuted => (targetLightLuma, minLightLuma, 1f, targetMutesSaturation, 0, maxMutesSaturation), - ColorType.DarkMuted => (targetDarkLuma, 0, maxDarkLuma, targetMutesSaturation, 0, maxMutesSaturation), - _ => (0.5f, 0f, 1f, 0.5f, 0f, 1f) - }; - - float bestColorScore = float.MinValue; + float bestColorScore = 0; SKColor bestColor = SKColor.Empty; + foreach (SKColor clr in colors) { - clr.ToHsl(out float _, out float sat, out float luma); - sat /= 100f; - luma /= 100f; - - if (!ignoreLimits && (sat <= minSaturation || sat >= maxSaturation || luma <= minLuma || luma >= maxLuma)) - continue; - - float score = GetComparisonValue(sat, targetSaturation, luma, targetLuma); + float score = GetScore(clr, type, ignoreLimits); if (score > bestColorScore) { bestColorScore = score; @@ -81,6 +49,82 @@ namespace Artemis.Core.Services return bestColor; } + /// + public ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false) + { + SKColor bestVibrantColor = SKColor.Empty; + SKColor bestLightVibrantColor = SKColor.Empty; + SKColor bestDarkVibrantColor = SKColor.Empty; + SKColor bestMutedColor = SKColor.Empty; + SKColor bestLightMutedColor = SKColor.Empty; + SKColor bestDarkMutedColor = SKColor.Empty; + float bestVibrantScore = 0; + float bestLightVibrantScore = 0; + float bestDarkVibrantScore = 0; + float bestMutedScore = 0; + float bestLightMutedScore = 0; + float bestDarkMutedScore = 0; + + //ugly but at least we only loop through the enumerable once ¯\_(ツ)_/¯ + foreach (var 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() + { + Vibrant = bestVibrantColor, + LightVibrant = bestLightVibrantColor, + DarkVibrant = bestDarkVibrantColor, + Muted = bestMutedColor, + LightMuted = bestLightMutedColor, + DarkMuted = bestDarkMutedColor, + }; + } + + 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; + } + #region Constants private const float targetDarkLuma = 0.26f; @@ -97,6 +141,72 @@ namespace Artemis.Core.Services private const float weightSaturation = 3f; private const float weightLuma = 5f; + private static float GetTargetLuma(ColorType colorType) => 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) => 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) => 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) => 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) => 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) => colorType switch + { + ColorType.Vibrant => 1f, + ColorType.LightVibrant => 1f, + ColorType.DarkVibrant => 1f, + ColorType.Muted => maxMutesSaturation, + ColorType.LightMuted => maxMutesSaturation, + ColorType.DarkMuted => maxMutesSaturation, + _ => throw new ArgumentException(nameof(colorType)) + }; + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs new file mode 100644 index 000000000..81abb7cc2 --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs @@ -0,0 +1,40 @@ +using SkiaSharp; + +namespace Artemis.Core.Services +{ + /// + /// Swatch containing the known useful color variations. + /// + public struct ColorSwatch + { + /// + /// The component. + /// + public SKColor Vibrant { get; init; } + + /// + /// The component. + /// + public SKColor LightVibrant { get; init; } + + /// + /// The component. + /// + public SKColor DarkVibrant { get; init; } + + /// + /// The component. + /// + public SKColor Muted { get; init; } + + /// + /// The component. + /// + public SKColor LightMuted { get; init; } + + /// + /// The component. + /// + public SKColor DarkMuted { get; init; } + } +} diff --git a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs index 6b0971684..82d70715a 100644 --- a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs +++ b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs @@ -26,5 +26,13 @@ namespace Artemis.Core.Services /// Ignore hard limits on whether a color is considered for each category. Result may be if this is false /// The color found public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false); + + /// + /// Finds all the color variations available and returns a struct containing them all. + /// + /// The colors to find the variations in + /// Ignore hard limits on whether a color is considered for each category. Some colors may be if this is false + /// A swatch containing all color variations + public ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false); } } diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index a9fc7a45e..9dee9fedb 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -13,7 +13,6 @@ using RGB.NET.Core; using Serilog; using Serilog.Events; using SkiaSharp; -using Module = Artemis.Core.Modules.Module; namespace Artemis.Core.Services { @@ -107,6 +106,13 @@ namespace Artemis.Core.Services if (_rgbService.IsRenderPaused) return; + if (_rgbService.FlushLeds) + { + _rgbService.FlushLeds = false; + _rgbService.Surface.Update(true); + return; + } + try { _frameStopWatch.Restart(); diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index 10ec9fa1b..5420edb8f 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -41,6 +41,11 @@ namespace Artemis.Core.Services /// bool RenderOpen { get; } + /// + /// Gets or sets a boolean indicating whether to flush the RGB.NET LEDs during next update + /// + bool FlushLeds { get; set; } + /// /// Opens the render pipeline /// diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index feff46dc2..da343b558 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; using System.Linq; using System.Threading; using Artemis.Core.DeviceProviders; @@ -53,7 +52,7 @@ namespace Artemis.Core.Services UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value}; Surface.RegisterUpdateTrigger(UpdateTrigger); - + Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; } @@ -137,8 +136,14 @@ namespace Artemis.Core.Services public bool IsRenderPaused { get; set; } public bool RenderOpen { get; private set; } + /// + public bool FlushLeds { get; set; } + public void AddDeviceProvider(IRGBDeviceProvider deviceProvider) { + if (RenderOpen) + throw new ArtemisCoreException("Cannot add a device provider while rendering"); + lock (_devices) { try @@ -199,6 +204,9 @@ namespace Artemis.Core.Services public void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider) { + if (RenderOpen) + throw new ArtemisCoreException("Cannot update the remove device provider while rendering"); + lock (_devices) { try @@ -267,36 +275,39 @@ namespace Artemis.Core.Services if (RenderOpen) throw new ArtemisCoreException("Cannot update the texture while rendering"); - IManagedGraphicsContext? graphicsContext = Constants.ManagedGraphicsContext = _newGraphicsContext; - if (!ReferenceEquals(graphicsContext, _newGraphicsContext)) - graphicsContext = _newGraphicsContext; - - if (graphicsContext != null) - _logger.Debug("Creating SKTexture with graphics context {graphicsContext}", graphicsContext.GetType().Name); - else - _logger.Debug("Creating SKTexture with software-based graphics context"); - - float evenWidth = Surface.Boundary.Size.Width; - if (evenWidth % 2 != 0) - evenWidth++; - float evenHeight = Surface.Boundary.Size.Height; - if (evenHeight % 2 != 0) - evenHeight++; - - float renderScale = (float) _renderScaleSetting.Value; - int width = Math.Max(1, MathF.Min(evenWidth * renderScale, 4096).RoundToInt()); - int height = Math.Max(1, MathF.Min(evenHeight * renderScale, 4096).RoundToInt()); - - _texture?.Dispose(); - _texture = new SKTexture(graphicsContext, width, height, renderScale); - _textureBrush.Texture = _texture; - - - if (!ReferenceEquals(_newGraphicsContext, Constants.ManagedGraphicsContext = _newGraphicsContext)) + lock (_devices) { - Constants.ManagedGraphicsContext?.Dispose(); - Constants.ManagedGraphicsContext = _newGraphicsContext; - _newGraphicsContext = null; + IManagedGraphicsContext? graphicsContext = Constants.ManagedGraphicsContext = _newGraphicsContext; + if (!ReferenceEquals(graphicsContext, _newGraphicsContext)) + graphicsContext = _newGraphicsContext; + + if (graphicsContext != null) + _logger.Debug("Creating SKTexture with graphics context {graphicsContext}", graphicsContext.GetType().Name); + else + _logger.Debug("Creating SKTexture with software-based graphics context"); + + float evenWidth = Surface.Boundary.Size.Width; + if (evenWidth % 2 != 0) + evenWidth++; + float evenHeight = Surface.Boundary.Size.Height; + if (evenHeight % 2 != 0) + evenHeight++; + + float renderScale = (float) _renderScaleSetting.Value; + int width = Math.Max(1, MathF.Min(evenWidth * renderScale, 4096).RoundToInt()); + int height = Math.Max(1, MathF.Min(evenHeight * renderScale, 4096).RoundToInt()); + + _texture?.Dispose(); + _texture = new SKTexture(graphicsContext, width, height, renderScale); + _textureBrush.Texture = _texture; + + + if (!ReferenceEquals(_newGraphicsContext, Constants.ManagedGraphicsContext = _newGraphicsContext)) + { + Constants.ManagedGraphicsContext?.Dispose(); + Constants.ManagedGraphicsContext = _newGraphicsContext; + _newGraphicsContext = null; + } } } diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs index 244b7e448..6d2800bfa 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -140,7 +140,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs Device.GreenScale = GreenScale / 100f; Device.BlueScale = BlueScale / 100f; - _rgbService.Surface.Update(true); + _rgbService.FlushLeds = true; } public void BrowseCustomLayout(object sender, MouseEventArgs e) diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml index 249956841..d569d59d4 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml @@ -63,9 +63,8 @@ - - + + + + - + SetAndNotify(ref _colorFirstLedOnly, value); } + public double MaxTextureSize => 4096 / _settingsService.GetSetting("Core.RenderScale", 0.5).Value; + public double MaxTextureSizeIndicatorThickness => 2 / PanZoomViewModel.Zoom; + public void OpenHyperlink(object sender, RequestNavigateEventArgs e) { Core.Utilities.OpenUrl(e.Uri.AbsoluteUri); @@ -155,7 +159,7 @@ namespace Artemis.UI.Screens.SurfaceEditor protected override void OnInitialActivate() { LoadWorkspaceSettings(); - SurfaceDeviceViewModels.AddRange(_rgbService.EnabledDevices.OrderBy(d => d.ZIndex).Select(d => new SurfaceDeviceViewModel(d, _rgbService))); + SurfaceDeviceViewModels.AddRange(_rgbService.EnabledDevices.OrderBy(d => d.ZIndex).Select(d => new SurfaceDeviceViewModel(d, _rgbService, _settingsService))); ListDeviceViewModels.AddRange(_rgbService.EnabledDevices.OrderBy(d => d.ZIndex * -1).Select(d => new ListDeviceViewModel(d, this))); List shuffledDevices = _rgbService.EnabledDevices.OrderBy(d => Guid.NewGuid()).ToList(); @@ -168,7 +172,7 @@ namespace Artemis.UI.Screens.SurfaceEditor } _coreService.FrameRendering += CoreServiceOnFrameRendering; - + PanZoomViewModel.PropertyChanged += PanZoomViewModelOnPropertyChanged; base.OnInitialActivate(); } @@ -179,10 +183,17 @@ namespace Artemis.UI.Screens.SurfaceEditor ListDeviceViewModels.Clear(); _coreService.FrameRendering -= CoreServiceOnFrameRendering; + PanZoomViewModel.PropertyChanged -= PanZoomViewModelOnPropertyChanged; base.OnClose(); } + private void PanZoomViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(PanZoomViewModel.Zoom)) + NotifyOfPropertyChange(nameof(MaxTextureSizeIndicatorThickness)); + } + #endregion #region Context menu actions diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs index 50b3bcec1..82d14326c 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs @@ -14,15 +14,17 @@ namespace Artemis.UI.Screens.SurfaceEditor.Visualization public class SurfaceDeviceViewModel : PropertyChangedBase { private readonly IRgbService _rgbService; + private readonly ISettingsService _settingsService; private Cursor _cursor; private double _dragOffsetX; private double _dragOffsetY; private SelectionStatus _selectionStatus; - public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService) + public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService, ISettingsService settingsService) { Device = device; _rgbService = rgbService; + _settingsService = settingsService; } public ArtemisDevice Device { get; } @@ -101,6 +103,10 @@ namespace Artemis.UI.Screens.SurfaceEditor.Visualization if (x < 0 || y < 0) return false; + double maxTextureSize = 4096 / _settingsService.GetSetting("Core.RenderScale", 0.5).Value; + if (x + Device.Rectangle.Width > maxTextureSize || y + Device.Rectangle.Height > maxTextureSize) + return false; + List own = Device.Leds .Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height)) .ToList();