|
| 1 | +using Aurora.Utils; |
| 2 | +using System; |
| 3 | +using System.Collections; |
| 4 | +using System.Collections.Generic; |
| 5 | +using System.Drawing; |
| 6 | +using System.Drawing.Drawing2D; |
| 7 | +using System.Linq; |
| 8 | + |
| 9 | +namespace Aurora.EffectsEngine { |
| 10 | + |
| 11 | + /// <summary> |
| 12 | + /// A factory that can create a segmented radial brush. |
| 13 | + /// </summary> |
| 14 | + /// <remarks> |
| 15 | + /// I originally tried creating this effect using the <see cref="PathGradientBrush"/>, however I cannot find a way of removing the central colour. This means that the |
| 16 | + /// 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, |
| 17 | + /// 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. |
| 18 | + /// </remarks> |
| 19 | + public class SegmentedRadialBrushFactory : ICloneable { |
| 20 | + |
| 21 | + // The resolution of the base texture size. |
| 22 | + private const int textureSize = 200; |
| 23 | + private static readonly Rectangle renderArea = new Rectangle(0, 0, textureSize, textureSize); |
| 24 | + private static readonly SolidBrush fallback = new SolidBrush(Color.Transparent); |
| 25 | + |
| 26 | + private ColorStopCollection colors; |
| 27 | + private int segmentCount = 24; |
| 28 | + private TextureBrush baseBrush; |
| 29 | + |
| 30 | + public SegmentedRadialBrushFactory(ColorStopCollection colors) { |
| 31 | + this.colors = colors; |
| 32 | + CreateBaseTextureBrush(); |
| 33 | + } |
| 34 | + |
| 35 | + /// <summary> |
| 36 | + /// Gets or sets the colors and their orders in use by the brush. |
| 37 | + /// </summary> |
| 38 | + public ColorStopCollection Colors { |
| 39 | + get => colors; |
| 40 | + set { |
| 41 | + // If the colors are equal, don't do anything |
| 42 | + if (colors.StopsEqual(value)) |
| 43 | + return; |
| 44 | + |
| 45 | + // If they are not equal, create a new texture brush |
| 46 | + colors = value; |
| 47 | + CreateBaseTextureBrush(); |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + /// <summary> |
| 52 | + /// How many segments should be created for this brush. Larger values appear smoother by may run more slowly. |
| 53 | + /// </summary> |
| 54 | + public int SegmentCount { |
| 55 | + get => segmentCount; |
| 56 | + set { |
| 57 | + if (segmentCount <= 0) |
| 58 | + throw new ArgumentOutOfRangeException(nameof(SegmentCount), "Segment count must not be lower than 1."); |
| 59 | + if (segmentCount != value) { |
| 60 | + segmentCount = value; |
| 61 | + CreateBaseTextureBrush(); |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + /// <summary> |
| 67 | + /// Creates a new base brush from the current properties. |
| 68 | + /// </summary> |
| 69 | + private void CreateBaseTextureBrush() { |
| 70 | + var angle = 360f / segmentCount; |
| 71 | + var segmentOffset = 1f / segmentCount; // how much each segment moves the offset forwards on the gradient |
| 72 | + |
| 73 | + // Get a list of all stops in the stop collection. |
| 74 | + // We use this to optimise the interpolation of the colors. |
| 75 | + // If we were to use ColorStopCollection.GetColorAt, it may end up running numerous for loops over the same stops, but given |
| 76 | + // the special requirements here, we can eliminate that and use less for loops and make the ones we do use slightly more optimal. |
| 77 | + var stops = colors.ToList(); |
| 78 | + var currentOffset = segmentOffset / 2; |
| 79 | + var stopIdx = 0; |
| 80 | + |
| 81 | + // 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. |
| 82 | + if (stops[0].Key != 0) |
| 83 | + stops.Insert(0, new KeyValuePair<float, Color>(0f, stops[0].Value)); |
| 84 | + if (stops[stops.Count - 1].Key != 1) |
| 85 | + stops.Add(new KeyValuePair<float, Color>(1f, stops[stops.Count - 1].Value)); |
| 86 | + |
| 87 | + // Create and draw texture |
| 88 | + var texture = new Bitmap(textureSize, textureSize); |
| 89 | + using (var gfx = Graphics.FromImage(texture)) { |
| 90 | + for (var i = 0; i < segmentCount; i++) { |
| 91 | + |
| 92 | + // Move the stop index forwards if required. |
| 93 | + // - 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. |
| 94 | + // - If it is exactly on a stop, make that matched stop at that index. |
| 95 | + while (stops[stopIdx + 1].Key < currentOffset) |
| 96 | + stopIdx++; |
| 97 | + |
| 98 | + // Now that stopIdx is in the right place, we can figure out which color we need. |
| 99 | + var color = stops[stopIdx].Key == currentOffset |
| 100 | + ? stops[stopIdx].Value // if exactly on a stop, don't need to interpolate it |
| 101 | + : ColorUtils.BlendColors( // otherwise, we need to calculate the blend between the two stops |
| 102 | + stops[stopIdx].Value, |
| 103 | + stops[stopIdx + 1].Value, |
| 104 | + (currentOffset - stops[stopIdx].Key) / (stops[stopIdx + 1].Key - stops[stopIdx].Key) |
| 105 | + ); |
| 106 | + |
| 107 | + // Draw this segment |
| 108 | + gfx.FillPie(new SolidBrush(color), renderArea, i * angle, angle); |
| 109 | + |
| 110 | + // Bump the offset |
| 111 | + currentOffset += segmentOffset; |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + // Create the texture brush from our custom bitmap texture |
| 116 | + baseBrush = new TextureBrush(texture); |
| 117 | + } |
| 118 | + |
| 119 | + /// <summary> |
| 120 | + /// Gets the brush that will be centered on and sized for the specified region. |
| 121 | + /// </summary> |
| 122 | + /// <param name="region">The region which defines where the brush will be drawn and where the brush will be centered.</param> |
| 123 | + /// <param name="angle">The angle which the brush will be rendered at.</param> |
| 124 | + /// <param name="keepAspectRatio">If <c>true</c>, the scale transformation will have the same value in x as it does in y. If <c>false</c>, the scale in each dimension may be different. |
| 125 | + /// When <c>true</c>, the sizes/areas of each color may appear different (due to being cut off), however when <c>false</c>, they appear more consistent. |
| 126 | + /// If the brush is animated, <c>true</c> will make the speeed appear constant whereas <c>false</c> will cause the rotation to appear slower on the shorter side.</param> |
| 127 | + public Brush GetBrush(RectangleF region, float angle = 0, bool keepAspectRatio = true) { |
| 128 | + // Check if the region has a 0 size. If so, just return a blank brush instead (the matrix becomes invalid with 0 size scaling). |
| 129 | + if (region.Width == 0 || region.Height == 0) return fallback; |
| 130 | + |
| 131 | + var brush = (TextureBrush)baseBrush.Clone(); // Clone the brush so we don't alter the transformation of it in other places accidently |
| 132 | + var mtx = new Matrix(); |
| 133 | + |
| 134 | + // Translate it so that the center of the texture (where all the colors meet) is at 0,0 |
| 135 | + mtx.Translate(-textureSize / 2, -textureSize / 2, MatrixOrder.Append); |
| 136 | + |
| 137 | + // Then, rotate it to the target angle |
| 138 | + mtx.Rotate(angle, MatrixOrder.Append); |
| 139 | + |
| 140 | + // Scale it so that it'll still completely cover the textureSize area. |
| 141 | + // 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] |
| 142 | + mtx.Scale(1.45f, 1.45f, MatrixOrder.Append); |
| 143 | + |
| 144 | + // Next we need to scale the texture so that it'll cover the area defined by the region |
| 145 | + float sx = region.Width / textureSize, sy = region.Height / textureSize; |
| 146 | + // If the aspect ratio is locked, we want to scale both dimensions up to the biggest required scale |
| 147 | + if (keepAspectRatio) |
| 148 | + sx = sy = Math.Max(sx, sy); |
| 149 | + mtx.Scale(sx, sy, MatrixOrder.Append); |
| 150 | + |
| 151 | + // Finally, we need to translate the texture so that it is in the center of the region |
| 152 | + // (At this point, the center of the texture where the colors meet is still at 0,0) |
| 153 | + mtx.Translate(region.Left + (region.Width / 2), region.Top + (region.Height / 2), MatrixOrder.Append); |
| 154 | + |
| 155 | + // Apply the transformation and return the texture brush |
| 156 | + brush.Transform = mtx; |
| 157 | + return brush; |
| 158 | + } |
| 159 | + |
| 160 | + /// <summary> |
| 161 | + /// Creates a clone of this factory. |
| 162 | + /// </summary> |
| 163 | + public object Clone() => new SegmentedRadialBrushFactory(new ColorStopCollection(colors)) { SegmentCount = SegmentCount }; |
| 164 | + } |
| 165 | +} |
0 commit comments