using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Extensions; using Artemis.Core.Models.Profile.LayerProperties.Attributes; using Artemis.Core.Models.Profile.LayerShapes; using Artemis.Core.Models.Surface; using Artemis.Core.Plugins.LayerBrush; using Artemis.Core.Services; using Artemis.Core.Services.Interfaces; using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.Models.Profile { /// /// Represents a layer on a profile. To create new layers use the by injecting /// into your code /// public sealed class Layer : ProfileElement { private LayerShape _layerShape; private List _leds; private SKPath _path; internal Layer(Profile profile, ProfileElement parent, string name) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); Profile = profile; Parent = parent; Name = name; General = new LayerGeneralProperties {IsCorePropertyGroup = true}; Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; _leds = new List(); } internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) { LayerEntity = layerEntity; EntityId = layerEntity.Id; Profile = profile; Parent = parent; Name = layerEntity.Name; Order = layerEntity.Order; General = new LayerGeneralProperties {IsCorePropertyGroup = true}; Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; _leds = new List(); } internal LayerEntity LayerEntity { get; set; } /// /// A collection of all the LEDs this layer is assigned to. /// public ReadOnlyCollection Leds => _leds.AsReadOnly(); /// /// Gets a copy of the path containing all the LEDs this layer is applied to, any rendering outside the layer Path is /// clipped. /// public SKPath Path { get => _path != null ? new SKPath(_path) : null; private set { _path = value; // I can't really be sure about the performance impact of calling Bounds often but // SkiaSharp calls SkiaApi.sk_path_get_bounds (Handle, &rect); which sounds expensive Bounds = value?.Bounds ?? SKRect.Empty; } } /// /// The bounds of this layer /// public SKRect Bounds { get; private set; } /// /// Defines the shape that is rendered by the . /// public LayerShape LayerShape { get => _layerShape; set { _layerShape = value; if (Path != null) CalculateRenderProperties(); } } [PropertyGroupDescription(Name = "General", Description = "A collection of general properties", ExpandByDefault = true)] public LayerGeneralProperties General { get; set; } [PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties", ExpandByDefault = true)] public LayerTransformProperties Transform { get; set; } /// /// The brush that will fill the . /// public BaseLayerBrush LayerBrush { get; internal set; } public override string ToString() { return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } #region Storage internal override void ApplyToEntity() { // Properties LayerEntity.Id = EntityId; LayerEntity.ParentId = Parent?.EntityId ?? new Guid(); LayerEntity.Order = Order; LayerEntity.Name = Name; LayerEntity.ProfileId = Profile.EntityId; General.ApplyToEntity(); Transform.ApplyToEntity(); LayerBrush.ApplyToEntity(); // LEDs LayerEntity.Leds.Clear(); foreach (var artemisLed in Leds) { var ledEntity = new LedEntity { DeviceIdentifier = artemisLed.Device.RgbDevice.GetDeviceIdentifier(), LedName = artemisLed.RgbLed.Id.ToString() }; LayerEntity.Leds.Add(ledEntity); } // Conditions TODO LayerEntity.Condition.Clear(); } #endregion #region Shape management private void ApplyShapeType() { switch (General.ShapeType.CurrentValue) { case LayerShapeType.Ellipse: LayerShape = new Ellipse(this); break; case LayerShapeType.Rectangle: LayerShape = new Rectangle(this); break; default: throw new ArgumentOutOfRangeException(); } } #endregion #region Properties internal void InitializeProperties(ILayerService layerService) { PropertiesInitialized = true; ApplyShapeType(); } public bool PropertiesInitialized { get; private set; } #endregion #region Rendering /// public override void Update(double deltaTime) { General.Update(deltaTime); Transform.Update(deltaTime); LayerBrush?.UpdateProperties(deltaTime); // TODO: Find the last keyframe and if required, reset the properties LayerBrush?.Update(deltaTime); } /// public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { if (Path == null || LayerShape?.Path == null) return; canvas.Save(); canvas.ClipPath(Path); using (var paint = new SKPaint()) { paint.BlendMode = General.BlendMode.CurrentValue; paint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f)); switch (General.FillType.CurrentValue) { case LayerFillType.Stretch: StretchRender(canvas, canvasInfo, paint); break; case LayerFillType.Clip: ClipRender(canvas, canvasInfo, paint); break; default: throw new ArgumentOutOfRangeException(); } } canvas.Restore(); } private void StretchRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint) { // Apply transformations var sizeProperty = Transform.Scale.CurrentValue; var rotationProperty = Transform.Rotation.CurrentValue; var anchorPosition = GetLayerAnchorPosition(); var anchorProperty = Transform.AnchorPoint.CurrentValue; // Translation originates from the unscaled center of the shape and is tied to the anchor var x = anchorPosition.X - Bounds.MidX - anchorProperty.X * Bounds.Width; var y = anchorPosition.Y - Bounds.MidY - anchorProperty.Y * Bounds.Height; // Apply these before translation because anchorPosition takes translation into account canvas.RotateDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y); canvas.Scale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y); canvas.Translate(x, y); LayerBrush?.Render(canvas, canvasInfo, new SKPath(LayerShape.Path), paint); } private void ClipRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint) { // Apply transformations var sizeProperty = Transform.Scale.CurrentValue; var rotationProperty = Transform.Rotation.CurrentValue; var anchorPosition = GetLayerAnchorPosition(); var anchorProperty = Transform.AnchorPoint.CurrentValue; // Translation originates from the unscaled center of the shape and is tied to the anchor var x = anchorPosition.X - Bounds.MidX - anchorProperty.X * Bounds.Width; var y = anchorPosition.Y - Bounds.MidY - anchorProperty.Y * Bounds.Height; var clipPath = new SKPath(LayerShape.Path); clipPath.Transform(SKMatrix.MakeTranslation(x, y)); clipPath.Transform(SKMatrix.MakeScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y)); clipPath.Transform(SKMatrix.MakeRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y)); canvas.ClipPath(clipPath); canvas.RotateDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y); canvas.Translate(x, y); // 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(boundsRect); LayerBrush?.Render(canvas, canvasInfo, renderPath, paint); } internal void CalculateRenderProperties() { if (!Leds.Any()) { Path = new SKPath(); LayerShape?.CalculateRenderProperties(); OnRenderPropertiesUpdated(); return; } var path = new SKPath {FillType = SKPathFillType.Winding}; foreach (var artemisLed in Leds) path.AddRect(artemisLed.AbsoluteRenderRectangle); Path = path; // This is called here so that the shape's render properties are up to date when other code // responds to OnRenderPropertiesUpdated LayerShape?.CalculateRenderProperties(); OnRenderPropertiesUpdated(); } internal SKPoint GetLayerAnchorPosition() { var positionProperty = Transform.Position.CurrentValue; // Start at the center of the shape var position = new SKPoint(Bounds.MidX, Bounds.MidY); // Apply translation position.X += positionProperty.X * Bounds.Width; position.Y += positionProperty.Y * Bounds.Height; return position; } #endregion #region LED management /// /// Adds a new to the layer and updates the render properties. /// /// The LED to add public void AddLed(ArtemisLed led) { _leds.Add(led); CalculateRenderProperties(); } /// /// Adds a collection of new s to the layer and updates the render properties. /// /// The LEDs to add public void AddLeds(IEnumerable leds) { _leds.AddRange(leds); CalculateRenderProperties(); } /// /// Removes a from the layer and updates the render properties. /// /// The LED to remove public void RemoveLed(ArtemisLed led) { _leds.Remove(led); CalculateRenderProperties(); } /// /// Removes all s from the layer and updates the render properties. /// public void ClearLeds() { _leds.Clear(); CalculateRenderProperties(); } internal void PopulateLeds(ArtemisSurface surface) { var leds = new List(); // Get the surface LEDs for this layer var availableLeds = surface.Devices.SelectMany(d => d.Leds).ToList(); foreach (var ledEntity in LayerEntity.Leds) { var match = availableLeds.FirstOrDefault(a => a.Device.RgbDevice.GetDeviceIdentifier() == ledEntity.DeviceIdentifier && a.RgbLed.Id.ToString() == ledEntity.LedName); if (match != null) leds.Add(match); } _leds = leds; CalculateRenderProperties(); } #endregion #region Events public event EventHandler RenderPropertiesUpdated; public event EventHandler ShapePropertiesUpdated; private void OnRenderPropertiesUpdated() { RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); } private void OnShapePropertiesUpdated() { ShapePropertiesUpdated?.Invoke(this, EventArgs.Empty); } #endregion } public enum LayerShapeType { Ellipse, Rectangle } public enum LayerFillType { Stretch, Clip } }