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..1bb71e973
--- /dev/null
+++ b/Project-Aurora/Project-Aurora/EffectsEngine/SegmentedRadialBrushFactory.cs
@@ -0,0 +1,165 @@
+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 {
+
+ ///
+ /// 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 : 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 readonly SolidBrush fallback = new SolidBrush(Color.Transparent);
+
+ private ColorStopCollection colors;
+ private int segmentCount = 24;
+ private TextureBrush baseBrush;
+
+ public SegmentedRadialBrushFactory(ColorStopCollection colors) {
+ this.colors = colors;
+ CreateBaseTextureBrush();
+ }
+
+ ///
+ /// Gets or sets the colors and their orders in use by the brush.
+ ///
+ public ColorStopCollection Colors {
+ get => colors;
+ set {
+ // If the colors are equal, don't do anything
+ if (colors.StopsEqual(value))
+ return;
+
+ // If they are not equal, create a new texture brush
+ colors = value;
+ CreateBaseTextureBrush();
+ }
+ }
+
+ ///
+ /// How many segments should be created for this brush. Larger values appear smoother by may run more slowly.
+ ///
+ public int SegmentCount {
+ get => segmentCount;
+ set {
+ if (segmentCount <= 0)
+ throw new ArgumentOutOfRangeException(nameof(SegmentCount), "Segment count must not be lower than 1.");
+ if (segmentCount != value) {
+ segmentCount = value;
+ CreateBaseTextureBrush();
+ }
+ }
+ }
+
+ ///
+ /// Creates a new base brush from the current properties.
+ ///
+ private void CreateBaseTextureBrush() {
+ 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 < 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);
+ }
+
+ ///
+ /// Gets the brush that will be centered on and sized for the specified region.
+ ///
+ /// 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).
+ 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;
+ }
+
+ ///
+ /// Creates a clone of this factory.
+ ///
+ public object Clone() => new SegmentedRadialBrushFactory(new ColorStopCollection(colors)) { SegmentCount = SegmentCount };
+ }
+}
diff --git a/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs b/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs
index 3b1589d0a..3bddc6ecc 100755
--- a/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs
+++ b/Project-Aurora/Project-Aurora/Profiles/LightingStateManager.cs
@@ -186,7 +186,8 @@ 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);
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:CompileDesigner
+
+ Designer
+ MSBuild:Compile
+ DesignerMSBuild:Compile
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/Control_RadialLayer.xaml b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml
new file mode 100644
index 000000000..923b3de4d
--- /dev/null
+++ b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..ff5b6c0a4
--- /dev/null
+++ b/Project-Aurora/Project-Aurora/Settings/Layers/Control_RadialLayer.xaml.cs
@@ -0,0 +1,24 @@
+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 = handler = context;
+ InitializeComponent();
+ }
+
+ private void UserControl_Loaded(object sender, System.Windows.RoutedEventArgs e) {
+ GradientPicker.Brush = handler.Properties.Brush.Colors.ToMediaBrush();
+ Loaded -= UserControl_Loaded;
+ }
+
+ 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
new file mode 100644
index 000000000..79bd79cb6
--- /dev/null
+++ b/Project-Aurora/Project-Aurora/Settings/Layers/RadialLayerHandler.cs
@@ -0,0 +1,63 @@
+using Aurora.EffectsEngine;
+using Aurora.Profiles;
+using Aurora.Settings.Overrides;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+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 {
+
+ 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;
+
+ // 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";
+ }
+
+ 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, angle, true);
+ return new EffectLayer().Set(Properties.Sequence, brush);
+ }
+ }
+}
diff --git a/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs b/Project-Aurora/Project-Aurora/Settings/Layers/SimpleParticleLayerHandler.cs
index d47b94915..a6a24a757 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() { }
@@ -144,33 +144,10 @@ 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);
}
-
- ///
- /// 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 +229,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 +269,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;
@@ -304,15 +281,14 @@ 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;
- 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;
@@ -321,15 +297,14 @@ 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;
} },
{ "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;
diff --git a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs
index 1f5b618a6..64e02f290 100644
--- a/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs
+++ b/Project-Aurora/Project-Aurora/Utils/BrushUtils.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -248,34 +249,144 @@ 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();
///
- /// Creates a from the given media brush.
+ /// Creates an empty ColorStopCollection.
///
- 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 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 = count > 2 ? 1f / (count - 1f) : 0f;
+ 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.
+ ///
+ ///
+ ///
+ public D.Color this[float offset] {
+ get => GetColorAt(offset);
+ set => SetColorAt(offset, value);
}
///
- /// Converts a into a media brush (either
- /// or a depending on the amount of stops in the collection).
+ /// Gets the color at the specific offset.
+ /// If this point is not at a stop, it's value is interpolated.
///
- public static M.Brush ToMediaBrush(this ColorStopCollecion stops) {
+ public D.Color GetColorAt(float offset) {
+ // If there are no stops, return a transparent color
if (stops.Count == 0)
- return M.Brushes.Transparent;
+ 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;
+ }
+
+ ///
+ /// Creates a new media brush from this stop collection.
+ ///
+ public M.LinearGradientBrush ToMediaBrush() {
+ M.GradientStopCollection gsc;
+ if (stops.Count == 0)
+ 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[0].color.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.color.ToMediaColor(), s.offset))
- ));
+ gsc = new M.GradientStopCollection(stops.Select(s => new M.GradientStop(s.Value.ToMediaColor(), s.Key)));
+ return new M.LinearGradientBrush(gsc);
}
+
+ ///
+ /// 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.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.");
+ }
+
+ ///
+ /// 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);
+
+ public IEnumerator> GetEnumerator() => ((IEnumerable>)stops).GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)stops).GetEnumerator();
+ #endregion
}
///