From a99bae8442726c2c309fcdffc52fc858c95632e0 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Jan 2020 15:12:50 +0000 Subject: [PATCH 01/11] Added initial prototype of a "segmented" radial brush which has cones of colour that radiate out from the center. Tried the System.Drawing.Drawing2D.PathGradientBrush to achieve this effect however that has a central colour that I am unable to remove. There's probably a better name for this brush. --- .../EffectsEngine/EffectLayer.cs | 35 ++++-- .../SegmentedRadialBrushFactory.cs | 103 ++++++++++++++++++ .../Profiles/LightingStateManager.cs | 1 + .../Project-Aurora/Project-Aurora.csproj | 9 ++ .../Settings/Layers/Control_RadialLayer.xaml | 14 +++ .../Layers/Control_RadialLayer.xaml.cs | 23 ++++ .../Settings/Layers/RadialLayerHandler.cs | 40 +++++++ 7 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs create mode 100644 Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml create mode 100644 Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs create mode 100644 Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs diff --git a/Project-Aurora/Project-Aurora/EffectsEngine/EffectLayer.cs b/Project-Aurora/Project-Aurora/EffectsEngine/EffectLayer.cs index 25fcc9fc8..d233c0bba 100755 --- a/Project-Aurora/Project-Aurora/EffectsEngine/EffectLayer.cs +++ b/Project-Aurora/Project-Aurora/EffectsEngine/EffectLayer.cs @@ -441,12 +441,20 @@ public EffectLayer Set(Devices.DeviceKeys[] keys, Color color) /// KeySequence to specify what regions of the bitmap need to be changed /// Color to be used /// Itself - public EffectLayer Set(KeySequence sequence, Color color) + public EffectLayer Set(KeySequence sequence, Color color) => Set(sequence, new SolidBrush(color)); + + /// + /// Sets a specific KeySequence on the bitmap with a specified brush. + /// + /// KeySequence to specify what regions of the bitmap need to be changed + /// Brush to be used + /// Itself + public EffectLayer Set(KeySequence sequence, Brush brush) { if (sequence.type == KeySequenceType.Sequence) { foreach (var key in sequence.keys) - Set(key, color); + SetOneKey(key, brush); } else { @@ -468,7 +476,7 @@ public EffectLayer Set(KeySequence sequence, Color color) myMatrix.RotateAt(sequence.freeform.Angle, rotatePoint, MatrixOrder.Append); g.Transform = myMatrix; - g.FillRectangle(new SolidBrush(color), rect); + g.FillRectangle(brush, rect); } } @@ -562,13 +570,24 @@ public EffectLayer DrawTransformed(KeySequence sequence, Action render /// DeviceKey to be set /// Color to be used /// Itself - private EffectLayer SetOneKey(Devices.DeviceKeys key, Color color) + private EffectLayer SetOneKey(Devices.DeviceKeys key, Color color) => SetOneKey(key, new SolidBrush(color)); + + /// + /// Sets one DeviceKeys key with a specific brush on the bitmap + /// + /// DeviceKey to be set + /// Brush to be used + /// Itself + private EffectLayer SetOneKey(Devices.DeviceKeys key, Brush brush) { BitmapRectangle keymaping = Effects.GetBitmappingFromDeviceKey(key); if (key == Devices.DeviceKeys.Peripheral) { - peripheral = color; + if (brush is SolidBrush solidBrush) + peripheral = solidBrush.Color; + // TODO Add support for this ^ to other brush types + using (Graphics g = Graphics.FromImage(colormap)) { foreach (Devices.DeviceKeys peri_key in possible_peripheral_keys) @@ -576,7 +595,7 @@ private EffectLayer SetOneKey(Devices.DeviceKeys key, Color color) BitmapRectangle peri_keymaping = Effects.GetBitmappingFromDeviceKey(peri_key); if (peri_keymaping.IsValid) - g.FillRectangle(new SolidBrush(color), peri_keymaping.Rectangle); + g.FillRectangle(brush, peri_keymaping.Rectangle); } needsRender = true; @@ -588,13 +607,13 @@ private EffectLayer SetOneKey(Devices.DeviceKeys key, Color color) keymaping.Left < 0 || keymaping.Right > Effects.canvas_width) { Global.logger.Warn("Coudln't set key color " + key.ToString()); - return this; ; + return this; } else { using (Graphics g = Graphics.FromImage(colormap)) { - g.FillRectangle(new SolidBrush(color), keymaping.Rectangle); + g.FillRectangle(brush, keymaping.Rectangle); needsRender = true; } } diff --git a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs new file mode 100644 index 000000000..09f2f9740 --- /dev/null +++ b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace Aurora.EffectsEngine { + + /// + /// A factory that can create a segmented radial brush. + /// + /// + /// I originally tried creating this effect using the , however I cannot find a way of removing the central colour. This means that the + /// colours gradually fade to another colour in the centre. Since the points on the path would need to be equidistant from the centre to preserve the angle and gradients, + /// it means that some of the brush is cut off and the colours appear washed out. All round, not ideal for this use case, so that is the reason I have created this instead. + /// + public class SegmentedRadialBrushFactory { + + // The resolution of the base texture size. + private const int textureSize = 50; + private static readonly Rectangle renderArea = new Rectangle(0, 0, textureSize, textureSize); + private static SolidBrush fallback = new SolidBrush(Color.Transparent); + + private Color[] colors; + private TextureBrush baseBrush; + + public SegmentedRadialBrushFactory(Color[] colors) { + this.colors = colors; + CreateBaseTextureBrush(); + } + + /// + /// Gets or sets the colors and their orders in use by the brush. + /// + public Color[] Colors { + get => (Color[])colors.Clone(); + set { + // If the colors are equal, don't do anything + if (colors.Length == value.Length && ((IStructuralEquatable)colors).Equals(value, StructuralComparisons.StructuralEqualityComparer)) + return; + + // If they are not equal, create a new texture brush + colors = value; + CreateBaseTextureBrush(); + } + } + + /// + /// Creates a new base brush from the current properties. + /// + private void CreateBaseTextureBrush() { + var angle = (float)(360 / colors.Length); + + // Draw the texture to be used for the brush. This is made up of circular segments + var texture = new Bitmap(textureSize, textureSize); + using (var gfx = Graphics.FromImage(texture)) + for (var i = 0; i < colors.Length; i++) + gfx.FillPie(new SolidBrush(colors[i]), renderArea, i * angle, angle); + + // Create the texture brush from our custom bitmap texture + baseBrush = new TextureBrush(texture); + } + + /// + /// Gets the brush that will be centered on and sized for the specified region. + /// + /// + /// The + /// If true, the scale transformation will have the same value in x as it does in y. If false, the scale in each dimension may be different. + /// If the brush is animated, true will make the speeed appear constant whereas false will cause the rotation to appear slower on the shorter side. + public Brush GetBrush(RectangleF region, float angle = 0, bool keepAspectRatio = true) { + // Check if the region has a 0 size. If so, just return a blank brush instead (the matrix becomes invalid with 0 size scaling). + if (region.Width == 0 || region.Height == 0) return fallback; + + var brush = (TextureBrush)baseBrush.Clone(); // Clone the brush so we don't alter the transformation of it in other places accidently + var mtx = new Matrix(); + + // Translate it so that the center of the texture (where all the colors meet) is at 0,0 + mtx.Translate(-textureSize / 2, -textureSize / 2, MatrixOrder.Append); + + // Then, rotate it to the target angle + mtx.Rotate(angle, MatrixOrder.Append); + + // Scale it so that it'll still completely cover the textureSize area. + // 1.45 is a rough approximation of SQRT(2) [it's actually 1.414 but we want to allow a bit of space incase of artifacts at the edges] + mtx.Scale(1.45f, 1.45f, MatrixOrder.Append); + + // Next we need to scale the texture so that it'll cover the area defined by the region + float sx = region.Width / textureSize, sy = region.Height / textureSize; + // If the aspect ratio is locked, we want to scale both dimensions up to the biggest required scale + if (keepAspectRatio) + sx = sy = Math.Max(sx, sy); + mtx.Scale(sx, sy, MatrixOrder.Append); + + // Finally, we need to translate the texture so that it is in the center of the region + // (At this point, the center of the texture where the colors meet is still at 0,0) + mtx.Translate(region.Left + (region.Width / 2), region.Top + (region.Height / 2), MatrixOrder.Append); + + // Apply the transformation and return the texture brush + brush.Transform = mtx; + return brush; + } + } +} diff --git a/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs b/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs index 3b1589d0a..e775b22a0 100755 --- a/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs +++ b/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs @@ -187,6 +187,7 @@ public bool Initialize() new LayerHandlerEntry("BinaryCounter", "Binary Counter Layer", typeof(BinaryCounterLayerHandler)), new LayerHandlerEntry("Particle", "Particle Layer", typeof(SimpleParticleLayerHandler)), new LayerHandlerEntry("InteractiveParticle", "Interactive Particle Layer", typeof(InteractiveParticleLayerHandler)) + new LayerHandlerEntry("Radial", "Radial Layer", typeof(RadialLayerHandler)) }, true); RegisterLayerHandler(new LayerHandlerEntry("WrapperLights", "Wrapper Lighting Layer", typeof(WrapperLightsLayerHandler)), false); diff --git a/Project-Aurora/Project-Aurora/Project-Aurora.csproj b/Project-Aurora/Project-Aurora/Project-Aurora.csproj index 2c3db0a09..2e9803326 100644 --- a/Project-Aurora/Project-Aurora/Project-Aurora.csproj +++ b/Project-Aurora/Project-Aurora/Project-Aurora.csproj @@ -417,6 +417,7 @@ + Control_Discord.xaml @@ -782,6 +783,10 @@ + + Control_RadialLayer.xaml + + Control_RazerLayer.xaml @@ -2296,6 +2301,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml new file mode 100644 index 000000000..acd424139 --- /dev/null +++ b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml @@ -0,0 +1,14 @@ + + + + + diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs new file mode 100644 index 000000000..77d79b86f --- /dev/null +++ b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs @@ -0,0 +1,23 @@ +using System.Windows.Controls; + +namespace Aurora.Settings.Layers { + + public partial class Control_RadialLayer : UserControl { + + public Control_RadialLayer(RadialLayerHandler context) { + DataContext = context; + InitializeComponent(); + } + + private void UserControl_Loaded(object sender, System.Windows.RoutedEventArgs e) { + if (DataContext is RadialLayerHandler context) { + Sequence.Sequence = context.Properties._Sequence; + Loaded -= UserControl_Loaded; + } + } + + private void Sequence_SequenceUpdated(object sender, System.EventArgs e) { + ((RadialLayerHandler)DataContext).Properties._Sequence = Sequence.Sequence; + } + } +} diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs new file mode 100644 index 000000000..08215a577 --- /dev/null +++ b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs @@ -0,0 +1,40 @@ +using Aurora.EffectsEngine; +using Aurora.Profiles; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; + +namespace Aurora.Settings.Layers { + public class RadialLayerProperties : LayerHandlerProperties { + + public SegmentedRadialBrushFactory _Brush { get; set; } + [JsonIgnore] public SegmentedRadialBrushFactory Brush => _Brush ?? Logic._Brush ?? new SegmentedRadialBrushFactory(new[] { Color.Magenta, Color.Yellow, Color.Cyan }); + + public RadialLayerProperties() : base() { } + public RadialLayerProperties(bool empty = false) : base(empty) { } + + public override void Default() { + base.Default(); + } + } + + public class RadialLayerHandler : LayerHandler { + + public RadialLayerHandler() { + _ID = "Radial"; + } + + protected override UserControl CreateControl() => new Control_RadialLayer(this); + + public override EffectLayer Render(IGameState gamestate) { + var area = Properties.Sequence.GetAffectedRegion(); + var brush = Properties.Brush.GetBrush(area); + return new EffectLayer().Set(Properties.Sequence, brush); + } + } +} From 436f194598711d4138fb37daed533cd7b7c60eda Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Jan 2020 19:15:31 +0000 Subject: [PATCH 02/11] Added property on the radial brush to generate interpolated colours between the defined colours. This can help achieve a smoothing effect between segments. Thanks to diogotr7 for the idea. --- .../SegmentedRadialBrushFactory.cs | 43 +++++++++++++++++-- .../Settings/Layers/RadialLayerHandler.cs | 4 +- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs index 09f2f9740..075e0fb0c 100644 --- a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs +++ b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs @@ -1,4 +1,5 @@ -using System; +using Aurora.Utils; +using System; using System.Collections; using System.Drawing; using System.Drawing.Drawing2D; @@ -21,6 +22,7 @@ public class SegmentedRadialBrushFactory { private static SolidBrush fallback = new SolidBrush(Color.Transparent); private Color[] colors; + private int easingAmount; private TextureBrush baseBrush; public SegmentedRadialBrushFactory(Color[] colors) { @@ -44,10 +46,24 @@ public Color[] Colors { } } + /// + /// Determines the number of auto-generated colors between each of the colors defined in the array. + /// + public int EasingAmount { + get => easingAmount; + set { + if (easingAmount != value) { + easingAmount = value; + CreateBaseTextureBrush(); + } + } + } + /// /// Creates a new base brush from the current properties. /// private void CreateBaseTextureBrush() { + var colors = GetBrushColors(); var angle = (float)(360 / colors.Length); // Draw the texture to be used for the brush. This is made up of circular segments @@ -60,12 +76,33 @@ private void CreateBaseTextureBrush() { baseBrush = new TextureBrush(texture); } + /// + /// Generates the colors array to be used for building the brush. This will also generate interpolated colors between defined stops if required. + /// + private Color[] GetBrushColors() { + // Simply return the colors array if easing is disabled. + if (easingAmount <= 0) return colors; + + // For each color, easingAmount many times, generate the interpolated color between the 'i'th color and the 'i + 1'th color. + var interpolatedColors = new Color[colors.Length * (easingAmount + 1)]; + var easeAmountScale = 1f / (easingAmount + 1); + for (var i = 0; i < colors.Length; i++) { + var s = i * (easingAmount + 1); // The start index for this color group + interpolatedColors[s] = colors[i]; + for (var j = 0; j < easingAmount; j++) + interpolatedColors[s + j + 1] = ColorUtils.BlendColors(colors[i], colors[(i + 1) % colors.Length], (j + 1) * easeAmountScale); + } + + return interpolatedColors; + } + /// /// Gets the brush that will be centered on and sized for the specified region. /// - /// - /// The + /// The region which defines where the brush will be drawn and where the brush will be centered. + /// The angle which the brush will be rendered at. /// If true, the scale transformation will have the same value in x as it does in y. If false, the scale in each dimension may be different. + /// When true, the sizes/areas of each color may appear different (due to being cut off), however when false, they appear more consistent. /// If the brush is animated, true will make the speeed appear constant whereas false will cause the rotation to appear slower on the shorter side. public Brush GetBrush(RectangleF region, float angle = 0, bool keepAspectRatio = true) { // Check if the region has a 0 size. If so, just return a blank brush instead (the matrix becomes invalid with 0 size scaling). diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs index 08215a577..4b73ad174 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs @@ -13,7 +13,7 @@ namespace Aurora.Settings.Layers { public class RadialLayerProperties : LayerHandlerProperties { public SegmentedRadialBrushFactory _Brush { get; set; } - [JsonIgnore] public SegmentedRadialBrushFactory Brush => _Brush ?? Logic._Brush ?? new SegmentedRadialBrushFactory(new[] { Color.Magenta, Color.Yellow, Color.Cyan }); + [JsonIgnore] public SegmentedRadialBrushFactory Brush => _Brush ?? Logic._Brush ?? new SegmentedRadialBrushFactory(new[] { Color.Magenta, Color.Yellow, Color.Cyan }) { EasingAmount = 3 }; public RadialLayerProperties() : base() { } public RadialLayerProperties(bool empty = false) : base(empty) { } @@ -33,7 +33,7 @@ public RadialLayerHandler() { public override EffectLayer Render(IGameState gamestate) { var area = Properties.Sequence.GetAffectedRegion(); - var brush = Properties.Brush.GetBrush(area); + var brush = Properties.Brush.GetBrush(area, keepAspectRatio: false); return new EffectLayer().Set(Properties.Sequence, brush); } } From a285577b603b4a2d04c575369defa2f87ace703f Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Sat, 25 Jan 2020 10:19:38 +0000 Subject: [PATCH 03/11] Corrected integer division to be done as float division. Was sometimes causing a black segment to appear at the end of the wheel. --- .../Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs index 075e0fb0c..de1b28ddd 100644 --- a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs +++ b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs @@ -64,7 +64,7 @@ public int EasingAmount { /// private void CreateBaseTextureBrush() { var colors = GetBrushColors(); - var angle = (float)(360 / colors.Length); + var angle = 360f / colors.Length; // Draw the texture to be used for the brush. This is made up of circular segments var texture = new Bitmap(textureSize, textureSize); From e6bfd5d4583013b1467ff7528a00013dcd838299 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Sat, 25 Jan 2020 10:55:33 +0000 Subject: [PATCH 04/11] Added support for animating the brush's rotation. Also increased brush texture size. --- .../SegmentedRadialBrushFactory.cs | 11 +++++--- .../Settings/Layers/RadialLayerHandler.cs | 25 +++++++++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs index de1b28ddd..d527e0f45 100644 --- a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs +++ b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs @@ -1,4 +1,4 @@ -using Aurora.Utils; +using Aurora.Utils; using System; using System.Collections; using System.Drawing; @@ -14,10 +14,10 @@ namespace Aurora.EffectsEngine { /// colours gradually fade to another colour in the centre. Since the points on the path would need to be equidistant from the centre to preserve the angle and gradients, /// it means that some of the brush is cut off and the colours appear washed out. All round, not ideal for this use case, so that is the reason I have created this instead. /// - public class SegmentedRadialBrushFactory { + public class SegmentedRadialBrushFactory : ICloneable { // The resolution of the base texture size. - private const int textureSize = 50; + private const int textureSize = 200; private static readonly Rectangle renderArea = new Rectangle(0, 0, textureSize, textureSize); private static SolidBrush fallback = new SolidBrush(Color.Transparent); @@ -136,5 +136,10 @@ public Brush GetBrush(RectangleF region, float angle = 0, bool keepAspectRatio = brush.Transform = mtx; return brush; } + + /// + /// Creates a clone of this factory. + /// + public object Clone() => new SegmentedRadialBrushFactory((Color[])colors.Clone()) { EasingAmount = EasingAmount }; } } diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs index 4b73ad174..0ac3d2d66 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs @@ -1,8 +1,10 @@ using Aurora.EffectsEngine; using Aurora.Profiles; +using Aurora.Settings.Overrides; using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; @@ -12,19 +14,31 @@ namespace Aurora.Settings.Layers { public class RadialLayerProperties : LayerHandlerProperties { + private static readonly SegmentedRadialBrushFactory defaultFactory = new SegmentedRadialBrushFactory(new[] { Color.Red, Color.Orange, Color.Yellow, Color.Lime, Color.Cyan, Color.Blue, Color.Purple }) { EasingAmount = 3 }; + public SegmentedRadialBrushFactory _Brush { get; set; } - [JsonIgnore] public SegmentedRadialBrushFactory Brush => _Brush ?? Logic._Brush ?? new SegmentedRadialBrushFactory(new[] { Color.Magenta, Color.Yellow, Color.Cyan }) { EasingAmount = 3 }; + [JsonIgnore] public SegmentedRadialBrushFactory Brush => Logic._Brush ?? _Brush ?? defaultFactory; + + // Number of degrees per second the brush rotates at. + [LogicOverridable("Animation Speed")] public int? _AnimationSpeed { get; set; } + [JsonIgnore] public int AnimationSpeed => Logic._AnimationSpeed ?? _AnimationSpeed ?? 60; public RadialLayerProperties() : base() { } public RadialLayerProperties(bool empty = false) : base(empty) { } public override void Default() { base.Default(); + _Sequence = new KeySequence(Effects.WholeCanvasFreeForm); + _Brush = (SegmentedRadialBrushFactory)defaultFactory.Clone(); + _AnimationSpeed = 60; } } public class RadialLayerHandler : LayerHandler { + private Stopwatch sw = new Stopwatch(); + private float angle; + public RadialLayerHandler() { _ID = "Radial"; } @@ -32,8 +46,15 @@ public RadialLayerHandler() { protected override UserControl CreateControl() => new Control_RadialLayer(this); public override EffectLayer Render(IGameState gamestate) { + // Calculate delta time + var dt = sw.Elapsed.TotalSeconds; + sw.Restart(); + + // Update angle + angle = (angle + (float)(dt * Properties.AnimationSpeed)) % 360; + var area = Properties.Sequence.GetAffectedRegion(); - var brush = Properties.Brush.GetBrush(area, keepAspectRatio: false); + var brush = Properties.Brush.GetBrush(area, angle, true); return new EffectLayer().Set(Properties.Sequence, brush); } } From 028d9c9c785513448a1fc86a7638b386003451d4 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Apr 2020 12:33:40 +0100 Subject: [PATCH 05/11] Created a color stop collection class and extracted code used in the particle layer to be reused with this layer. Having it in a custom class can help with UI multithread issues when trying to access a gradientstopcollection of a media brush. --- .../Profiles/LightingStateManager.cs | 2 +- .../Layers/Control_ParticleLayer.xaml.cs | 2 +- .../Layers/SimpleParticleLayerHandler.cs | 50 +++----- .../Project-Aurora/Utils/BrushUtils.cs | 113 +++++++++++++++--- 4 files changed, 114 insertions(+), 53 deletions(-) diff --git a/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs b/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs index e775b22a0..3bddc6ecc 100755 --- a/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs +++ b/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs @@ -186,7 +186,7 @@ public bool Initialize() new LayerHandlerEntry("Toolbar", "Toolbar Layer", typeof(ToolbarLayerHandler)), new LayerHandlerEntry("BinaryCounter", "Binary Counter Layer", typeof(BinaryCounterLayerHandler)), new LayerHandlerEntry("Particle", "Particle Layer", typeof(SimpleParticleLayerHandler)), - new LayerHandlerEntry("InteractiveParticle", "Interactive Particle Layer", typeof(InteractiveParticleLayerHandler)) + new LayerHandlerEntry("InteractiveParticle", "Interactive Particle Layer", typeof(InteractiveParticleLayerHandler)), new LayerHandlerEntry("Radial", "Radial Layer", typeof(RadialLayerHandler)) }, true); diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/Control_ParticleLayer.xaml.cs b/Project-Aurora/Project-Aurora/Settings/Layers/Control_ParticleLayer.xaml.cs index cc3cd574b..f7694fb4b 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/Control_ParticleLayer.xaml.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/Control_ParticleLayer.xaml.cs @@ -33,7 +33,7 @@ private void ApplyGradientToEditor() { private void GradientEditor_BrushChanged(object sender, ColorBox.BrushChangedEventArgs e) { // Set the particle's color stops from the media brush. We cannot pass the media brush directly as it causes issues with UI threading - handler.Properties._ParticleColorStops = e.Brush.ToColorStopCollection(); + handler.Properties._ParticleColorStops = ColorStopCollection.FromMediaBrush(e.Brush); } private void ApplyButton_Click(object sender, RoutedEventArgs e) { diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs index cad727226..f27ecd179 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs @@ -119,13 +119,13 @@ public class SimpleParticleLayerProperties : ParticleLayerPropertiesBase< // The color gradient stops for the particle. Note this is sorted by offset when set using _ParticleColorStops. Not using a linear brush here because: // 1) there are multithreading issues when trying to access a Media brush's gradient collection since it belongs to the UI thread // 2) We don't actually need the gradient as a brush since we're not drawing particles as gradients, only a solid color based on their lifetime, so we only need to access the color stops - private List<(Color color, float offset)> particleColorStops; - public List<(Color color, float offset)> _ParticleColorStops { get => particleColorStops; set => SetAndNotify(ref particleColorStops, value.OrderBy(s => s.offset).ToList()); } - [JsonIgnore] public List<(Color color, float offset)> ParticleColorStops => Logic._ParticleColorStops ?? _ParticleColorStops ?? defaultParticleColor; + private ColorStopCollection particleColorStops; + public ColorStopCollection _ParticleColorStops { get => particleColorStops; set => SetAndNotify(ref particleColorStops, value); } + [JsonIgnore] public ColorStopCollection ParticleColorStops => Logic._ParticleColorStops ?? _ParticleColorStops ?? defaultParticleColor; - private static readonly List<(Color, float)> defaultParticleColor = new List<(Color, float)> { - (Color.White, 0f), - (Color.FromArgb(0, Color.White), 1f) + private static readonly ColorStopCollection defaultParticleColor = new ColorStopCollection { + {0f, Color.White }, + {1f, Color.FromArgb(0, Color.White) } }; public SimpleParticleLayerProperties() : base() { } @@ -149,28 +149,6 @@ public override void Default() { _DeltaSize = 0; _Sequence = new KeySequence(Effects.WholeCanvasFreeForm); } - - /// - /// Returns the color at the specified offset for the current . - /// - /// A value between 0 and 1. At 0, the left-most color is returned, at 1, the rightmost is. - /// - public Color ColorAt(float offset) { - // Check if any GradientStops are exactly at the requested offset. If so, return that - var exact = particleColorStops.Where(s => s.offset == offset); - if (exact.Count() == 1) return exact.Single().color; - - // Check if the requested offset is outside of bounds of the offset range. If so, return the nearest offset - if (offset <= particleColorStops.First().offset) return particleColorStops.First().color; - if (offset >= particleColorStops.Last().offset) return particleColorStops.Last().color; - - // Find the two stops either side of the requsted offset - var left = particleColorStops.Last(s => s.offset < offset); - var right = particleColorStops.First(s => s.offset > offset); - - // Return the blended color that is the correct ratio between left and right - return ColorUtils.BlendColors(left.color, right.color, (offset - left.offset) / (right.offset - left.offset)); - } } @@ -252,7 +230,7 @@ public SimpleParticle(SimpleParticleLayerProperties properties) { public void Render(Graphics gfx, SimpleParticleLayerProperties properties, IGameState gameState) { var s2 = Size / 2; - gfx.FillEllipse(new SolidBrush(properties.ColorAt((float)(Lifetime / MaxLifetime))), new RectangleF(PositionX - s2, PositionY - s2, Size, Size)); + gfx.FillEllipse(new SolidBrush(properties.ParticleColorStops.GetColorAt((float)(Lifetime / MaxLifetime))), new RectangleF(PositionX - s2, PositionY - s2, Size, Size)); } /// @@ -292,10 +270,10 @@ public static class ParticleLayerPresets { new Dictionary> { { "Fire", p => { p._SpawnLocation = ParticleSpawnLocations.BottomEdge; - p._ParticleColorStops = new List<(Color color, float offset)> { - (Color.Yellow, 0f), - (Color.FromArgb(128, Color.Red), 0.6f), - (Color.FromArgb(0, Color.Black), 1f) + p._ParticleColorStops = new ColorStopCollection { + { 0f, Color.Yellow }, + { 0.6f, Color.FromArgb(128, Color.Red) }, + { 1f, Color.FromArgb(0, Color.Black) } }; p._MinSpawnTime = p._MaxSpawnTime = .05f; p._MinSpawnAmount = 4; p._MaxSpawnAmount = 6; @@ -310,9 +288,9 @@ public static class ParticleLayerPresets { } }, { "Matrix", p => { p._SpawnLocation = ParticleSpawnLocations.TopEdge; - p._ParticleColorStops = new List<(Color color, float offset)> { - (Color.FromArgb(0,255,0), 0f), - (Color.FromArgb(0,255,0), 1f) + p._ParticleColorStops = new ColorStopCollection { + { 0f, Color.FromArgb(0,255,0) }, + { 1f, Color.FromArgb(0,255,0) } }; p._MinSpawnTime = .1f; p._MaxSpawnTime = .2f; p._MinSpawnAmount = 1; p._MaxSpawnAmount = 2; diff --git a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs index 59d6d1d12..dee7e3f9c 100644 --- a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs +++ b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs @@ -5,8 +5,10 @@ using System.Windows.Data; using D = System.Drawing; using M = System.Windows.Media; -using ColorStopCollecion = System.Collections.Generic.List<(System.Drawing.Color color, float offset)>; using System.Linq; +using System.Drawing; +using System.Collections; +using Aurora.Profiles.LeagueOfLegends.Layers; namespace Aurora.Utils { @@ -249,34 +251,115 @@ public static M.Brush DrawingBrushToMediaBrush(D.Brush in_brush) return new M.SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 255, 0, 0)); //Return error color } } + } + + /// + /// A collection which stores and interpolates a collection of colors that can represent a gradient. + /// + /// + /// I've made this as it's own class rather than using one of the built-in collections as there can sometimes be UI multi-thead issues if trying + /// to access a gradient stop collection that is being used by a gradient editor in the UI. + /// + public class ColorStopCollection : IEnumerable> { + + private readonly SortedList stops = new SortedList(); + + public ColorStopCollection() { } + + public ColorStopCollection(IEnumerable> stops) { + foreach (var stop in stops) + SetColorAt(stop.Key, stop.Value); + } /// - /// Creates a from the given media brush. + /// Gets or sets the color at the specified offset. + /// When setting a value, a new color stop will be created at the given offset if one does not already exist. /// - public static ColorStopCollecion ToColorStopCollection(this M.Brush brush) { - ColorStopCollecion csc = null; - if (brush is M.GradientBrush gb) - csc = gb.GradientStops.Select(gs => (gs.Color.ToDrawingColor(), (float)gs.Offset)).ToList(); - else if (brush is M.SolidColorBrush sb) - csc = new ColorStopCollecion { (sb.Color.ToDrawingColor(), 0f) }; - csc?.Sort((a, b) => Comparer.Default.Compare(a.offset, b.offset)); - return csc; + /// + /// + public D.Color this[float offset] { + get => GetColorAt(offset); + set => SetColorAt(offset, value); + } + + /// + /// Gets the color at the specific offset. + /// If this point is not at a stop, it's value is interpolated. + /// + public D.Color GetColorAt(float offset) { + // If there are no stops, return a transparent color + if (stops.Count == 0) + return D.Color.Transparent; + + offset = Math.Max(Math.Min(offset, 1), 0); + + // First, check if the target offset is at a stop. If so, return the value of that stop. + if (stops.ContainsKey(offset)) + return stops[offset]; + + // Next, check to see if the target offset is before the first stop or after the last, if so, return that stop. + if (offset < stops.First().Key) + return stops.First().Value; + if (offset > stops.Last().Key) + return stops.Last().Value; + + // At this point, offset is determined to be between two stops, so find which two and then interpolate them. + for (var i = 1; i < stops.Count; i++) { + if (offset > stops.Keys[i - 1] && offset < stops.Keys[i]) + return ColorUtils.BlendColors( + stops.Values[i - 1], + stops.Values[i], + (offset - stops.Keys[i - 1]) / (stops.Keys[i] - stops.Keys[i - 1]) + ); + } + + // Logically, should never get here. + throw new InvalidOperationException("No idea what happened."); + } + + /// + /// Sets the color at the specified offset to the given value. + /// If an offset does not exist at this point, one will be created. + /// + public void SetColorAt(float offset, D.Color color) { + if (offset < 0 || offset > 1) + throw new ArgumentOutOfRangeException(nameof(offset), $"Gradient stop at offset {offset} is out of range. Value must be between 0 and 1 (inclusive)."); + stops[offset] = color; } /// - /// Converts a into a media brush (either - /// or a depending on the amount of stops in the collection). + /// Creates a new media brush from this stop collection. + /// Either or a will be created depending on the number of stops. /// - public static M.Brush ToMediaBrush(this ColorStopCollecion stops) { + public M.Brush ToMediaBrush() { if (stops.Count == 0) return M.Brushes.Transparent; else if (stops.Count == 1) - return new M.SolidColorBrush(stops[0].color.ToMediaColor()); + return new M.SolidColorBrush(stops.Values[0].ToMediaColor()); else return new M.LinearGradientBrush(new M.GradientStopCollection( - stops.Select(s => new M.GradientStop(s.color.ToMediaColor(), s.offset)) + stops.Select(s => new M.GradientStop(s.Value.ToMediaColor(), s.Key)) )); } + + /// + /// Creates a new stop collection from the given media brush. + /// + public static ColorStopCollection FromMediaBrush(M.Brush brush) { + if (brush is M.GradientBrush gb) + return new ColorStopCollection(gb.GradientStops.ToDictionary(gs => (float)gs.Offset, gs => gs.Color.ToDrawingColor())); + else if (brush is M.SolidColorBrush sb) + return new ColorStopCollection { { 0f, sb.Color.ToDrawingColor() } }; + throw new InvalidOperationException($"Brush of type '{brush.GetType().Name} could not be converted to a ColorStopCollection."); + } + + #region IEnumerable + /// Alias for to allow for list constructor syntax. + public void Add(float offset, D.Color color) => SetColorAt(offset, color); + + public IEnumerator> GetEnumerator() => ((IEnumerable>)stops).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)stops).GetEnumerator(); + #endregion } /// From 8ca3981b34b4bf21f7749d8342ea762cc908dac1 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Apr 2020 13:31:33 +0100 Subject: [PATCH 06/11] Tweaked particle layer fire preset values. --- .../Settings/Layers/SimpleParticleLayerHandler.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs index f27ecd179..8dc5ba658 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs @@ -144,8 +144,7 @@ public override void Default() { _AccelerationY = .5f; _DragX = 0; _DragY = 0; - _MinSize = 6; - _MaxSize = 6; + _MinSize = 6; _MaxSize = 6; _DeltaSize = 0; _Sequence = new KeySequence(Effects.WholeCanvasFreeForm); } @@ -282,9 +281,8 @@ public static class ParticleLayerPresets { p._MinInitialVelocityY = -1.3f; p._MaxInitialVelocityY = -0.8f; p._AccelerationX = 0; p._AccelerationY = 0.5f; - p._MinSize = 6; - p._MaxSize = 6; - p._DeltaSize = 0; + p._MinSize = 8; p._MaxSize = 12; + p._DeltaSize = -4; } }, { "Matrix", p => { p._SpawnLocation = ParticleSpawnLocations.TopEdge; @@ -299,8 +297,7 @@ public static class ParticleLayerPresets { p._MinInitialVelocityY = p._MaxInitialVelocityY = 3; p._AccelerationX = 0; p._AccelerationY = 0; - p._MinSize = 6; - p._MaxSize = 6; + p._MinSize = 6; p._MaxSize = 6; p._DeltaSize = 0; } } } From 65c7ede397def67966561a785ae332e0d5d49869 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Apr 2020 14:02:08 +0100 Subject: [PATCH 07/11] Made ToMediaBrush always return a LinearGradientBrush because ncore ColorBox was having some problems with the solid brush. Fixed crash that could occur with FromMediaBrush if two points were at the same offset. (Common with stops at 0 or 1) --- .../Project-Aurora/Utils/BrushUtils.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs index dee7e3f9c..3a59e9e76 100644 --- a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs +++ b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs @@ -1,4 +1,4 @@ -using Corale.Colore.Core; +using Corale.Colore.Core; using System; using System.Collections.Generic; using System.Globalization; @@ -329,17 +329,16 @@ public void SetColorAt(float offset, D.Color color) { /// /// Creates a new media brush from this stop collection. - /// Either or a will be created depending on the number of stops. /// - public M.Brush ToMediaBrush() { + public M.LinearGradientBrush ToMediaBrush() { + M.GradientStopCollection gsc; if (stops.Count == 0) - return M.Brushes.Transparent; + gsc = new M.GradientStopCollection(new[] { new M.GradientStop(M.Colors.Transparent, 0), new M.GradientStop(M.Colors.Transparent, 1) }); else if (stops.Count == 1) - return new M.SolidColorBrush(stops.Values[0].ToMediaColor()); + gsc = new M.GradientStopCollection(new[] { new M.GradientStop(stops.Values[0].ToMediaColor(), 0), new M.GradientStop(stops.Values[0].ToMediaColor(), 1) }); else - return new M.LinearGradientBrush(new M.GradientStopCollection( - stops.Select(s => new M.GradientStop(s.Value.ToMediaColor(), s.Key)) - )); + gsc = new M.GradientStopCollection(stops.Select(s => new M.GradientStop(s.Value.ToMediaColor(), s.Key))); + return new M.LinearGradientBrush(gsc); } /// @@ -347,7 +346,7 @@ public M.Brush ToMediaBrush() { /// public static ColorStopCollection FromMediaBrush(M.Brush brush) { if (brush is M.GradientBrush gb) - return new ColorStopCollection(gb.GradientStops.ToDictionary(gs => (float)gs.Offset, gs => gs.Color.ToDrawingColor())); + return new ColorStopCollection(gb.GradientStops.GroupBy(gs => gs.Offset).ToDictionary(gs => (float)gs.First().Offset, gs => gs.First().Color.ToDrawingColor())); else if (brush is M.SolidColorBrush sb) return new ColorStopCollection { { 0f, sb.Color.ToDrawingColor() } }; throw new InvalidOperationException($"Brush of type '{brush.GetType().Name} could not be converted to a ColorStopCollection."); From 8fb66fd958224f3f3e93f0a6b07de78df9efd2cc Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Apr 2020 14:06:45 +0100 Subject: [PATCH 08/11] Rewrote the radial brush to accept a gradient instead of a list of colours. Implemented radial layer's UI. --- .../SegmentedRadialBrushFactory.cs | 100 +++++++++++------- .../Settings/Layers/Control_RadialLayer.xaml | 19 +++- .../Layers/Control_RadialLayer.xaml.cs | 17 +-- .../Settings/Layers/RadialLayerHandler.cs | 4 +- .../Project-Aurora/Utils/BrushUtils.cs | 32 +++++- 5 files changed, 119 insertions(+), 53 deletions(-) diff --git a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs index d527e0f45..1bb71e973 100644 --- a/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs +++ b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs @@ -1,8 +1,10 @@ using Aurora.Utils; using System; using System.Collections; +using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; +using System.Linq; namespace Aurora.EffectsEngine { @@ -19,13 +21,13 @@ public class SegmentedRadialBrushFactory : ICloneable { // The resolution of the base texture size. private const int textureSize = 200; private static readonly Rectangle renderArea = new Rectangle(0, 0, textureSize, textureSize); - private static SolidBrush fallback = new SolidBrush(Color.Transparent); + private static readonly SolidBrush fallback = new SolidBrush(Color.Transparent); - private Color[] colors; - private int easingAmount; + private ColorStopCollection colors; + private int segmentCount = 24; private TextureBrush baseBrush; - public SegmentedRadialBrushFactory(Color[] colors) { + public SegmentedRadialBrushFactory(ColorStopCollection colors) { this.colors = colors; CreateBaseTextureBrush(); } @@ -33,11 +35,11 @@ public SegmentedRadialBrushFactory(Color[] colors) { /// /// Gets or sets the colors and their orders in use by the brush. /// - public Color[] Colors { - get => (Color[])colors.Clone(); + public ColorStopCollection Colors { + get => colors; set { // If the colors are equal, don't do anything - if (colors.Length == value.Length && ((IStructuralEquatable)colors).Equals(value, StructuralComparisons.StructuralEqualityComparer)) + if (colors.StopsEqual(value)) return; // If they are not equal, create a new texture brush @@ -47,13 +49,15 @@ public Color[] Colors { } /// - /// Determines the number of auto-generated colors between each of the colors defined in the array. + /// How many segments should be created for this brush. Larger values appear smoother by may run more slowly. /// - public int EasingAmount { - get => easingAmount; + public int SegmentCount { + get => segmentCount; set { - if (easingAmount != value) { - easingAmount = value; + if (segmentCount <= 0) + throw new ArgumentOutOfRangeException(nameof(SegmentCount), "Segment count must not be lower than 1."); + if (segmentCount != value) { + segmentCount = value; CreateBaseTextureBrush(); } } @@ -63,39 +67,55 @@ public int EasingAmount { /// Creates a new base brush from the current properties. /// private void CreateBaseTextureBrush() { - var colors = GetBrushColors(); - var angle = 360f / colors.Length; - - // Draw the texture to be used for the brush. This is made up of circular segments + var angle = 360f / segmentCount; + var segmentOffset = 1f / segmentCount; // how much each segment moves the offset forwards on the gradient + + // Get a list of all stops in the stop collection. + // We use this to optimise the interpolation of the colors. + // If we were to use ColorStopCollection.GetColorAt, it may end up running numerous for loops over the same stops, but given + // the special requirements here, we can eliminate that and use less for loops and make the ones we do use slightly more optimal. + var stops = colors.ToList(); + var currentOffset = segmentOffset / 2; + var stopIdx = 0; + + // If there isn't a stop at offsets 0 and 1, create them. This makes it easier during the loop since we don't have to check if we're left/right of the first/last stops. + if (stops[0].Key != 0) + stops.Insert(0, new KeyValuePair(0f, stops[0].Value)); + if (stops[stops.Count - 1].Key != 1) + stops.Add(new KeyValuePair(1f, stops[stops.Count - 1].Value)); + + // Create and draw texture var texture = new Bitmap(textureSize, textureSize); - using (var gfx = Graphics.FromImage(texture)) - for (var i = 0; i < colors.Length; i++) - gfx.FillPie(new SolidBrush(colors[i]), renderArea, i * angle, angle); + using (var gfx = Graphics.FromImage(texture)) { + for (var i = 0; i < segmentCount; i++) { + + // Move the stop index forwards if required. + // - It needs to more fowards until the the stop at that index is to the left of the current offset and the point at that index+1 is to the right. + // - If it is exactly on a stop, make that matched stop at that index. + while (stops[stopIdx + 1].Key < currentOffset) + stopIdx++; + + // Now that stopIdx is in the right place, we can figure out which color we need. + var color = stops[stopIdx].Key == currentOffset + ? stops[stopIdx].Value // if exactly on a stop, don't need to interpolate it + : ColorUtils.BlendColors( // otherwise, we need to calculate the blend between the two stops + stops[stopIdx].Value, + stops[stopIdx + 1].Value, + (currentOffset - stops[stopIdx].Key) / (stops[stopIdx + 1].Key - stops[stopIdx].Key) + ); + + // Draw this segment + gfx.FillPie(new SolidBrush(color), renderArea, i * angle, angle); + + // Bump the offset + currentOffset += segmentOffset; + } + } // Create the texture brush from our custom bitmap texture baseBrush = new TextureBrush(texture); } - /// - /// Generates the colors array to be used for building the brush. This will also generate interpolated colors between defined stops if required. - /// - private Color[] GetBrushColors() { - // Simply return the colors array if easing is disabled. - if (easingAmount <= 0) return colors; - - // For each color, easingAmount many times, generate the interpolated color between the 'i'th color and the 'i + 1'th color. - var interpolatedColors = new Color[colors.Length * (easingAmount + 1)]; - var easeAmountScale = 1f / (easingAmount + 1); - for (var i = 0; i < colors.Length; i++) { - var s = i * (easingAmount + 1); // The start index for this color group - interpolatedColors[s] = colors[i]; - for (var j = 0; j < easingAmount; j++) - interpolatedColors[s + j + 1] = ColorUtils.BlendColors(colors[i], colors[(i + 1) % colors.Length], (j + 1) * easeAmountScale); - } - - return interpolatedColors; - } - /// /// Gets the brush that will be centered on and sized for the specified region. /// @@ -140,6 +160,6 @@ public Brush GetBrush(RectangleF region, float angle = 0, bool keepAspectRatio = /// /// Creates a clone of this factory. /// - public object Clone() => new SegmentedRadialBrushFactory((Color[])colors.Clone()) { EasingAmount = EasingAmount }; + public object Clone() => new SegmentedRadialBrushFactory(new ColorStopCollection(colors)) { SegmentCount = SegmentCount }; } } diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml index acd424139..923b3de4d 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml +++ b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml @@ -3,12 +3,25 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:local="clr-namespace:Aurora.Settings.Layers" xmlns:controls="clr-namespace:Aurora.Controls" + xmlns:ncore="http://schemas.ncore.com/wpf/xaml/colorbox" + xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" + xmlns:u="clr-namespace:Aurora.Utils" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" Loaded="UserControl_Loaded"> - - + + + diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs index 77d79b86f..ff5b6c0a4 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs @@ -1,23 +1,24 @@ -using System.Windows.Controls; +using Aurora.Utils; +using System.Windows.Controls; namespace Aurora.Settings.Layers { public partial class Control_RadialLayer : UserControl { + private readonly RadialLayerHandler handler; + public Control_RadialLayer(RadialLayerHandler context) { - DataContext = context; + DataContext = handler = context; InitializeComponent(); } private void UserControl_Loaded(object sender, System.Windows.RoutedEventArgs e) { - if (DataContext is RadialLayerHandler context) { - Sequence.Sequence = context.Properties._Sequence; - Loaded -= UserControl_Loaded; - } + GradientPicker.Brush = handler.Properties.Brush.Colors.ToMediaBrush(); + Loaded -= UserControl_Loaded; } - private void Sequence_SequenceUpdated(object sender, System.EventArgs e) { - ((RadialLayerHandler)DataContext).Properties._Sequence = Sequence.Sequence; + private void GradientPicker_BrushChanged(object sender, ColorBox.BrushChangedEventArgs e) { + handler.Properties.Brush.Colors = ColorStopCollection.FromMediaBrush(GradientPicker.Brush); } } } diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs index 0ac3d2d66..79bd79cb6 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Linq; @@ -12,9 +13,10 @@ using System.Windows.Controls; namespace Aurora.Settings.Layers { + public class RadialLayerProperties : LayerHandlerProperties { - private static readonly SegmentedRadialBrushFactory defaultFactory = new SegmentedRadialBrushFactory(new[] { Color.Red, Color.Orange, Color.Yellow, Color.Lime, Color.Cyan, Color.Blue, Color.Purple }) { EasingAmount = 3 }; + private static readonly SegmentedRadialBrushFactory defaultFactory = new SegmentedRadialBrushFactory(new Utils.ColorStopCollection(new[] { Color.Red, Color.Orange, Color.Yellow, Color.Lime, Color.Cyan, Color.Blue, Color.Purple, Color.Red })); public SegmentedRadialBrushFactory _Brush { get; set; } [JsonIgnore] public SegmentedRadialBrushFactory Brush => Logic._Brush ?? _Brush ?? defaultFactory; diff --git a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs index 3a59e9e76..e64000e6e 100644 --- a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs +++ b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs @@ -1,4 +1,4 @@ -using Corale.Colore.Core; +using Corale.Colore.Core; using System; using System.Collections.Generic; using System.Globalization; @@ -264,13 +264,38 @@ public class ColorStopCollection : IEnumerable> { private readonly SortedList stops = new SortedList(); + /// + /// Creates an empty ColorStopCollection. + /// public ColorStopCollection() { } + /// + /// Creates a ColorStopCollection from the given float-color key-value-pairs. + /// public ColorStopCollection(IEnumerable> stops) { foreach (var stop in stops) SetColorAt(stop.Key, stop.Value); } + /// + /// Creates a ColorStopCollection from the given colors, which are automatically evenly placed, with the first being at offset 0 and the last at offset 1. + /// + public ColorStopCollection(IEnumerable colors) { + var count = colors.Count(); + if (count > 0) { + float offset = 0, d = offset / count; + foreach (var color in colors) { + SetColorAt(offset, color); + offset += d; + } + } + } + + /// + /// The number of stops in this stop collection. + /// + public int StopCount => stops.Count; + /// /// Gets or sets the color at the specified offset. /// When setting a value, a new color stop will be created at the given offset if one does not already exist. @@ -352,6 +377,11 @@ public static ColorStopCollection FromMediaBrush(M.Brush brush) { throw new InvalidOperationException($"Brush of type '{brush.GetType().Name} could not be converted to a ColorStopCollection."); } + /// + /// Determines if this color stop collection contains the same stops as another collection. + /// + public bool StopsEqual(ColorStopCollection other) => Enumerable.SequenceEqual(stops, other.stops); + #region IEnumerable /// Alias for to allow for list constructor syntax. public void Add(float offset, D.Color color) => SetColorAt(offset, color); From 0d51e1625f589335afeb7435da590c430a118c36 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Apr 2020 14:18:02 +0100 Subject: [PATCH 09/11] Fixed a ColorStopCollection constructor not behaving as expected. --- Project-Aurora/Project-Aurora/Utils/BrushUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs index e64000e6e..16797b884 100644 --- a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs +++ b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs @@ -283,7 +283,7 @@ public ColorStopCollection(IEnumerable> stops) { public ColorStopCollection(IEnumerable colors) { var count = colors.Count(); if (count > 0) { - float offset = 0, d = offset / count; + float offset = 0, d = count > 2 ? 1f / (count - 1f) : 0f; foreach (var color in colors) { SetColorAt(offset, color); offset += d; From d81e8aa1ee62f8c5a60835eb65a56fa06ed9bd5a Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Fri, 24 Apr 2020 15:17:08 +0100 Subject: [PATCH 10/11] Cleaned up BrushUtils references. Stupid references. Can't live with em can't live without them. --- Project-Aurora/Project-Aurora/Utils/BrushUtils.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs index 16797b884..811bfcc35 100644 --- a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs +++ b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs @@ -1,14 +1,11 @@ -using Corale.Colore.Core; -using System; +using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Windows.Data; using D = System.Drawing; using M = System.Windows.Media; -using System.Linq; -using System.Drawing; -using System.Collections; -using Aurora.Profiles.LeagueOfLegends.Layers; namespace Aurora.Utils { From 7fadd0acb05f3e2362d76406e96c18b2648e6a93 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Fri, 24 Apr 2020 15:23:42 +0100 Subject: [PATCH 11/11] Fixed change in particle color stops --- .../Settings/Layers/SimpleParticleLayerHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs index f691553cd..a6a24a757 100644 --- a/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs +++ b/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs @@ -302,9 +302,9 @@ public static class ParticleLayerPresets { } }, { "Rain", p => { p._SpawnLocation = ParticleSpawnLocations.TopEdge; - p._ParticleColorStops = new List<(Color color, float offset)> { - (Color.Cyan, 0f), - (Color.Cyan, 1f) + p._ParticleColorStops = new ColorStopCollection { + { 0f, Color.Cyan }, + { 1f, Color.Cyan } }; p._MinSpawnTime = .1f; p._MaxSpawnTime = .2f; p._MinSpawnAmount = 1; p._MaxSpawnAmount = 2;