diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 8283d2827..3389ff82e 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -55,11 +55,11 @@ namespace Artemis.Core.Models.Profile profileElement.Update(deltaTime); } - public override void Render(double deltaTime, SKCanvas canvas) + public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { // Folders don't render but their children do foreach (var profileElement in Children) - profileElement.Render(deltaTime, canvas); + profileElement.Render(deltaTime, canvas, canvasInfo); } public Folder AddFolder(string name) diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index d10a149ca..361e32d95 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -72,7 +72,7 @@ namespace Artemis.Core.Models.Profile /// public SKPath Path { - get => _path; + get => _path != null ? new SKPath(_path) : null; private set { _path = value; @@ -231,7 +231,7 @@ namespace Artemis.Core.Models.Profile LayerBrush?.Update(deltaTime); } - public override void Render(double deltaTime, SKCanvas canvas) + public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { if (Path == null || LayerShape?.Path == null) return; @@ -247,10 +247,10 @@ namespace Artemis.Core.Models.Profile switch (FillTypeProperty.CurrentValue) { case LayerFillType.Stretch: - StretchRender(canvas, paint); + StretchRender(canvas, canvasInfo, paint); break; case LayerFillType.Clip: - ClipRender(canvas, paint, true); + ClipRender(canvas, canvasInfo, paint); break; default: throw new ArgumentOutOfRangeException(); @@ -260,7 +260,7 @@ namespace Artemis.Core.Models.Profile canvas.Restore(); } - private void StretchRender(SKCanvas canvas, SKPaint paint) + private void StretchRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint) { // Apply transformations var sizeProperty = ScaleProperty.CurrentValue; @@ -278,10 +278,10 @@ namespace Artemis.Core.Models.Profile canvas.Scale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y); canvas.Translate(x, y); - LayerBrush?.Render(canvas, new SKPath(LayerShape.Path), paint); + LayerBrush?.Render(canvas, canvasInfo, new SKPath(LayerShape.Path), paint); } - private void ClipRender(SKCanvas canvas, SKPaint paint, bool rotatePath) + private void ClipRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint) { // Apply transformations var sizeProperty = ScaleProperty.CurrentValue; @@ -303,10 +303,17 @@ namespace Artemis.Core.Models.Profile canvas.RotateDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y); canvas.Translate(x, y); - // Render the entire layer, the clip will ensure the shape is matched + // Render the layer in the largest required bounds, this still creates stretching in some situations + // but the only alternative I see right now is always forcing brushes to render on the entire canvas + var boundsRect = new SKRect( + Math.Min(clipPath.Bounds.Left - x, Bounds.Left - x), + Math.Min(clipPath.Bounds.Top - y, Bounds.Top - y), + Math.Max(clipPath.Bounds.Right - x, Bounds.Right - x), + Math.Max(clipPath.Bounds.Bottom - y, Bounds.Bottom - y) + ); var renderPath = new SKPath(); - renderPath.AddRect(Path.Bounds); - LayerBrush?.Render(canvas, renderPath, paint); + renderPath.AddRect(boundsRect); + LayerBrush?.Render(canvas, canvasInfo, renderPath, paint); } internal void CalculateRenderProperties() @@ -332,7 +339,7 @@ namespace Artemis.Core.Models.Profile OnRenderPropertiesUpdated(); } - private SKPoint GetLayerAnchorPosition() + internal SKPoint GetLayerAnchorPosition() { var positionProperty = PositionProperty.CurrentValue; diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 6cb9da5e2..2302fc436 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -57,7 +57,7 @@ namespace Artemis.Core.Models.Profile } } - public override void Render(double deltaTime, SKCanvas canvas) + public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { lock (this) { @@ -65,7 +65,7 @@ namespace Artemis.Core.Models.Profile throw new ArtemisCoreException($"Cannot render inactive profile: {this}"); foreach (var profileElement in Children) - profileElement.Render(deltaTime, canvas); + profileElement.Render(deltaTime, canvas, canvasInfo); } } diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 0718aa2f7..89d74aa99 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -44,7 +44,7 @@ namespace Artemis.Core.Models.Profile /// /// Renders the element /// - public abstract void Render(double deltaTime, SKCanvas canvas); + public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo); public List GetAllFolders() { diff --git a/src/Artemis.Core/Plugins/Abstract/Module.cs b/src/Artemis.Core/Plugins/Abstract/Module.cs index 9050f9646..67b8e5f97 100644 --- a/src/Artemis.Core/Plugins/Abstract/Module.cs +++ b/src/Artemis.Core/Plugins/Abstract/Module.cs @@ -44,7 +44,8 @@ namespace Artemis.Core.Plugins.Abstract /// Time since the last render /// The RGB Surface to render to /// - public abstract void Render(double deltaTime, ArtemisSurface surface, SKCanvas canvas); + /// + public abstract void Render(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo); /// /// Called when the module's view model is being show, return view models here to create tabs for them diff --git a/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs b/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs index d7a0ab15f..72c07af99 100644 --- a/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Abstract/ProfileModule.cs @@ -31,12 +31,12 @@ namespace Artemis.Core.Plugins.Abstract } /// - public override void Render(double deltaTime, ArtemisSurface surface, SKCanvas canvas) + public override void Render(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo) { lock (this) { // Render the profile - ActiveProfile?.Render(deltaTime, canvas); + ActiveProfile?.Render(deltaTime, canvas, canvasInfo); } } diff --git a/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs index 43305b4a8..4c9e66790 100644 --- a/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs @@ -37,9 +37,10 @@ namespace Artemis.Core.Plugins.LayerBrush /// Called during rendering, in the order configured on the layer /// /// The layer canvas + /// /// The path to be filled, represents the shape /// The paint to be used to fill the shape - public virtual void Render(SKCanvas canvas, SKPath path, SKPaint paint) + public virtual void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint) { } diff --git a/src/Artemis.Core/RGB.NET/BitmapBrush.cs b/src/Artemis.Core/RGB.NET/BitmapBrush.cs index 3e37bba20..bae50acb3 100644 --- a/src/Artemis.Core/RGB.NET/BitmapBrush.cs +++ b/src/Artemis.Core/RGB.NET/BitmapBrush.cs @@ -86,32 +86,28 @@ namespace Artemis.Core.RGB.NET var sampleSize = _sampleSizeSetting.Value; var sampleDepth = Math.Sqrt(sampleSize).RoundToInt(); + var bitmapWidth = Bitmap.Width; + var bitmapHeight = Bitmap.Height; + foreach (var renderTarget in renderTargets) { // SKRect has all the good stuff we need - var rect = SKRect.Create( - (float) ((renderTarget.Rectangle.Location.X + 4) * Scale.Horizontal), - (float) ((renderTarget.Rectangle.Location.Y + 4) * Scale.Vertical), - (float) ((renderTarget.Rectangle.Size.Width - 8) * Scale.Horizontal), - (float) ((renderTarget.Rectangle.Size.Height - 8) * Scale.Vertical) - ); + var left = (int) ((renderTarget.Rectangle.Location.X + 4) * Scale.Horizontal); + var top = (int) ((renderTarget.Rectangle.Location.Y + 4) * Scale.Vertical); + var width = (int) ((renderTarget.Rectangle.Size.Width - 8) * Scale.Horizontal); + var height = (int) ((renderTarget.Rectangle.Size.Height - 8) * Scale.Vertical); - var verticalSteps = rect.Height / (sampleDepth - 1); - var horizontalSteps = rect.Width / (sampleDepth - 1); + var verticalSteps = height / (sampleDepth - 1); + var horizontalSteps = width / (sampleDepth - 1); - var a = 0; - var r = 0; - var g = 0; - var b = 0; - - // TODO: Compare this with LINQ, might be quicker and cleaner + int a = 0, r = 0, g = 0, b = 0; for (var horizontalStep = 0; horizontalStep < sampleDepth; horizontalStep++) { for (var verticalStep = 0; verticalStep < sampleDepth; verticalStep++) { - var x = (rect.Left + horizontalSteps * horizontalStep).RoundToInt(); - var y = (rect.Top + verticalSteps * verticalStep).RoundToInt(); - if (x < 0 || x > Bitmap.Width || y < 0 || y > Bitmap.Height) + var x = left + horizontalSteps * horizontalStep; + var y = top + verticalSteps * verticalStep; + if (x < 0 || x > bitmapWidth || y < 0 || y > bitmapHeight) continue; var color = Bitmap.GetPixel(x, y); diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 4ca70e968..80203346f 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -127,7 +127,7 @@ namespace Artemis.Core.Services lock (_modules) { foreach (var module in _modules) - module.Render(args.DeltaTime, _surfaceService.ActiveSurface, canvas); + module.Render(args.DeltaTime, _surfaceService.ActiveSurface, canvas, _rgbService.BitmapBrush.Bitmap.Info); } } diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index 1148bbaa1..19ca009a7 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -8,8 +8,24 @@ namespace Artemis.Core.Services.Interfaces { public interface IRgbService : IArtemisService { + /// + /// Gets or sets the RGB surface rendering is performed on + /// RGBSurface Surface { get; set; } + + /// + /// Gets the bitmap brush used to convert the rendered frame to LED-colors + /// BitmapBrush BitmapBrush { get; } + + /// + /// Gets the scale the frames are rendered on, a scale of 1.0 means 1 pixel = 1mm + /// + double RenderScale { get; } + + /// + /// Gets all loaded RGB devices + /// IReadOnlyCollection LoadedDevices { get; } void AddDeviceProvider(IRGBDeviceProvider deviceProvider); diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 2eb5e0f16..10fa534fe 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -48,6 +48,8 @@ namespace Artemis.Core.Services public IReadOnlyCollection LoadedDevices => _loadedDevices.AsReadOnly(); + public double RenderScale => _renderScaleSetting.Value; + public void AddDeviceProvider(IRGBDeviceProvider deviceProvider) { Surface.LoadDevices(deviceProvider); diff --git a/src/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs b/src/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs index da15fd3a5..7206f1c79 100644 --- a/src/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs +++ b/src/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs @@ -14,6 +14,7 @@ namespace Artemis.Plugins.LayerBrushes.Color private SKColor _color; private SKPaint _paint; private SKShader _shader; + private SKRect _shaderBounds; public ColorBrush(Layer layer, LayerBrushDescriptor descriptor) : base(layer, descriptor) { @@ -30,9 +31,9 @@ namespace Artemis.Plugins.LayerBrushes.Color _testColors.Add(SKColor.FromHsv(0, 100, 100)); } - CreateShader(); - Layer.RenderPropertiesUpdated += (sender, args) => CreateShader(); - GradientTypeProperty.ValueChanged += (sender, args) => CreateShader(); + CreateShader(_shaderBounds); + Layer.RenderPropertiesUpdated += (sender, args) => CreateShader(_shaderBounds); + GradientTypeProperty.ValueChanged += (sender, args) => CreateShader(_shaderBounds); } public LayerProperty ColorProperty { get; set; } @@ -44,19 +45,22 @@ namespace Artemis.Plugins.LayerBrushes.Color if (_color != ColorProperty.CurrentValue) { _color = ColorProperty.CurrentValue; - CreateShader(); + CreateShader(_shaderBounds); } base.Update(deltaTime); } - public override void Render(SKCanvas canvas, SKPath path, SKPaint paint) + public override void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint) { + if (path.Bounds != _shaderBounds) + CreateShader(path.Bounds); + paint.Shader = _shader; canvas.DrawPath(path, paint); } - private void CreateShader() + private void CreateShader(SKRect pathBounds) { var center = new SKPoint(Layer.Bounds.MidX, Layer.Bounds.MidY); SKShader shader; @@ -66,10 +70,10 @@ namespace Artemis.Plugins.LayerBrushes.Color shader = SKShader.CreateColor(_color); break; case GradientType.LinearGradient: - shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(Layer.Bounds.Width, 0), _testColors.ToArray(), SKShaderTileMode.Repeat); + shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(pathBounds.Width, 0), _testColors.ToArray(), SKShaderTileMode.Repeat); break; case GradientType.RadialGradient: - shader = SKShader.CreateRadialGradient(center, Math.Min(Layer.Bounds.Width, Layer.Bounds.Height), _testColors.ToArray(), SKShaderTileMode.Repeat); + shader = SKShader.CreateRadialGradient(center, Math.Min(pathBounds.Width, pathBounds.Height), _testColors.ToArray(), SKShaderTileMode.Repeat); break; case GradientType.SweepGradient: shader = SKShader.CreateSweepGradient(center, _testColors.ToArray(), null, SKShaderTileMode.Clamp, 0, 360); @@ -82,6 +86,7 @@ namespace Artemis.Plugins.LayerBrushes.Color var oldPaint = _paint; _shader = shader; _paint = new SKPaint {Shader = _shader, FilterQuality = SKFilterQuality.Low}; + _shaderBounds = pathBounds; oldShader?.Dispose(); oldPaint?.Dispose(); } diff --git a/src/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs b/src/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs index 2cd3c93e8..a19d12079 100644 --- a/src/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs +++ b/src/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs @@ -2,6 +2,7 @@ using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerBrush; +using Artemis.Core.Services.Interfaces; using Artemis.Plugins.LayerBrushes.Noise.Utilities; using SkiaSharp; @@ -10,16 +11,23 @@ namespace Artemis.Plugins.LayerBrushes.Noise public class NoiseBrush : LayerBrush { private static readonly Random Rand = new Random(); + private readonly OpenSimplexNoise _noise; + private readonly IRgbService _rgbService; + private SKBitmap _bitmap; private float _renderScale; - private readonly OpenSimplexNoise _noise; private float _x; private float _y; private float _z; - private SKBitmap _bitmap; - public NoiseBrush(Layer layer, LayerBrushDescriptor descriptor) : base(layer, descriptor) + public NoiseBrush(Layer layer, LayerBrushDescriptor descriptor, IRgbService rgbService) : base(layer, descriptor) { + _rgbService = rgbService; + _x = Rand.Next(0, 4096); + _y = Rand.Next(0, 4096); + _z = Rand.Next(0, 4096); + _noise = new OpenSimplexNoise(Rand.Next(0, 4096)); + MainColorProperty = RegisterLayerProperty("Brush.MainColor", "Main color", "The main color of the noise."); SecondaryColorProperty = RegisterLayerProperty("Brush.SecondaryColor", "Secondary color", "The secondary color of the noise."); ScaleProperty = RegisterLayerProperty("Brush.Scale", "Scale", "The scale of the noise."); @@ -27,10 +35,7 @@ namespace Artemis.Plugins.LayerBrushes.Noise AnimationSpeedProperty = RegisterLayerProperty("Brush.AnimationSpeed", "Animation speed", "The speed at which the noise moves."); ScaleProperty.InputAffix = "%"; - _x = Rand.Next(0, 4096); - _y = Rand.Next(0, 4096); - _z = Rand.Next(0, 4096); - _noise = new OpenSimplexNoise(Rand.Next(0, 4096)); + DetermineRenderScale(); } public LayerProperty MainColorProperty { get; set; } @@ -52,28 +57,35 @@ namespace Artemis.Plugins.LayerBrushes.Noise _y = 0; if (float.IsPositiveInfinity(_z) || float.IsNegativeInfinity(_z) || float.IsNaN(_z)) _z = 0; + + DetermineRenderScale(); base.Update(deltaTime); } - public override void Render(SKCanvas canvas, SKPath path, SKPaint paint) + public override void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint) { var mainColor = MainColorProperty.CurrentValue; var scale = ScaleProperty.CurrentValue; - // Scale down the render path to avoid computing a value for every pixel - var width = Math.Floor(path.Bounds.Width * RenderScale); - var height = Math.Floor(path.Bounds.Height * RenderScale); - - CreateBitmap((int) width, (int) height); var opacity = (float) Math.Round(mainColor.Alpha / 255.0, 2, MidpointRounding.AwayFromZero); - _bitmap.Erase(SKColor.Empty); + // Scale down the render path to avoid computing a value for every pixel + var width = Math.Floor(path.Bounds.Width * _renderScale); + var height = Math.Floor(path.Bounds.Height * _renderScale); + + CreateBitmap((int) width, (int) height); + for (var x = 0; x < width; x++) { var scrolledX = x + _x; for (var y = 0; y < height; y++) { var scrolledY = y + _y; - var v = _noise.Evaluate(0.1f * scale.Width * scrolledX / width, 0.1f * scale.Height * scrolledY / height, _z); + var evalX = 0.1 * scale.Width * scrolledX / width; + var evalY = 0.1 * scale.Height * scrolledY / height; + if (double.IsNaN(evalX) || double.IsNaN(evalY)) + continue; + + var v = _noise.Evaluate(evalX, evalY, _z); var alpha = (byte) Math.Max(0, Math.Min(255, v * 1024)); _bitmap.SetPixel(x, y, new SKColor(mainColor.Red, mainColor.Green, mainColor.Blue, (byte) (alpha * opacity))); } @@ -81,7 +93,7 @@ namespace Artemis.Plugins.LayerBrushes.Noise var bitmapTransform = SKMatrix.Concat( SKMatrix.MakeTranslation(path.Bounds.Left, path.Bounds.Top), - SKMatrix.MakeScale(1f / RenderScale, 1f / RenderScale) + SKMatrix.MakeScale(1f / _renderScale, 1f / _renderScale) ); using (var backgroundShader = SKShader.CreateColor(SecondaryColorProperty.CurrentValue)) using (var foregroundShader = SKShader.CreateBitmap(_bitmap, SKShaderTileMode.Clamp, SKShaderTileMode.Clamp, bitmapTransform)) @@ -94,12 +106,15 @@ namespace Artemis.Plugins.LayerBrushes.Noise } } + private void DetermineRenderScale() + { + _renderScale = (float) (0.125f / _rgbService.RenderScale); + } + private void CreateBitmap(int width, int height) { if (_bitmap == null) - { _bitmap = new SKBitmap(new SKImageInfo(width, height)); - } else if (_bitmap.Width != width || _bitmap.Height != height) { _bitmap.Dispose();