mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-09 18:36:27 +00:00
Core - Fix profile rendering and timeline bugs found by the tests (#909)
* Core - Keep the name of renamed layer effects when loading BaseLayerEffect.Load checked HasBeenRenamed before reading it from the entity, so renamed effects always fell back to the descriptor's display name. * Core - Apply layer effect filters once on layers Layer.DelegateRendering composited the layer with the pre-processed paint and also handed that paint to the brush, so color and image filters of effects applied to the brush output and again when compositing. The brush now gets a copy of the paint without those filters, like folders which apply them once. * Core - Disable folders that get suspended while enabled Layer.Update disables a suspended layer but Folder.Update only disabled a folder once its timeline finished, so a folder suspended outside of the editor kept rendering its children. * Core - Update device LEDs when rotating or scaling a device The Rotation and Scale setters only updated the RGB.NET device, the Artemis LED rectangles and the device path kept their old positions and DeviceUpdated wasn't raised until X or Y was set. * Core - Correct the docs of the Layer and Folder constructors They claimed the new element adds itself to the child collection of the parent, it doesn't and callers rely on adding it themselves. * Core - Apply layer opacity once for brushes that draw with the paint Brushes drawing with a shader or image keep the alpha of the paint they are given, so the layer opacity and opacity effects applied to the brush output and again when compositing, 50% opacity rendered at 25%. The brush now gets an opaque paint, per-LED brushes already overrode the color. The solid color test brush now draws with a shader like the real solid brush so the opacity tests cover this. * Tests - Describe expected faded colors by their opacity Adds an AtOpacity helper so assertions read as red at 50% over black instead of a hand-computed 127, and uses it for layer opacity, opacity effects, folder fades, the profile fade and semi-transparent drawing. * Core - Wrap timelines around by how far they went past the main segment Repeating timelines wrapped around by the frame time rather than by how far they went past the end of the main segment, so wrapping could be off by up to a frame. A timeline that was already past the end, like the finished timeline new layers start with, still starts the main segment over. * Tests - Mention the defaults the timeline wrap-around tests depend on
This commit is contained in:
1 parent
504845c7d2
commit
a25d0f0c9c
18 files changed
+174
-67
No files matched your search
@@ -239,14 +239,16 @@ public class EventConditionTests : RenderTestBase
|
|||||||
(Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle);
|
(Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle);
|
||||||
|
|
||||||
DataModel.Event.Trigger();
|
DataModel.Event.Trigger();
|
||||||
|
// Render defaults to a 0.1s frame, so the timeline starts playing and ends up at 0.1s
|
||||||
using RenderFrame on = Render(layer.Profile);
|
using RenderFrame on = Render(layer.Profile);
|
||||||
|
// AddLayer gives the layer a 1 second main segment and no start or end segment. 0.1s + 1.5s = 1.6s goes 0.6s past
|
||||||
|
// the end, and while toggled on the timeline wraps around rather than finishing
|
||||||
using RenderFrame stillOn = Render(layer.Profile, 1.5);
|
using RenderFrame stillOn = Render(layer.Profile, 1.5);
|
||||||
|
|
||||||
condition.IsMet.ShouldBeTrue();
|
condition.IsMet.ShouldBeTrue();
|
||||||
ShouldBeOn(on);
|
ShouldBeOn(on);
|
||||||
ShouldBeOn(stillOn);
|
ShouldBeOn(stillOn);
|
||||||
// Wrapped around the main segment rather than finishing
|
layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.6));
|
||||||
layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.5));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace Artemis.Core.Tests.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helpers to describe expected colors by what produced them rather than by their channel values.
|
||||||
|
/// </summary>
|
||||||
|
public static class ExpectedColors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the color an LED shows when <paramref name="color" /> is drawn at the given opacity over an opaque
|
||||||
|
/// background. The canvas is cleared to black before each frame, so that is the default background.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="color">The color being drawn</param>
|
||||||
|
/// <param name="opacity">The opacity in percent, like the opacity properties of layers and effects</param>
|
||||||
|
/// <param name="over">The opaque color underneath, defaults to black</param>
|
||||||
|
public static SKColor AtOpacity(this SKColor color, float opacity, SKColor? over = null)
|
||||||
|
{
|
||||||
|
SKColor background = over ?? SKColors.Black;
|
||||||
|
float alpha = opacity / 100f;
|
||||||
|
return new SKColor(
|
||||||
|
Blend(color.Red, background.Red, alpha),
|
||||||
|
Blend(color.Green, background.Green, alpha),
|
||||||
|
Blend(color.Blue, background.Blue, alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte Blend(byte top, byte bottom, float alpha)
|
||||||
|
{
|
||||||
|
return (byte) MathF.Round(top * alpha + bottom * (1 - alpha));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,10 @@ public class SolidColorTestBrush : LayerBrush<SolidColorTestBrushProperties>
|
|||||||
|
|
||||||
public override void Render(SKCanvas canvas, SKRect bounds, SKPaint paint)
|
public override void Render(SKCanvas canvas, SKRect bounds, SKPaint paint)
|
||||||
{
|
{
|
||||||
// Like the real brushes this overrides the paint color, the layer applies its opacity when compositing
|
// Like the real solid brush this draws with a shader, which keeps the alpha of the paint it is given
|
||||||
paint.Color = Properties.Color.CurrentValue;
|
using SKShader shader = SKShader.CreateColor(Properties.Color.CurrentValue);
|
||||||
|
paint.Shader = shader;
|
||||||
canvas.DrawRect(bounds, paint);
|
canvas.DrawRect(bounds, paint);
|
||||||
|
paint.Shader = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ public sealed class RenderHarness : IRenderer, IDisposable
|
|||||||
/// <returns>The layer and its brush, the brush exposes the brush properties</returns>
|
/// <returns>The layer and its brush, the brush exposes the brush properties</returns>
|
||||||
public (Layer Layer, TBrush Brush) AddLayer<TBrush>(Folder parent, IEnumerable<ArtemisLed> leds, string name = "Test layer", int? order = null) where TBrush : BaseLayerBrush
|
public (Layer Layer, TBrush Brush) AddLayer<TBrush>(Folder parent, IEnumerable<ArtemisLed> leds, string name = "Test layer", int? order = null) where TBrush : BaseLayerBrush
|
||||||
{
|
{
|
||||||
// Note: despite its documentation the Layer constructor does not add the layer to the parent
|
// The Layer constructor does not add the layer to the parent
|
||||||
Layer layer = new(parent, name);
|
Layer layer = new(parent, name);
|
||||||
parent.AddChild(layer, order);
|
parent.AddChild(layer, order);
|
||||||
layer.AddLeds(leds);
|
layer.AddLeds(leds);
|
||||||
|
|||||||
@@ -177,7 +177,8 @@ public class CompositingTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(127, 0, 127));
|
// Blue plus red is magenta, the folder fades that as a whole
|
||||||
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Magenta.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class FolderEffectTests : RenderTestBase
|
|||||||
|
|
||||||
(RenderFrame before, RenderFrame after) = RenderWithAndWithout(profile, opacity);
|
(RenderFrame before, RenderFrame after) = RenderWithAndWithout(profile, opacity);
|
||||||
|
|
||||||
after.ShouldOnlyDifferFrom(before, busy.Leds, c => Scale(c, 127));
|
after.ShouldOnlyDifferFrom(before, busy.Leds, c => c.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -79,7 +79,7 @@ public class FolderEffectTests : RenderTestBase
|
|||||||
// The nested children get both, the rest of the folder only gets the swap
|
// The nested children get both, the rest of the folder only gets the swap
|
||||||
ArtemisLed[] outerOnly = busy.Leds.Except(busy.NestedLeds).ToArray();
|
ArtemisLed[] outerOnly = busy.Leds.Except(busy.NestedLeds).ToArray();
|
||||||
foreach (ArtemisLed led in busy.NestedLeds)
|
foreach (ArtemisLed led in busy.NestedLeds)
|
||||||
after.LedColors[led].ShouldBeColor(Scale(Swap(before.LedColors[led]), 127), 3);
|
after.LedColors[led].ShouldBeColor(Swap(before.LedColors[led]).AtOpacity(50), 3);
|
||||||
after.ShouldOnlyDifferFrom(before, outerOnly, Swap, busy.NestedLeds);
|
after.ShouldOnlyDifferFrom(before, outerOnly, Swap, busy.NestedLeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ public class FolderEffectTests : RenderTestBase
|
|||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
// The children are flattened before the folder is faded, the bottom layer does not shine through
|
// The children are flattened before the folder is faded, the bottom layer does not shine through
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(0, 127, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Lime.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -125,7 +125,7 @@ public class FolderEffectTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(128, 127, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Lime.AtOpacity(50, over: SKColors.Red));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -201,11 +201,6 @@ public class FolderEffectTests : RenderTestBase
|
|||||||
return new SKColor(color.Blue, color.Green, color.Red, color.Alpha);
|
return new SKColor(color.Blue, color.Green, color.Red, color.Alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SKColor Scale(SKColor color, byte factor)
|
|
||||||
{
|
|
||||||
return new SKColor((byte) (color.Red * factor / 255), (byte) (color.Green * factor / 255), (byte) (color.Blue * factor / 255));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SKColor Multiply(SKColor color, SKColor with)
|
private static SKColor Multiply(SKColor color, SKColor with)
|
||||||
{
|
{
|
||||||
return new SKColor((byte) (color.Red * with.Red / 255), (byte) (color.Green * with.Green / 255), (byte) (color.Blue * with.Blue / 255));
|
return new SKColor((byte) (color.Red * with.Red / 255), (byte) (color.Green * with.Green / 255), (byte) (color.Blue * with.Blue / 255));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class LayerEffectTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(127, 0, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -32,11 +32,11 @@ public class LayerEffectTests : RenderTestBase
|
|||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
// 50% of 50%
|
// 50% of 50%
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(63, 0, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(25));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PreProcessColorFilter_IsAppliedTwiceOnLayers()
|
public void PreProcessColorFilter_IsAppliedOnceOnLayers()
|
||||||
{
|
{
|
||||||
Profile profile = Harness.CreateProfile();
|
Profile profile = Harness.CreateProfile();
|
||||||
(Layer layer, _) = Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
(Layer layer, _) = Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
||||||
@@ -44,11 +44,21 @@ public class LayerEffectTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
// Pinned as-is, likely unintended: Layer.DelegateRendering composites with the pre-processed paint and also hands
|
// Swapping red and blue twice would cancel out
|
||||||
// that same paint to the brush, so for any brush drawing with that paint (regular and per-LED alike) the filter
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Blue);
|
||||||
// applies to the brush output and again when compositing.
|
}
|
||||||
// Swapping red and blue twice cancels out, a hue shift effect would shift twice. Folders apply it once.
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red);
|
[Fact]
|
||||||
|
public void PreProcessColorFilter_IsAppliedOnceOnPerLedBrushes()
|
||||||
|
{
|
||||||
|
Profile profile = Harness.CreateProfile();
|
||||||
|
(Layer layer, PerLedTestBrush brush) = Harness.AddLayer<PerLedTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
||||||
|
brush.ColorSelector = (_, _) => SKColors.Red;
|
||||||
|
Harness.AddEffect<ChannelSwapTestEffect>(layer);
|
||||||
|
|
||||||
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -137,8 +147,8 @@ public class LayerEffectTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderTimeline timeline = RenderTimeline(profile, 0.25, 0.75);
|
using RenderTimeline timeline = RenderTimeline(profile, 0.25, 0.75);
|
||||||
|
|
||||||
timeline[0].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(191, 0, 0));
|
timeline[0].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(75));
|
||||||
timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(63, 0, 0));
|
timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(25));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class LayerTransformTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(127, 0, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -30,7 +30,22 @@ public class LayerTransformTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderFrame frame = Render(profile);
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(127, 0, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Opacity_AppliesOnceToGradientBrushes()
|
||||||
|
{
|
||||||
|
Profile profile = Harness.CreateProfile();
|
||||||
|
(Layer layer, GradientTestBrush brush) = Harness.AddLayer<GradientTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
||||||
|
brush.Properties.Left.SetCurrentValue(SKColors.Red);
|
||||||
|
brush.Properties.Right.SetCurrentValue(SKColors.Red);
|
||||||
|
layer.Transform.Opacity.SetCurrentValue(50);
|
||||||
|
|
||||||
|
using RenderFrame frame = Render(profile);
|
||||||
|
|
||||||
|
// The gradient shader keeps the alpha of the paint, the brush must not get the layer opacity on top of compositing
|
||||||
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ public class ProfileSerializationTests : RenderTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RoundTrip_LosesTheNameOfRenamedEffects()
|
public void RoundTrip_KeepsTheNameOfRenamedEffects()
|
||||||
{
|
{
|
||||||
Profile profile = Harness.CreateProfile();
|
Profile profile = Harness.CreateProfile();
|
||||||
(Layer layer, _) = Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
(Layer layer, _) = Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
||||||
@@ -134,10 +134,22 @@ public class ProfileSerializationTests : RenderTestBase
|
|||||||
|
|
||||||
Profile reloaded = Harness.SaveAndReload(profile);
|
Profile reloaded = Harness.SaveAndReload(profile);
|
||||||
|
|
||||||
// Pinned as-is, a bug: BaseLayerEffect.Load checks HasBeenRenamed before loading it from the entity, so it is
|
|
||||||
// always false at that point and the name falls back to the display name of the descriptor
|
|
||||||
BaseLayerEffect reloadedTint = reloaded.GetAllLayers().Single().LayerEffects.Single();
|
BaseLayerEffect reloadedTint = reloaded.GetAllLayers().Single().LayerEffects.Single();
|
||||||
reloadedTint.HasBeenRenamed.ShouldBeTrue();
|
reloadedTint.HasBeenRenamed.ShouldBeTrue();
|
||||||
|
reloadedTint.Name.ShouldBe("Renamed tint");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundTrip_UsesTheDisplayNameOfEffectsThatWereNotRenamed()
|
||||||
|
{
|
||||||
|
Profile profile = Harness.CreateProfile();
|
||||||
|
(Layer layer, _) = Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
||||||
|
Harness.AddEffect<TintTestEffect>(layer);
|
||||||
|
|
||||||
|
Profile reloaded = Harness.SaveAndReload(profile);
|
||||||
|
|
||||||
|
BaseLayerEffect reloadedTint = reloaded.GetAllLayers().Single().LayerEffects.Single();
|
||||||
|
reloadedTint.HasBeenRenamed.ShouldBeFalse();
|
||||||
reloadedTint.Name.ShouldBe("Tint");
|
reloadedTint.Name.ShouldBe("Tint");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,22 +147,21 @@ public class ProfileStructureTests : RenderTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SuspendingARenderingFolder_KeepsRenderingItsChildren()
|
public void SuspendingARenderingFolder_StopsRenderingItsChildren()
|
||||||
{
|
{
|
||||||
Profile profile = Harness.CreateProfile();
|
Profile profile = Harness.CreateProfile();
|
||||||
BusyFolder busy = BusyFolder.Create(Harness, Keyboard, profile);
|
BusyFolder busy = BusyFolder.Create(Harness, Keyboard, profile);
|
||||||
using RenderFrame before = Harness.Render(profile, TimeSpan.FromSeconds(0.1));
|
using RenderFrame before = Harness.Render(profile, TimeSpan.FromSeconds(0.1));
|
||||||
|
|
||||||
busy.Folder.Suspended = true;
|
busy.Folder.Suspended = true;
|
||||||
Harness.Render(profile, TimeSpan.FromSeconds(10)).Dispose();
|
|
||||||
using RenderFrame after = Render(profile);
|
using RenderFrame after = Render(profile);
|
||||||
|
|
||||||
// Pinned as-is, an inconsistency: Layer.Update disables a suspended layer, Folder.Update only disables a folder
|
busy.Folder.Enabled.ShouldBeFalse();
|
||||||
// once its timeline finishes, which never happens with the default always-on condition. The editor hides this by
|
foreach (ArtemisLed led in busy.Leds)
|
||||||
// disabling suspended elements itself, so it only affects code suspending folders outside of the editor
|
{
|
||||||
busy.Folder.Enabled.ShouldBeTrue();
|
before.LedColors[led].ShouldNotBe(SKColors.Black, led.RgbLed.Id.ToString());
|
||||||
foreach ((ArtemisLed led, SKColor color) in before.LedColors)
|
after.LedColors[led].ShouldBeColor(SKColors.Black, 2, led.RgbLed.Id.ToString());
|
||||||
after.LedColors[led].ShouldBeColor(color, 2, led.RgbLed.Id.ToString());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -218,7 +217,7 @@ public class ProfileStructureTests : RenderTestBase
|
|||||||
using RenderTimeline timeline = RenderTimeline(profile, 0.5, 1, 1.5);
|
using RenderTimeline timeline = RenderTimeline(profile, 0.5, 1, 1.5);
|
||||||
|
|
||||||
// The fade uses a cubic ease-in-out, which is at exactly half at the halfway point
|
// The fade uses a cubic ease-in-out, which is at exactly half at the halfway point
|
||||||
timeline[0].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(127, 0, 0));
|
timeline[0].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50));
|
||||||
timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red);
|
timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red);
|
||||||
timeline[2].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red);
|
timeline[2].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public class TimelineTests : RenderTestBase
|
|||||||
|
|
||||||
using RenderFrame frame = Render(profile, 0.5);
|
using RenderFrame frame = Render(profile, 0.5);
|
||||||
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(127, 0, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -117,19 +117,20 @@ public class TimelineTests : RenderTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AlwaysOn_WrapsAroundUsingTheFrameDeltaRatherThanTheOvershoot()
|
public void AlwaysOn_WrapsAroundByTheOvershoot()
|
||||||
{
|
{
|
||||||
Profile profile = Harness.CreateProfile();
|
Profile profile = Harness.CreateProfile();
|
||||||
(Layer layer, SolidColorTestBrush brush) = Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
(Layer layer, SolidColorTestBrush brush) = Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
|
||||||
layer.Timeline.MainSegmentLength = TimeSpan.FromSeconds(2);
|
layer.Timeline.MainSegmentLength = TimeSpan.FromSeconds(2);
|
||||||
AddKeyframes(brush.Properties.Color, (0, SKColors.Red), (2, SKColors.Blue));
|
AddKeyframes(brush.Properties.Color, (0, SKColors.Red), (2, SKColors.Blue));
|
||||||
|
|
||||||
// Going from 1.5s to 2.5s overshoots the 2s main segment by 0.5s, the timeline lands on 1s instead of 0.5s.
|
// Going from 1.5s to 2.5s overshoots the 2s main segment by 0.5s. A new timeline has no start segment, so the
|
||||||
// Pinned as-is: with real frame times (~33ms) the difference is at most a single frame.
|
// main segment starts at 0s and the timeline lands on 0.5s
|
||||||
using RenderTimeline timeline = RenderTimeline(profile, 1.5, 2.5);
|
using RenderTimeline timeline = RenderTimeline(profile, 1.5, 2.5);
|
||||||
|
|
||||||
layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(1));
|
layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.5));
|
||||||
timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(Purple);
|
// 0.5s is a quarter of the way from red to blue
|
||||||
|
timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(191, 0, 64));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -95,28 +95,40 @@ public class SurfaceTests : RenderTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RotatingADevice_DoesNotUpdateItsLedsUntilItIsMoved()
|
public void RotatingADevice_UpdatesItsLeds()
|
||||||
{
|
{
|
||||||
ArtemisLed a = Led(LedId.Keyboard_A);
|
ArtemisLed a = Led(LedId.Keyboard_A);
|
||||||
SKRect beforeRotation = a.AbsoluteRectangle;
|
SKRect beforeRotation = a.AbsoluteRectangle;
|
||||||
|
int updates = 0;
|
||||||
|
Keyboard.DeviceUpdated += (_, _) => updates++;
|
||||||
|
|
||||||
Keyboard.Rotation = 90;
|
Keyboard.Rotation = 90;
|
||||||
|
|
||||||
// Pinned as-is: the Rotation and Scale setters only update the RGB.NET device. The Artemis LEDs, and with them the
|
|
||||||
// texture, keep their old positions until X or Y is set. The device properties dialog gets away with this because
|
|
||||||
// it reloads the device layout after saving
|
|
||||||
a.AbsoluteRectangle.ShouldBe(beforeRotation);
|
|
||||||
Math.Abs(a.RgbLed.AbsoluteBoundary.Location.X - beforeRotation.Left).ShouldBeGreaterThan(1);
|
|
||||||
|
|
||||||
Keyboard.X = Keyboard.X;
|
|
||||||
a.AbsoluteRectangle.ShouldNotBe(beforeRotation);
|
a.AbsoluteRectangle.ShouldNotBe(beforeRotation);
|
||||||
|
a.AbsoluteRectangle.Left.ShouldBe(a.RgbLed.AbsoluteBoundary.Location.X, 1);
|
||||||
|
a.AbsoluteRectangle.Top.ShouldBe(a.RgbLed.AbsoluteBoundary.Location.Y, 1);
|
||||||
|
updates.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScalingADevice_UpdatesItsLeds()
|
||||||
|
{
|
||||||
|
ArtemisLed a = Led(LedId.Keyboard_A);
|
||||||
|
SKRect beforeScaling = a.AbsoluteRectangle;
|
||||||
|
int updates = 0;
|
||||||
|
Keyboard.DeviceUpdated += (_, _) => updates++;
|
||||||
|
|
||||||
|
Keyboard.Scale = 2;
|
||||||
|
|
||||||
|
// Rounded outwards to the render scale, so slightly wider than twice the size
|
||||||
|
a.AbsoluteRectangle.Width.ShouldBeInRange(beforeScaling.Width * 2, beforeScaling.Width * 2 + 4);
|
||||||
|
updates.ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RotatedDevice_SamplesFromItsRotatedLedPositions()
|
public void RotatedDevice_SamplesFromItsRotatedLedPositions()
|
||||||
{
|
{
|
||||||
Keyboard.Rotation = 90;
|
Keyboard.Rotation = 90;
|
||||||
Keyboard.X = Keyboard.X;
|
|
||||||
SKRect rotatedA = Led(LedId.Keyboard_A).AbsoluteRectangle;
|
SKRect rotatedA = Led(LedId.Keyboard_A).AbsoluteRectangle;
|
||||||
|
|
||||||
using RenderFrame frame = Harness.Render(canvas => Fill(canvas, rotatedA, SKColors.Red));
|
using RenderFrame frame = Harness.Render(canvas => Fill(canvas, rotatedA, SKColors.Red));
|
||||||
@@ -130,7 +142,6 @@ public class SurfaceTests : RenderTestBase
|
|||||||
public void ScaledDevice_SamplesFromItsScaledLedPositions()
|
public void ScaledDevice_SamplesFromItsScaledLedPositions()
|
||||||
{
|
{
|
||||||
Keyboard.Scale = 2;
|
Keyboard.Scale = 2;
|
||||||
Keyboard.X = Keyboard.X;
|
|
||||||
SKRect scaledA = Led(LedId.Keyboard_A).AbsoluteRectangle;
|
SKRect scaledA = Led(LedId.Keyboard_A).AbsoluteRectangle;
|
||||||
|
|
||||||
using RenderFrame frame = Harness.Render(canvas => Fill(canvas, scaledA, SKColors.Red));
|
using RenderFrame frame = Harness.Render(canvas => Fill(canvas, scaledA, SKColors.Red));
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ public class TextureSamplingTests : RenderTestBase
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SemiTransparentDrawing_IsBlendedOntoBlack()
|
public void SemiTransparentDrawing_IsBlendedOntoBlack()
|
||||||
{
|
{
|
||||||
using RenderFrame frame = Harness.Render(canvas => Fill(canvas, Led(LedId.Keyboard_A).AbsoluteRectangle, new SKColor(255, 0, 0, 128)));
|
using RenderFrame frame = Harness.Render(canvas => Fill(canvas, Led(LedId.Keyboard_A).AbsoluteRectangle, SKColors.Red.WithAlpha(128)));
|
||||||
SaveOutput(frame);
|
SaveOutput(frame);
|
||||||
|
|
||||||
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(128, 0, 0));
|
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ public sealed class Folder : RenderProfileElement
|
|||||||
private bool _isExpanded;
|
private bool _isExpanded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the <see cref="Folder" /> class and adds itself to the child collection of the provided
|
/// Creates a new instance of the <see cref="Folder" /> class with the provided <paramref name="parent" />.
|
||||||
/// <paramref name="parent" />
|
/// <para>
|
||||||
|
/// The folder is not added to the child collection of the parent, use <see cref="ProfileElement.AddChild" /> to
|
||||||
|
/// add it.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parent">The parent of the folder</param>
|
/// <param name="parent">The parent of the folder</param>
|
||||||
/// <param name="name">The name of the folder</param>
|
/// <param name="name">The name of the folder</param>
|
||||||
@@ -106,7 +109,7 @@ public sealed class Folder : RenderProfileElement
|
|||||||
|
|
||||||
if (ShouldBeEnabled)
|
if (ShouldBeEnabled)
|
||||||
Enable();
|
Enable();
|
||||||
else if (Timeline.IsFinished)
|
else if (Suspended || Timeline.IsFinished)
|
||||||
Disable();
|
Disable();
|
||||||
|
|
||||||
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
|
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ public sealed class Layer : RenderProfileElement
|
|||||||
private List<LedEntity> _missingLeds = [];
|
private List<LedEntity> _missingLeds = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the <see cref="Layer" /> class and adds itself to the child collection of the provided
|
/// Creates a new instance of the <see cref="Layer" /> class with the provided <paramref name="parent" />.
|
||||||
/// <paramref name="parent" />
|
/// <para>
|
||||||
|
/// The layer is not added to the child collection of the parent, use <see cref="ProfileElement.AddChild" /> to
|
||||||
|
/// add it.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parent">The parent of the layer</param>
|
/// <param name="parent">The parent of the layer</param>
|
||||||
/// <param name="name">The name of the layer</param>
|
/// <param name="name">The name of the layer</param>
|
||||||
@@ -663,7 +666,14 @@ public sealed class Layer : RenderProfileElement
|
|||||||
// Restore the blend mode before doing the actual render
|
// Restore the blend mode before doing the actual render
|
||||||
layerPaint.BlendMode = SKBlendMode.SrcOver;
|
layerPaint.BlendMode = SKBlendMode.SrcOver;
|
||||||
|
|
||||||
LayerBrush.InternalRender(canvas, bounds, layerPaint);
|
// The opacity and the filters of the effects are applied when compositing the layer, the brush gets an opaque
|
||||||
|
// paint without filters so they don't apply to the brush output a second time
|
||||||
|
using SKPaint brushPaint = layerPaint.Clone();
|
||||||
|
brushPaint.Color = brushPaint.Color.WithAlpha(255);
|
||||||
|
brushPaint.ColorFilter = null;
|
||||||
|
brushPaint.ImageFilter = null;
|
||||||
|
|
||||||
|
LayerBrush.InternalRender(canvas, bounds, brushPaint);
|
||||||
|
|
||||||
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ public class Timeline : CorePropertyChanged, IStorageModel
|
|||||||
if (IsOverridden)
|
if (IsOverridden)
|
||||||
throw new ArtemisCoreException("Can't update an overridden timeline, call ClearOverride first.");
|
throw new ArtemisCoreException("Can't update an overridden timeline, call ClearOverride first.");
|
||||||
|
|
||||||
|
TimeSpan previousPosition = Position;
|
||||||
Delta += delta;
|
Delta += delta;
|
||||||
Position += delta;
|
Position += delta;
|
||||||
|
|
||||||
@@ -251,10 +252,15 @@ public class Timeline : CorePropertyChanged, IStorageModel
|
|||||||
|
|
||||||
// If the main segment has no length, simply stick to the start of the segment
|
// If the main segment has no length, simply stick to the start of the segment
|
||||||
if (MainSegmentLength == TimeSpan.Zero)
|
if (MainSegmentLength == TimeSpan.Zero)
|
||||||
|
{
|
||||||
Position = MainSegmentStartPosition;
|
Position = MainSegmentStartPosition;
|
||||||
// Ensure wrapping back around retains the delta time
|
return;
|
||||||
else
|
}
|
||||||
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
|
|
||||||
|
// Wrap around by as much as the timeline went past the end of the main segment. If it was already past the end
|
||||||
|
// (for example a timeline that had finished), start the main segment over instead
|
||||||
|
TimeSpan overshoot = previousPosition <= MainSegmentEndPosition ? Position - MainSegmentEndPosition : delta;
|
||||||
|
Position = MainSegmentStartPosition + TimeSpan.FromTicks(overshoot.Ticks % MainSegmentLength.Ticks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,11 @@ public class ArtemisDevice : CorePropertyChanged
|
|||||||
{
|
{
|
||||||
DeviceEntity.Rotation = value;
|
DeviceEntity.Rotation = value;
|
||||||
if (RgbDevice.Surface != null)
|
if (RgbDevice.Surface != null)
|
||||||
|
{
|
||||||
RgbDevice.Rotation = DeviceEntity.Rotation;
|
RgbDevice.Rotation = DeviceEntity.Rotation;
|
||||||
|
CalculateRenderProperties();
|
||||||
|
}
|
||||||
|
|
||||||
OnPropertyChanged(nameof(Rotation));
|
OnPropertyChanged(nameof(Rotation));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +219,11 @@ public class ArtemisDevice : CorePropertyChanged
|
|||||||
{
|
{
|
||||||
DeviceEntity.Scale = value;
|
DeviceEntity.Scale = value;
|
||||||
if (RgbDevice.Surface != null)
|
if (RgbDevice.Surface != null)
|
||||||
|
{
|
||||||
RgbDevice.Scale = DeviceEntity.Scale;
|
RgbDevice.Scale = DeviceEntity.Scale;
|
||||||
|
CalculateRenderProperties();
|
||||||
|
}
|
||||||
|
|
||||||
OnPropertyChanged(nameof(Scale));
|
OnPropertyChanged(nameof(Scale));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageMod
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Load()
|
public void Load()
|
||||||
{
|
{
|
||||||
Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName;
|
|
||||||
HasBeenRenamed = LayerEffectEntity.HasBeenRenamed;
|
HasBeenRenamed = LayerEffectEntity.HasBeenRenamed;
|
||||||
|
Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName;
|
||||||
Order = LayerEffectEntity.Order;
|
Order = LayerEffectEntity.Order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user