diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 3a632ed07..fca9504ac 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Artemis.Core/Events/LayerPropertyEventArgs.cs b/src/Artemis.Core/Events/LayerPropertyEventArgs.cs index 9f16dc690..9ec63f95f 100644 --- a/src/Artemis.Core/Events/LayerPropertyEventArgs.cs +++ b/src/Artemis.Core/Events/LayerPropertyEventArgs.cs @@ -5,11 +5,11 @@ namespace Artemis.Core.Events { public class LayerPropertyEventArgs : EventArgs { - public LayerPropertyEventArgs(BaseLayerProperty layerProperty) + public LayerPropertyEventArgs(LayerProperty layerProperty) { LayerProperty = layerProperty; } - public BaseLayerProperty LayerProperty { get; } + public LayerProperty LayerProperty { get; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs index 60396b945..e77db585e 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs @@ -17,7 +17,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines /// /// The layer property this keyframe engine applies to. /// - public BaseLayerProperty LayerProperty { get; private set; } + public LayerProperty LayerProperty { get; private set; } /// /// The total progress @@ -55,7 +55,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines /// Associates the keyframe engine with the provided layer property. /// /// - public void Initialize(BaseLayerProperty layerProperty) + public void Initialize(LayerProperty layerProperty) { if (Initialized) throw new ArtemisCoreException("Cannot initialize the same keyframe engine twice"); diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 1de5eb131..f4b8cf4f8 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -3,22 +3,28 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Extensions; -using Artemis.Core.Models.Profile.LayerProperties; +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; - public Layer(Profile profile, ProfileElement parent, string name) + internal Layer(Profile profile, ProfileElement parent, string name) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); @@ -26,12 +32,10 @@ namespace Artemis.Core.Models.Profile Profile = profile; Parent = parent; Name = name; - Properties = new LayerPropertyCollection(this); + General = new LayerGeneralProperties(); + Transform = new LayerTransformProperties(); _leds = new List(); - - ApplyShapeType(); - Properties.ShapeType.ValueChanged += (sender, args) => ApplyShapeType(); } internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) @@ -43,12 +47,10 @@ namespace Artemis.Core.Models.Profile Parent = parent; Name = layerEntity.Name; Order = layerEntity.Order; - Properties = new LayerPropertyCollection(this); + General = new LayerGeneralProperties(); + Transform = new LayerTransformProperties(); _leds = new List(); - - ApplyShapeType(); - Properties.ShapeType.ValueChanged += (sender, args) => ApplyShapeType(); } internal LayerEntity LayerEntity { get; set; } @@ -93,15 +95,16 @@ namespace Artemis.Core.Models.Profile } } - /// - /// The properties of this layer - /// - public LayerPropertyCollection Properties { get; set; } + [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 LayerBrush LayerBrush { get; internal set; } + public ILayerBrush LayerBrush { get; internal set; } public override string ToString() { @@ -158,6 +161,19 @@ namespace Artemis.Core.Models.Profile #endregion + #region Properties + + internal void InitializeProperties(ILayerService layerService) + { + PropertiesInitialized = true; + + ApplyShapeType(); + } + + public bool PropertiesInitialized { get; private set; } + + #endregion + #region Rendering /// diff --git a/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs b/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs new file mode 100644 index 000000000..f12489f22 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs @@ -0,0 +1,35 @@ +using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Models.Profile.LayerProperties.Types; +using SkiaSharp; + +namespace Artemis.Core.Models.Profile +{ + public class LayerGeneralProperties : LayerPropertyGroup + { + [PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")] + public EnumLayerProperty ShapeType { get; set; } + + [PropertyDescription(Name = "Fill type", Description = "How to make the shape adjust to scale changes")] + public EnumLayerProperty FillType { get; set; } + + [PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")] + public EnumLayerProperty BlendMode { get; set; } + + [PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")] + public LayerBrushReferenceLayerProperty BrushReference { get; set; } + + protected override void OnPropertiesInitialized() + { + // Populate defaults + if (!ShapeType.IsLoadedFromStorage) + ShapeType.BaseValue = LayerShapeType.Rectangle; + if (!FillType.IsLoadedFromStorage) + FillType.BaseValue = LayerFillType.Stretch; + if (!BlendMode.IsLoadedFromStorage) + BlendMode.BaseValue = SKBlendMode.SrcOver; + + // TODO: SpoinkyNL 28-4-2020: Select preferred default brush type with a fallback to the first available + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Abstract/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Abstract/LayerProperty.cs deleted file mode 100644 index 201aaefb4..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Abstract/LayerProperty.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Utilities; - -namespace Artemis.Core.Models.Profile.LayerProperties.Abstract -{ - public abstract class LayerProperty - { - private List> _keyframes; - - protected LayerProperty() - { - _keyframes = new List>(); - } - - public T BaseValue { get; set; } - public T CurrentValue { get; set; } - public IReadOnlyList> Keyframes => _keyframes.AsReadOnly(); - - /// - /// The total progress on the timeline - /// - public TimeSpan TimelineProgress { get; private set; } - - /// - /// The current keyframe in the timeline - /// - public LayerPropertyKeyFrame CurrentKeyframe { get; protected set; } - - /// - /// The next keyframe in the timeline - /// - public LayerPropertyKeyFrame NextKeyframe { get; protected set; } - - public void Update(double deltaTime) - { - float keyframeProgress; - float keyframeProgressEased; - - TimelineProgress = TimelineProgress.Add(TimeSpan.FromSeconds(deltaTime)); - // The current keyframe is the last keyframe before the current time - CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= TimelineProgress); - // The next keyframe is the first keyframe that's after the current time - NextKeyframe = _keyframes.FirstOrDefault(k => k.Position > TimelineProgress); - - if (CurrentKeyframe == null) - { - keyframeProgress = 0; - keyframeProgressEased = 0; - } - else if (NextKeyframe == null) - { - keyframeProgress = 1; - keyframeProgressEased = 1; - } - else - { - var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; - keyframeProgress = (float) ((TimelineProgress - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); - keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); - } - - UpdateCurrentValue(keyframeProgress, keyframeProgressEased); - } - - protected abstract void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased); - - public void OverrideProgress(TimeSpan progress) - { - TimelineProgress = TimeSpan.Zero; - Update(progress.TotalSeconds); - } - - internal void SortKeyframes() - { - _keyframes = _keyframes.OrderBy(k => k.Position).ToList(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs index d6eb245d4..ad00c49a9 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs @@ -1,10 +1,42 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Artemis.Core.Models.Profile.LayerProperties.Attributes { public class PropertyDescriptionAttribute : Attribute { + /// + /// The user-friendly name for this property, shown in the UI + /// + public string Name { get; set; } + + /// + /// The user-friendly description for this property, shown in the UI + /// + public string Description { get; set; } + + /// + /// Input prefix to show before input elements in the UI + /// + public string InputPrefix { get; set; } + + /// + /// Input affix to show behind input elements in the UI + /// + public string InputAffix { get; set; } + + /// + /// The input drag step size, used in the UI + /// + public float InputStepSize { get; set; } + + /// + /// Minimum input value, only enforced in the UI + /// + public object MinInputValue { get; set; } + + /// + /// Maximum input value, only enforced in the UI + /// + public object MaxInputValue { get; set; } } -} +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs new file mode 100644 index 000000000..ba725a66a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs @@ -0,0 +1,22 @@ +using System; + +namespace Artemis.Core.Models.Profile.LayerProperties.Attributes +{ + public class PropertyGroupDescriptionAttribute : Attribute + { + /// + /// The user-friendly name for this property, shown in the UI. + /// + public string Name { get; set; } + + /// + /// The user-friendly description for this property, shown in the UI. + /// + public string Description { get; set; } + + /// + /// Whether to expand this property by default, this is useful for important parent properties. + /// + public bool ExpandByDefault { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs deleted file mode 100644 index 2f68a2a71..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Artemis.Core.Utilities; - -namespace Artemis.Core.Models.Profile.LayerProperties -{ - public class BaseKeyframe - { - private TimeSpan _position; - - protected BaseKeyframe(Layer layer, BaseLayerProperty property) - { - Layer = layer; - BaseProperty = property; - } - - public Layer Layer { get; set; } - - public TimeSpan Position - { - get => _position; - set - { - if (value == _position) return; - _position = value; - BaseProperty.SortKeyframes(); - } - } - - protected BaseLayerProperty BaseProperty { get; } - public object BaseValue { get; internal set; } - public Easings.Functions EasingFunction { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs deleted file mode 100644 index 78edeed6c..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ /dev/null @@ -1,375 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Exceptions; -using Artemis.Core.Models.Profile.KeyframeEngines; -using Artemis.Core.Models.Profile.LayerProperties.Abstract; -using Artemis.Core.Plugins.Models; -using Artemis.Core.Utilities; -using Artemis.Storage.Entities.Profile; -using Newtonsoft.Json; -using Stylet; - -namespace Artemis.Core.Models.Profile.LayerProperties -{ - public abstract class BaseLayerProperty : PropertyChangedBase - { - private object _baseValue; - private bool _isHidden; - - protected BaseLayerProperty(Layer layer, PluginInfo pluginInfo, BaseLayerProperty parent, string id, string name, string description, Type type) - { - Layer = layer; - PluginInfo = pluginInfo; - Parent = parent; - Id = id; - Name = name; - Description = description; - Type = type; - CanUseKeyframes = true; - InputStepSize = 1; - - // This can only be null if accessed internally, all public ways of creating enforce a plugin info - if (PluginInfo == null) - PluginInfo = Constants.CorePluginInfo; - - Children = new List(); - BaseKeyframes = new List(); - - - parent?.Children.Add(this); - } - - /// - /// Gets the layer this property applies to - /// - public Layer Layer { get; } - - /// - /// Info of the plugin associated with this property - /// - public PluginInfo PluginInfo { get; } - - /// - /// Gets the parent property of this property. - /// - public BaseLayerProperty Parent { get; } - - /// - /// Gets or sets the child properties of this property. - /// If the layer has children it cannot contain a value or keyframes. - /// - public List Children { get; set; } - - /// - /// Gets or sets a unique identifier for this property, a layer may not contain two properties with the same ID. - /// - public string Id { get; set; } - - /// - /// Gets or sets the user-friendly name for this property, shown in the UI. - /// - public string Name { get; set; } - - /// - /// Gets or sets the user-friendly description for this property, shown in the UI. - /// - public string Description { get; set; } - - /// - /// Gets or sets whether to expand this property by default, this is useful for important parent properties. - /// - public bool ExpandByDefault { get; set; } - - /// - /// Gets or sets the an optional input prefix to show before input elements in the UI. - /// - public string InputPrefix { get; set; } - - /// - /// Gets or sets an optional input affix to show behind input elements in the UI. - /// - public string InputAffix { get; set; } - - /// - /// Gets or sets an optional maximum input value, only enforced in the UI. - /// - public object MaxInputValue { get; set; } - - /// - /// Gets or sets the input drag step size, used in the UI. - /// - public float InputStepSize { get; set; } - - /// - /// Gets or sets an optional minimum input value, only enforced in the UI. - /// - public object MinInputValue { get; set; } - - /// - /// Gets or sets whether this property can use keyframes, True by default. - /// - public bool CanUseKeyframes { get; set; } - - /// - /// Gets or sets whether this property is using keyframes. - /// - public bool IsUsingKeyframes { get; set; } - - /// - /// Gets the type of value this layer property contains. - /// - public Type Type { get; protected set; } - - /// - /// Gets or sets whether this property is hidden in the UI. - /// - public bool IsHidden - { - get => _isHidden; - set - { - _isHidden = value; - OnVisibilityChanged(); - } - } - - /// - /// Gets a list of keyframes defining different values of the property in time, this list contains the untyped - /// . - /// - public IReadOnlyCollection UntypedKeyframes => BaseKeyframes.AsReadOnly(); - - /// - /// Gets the keyframe engine instance of this property - /// - public KeyframeEngine KeyframeEngine { get; internal set; } - - protected List BaseKeyframes { get; set; } - - public object BaseValue - { - get => _baseValue; - internal set - { - if (value != null && value.GetType() != Type) - throw new ArtemisCoreException($"Cannot set value of type {value.GetType()} on property {this}, expected type is {Type}."); - if (!Equals(_baseValue, value)) - { - _baseValue = value; - OnValueChanged(); - } - } - } - - /// - /// Creates a new keyframe for this base property without knowing the type - /// - /// - public BaseKeyframe CreateNewKeyframe(TimeSpan position, object value) - { - // Create a strongly typed keyframe or else it cannot be cast later on - var keyframeType = typeof(Keyframe<>); - var keyframe = (BaseKeyframe) Activator.CreateInstance(keyframeType.MakeGenericType(Type), Layer, this); - keyframe.Position = position; - keyframe.BaseValue = value; - BaseKeyframes.Add(keyframe); - SortKeyframes(); - - return keyframe; - } - - /// - /// Removes all keyframes from the property and sets the base value to the current value. - /// - public void ClearKeyframes() - { - if (KeyframeEngine != null) - BaseValue = KeyframeEngine.GetCurrentValue(); - - BaseKeyframes.Clear(); - } - - /// - /// Gets the current value using the regular value or if present, keyframes - /// - public object GetCurrentValue() - { - if (KeyframeEngine == null || !UntypedKeyframes.Any()) - return BaseValue; - - return KeyframeEngine.GetCurrentValue(); - } - - /// - /// Gets the current value using the regular value or keyframes. - /// - /// The value to set. - /// - /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new - /// or existing keyframe. - /// - public void SetCurrentValue(object value, TimeSpan? time) - { - if (value != null && value.GetType() != Type) - throw new ArtemisCoreException($"Cannot set value of type {value.GetType()} on property {this}, expected type is {Type}."); - - if (time == null || !CanUseKeyframes || !IsUsingKeyframes) - BaseValue = value; - else - { - // If on a keyframe, update the keyframe - var currentKeyframe = UntypedKeyframes.FirstOrDefault(k => k.Position == time.Value); - // Create a new keyframe if none found - if (currentKeyframe == null) - currentKeyframe = CreateNewKeyframe(time.Value, value); - - currentKeyframe.BaseValue = value; - } - - OnValueChanged(); - } - - /// - /// Adds a keyframe to the property. - /// - /// The keyframe to remove - public void AddKeyframe(BaseKeyframe keyframe) - { - BaseKeyframes.Add(keyframe); - SortKeyframes(); - } - - /// - /// Removes a keyframe from the property. - /// - /// The keyframe to remove - public void RemoveKeyframe(BaseKeyframe keyframe) - { - BaseKeyframes.Remove(keyframe); - SortKeyframes(); - } - - /// - /// Returns the flattened index of this property on the layer - /// - /// - public int GetFlattenedIndex() - { - if (Parent == null) - return Layer.Properties.ToList().IndexOf(this); - - // Create a flattened list of all properties in their order as defined by the parent/child hierarchy - var properties = new List(); - // Iterate root properties (those with children) - foreach (var baseLayerProperty in Layer.Properties) - { - // First add self, then add all children - if (baseLayerProperty.Children.Any()) - { - properties.Add(baseLayerProperty); - properties.AddRange(baseLayerProperty.GetAllChildren()); - } - } - - return properties.IndexOf(this); - } - - public override string ToString() - { - return $"{nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Description)}: {Description}"; - } - - internal void ApplyToEntity() - { - var propertyEntity = Layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.Id == Id); - if (propertyEntity == null) - { - propertyEntity = new PropertyEntity {Id = Id}; - Layer.LayerEntity.PropertyEntities.Add(propertyEntity); - } - - propertyEntity.ValueType = Type.Name; - propertyEntity.Value = JsonConvert.SerializeObject(BaseValue); - propertyEntity.IsUsingKeyframes = IsUsingKeyframes; - - propertyEntity.KeyframeEntities.Clear(); - foreach (var baseKeyframe in BaseKeyframes) - { - propertyEntity.KeyframeEntities.Add(new KeyframeEntity - { - Position = baseKeyframe.Position, - Value = JsonConvert.SerializeObject(baseKeyframe.BaseValue), - EasingFunction = (int) baseKeyframe.EasingFunction - }); - } - } - - internal void ApplyToProperty(PropertyEntity propertyEntity) - { - BaseValue = DeserializePropertyValue(propertyEntity.Value); - IsUsingKeyframes = propertyEntity.IsUsingKeyframes; - - BaseKeyframes.Clear(); - foreach (var keyframeEntity in propertyEntity.KeyframeEntities.OrderBy(e => e.Position)) - { - // Create a strongly typed keyframe or else it cannot be cast later on - var keyframeType = typeof(Keyframe<>); - var keyframe = (BaseKeyframe) Activator.CreateInstance(keyframeType.MakeGenericType(Type), Layer, this); - keyframe.Position = keyframeEntity.Position; - keyframe.BaseValue = DeserializePropertyValue(keyframeEntity.Value); - keyframe.EasingFunction = (Easings.Functions) keyframeEntity.EasingFunction; - - BaseKeyframes.Add(keyframe); - } - } - - internal void SortKeyframes() - { - BaseKeyframes = BaseKeyframes.OrderBy(k => k.Position).ToList(); - } - - internal IEnumerable GetAllChildren() - { - var children = new List(); - children.AddRange(Children); - foreach (var layerPropertyViewModel in Children) - children.AddRange(layerPropertyViewModel.GetAllChildren()); - - return children; - } - - private object DeserializePropertyValue(string value) - { - if (value == "null") - return Type.IsValueType ? Activator.CreateInstance(Type) : null; - return JsonConvert.DeserializeObject(value, Type); - } - - #region Events - - /// - /// Occurs when this property's value was changed outside regular keyframe updates - /// - public event EventHandler ValueChanged; - - /// - /// Occurs when this property or any of it's ancestors visibility is changed - /// - public event EventHandler VisibilityChanged; - - protected virtual void OnValueChanged() - { - ValueChanged?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnVisibilityChanged() - { - VisibilityChanged?.Invoke(this, EventArgs.Empty); - foreach (var baseLayerProperty in Children) - baseLayerProperty.OnVisibilityChanged(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/GenericLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/GenericLayerProperty.cs new file mode 100644 index 000000000..f07b9c909 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/GenericLayerProperty.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.Utilities; +using Artemis.Storage.Entities.Profile; +using Newtonsoft.Json; + +namespace Artemis.Core.Models.Profile.LayerProperties +{ + /// + /// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI. + /// + /// Note: You cannot initialize layer properties yourself. If properly placed, the Artemis core will initialize + /// these for you. + /// + /// + /// The type of property encapsulated in this layer property + public abstract class GenericLayerProperty : LayerProperty + { + private T _currentValue; + private List> _keyframes; + private T _baseValue; + + protected GenericLayerProperty() + { + _keyframes = new List>(); + } + + /// + /// Gets or sets the base value of this layer property without any keyframes applied + /// + public T BaseValue + { + get => _baseValue; + set + { + if (_baseValue != null && !_baseValue.Equals(value) || _baseValue == null && value != null) + { + _baseValue = value; + OnBaseValueChanged(); + } + } + } + + /// + /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame + /// + public T CurrentValue + { + get => !KeyframesEnabled || !KeyframesSupported ? BaseValue : _currentValue; + internal set => _currentValue = value; + } + + /// + /// Gets whether keyframes are supported on this property + /// + public bool KeyframesSupported { get; internal set; } + + /// + /// Gets or sets whether keyframes are enabled on this property, has no effect if is + /// False + /// + public bool KeyframesEnabled { get; set; } + + /// + /// Gets or sets whether the property is hidden in the UI + /// + public bool IsHidden { get; set; } + + /// + /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied + /// + public bool IsLoadedFromStorage { get; internal set; } + + /// + /// Gets a read-only list of all the keyframes on this layer property + /// + public IReadOnlyList> Keyframes => _keyframes.AsReadOnly(); + + /// + /// Gets the total progress on the timeline + /// + public TimeSpan TimelineProgress { get; private set; } + + /// + /// Gets the current keyframe in the timeline according to the current progress + /// + public LayerPropertyKeyframe CurrentKeyframe { get; protected set; } + + /// + /// Gets the next keyframe in the timeline according to the current progress + /// + public LayerPropertyKeyframe NextKeyframe { get; protected set; } + + /// + /// Updates the property, moving the timeline forwards by the provided + /// + /// The amount of time to move the timeline forwards + public void Update(double deltaTime) + { + TimelineProgress = TimelineProgress.Add(TimeSpan.FromSeconds(deltaTime)); + if (!KeyframesSupported || !KeyframesEnabled) + return; + + // The current keyframe is the last keyframe before the current time + CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= TimelineProgress); + // The next keyframe is the first keyframe that's after the current time + NextKeyframe = _keyframes.FirstOrDefault(k => k.Position > TimelineProgress); + + // No need to update the current value if either of the keyframes are null + if (CurrentKeyframe == null) + CurrentValue = BaseValue; + else if (NextKeyframe == null) + CurrentValue = CurrentKeyframe.Value; + // Only determine progress and current value if both keyframes are present + else + { + var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; + var keyframeProgress = (float) ((TimelineProgress - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); + var keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); + UpdateCurrentValue(keyframeProgress, keyframeProgressEased); + } + + OnUpdated(); + } + + /// + /// Overrides the timeline progress to match the provided + /// + /// The new progress to set the layer property timeline to. + public void OverrideProgress(TimeSpan progress) + { + TimelineProgress = TimeSpan.Zero; + Update(progress.TotalSeconds); + } + + /// + /// Adds a keyframe to the layer property + /// + /// The keyframe to add + public void AddKeyframe(LayerPropertyKeyframe keyframe) + { + keyframe.LayerProperty = this; + _keyframes.Add(keyframe); + SortKeyframes(); + OnKeyframeAdded(); + } + + /// + /// Removes a keyframe from the layer property + /// + /// The keyframe to remove + public void RemoveKeyframe(LayerPropertyKeyframe keyframe) + { + _keyframes.Remove(keyframe); + keyframe.LayerProperty = null; + SortKeyframes(); + OnKeyframeRemoved(); + } + + /// + /// Called every update (if keyframes are both supported and enabled) to determine the new + /// based on the provided progress + /// + /// The linear current keyframe progress + /// The current keyframe progress, eased with the current easing function + protected abstract void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased); + + internal void SortKeyframes() + { + _keyframes = _keyframes.OrderBy(k => k.Position).ToList(); + } + + internal override void LoadFromEntity(PropertyEntity entity) + { + BaseValue = JsonConvert.DeserializeObject(entity.Value); + CurrentValue = BaseValue; + + _keyframes.Clear(); + foreach (var keyframeEntity in entity.KeyframeEntities) + { + var value = JsonConvert.DeserializeObject(keyframeEntity.Value); + var keyframe = new LayerPropertyKeyframe(value, keyframeEntity.Position, (Easings.Functions) keyframeEntity.EasingFunction); + _keyframes.Add(keyframe); + } + SortKeyframes(); + } + + #region Events + + public event EventHandler Updated; + public event EventHandler BaseValueChanged; + public event EventHandler KeyframeAdded; + public event EventHandler KeyframeRemoved; + + protected virtual void OnUpdated() + { + Updated?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnBaseValueChanged() + { + BaseValueChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnKeyframeAdded() + { + KeyframeAdded?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnKeyframeRemoved() + { + KeyframeRemoved?.Invoke(this, EventArgs.Empty); + } + + #endregion + } + + public abstract class LayerProperty + { + internal abstract void LoadFromEntity(PropertyEntity entity); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Keyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Keyframe.cs deleted file mode 100644 index 5cd31a61e..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Keyframe.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Artemis.Core.Models.Profile.LayerProperties -{ - /// - public class Keyframe : BaseKeyframe - { - public Keyframe(Layer layer, LayerProperty propertyBase) : base(layer, propertyBase) - { - } - - public LayerProperty Property => (LayerProperty) BaseProperty; - - public T Value - { - get => BaseValue != null ? (T) BaseValue : default; - set => BaseValue = value; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs deleted file mode 100644 index 644957862..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; -using Artemis.Core.Plugins.LayerBrush; -using Artemis.Core.Plugins.Models; - -namespace Artemis.Core.Models.Profile.LayerProperties -{ - /// - /// Represents a property on the layer. This property is visible in the profile editor and can be key-framed (unless - /// opted out). - /// To create and register a new LayerProperty use - /// - /// - public class LayerProperty : BaseLayerProperty - { - internal LayerProperty(Layer layer, BaseLayerProperty parent, string id, string name, string description) - : base(layer, null, parent, id, name, description, typeof(T)) - { - } - - internal LayerProperty(Layer layer, string id, string name, string description) - : base(layer, null, null, id, name, description, typeof(T)) - { - } - - internal LayerProperty(Layer layer, PluginInfo pluginInfo, BaseLayerProperty parent, string id, string name, string description) - : base(layer, pluginInfo, parent, id, name, description, typeof(T)) - { - } - - /// - /// Gets or sets the value of the property without any keyframes applied - /// - public T Value - { - get => BaseValue != null ? (T) BaseValue : default; - set => BaseValue = value; - } - - /// - /// Gets the value of the property with keyframes applied - /// - public T CurrentValue - { - get - { - var currentValue = GetCurrentValue(); - return currentValue == null ? default : (T) currentValue; - } - } - - /// - /// Gets a list of keyframes defining different values of the property in time, this list contains the strongly typed - /// - /// - public ReadOnlyCollection> Keyframes => BaseKeyframes.Cast>().ToList().AsReadOnly(); - - /// - /// Adds a keyframe to the property. - /// - /// The keyframe to remove - public void AddKeyframe(Keyframe keyframe) - { - base.AddKeyframe(keyframe); - } - - /// - /// Removes a keyframe from the property. - /// - /// The keyframe to remove - public void RemoveKeyframe(Keyframe keyframe) - { - base.RemoveKeyframe(keyframe); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyCollection.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyCollection.cs deleted file mode 100644 index 80b5a65e6..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyCollection.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Events; -using Artemis.Core.Exceptions; -using Artemis.Core.Plugins.Models; -using SkiaSharp; - -namespace Artemis.Core.Models.Profile.LayerProperties -{ - /// - /// Contains all the properties of the layer and provides easy access to the default properties. - /// - public class LayerPropertyCollection : IEnumerable - { - private readonly Dictionary<(Guid, string), BaseLayerProperty> _properties; - - internal LayerPropertyCollection(Layer layer) - { - _properties = new Dictionary<(Guid, string), BaseLayerProperty>(); - - Layer = layer; - CreateDefaultProperties(); - } - - /// - /// Gets the layer these properties are applied on - /// - public Layer Layer { get; } - - /// - public IEnumerator GetEnumerator() - { - return _properties.Values.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// 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)]; - } - - /// - /// Removes the provided layer property from the layer. - /// - /// The type of value of the layer property - /// The property to remove from the layer - internal void RemoveLayerProperty(LayerProperty layerProperty) - { - RemoveLayerProperty((BaseLayerProperty) layerProperty); - } - - /// - /// Removes the provided layer property from the layer. - /// - /// The property to remove from the layer - internal 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)); - } - - /// - /// 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 entity = Layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.Id == layerProperty.Id && p.ValueType == layerProperty.Type.Name); - // TODO: Catch serialization exceptions and log them - if (entity != null) - layerProperty.ApplyToProperty(entity); - - _properties.Add((layerProperty.PluginInfo.Guid, layerProperty.Id), layerProperty); - OnLayerPropertyRegistered(new LayerPropertyEventArgs(layerProperty)); - return entity != null; - } - - #region Default properties - - /// - /// Gets the shape type property of the layer - /// - public LayerProperty ShapeType { get; private set; } - - /// - /// Gets the fill type property of the layer - /// - public LayerProperty FillType { get; private set; } - - /// - /// Gets the blend mode property of the layer - /// - public LayerProperty BlendMode { get; private set; } - - /// - /// Gets the brush reference property of the layer - /// - public LayerProperty BrushReference { get; private set; } - - /// - /// Gets the anchor point property of the layer - /// - public LayerProperty AnchorPoint { get; private set; } - - /// - /// Gets the position of the layer - /// - public LayerProperty Position { get; private set; } - - /// - /// Gets the size property of the layer - /// - public LayerProperty Scale { get; private set; } - - /// - /// Gets the rotation property of the layer range 0 - 360 - /// - public LayerProperty Rotation { get; private set; } - - /// - /// Gets the opacity property of the layer range 0 - 100 - /// - public LayerProperty Opacity { get; private set; } - - private void CreateDefaultProperties() - { - // Shape - var shape = new LayerProperty(Layer, "Core.Shape", "Shape", "A collection of basic shape properties"); - ShapeType = new LayerProperty(Layer, shape, "Core.ShapeType", "Shape type", "The type of shape to draw in this layer") {CanUseKeyframes = false}; - FillType = new LayerProperty(Layer, shape, "Core.FillType", "Fill type", "How to make the shape adjust to scale changes") {CanUseKeyframes = false}; - BlendMode = new LayerProperty(Layer, shape, "Core.BlendMode", "Blend mode", "How to blend this layer into the resulting image") {CanUseKeyframes = false}; - ShapeType.Value = LayerShapeType.Rectangle; - FillType.Value = LayerFillType.Stretch; - BlendMode.Value = SKBlendMode.SrcOver; - - RegisterLayerProperty(shape); - foreach (var shapeProperty in shape.Children) - RegisterLayerProperty(shapeProperty); - - // Brush - var brush = new LayerProperty(Layer, "Core.Brush", "Brush", "A collection of properties that configure the selected brush"); - BrushReference = new LayerProperty(Layer, 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(Layer, "Core.Transform", "Transform", "A collection of transformation properties") {ExpandByDefault = true}; - AnchorPoint = new LayerProperty(Layer, transform, "Core.AnchorPoint", "Anchor Point", "The point at which the shape is attached to its position") {InputStepSize = 0.001f}; - Position = new LayerProperty(Layer, transform, "Core.Position", "Position", "The position of the shape") {InputStepSize = 0.001f}; - Scale = new LayerProperty(Layer, transform, "Core.Scale", "Scale", "The scale of the shape") {InputAffix = "%", MinInputValue = 0f}; - Rotation = new LayerProperty(Layer, transform, "Core.Rotation", "Rotation", "The rotation of the shape in degrees") {InputAffix = "°"}; - Opacity = new LayerProperty(Layer, transform, "Core.Opacity", "Opacity", "The opacity of the shape") {InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f}; - Scale.Value = new SKSize(100, 100); - Opacity.Value = 100; - - RegisterLayerProperty(transform); - foreach (var transformProperty in transform.Children) - RegisterLayerProperty(transformProperty); - } - - #endregion - - #region Events - - public event EventHandler LayerPropertyRegistered; - public event EventHandler LayerPropertyRemoved; - - private void OnLayerPropertyRegistered(LayerPropertyEventArgs e) - { - LayerPropertyRegistered?.Invoke(this, e); - } - - private void OnLayerPropertyRemoved(LayerPropertyEventArgs e) - { - LayerPropertyRemoved?.Invoke(this, e); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index cb94d88e6..ec4694d30 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -3,19 +3,18 @@ using Artemis.Core.Utilities; namespace Artemis.Core.Models.Profile.LayerProperties { - public class LayerPropertyKeyFrame + public class LayerPropertyKeyframe { private TimeSpan _position; - public LayerPropertyKeyFrame(LayerProperty layerProperty, T value, TimeSpan position, Easings.Functions easingFunction) + public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction) { _position = position; Value = value; - LayerProperty = layerProperty; EasingFunction = easingFunction; } - public LayerProperty LayerProperty { get; set; } + public GenericLayerProperty LayerProperty { get; internal set; } public T Value { get; set; } public TimeSpan Position diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs new file mode 100644 index 000000000..c7c5af8d1 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs @@ -0,0 +1,19 @@ +using Artemis.Core.Exceptions; +using Artemis.Core.Models.Profile.Colors; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class ColorGradientLayerProperty : GenericLayerProperty + { + internal ColorGradientLayerProperty() + { + KeyframesSupported = false; + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Color Gradients do not support keyframes."); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs new file mode 100644 index 000000000..f10e6253b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs @@ -0,0 +1,18 @@ +using Artemis.Core.Exceptions; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class EnumLayerProperty : GenericLayerProperty where T : System.Enum + { + public EnumLayerProperty() + { + KeyframesSupported = false; + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Enum properties do not support keyframes."); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/FloatLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/FloatLayerProperty.cs similarity index 57% rename from src/Artemis.Core/Models/Profile/LayerProperties/FloatLayerProperty.cs rename to src/Artemis.Core/Models/Profile/LayerProperties/Types/FloatLayerProperty.cs index 5fa765240..f7b9b9aa1 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/FloatLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/FloatLayerProperty.cs @@ -1,9 +1,12 @@ -using Artemis.Core.Models.Profile.LayerProperties.Abstract; - -namespace Artemis.Core.Models.Profile.LayerProperties +namespace Artemis.Core.Models.Profile.LayerProperties.Types { - public class FloatLayerProperty : LayerProperty + /// + public class FloatLayerProperty : GenericLayerProperty { + internal FloatLayerProperty() + { + } + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { var diff = NextKeyframe.Value - CurrentKeyframe.Value; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/IntLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/IntLayerProperty.cs similarity index 63% rename from src/Artemis.Core/Models/Profile/LayerProperties/IntLayerProperty.cs rename to src/Artemis.Core/Models/Profile/LayerProperties/Types/IntLayerProperty.cs index 7f315a1e3..80cf9ff4a 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/IntLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/IntLayerProperty.cs @@ -1,10 +1,14 @@ using System; -using Artemis.Core.Models.Profile.LayerProperties.Abstract; -namespace Artemis.Core.Models.Profile.LayerProperties +namespace Artemis.Core.Models.Profile.LayerProperties.Types { - public class IntLayerProperty : LayerProperty + /// + public class IntLayerProperty : GenericLayerProperty { + internal IntLayerProperty() + { + } + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { var diff = NextKeyframe.Value - CurrentKeyframe.Value; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs new file mode 100644 index 000000000..654001a56 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs @@ -0,0 +1,20 @@ +using Artemis.Core.Exceptions; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + /// A special layer property used to configure the selected layer brush + /// + public class LayerBrushReferenceLayerProperty : GenericLayerProperty + { + internal LayerBrushReferenceLayerProperty() + { + KeyframesSupported = false; + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Layer brush references do not support keyframes."); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/SKColorLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs similarity index 83% rename from src/Artemis.Core/Models/Profile/LayerProperties/SKColorLayerProperty.cs rename to src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs index 78635385a..c765d849c 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/SKColorLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs @@ -1,11 +1,15 @@ using System; -using Artemis.Core.Models.Profile.LayerProperties.Abstract; using SkiaSharp; -namespace Artemis.Core.Models.Profile.LayerProperties +namespace Artemis.Core.Models.Profile.LayerProperties.Types { - public class SKColorLayerProperty : LayerProperty + /// + public class SKColorLayerProperty : GenericLayerProperty { + internal SKColorLayerProperty() + { + } + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { var redDiff = NextKeyframe.Value.Red - CurrentKeyframe.Value.Red; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/SKPointLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs similarity index 65% rename from src/Artemis.Core/Models/Profile/LayerProperties/SKPointLayerProperty.cs rename to src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs index a11719034..d4694d283 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/SKPointLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs @@ -1,10 +1,14 @@ -using Artemis.Core.Models.Profile.LayerProperties.Abstract; -using SkiaSharp; +using SkiaSharp; -namespace Artemis.Core.Models.Profile.LayerProperties +namespace Artemis.Core.Models.Profile.LayerProperties.Types { - public class SKPointLayerProperty : LayerProperty + /// + public class SKPointLayerProperty : GenericLayerProperty { + internal SKPointLayerProperty() + { + } + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { var xDiff = NextKeyframe.Value.X - CurrentKeyframe.Value.X; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/SKSizeLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs similarity index 67% rename from src/Artemis.Core/Models/Profile/LayerProperties/SKSizeLayerProperty.cs rename to src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs index 46fceec37..259d4a910 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/SKSizeLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs @@ -1,10 +1,14 @@ -using Artemis.Core.Models.Profile.LayerProperties.Abstract; -using SkiaSharp; +using SkiaSharp; -namespace Artemis.Core.Models.Profile.LayerProperties +namespace Artemis.Core.Models.Profile.LayerProperties.Types { - public class SKSizeLayerProperty : LayerProperty + /// + public class SKSizeLayerProperty : GenericLayerProperty { + internal SKSizeLayerProperty() + { + } + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { var widthDiff = NextKeyframe.Value.Width - CurrentKeyframe.Value.Width; diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs new file mode 100644 index 000000000..7e61ec93b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Plugins.Exceptions; +using Artemis.Core.Services.Interfaces; + +namespace Artemis.Core.Models.Profile +{ + public class LayerPropertyGroup + { + public bool PropertiesInitialized { get; private set; } + + /// + /// Called when all layer properties in this property group have been initialized + /// + protected virtual void OnPropertiesInitialized() + { + } + + internal void InitializeProperties(ILayerService layerService, Layer layer, string path) + { + // Get all properties with a PropertyDescriptionAttribute + foreach (var propertyInfo in GetType().GetProperties()) + { + var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); + if (propertyDescription != null) + { + if (!typeof(GenericLayerProperty<>).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException("Layer property with PropertyDescription attribute must be of type LayerProperty"); + + var instance = (LayerProperty) Activator.CreateInstance(propertyInfo.PropertyType); + InitializeProperty(layer, path, instance); + propertyInfo.SetValue(this, instance); + } + else + { + var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); + if (propertyGroupDescription != null) + { + if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); + + var instance = (LayerPropertyGroup) Activator.CreateInstance(propertyInfo.PropertyType); + instance.InitializeProperties(layerService, layer, $"{path}{propertyInfo.Name}."); + propertyInfo.SetValue(this, instance); + } + } + } + + OnPropertiesInitialized(); + PropertiesInitialized = true; + } + + private void InitializeProperty(Layer layer, string path, LayerProperty instance) + { + var entity = layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.Id == path); + if (entity != null) + instance.LoadFromEntity(entity); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs new file mode 100644 index 000000000..cb67e4dfb --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs @@ -0,0 +1,34 @@ +using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Models.Profile.LayerProperties.Types; +using SkiaSharp; + +namespace Artemis.Core.Models.Profile +{ + public class LayerTransformProperties : LayerPropertyGroup + { + [PropertyDescription(Description = "The point at which the shape is attached to its position", InputStepSize = 0.001f)] + public SKPointLayerProperty AnchorPoint { get; set; } + + [PropertyDescription(Description = "The position of the shape", InputStepSize = 0.001f)] + public SKPointLayerProperty Position { get; set; } + + [PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)] + public SKSizeLayerProperty Scale { get; set; } + + [PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°")] + public FloatLayerProperty Rotation { get; set; } + + [PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f)] + public FloatLayerProperty Opacity { get; set; } + + protected override void OnPropertiesInitialized() + { + // Populate defaults + if (!Scale.IsLoadedFromStorage) + Scale.BaseValue = new SKSize(100, 100); + if (!Opacity.IsLoadedFromStorage) + Opacity.BaseValue = 100; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrush/ILayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrush/ILayerBrush.cs new file mode 100644 index 000000000..2b688fe91 --- /dev/null +++ b/src/Artemis.Core/Plugins/LayerBrush/ILayerBrush.cs @@ -0,0 +1,39 @@ +using System; +using Artemis.Core.Models.Profile; +using Artemis.Core.Services.Interfaces; +using SkiaSharp; + +namespace Artemis.Core.Plugins.LayerBrush +{ + public interface ILayerBrush : IDisposable + { + /// + /// Gets the layer this brush is applied to + /// + Layer Layer { get; } + + /// + /// Gets the descriptor of this brush + /// + LayerBrushDescriptor Descriptor { get; } + + /// + /// Called before rendering every frame, write your update logic here + /// + /// + void Update(double deltaTime); + + /// + /// The main method of rendering anything to the layer. The provided is specific to the layer + /// and matches it's width and height. + /// Called during rendering or layer preview, 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 + void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint); + + public void InitializeProperties(ILayerService layerService, string path); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs index bd83f5f8e..7103ea0e1 100644 --- a/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs @@ -1,16 +1,13 @@ -using System; -using System.Linq; -using Artemis.Core.Models.Profile; -using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Models.Profile; using Artemis.Core.Plugins.Exceptions; using Artemis.Core.Services.Interfaces; using SkiaSharp; namespace Artemis.Core.Plugins.LayerBrush { - public abstract class LayerBrush : IDisposable + public abstract class LayerBrush : ILayerBrush where T : LayerPropertyGroup { - private ILayerService _layerService; + private T _properties; protected LayerBrush(Layer layer, LayerBrushDescriptor descriptor) { @@ -18,112 +15,63 @@ namespace Artemis.Core.Plugins.LayerBrush Descriptor = descriptor; } + /// public Layer Layer { get; } + + /// public LayerBrushDescriptor Descriptor { get; } + /// public virtual void Dispose() { } - /// - /// Called before rendering every frame, write your update logic here - /// - /// + /// public virtual void Update(double deltaTime) { } - /// - /// The main method of rendering anything to the layer. The provided is specific to the layer - /// and matches it's width and height. - /// Called during rendering or layer preview, 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, SKImageInfo canvasInfo, SKPath path, SKPaint paint) { } - /// - /// Provides an easy way to add your own properties to the layer, for more info see . - /// Note: If found, the last value and keyframes are loaded and set when calling this method. - /// - /// - /// The parent of this property, use this to create a tree-hierarchy in the editor - /// A and ID identifying your property, must be unique within your plugin - /// A name for your property, this is visible in the editor - /// A description for your property, this is visible in the editor - /// The default value of the property, if not configured by the user - /// The layer property - protected LayerProperty RegisterLayerProperty(BaseLayerProperty parent, string id, string name, string description, T defaultValue = default) - { - var property = new LayerProperty(Layer, Descriptor.LayerBrushProvider.PluginInfo, parent, id, name, description) {Value = defaultValue}; - Layer.Properties.RegisterLayerProperty(property); - // It's fine if this is null, it'll be picked up by SetLayerService later - _layerService?.InstantiateKeyframeEngine(property); - return property; - } + #region Properties /// - /// Provides an easy way to add your own properties to the layer, for more info see . - /// Note: If found, the last value and keyframes are loaded and set when calling this method. + /// Gets the properties of this brush. /// - /// - /// A and ID identifying your property, must be unique within your plugin - /// A name for your property, this is visible in the editor - /// A description for your property, this is visible in the editor - /// The default value of the property, if not configured by the user - /// The layer property - protected LayerProperty RegisterLayerProperty(string id, string name, string description, T defaultValue = default) + public T Properties { - // Check if the property already exists - var existing = Layer.Properties.FirstOrDefault(p => - p.PluginInfo.Guid == Descriptor.LayerBrushProvider.PluginInfo.Guid && - p.Id == id && - p.Name == name && - p.Description == description); - - if (existing != null) + get { - // If it exists and the types match, return the existing property - if (existing.Type == typeof(T)) - return (LayerProperty) existing; - // If it exists and the types are different, something is wrong - throw new ArtemisPluginException($"Cannot register the property {id} with different types twice."); + // I imagine a null reference here can be confusing, so lets throw an exception explaining what to do + if (_properties == null) + throw new ArtemisPluginException("Cannot access brush properties until OnPropertiesInitialized has been called"); + return _properties; } - - var property = new LayerProperty(Layer, Descriptor.LayerBrushProvider.PluginInfo, Layer.Properties.BrushReference.Parent, id, name, description) - { - Value = defaultValue - }; - - Layer.Properties.RegisterLayerProperty(property); - // It's fine if this is null, it'll be picked up by SetLayerService later - _layerService?.InstantiateKeyframeEngine(property); - return property; + internal set => _properties = value; } - + /// - /// Allows you to remove layer properties previously added by using . + /// Gets whether all properties on this brush are initialized /// - /// - /// - protected void UnRegisterLayerProperty(LayerProperty layerProperty) + public bool PropertiesInitialized { get; private set; } + + /// + /// Called when all layer properties in this brush have been initialized + /// + protected virtual void OnPropertiesInitialized() { - if (layerProperty == null) - return; - - if (Layer.Properties.Any(p => p == layerProperty)) - Layer.Properties.RemoveLayerProperty(layerProperty); } - internal void SetLayerService(ILayerService layerService) + public void InitializeProperties(ILayerService layerService, string path) { - _layerService = layerService; - foreach (var baseLayerProperty in Layer.Properties) - _layerService.InstantiateKeyframeEngine(baseLayerProperty); + Properties.InitializeProperties(layerService, Descriptor.LayerBrushProvider.PluginInfo, path); + OnPropertiesInitialized(); + PropertiesInitialized = true; } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs b/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs index 65d73d17d..2631161dd 100644 --- a/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs +++ b/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs @@ -20,7 +20,7 @@ namespace Artemis.Core.Plugins.LayerBrush public ReadOnlyCollection LayerBrushDescriptors => _layerBrushDescriptors.AsReadOnly(); - protected void AddLayerBrushDescriptor(string displayName, string description, string icon) where T : LayerBrush + protected void AddLayerBrushDescriptor(string displayName, string description, string icon) where T : ILayerBrush { _layerBrushDescriptors.Add(new LayerBrushDescriptor(displayName, description, icon, typeof(T), this)); } diff --git a/src/Artemis.Core/Services/Interfaces/ILayerService.cs b/src/Artemis.Core/Services/Interfaces/ILayerService.cs index 95b098f1e..7731b655e 100644 --- a/src/Artemis.Core/Services/Interfaces/ILayerService.cs +++ b/src/Artemis.Core/Services/Interfaces/ILayerService.cs @@ -13,22 +13,9 @@ namespace Artemis.Core.Services.Interfaces /// /// The layer to instantiate the brush for /// - LayerBrush InstantiateLayerBrush(Layer layer); + ILayerBrush InstantiateLayerBrush(Layer layer); - /// - /// Instantiates and adds a compatible to the provided . - /// If the property already has a compatible keyframe engine, nothing happens. - /// - /// The layer property to apply the keyframe engine to. - /// The resulting keyframe engine, if a compatible engine was found. - KeyframeEngine InstantiateKeyframeEngine(LayerProperty layerProperty); - - /// - /// Instantiates and adds a compatible to the provided . - /// If the property already has a compatible keyframe engine, nothing happens. - /// - /// The layer property to apply the keyframe engine to. - /// The resulting keyframe engine, if a compatible engine was found. - KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty); + void LoadPropertyBaseValue(Layer layer, string path, object layerProperty); + void LoadPropertyKeyframes(Layer layer, string path, object layerProperty); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/LayerService.cs b/src/Artemis.Core/Services/LayerService.cs index e1a4f03ea..5b154ba52 100644 --- a/src/Artemis.Core/Services/LayerService.cs +++ b/src/Artemis.Core/Services/LayerService.cs @@ -4,7 +4,9 @@ using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.KeyframeEngines; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerBrush; +using Artemis.Core.Plugins.Models; using Artemis.Core.Services.Interfaces; +using Newtonsoft.Json; using Ninject; using Ninject.Parameters; using Serilog; @@ -24,11 +26,25 @@ namespace Artemis.Core.Services _pluginService = pluginService; } - public LayerBrush InstantiateLayerBrush(Layer layer) + public Layer CreateLayer(Profile profile, ProfileElement parent, string name) + { + var layer = new Layer(profile, parent, name); + + // Layers have two hardcoded property groups, instantiate them + layer.General.InitializeProperties(this, layer, null, null); + layer.Transform.InitializeProperties(this, layer, null, null); + + // With the properties loaded, the layer brush can be instantiated + InstantiateLayerBrush(layer); + + return layer; + } + + public ILayerBrush InstantiateLayerBrush(Layer layer) { RemoveLayerBrush(layer); - var descriptorReference = layer.Properties.BrushReference.CurrentValue; + var descriptorReference = layer.General.BrushReference.CurrentValue; if (descriptorReference == null) return null; @@ -46,36 +62,13 @@ namespace Artemis.Core.Services new ConstructorArgument("layer", layer), new ConstructorArgument("descriptor", descriptor) }; - var layerBrush = (LayerBrush) _kernel.Get(descriptor.LayerBrushType, arguments); - // Set the layer service after the brush was created to avoid constructor clutter, SetLayerService will play catch-up for us. - // If layer brush implementations need the LayerService they can inject it themselves, but don't require it by default - layerBrush.SetLayerService(this); + var layerBrush = (ILayerBrush) _kernel.Get(descriptor.LayerBrushType, arguments); + layerBrush.InitializeProperties(this, null); layer.LayerBrush = layerBrush; return layerBrush; } - public KeyframeEngine InstantiateKeyframeEngine(LayerProperty layerProperty) - { - return InstantiateKeyframeEngine((BaseLayerProperty) layerProperty); - } - - public KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty) - { - if (layerProperty.KeyframeEngine != null && layerProperty.KeyframeEngine.CompatibleTypes.Contains(layerProperty.Type)) - return layerProperty.KeyframeEngine; - - // This creates an instance of each keyframe engine, which is pretty cheap since all the expensive stuff is done during - // Initialize() call but it's not ideal - var keyframeEngines = _kernel.Get>(); - var keyframeEngine = keyframeEngines.FirstOrDefault(k => k.CompatibleTypes.Contains(layerProperty.Type)); - if (keyframeEngine == null) - return null; - - keyframeEngine.Initialize(layerProperty); - return keyframeEngine; - } - public void RemoveLayerBrush(Layer layer) { if (layer.LayerBrush == null) diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index d803e5918..7bd7aae57 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,6 +1,5 @@ using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.LayerProperties; -using Artemis.Core.Models.Profile.LayerProperties.Abstract; using Artemis.Core.Models.Surface; using Artemis.Core.Plugins.Abstract; using Artemis.UI.Screens.Module; @@ -51,7 +50,7 @@ namespace Artemis.UI.Ninject.Factories public interface ILayerPropertyVmFactory : IVmFactory { - LayerPropertyViewModel Create(BaseLayerProperty layerProperty, LayerPropertyViewModel parent); + LayerPropertyViewModel Create(LayerProperty layerProperty, LayerPropertyViewModel parent); } public interface IPropertyTreeVmFactory : IVmFactory diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index cc4ef6b8f..5dde7e5fd 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -169,7 +169,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties PopulateProperties(e.LayerProperty.Layer); } - private LayerPropertyViewModel CreatePropertyViewModel(BaseLayerProperty layerProperty) + private LayerPropertyViewModel CreatePropertyViewModel(LayerProperty layerProperty) { LayerPropertyViewModel parent = null; // If the property has a parent, find it's VM diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs index 497e6f379..b3c72586d 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs @@ -16,7 +16,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties private bool _keyframesEnabled; private bool _isExpanded; - public LayerPropertyViewModel(BaseLayerProperty layerProperty, LayerPropertyViewModel parent, IKernel kernel, IProfileEditorService profileEditorService) + public LayerPropertyViewModel(LayerProperty layerProperty, LayerPropertyViewModel parent, IKernel kernel, IProfileEditorService profileEditorService) { _kernel = kernel; _profileEditorService = profileEditorService; @@ -30,7 +30,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties Parent?.Children.Add(this); } - public BaseLayerProperty LayerProperty { get; } + public LayerProperty LayerProperty { get; } public LayerPropertyViewModel Parent { get; } public List Children { get; } diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs index 79d4e6483..30876cdbe 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Windows; using System.Windows.Input; using Artemis.Core.Models.Profile.LayerProperties; -using Artemis.Core.Models.Profile.LayerProperties.Abstract; using Artemis.Core.Utilities; using Artemis.UI.Services.Interfaces; using Stylet; diff --git a/src/Plugins/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs index ac9956297..033143871 100644 --- a/src/Plugins/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs +++ b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/ColorBrush.cs @@ -1,15 +1,11 @@ using System; -using System.ComponentModel; -using System.Linq; using Artemis.Core.Models.Profile; -using Artemis.Core.Models.Profile.Colors; -using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerBrush; using SkiaSharp; namespace Artemis.Plugins.LayerBrushes.Color { - public class ColorBrush : LayerBrush + public class ColorBrush : LayerBrush { private SKColor _color; private SKPaint _paint; @@ -18,33 +14,16 @@ namespace Artemis.Plugins.LayerBrushes.Color public ColorBrush(Layer layer, LayerBrushDescriptor descriptor) : base(layer, descriptor) { - GradientTypeProperty = RegisterLayerProperty("Brush.GradientType", "Gradient type", "The type of color brush to draw", GradientType.Solid); - GradientTypeProperty.CanUseKeyframes = false; - ColorProperty = RegisterLayerProperty("Brush.Color", "Color", "The color of the brush", new SKColor(255, 0, 0)); - GradientProperty = RegisterLayerProperty("Brush.Gradient", "Gradient", "The gradient of the brush", new ColorGradient()); - GradientProperty.CanUseKeyframes = false; - if (!GradientProperty.Value.Stops.Any()) - GradientProperty.Value.MakeFabulous(); - - UpdateColorProperties(); - Layer.RenderPropertiesUpdated += (sender, args) => CreateShader(); - GradientTypeProperty.ValueChanged += (sender, args) => UpdateColorProperties(); - ColorProperty.ValueChanged += (sender, args) => CreateShader(); - GradientProperty.Value.PropertyChanged += (sender, args) => CreateShader(); } - public LayerProperty ColorProperty { get; set; } - public LayerProperty GradientProperty { get; set; } - public LayerProperty GradientTypeProperty { get; set; } - public override void Update(double deltaTime) { // Only check if a solid is being drawn, because that can be changed by keyframes - if (GradientTypeProperty.Value == GradientType.Solid && _color != ColorProperty.CurrentValue) + if (Properties.GradientType.BaseValue == GradientType.Solid && _color != Properties.Color.CurrentValue) { // If the color was changed since the last frame, recreate the shader - _color = ColorProperty.CurrentValue; + _color = Properties.Color.CurrentValue; CreateShader(); } @@ -63,36 +42,35 @@ namespace Artemis.Plugins.LayerBrushes.Color canvas.DrawPath(path, paint); } - private void UpdateColorProperties() + protected override void OnPropertiesInitialized() { - ColorProperty.IsHidden = GradientTypeProperty.Value != GradientType.Solid; - GradientProperty.IsHidden = GradientTypeProperty.Value == GradientType.Solid; - - CreateShader(); + Properties.GradientType.BaseValueChanged += (sender, args) => CreateShader(); + Properties.Color.BaseValueChanged += (sender, args) => CreateShader(); + Properties.Gradient.BaseValue.PropertyChanged += (sender, args) => CreateShader(); } private void CreateShader() { var center = new SKPoint(_shaderBounds.MidX, _shaderBounds.MidY); - var shader = GradientTypeProperty.CurrentValue switch + var shader = Properties.GradientType.CurrentValue switch { GradientType.Solid => SKShader.CreateColor(_color), GradientType.LinearGradient => SKShader.CreateLinearGradient( new SKPoint(_shaderBounds.Left, _shaderBounds.Top), new SKPoint(_shaderBounds.Right, _shaderBounds.Top), - GradientProperty.Value.GetColorsArray(), - GradientProperty.Value.GetPositionsArray(), + Properties.Gradient.BaseValue.GetColorsArray(), + Properties.Gradient.BaseValue.GetPositionsArray(), SKShaderTileMode.Repeat), GradientType.RadialGradient => SKShader.CreateRadialGradient( center, Math.Min(_shaderBounds.Width, _shaderBounds.Height), - GradientProperty.Value.GetColorsArray(), - GradientProperty.Value.GetPositionsArray(), + Properties.Gradient.BaseValue.GetColorsArray(), + Properties.Gradient.BaseValue.GetPositionsArray(), SKShaderTileMode.Repeat), GradientType.SweepGradient => SKShader.CreateSweepGradient( center, - GradientProperty.Value.GetColorsArray(), - GradientProperty.Value.GetPositionsArray(), + Properties.Gradient.BaseValue.GetColorsArray(), + Properties.Gradient.BaseValue.GetPositionsArray(), SKShaderTileMode.Clamp, 0, 360), @@ -107,19 +85,4 @@ namespace Artemis.Plugins.LayerBrushes.Color oldPaint?.Dispose(); } } - - public enum GradientType - { - [Description("Solid")] - Solid, - - [Description("Linear Gradient")] - LinearGradient, - - [Description("Radial Gradient")] - RadialGradient, - - [Description("Sweep Gradient")] - SweepGradient - } } \ No newline at end of file diff --git a/src/Plugins/Artemis.Plugins.LayerBrushes.Color/ColorBrushProperties.cs b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/ColorBrushProperties.cs new file mode 100644 index 000000000..c76fd5bbe --- /dev/null +++ b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/ColorBrushProperties.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel; +using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.Colors; +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Models.Profile.LayerProperties.Types; +using SkiaSharp; + +namespace Artemis.Plugins.LayerBrushes.Color +{ + public class ColorBrushProperties : LayerPropertyGroup + { + [PropertyDescription(Description = "The type of color brush to draw")] + public EnumLayerProperty GradientType { get; set; } + + [PropertyDescription(Description = "The color of the brush")] + public SKColorLayerProperty Color { get; set; } + + [PropertyDescription(Description = "The gradient of the brush")] + public ColorGradientLayerProperty Gradient { get; set; } + + protected override void OnPropertiesInitialized() + { + // Populate defaults + if (!GradientType.IsLoadedFromStorage) + GradientType.BaseValue = LayerBrushes.Color.GradientType.Solid; + if (!Color.IsLoadedFromStorage) + Color.BaseValue = new SKColor(255, 0, 0); + if (!Gradient.IsLoadedFromStorage) + { + Gradient.BaseValue = new ColorGradient(); + Gradient.BaseValue.MakeFabulous(); + } + + GradientType.BaseValueChanged += GradientTypeOnBaseValueChanged; + } + + private void GradientTypeOnBaseValueChanged(object? sender, EventArgs e) + { + Color.IsHidden = GradientType.BaseValue != LayerBrushes.Color.GradientType.Solid; + Gradient.IsHidden = GradientType.BaseValue == LayerBrushes.Color.GradientType.Solid; + } + } + + public enum GradientType + { + [Description("Solid")] + Solid, + + [Description("Linear Gradient")] + LinearGradient, + + [Description("Radial Gradient")] + RadialGradient, + + [Description("Sweep Gradient")] + SweepGradient + } +} \ No newline at end of file diff --git a/src/Plugins/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs b/src/Plugins/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs index 81627c599..e70611f69 100644 --- a/src/Plugins/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs +++ b/src/Plugins/Artemis.Plugins.LayerBrushes.Noise/NoiseBrush.cs @@ -1,9 +1,6 @@ using System; using System.ComponentModel; -using System.Linq; using Artemis.Core.Models.Profile; -using Artemis.Core.Models.Profile.Colors; -using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerBrush; using Artemis.Core.Services.Interfaces; using Artemis.Plugins.LayerBrushes.Noise.Utilities; @@ -11,18 +8,18 @@ using SkiaSharp; namespace Artemis.Plugins.LayerBrushes.Noise { - public class NoiseBrush : LayerBrush + public class NoiseBrush : LayerBrush { private static readonly Random Rand = new Random(); private readonly OpenSimplexNoise _noise; private readonly IRgbService _rgbService; private SKBitmap _bitmap; + private SKColor[] _colorMap; private float _renderScale; private float _x; private float _y; private float _z; - private SKColor[] _colorMap; public NoiseBrush(Layer layer, LayerBrushDescriptor descriptor, IRgbService rgbService) : base(layer, descriptor) { @@ -31,58 +28,15 @@ namespace Artemis.Plugins.LayerBrushes.Noise _y = Rand.Next(0, 4096); _z = Rand.Next(0, 4096); _noise = new OpenSimplexNoise(Rand.Next(0, 4096)); + DetermineRenderScale(); - - ColorTypeProperty = RegisterLayerProperty("Brush.ColorType", "Color mapping type", "The way the noise is converted to colors", ColorMappingType.Simple); - ColorTypeProperty.CanUseKeyframes = false; - ColorTypeProperty.ValueChanged += (sender, args) => UpdateColorProperties(); - ScaleProperty = RegisterLayerProperty("Brush.Scale", "Scale", "The scale of the noise.", new SKSize(100, 100)); - ScaleProperty.MinInputValue = 0f; - HardnessProperty = RegisterLayerProperty("Brush.Hardness", "Hardness", "The hardness of the noise, lower means there are gradients in the noise, higher means hard lines", 500f); - HardnessProperty.MinInputValue = 0f; - HardnessProperty.MaxInputValue = 2048f; - ScrollSpeedProperty = RegisterLayerProperty("Brush.ScrollSpeed", "Movement speed", "The speed at which the noise moves vertically and horizontally"); - ScrollSpeedProperty.MinInputValue = -64f; - ScrollSpeedProperty.MaxInputValue = 64f; - AnimationSpeedProperty = RegisterLayerProperty("Brush.AnimationSpeed", "Animation speed", "The speed at which the noise moves", 25f); - AnimationSpeedProperty.MinInputValue = 0f; - AnimationSpeedProperty.MaxInputValue = 64f; - ScaleProperty.InputAffix = "%"; - MainColorProperty = RegisterLayerProperty("Brush.MainColor", "Main color", "The main color of the noise", new SKColor(255, 0, 0)); - SecondaryColorProperty = RegisterLayerProperty("Brush.SecondaryColor", "Secondary color", "The secondary color of the noise", new SKColor(0, 0, 255)); - GradientColorProperty = RegisterLayerProperty("Brush.GradientColor", "Noise gradient map", "The gradient the noise will map it's value to", new ColorGradient()); - GradientColorProperty.CanUseKeyframes = false; - if (!GradientColorProperty.Value.Stops.Any()) - GradientColorProperty.Value.MakeFabulous(); - GradientColorProperty.Value.PropertyChanged += CreateColorMap; - - UpdateColorProperties(); - CreateColorMap(null, null); - } - - - public LayerProperty ColorTypeProperty { get; set; } - public LayerProperty MainColorProperty { get; set; } - public LayerProperty SecondaryColorProperty { get; set; } - public LayerProperty GradientColorProperty { get; set; } - - public LayerProperty ScaleProperty { get; set; } - public LayerProperty HardnessProperty { get; set; } - public LayerProperty ScrollSpeedProperty { get; set; } - public LayerProperty AnimationSpeedProperty { get; set; } - - private void UpdateColorProperties() - { - GradientColorProperty.IsHidden = ColorTypeProperty.Value != ColorMappingType.Gradient; - MainColorProperty.IsHidden = ColorTypeProperty.Value != ColorMappingType.Simple; - SecondaryColorProperty.IsHidden = ColorTypeProperty.Value != ColorMappingType.Simple; } public override void Update(double deltaTime) { - _x += ScrollSpeedProperty.CurrentValue.X / 500f / (float) deltaTime; - _y += ScrollSpeedProperty.CurrentValue.Y / 500f / (float) deltaTime; - _z += AnimationSpeedProperty.CurrentValue / 500f / 0.04f * (float) deltaTime; + _x += Properties.ScrollSpeed.CurrentValue.X / 500f / (float) deltaTime; + _y += Properties.ScrollSpeed.CurrentValue.Y / 500f / (float) deltaTime; + _z += Properties.AnimationSpeed.CurrentValue / 500f / 0.04f * (float) deltaTime; // A telltale sign of someone who can't do math very well if (float.IsPositiveInfinity(_x) || float.IsNegativeInfinity(_x) || float.IsNaN(_x)) @@ -98,11 +52,11 @@ namespace Artemis.Plugins.LayerBrushes.Noise public override void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint) { - var mainColor = MainColorProperty?.CurrentValue; - var gradientColor = GradientColorProperty?.CurrentValue; - var scale = ScaleProperty.CurrentValue; + var mainColor = Properties.MainColor?.CurrentValue; + var gradientColor = Properties.GradientColor?.CurrentValue; + var scale = Properties.Scale.CurrentValue; var opacity = mainColor != null ? (float) Math.Round(mainColor.Value.Alpha / 255.0, 2, MidpointRounding.AwayFromZero) : 0; - var hardness = 127 + HardnessProperty.CurrentValue; + var hardness = 127 + Properties.Hardness.CurrentValue; // Scale down the render path to avoid computing a value for every pixel var width = (int) Math.Floor(path.Bounds.Width * _renderScale); @@ -122,10 +76,8 @@ namespace Artemis.Plugins.LayerBrushes.Noise var v = _noise.Evaluate(evalX, evalY, _z); var alpha = (byte) Math.Max(0, Math.Min(255, v * hardness)); - if (ColorTypeProperty.Value == ColorMappingType.Simple && mainColor != null) - { + if (Properties.ColorType.BaseValue == ColorMappingType.Simple && mainColor != null) _bitmap.SetPixel(x, y, new SKColor(mainColor.Value.Red, mainColor.Value.Green, mainColor.Value.Blue, (byte) (alpha * opacity))); - } else if (gradientColor != null && _colorMap.Length == 101) { var color = _colorMap[(int) Math.Round(alpha / 255f * 100, MidpointRounding.AwayFromZero)]; @@ -141,9 +93,9 @@ namespace Artemis.Plugins.LayerBrushes.Noise ); canvas.ClipPath(path); - if (ColorTypeProperty.Value == ColorMappingType.Simple) + if (Properties.ColorType.BaseValue == ColorMappingType.Simple) { - using var backgroundShader = SKShader.CreateColor(SecondaryColorProperty.CurrentValue); + using var backgroundShader = SKShader.CreateColor(Properties.SecondaryColor.CurrentValue); paint.Shader = backgroundShader; canvas.DrawRect(path.Bounds, paint); } @@ -153,6 +105,17 @@ namespace Artemis.Plugins.LayerBrushes.Noise canvas.DrawRect(path.Bounds, paint); } + protected override void OnPropertiesInitialized() + { + Properties.GradientColor.BaseValue.PropertyChanged += GradientColorChanged; + CreateColorMap(); + } + + private void GradientColorChanged(object sender, PropertyChangedEventArgs e) + { + CreateColorMap(); + } + private void DetermineRenderScale() { @@ -170,11 +133,11 @@ namespace Artemis.Plugins.LayerBrushes.Noise } } - private void CreateColorMap(object sender, EventArgs e) + private void CreateColorMap() { var colorMap = new SKColor[101]; for (var i = 0; i < 101; i++) - colorMap[i] = GradientColorProperty.Value.GetColor(i / 100f); + colorMap[i] = Properties.GradientColor.BaseValue.GetColor(i / 100f); _colorMap = colorMap; } diff --git a/src/Plugins/Artemis.Plugins.LayerBrushes.Noise/NoiseBrushProperties.cs b/src/Plugins/Artemis.Plugins.LayerBrushes.Noise/NoiseBrushProperties.cs new file mode 100644 index 000000000..89f5f6790 --- /dev/null +++ b/src/Plugins/Artemis.Plugins.LayerBrushes.Noise/NoiseBrushProperties.cs @@ -0,0 +1,67 @@ +using System; +using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.Colors; +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Models.Profile.LayerProperties.Types; +using SkiaSharp; + +namespace Artemis.Plugins.LayerBrushes.Noise +{ + public class NoiseBrushProperties : LayerPropertyGroup + { + [PropertyDescription(Name = "Color mapping type", Description = "The way the noise is converted to colors")] + public EnumLayerProperty ColorType { get; set; } + + [PropertyDescription(Description = "The main color of the noise")] + public SKColorLayerProperty MainColor { get; set; } + + [PropertyDescription(Description = "The secondary color of the noise")] + public SKColorLayerProperty SecondaryColor { get; set; } + + [PropertyDescription(Name = "Noise gradient map", Description = "The gradient the noise will map it's value to")] + public ColorGradientLayerProperty GradientColor { get; set; } + + + [PropertyDescription(Description = "The scale of the noise", MinInputValue = 0f, InputAffix = "%")] + public SKSizeLayerProperty Scale { get; set; } + + [PropertyDescription(Description = "The hardness of the noise, lower means there are gradients in the noise, higher means hard lines", MinInputValue = 0f, MaxInputValue = 2048f)] + public FloatLayerProperty Hardness { get; set; } + + [PropertyDescription(Description = "The speed at which the noise moves vertically and horizontally", MinInputValue = -64f, MaxInputValue = 64f)] + public SKPointLayerProperty ScrollSpeed { get; set; } + + [PropertyDescription(Description = "The speed at which the noise moves", MinInputValue = 0f, MaxInputValue = 64f)] + public FloatLayerProperty AnimationSpeed { get; set; } + + protected override void OnPropertiesInitialized() + { + // Populate defaults + if (!MainColor.IsLoadedFromStorage) + MainColor.BaseValue = new SKColor(255, 0, 0); + if (!SecondaryColor.IsLoadedFromStorage) + SecondaryColor.BaseValue = new SKColor(0, 0, 255); + if (!GradientColor.IsLoadedFromStorage) + { + GradientColor.BaseValue = new ColorGradient(); + GradientColor.BaseValue.MakeFabulous(); + } + + if (!Scale.IsLoadedFromStorage) + Scale.BaseValue = new SKSize(100, 100); + if (!Hardness.IsLoadedFromStorage) + Hardness.BaseValue = 500f; + if (!AnimationSpeed.IsLoadedFromStorage) + AnimationSpeed.BaseValue = 25f; + + ColorType.BaseValueChanged += ColorTypeOnBaseValueChanged; + } + + private void ColorTypeOnBaseValueChanged(object? sender, EventArgs e) + { + GradientColor.IsHidden = ColorType.BaseValue != ColorMappingType.Gradient; + MainColor.IsHidden = ColorType.BaseValue != ColorMappingType.Simple; + SecondaryColor.IsHidden = ColorType.BaseValue != ColorMappingType.Simple; + } + } +} \ No newline at end of file