using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; namespace Artemis.Core { /// /// 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 and annotated, the Artemis core will /// initialize these for you. /// /// /// The type of property encapsulated in this layer property public abstract class LayerProperty : ILayerProperty { private bool _disposed; /// /// Creates a new instance of the class /// protected LayerProperty() { _keyframes = new List>(); } /// /// Gets the description attribute applied to this property /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } /// /// Updates the property, applying keyframes and data bindings to the current value /// public void Update(double deltaTime) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); CurrentValue = BaseValue; UpdateKeyframes(); UpdateDataBindings(deltaTime); OnUpdated(); } /// /// Returns the type of the property /// public Type GetPropertyType() { return typeof(T); } #region Hierarchy private bool _isHidden; /// /// Gets or sets whether the property is hidden in the UI /// public bool IsHidden { get => _isHidden; set { _isHidden = value; OnVisibilityChanged(); } } /// /// Gets the profile element (such as layer or folder) this property is applied to /// public RenderProfileElement ProfileElement { get; internal set; } /// /// The parent group of this layer property, set after construction /// public LayerPropertyGroup LayerPropertyGroup { get; internal set; } #endregion #region Value management private T _baseValue; /// /// 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 virtual void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { throw new NotImplementedException(); } /// /// Gets or sets the base value of this layer property without any keyframes applied /// public T BaseValue { get => _baseValue; set { if (Equals(_baseValue, value)) return; _baseValue = value; Update(0); OnBaseValueChanged(); LayerPropertyGroup.OnLayerPropertyBaseValueChanged(new LayerPropertyEventArgs(this)); } } /// /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame /// public T CurrentValue { get; set; } /// /// Gets or sets the default value of this layer property. If set, this value is automatically applied if the property /// has no value in storage /// public T DefaultValue { get; set; } /// /// Sets the current value, using either keyframes if enabled or the base value. /// /// 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(T value, TimeSpan? time) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (time == null || !KeyframesEnabled || !KeyframesSupported) BaseValue = value; else { // If on a keyframe, update the keyframe var currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); // Create a new keyframe if none found if (currentKeyframe == null) AddKeyframe(new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this)); else currentKeyframe.Value = value; } // Force an update so that the base value is applied to the current value and // keyframes/data bindings are applied using the new base value Update(0); } /// /// Overrides the property value with the default value /// public void ApplyDefaultValue() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); BaseValue = DefaultValue; CurrentValue = DefaultValue; } #endregion #region Keyframes private bool _keyframesEnabled; private List> _keyframes; /// /// Gets whether keyframes are supported on this type of property /// public bool KeyframesSupported { get; protected internal set; } = true; /// /// Gets or sets whether keyframes are enabled on this property, has no effect if is /// False /// public bool KeyframesEnabled { get => _keyframesEnabled; set { if (_keyframesEnabled == value) return; _keyframesEnabled = value; OnKeyframesToggled(); } } /// /// Gets a read-only list of all the keyframes on this layer property /// public IReadOnlyList> Keyframes => _keyframes.AsReadOnly(); /// /// 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; } /// /// Adds a keyframe to the layer property /// /// The keyframe to add public void AddKeyframe(LayerPropertyKeyframe keyframe) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (_keyframes.Contains(keyframe)) return; keyframe.LayerProperty?.RemoveKeyframe(keyframe); keyframe.LayerProperty = this; _keyframes.Add(keyframe); SortKeyframes(); OnKeyframeAdded(); } /// /// Removes a keyframe from the layer property /// /// The keyframe to remove public LayerPropertyKeyframe CopyKeyframe(LayerPropertyKeyframe keyframe) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); var newKeyframe = new LayerPropertyKeyframe( keyframe.Value, keyframe.Position, keyframe.EasingFunction, keyframe.LayerProperty ); AddKeyframe(newKeyframe); return newKeyframe; } /// /// Removes a keyframe from the layer property /// /// The keyframe to remove public void RemoveKeyframe(LayerPropertyKeyframe keyframe) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (!_keyframes.Contains(keyframe)) return; _keyframes.Remove(keyframe); keyframe.LayerProperty = null; SortKeyframes(); OnKeyframeRemoved(); } /// /// Sorts the keyframes in ascending order by position /// internal void SortKeyframes() { _keyframes = _keyframes.OrderBy(k => k.Position).ToList(); } private void UpdateKeyframes() { if (!KeyframesSupported || !KeyframesEnabled) return; // The current keyframe is the last keyframe before the current time CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= ProfileElement.TimelinePosition); // Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current var nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1; NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null; // No need to update the current value if either of the keyframes are null if (CurrentKeyframe == null) CurrentValue = _keyframes.Any() ? _keyframes[0].Value : 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) ((ProfileElement.TimelinePosition - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); var keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); UpdateCurrentValue(keyframeProgress, keyframeProgressEased); } } #endregion #region Data bindings internal readonly List _dataBindingRegistrations = new List(); internal readonly List _dataBindings = new List(); /// /// Gets whether data bindings are supported on this type of property /// public bool DataBindingsSupported { get; protected internal set; } = true; public DataBindingRegistration GetDataBindingRegistration(string expression) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); var match = _dataBindingRegistrations.FirstOrDefault(r => r is DataBindingRegistration registration && registration.PropertyExpression.ToString() == expression); return (DataBindingRegistration) match; } public List GetAllDataBindingRegistrations() { return _dataBindingRegistrations; } public void RegisterDataBindingProperty(Expression> propertyExpression, DataBindingConverter converter) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (propertyExpression.Body.NodeType != ExpressionType.MemberAccess && propertyExpression.Body.NodeType != ExpressionType.Parameter) throw new ArtemisCoreException("Provided expression is invalid, it must be 'value => value' or 'value => value.Property'"); if (converter.SupportedType != propertyExpression.ReturnType) { throw new ArtemisCoreException($"Cannot register data binding property for property {PropertyDescription.Name} " + "because the provided converter does not support the property's type"); } _dataBindingRegistrations.Add(new DataBindingRegistration(this, converter, propertyExpression)); } /// /// Enables a data binding for the provided /// /// The newly created data binding public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (dataBindingRegistration.LayerProperty != this) throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); var dataBinding = new DataBinding(dataBindingRegistration); _dataBindings.Add(dataBinding); return dataBinding; } /// /// Disables the provided data binding /// /// The data binding to remove public void DisableDataBinding(DataBinding dataBinding) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); _dataBindings.Remove(dataBinding); } private void UpdateDataBindings(double deltaTime) { foreach (var dataBinding in _dataBindings) { dataBinding.Update(deltaTime); dataBinding.Apply(); } } #endregion #region Storage private bool _isInitialized; /// /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied /// public bool IsLoadedFromStorage { get; internal set; } internal PropertyEntity Entity { get; set; } /// public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); _isInitialized = true; ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); Entity = entity ?? throw new ArgumentNullException(nameof(entity)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); IsLoadedFromStorage = fromStorage; LayerPropertyGroup.PropertyGroupUpdating += (sender, args) => Update(args.DeltaTime); } /// public void Load() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (!_isInitialized) throw new ArtemisCoreException("Layer property is not yet initialized"); if (!IsLoadedFromStorage) ApplyDefaultValue(); else { try { if (Entity.Value != null) BaseValue = JsonConvert.DeserializeObject(Entity.Value); } catch (JsonException e) { // ignored for now } } CurrentValue = BaseValue; KeyframesEnabled = Entity.KeyframesEnabled; _keyframes.Clear(); try { _keyframes.AddRange(Entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe( JsonConvert.DeserializeObject(k.Value), k.Position, (Easings.Functions) k.EasingFunction, this ))); } catch (JsonException e) { // ignored for now } _dataBindings.Clear(); foreach (var dataBindingRegistration in _dataBindingRegistrations) { var dataBinding = dataBindingRegistration.CreateDataBinding(); if (dataBinding != null) _dataBindings.Add(dataBinding); } } /// /// Saves the property to the underlying property entity that was configured when calling /// /// public void Save() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (!_isInitialized) throw new ArtemisCoreException("Layer property is not yet initialized"); Entity.Value = JsonConvert.SerializeObject(BaseValue); Entity.KeyframesEnabled = KeyframesEnabled; Entity.KeyframeEntities.Clear(); Entity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity { Value = JsonConvert.SerializeObject(k.Value), Position = k.Position, EasingFunction = (int) k.EasingFunction })); Entity.DataBindingEntities.Clear(); foreach (var dataBinding in _dataBindings) dataBinding.Save(); } #endregion #region Events /// /// Occurs once every frame when the layer property is updated /// public event EventHandler> Updated; /// /// Occurs when the base value of the layer property was updated /// public event EventHandler> BaseValueChanged; /// /// Occurs when the value of the layer property was updated /// public event EventHandler> VisibilityChanged; /// /// Occurs when keyframes are enabled/disabled /// public event EventHandler> KeyframesToggled; /// /// Occurs when a new keyframe was added to the layer property /// public event EventHandler> KeyframeAdded; /// /// Occurs when a keyframe was removed from the layer property /// public event EventHandler> KeyframeRemoved; protected virtual void OnUpdated() { Updated?.Invoke(this, new LayerPropertyEventArgs(this)); } protected virtual void OnBaseValueChanged() { BaseValueChanged?.Invoke(this, new LayerPropertyEventArgs(this)); } protected virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); } protected virtual void OnKeyframesToggled() { KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); } protected virtual void OnKeyframeAdded() { KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); } protected virtual void OnKeyframeRemoved() { KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); } #endregion /// public void Dispose() { _disposed = true; foreach (var dataBinding in _dataBindings) dataBinding.Dispose(); } } }