diff --git a/src/Artemis.Core.Tests/Conditions/EventConditionTests.cs b/src/Artemis.Core.Tests/Conditions/EventConditionTests.cs index a9dfc3a84..c5bb7edf6 100644 --- a/src/Artemis.Core.Tests/Conditions/EventConditionTests.cs +++ b/src/Artemis.Core.Tests/Conditions/EventConditionTests.cs @@ -239,14 +239,16 @@ public class EventConditionTests : RenderTestBase (Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle); 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); + // 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); condition.IsMet.ShouldBeTrue(); ShouldBeOn(on); ShouldBeOn(stillOn); - // Wrapped around the main segment rather than finishing - layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.5)); + layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.6)); } [Fact] diff --git a/src/Artemis.Core.Tests/Infrastructure/ExpectedColors.cs b/src/Artemis.Core.Tests/Infrastructure/ExpectedColors.cs new file mode 100644 index 000000000..c08e40093 --- /dev/null +++ b/src/Artemis.Core.Tests/Infrastructure/ExpectedColors.cs @@ -0,0 +1,32 @@ +using SkiaSharp; + +namespace Artemis.Core.Tests.Infrastructure; + +/// +/// Helpers to describe expected colors by what produced them rather than by their channel values. +/// +public static class ExpectedColors +{ + /// + /// Returns the color an LED shows when 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. + /// + /// The color being drawn + /// The opacity in percent, like the opacity properties of layers and effects + /// The opaque color underneath, defaults to black + 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)); + } +} diff --git a/src/Artemis.Core.Tests/Infrastructure/Plugins/LayerBrushes/SolidColorTestBrush.cs b/src/Artemis.Core.Tests/Infrastructure/Plugins/LayerBrushes/SolidColorTestBrush.cs index 504df0e3c..79922550e 100644 --- a/src/Artemis.Core.Tests/Infrastructure/Plugins/LayerBrushes/SolidColorTestBrush.cs +++ b/src/Artemis.Core.Tests/Infrastructure/Plugins/LayerBrushes/SolidColorTestBrush.cs @@ -19,8 +19,10 @@ public class SolidColorTestBrush : LayerBrush 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 - paint.Color = Properties.Color.CurrentValue; + // Like the real solid brush this draws with a shader, which keeps the alpha of the paint it is given + using SKShader shader = SKShader.CreateColor(Properties.Color.CurrentValue); + paint.Shader = shader; canvas.DrawRect(bounds, paint); + paint.Shader = null; } } diff --git a/src/Artemis.Core.Tests/Infrastructure/Rendering/RenderHarness.cs b/src/Artemis.Core.Tests/Infrastructure/Rendering/RenderHarness.cs index d2aec92d1..91e96baf5 100644 --- a/src/Artemis.Core.Tests/Infrastructure/Rendering/RenderHarness.cs +++ b/src/Artemis.Core.Tests/Infrastructure/Rendering/RenderHarness.cs @@ -122,7 +122,7 @@ public sealed class RenderHarness : IRenderer, IDisposable /// The layer and its brush, the brush exposes the brush properties public (Layer Layer, TBrush Brush) AddLayer(Folder parent, IEnumerable 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); parent.AddChild(layer, order); layer.AddLeds(leds); diff --git a/src/Artemis.Core.Tests/Profiles/CompositingTests.cs b/src/Artemis.Core.Tests/Profiles/CompositingTests.cs index e404f22ee..8a64471c1 100644 --- a/src/Artemis.Core.Tests/Profiles/CompositingTests.cs +++ b/src/Artemis.Core.Tests/Profiles/CompositingTests.cs @@ -177,7 +177,8 @@ public class CompositingTests : RenderTestBase 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 diff --git a/src/Artemis.Core.Tests/Profiles/FolderEffectTests.cs b/src/Artemis.Core.Tests/Profiles/FolderEffectTests.cs index d164eb0f1..ba109a761 100644 --- a/src/Artemis.Core.Tests/Profiles/FolderEffectTests.cs +++ b/src/Artemis.Core.Tests/Profiles/FolderEffectTests.cs @@ -38,7 +38,7 @@ public class FolderEffectTests : RenderTestBase (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] @@ -79,7 +79,7 @@ public class FolderEffectTests : RenderTestBase // The nested children get both, the rest of the folder only gets the swap ArtemisLed[] outerOnly = busy.Leds.Except(busy.NestedLeds).ToArray(); 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); } @@ -109,7 +109,7 @@ public class FolderEffectTests : RenderTestBase using RenderFrame frame = Render(profile); // 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] @@ -125,7 +125,7 @@ public class FolderEffectTests : RenderTestBase 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] @@ -201,11 +201,6 @@ public class FolderEffectTests : RenderTestBase 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) { return new SKColor((byte) (color.Red * with.Red / 255), (byte) (color.Green * with.Green / 255), (byte) (color.Blue * with.Blue / 255)); diff --git a/src/Artemis.Core.Tests/Profiles/LayerEffectTests.cs b/src/Artemis.Core.Tests/Profiles/LayerEffectTests.cs index 37d043011..fddede447 100644 --- a/src/Artemis.Core.Tests/Profiles/LayerEffectTests.cs +++ b/src/Artemis.Core.Tests/Profiles/LayerEffectTests.cs @@ -18,7 +18,7 @@ public class LayerEffectTests : RenderTestBase 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] @@ -32,11 +32,11 @@ public class LayerEffectTests : RenderTestBase using RenderFrame frame = Render(profile); // 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] - public void PreProcessColorFilter_IsAppliedTwiceOnLayers() + public void PreProcessColorFilter_IsAppliedOnceOnLayers() { Profile profile = Harness.CreateProfile(); (Layer layer, _) = Harness.AddLayer(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]); @@ -44,11 +44,21 @@ public class LayerEffectTests : RenderTestBase using RenderFrame frame = Render(profile); - // Pinned as-is, likely unintended: Layer.DelegateRendering composites with the pre-processed paint and also hands - // that same paint to the brush, so for any brush drawing with that paint (regular and per-LED alike) the filter - // 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); + // Swapping red and blue twice would cancel out + frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Blue); + } + + [Fact] + public void PreProcessColorFilter_IsAppliedOnceOnPerLedBrushes() + { + Profile profile = Harness.CreateProfile(); + (Layer layer, PerLedTestBrush brush) = Harness.AddLayer(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]); + brush.ColorSelector = (_, _) => SKColors.Red; + Harness.AddEffect(layer); + + using RenderFrame frame = Render(profile); + + frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Blue); } [Fact] @@ -137,8 +147,8 @@ public class LayerEffectTests : RenderTestBase using RenderTimeline timeline = RenderTimeline(profile, 0.25, 0.75); - timeline[0].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(191, 0, 0)); - timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(63, 0, 0)); + timeline[0].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(75)); + timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(25)); } [Fact] diff --git a/src/Artemis.Core.Tests/Profiles/LayerTransformTests.cs b/src/Artemis.Core.Tests/Profiles/LayerTransformTests.cs index f013913cb..9ecd37e33 100644 --- a/src/Artemis.Core.Tests/Profiles/LayerTransformTests.cs +++ b/src/Artemis.Core.Tests/Profiles/LayerTransformTests.cs @@ -17,7 +17,7 @@ public class LayerTransformTests : RenderTestBase 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] @@ -30,7 +30,22 @@ public class LayerTransformTests : RenderTestBase 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(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] diff --git a/src/Artemis.Core.Tests/Profiles/ProfileSerializationTests.cs b/src/Artemis.Core.Tests/Profiles/ProfileSerializationTests.cs index 187cc3cc8..e34dbf0e0 100644 --- a/src/Artemis.Core.Tests/Profiles/ProfileSerializationTests.cs +++ b/src/Artemis.Core.Tests/Profiles/ProfileSerializationTests.cs @@ -124,7 +124,7 @@ public class ProfileSerializationTests : RenderTestBase } [Fact] - public void RoundTrip_LosesTheNameOfRenamedEffects() + public void RoundTrip_KeepsTheNameOfRenamedEffects() { Profile profile = Harness.CreateProfile(); (Layer layer, _) = Harness.AddLayer(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]); @@ -134,10 +134,22 @@ public class ProfileSerializationTests : RenderTestBase 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(); reloadedTint.HasBeenRenamed.ShouldBeTrue(); + reloadedTint.Name.ShouldBe("Renamed tint"); + } + + [Fact] + public void RoundTrip_UsesTheDisplayNameOfEffectsThatWereNotRenamed() + { + Profile profile = Harness.CreateProfile(); + (Layer layer, _) = Harness.AddLayer(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]); + Harness.AddEffect(layer); + + Profile reloaded = Harness.SaveAndReload(profile); + + BaseLayerEffect reloadedTint = reloaded.GetAllLayers().Single().LayerEffects.Single(); + reloadedTint.HasBeenRenamed.ShouldBeFalse(); reloadedTint.Name.ShouldBe("Tint"); } diff --git a/src/Artemis.Core.Tests/Profiles/ProfileStructureTests.cs b/src/Artemis.Core.Tests/Profiles/ProfileStructureTests.cs index 0c9f86267..bfbce3314 100644 --- a/src/Artemis.Core.Tests/Profiles/ProfileStructureTests.cs +++ b/src/Artemis.Core.Tests/Profiles/ProfileStructureTests.cs @@ -147,22 +147,21 @@ public class ProfileStructureTests : RenderTestBase } [Fact] - public void SuspendingARenderingFolder_KeepsRenderingItsChildren() + public void SuspendingARenderingFolder_StopsRenderingItsChildren() { Profile profile = Harness.CreateProfile(); BusyFolder busy = BusyFolder.Create(Harness, Keyboard, profile); using RenderFrame before = Harness.Render(profile, TimeSpan.FromSeconds(0.1)); busy.Folder.Suspended = true; - Harness.Render(profile, TimeSpan.FromSeconds(10)).Dispose(); using RenderFrame after = Render(profile); - // Pinned as-is, an inconsistency: Layer.Update disables a suspended layer, Folder.Update only disables a folder - // once its timeline finishes, which never happens with the default always-on condition. The editor hides this by - // disabling suspended elements itself, so it only affects code suspending folders outside of the editor - busy.Folder.Enabled.ShouldBeTrue(); - foreach ((ArtemisLed led, SKColor color) in before.LedColors) - after.LedColors[led].ShouldBeColor(color, 2, led.RgbLed.Id.ToString()); + busy.Folder.Enabled.ShouldBeFalse(); + foreach (ArtemisLed led in busy.Leds) + { + before.LedColors[led].ShouldNotBe(SKColors.Black, led.RgbLed.Id.ToString()); + after.LedColors[led].ShouldBeColor(SKColors.Black, 2, led.RgbLed.Id.ToString()); + } } [Fact] @@ -218,7 +217,7 @@ public class ProfileStructureTests : RenderTestBase 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 - 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[2].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red); } diff --git a/src/Artemis.Core.Tests/Profiles/TimelineTests.cs b/src/Artemis.Core.Tests/Profiles/TimelineTests.cs index 57c4ab80c..674f4a08a 100644 --- a/src/Artemis.Core.Tests/Profiles/TimelineTests.cs +++ b/src/Artemis.Core.Tests/Profiles/TimelineTests.cs @@ -92,7 +92,7 @@ public class TimelineTests : RenderTestBase 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 @@ -117,19 +117,20 @@ public class TimelineTests : RenderTestBase } [Fact] - public void AlwaysOn_WrapsAroundUsingTheFrameDeltaRatherThanTheOvershoot() + public void AlwaysOn_WrapsAroundByTheOvershoot() { Profile profile = Harness.CreateProfile(); (Layer layer, SolidColorTestBrush brush) = Harness.AddLayer(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]); layer.Timeline.MainSegmentLength = TimeSpan.FromSeconds(2); 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. - // Pinned as-is: with real frame times (~33ms) the difference is at most a single frame. + // Going from 1.5s to 2.5s overshoots the 2s main segment by 0.5s. A new timeline has no start segment, so the + // main segment starts at 0s and the timeline lands on 0.5s using RenderTimeline timeline = RenderTimeline(profile, 1.5, 2.5); - layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(1)); - timeline[1].GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(Purple); + layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.5)); + // 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] diff --git a/src/Artemis.Core.Tests/Rendering/SurfaceTests.cs b/src/Artemis.Core.Tests/Rendering/SurfaceTests.cs index 268abd02d..1144db283 100644 --- a/src/Artemis.Core.Tests/Rendering/SurfaceTests.cs +++ b/src/Artemis.Core.Tests/Rendering/SurfaceTests.cs @@ -95,28 +95,40 @@ public class SurfaceTests : RenderTestBase } [Fact] - public void RotatingADevice_DoesNotUpdateItsLedsUntilItIsMoved() + public void RotatingADevice_UpdatesItsLeds() { ArtemisLed a = Led(LedId.Keyboard_A); SKRect beforeRotation = a.AbsoluteRectangle; + int updates = 0; + Keyboard.DeviceUpdated += (_, _) => updates++; 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.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] public void RotatedDevice_SamplesFromItsRotatedLedPositions() { Keyboard.Rotation = 90; - Keyboard.X = Keyboard.X; SKRect rotatedA = Led(LedId.Keyboard_A).AbsoluteRectangle; using RenderFrame frame = Harness.Render(canvas => Fill(canvas, rotatedA, SKColors.Red)); @@ -130,7 +142,6 @@ public class SurfaceTests : RenderTestBase public void ScaledDevice_SamplesFromItsScaledLedPositions() { Keyboard.Scale = 2; - Keyboard.X = Keyboard.X; SKRect scaledA = Led(LedId.Keyboard_A).AbsoluteRectangle; using RenderFrame frame = Harness.Render(canvas => Fill(canvas, scaledA, SKColors.Red)); diff --git a/src/Artemis.Core.Tests/Rendering/TextureSamplingTests.cs b/src/Artemis.Core.Tests/Rendering/TextureSamplingTests.cs index bf78dc908..f02d9f368 100644 --- a/src/Artemis.Core.Tests/Rendering/TextureSamplingTests.cs +++ b/src/Artemis.Core.Tests/Rendering/TextureSamplingTests.cs @@ -63,10 +63,10 @@ public class TextureSamplingTests : RenderTestBase [Fact] 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); - frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(new SKColor(128, 0, 0)); + frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red.AtOpacity(50)); } [Fact] diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 548931484..dc9fe1160 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -16,8 +16,11 @@ public sealed class Folder : RenderProfileElement private bool _isExpanded; /// - /// Creates a new instance of the class and adds itself to the child collection of the provided - /// + /// Creates a new instance of the class with the provided . + /// + /// The folder is not added to the child collection of the parent, use to + /// add it. + /// /// /// The parent of the folder /// The name of the folder @@ -106,7 +109,7 @@ public sealed class Folder : RenderProfileElement if (ShouldBeEnabled) Enable(); - else if (Timeline.IsFinished) + else if (Suspended || Timeline.IsFinished) Disable(); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 88f605584..617035b5a 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -28,8 +28,11 @@ public sealed class Layer : RenderProfileElement private List _missingLeds = []; /// - /// Creates a new instance of the class and adds itself to the child collection of the provided - /// + /// Creates a new instance of the class with the provided . + /// + /// The layer is not added to the child collection of the parent, use to + /// add it. + /// /// /// The parent of the layer /// The name of the layer @@ -663,7 +666,14 @@ public sealed class Layer : RenderProfileElement // Restore the blend mode before doing the actual render 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) { diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs index 6ba2b7001..b827aa5df 100644 --- a/src/Artemis.Core/Models/Profile/Timeline.cs +++ b/src/Artemis.Core/Models/Profile/Timeline.cs @@ -243,6 +243,7 @@ public class Timeline : CorePropertyChanged, IStorageModel if (IsOverridden) throw new ArtemisCoreException("Can't update an overridden timeline, call ClearOverride first."); + TimeSpan previousPosition = Position; Delta += 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 (MainSegmentLength == TimeSpan.Zero) + { Position = MainSegmentStartPosition; - // Ensure wrapping back around retains the delta time - else - Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); + return; + } + + // 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); } } diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 3874d3469..812f86147 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -200,7 +200,11 @@ public class ArtemisDevice : CorePropertyChanged { DeviceEntity.Rotation = value; if (RgbDevice.Surface != null) + { RgbDevice.Rotation = DeviceEntity.Rotation; + CalculateRenderProperties(); + } + OnPropertyChanged(nameof(Rotation)); } } @@ -215,7 +219,11 @@ public class ArtemisDevice : CorePropertyChanged { DeviceEntity.Scale = value; if (RgbDevice.Surface != null) + { RgbDevice.Scale = DeviceEntity.Scale; + CalculateRenderProperties(); + } + OnPropertyChanged(nameof(Scale)); } } diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index 651df32f9..d615f150e 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -217,8 +217,8 @@ public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageMod /// public void Load() { - Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName; HasBeenRenamed = LayerEffectEntity.HasBeenRenamed; + Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName; Order = LayerEffectEntity.Order; }