using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Events; using Artemis.Core.Exceptions; using Artemis.Core.Extensions; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Models.Profile.LayerShapes; using Artemis.Core.Models.Surface; using Artemis.Core.Plugins.LayerBrush; using Artemis.Core.Plugins.Models; using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.Models.Profile { public sealed class Layer : ProfileElement { private readonly Dictionary<(Guid, string), BaseLayerProperty> _properties; private LayerShape _layerShape; private List _leds; private SKPath _path; public Layer(Profile profile, ProfileElement parent, string name) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); Profile = profile; Parent = parent; Name = name; _leds = new List(); _properties = new Dictionary<(Guid, string), BaseLayerProperty>(); CreateDefaultProperties(); ApplyShapeType(); ShapeTypeProperty.ValueChanged += (sender, args) => ApplyShapeType(); } internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) { LayerEntity = layerEntity; EntityId = layerEntity.Id; Profile = profile; Parent = parent; Name = layerEntity.Name; Order = layerEntity.Order; _leds = new List(); _properties = new Dictionary<(Guid, string), BaseLayerProperty>(); CreateDefaultProperties(); ApplyShapeType(); ShapeTypeProperty.ValueChanged += (sender, args) => ApplyShapeType(); } 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(); } } /// /// A collection of all the properties on this layer /// public ReadOnlyCollection Properties => _properties.Values.ToList().AsReadOnly(); public LayerProperty ShapeTypeProperty { get; set; } public LayerProperty FillTypeProperty { get; set; } public LayerProperty BlendModeProperty { get; set; } public LayerProperty BrushReferenceProperty { get; set; } /// /// The anchor point property of this layer, also found in /// public LayerProperty AnchorPointProperty { get; private set; } /// /// The position of this layer, also found in /// public LayerProperty PositionProperty { get; private set; } /// /// The size property of this layer, also found in /// public LayerProperty ScaleProperty { get; private set; } /// /// The rotation property of this layer range 0 - 360, also found in /// public LayerProperty RotationProperty { get; private set; } /// /// The opacity property of this layer range 0 - 100, also found in /// public LayerProperty OpacityProperty { get; private set; } /// /// The brush that will fill the . /// public LayerBrush 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; foreach (var layerProperty in Properties) layerProperty.ApplyToEntity(); // LEDs LayerEntity.Leds.Clear(); foreach (var artemisLed in Leds) { var ledEntity = new LedEntity { DeviceHash = artemisLed.Device.RgbDevice.GetDeviceHashCode(), LedName = artemisLed.RgbLed.Id.ToString() }; LayerEntity.Leds.Add(ledEntity); } // Conditions TODO LayerEntity.Condition.Clear(); } #endregion #region Shape management private void ApplyShapeType() { switch (ShapeTypeProperty.CurrentValue) { case LayerShapeType.Ellipse: LayerShape = new Ellipse(this); break; case LayerShapeType.Rectangle: LayerShape = new Rectangle(this); break; default: throw new ArgumentOutOfRangeException(); } } #endregion private void OnLayerPropertyRegistered(LayerPropertyEventArgs e) { LayerPropertyRegistered?.Invoke(this, e); } private void OnLayerPropertyRemoved(LayerPropertyEventArgs e) { LayerPropertyRemoved?.Invoke(this, e); } #region Rendering /// public override void Update(double deltaTime) { foreach (var property in Properties) property.KeyframeEngine?.Update(deltaTime); // For now, reset all keyframe engines after the last keyframe was hit // This is a placeholder method of repeating the animation until repeat modes are implemented var lastKeyframe = Properties.SelectMany(p => p.UntypedKeyframes).OrderByDescending(t => t.Position).FirstOrDefault(); if (lastKeyframe != null) { if (Properties.Any(p => p.KeyframeEngine?.Progress > lastKeyframe.Position)) { foreach (var baseLayerProperty in Properties) baseLayerProperty.KeyframeEngine?.OverrideProgress(TimeSpan.Zero); } } 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 = BlendModeProperty.CurrentValue; paint.Color = new SKColor(0, 0, 0, (byte) (OpacityProperty.CurrentValue * 2.55f)); switch (FillTypeProperty.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 = ScaleProperty.CurrentValue; var rotationProperty = RotationProperty.CurrentValue; var anchorPosition = GetLayerAnchorPosition(); var anchorProperty = AnchorPointProperty.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 = ScaleProperty.CurrentValue; var rotationProperty = RotationProperty.CurrentValue; var anchorPosition = GetLayerAnchorPosition(); var anchorProperty = AnchorPointProperty.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 = PositionProperty.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.GetDeviceHashCode() == ledEntity.DeviceHash && a.RgbLed.Id.ToString() == ledEntity.LedName); if (match != null) leds.Add(match); } _leds = leds; CalculateRenderProperties(); } #endregion #region Properties /// /// Adds the provided layer property and its children to the layer. /// If found, the last stored base value and keyframes will be applied to the provided property. /// /// The type of value of the layer property /// The property to apply to the layer /// True if an existing value was found and applied, otherwise false. internal bool RegisterLayerProperty(LayerProperty layerProperty) { return RegisterLayerProperty((BaseLayerProperty) layerProperty); } /// /// Adds the provided layer property to the layer. /// If found, the last stored base value and keyframes will be applied to the provided property. /// /// The property to apply to the layer /// True if an existing value was found and applied, otherwise false. internal bool RegisterLayerProperty(BaseLayerProperty layerProperty) { if (_properties.ContainsKey((layerProperty.PluginInfo.Guid, layerProperty.Id))) throw new ArtemisCoreException($"Duplicate property ID detected. Layer already contains a property with ID {layerProperty.Id}."); var propertyEntity = LayerEntity.PropertyEntities.FirstOrDefault(p => p.Id == layerProperty.Id && p.ValueType == layerProperty.Type.Name); // TODO: Catch serialization exceptions and log them if (propertyEntity != null) layerProperty.ApplyToProperty(propertyEntity); _properties.Add((layerProperty.PluginInfo.Guid, layerProperty.Id), layerProperty); OnLayerPropertyRegistered(new LayerPropertyEventArgs(layerProperty)); return propertyEntity != null; } /// /// Removes the provided layer property from the layer. /// /// The type of value of the layer property /// The property to remove from the layer public void RemoveLayerProperty(LayerProperty layerProperty) { RemoveLayerProperty((BaseLayerProperty) layerProperty); } /// /// Removes the provided layer property from the layer. /// /// The property to remove from the layer public void RemoveLayerProperty(BaseLayerProperty layerProperty) { if (!_properties.ContainsKey((layerProperty.PluginInfo.Guid, layerProperty.Id))) throw new ArtemisCoreException($"Could not find a property with ID {layerProperty.Id}."); var property = _properties[(layerProperty.PluginInfo.Guid, layerProperty.Id)]; property.Parent?.Children.Remove(property); _properties.Remove((layerProperty.PluginInfo.Guid, layerProperty.Id)); OnLayerPropertyRemoved(new LayerPropertyEventArgs(property)); } /// /// If found, returns the matching the provided ID /// /// The type of the layer property /// The plugin this property belongs to /// /// public LayerProperty GetLayerPropertyById(PluginInfo pluginInfo, string id) { if (!_properties.ContainsKey((pluginInfo.Guid, id))) return null; var property = _properties[(pluginInfo.Guid, id)]; if (property.Type != typeof(T)) throw new ArtemisCoreException($"Property type mismatch. Expected property {property} to have type {typeof(T)} but it has {property.Type} instead."); return (LayerProperty) _properties[(pluginInfo.Guid, id)]; } private void CreateDefaultProperties() { // Shape var shape = new LayerProperty(this, "Core.Shape", "Shape", "A collection of basic shape properties."); ShapeTypeProperty = new LayerProperty(this, shape, "Core.ShapeType", "Shape type", "The type of shape to draw in this layer.") {CanUseKeyframes = false}; FillTypeProperty = new LayerProperty(this, shape, "Core.FillType", "Fill type", "How to make the shape adjust to scale changes.") {CanUseKeyframes = false}; BlendModeProperty = new LayerProperty(this, shape, "Core.BlendMode", "Blend mode", "How to blend this layer into the resulting image.") {CanUseKeyframes = false}; ShapeTypeProperty.Value = LayerShapeType.Rectangle; FillTypeProperty.Value = LayerFillType.Stretch; BlendModeProperty.Value = SKBlendMode.SrcOver; RegisterLayerProperty(shape); foreach (var shapeProperty in shape.Children) RegisterLayerProperty(shapeProperty); // Brush var brush = new LayerProperty(this, "Core.Brush", "Brush", "A collection of properties that configure the selected brush."); BrushReferenceProperty = new LayerProperty(this, brush, "Core.BrushReference", "Brush type", "The type of brush to use for this layer.") {CanUseKeyframes = false}; RegisterLayerProperty(brush); foreach (var brushProperty in brush.Children) RegisterLayerProperty(brushProperty); // Transform var transform = new LayerProperty(this, "Core.Transform", "Transform", "A collection of transformation properties.") {ExpandByDefault = true}; AnchorPointProperty = new LayerProperty(this, transform, "Core.AnchorPoint", "Anchor Point", "The point at which the shape is attached to its position."); PositionProperty = new LayerProperty(this, transform, "Core.Position", "Position", "The position of the shape."); ScaleProperty = new LayerProperty(this, transform, "Core.Scale", "Scale", "The scale of the shape.") {InputAffix = "%"}; RotationProperty = new LayerProperty(this, transform, "Core.Rotation", "Rotation", "The rotation of the shape in degrees.") {InputAffix = "°"}; OpacityProperty = new LayerProperty(this, transform, "Core.Opacity", "Opacity", "The opacity of the shape.") {InputAffix = "%"}; ScaleProperty.Value = new SKSize(100, 100); OpacityProperty.Value = 100; RegisterLayerProperty(transform); foreach (var transformProperty in transform.Children) RegisterLayerProperty(transformProperty); } #endregion #region Events public event EventHandler RenderPropertiesUpdated; public event EventHandler ShapePropertiesUpdated; public event EventHandler LayerPropertyRegistered; public event EventHandler LayerPropertyRemoved; 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 } }