Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Segmented radial brush #1978

Merged
merged 12 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions Project-Aurora/Project-Aurora/EffectsEngine/EffectLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,12 +441,20 @@ public EffectLayer Set(Devices.DeviceKeys[] keys, Color color)
/// <param name="sequence">KeySequence to specify what regions of the bitmap need to be changed</param>
/// <param name="color">Color to be used</param>
/// <returns>Itself</returns>
public EffectLayer Set(KeySequence sequence, Color color)
public EffectLayer Set(KeySequence sequence, Color color) => Set(sequence, new SolidBrush(color));

/// <summary>
/// Sets a specific KeySequence on the bitmap with a specified brush.
/// </summary>
/// <param name="sequence">KeySequence to specify what regions of the bitmap need to be changed</param>
/// <param name="brush">Brush to be used</param>
/// <returns>Itself</returns>
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
{
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -562,21 +570,32 @@ public EffectLayer DrawTransformed(KeySequence sequence, Action<Graphics> render
/// <param name="key">DeviceKey to be set</param>
/// <param name="color">Color to be used</param>
/// <returns>Itself</returns>
private EffectLayer SetOneKey(Devices.DeviceKeys key, Color color)
private EffectLayer SetOneKey(Devices.DeviceKeys key, Color color) => SetOneKey(key, new SolidBrush(color));

/// <summary>
/// Sets one DeviceKeys key with a specific brush on the bitmap
/// </summary>
/// <param name="key">DeviceKey to be set</param>
/// <param name="brush">Brush to be used</param>
/// <returns>Itself</returns>
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)
{
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;
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

/// <summary>
/// A factory that can create a segmented radial brush.
/// </summary>
/// <remarks>
/// 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
/// 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.
/// </remarks>
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();
}

/// <summary>
/// Gets or sets the colors and their orders in use by the brush.
/// </summary>
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();
}
}

/// <summary>
/// How many segments should be created for this brush. Larger values appear smoother by may run more slowly.
/// </summary>
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();
}
}
}

/// <summary>
/// Creates a new base brush from the current properties.
/// </summary>
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<float, Color>(0f, stops[0].Value));
if (stops[stops.Count - 1].Key != 1)
stops.Add(new KeyValuePair<float, Color>(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);
}

/// <summary>
/// Gets the brush that will be centered on and sized for the specified region.
/// </summary>
/// <param name="region">The region which defines where the brush will be drawn and where the brush will be centered.</param>
/// <param name="angle">The angle which the brush will be rendered at.</param>
/// <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.
/// 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.
/// 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>
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;
}

/// <summary>
/// Creates a clone of this factory.
/// </summary>
public object Clone() => new SegmentedRadialBrushFactory(new ColorStopCollection(colors)) { SegmentCount = SegmentCount };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions Project-Aurora/Project-Aurora/Project-Aurora.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@
</Compile>
<Compile Include="Profiles\CSGO\Layers\CSGODeathLayerHandler.cs" />
<Compile Include="Profiles\CSGO\Layers\CSGOWinningTeamLayerHandler.cs" />
<Compile Include="EffectsEngine\SegmentedRadialBrushFactory.cs" />
<Compile Include="Profiles\Discord\Control_Discord.xaml.cs">
<DependentUpon>Control_Discord.xaml</DependentUpon>
</Compile>
Expand Down Expand Up @@ -782,6 +783,10 @@
<Compile Include="Settings\Layers\InteractiveParticleLayerHandler.cs" />
<Compile Include="Settings\Layers\SimpleParticleLayerHandler.cs" />
<Compile Include="Settings\Layers\ParticleLayerHandlerBase.cs" />
<Compile Include="Settings\Layers\Control_RadialLayer.xaml.cs">
<DependentUpon>Control_RadialLayer.xaml</DependentUpon>
</Compile>
<Compile Include="Settings\Layers\RadialLayerHandler.cs" />
<Compile Include="Settings\Layers\RazerLayerHandler.cs" />
<Compile Include="Settings\Layers\Control_RazerLayer.xaml.cs">
<DependentUpon>Control_RazerLayer.xaml</DependentUpon>
Expand Down Expand Up @@ -2296,6 +2301,10 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Settings\Layers\Control_RadialLayer.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Settings\Layers\Control_RazerLayer.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<UserControl x:Class="Aurora.Settings.Layers.Control_RadialLayer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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: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">

<Grid u:GridHelper.Rows="28px,28px,28px,180px,1*" u:GridHelper.Columns="95px,240px,1*">
<Label Content="Gradient:" />
<ncore:ColorBox x:Name="GradientPicker" BrushChanged="GradientPicker_BrushChanged" Height="28" Grid.Column="1" Margin="0,4" />

<Label Content="Rotation speed:" Grid.Row="1" />
<xctk:IntegerUpDown Value="{Binding Properties._AnimationSpeed}" Grid.Column="1" Grid.Row="1" Margin="0,4" />

<Label Content="Segment count:" Grid.Row="2" />
<xctk:IntegerUpDown Value="{Binding Properties._Brush.SegmentCount}" Minimum="1" Grid.Column="1" Grid.Row="2" Margin="0,4" />

<Label Content="Affected keys:" Grid.Row="3" VerticalAlignment="Top" />
<controls:KeySequence Sequence="{Binding Properties._Sequence, Mode=TwoWay}" RecordingTag="RadialLayer" Title="Affected Keys" Grid.Row="3" Grid.Column="1" Margin="0,4" />
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading