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

EffectLayer DrawTransformed #1814

Merged
81 changes: 81 additions & 0 deletions Project-Aurora/Project-Aurora/EffectsEngine/EffectLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,87 @@ public EffectLayer Set(KeySequence sequence, Color color)
return this;
}

/// <summary>
/// Allows drawing some arbitrary content to the sequence bounds, including translation, scaling and rotation.<para/>
/// Usage:<code>
/// someEffectLayer.DrawTransformed(Properties.Sequence,<br/>
/// m => {<br/>
/// // We are prepending the transformations since we want the mirroring to happen BEFORE the rotation and scaling happens.<br/>
/// m.Translate(100, 0, MatrixOrder.Prepend); // These two are backwards because we are Prepending (so this is prepended first)<br/>
/// m.Scale(-1, 1, MatrixOrder.Prepend); // Then this is prepended before the tranlate.<br/>
/// },<br/>
/// gfx => {<br/>
/// gfx.FillRectangle(Brushes.Red, 0, 0, 30, 100);<br/>
/// gfx.FillRectangle(Brushes.Blue, 70, 0, 30, 100);<br/>
/// },
/// new RectangleF(0, 0, 100, 100);</code>
/// This code will draw an X-mirrored image of a red stipe and a blue stripe (with a transparent gap in between) to the target keysequence area.
/// </summary>
/// <param name="sequence">The target sequence whose bounds will be used as the target location on the drawing canvas.</param>
/// <param name="configureMatrix">An action that further configures the transformation matrix before render is called.</param>
/// <param name="render">An action that receives a transformed graphics context and can render whatever it needs to.</param>
/// <param name="sourceRegion">The source region of the rendered content. This is used when calculating the transformation matrix, so that this
/// rectangle in the render context is transformed to the keysequence bounds in the layer's context. Note that no clipping is performed.</param>
public EffectLayer DrawTransformed(KeySequence sequence, Action<Matrix> configureMatrix, Action<Graphics> render, RectangleF sourceRegion) {
// The matrix represents the transformation that will be applied to the rendered content
var matrix = new Matrix();

// The bounds represent the target position of the render part
// Note that we round the X and Y off to properly imitate the above `Set(KeySequence, Color)` method. Unsure exactly why this is done, but it _is_ done to replicate behaviour properly.
// Also unsure why the X and Y are rounded using math.Round but Width and Height are just truncated using an int cast??
var boundsRaw = sequence.GetAffectedRegion();
var bounds = new RectangleF((int)Math.Round(boundsRaw.X), (int)Math.Round(boundsRaw.Y), (int)boundsRaw.Width, (int)boundsRaw.Height);

using (var gfx = Graphics.FromImage(colormap)) {

// First, calculate the scaling required to transform the sourceRect's size into the bounds' size
float sx = bounds.Width / sourceRegion.Width, sy = bounds.Height / sourceRegion.Height;

// Perform this scale first
// Note: that if the scale is zero, when setting the graphics transform to the matrix, it throws an error, so we must have NON-ZERO values
// Note 2: Also tried using float.Epsilon but this also caused the exception, so a somewhat small number will have to suffice. Not noticed any visual issues with 0.001f.
matrix.Scale(sx == 0 ? .001f : sx, sy == 0 ? .001f : sy, MatrixOrder.Append);

// Second, for freeform objects, apply the rotation. This needs to be done AFTER the scaling, else the scaling is applied to the rotated object, which skews it
// We rotate around the central point of the source region, but we need to take the scaling of the dimensions into account
if (sequence.type == KeySequenceType.FreeForm)
matrix.RotateAt(sequence.freeform.Angle, new PointF((sourceRegion.Left + (sourceRegion.Width / 2f)) * sx, (sourceRegion.Top + (sourceRegion.Height / 2f)) * sy), MatrixOrder.Append);

// Third, we can translate the matrix from the source to the target location.
matrix.Translate(bounds.X - sourceRegion.Left, bounds.Y - sourceRegion.Top, MatrixOrder.Append);

// Finally, call the custom matrix configure action
configureMatrix(matrix);

// Apply the matrix transform to the graphics context and then render
gfx.Transform = matrix;
render(gfx);
}

return this;
}

/// <summary>
/// Allows drawing some arbitrary content to the sequence bounds, including translation, scaling and rotation.<para/>
/// See <see cref="DrawTransformed(KeySequence, Action{Matrix}, Action{Graphics}, RectangleF)"/> for usage.
/// </summary>
/// <param name="sequence">The target sequence whose bounds will be used as the target location on the drawing canvas.</param>
/// <param name="render">An action that receives a transformed graphics context and can render whatever it needs to.</param>
/// <param name="sourceRegion">The source region of the rendered content. This is used when calculating the transformation matrix, so that this
/// rectangle in the render context is transformed to the keysequence bounds in the layer's context. Note that no clipping is performed.</param>
public EffectLayer DrawTransformed(KeySequence sequence, Action<Graphics> render, RectangleF sourceRegion)
=> DrawTransformed(sequence, _ => { }, render, sourceRegion);

/// <summary>
/// Allows drawing some arbitrary content to the sequence bounds, including translation, scaling and rotation.
/// Uses the full canvas size as the source region.<para/>
/// See <see cref="DrawTransformed(KeySequence, Action{Matrix}, Action{Graphics}, RectangleF)"/> for usage.
/// </summary>
/// <param name="sequence">The target sequence whose bounds will be used as the target location on the drawing canvas.</param>
/// <param name="render">An action that receives a transformed graphics context and can render whatever it needs to.</param>
public EffectLayer DrawTransformed(KeySequence sequence, Action<Graphics> render) =>
DrawTransformed(sequence, render, new RectangleF(0, 0, Effects.canvas_width, Effects.canvas_height));

/// <summary>
/// Sets one DeviceKeys key with a specific color on the bitmap
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -152,6 +153,11 @@ public class EqualizerLayerHandler : LayerHandler<EqualizerLayerHandlerPropertie
private IWaveIn waveIn;
private static int fftLength = 1024; // NAudio fft wants powers of two! was 8192

// Base rectangle that defines the region that is used to render the audio output
// Higher values mean a higher initial resolution, but may increase memory usage (looking at you, waveform).
// KEEP X AND Y AT 0
private static readonly RectangleF sourceRect = new RectangleF(0, 0, 40, 40);

private SampleAggregator sampleAggregator = new SampleAggregator(fftLength);
private Complex[] _ffts = { };
private Complex[] _ffts_prev = { };
Expand Down Expand Up @@ -261,54 +267,44 @@ public override EffectLayer Render(IGameState gamestate)
break;
}

// The region in which to draw the equalizer.
var rect = Properties.Sequence.GetAffectedRegion(); //new RectangleF(0, 0, Effects.canvas_width, Effects.canvas_height);
if (rect.Width == 0 || rect.Height == 0)
{
// No region to draw in, prevents filling log with exceptions
return new EffectLayer();
}

if (BgEnabled)
equalizer_layer.Set(Properties.Sequence, Properties.DimColor);
// Use the new transform render method to draw the equalizer layer
equalizer_layer.DrawTransformed(Properties.Sequence, g => {
// Here we draw the equalizer relative to our source rectangle and the DrawTransformed method handles sizing and positioning it correctly for us

using (Graphics g = equalizer_layer.GetGraphics())
{
g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
// Draw a rectangle background over the entire source rect if bg is enabled
if (BgEnabled)
g.FillRectangle(new SolidBrush(Properties.DimColor), sourceRect);

int wave_step_amount = _local_fft.Length / (int)rect.Width;
g.CompositingMode = CompositingMode.SourceCopy;

int wave_step_amount = _local_fft.Length / (int)sourceRect.Width;

switch (Properties.EQType)
{
switch (Properties.EQType) {
case EqualizerType.Waveform:
for (int x = 0; x < (int)rect.Width; x++)
{
var halfHeight = sourceRect.Height / 2f;
for (int x = 0; x < (int)sourceRect.Width; x++) {
float fft_val = _local_fft.Length > x * wave_step_amount ? _local_fft[x * wave_step_amount].X : 0.0f;

Brush brush = GetBrush(fft_val, x, rect.Width);

g.DrawLine(new Pen(brush), x + rect.X, (rect.Height / 2) + rect.Y, x + rect.X, (rect.Height / 2) + rect.Y - Math.Max(Math.Min(fft_val / scaled_max_amplitude * 500.0f, rect.Height / 2), -rect.Height / 2));
Brush brush = GetBrush(fft_val, x, sourceRect.Width);
var yOff = -Math.Max(Math.Min(fft_val / scaled_max_amplitude * 500.0f, halfHeight), -halfHeight);
g.DrawLine(new Pen(brush), x, halfHeight, x, halfHeight + yOff);
}
break;

case EqualizerType.Waveform_Bottom:
for (int x = 0; x < (int)rect.Width; x++)
{
for (int x = 0; x < (int)sourceRect.Width; x++) {
float fft_val = _local_fft.Length > x * wave_step_amount ? _local_fft[x * wave_step_amount].X : 0.0f;

Brush brush = GetBrush(fft_val, x, rect.Width);

g.DrawLine(new Pen(brush), x + rect.X, rect.Height + rect.Y, x + rect.X, rect.Height + rect.Y - Math.Min(Math.Abs(fft_val / scaled_max_amplitude) * 1000.0f, rect.Height));
Brush brush = GetBrush(fft_val, x, sourceRect.Width);
g.DrawLine(new Pen(brush), x, sourceRect.Height, x, sourceRect.Height - Math.Min(Math.Abs(fft_val / scaled_max_amplitude) * 1000.0f, sourceRect.Height));
}
break;
case EqualizerType.PowerBars:

case EqualizerType.PowerBars:
//Perform FFT again to get frequencies
FastFourierTransform.FFT(false, (int)Math.Log(fftLength, 2.0), _local_fft);

while (flux_array.Count < freqs.Length)
{
flux_array.Add(0.0f);
}

int startF = 0;
int endF = 0;
Expand Down Expand Up @@ -340,7 +336,7 @@ public override EffectLayer Render(IGameState gamestate)

//System.Diagnostics.Debug.WriteLine($"flux max: {flux_array.Max()}");

float bar_width = rect.Width / (float)(freqs.Length - 1);
float bar_width = sourceRect.Width / (float)(freqs.Length - 1);

for (int f_x = 0; f_x < freq_results.Length - 1; f_x++)
{
Expand All @@ -351,20 +347,21 @@ public override EffectLayer Render(IGameState gamestate)
if (previous_freq_results[f_x] - fft_val > 0.10)
fft_val = previous_freq_results[f_x] - 0.15f;

float x = (f_x * bar_width) + rect.X;
float y = rect.Height + rect.Y;
float height = fft_val * rect.Height;
float x = f_x * bar_width;
float y = sourceRect.Height;
float height = fft_val * sourceRect.Height;

previous_freq_results[f_x] = fft_val;

Brush brush = GetBrush(-(f_x % 2), f_x, freq_results.Length - 1);

g.FillRectangle(brush, x, y - height, bar_width, height);
}

break;
}
}

}, sourceRect);


var hander = NewLayerRender;
if (hander != null)
Expand Down Expand Up @@ -420,7 +417,6 @@ private int freqToBin(float freq)

private Brush GetBrush(float value, float position, float max_position)
{
var rect = Properties.Sequence.GetAffectedRegion();
if (Properties.ViewType == EqualizerPresentationType.AlternatingColor)
{
if (value >= 0)
Expand All @@ -432,19 +428,21 @@ private Brush GetBrush(float value, float position, float max_position)
return new SolidBrush(Properties.Gradient.GetColorSpectrum().GetColorAt(position, max_position));
else if (Properties.ViewType == EqualizerPresentationType.GradientHorizontal)
{
EffectBrush e_brush = new EffectBrush(Properties.Gradient.GetColorSpectrum());
e_brush.start = new PointF(rect.X, 0);
e_brush.end = new PointF(rect.Width + rect.X, 0);
EffectBrush e_brush = new EffectBrush(Properties.Gradient.GetColorSpectrum()) {
start = PointF.Empty,
end = new PointF(sourceRect.Width, 0)
};

return e_brush.GetDrawingBrush();
}
else if (Properties.ViewType == EqualizerPresentationType.GradientColorShift)
return new SolidBrush(Properties.Gradient.GetColorSpectrum().GetColorAt(Utils.Time.GetMilliSeconds(), 1000));
else if (Properties.ViewType == EqualizerPresentationType.GradientVertical)
{
EffectBrush e_brush = new EffectBrush(Properties.Gradient.GetColorSpectrum());
e_brush.start = new PointF(0, rect.Height + rect.Y);
e_brush.end = new PointF(0, rect.Y);
EffectBrush e_brush = new EffectBrush(Properties.Gradient.GetColorSpectrum()) {
start = new PointF(0, sourceRect.Height),
end = PointF.Empty
};

return e_brush.GetDrawingBrush();
}
Expand Down