1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Conditions - Refactor layer properties WIP (needed for consistency..)

This commit is contained in:
Robert 2020-04-28 19:40:03 +02:00
parent 302ba10caa
commit d9bba8cb54
38 changed files with 809 additions and 1099 deletions

View File

@ -22,6 +22,7 @@
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.1.6" />
<PackageReference Include="Castle.Core" Version="4.4.0" />
<PackageReference Include="FastMember" Version="1.5.0" />
<PackageReference Include="HidSharp" Version="2.1.0" />
<PackageReference Include="LiteDB" Version="5.0.7" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.2.0" />

View File

@ -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; }
}
}

View File

@ -17,7 +17,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
/// <summary>
/// The layer property this keyframe engine applies to.
/// </summary>
public BaseLayerProperty LayerProperty { get; private set; }
public LayerProperty LayerProperty { get; private set; }
/// <summary>
/// The total progress
@ -55,7 +55,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines
/// Associates the keyframe engine with the provided layer property.
/// </summary>
/// <param name="layerProperty"></param>
public void Initialize(BaseLayerProperty layerProperty)
public void Initialize(LayerProperty layerProperty)
{
if (Initialized)
throw new ArtemisCoreException("Cannot initialize the same keyframe engine twice");

View File

@ -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
{
/// <summary>
/// Represents a layer on a profile. To create new layers use the <see cref="LayerService" /> by injecting
/// <see cref="ILayerService" /> into your code
/// </summary>
public sealed class Layer : ProfileElement
{
private LayerShape _layerShape;
private List<ArtemisLed> _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<ArtemisLed>();
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<ArtemisLed>();
ApplyShapeType();
Properties.ShapeType.ValueChanged += (sender, args) => ApplyShapeType();
}
internal LayerEntity LayerEntity { get; set; }
@ -93,15 +95,16 @@ namespace Artemis.Core.Models.Profile
}
}
/// <summary>
/// The properties of this layer
/// </summary>
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; }
/// <summary>
/// The brush that will fill the <see cref="LayerShape" />.
/// </summary>
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
/// <inheritdoc />

View File

@ -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<LayerShapeType> ShapeType { get; set; }
[PropertyDescription(Name = "Fill type", Description = "How to make the shape adjust to scale changes")]
public EnumLayerProperty<LayerFillType> FillType { get; set; }
[PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")]
public EnumLayerProperty<SKBlendMode> 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
}
}
}

View File

@ -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<T>
{
private List<LayerPropertyKeyFrame<T>> _keyframes;
protected LayerProperty()
{
_keyframes = new List<LayerPropertyKeyFrame<T>>();
}
public T BaseValue { get; set; }
public T CurrentValue { get; set; }
public IReadOnlyList<LayerPropertyKeyFrame<T>> Keyframes => _keyframes.AsReadOnly();
/// <summary>
/// The total progress on the timeline
/// </summary>
public TimeSpan TimelineProgress { get; private set; }
/// <summary>
/// The current keyframe in the timeline
/// </summary>
public LayerPropertyKeyFrame<T> CurrentKeyframe { get; protected set; }
/// <summary>
/// The next keyframe in the timeline
/// </summary>
public LayerPropertyKeyFrame<T> 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();
}
}
}

View File

@ -1,10 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Artemis.Core.Models.Profile.LayerProperties.Attributes
{
public class PropertyDescriptionAttribute : Attribute
{
/// <summary>
/// The user-friendly name for this property, shown in the UI
/// </summary>
public string Name { get; set; }
/// <summary>
/// The user-friendly description for this property, shown in the UI
/// </summary>
public string Description { get; set; }
/// <summary>
/// Input prefix to show before input elements in the UI
/// </summary>
public string InputPrefix { get; set; }
/// <summary>
/// Input affix to show behind input elements in the UI
/// </summary>
public string InputAffix { get; set; }
/// <summary>
/// The input drag step size, used in the UI
/// </summary>
public float InputStepSize { get; set; }
/// <summary>
/// Minimum input value, only enforced in the UI
/// </summary>
public object MinInputValue { get; set; }
/// <summary>
/// Maximum input value, only enforced in the UI
/// </summary>
public object MaxInputValue { get; set; }
}
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace Artemis.Core.Models.Profile.LayerProperties.Attributes
{
public class PropertyGroupDescriptionAttribute : Attribute
{
/// <summary>
/// The user-friendly name for this property, shown in the UI.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The user-friendly description for this property, shown in the UI.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Whether to expand this property by default, this is useful for important parent properties.
/// </summary>
public bool ExpandByDefault { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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<BaseLayerProperty>();
BaseKeyframes = new List<BaseKeyframe>();
parent?.Children.Add(this);
}
/// <summary>
/// Gets the layer this property applies to
/// </summary>
public Layer Layer { get; }
/// <summary>
/// Info of the plugin associated with this property
/// </summary>
public PluginInfo PluginInfo { get; }
/// <summary>
/// Gets the parent property of this property.
/// </summary>
public BaseLayerProperty Parent { get; }
/// <summary>
/// Gets or sets the child properties of this property.
/// <remarks>If the layer has children it cannot contain a value or keyframes.</remarks>
/// </summary>
public List<BaseLayerProperty> Children { get; set; }
/// <summary>
/// Gets or sets a unique identifier for this property, a layer may not contain two properties with the same ID.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the user-friendly name for this property, shown in the UI.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the user-friendly description for this property, shown in the UI.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets whether to expand this property by default, this is useful for important parent properties.
/// </summary>
public bool ExpandByDefault { get; set; }
/// <summary>
/// Gets or sets the an optional input prefix to show before input elements in the UI.
/// </summary>
public string InputPrefix { get; set; }
/// <summary>
/// Gets or sets an optional input affix to show behind input elements in the UI.
/// </summary>
public string InputAffix { get; set; }
/// <summary>
/// Gets or sets an optional maximum input value, only enforced in the UI.
/// </summary>
public object MaxInputValue { get; set; }
/// <summary>
/// Gets or sets the input drag step size, used in the UI.
/// </summary>
public float InputStepSize { get; set; }
/// <summary>
/// Gets or sets an optional minimum input value, only enforced in the UI.
/// </summary>
public object MinInputValue { get; set; }
/// <summary>
/// Gets or sets whether this property can use keyframes, True by default.
/// </summary>
public bool CanUseKeyframes { get; set; }
/// <summary>
/// Gets or sets whether this property is using keyframes.
/// </summary>
public bool IsUsingKeyframes { get; set; }
/// <summary>
/// Gets the type of value this layer property contains.
/// </summary>
public Type Type { get; protected set; }
/// <summary>
/// Gets or sets whether this property is hidden in the UI.
/// </summary>
public bool IsHidden
{
get => _isHidden;
set
{
_isHidden = value;
OnVisibilityChanged();
}
}
/// <summary>
/// Gets a list of keyframes defining different values of the property in time, this list contains the untyped
/// <see cref="BaseKeyframe" />.
/// </summary>
public IReadOnlyCollection<BaseKeyframe> UntypedKeyframes => BaseKeyframes.AsReadOnly();
/// <summary>
/// Gets the keyframe engine instance of this property
/// </summary>
public KeyframeEngine KeyframeEngine { get; internal set; }
protected List<BaseKeyframe> 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();
}
}
}
/// <summary>
/// Creates a new keyframe for this base property without knowing the type
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Removes all keyframes from the property and sets the base value to the current value.
/// </summary>
public void ClearKeyframes()
{
if (KeyframeEngine != null)
BaseValue = KeyframeEngine.GetCurrentValue();
BaseKeyframes.Clear();
}
/// <summary>
/// Gets the current value using the regular value or if present, keyframes
/// </summary>
public object GetCurrentValue()
{
if (KeyframeEngine == null || !UntypedKeyframes.Any())
return BaseValue;
return KeyframeEngine.GetCurrentValue();
}
/// <summary>
/// Gets the current value using the regular value or keyframes.
/// </summary>
/// <param name="value">The value to set.</param>
/// <param name="time">
/// 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.
/// </param>
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();
}
/// <summary>
/// Adds a keyframe to the property.
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void AddKeyframe(BaseKeyframe keyframe)
{
BaseKeyframes.Add(keyframe);
SortKeyframes();
}
/// <summary>
/// Removes a keyframe from the property.
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void RemoveKeyframe(BaseKeyframe keyframe)
{
BaseKeyframes.Remove(keyframe);
SortKeyframes();
}
/// <summary>
/// Returns the flattened index of this property on the layer
/// </summary>
/// <returns></returns>
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<BaseLayerProperty>();
// 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<BaseLayerProperty> GetAllChildren()
{
var children = new List<BaseLayerProperty>();
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
/// <summary>
/// Occurs when this property's value was changed outside regular keyframe updates
/// </summary>
public event EventHandler<EventArgs> ValueChanged;
/// <summary>
/// Occurs when this property or any of it's ancestors visibility is changed
/// </summary>
public event EventHandler<EventArgs> 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
}
}

View File

@ -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
{
/// <summary>
/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI.
/// <para>
/// Note: You cannot initialize layer properties yourself. If properly placed, the Artemis core will initialize
/// these for you.
/// </para>
/// </summary>
/// <typeparam name="T">The type of property encapsulated in this layer property</typeparam>
public abstract class GenericLayerProperty<T> : LayerProperty
{
private T _currentValue;
private List<LayerPropertyKeyframe<T>> _keyframes;
private T _baseValue;
protected GenericLayerProperty()
{
_keyframes = new List<LayerPropertyKeyframe<T>>();
}
/// <summary>
/// Gets or sets the base value of this layer property without any keyframes applied
/// </summary>
public T BaseValue
{
get => _baseValue;
set
{
if (_baseValue != null && !_baseValue.Equals(value) || _baseValue == null && value != null)
{
_baseValue = value;
OnBaseValueChanged();
}
}
}
/// <summary>
/// Gets the current value of this property as it is affected by it's keyframes, updated once every frame
/// </summary>
public T CurrentValue
{
get => !KeyframesEnabled || !KeyframesSupported ? BaseValue : _currentValue;
internal set => _currentValue = value;
}
/// <summary>
/// Gets whether keyframes are supported on this property
/// </summary>
public bool KeyframesSupported { get; internal set; }
/// <summary>
/// Gets or sets whether keyframes are enabled on this property, has no effect if <see cref="KeyframesSupported" /> is
/// False
/// </summary>
public bool KeyframesEnabled { get; set; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary>
public bool IsLoadedFromStorage { get; internal set; }
/// <summary>
/// Gets a read-only list of all the keyframes on this layer property
/// </summary>
public IReadOnlyList<LayerPropertyKeyframe<T>> Keyframes => _keyframes.AsReadOnly();
/// <summary>
/// Gets the total progress on the timeline
/// </summary>
public TimeSpan TimelineProgress { get; private set; }
/// <summary>
/// Gets the current keyframe in the timeline according to the current progress
/// </summary>
public LayerPropertyKeyframe<T> CurrentKeyframe { get; protected set; }
/// <summary>
/// Gets the next keyframe in the timeline according to the current progress
/// </summary>
public LayerPropertyKeyframe<T> NextKeyframe { get; protected set; }
/// <summary>
/// Updates the property, moving the timeline forwards by the provided <paramref name="deltaTime" />
/// </summary>
/// <param name="deltaTime">The amount of time to move the timeline forwards</param>
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();
}
/// <summary>
/// Overrides the timeline progress to match the provided <paramref name="progress" />
/// </summary>
/// <param name="progress">The new progress to set the layer property timeline to.</param>
public void OverrideProgress(TimeSpan progress)
{
TimelineProgress = TimeSpan.Zero;
Update(progress.TotalSeconds);
}
/// <summary>
/// Adds a keyframe to the layer property
/// </summary>
/// <param name="keyframe">The keyframe to add</param>
public void AddKeyframe(LayerPropertyKeyframe<T> keyframe)
{
keyframe.LayerProperty = this;
_keyframes.Add(keyframe);
SortKeyframes();
OnKeyframeAdded();
}
/// <summary>
/// Removes a keyframe from the layer property
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void RemoveKeyframe(LayerPropertyKeyframe<T> keyframe)
{
_keyframes.Remove(keyframe);
keyframe.LayerProperty = null;
SortKeyframes();
OnKeyframeRemoved();
}
/// <summary>
/// Called every update (if keyframes are both supported and enabled) to determine the new <see cref="CurrentValue" />
/// based on the provided progress
/// </summary>
/// <param name="keyframeProgress">The linear current keyframe progress</param>
/// <param name="keyframeProgressEased">The current keyframe progress, eased with the current easing function</param>
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<T>(entity.Value);
CurrentValue = BaseValue;
_keyframes.Clear();
foreach (var keyframeEntity in entity.KeyframeEntities)
{
var value = JsonConvert.DeserializeObject<T>(keyframeEntity.Value);
var keyframe = new LayerPropertyKeyframe<T>(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);
}
}

View File

@ -1,18 +0,0 @@
namespace Artemis.Core.Models.Profile.LayerProperties
{
/// <inheritdoc />
public class Keyframe<T> : BaseKeyframe
{
public Keyframe(Layer layer, LayerProperty<T> propertyBase) : base(layer, propertyBase)
{
}
public LayerProperty<T> Property => (LayerProperty<T>) BaseProperty;
public T Value
{
get => BaseValue != null ? (T) BaseValue : default;
set => BaseValue = value;
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a property on the layer. This property is visible in the profile editor and can be key-framed (unless
/// opted out).
/// <para>To create and register a new LayerProperty use <see cref="LayerBrush.RegisterLayerProperty" /></para>
/// </summary>
/// <typeparam name="T"></typeparam>
public class LayerProperty<T> : 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))
{
}
/// <summary>
/// Gets or sets the value of the property without any keyframes applied
/// </summary>
public T Value
{
get => BaseValue != null ? (T) BaseValue : default;
set => BaseValue = value;
}
/// <summary>
/// Gets the value of the property with keyframes applied
/// </summary>
public T CurrentValue
{
get
{
var currentValue = GetCurrentValue();
return currentValue == null ? default : (T) currentValue;
}
}
/// <summary>
/// Gets a list of keyframes defining different values of the property in time, this list contains the strongly typed
/// <see cref="Keyframe{T}" />
/// </summary>
public ReadOnlyCollection<Keyframe<T>> Keyframes => BaseKeyframes.Cast<Keyframe<T>>().ToList().AsReadOnly();
/// <summary>
/// Adds a keyframe to the property.
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void AddKeyframe(Keyframe<T> keyframe)
{
base.AddKeyframe(keyframe);
}
/// <summary>
/// Removes a keyframe from the property.
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void RemoveKeyframe(Keyframe<T> keyframe)
{
base.RemoveKeyframe(keyframe);
}
}
}

View File

@ -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
{
/// <summary>
/// Contains all the properties of the layer and provides easy access to the default properties.
/// </summary>
public class LayerPropertyCollection : IEnumerable<BaseLayerProperty>
{
private readonly Dictionary<(Guid, string), BaseLayerProperty> _properties;
internal LayerPropertyCollection(Layer layer)
{
_properties = new Dictionary<(Guid, string), BaseLayerProperty>();
Layer = layer;
CreateDefaultProperties();
}
/// <summary>
/// Gets the layer these properties are applied on
/// </summary>
public Layer Layer { get; }
/// <inheritdoc />
public IEnumerator<BaseLayerProperty> GetEnumerator()
{
return _properties.Values.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// If found, returns the <see cref="LayerProperty{T}" /> matching the provided ID
/// </summary>
/// <typeparam name="T">The type of the layer property</typeparam>
/// <param name="pluginInfo">The plugin this property belongs to</param>
/// <param name="id"></param>
/// <returns></returns>
public LayerProperty<T> GetLayerPropertyById<T>(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<T>)_properties[(pluginInfo.Guid, id)];
}
/// <summary>
/// Removes the provided layer property from the layer.
/// </summary>
/// <typeparam name="T">The type of value of the layer property</typeparam>
/// <param name="layerProperty">The property to remove from the layer</param>
internal void RemoveLayerProperty<T>(LayerProperty<T> layerProperty)
{
RemoveLayerProperty((BaseLayerProperty) layerProperty);
}
/// <summary>
/// Removes the provided layer property from the layer.
/// </summary>
/// <param name="layerProperty">The property to remove from the layer</param>
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));
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The type of value of the layer property</typeparam>
/// <param name="layerProperty">The property to apply to the layer</param>
/// <returns>True if an existing value was found and applied, otherwise false.</returns>
internal bool RegisterLayerProperty<T>(LayerProperty<T> layerProperty)
{
return RegisterLayerProperty((BaseLayerProperty) layerProperty);
}
/// <summary>
/// Adds the provided layer property to the layer.
/// If found, the last stored base value and keyframes will be applied to the provided property.
/// </summary>
/// <param name="layerProperty">The property to apply to the layer</param>
/// <returns>True if an existing value was found and applied, otherwise false.</returns>
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
/// <summary>
/// Gets the shape type property of the layer
/// </summary>
public LayerProperty<LayerShapeType> ShapeType { get; private set; }
/// <summary>
/// Gets the fill type property of the layer
/// </summary>
public LayerProperty<LayerFillType> FillType { get; private set; }
/// <summary>
/// Gets the blend mode property of the layer
/// </summary>
public LayerProperty<SKBlendMode> BlendMode { get; private set; }
/// <summary>
/// Gets the brush reference property of the layer
/// </summary>
public LayerProperty<LayerBrushReference> BrushReference { get; private set; }
/// <summary>
/// Gets the anchor point property of the layer
/// </summary>
public LayerProperty<SKPoint> AnchorPoint { get; private set; }
/// <summary>
/// Gets the position of the layer
/// </summary>
public LayerProperty<SKPoint> Position { get; private set; }
/// <summary>
/// Gets the size property of the layer
/// </summary>
public LayerProperty<SKSize> Scale { get; private set; }
/// <summary>
/// Gets the rotation property of the layer range 0 - 360
/// </summary>
public LayerProperty<float> Rotation { get; private set; }
/// <summary>
/// Gets the opacity property of the layer range 0 - 100
/// </summary>
public LayerProperty<float> Opacity { get; private set; }
private void CreateDefaultProperties()
{
// Shape
var shape = new LayerProperty<object>(Layer, "Core.Shape", "Shape", "A collection of basic shape properties");
ShapeType = new LayerProperty<LayerShapeType>(Layer, shape, "Core.ShapeType", "Shape type", "The type of shape to draw in this layer") {CanUseKeyframes = false};
FillType = new LayerProperty<LayerFillType>(Layer, shape, "Core.FillType", "Fill type", "How to make the shape adjust to scale changes") {CanUseKeyframes = false};
BlendMode = new LayerProperty<SKBlendMode>(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<object>(Layer, "Core.Brush", "Brush", "A collection of properties that configure the selected brush");
BrushReference = new LayerProperty<LayerBrushReference>(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<object>(Layer, "Core.Transform", "Transform", "A collection of transformation properties") {ExpandByDefault = true};
AnchorPoint = new LayerProperty<SKPoint>(Layer, transform, "Core.AnchorPoint", "Anchor Point", "The point at which the shape is attached to its position") {InputStepSize = 0.001f};
Position = new LayerProperty<SKPoint>(Layer, transform, "Core.Position", "Position", "The position of the shape") {InputStepSize = 0.001f};
Scale = new LayerProperty<SKSize>(Layer, transform, "Core.Scale", "Scale", "The scale of the shape") {InputAffix = "%", MinInputValue = 0f};
Rotation = new LayerProperty<float>(Layer, transform, "Core.Rotation", "Rotation", "The rotation of the shape in degrees") {InputAffix = "°"};
Opacity = new LayerProperty<float>(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<LayerPropertyEventArgs> LayerPropertyRegistered;
public event EventHandler<LayerPropertyEventArgs> LayerPropertyRemoved;
private void OnLayerPropertyRegistered(LayerPropertyEventArgs e)
{
LayerPropertyRegistered?.Invoke(this, e);
}
private void OnLayerPropertyRemoved(LayerPropertyEventArgs e)
{
LayerPropertyRemoved?.Invoke(this, e);
}
#endregion
}
}

View File

@ -3,19 +3,18 @@ using Artemis.Core.Utilities;
namespace Artemis.Core.Models.Profile.LayerProperties
{
public class LayerPropertyKeyFrame<T>
public class LayerPropertyKeyframe<T>
{
private TimeSpan _position;
public LayerPropertyKeyFrame(LayerProperty<T> 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<T> LayerProperty { get; set; }
public GenericLayerProperty<T> LayerProperty { get; internal set; }
public T Value { get; set; }
public TimeSpan Position

View File

@ -0,0 +1,19 @@
using Artemis.Core.Exceptions;
using Artemis.Core.Models.Profile.Colors;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc/>
public class ColorGradientLayerProperty : GenericLayerProperty<ColorGradient>
{
internal ColorGradientLayerProperty()
{
KeyframesSupported = false;
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Color Gradients do not support keyframes.");
}
}
}

View File

@ -0,0 +1,18 @@
using Artemis.Core.Exceptions;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc/>
public class EnumLayerProperty<T> : GenericLayerProperty<T> 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.");
}
}
}

View File

@ -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<float>
/// <inheritdoc/>
public class FloatLayerProperty : GenericLayerProperty<float>
{
internal FloatLayerProperty()
{
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
var diff = NextKeyframe.Value - CurrentKeyframe.Value;

View File

@ -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<int>
/// <inheritdoc/>
public class IntLayerProperty : GenericLayerProperty<int>
{
internal IntLayerProperty()
{
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
var diff = NextKeyframe.Value - CurrentKeyframe.Value;

View File

@ -0,0 +1,20 @@
using Artemis.Core.Exceptions;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <summary>
/// A special layer property used to configure the selected layer brush
/// </summary>
public class LayerBrushReferenceLayerProperty : GenericLayerProperty<LayerBrushReference>
{
internal LayerBrushReferenceLayerProperty()
{
KeyframesSupported = false;
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Layer brush references do not support keyframes.");
}
}
}

View File

@ -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<SKColor>
/// <inheritdoc/>
public class SKColorLayerProperty : GenericLayerProperty<SKColor>
{
internal SKColorLayerProperty()
{
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
var redDiff = NextKeyframe.Value.Red - CurrentKeyframe.Value.Red;

View File

@ -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<SKPoint>
/// <inheritdoc/>
public class SKPointLayerProperty : GenericLayerProperty<SKPoint>
{
internal SKPointLayerProperty()
{
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
var xDiff = NextKeyframe.Value.X - CurrentKeyframe.Value.X;

View File

@ -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<SKSize>
/// <inheritdoc/>
public class SKSizeLayerProperty : GenericLayerProperty<SKSize>
{
internal SKSizeLayerProperty()
{
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
var widthDiff = NextKeyframe.Value.Width - CurrentKeyframe.Value.Width;

View File

@ -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; }
/// <summary>
/// Called when all layer properties in this property group have been initialized
/// </summary>
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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
{
/// <summary>
/// Gets the layer this brush is applied to
/// </summary>
Layer Layer { get; }
/// <summary>
/// Gets the descriptor of this brush
/// </summary>
LayerBrushDescriptor Descriptor { get; }
/// <summary>
/// Called before rendering every frame, write your update logic here
/// </summary>
/// <param name="deltaTime"></param>
void Update(double deltaTime);
/// <summary>
/// The main method of rendering anything to the layer. The provided <see cref="SKCanvas" /> is specific to the layer
/// and matches it's width and height.
/// <para>Called during rendering or layer preview, in the order configured on the layer</para>
/// </summary>
/// <param name="canvas">The layer canvas</param>
/// <param name="canvasInfo"></param>
/// <param name="path">The path to be filled, represents the shape</param>
/// <param name="paint">The paint to be used to fill the shape</param>
void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint);
public void InitializeProperties(ILayerService layerService, string path);
}
}

View File

@ -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<T> : 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;
}
/// <inheritdoc />
public Layer Layer { get; }
/// <inheritdoc />
public LayerBrushDescriptor Descriptor { get; }
/// <inheritdoc />
public virtual void Dispose()
{
}
/// <summary>
/// Called before rendering every frame, write your update logic here
/// </summary>
/// <param name="deltaTime"></param>
/// <inheritdoc />
public virtual void Update(double deltaTime)
{
}
/// <summary>
/// The main method of rendering anything to the layer. The provided <see cref="SKCanvas" /> is specific to the layer
/// and matches it's width and height.
/// <para>Called during rendering or layer preview, in the order configured on the layer</para>
/// </summary>
/// <param name="canvas">The layer canvas</param>
/// <param name="canvasInfo"></param>
/// <param name="path">The path to be filled, represents the shape</param>
/// <param name="paint">The paint to be used to fill the shape</param>
/// <inheritdoc />
public virtual void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint)
{
}
/// <summary>
/// Provides an easy way to add your own properties to the layer, for more info see <see cref="LayerProperty{T}" />.
/// <para>Note: If found, the last value and keyframes are loaded and set when calling this method.</para>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parent">The parent of this property, use this to create a tree-hierarchy in the editor</param>
/// <param name="id">A and ID identifying your property, must be unique within your plugin</param>
/// <param name="name">A name for your property, this is visible in the editor</param>
/// <param name="description">A description for your property, this is visible in the editor</param>
/// <param name="defaultValue">The default value of the property, if not configured by the user</param>
/// <returns>The layer property</returns>
protected LayerProperty<T> RegisterLayerProperty<T>(BaseLayerProperty parent, string id, string name, string description, T defaultValue = default)
{
var property = new LayerProperty<T>(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
/// <summary>
/// Provides an easy way to add your own properties to the layer, for more info see <see cref="LayerProperty{T}" />.
/// <para>Note: If found, the last value and keyframes are loaded and set when calling this method.</para>
/// Gets the properties of this brush.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="id">A and ID identifying your property, must be unique within your plugin</param>
/// <param name="name">A name for your property, this is visible in the editor</param>
/// <param name="description">A description for your property, this is visible in the editor</param>
/// <param name="defaultValue">The default value of the property, if not configured by the user</param>
/// <returns>The layer property</returns>
protected LayerProperty<T> RegisterLayerProperty<T>(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<T>) 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<T>(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;
}
/// <summary>
/// Allows you to remove layer properties previously added by using <see cref="RegisterLayerProperty{T}(BaseLayerProperty,string,string,string,T)" />.
/// Gets whether all properties on this brush are initialized
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="layerProperty"></param>
protected void UnRegisterLayerProperty<T>(LayerProperty<T> layerProperty)
public bool PropertiesInitialized { get; private set; }
/// <summary>
/// Called when all layer properties in this brush have been initialized
/// </summary>
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
}
}

View File

@ -20,7 +20,7 @@ namespace Artemis.Core.Plugins.LayerBrush
public ReadOnlyCollection<LayerBrushDescriptor> LayerBrushDescriptors => _layerBrushDescriptors.AsReadOnly();
protected void AddLayerBrushDescriptor<T>(string displayName, string description, string icon) where T : LayerBrush
protected void AddLayerBrushDescriptor<T>(string displayName, string description, string icon) where T : ILayerBrush
{
_layerBrushDescriptors.Add(new LayerBrushDescriptor(displayName, description, icon, typeof(T), this));
}

View File

@ -13,22 +13,9 @@ namespace Artemis.Core.Services.Interfaces
/// </summary>
/// <param name="layer">The layer to instantiate the brush for</param>
/// <returns></returns>
LayerBrush InstantiateLayerBrush(Layer layer);
ILayerBrush InstantiateLayerBrush(Layer layer);
/// <summary>
/// Instantiates and adds a compatible <see cref="KeyframeEngine" /> to the provided <see cref="LayerProperty{T}" />.
/// If the property already has a compatible keyframe engine, nothing happens.
/// </summary>
/// <param name="layerProperty">The layer property to apply the keyframe engine to.</param>
/// <returns>The resulting keyframe engine, if a compatible engine was found.</returns>
KeyframeEngine InstantiateKeyframeEngine<T>(LayerProperty<T> layerProperty);
/// <summary>
/// Instantiates and adds a compatible <see cref="KeyframeEngine" /> to the provided <see cref="BaseLayerProperty" />.
/// If the property already has a compatible keyframe engine, nothing happens.
/// </summary>
/// <param name="layerProperty">The layer property to apply the keyframe engine to.</param>
/// <returns>The resulting keyframe engine, if a compatible engine was found.</returns>
KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty);
void LoadPropertyBaseValue(Layer layer, string path, object layerProperty);
void LoadPropertyKeyframes(Layer layer, string path, object layerProperty);
}
}

View File

@ -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<T>(LayerProperty<T> 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<List<KeyframeEngine>>();
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)

View File

@ -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

View File

@ -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

View File

@ -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<LayerPropertyViewModel> Children { get; }

View File

@ -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;

View File

@ -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<ColorBrushProperties>
{
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<SKColor> ColorProperty { get; set; }
public LayerProperty<ColorGradient> GradientProperty { get; set; }
public LayerProperty<GradientType> 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
}
}

View File

@ -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> 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
}
}

View File

@ -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<NoiseBrushProperties>
{
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<SKPoint>("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<ColorMappingType> ColorTypeProperty { get; set; }
public LayerProperty<SKColor> MainColorProperty { get; set; }
public LayerProperty<SKColor> SecondaryColorProperty { get; set; }
public LayerProperty<ColorGradient> GradientColorProperty { get; set; }
public LayerProperty<SKSize> ScaleProperty { get; set; }
public LayerProperty<float> HardnessProperty { get; set; }
public LayerProperty<SKPoint> ScrollSpeedProperty { get; set; }
public LayerProperty<float> 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;
}

View File

@ -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<ColorMappingType> 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;
}
}
}