using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; 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 class LayerProperty : BaseLayerProperty { private T _baseValue; private bool _isInitialized; private List> _keyframes; protected LayerProperty() { _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; 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; } /// /// 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; } public override IReadOnlyList BaseKeyframes => _keyframes.Cast().ToList().AsReadOnly(); /// /// 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 (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); } /// /// Adds a keyframe to the layer property /// /// The keyframe to add public void AddKeyframe(LayerPropertyKeyframe keyframe) { if (_keyframes.Contains(keyframe)) return; keyframe.LayerProperty?.RemoveKeyframe(keyframe); keyframe.LayerProperty = this; keyframe.BaseLayerProperty = this; _keyframes.Add(keyframe); SortKeyframes(); OnKeyframeAdded(); } /// /// Removes a keyframe from the layer property /// /// The keyframe to remove public LayerPropertyKeyframe CopyKeyframe(LayerPropertyKeyframe keyframe) { 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 (!_keyframes.Contains(keyframe)) return; _keyframes.Remove(keyframe); keyframe.LayerProperty = null; keyframe.BaseLayerProperty = null; SortKeyframes(); OnKeyframeRemoved(); } /// /// Removes all keyframes from the layer property /// public void ClearKeyframes() { var keyframes = new List>(_keyframes); foreach (var layerPropertyKeyframe in keyframes) RemoveKeyframe(layerPropertyKeyframe); } /// public override void ApplyDefaultValue() { BaseValue = DefaultValue; CurrentValue = DefaultValue; } /// public override Type GetPropertyType() { return typeof(T); } /// /// 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(); } /// /// Updates the property, applying keyframes and data bindings to the current value /// internal void Update(double deltaTime) { CurrentValue = BaseValue; UpdateKeyframes(); UpdateDataBindings(deltaTime); OnUpdated(); } 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); } } private void UpdateDataBindings(double deltaTime) { foreach (var dataBinding in DataBindings) { dataBinding.Update(deltaTime); ApplyDataBinding(dataBinding); } } /// /// Sorts the keyframes in ascending order by position /// internal void SortKeyframes() { _keyframes = _keyframes.OrderBy(k => k.Position).ToList(); } internal override void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage) { // Doubt this will happen but let's make sure if (_isInitialized) throw new ArtemisCoreException("Layer property already initialized, wut"); PropertyEntity = entity; LayerPropertyGroup = layerPropertyGroup; LayerPropertyGroup.PropertyGroupUpdating += (sender, args) => Update(args.DeltaTime); try { if (entity.Value != null) BaseValue = JsonConvert.DeserializeObject(entity.Value); IsLoadedFromStorage = fromStorage; CurrentValue = BaseValue; KeyframesEnabled = entity.KeyframesEnabled; _keyframes.Clear(); _keyframes.AddRange(entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe( JsonConvert.DeserializeObject(k.Value), k.Position, (Easings.Functions) k.EasingFunction, this ))); _dataBindings.Clear(); foreach (var entityDataBindingEntity in entity.DataBindingEntities) { var dataBinding = new DataBinding(this, entityDataBindingEntity); _dataBindings.Add(dataBinding); } } catch (JsonException e) { // TODO: Properly log the JSON exception Debug.WriteLine($"JSON exception while deserializing: {e}"); IsLoadedFromStorage = false; } finally { SortKeyframes(); _isInitialized = true; } } internal override void ApplyToEntity() { if (!_isInitialized) throw new ArtemisCoreException("Layer property is not yet initialized"); PropertyEntity.Value = JsonConvert.SerializeObject(BaseValue); PropertyEntity.KeyframesEnabled = KeyframesEnabled; PropertyEntity.KeyframeEntities.Clear(); PropertyEntity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity { Value = JsonConvert.SerializeObject(k.Value), Position = k.Position, EasingFunction = (int) k.EasingFunction })); } #region Data bindings /// public override List GetDataBindingProperties() { return new List {GetType().GetProperty(nameof(CurrentValue))}; } /// protected override void ApplyDataBinding(DataBinding dataBinding) { CurrentValue = (T) dataBinding.GetValue(CurrentValue); } #endregion } }