diff --git a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs index 75d7c8a61..fa786e7c3 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs @@ -20,7 +20,7 @@ namespace Artemis.Core private void CreateDataBindingRegistrations() { DataBinding.ClearDataBindingProperties(); - if (CurrentValue == null) + if (CurrentValue == null!) return; for (int index = 0; index < CurrentValue.Count; index++) @@ -54,10 +54,10 @@ namespace Artemis.Core private void OnCurrentValueSet(object? sender, LayerPropertyEventArgs e) { // Don't allow color gradients to be null - if (BaseValue == null) - BaseValue = DefaultValue ?? new ColorGradient(); + if (BaseValue == null!) + BaseValue = new ColorGradient(DefaultValue); - if (_subscribedGradient != BaseValue) + if (!ReferenceEquals(_subscribedGradient, BaseValue)) { if (_subscribedGradient != null) _subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged; @@ -80,8 +80,8 @@ namespace Artemis.Core protected override void OnInitialize() { // Don't allow color gradients to be null - if (BaseValue == null) - BaseValue = DefaultValue ?? new ColorGradient(); + if (BaseValue == null!) + BaseValue = new ColorGradient(DefaultValue); base.OnInitialize(); } diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 589b7f5ba..173c1481e 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -6,379 +6,571 @@ using System.ComponentModel; using System.Linq; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A gradient containing a list of s +/// +public class ColorGradient : IList, IList, INotifyCollectionChanged { + #region Equality members + /// - /// A gradient containing a list of s + /// Determines whether all the stops in this gradient are equal to the stops in the given gradient. /// - public class ColorGradient : IList, IList, INotifyCollectionChanged + /// The other gradient to compare to + protected bool Equals(ColorGradient other) { - private static readonly SKColor[] FastLedRainbow = - { - new(0xFFFF0000), // Red - new(0xFFFF9900), // Orange - new(0xFFFFFF00), // Yellow - new(0xFF00FF00), // Green - new(0xFF00FF7E), // Aqua - new(0xFF0078FF), // Blue - new(0xFF9E22FF), // Purple - new(0xFFFF34AE), // Pink - new(0xFFFF0000) // and back to Red - }; + if (Count != other.Count) + return false; - private readonly List _stops; - - /// - /// Creates a new instance of the class - /// - public ColorGradient() + for (int i = 0; i < Count; i++) { - _stops = new List(); + if (!Equals(this[i], other[i])) + return false; } - /// - /// Gets all the colors in the color gradient - /// - /// The amount of times to repeat the colors - /// - /// A boolean indicating whether to make the gradient seamless by adding the first color behind the - /// last color - /// - /// An array containing each color in the gradient - public SKColor[] GetColorsArray(int timesToRepeat = 0, bool seamless = false) + return true; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != this.GetType()) + return false; + return Equals((ColorGradient) obj); + } + + /// + public override int GetHashCode() + { + unchecked { - List result = new(); - if (timesToRepeat == 0) - { - result = this.Select(c => c.Color).ToList(); - } - else - { - List colors = this.Select(c => c.Color).ToList(); - for (int i = 0; i <= timesToRepeat; i++) - result.AddRange(colors); - } - - if (seamless && !IsSeamless()) - result.Add(result[0]); - - return result.ToArray(); - } - - /// - /// Gets all the positions in the color gradient - /// - /// - /// The amount of times to repeat the positions - /// - /// - /// A boolean indicating whether to make the gradient seamless by adding the first color behind the - /// last color - /// - /// An array containing a position for each color between 0.0 and 1.0 - public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false) - { - List result = new(); - if (timesToRepeat == 0) - { - result = this.Select(c => c.Position).ToList(); - } - else - { - // Create stops and a list of divided stops - List stops = this.Select(c => c.Position / (timesToRepeat + 1)).ToList(); - - // For each repeat cycle, add the base stops to the end result - for (int i = 0; i <= timesToRepeat; i++) - { - float lastStop = result.LastOrDefault(); - result.AddRange(stops.Select(s => s + lastStop)); - } - } - - if (seamless && !IsSeamless()) - { - // Compress current points evenly - float compression = 1f - 1f / result.Count; - for (int index = 0; index < result.Count; index++) - result[index] = result[index] * compression; - // Add one extra point at the end - result.Add(1f); - } - - return result.ToArray(); - } - - /// - /// Gets a color at any position between 0.0 and 1.0 using interpolation - /// - /// A position between 0.0 and 1.0 - public SKColor GetColor(float position) - { - if (!this.Any()) - return SKColor.Empty; - - ColorGradientStop[] stops = this.ToArray(); - if (position <= 0) return stops[0].Color; - if (position >= 1) return stops[^1].Color; - ColorGradientStop left = stops[0]; - ColorGradientStop? right = null; - foreach (ColorGradientStop stop in stops) - { - if (stop.Position >= position) - { - right = stop; - break; - } - - left = stop; - } - - if (right == null || left == right) - return left.Color; - - position = (float) Math.Round((position - left.Position) / (right.Position - left.Position), 2); - byte a = (byte) ((right.Color.Alpha - left.Color.Alpha) * position + left.Color.Alpha); - byte r = (byte) ((right.Color.Red - left.Color.Red) * position + left.Color.Red); - byte g = (byte) ((right.Color.Green - left.Color.Green) * position + left.Color.Green); - byte b = (byte) ((right.Color.Blue - left.Color.Blue) * position + left.Color.Blue); - return new SKColor(r, g, b, a); - } - - /// - /// Gets a new ColorGradient with colors looping through the HSV-spectrum - /// - /// - public static ColorGradient GetUnicornBarf() - { - ColorGradient gradient = new(); - for (int index = 0; index < FastLedRainbow.Length; index++) - { - SKColor skColor = FastLedRainbow[index]; - float position = 1f / (FastLedRainbow.Length - 1f) * index; - gradient.Add(new ColorGradientStop(skColor, position)); - } - - return gradient; - } - - /// - /// Determines whether the gradient is seamless - /// - /// if the gradient is seamless; otherwise - public bool IsSeamless() - { - return Count == 0 || this.First().Color.Equals(this.Last().Color); - } - - internal void Sort() - { - int requiredIndex = 0; - foreach (ColorGradientStop colorGradientStop in _stops.OrderBy(s => s.Position).ToList()) - { - int actualIndex = _stops.IndexOf(colorGradientStop); - if (requiredIndex != actualIndex) - { - _stops.RemoveAt(actualIndex); - _stops.Insert(requiredIndex, colorGradientStop); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, colorGradientStop, requiredIndex, actualIndex)); - } - - requiredIndex++; - } - } - - private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - Sort(); - OnStopChanged(); - } - - #region Implementation of IEnumerable - - /// - public IEnumerator GetEnumerator() - { - return _stops.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - - #region Implementation of ICollection - - /// - public void Add(ColorGradientStop item) - { - _stops.Add(item); - item.PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _stops.IndexOf(item))); - } - - /// - public int Add(object? value) - { - if (value is ColorGradientStop stop) - _stops.Add(stop); - - return IndexOf(value); - } - - /// - public void Clear() - { - foreach (ColorGradientStop item in _stops) - item.PropertyChanged -= ItemOnPropertyChanged; - _stops.Clear(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - public bool Contains(object? value) - { - return _stops.Contains(value); - } - - /// - public int IndexOf(object? value) - { - return _stops.IndexOf(value); - } - - /// - public void Insert(int index, object? value) - { - if (value is ColorGradientStop stop) - _stops.Insert(index, stop); - } - - /// - public void Remove(object? value) - { - if (value is ColorGradientStop stop) - _stops.Remove(stop); - } - - /// - public bool Contains(ColorGradientStop item) - { - return _stops.Contains(item); - } - - /// - public void CopyTo(ColorGradientStop[] array, int arrayIndex) - { - _stops.CopyTo(array, arrayIndex); - } - - /// - public bool Remove(ColorGradientStop item) - { - item.PropertyChanged -= ItemOnPropertyChanged; - int index = _stops.IndexOf(item); - bool removed = _stops.Remove(item); - if (removed) - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); - - return removed; - } - - /// - public void CopyTo(Array array, int index) - { - _stops.CopyTo((ColorGradientStop[]) array, index); - } - - /// - public int Count => _stops.Count; - - /// - public bool IsSynchronized => false; - - /// - public object SyncRoot => this; - - /// - public bool IsReadOnly => false; - - object? IList.this[int index] - { - get => this[index]; - set => this[index] = (ColorGradientStop) value!; - } - - #endregion - - #region Implementation of IList - - /// - public int IndexOf(ColorGradientStop item) - { - return _stops.IndexOf(item); - } - - /// - public void Insert(int index, ColorGradientStop item) - { - _stops.Insert(index, item); - item.PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); - } - - /// - public void RemoveAt(int index) - { - _stops[index].PropertyChanged -= ItemOnPropertyChanged; - _stops.RemoveAt(index); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); - } - - public bool IsFixedSize { get; } - - /// - public ColorGradientStop this[int index] - { - get => _stops[index]; - set - { - ColorGradientStop? oldValue = _stops[index]; - oldValue.PropertyChanged -= ItemOnPropertyChanged; - _stops[index] = value; - _stops[index].PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); - } - } - - #endregion - - #region Implementation of INotifyCollectionChanged - - /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) - { - CollectionChanged?.Invoke(this, e); - } - - #endregion - - /// - /// Occurs when any of the stops has changed in some way - /// - public event EventHandler? StopChanged; - - private void OnStopChanged() - { - StopChanged?.Invoke(this, EventArgs.Empty); + int hash = 19; + foreach (ColorGradientStop stops in this) + hash = hash * 31 + stops.GetHashCode(); + return hash; } } + + #endregion + + private static readonly SKColor[] FastLedRainbow = + { + new(0xFFFF0000), // Red + new(0xFFFF9900), // Orange + new(0xFFFFFF00), // Yellow + new(0xFF00FF00), // Green + new(0xFF00FF7E), // Aqua + new(0xFF0078FF), // Blue + new(0xFF9E22FF), // Purple + new(0xFFFF34AE), // Pink + new(0xFFFF0000) // and back to Red + }; + + private readonly List _stops; + private bool _updating; + + /// + /// Creates a new instance of the class + /// + public ColorGradient() + { + _stops = new List(); + } + + /// + /// Creates a new instance of the class + /// + /// The color gradient to copy + public ColorGradient(ColorGradient? colorGradient) + { + _stops = new List(); + if (colorGradient == null) + return; + + foreach (ColorGradientStop colorGradientStop in colorGradient) + { + ColorGradientStop stop = new(colorGradientStop.Color, colorGradientStop.Position); + stop.PropertyChanged += ItemOnPropertyChanged; + _stops.Add(stop); + } + } + + /// + /// Gets all the colors in the color gradient + /// + /// The amount of times to repeat the colors + /// + /// A boolean indicating whether to make the gradient seamless by adding the first color behind the + /// last color + /// + /// An array containing each color in the gradient + public SKColor[] GetColorsArray(int timesToRepeat = 0, bool seamless = false) + { + List result = new(); + if (timesToRepeat == 0) + { + result = this.Select(c => c.Color).ToList(); + } + else + { + List colors = this.Select(c => c.Color).ToList(); + for (int i = 0; i <= timesToRepeat; i++) + result.AddRange(colors); + } + + if (seamless && !IsSeamless()) + result.Add(result[0]); + + return result.ToArray(); + } + + /// + /// Gets all the positions in the color gradient + /// + /// + /// The amount of times to repeat the positions + /// + /// + /// A boolean indicating whether to make the gradient seamless by adding the first color behind the + /// last color + /// + /// An array containing a position for each color between 0.0 and 1.0 + public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false) + { + List result = new(); + if (timesToRepeat == 0) + { + result = this.Select(c => c.Position).ToList(); + } + else + { + // Create stops and a list of divided stops + List stops = this.Select(c => c.Position / (timesToRepeat + 1)).ToList(); + + // For each repeat cycle, add the base stops to the end result + for (int i = 0; i <= timesToRepeat; i++) + { + float lastStop = result.LastOrDefault(); + result.AddRange(stops.Select(s => s + lastStop)); + } + } + + if (seamless && !IsSeamless()) + { + // Compress current points evenly + float compression = 1f - 1f / result.Count; + for (int index = 0; index < result.Count; index++) + result[index] = result[index] * compression; + // Add one extra point at the end + result.Add(1f); + } + + return result.ToArray(); + } + + /// + /// Gets a color at any position between 0.0 and 1.0 using interpolation + /// + /// A position between 0.0 and 1.0 + public SKColor GetColor(float position) + { + if (!this.Any()) + return new SKColor(255, 255, 255); + + ColorGradientStop[] stops = this.ToArray(); + if (position <= 0) return stops[0].Color; + if (position >= 1) return stops[^1].Color; + ColorGradientStop left = stops[0]; + ColorGradientStop? right = null; + foreach (ColorGradientStop stop in stops) + { + if (stop.Position >= position) + { + right = stop; + break; + } + + left = stop; + } + + if (right == null || left == right) + return left.Color; + + position = (float) Math.Round((position - left.Position) / (right.Position - left.Position), 2); + byte a = (byte) ((right.Color.Alpha - left.Color.Alpha) * position + left.Color.Alpha); + byte r = (byte) ((right.Color.Red - left.Color.Red) * position + left.Color.Red); + byte g = (byte) ((right.Color.Green - left.Color.Green) * position + left.Color.Green); + byte b = (byte) ((right.Color.Blue - left.Color.Blue) * position + left.Color.Blue); + return new SKColor(r, g, b, a); + } + + /// + /// Gets a new ColorGradient with colors looping through the HSV-spectrum + /// + public static ColorGradient GetUnicornBarf() + { + ColorGradient gradient = new(); + for (int index = 0; index < FastLedRainbow.Length; index++) + { + SKColor skColor = FastLedRainbow[index]; + float position = 1f / (FastLedRainbow.Length - 1f) * index; + gradient.Add(new ColorGradientStop(skColor, position)); + } + + return gradient; + } + + /// + /// Gets a new ColorGradient with random colors from the HSV-spectrum + /// + /// The amount of stops to add + public ColorGradient GetRandom(int stops) + { + ColorGradient gradient = new(); + Random random = new(); + for (int index = 0; index < stops; index++) + { + SKColor skColor = SKColor.FromHsv(random.NextSingle() * 360, 100, 100); + float position = 1f / (stops - 1f) * index; + gradient.Add(new ColorGradientStop(skColor, position)); + } + + return gradient; + } + + /// + /// Determines whether the gradient is seamless + /// + /// if the gradient is seamless; otherwise + public bool IsSeamless() + { + return Count == 0 || this.First().Color.Equals(this.Last().Color); + } + + /// + /// Spreads the color stops equally across the gradient. + /// + public void SpreadStops() + { + try + { + _updating = true; + for (int i = 0; i < Count; i++) + this[i].Position = MathF.Round(i / ((float) Count - 1), 3, MidpointRounding.AwayFromZero); + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// If not already the case, makes the gradient seamless by adding the first color to the end of the gradient and + /// compressing the other stops. + /// + /// If the gradient is already seamless, removes the last color and spreads the remaining stops to fill the freed + /// space. + /// + /// + public void ToggleSeamless() + { + try + { + _updating = true; + + if (IsSeamless()) + { + ColorGradientStop stopToRemove = this.Last(); + Remove(stopToRemove); + + // Uncompress the stops if there is still more than one + if (Count >= 2) + { + float multiplier = Count / (Count - 1f); + foreach (ColorGradientStop stop in this) + stop.Position = MathF.Round(Math.Min(stop.Position * multiplier, 100f), 3, MidpointRounding.AwayFromZero); + } + } + else + { + // Compress existing stops to the left + float multiplier = (Count - 1f) / Count; + foreach (ColorGradientStop stop in this) + stop.Position = MathF.Round(stop.Position * multiplier, 3, MidpointRounding.AwayFromZero); + + // Add a stop to the end that is the same color as the first stop + ColorGradientStop newStop = new(this.First().Color, 1f); + Add(newStop); + } + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Flips the stops of the gradient. + /// + public void FlipStops() + { + try + { + _updating = true; + foreach (ColorGradientStop stop in this) + stop.Position = 1 - stop.Position; + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Rotates the stops of the gradient shifting every stop over to the position of it's neighbor and wrapping around at + /// the end of the gradient. + /// + /// A boolean indicating whether or not the invert the rotation. + public void RotateStops(bool inverse) + { + try + { + _updating = true; + List stops = inverse + ? this.OrderBy(s => s.Position).ToList() + : this.OrderByDescending(s => s.Position).ToList(); + + float lastStopPosition = stops.Last().Position; + foreach (ColorGradientStop stop in stops) + (stop.Position, lastStopPosition) = (lastStopPosition, stop.Position); + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Occurs when any of the stops has changed in some way + /// + public event EventHandler? StopChanged; + + internal void Sort() + { + if (_updating) + return; + + int requiredIndex = 0; + foreach (ColorGradientStop colorGradientStop in _stops.OrderBy(s => s.Position).ToList()) + { + int actualIndex = _stops.IndexOf(colorGradientStop); + if (requiredIndex != actualIndex) + { + _stops.RemoveAt(actualIndex); + _stops.Insert(requiredIndex, colorGradientStop); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, colorGradientStop, requiredIndex, actualIndex)); + } + + requiredIndex++; + } + } + + private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + Sort(); + OnStopChanged(); + } + + private void OnStopChanged() + { + StopChanged?.Invoke(this, EventArgs.Empty); + } + + #region Implementation of IEnumerable + + /// + public IEnumerator GetEnumerator() + { + return _stops.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region Implementation of ICollection + + /// + public void Add(ColorGradientStop item) + { + _stops.Add(item); + item.PropertyChanged += ItemOnPropertyChanged; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); + Sort(); + } + + /// + public int Add(object? value) + { + if (value is ColorGradientStop stop) + _stops.Add(stop); + + return IndexOf(value); + } + + /// + public void Clear() + { + foreach (ColorGradientStop item in _stops) + item.PropertyChanged -= ItemOnPropertyChanged; + _stops.Clear(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + public bool Contains(object? value) + { + return _stops.Contains(value); + } + + /// + public int IndexOf(object? value) + { + return _stops.IndexOf(value); + } + + /// + public void Insert(int index, object? value) + { + if (value is ColorGradientStop stop) + _stops.Insert(index, stop); + } + + /// + public void Remove(object? value) + { + if (value is ColorGradientStop stop) + _stops.Remove(stop); + } + + /// + public bool Contains(ColorGradientStop item) + { + return _stops.Contains(item); + } + + /// + public void CopyTo(ColorGradientStop[] array, int arrayIndex) + { + _stops.CopyTo(array, arrayIndex); + } + + /// + public bool Remove(ColorGradientStop item) + { + item.PropertyChanged -= ItemOnPropertyChanged; + int index = _stops.IndexOf(item); + bool removed = _stops.Remove(item); + if (removed) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + + return removed; + } + + /// + public void CopyTo(Array array, int index) + { + _stops.CopyTo((ColorGradientStop[]) array, index); + } + + /// + public int Count => _stops.Count; + + /// + public bool IsSynchronized => false; + + /// + public object SyncRoot => this; + + /// + public bool IsReadOnly => false; + + object? IList.this[int index] + { + get => this[index]; + set => this[index] = (ColorGradientStop) value!; + } + + #endregion + + #region Implementation of IList + + /// + public int IndexOf(ColorGradientStop item) + { + return _stops.IndexOf(item); + } + + /// + public void Insert(int index, ColorGradientStop item) + { + _stops.Insert(index, item); + item.PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); + } + + /// + public void RemoveAt(int index) + { + _stops[index].PropertyChanged -= ItemOnPropertyChanged; + _stops.RemoveAt(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); + } + + public bool IsFixedSize { get; } + + /// + public ColorGradientStop this[int index] + { + get => _stops[index]; + set + { + ColorGradientStop? oldValue = _stops[index]; + oldValue.PropertyChanged -= ItemOnPropertyChanged; + _stops[index] = value; + _stops[index].PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); + } + } + + #endregion + + #region Implementation of INotifyCollectionChanged + + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs index 242a883d3..8f19b2b95 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs @@ -1,4 +1,5 @@ -using SkiaSharp; +using System; +using SkiaSharp; namespace Artemis.Core { @@ -7,6 +8,34 @@ namespace Artemis.Core /// public class ColorGradientStop : CorePropertyChanged { + #region Equality members + + /// + protected bool Equals(ColorGradientStop other) + { + return _color.Equals(other._color) && _position.Equals(other._position); + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != this.GetType()) + return false; + return Equals((ColorGradientStop) obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(_color, _position); + } + + #endregion + private SKColor _color; private float _position; diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs b/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs new file mode 100644 index 000000000..ba4be29af --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs @@ -0,0 +1,25 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Artemis.UI.Shared.Controls.Flyouts; + +/// +/// Defines a flyout that hosts a gradient picker. +/// +public sealed class GradientPickerFlyout : Flyout +{ + private GradientPicker.GradientPicker? _picker; + + /// + /// Gets the gradient picker that this flyout hosts + /// + public GradientPicker.GradientPicker GradientPicker => _picker ??= new GradientPicker.GradientPicker(); + + /// + protected override Control CreatePresenter() + { + _picker ??= new GradientPicker.GradientPicker(); + FlyoutPresenter presenter = new() {Content = GradientPicker}; + return presenter; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs index 8359568f5..78dd6cf72 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs @@ -1,24 +1,33 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Windows.Input; using Artemis.Core; +using Artemis.UI.Shared.Providers; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Media; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Media; +using ReactiveUI; +using Button = Avalonia.Controls.Button; namespace Artemis.UI.Shared.Controls.GradientPicker; +/// +/// Represents a gradient picker that can be used to edit a gradient. +/// public class GradientPicker : TemplatedControl { - private LinearGradientBrush _linearGradientBrush = new(); - /// /// Gets or sets the color gradient. /// public static readonly StyledProperty ColorGradientProperty = - AvaloniaProperty.Register(nameof(Core.ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf()); + AvaloniaProperty.Register(nameof(ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf()); /// /// Gets or sets the currently selected color stop. @@ -27,11 +36,55 @@ public class GradientPicker : TemplatedControl AvaloniaProperty.Register(nameof(SelectedColorStop), defaultBindingMode: BindingMode.TwoWay); /// - /// Gets the linear gradient brush representing the color gradient. + /// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact), defaultBindingMode: BindingMode.TwoWay); + + /// + /// Gets or sets a storage provider to use for storing and loading gradients. + /// + public static readonly StyledProperty StorageProviderProperty = + AvaloniaProperty.Register(nameof(StorageProvider), notifying: StorageProviderChanged); + + /// + /// Gets the linear gradient brush representing the color gradient. /// public static readonly DirectProperty LinearGradientBrushProperty = AvaloniaProperty.RegisterDirect(nameof(LinearGradientBrush), g => g.LinearGradientBrush); + /// + /// Gets the command to execute when deleting stops. + /// + public static readonly DirectProperty DeleteStopProperty = + AvaloniaProperty.RegisterDirect(nameof(DeleteStop), g => g.DeleteStop); + + private readonly ICommand _deleteStop; + private Button? _flipStops; + private Border? _gradient; + private Button? _rotateStops; + private bool _shiftDown; + private Button? _spreadStops; + private Button? _toggleSeamless; + private ColorGradient? _lastColorGradient; + private ColorPicker? _colorPicker; + + public GradientPicker() + { + _deleteStop = ReactiveCommand.Create(s => + { + if (ColorGradient.Count <= 2) + return; + + int index = ColorGradient.IndexOf(s); + ColorGradient.Remove(s); + if (index > ColorGradient.Count - 1) + index--; + + SelectedColorStop = ColorGradient.ElementAtOrDefault(index); + }); + } + /// /// Gets or sets the color gradient. /// @@ -47,39 +100,130 @@ public class GradientPicker : TemplatedControl public ColorGradientStop? SelectedColorStop { get => GetValue(SelectedColorStopProperty); - set => SetValue(SelectedColorStopProperty, value); + set + { + if (_colorPicker != null && SelectedColorStop != null) + _colorPicker.PreviousColor = new Color2(SelectedColorStop.Color.Red, SelectedColorStop.Color.Green, SelectedColorStop.Color.Blue, SelectedColorStop.Color.Alpha); + SetValue(SelectedColorStopProperty, value); + } } /// - /// Gets the linear gradient brush representing the color gradient. + /// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not. /// - public LinearGradientBrush LinearGradientBrush + public bool IsCompact { - get => _linearGradientBrush; - private set => SetAndRaise(LinearGradientBrushProperty, ref _linearGradientBrush, value); + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets a storage provider to use for storing and loading gradients. + /// + public IColorGradientStorageProvider? StorageProvider + { + get => GetValue(StorageProviderProperty); + set => SetValue(StorageProviderProperty, value); + } + + /// + /// Gets the linear gradient brush representing the color gradient. + /// + public LinearGradientBrush LinearGradientBrush { get; } = new(); + + /// + /// Gets the command to execute when deleting stops. + /// + public ICommand DeleteStop + { + get => _deleteStop; + private init => SetAndRaise(DeleteStopProperty, ref _deleteStop, value); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + if (_gradient != null) + _gradient.PointerPressed -= GradientOnPointerPressed; + if (_spreadStops != null) + _spreadStops.Click -= SpreadStopsOnClick; + if (_toggleSeamless != null) + _toggleSeamless.Click -= ToggleSeamlessOnClick; + if (_flipStops != null) + _flipStops.Click -= FlipStopsOnClick; + if (_rotateStops != null) + _rotateStops.Click -= RotateStopsOnClick; + + _colorPicker = e.NameScope.Find("ColorPicker"); + _gradient = e.NameScope.Find("Gradient"); + _spreadStops = e.NameScope.Find + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml new file mode 100644 index 000000000..f1b53611a --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml index b62cb245a..79b3a3fd2 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml @@ -2,7 +2,15 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared" + xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.DefaultTypes.PropertyInput.ColorGradientPropertyInputView"> - TODO - + x:Class="Artemis.UI.DefaultTypes.PropertyInput.ColorGradientPropertyInputView" + x:DataType="propertyInput:ColorGradientPropertyInputViewModel"> + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs index 26cf7391d..2288a4821 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs @@ -1,20 +1,29 @@ -using Avalonia; -using Avalonia.Controls; +using System; +using Artemis.UI.Shared.Controls.GradientPicker; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.DefaultTypes.PropertyInput -{ - public partial class ColorGradientPropertyInputView : ReactiveUserControl - { - public ColorGradientPropertyInputView() - { - InitializeComponent(); - } +namespace Artemis.UI.DefaultTypes.PropertyInput; - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } +public class ColorGradientPropertyInputView : ReactiveUserControl +{ + public ColorGradientPropertyInputView() + { + InitializeComponent(); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void GradientPickerButton_OnFlyoutOpened(GradientPickerButton sender, EventArgs args) + { + ViewModel?.StartPreview(); + } + + private void GradientPickerButton_OnFlyoutClosed(GradientPickerButton sender, EventArgs args) + { + ViewModel?.ApplyPreview(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs index 262dd7117..0fe7783a7 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs @@ -1,19 +1,87 @@ using System; +using System.Collections.Specialized; +using System.Linq; using Artemis.Core; using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.PropertyInput; +using Avalonia.Media; +using ReactiveUI; namespace Artemis.UI.DefaultTypes.PropertyInput; public class ColorGradientPropertyInputViewModel : PropertyInputViewModel { + private ColorGradient _colorGradient; + private ColorGradient? _originalGradient; + public ColorGradientPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) : base(layerProperty, profileEditorService, propertyInputService) { } - public void DialogClosed(object sender, EventArgs e) + public ColorGradient ColorGradient { - ApplyInputValue(); + get => _colorGradient; + set => this.RaiseAndSetIfChanged(ref _colorGradient, value); } + + protected override void OnInputValueChanged() + { + ColorGradient = new ColorGradient(InputValue); + } + + #region Overrides of PropertyInputViewModel + + /// + public override void StartPreview() + { + _originalGradient = LayerProperty.CurrentValue; + + // Set the property value to the gradient being edited by the picker, this will cause any updates to show right away because + // ColorGradient is a reference type + LayerProperty.CurrentValue = ColorGradient; + + // This won't fly if we ever support keyframes but at that point ColorGradient would have to be a value type anyway and this + // whole VM no longer makes sense + } + + /// + protected override void ApplyInputValue() + { + // Don't do anything, ColorGradient is a reference type and will update regardless + } + + /// + public override void ApplyPreview() + { + if (_originalGradient == null) + return; + + // Make sure something actually changed + if (Equals(ColorGradient, _originalGradient)) + { + LayerProperty.CurrentValue = _originalGradient; + } + else + { + // Update the gradient for realsies, giving the command a reference to the old gradient + ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, ColorGradient, _originalGradient, Time)); + } + + _originalGradient = null; + } + + /// + public override void DiscardPreview() + { + if (_originalGradient == null) + return; + + // Put the old gradient back + InputValue = _originalGradient; + ColorGradient = new ColorGradient(InputValue); + } + + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs index 5dcf03e62..ee6f506f4 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs @@ -28,7 +28,8 @@ namespace Artemis.UI.Screens.SurfaceEditor private double _dragOffsetY; private SelectionStatus _selectionStatus; - public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService, IDeviceService deviceService, ISettingsService settingsService, IDeviceVmFactory deviceVmFactory, IWindowService windowService) + public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService, IDeviceService deviceService, ISettingsService settingsService, IDeviceVmFactory deviceVmFactory, + IWindowService windowService) { _rgbService = rgbService; _deviceService = deviceService; @@ -77,29 +78,36 @@ namespace Artemis.UI.Screens.SurfaceEditor _dragOffsetY = Device.Y - mouseStartPosition.Y; } - public void UpdateMouseDrag(Point mousePosition) + public void UpdateMouseDrag(Point mousePosition, bool round, bool ignoreOverlap) { if (SelectionStatus != SelectionStatus.Selected) return; - float roundedX = (float) Math.Round((mousePosition.X + _dragOffsetX) / 10d, 0, MidpointRounding.AwayFromZero) * 10f; - float roundedY = (float) Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10f; + float x = (float) (mousePosition.X + _dragOffsetX); + float y = (float) (mousePosition.Y + _dragOffsetY); - if (Fits(roundedX, roundedY)) + if (round) { - Device.X = roundedX; - Device.Y = roundedY; + x = (float) Math.Round(x / 10d, 0, MidpointRounding.AwayFromZero) * 10f; + y = (float) Math.Round(y / 10d, 0, MidpointRounding.AwayFromZero) * 10f; } - else if (Fits(roundedX, Device.Y)) + + + if (Fits(x, y, ignoreOverlap)) { - Device.X = roundedX; + Device.X = x; + Device.Y = y; } - else if (Fits(Device.X, roundedY)) + else if (Fits(x, Device.Y, ignoreOverlap)) { - Device.Y = roundedY; + Device.X = x; + } + else if (Fits(Device.X, y, ignoreOverlap)) + { + Device.Y = y; } } - + private void ExecuteIdentifyDevice(ArtemisDevice device) { _deviceService.IdentifyDevice(device); @@ -110,7 +118,7 @@ namespace Artemis.UI.Screens.SurfaceEditor await _windowService.ShowDialogAsync(_deviceVmFactory.DevicePropertiesViewModel(device)); } - private bool Fits(float x, float y) + private bool Fits(float x, float y, bool ignoreOverlap) { if (x < 0 || y < 0) return false; @@ -119,16 +127,16 @@ namespace Artemis.UI.Screens.SurfaceEditor 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(); - List others = _rgbService.EnabledDevices + if (ignoreOverlap) + return true; + + IEnumerable own = Device.Leds + .Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height)); + IEnumerable others = _rgbService.EnabledDevices .Where(d => d != Device && d.IsEnabled) .SelectMany(d => d.Leds) - .Select(l => SKRect.Create(l.Rectangle.Left + l.Device.X, l.Rectangle.Top + l.Device.Y, l.Rectangle.Width, l.Rectangle.Height)) - .ToList(); - - + .Select(l => SKRect.Create(l.Rectangle.Left + l.Device.X, l.Rectangle.Top + l.Device.Y, l.Rectangle.Width, l.Rectangle.Height)); + return !own.Any(o => others.Any(l => l.IntersectsWith(o))); } } diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs index 696813b23..894b17306 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs @@ -73,7 +73,7 @@ namespace Artemis.UI.Screens.SurfaceEditor { if (!_dragging) ViewModel?.StartMouseDrag(e.GetPosition(_containerGrid)); - ViewModel?.UpdateMouseDrag(e.GetPosition(_containerGrid)); + ViewModel?.UpdateMouseDrag(e.GetPosition(_containerGrid), e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Alt)); } _dragging = true; @@ -93,7 +93,7 @@ namespace Artemis.UI.Screens.SurfaceEditor if (ReferenceEquals(e.Pointer.Captured, sender)) { - ViewModel?.StopMouseDrag(e.GetPosition(_containerGrid)); + ViewModel?.StopMouseDrag(e.GetPosition(_containerGrid), e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Alt)); e.Pointer.Capture(null); } } diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index e82817c31..8f8dc0f93 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -61,23 +61,22 @@ namespace Artemis.UI.Screens.SurfaceEditor startedOn.SelectionStatus = SelectionStatus.Selected; foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels.Where(vm => vm != startedOn)) device.SelectionStatus = SelectionStatus.None; - } foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) surfaceDeviceViewModel.StartMouseDrag(mousePosition); } - public void UpdateMouseDrag(Point mousePosition) + public void UpdateMouseDrag(Point mousePosition, bool round, bool ignoreOverlap) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) - surfaceDeviceViewModel.UpdateMouseDrag(mousePosition); + surfaceDeviceViewModel.UpdateMouseDrag(mousePosition, round, ignoreOverlap); } - public void StopMouseDrag(Point mousePosition) + public void StopMouseDrag(Point mousePosition, bool round, bool ignoreOverlap) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) - surfaceDeviceViewModel.UpdateMouseDrag(mousePosition); + surfaceDeviceViewModel.UpdateMouseDrag(mousePosition, round, ignoreOverlap); if (_saving) return; diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml index cff2680bd..1580a3f95 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -8,7 +8,7 @@ xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared" xmlns:workshop="clr-namespace:Artemis.UI.Screens.Workshop" xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="800" x:Class="Artemis.UI.Screens.Workshop.WorkshopView" x:DataType="workshop:WorkshopViewModel"> @@ -47,7 +47,12 @@ - + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 97e951bf0..ca36e0e4c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -5,6 +5,7 @@ using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Input; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Screens.Workshop { @@ -13,6 +14,15 @@ namespace Artemis.UI.Screens.Workshop private readonly INotificationService _notificationService; private StandardCursorType _selectedCursor; private readonly ObservableAsPropertyHelper _cursor; + private ColorGradient _colorGradient = new() + { + new ColorGradientStop(new SKColor(0xFFFF6D00), 0f), + new ColorGradientStop(new SKColor(0xFFFE6806), 0.2f), + new ColorGradientStop(new SKColor(0xFFEF1788), 0.4f), + new ColorGradientStop(new SKColor(0xFFEF1788), 0.6f), + new ColorGradientStop(new SKColor(0xFF00FCCC), 0.8f), + new ColorGradientStop(new SKColor(0xFF00FCCC), 1f), + }; public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop") { @@ -33,7 +43,16 @@ namespace Artemis.UI.Screens.Workshop public Cursor Cursor => _cursor.Value; - public ColorGradient ColorGradient { get; set; } = ColorGradient.GetUnicornBarf(); + public ColorGradient ColorGradient + { + get => _colorGradient; + set => RaiseAndSetIfChanged(ref _colorGradient, value); + } + + public void CreateRandomGradient() + { + ColorGradient = ColorGradient.GetRandom(6); + } private void ExecuteShowNotification(NotificationSeverity severity) {