From 0958c3af9f095865f7a2432d66746591cc611b40 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 9 Jan 2020 21:13:29 +0100 Subject: [PATCH] Implemented basic keyframes --- src/Artemis.Core/Artemis.Core.csproj | 1 + .../KeyframeEngines/FloatKeyframeEngine.cs | 9 +- .../KeyframeEngines/IntKeyframeEngine.cs | 9 +- .../Profile/KeyframeEngines/KeyframeEngine.cs | 64 ++- .../KeyframeEngines/SKPointKeyframeEngine.cs | 10 +- .../KeyframeEngines/SKSizeKeyframeEngine.cs | 10 +- .../Profile/LayerProperties/BaseKeyframe.cs | 2 +- .../LayerProperties/BaseLayerProperty.cs | 34 +- .../Profile/LayerProperties/LayerProperty.cs | 17 +- .../Models/Profile/LayerShapes/LayerShape.cs | 10 +- src/Artemis.Core/Ninject/CoreModule.cs | 10 + .../Services/Interfaces/ILayerService.cs | 16 + src/Artemis.Core/Services/LayerService.cs | 21 + .../Services/Storage/ProfileService.cs | 23 + src/Artemis.Core/Utilities/Easings.cs | 398 ++++++++++++++++++ .../Ninject/Factories/IViewModelFactory.cs | 12 + .../Screens/Module/ModuleRootView.xaml | 2 +- .../LayerPropertiesViewModel.cs | 45 +- .../LayerProperties/LayerPropertyViewModel.cs | 16 +- .../PropertyInput/FloatPropertyInputView.xaml | 1 + .../FloatPropertyInputViewModel.cs | 29 +- .../PropertyInput/IntPropertyInputView.xaml | 1 + .../IntPropertyInputViewModel.cs | 29 +- .../PropertyInput/PropertyInputViewModel.cs | 34 +- .../SKPointPropertyInputView.xaml | 8 +- .../SKPointPropertyInputViewModel.cs | 40 +- .../SKSizePropertyInputView.xaml | 2 + .../SKSizePropertyInputViewModel.cs | 40 +- .../PropertyTree/PropertyTreeChildView.xaml | 2 +- .../PropertyTreeChildViewModel.cs | 12 + .../PropertyTree/PropertyTreeItemViewModel.cs | 7 +- .../PropertyTreeParentViewModel.cs | 6 + .../PropertyTree/PropertyTreeViewModel.cs | 23 +- .../Timeline/PropertyTimelineView.xaml | 2 +- .../Timeline/PropertyTimelineViewModel.cs | 31 +- .../Timeline/PropertyTrackViewModel.cs | 12 +- .../Visualization/ProfileLayerView.xaml | 2 +- .../Visualization/ProfileLayerViewModel.cs | 21 +- src/Artemis.UI/Screens/RootView.xaml | 2 +- .../Interfaces/IProfileEditorService.cs | 21 + .../Services/ProfileEditorService.cs | 40 ++ 41 files changed, 951 insertions(+), 123 deletions(-) create mode 100644 src/Artemis.Core/Utilities/Easings.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index d25e4455b..d9240f12b 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -212,6 +212,7 @@ + diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs index 75272488b..4fd1313b6 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs @@ -9,10 +9,13 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines { public sealed override List CompatibleTypes { get; } = new List {typeof(float)}; - public override object GetCurrentValue() + protected override object GetInterpolatedValue() { - // Nothing fancy for now, just return the base value - return ((LayerProperty) LayerProperty).Value; + var currentKeyframe = (Keyframe) CurrentKeyframe; + var nextKeyframe = (Keyframe) NextKeyframe; + + var diff = nextKeyframe.Value - currentKeyframe.Value; + return currentKeyframe.Value + diff * KeyframeProgress; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs index e387e071e..31b202643 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs @@ -9,10 +9,13 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines { public sealed override List CompatibleTypes { get; } = new List {typeof(int)}; - public override object GetCurrentValue() + protected override object GetInterpolatedValue() { - // Nothing fancy for now, just return the base value - return ((LayerProperty) LayerProperty).Value; + var currentKeyframe = (Keyframe) CurrentKeyframe; + var nextKeyframe = (Keyframe) NextKeyframe; + + var diff = nextKeyframe.Value - currentKeyframe.Value; + return currentKeyframe.Value + diff * KeyframeProgress; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs index a6a6d00d1..c9793812f 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Artemis.Core.Exceptions; using Artemis.Core.Models.Profile.LayerProperties; @@ -15,12 +16,27 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines /// /// The layer property this keyframe engine applies to. /// - public BaseLayerProperty LayerProperty { get; set; } + public BaseLayerProperty LayerProperty { get; private set; } /// - /// The keyframe progress in milliseconds. + /// The total progress /// - public double Progress { get; set; } + public TimeSpan Progress { get; private set; } + + /// + /// The progress from the current keyframe to the next 0 to 1 + /// + public float KeyframeProgress { get; private set; } + + /// + /// The current keyframe + /// + public BaseKeyframe CurrentKeyframe { get; private set; } + + /// + /// The next keyframe + /// + public BaseKeyframe NextKeyframe { get; private set; } /// /// The types this keyframe engine supports. @@ -39,6 +55,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines throw new ArtemisCoreException($"This property engine does not support the provided type {layerProperty.Type.Name}"); LayerProperty = layerProperty; + LayerProperty.KeyframeEngine = this; Initialized = true; } @@ -51,15 +68,52 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines if (!Initialized) return; - Progress += deltaTime; + Progress = Progress.Add(TimeSpan.FromMilliseconds(deltaTime)); + + // TODO Keep them sorted somewhere else, iterating all keyframes multiple times sucks + var sortedKeyframes = LayerProperty.UntypedKeyframes.ToList().OrderBy(k => k.Position).ToList(); + + CurrentKeyframe = sortedKeyframes.LastOrDefault(k => k.Position <= Progress); + NextKeyframe = sortedKeyframes.FirstOrDefault(k => k.Position > Progress); + if (CurrentKeyframe == null) + KeyframeProgress = 0; + else if (NextKeyframe == null) + KeyframeProgress = 1; + else + { + var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; + KeyframeProgress = (float) ((Progress - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); + } + + // TODO Apply easing and store it separately // LayerProperty determines what's next: reset, stop, continue } + /// + /// Overrides the engine's progress to the provided value + /// + /// + public void OverrideProgress(TimeSpan progress) + { + Progress = TimeSpan.Zero; + Update(progress.TotalMilliseconds); + } + /// /// Gets the current value, if the progress is in between two keyframes the value will be interpolated /// /// - public abstract object GetCurrentValue(); + public object GetCurrentValue() + { + if (CurrentKeyframe == null) + return LayerProperty.BaseValue; + if (NextKeyframe == null) + return CurrentKeyframe.BaseValue; + + return GetInterpolatedValue(); + } + + protected abstract object GetInterpolatedValue(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs index 15801bf3c..b46e34bea 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs @@ -10,10 +10,14 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines { public sealed override List CompatibleTypes { get; } = new List {typeof(SKPoint)}; - public override object GetCurrentValue() + protected override object GetInterpolatedValue() { - // Nothing fancy for now, just return the base value - return ((LayerProperty) LayerProperty).Value; + var currentKeyframe = (Keyframe) CurrentKeyframe; + var nextKeyframe = (Keyframe) NextKeyframe; + + var xDiff = nextKeyframe.Value.X - currentKeyframe.Value.X; + var yDiff = nextKeyframe.Value.Y - currentKeyframe.Value.Y; + return new SKPoint(currentKeyframe.Value.X + xDiff * KeyframeProgress, currentKeyframe.Value.Y + yDiff * KeyframeProgress); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs index 7bc7a1f7a..59c4c5b97 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs @@ -10,10 +10,14 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines { public sealed override List CompatibleTypes { get; } = new List {typeof(SKSize)}; - public override object GetCurrentValue() + protected override object GetInterpolatedValue() { - // Nothing fancy for now, just return the base value - return ((LayerProperty) LayerProperty).Value; + var currentKeyframe = (Keyframe)CurrentKeyframe; + var nextKeyframe = (Keyframe)NextKeyframe; + + var widthDiff = nextKeyframe.Value.Width - currentKeyframe.Value.Width; + var heightDiff = nextKeyframe.Value.Height - currentKeyframe.Value.Height; + return new SKSize(currentKeyframe.Value.Width + widthDiff * KeyframeProgress, currentKeyframe.Value.Height + heightDiff * KeyframeProgress); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs index ddb571051..a7a36f52b 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs @@ -4,7 +4,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties { public class BaseKeyframe { - protected internal BaseKeyframe(Layer layer, BaseLayerProperty property) + protected BaseKeyframe(Layer layer, BaseLayerProperty property) { Layer = layer; BaseProperty = property; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs index d5a3fc074..fef95bb8d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs @@ -12,7 +12,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties { private object _baseValue; - protected internal BaseLayerProperty(Layer layer, BaseLayerProperty parent, string id, string name, string description, Type type) + protected BaseLayerProperty(Layer layer, BaseLayerProperty parent, string id, string name, string description, Type type) { Layer = layer; Parent = parent; @@ -125,7 +125,37 @@ namespace Artemis.Core.Models.Profile.LayerProperties BaseKeyframes.Clear(); foreach (var keyframeEntity in propertyEntity.KeyframeEntities) - BaseKeyframes.Add(new BaseKeyframe(Layer, this) {BaseValue = DeserializePropertyValue(keyframeEntity.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.BaseValue = DeserializePropertyValue(keyframeEntity.Value); + BaseKeyframes.Add(keyframe); + } + } + + /// + /// Creates a new keyframe for this base property without knowing the type + /// + /// + public BaseKeyframe CreateNewKeyframe(TimeSpan 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 = position; + keyframe.BaseValue = BaseValue; + BaseKeyframes.Add(keyframe); + + return keyframe; + } + + /// + /// Removes all keyframes from the property. + /// + public void ClearKeyframes() + { + BaseKeyframes.Clear(); } public override string ToString() diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 70a3530d2..59fe67272 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -18,6 +18,11 @@ namespace Artemis.Core.Models.Profile.LayerProperties set => BaseValue = value; } + /// + /// The value of the property with keyframes applied + /// + public T CurrentValue => (T) KeyframeEngine.GetCurrentValue(); + /// /// A list of keyframes defining different values of the property in time, this list contains the strongly typed /// @@ -43,20 +48,12 @@ namespace Artemis.Core.Models.Profile.LayerProperties } /// - /// Removes all keyframes from the property. - /// - public void ClearKeyframes() - { - BaseKeyframes.Clear(); - } - - /// - /// Gets the current value using the keyframes + /// Gets the current value using the keyframes /// /// public T GetCurrentValue() { - if (KeyframeEngine == null) + if (KeyframeEngine == null || !Keyframes.Any()) return Value; return (T) KeyframeEngine.GetCurrentValue(); diff --git a/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs b/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs index 074908089..98b7654fc 100644 --- a/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs +++ b/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs @@ -60,7 +60,7 @@ namespace Artemis.Core.Models.Profile.LayerShapes Layer.SizeProperty.Value = new SKSize((float) (100f / width * rect.Width) / 100f, (float) (100f / height * rect.Height) / 100f); // TODO: Update keyframes - CalculateRenderProperties(Layer.PositionProperty.Value, Layer.SizeProperty.Value); + CalculateRenderProperties(Layer.PositionProperty.CurrentValue, Layer.SizeProperty.CurrentValue); } public SKRect GetUnscaledRectangle() @@ -74,10 +74,10 @@ namespace Artemis.Core.Models.Profile.LayerShapes var height = Layer.Leds.Max(l => l.RgbLed.AbsoluteLedRectangle.Location.Y + l.RgbLed.AbsoluteLedRectangle.Size.Height) - y; return SKRect.Create( - (float) (x + width * Layer.PositionProperty.Value.X), - (float) (y + height * Layer.PositionProperty.Value.Y), - (float) (width * Layer.SizeProperty.Value.Width), - (float) (height * Layer.SizeProperty.Value.Height) + (float) (x + width * Layer.PositionProperty.CurrentValue.X), + (float) (y + height * Layer.PositionProperty.CurrentValue.Y), + (float) (width * Layer.SizeProperty.CurrentValue.Width), + (float) (height * Layer.SizeProperty.CurrentValue.Height) ); } } diff --git a/src/Artemis.Core/Ninject/CoreModule.cs b/src/Artemis.Core/Ninject/CoreModule.cs index 521866a3c..66ea2d9d0 100644 --- a/src/Artemis.Core/Ninject/CoreModule.cs +++ b/src/Artemis.Core/Ninject/CoreModule.cs @@ -1,5 +1,6 @@ using System.IO; using Artemis.Core.Exceptions; +using Artemis.Core.Models.Profile.KeyframeEngines; using Artemis.Core.Plugins.Models; using Artemis.Core.Services.Interfaces; using Artemis.Storage.Repositories.Interfaces; @@ -50,6 +51,15 @@ namespace Artemis.Core.Ninject .Configure(c => c.InSingletonScope()); }); + // Bind all keyframe engines + Kernel.Bind(x => + { + x.FromAssemblyContaining() + .SelectAllClasses() + .InheritedFrom() + .BindAllBaseClasses(); + }); + Kernel.Bind().ToProvider(); Kernel.Bind().ToProvider(); } diff --git a/src/Artemis.Core/Services/Interfaces/ILayerService.cs b/src/Artemis.Core/Services/Interfaces/ILayerService.cs index 73c4a6ef4..77e0cba6b 100644 --- a/src/Artemis.Core/Services/Interfaces/ILayerService.cs +++ b/src/Artemis.Core/Services/Interfaces/ILayerService.cs @@ -1,4 +1,6 @@ using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.KeyframeEngines; +using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerBrush; namespace Artemis.Core.Services.Interfaces @@ -14,5 +16,19 @@ namespace Artemis.Core.Services.Interfaces /// JSON settings to be deserialized and injected into the layer brush /// LayerBrush InstantiateLayerBrush(Layer layer, LayerBrushDescriptor brushDescriptor, string settings = null); + + /// + /// Instantiates and adds a compatible to the provided + /// + /// The layer property to apply the keyframe engine to. + /// The resulting keyframe engine, if a compatible engine was found. + KeyframeEngine InstantiateKeyframeEngine(LayerProperty layerProperty); + + /// + /// Instantiates and adds a compatible to the provided . + /// + /// The layer property to apply the keyframe engine to. + /// The resulting keyframe engine, if a compatible engine was found. + KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/LayerService.cs b/src/Artemis.Core/Services/LayerService.cs index 17f52dc5a..1bbc10eba 100644 --- a/src/Artemis.Core/Services/LayerService.cs +++ b/src/Artemis.Core/Services/LayerService.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.KeyframeEngines; +using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.Exceptions; using Artemis.Core.Plugins.LayerBrush; using Artemis.Core.Services.Interfaces; @@ -74,6 +77,24 @@ namespace Artemis.Core.Services return layerElement; } + public KeyframeEngine InstantiateKeyframeEngine(LayerProperty layerProperty) + { + return InstantiateKeyframeEngine((BaseLayerProperty) layerProperty); + } + + public KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty) + { + // This creates an instance of each keyframe engine, which is pretty cheap since all the expensive stuff is done during + // Initialize() call but it's not ideal + var keyframeEngines = _kernel.Get>(); + var keyframeEngine = keyframeEngines.FirstOrDefault(k => k.CompatibleTypes.Contains(layerProperty.Type)); + if (keyframeEngine == null) + return null; + + keyframeEngine.Initialize(layerProperty); + return keyframeEngine; + } + public void RemoveLayerBrush(Layer layer, LayerBrush layerElement) { var brush = layer.LayerBrush; diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index bc661c5c7..1f84a08a5 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -70,6 +70,7 @@ namespace Artemis.Core.Services.Storage if (_surfaceService.ActiveSurface != null) profile.PopulateLeds(_surfaceService.ActiveSurface); + return profile; } @@ -78,7 +79,10 @@ namespace Artemis.Core.Services.Storage { module.ChangeActiveProfile(profile, _surfaceService.ActiveSurface); if (profile != null) + { InstantiateProfileLayerBrushes(profile); + InstantiateProfileKeyframeEngines(profile); + } } public void DeleteProfile(Profile profile) @@ -118,6 +122,15 @@ namespace Artemis.Core.Services.Storage } } + private void InstantiateProfileKeyframeEngines(Profile profile) + { + // Only instantiate engines for properties without an existing engine instance + foreach (var layerProperty in profile.GetAllLayers().SelectMany(l => l.Properties).Where(p => p.KeyframeEngine == null)) + { + _layerService.InstantiateKeyframeEngine(layerProperty); + } + } + private void ActiveProfilesPopulateLeds(ArtemisSurface surface) { var profileModules = _pluginService.GetPluginsOfType(); @@ -132,6 +145,13 @@ namespace Artemis.Core.Services.Storage InstantiateProfileLayerBrushes(profileModule.ActiveProfile); } + private void ActiveProfilesInstantiateKeyframeEngines() + { + var profileModules = _pluginService.GetPluginsOfType(); + foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList()) + InstantiateProfileKeyframeEngines(profileModule.ActiveProfile); + } + #region Event handlers private void OnActiveSurfaceConfigurationChanged(object sender, SurfaceConfigurationEventArgs e) @@ -148,7 +168,10 @@ namespace Artemis.Core.Services.Storage private void OnPluginLoaded(object sender, PluginEventArgs e) { if (e.PluginInfo.Instance is LayerBrushProvider) + { ActiveProfilesInstantiateProfileLayerBrushes(); + ActiveProfilesInstantiateKeyframeEngines(); + } } #endregion diff --git a/src/Artemis.Core/Utilities/Easings.cs b/src/Artemis.Core/Utilities/Easings.cs new file mode 100644 index 000000000..86e9448a9 --- /dev/null +++ b/src/Artemis.Core/Utilities/Easings.cs @@ -0,0 +1,398 @@ +using System; + +namespace Artemis.Core.Utilities +{ + public static class Easings + { + /// + /// Easing Functions enumeration + /// + public enum Functions + { + Linear, + QuadraticEaseIn, + QuadraticEaseOut, + QuadraticEaseInOut, + CubicEaseIn, + CubicEaseOut, + CubicEaseInOut, + QuarticEaseIn, + QuarticEaseOut, + QuarticEaseInOut, + QuinticEaseIn, + QuinticEaseOut, + QuinticEaseInOut, + SineEaseIn, + SineEaseOut, + SineEaseInOut, + CircularEaseIn, + CircularEaseOut, + CircularEaseInOut, + ExponentialEaseIn, + ExponentialEaseOut, + ExponentialEaseInOut, + ElasticEaseIn, + ElasticEaseOut, + ElasticEaseInOut, + BackEaseIn, + BackEaseOut, + BackEaseInOut, + BounceEaseIn, + BounceEaseOut, + BounceEaseInOut + } + + /// + /// Constant Pi. + /// + private const double PI = Math.PI; + + /// + /// Constant Pi / 2. + /// + private const double HALFPI = Math.PI / 2.0; + + /// + /// Interpolate using the specified function. + /// + public static double Interpolate(double p, Functions function) + { + switch (function) + { + default: + case Functions.Linear: return Linear(p); + case Functions.QuadraticEaseOut: return QuadraticEaseOut(p); + case Functions.QuadraticEaseIn: return QuadraticEaseIn(p); + case Functions.QuadraticEaseInOut: return QuadraticEaseInOut(p); + case Functions.CubicEaseIn: return CubicEaseIn(p); + case Functions.CubicEaseOut: return CubicEaseOut(p); + case Functions.CubicEaseInOut: return CubicEaseInOut(p); + case Functions.QuarticEaseIn: return QuarticEaseIn(p); + case Functions.QuarticEaseOut: return QuarticEaseOut(p); + case Functions.QuarticEaseInOut: return QuarticEaseInOut(p); + case Functions.QuinticEaseIn: return QuinticEaseIn(p); + case Functions.QuinticEaseOut: return QuinticEaseOut(p); + case Functions.QuinticEaseInOut: return QuinticEaseInOut(p); + case Functions.SineEaseIn: return SineEaseIn(p); + case Functions.SineEaseOut: return SineEaseOut(p); + case Functions.SineEaseInOut: return SineEaseInOut(p); + case Functions.CircularEaseIn: return CircularEaseIn(p); + case Functions.CircularEaseOut: return CircularEaseOut(p); + case Functions.CircularEaseInOut: return CircularEaseInOut(p); + case Functions.ExponentialEaseIn: return ExponentialEaseIn(p); + case Functions.ExponentialEaseOut: return ExponentialEaseOut(p); + case Functions.ExponentialEaseInOut: return ExponentialEaseInOut(p); + case Functions.ElasticEaseIn: return ElasticEaseIn(p); + case Functions.ElasticEaseOut: return ElasticEaseOut(p); + case Functions.ElasticEaseInOut: return ElasticEaseInOut(p); + case Functions.BackEaseIn: return BackEaseIn(p); + case Functions.BackEaseOut: return BackEaseOut(p); + case Functions.BackEaseInOut: return BackEaseInOut(p); + case Functions.BounceEaseIn: return BounceEaseIn(p); + case Functions.BounceEaseOut: return BounceEaseOut(p); + case Functions.BounceEaseInOut: return BounceEaseInOut(p); + } + } + + /// + /// Modeled after the line y = x + /// + public static double Linear(double p) + { + return p; + } + + /// + /// Modeled after the parabola y = x^2 + /// + public static double QuadraticEaseIn(double p) + { + return p * p; + } + + /// + /// Modeled after the parabola y = -x^2 + 2x + /// + public static double QuadraticEaseOut(double p) + { + return -(p * (p - 2)); + } + + /// + /// Modeled after the piecewise quadratic + /// y = (1/2)((2x)^2) ; [0, 0.5) + /// y = -(1/2)((2x-1)*(2x-3) - 1) ; [0.5, 1] + /// + public static double QuadraticEaseInOut(double p) + { + if (p < 0.5) + return 2 * p * p; + return -2 * p * p + 4 * p - 1; + } + + /// + /// Modeled after the cubic y = x^3 + /// + public static double CubicEaseIn(double p) + { + return p * p * p; + } + + /// + /// Modeled after the cubic y = (x - 1)^3 + 1 + /// + public static double CubicEaseOut(double p) + { + var f = p - 1; + return f * f * f + 1; + } + + /// + /// Modeled after the piecewise cubic + /// y = (1/2)((2x)^3) ; [0, 0.5) + /// y = (1/2)((2x-2)^3 + 2) ; [0.5, 1] + /// + public static double CubicEaseInOut(double p) + { + if (p < 0.5) + return 4 * p * p * p; + var f = 2 * p - 2; + return 0.5 * f * f * f + 1; + } + + /// + /// Modeled after the quartic x^4 + /// + public static double QuarticEaseIn(double p) + { + return p * p * p * p; + } + + /// + /// Modeled after the quartic y = 1 - (x - 1)^4 + /// + public static double QuarticEaseOut(double p) + { + var f = p - 1; + return f * f * f * (1 - p) + 1; + } + + /// + // Modeled after the piecewise quartic + // y = (1/2)((2x)^4) ; [0, 0.5) + // y = -(1/2)((2x-2)^4 - 2) ; [0.5, 1] + /// + public static double QuarticEaseInOut(double p) + { + if (p < 0.5) + return 8 * p * p * p * p; + var f = p - 1; + return -8 * f * f * f * f + 1; + } + + /// + /// Modeled after the quintic y = x^5 + /// + public static double QuinticEaseIn(double p) + { + return p * p * p * p * p; + } + + /// + /// Modeled after the quintic y = (x - 1)^5 + 1 + /// + public static double QuinticEaseOut(double p) + { + var f = p - 1; + return f * f * f * f * f + 1; + } + + /// + /// Modeled after the piecewise quintic + /// y = (1/2)((2x)^5) ; [0, 0.5) + /// y = (1/2)((2x-2)^5 + 2) ; [0.5, 1] + /// + public static double QuinticEaseInOut(double p) + { + if (p < 0.5) + return 16 * p * p * p * p * p; + var f = 2 * p - 2; + return 0.5 * f * f * f * f * f + 1; + } + + /// + /// Modeled after quarter-cycle of sine wave + /// + public static double SineEaseIn(double p) + { + return Math.Sin((p - 1) * HALFPI) + 1; + } + + /// + /// Modeled after quarter-cycle of sine wave (different phase) + /// + public static double SineEaseOut(double p) + { + return Math.Sin(p * HALFPI); + } + + /// + /// Modeled after half sine wave + /// + public static double SineEaseInOut(double p) + { + return 0.5 * (1 - Math.Cos(p * PI)); + } + + /// + /// Modeled after shifted quadrant IV of unit circle + /// + public static double CircularEaseIn(double p) + { + return 1 - Math.Sqrt(1 - p * p); + } + + /// + /// Modeled after shifted quadrant II of unit circle + /// + public static double CircularEaseOut(double p) + { + return Math.Sqrt((2 - p) * p); + } + + /// + /// Modeled after the piecewise circular function + /// y = (1/2)(1 - Math.Sqrt(1 - 4x^2)) ; [0, 0.5) + /// y = (1/2)(Math.Sqrt(-(2x - 3)*(2x - 1)) + 1) ; [0.5, 1] + /// + public static double CircularEaseInOut(double p) + { + if (p < 0.5) + return 0.5 * (1 - Math.Sqrt(1 - 4 * (p * p))); + return 0.5 * (Math.Sqrt(-(2 * p - 3) * (2 * p - 1)) + 1); + } + + /// + /// Modeled after the exponential function y = 2^(10(x - 1)) + /// + public static double ExponentialEaseIn(double p) + { + return p == 0.0 ? p : Math.Pow(2, 10 * (p - 1)); + } + + /// + /// Modeled after the exponential function y = -2^(-10x) + 1 + /// + public static double ExponentialEaseOut(double p) + { + return p == 1.0 ? p : 1 - Math.Pow(2, -10 * p); + } + + /// + /// Modeled after the piecewise exponential + /// y = (1/2)2^(10(2x - 1)) ; [0,0.5) + /// y = -(1/2)*2^(-10(2x - 1))) + 1 ; [0.5,1] + /// + public static double ExponentialEaseInOut(double p) + { + if (p == 0.0 || p == 1.0) return p; + + if (p < 0.5) + return 0.5 * Math.Pow(2, 20 * p - 10); + return -0.5 * Math.Pow(2, -20 * p + 10) + 1; + } + + /// + /// Modeled after the damped sine wave y = sin(13pi/2*x)*Math.Pow(2, 10 * (x - 1)) + /// + public static double ElasticEaseIn(double p) + { + return Math.Sin(13 * HALFPI * p) * Math.Pow(2, 10 * (p - 1)); + } + + /// + /// Modeled after the damped sine wave y = sin(-13pi/2*(x + 1))*Math.Pow(2, -10x) + 1 + /// + public static double ElasticEaseOut(double p) + { + return Math.Sin(-13 * HALFPI * (p + 1)) * Math.Pow(2, -10 * p) + 1; + } + + /// + /// Modeled after the piecewise exponentially-damped sine wave: + /// y = (1/2)*sin(13pi/2*(2*x))*Math.Pow(2, 10 * ((2*x) - 1)) ; [0,0.5) + /// y = (1/2)*(sin(-13pi/2*((2x-1)+1))*Math.Pow(2,-10(2*x-1)) + 2) ; [0.5, 1] + /// + public static double ElasticEaseInOut(double p) + { + if (p < 0.5) + return 0.5 * Math.Sin(13 * HALFPI * (2 * p)) * Math.Pow(2, 10 * (2 * p - 1)); + return 0.5 * (Math.Sin(-13 * HALFPI * (2 * p - 1 + 1)) * Math.Pow(2, -10 * (2 * p - 1)) + 2); + } + + /// + /// Modeled after the overshooting cubic y = x^3-x*sin(x*pi) + /// + public static double BackEaseIn(double p) + { + return p * p * p - p * Math.Sin(p * PI); + } + + /// + /// Modeled after overshooting cubic y = 1-((1-x)^3-(1-x)*sin((1-x)*pi)) + /// + public static double BackEaseOut(double p) + { + var f = 1 - p; + return 1 - (f * f * f - f * Math.Sin(f * PI)); + } + + /// + /// Modeled after the piecewise overshooting cubic function: + /// y = (1/2)*((2x)^3-(2x)*sin(2*x*pi)) ; [0, 0.5) + /// y = (1/2)*(1-((1-x)^3-(1-x)*sin((1-x)*pi))+1) ; [0.5, 1] + /// + public static double BackEaseInOut(double p) + { + if (p < 0.5) + { + var f = 2 * p; + return 0.5 * (f * f * f - f * Math.Sin(f * PI)); + } + else + { + var f = 1 - (2 * p - 1); + return 0.5 * (1 - (f * f * f - f * Math.Sin(f * PI))) + 0.5; + } + } + + /// + /// + public static double BounceEaseIn(double p) + { + return 1 - BounceEaseOut(1 - p); + } + + /// + /// + public static double BounceEaseOut(double p) + { + if (p < 4 / 11.0) + return 121 * p * p / 16.0; + if (p < 8 / 11.0) + return 363 / 40.0 * p * p - 99 / 10.0 * p + 17 / 5.0; + if (p < 9 / 10.0) + return 4356 / 361.0 * p * p - 35442 / 1805.0 * p + 16061 / 1805.0; + return 54 / 5.0 * p * p - 513 / 25.0 * p + 268 / 25.0; + } + + /// + /// + public static double BounceEaseInOut(double p) + { + if (p < 0.5) + return 0.5 * BounceEaseIn(p * 2); + return 0.5 * BounceEaseOut(p * 2 - 1) + 0.5; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs b/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs index 7ef5d016b..74de4bf92 100644 --- a/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs @@ -5,6 +5,8 @@ using Artemis.Core.Plugins.Abstract; using Artemis.UI.Screens.Module; using Artemis.UI.Screens.Module.ProfileEditor; using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties; +using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree; +using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Screens.Module.ProfileEditor.ProfileTree.TreeItem; using Artemis.UI.Screens.Module.ProfileEditor.Visualization; using Artemis.UI.Screens.Settings.Tabs.Devices; @@ -50,4 +52,14 @@ namespace Artemis.UI.Ninject.Factories { LayerPropertyViewModel Create(BaseLayerProperty layerProperty, LayerPropertyViewModel parent); } + + public interface IPropertyTreeViewModelFactory : IViewModelFactory + { + PropertyTreeViewModel Create(LayerPropertiesViewModel layerPropertiesViewModel); + } + + public interface IPropertyTimelineViewModelFactory : IViewModelFactory + { + PropertyTimelineViewModel Create(LayerPropertiesViewModel layerPropertiesViewModel); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ModuleRootView.xaml b/src/Artemis.UI/Screens/Module/ModuleRootView.xaml index 2f4034e6c..12b89d48e 100644 --- a/src/Artemis.UI/Screens/Module/ModuleRootView.xaml +++ b/src/Artemis.UI/Screens/Module/ModuleRootView.xaml @@ -18,7 +18,7 @@ - + diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 7ce4bd527..3f9535ed2 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -15,29 +15,22 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties private readonly ILayerPropertyViewModelFactory _layerPropertyViewModelFactory; private readonly IProfileEditorService _profileEditorService; - public LayerPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyViewModelFactory layerPropertyViewModelFactory) + public LayerPropertiesViewModel(IProfileEditorService profileEditorService, + ILayerPropertyViewModelFactory layerPropertyViewModelFactory, + IPropertyTreeViewModelFactory propertyTreeViewModelFactory, + IPropertyTimelineViewModelFactory propertyTimelineViewModelFactory) { _profileEditorService = profileEditorService; _layerPropertyViewModelFactory = layerPropertyViewModelFactory; - CurrentTime = TimeSpan.Zero; PixelsPerSecond = 1; - PropertyTree = new PropertyTreeViewModel(this); - PropertyTimeline = new PropertyTimelineViewModel(this); + PropertyTree = propertyTreeViewModelFactory.Create(this); + PropertyTimeline = propertyTimelineViewModelFactory.Create(this); PopulateProperties(); _profileEditorService.SelectedProfileElementChanged += (sender, args) => PopulateProperties(); - } - - public TimeSpan CurrentTime - { - get => _currentTime; - set - { - _currentTime = value; - OnCurrentTimeChanged(); - } + _profileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged; } public string FormattedCurrentTime @@ -45,10 +38,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties get { if (PixelsPerSecond > 200) - return $"{Math.Floor(CurrentTime.TotalSeconds):00}.{CurrentTime.Milliseconds:000}"; + return $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}"; if (PixelsPerSecond > 60) - return $"{Math.Floor(CurrentTime.TotalSeconds):00}.{CurrentTime.Milliseconds:000}"; - return $"{Math.Floor(CurrentTime.TotalMinutes):0}:{CurrentTime.Seconds:00}"; + return $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}"; + return $"{Math.Floor(_profileEditorService.CurrentTime.TotalMinutes):0}:{_profileEditorService.CurrentTime.Seconds:00}"; } } @@ -64,8 +57,8 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties public Thickness TimeCaretPosition { - get => new Thickness(CurrentTime.TotalSeconds * PixelsPerSecond, 0, 0, 0); - set => CurrentTime = TimeSpan.FromSeconds(value.Left / PixelsPerSecond); + get => new Thickness(_profileEditorService.CurrentTime.TotalSeconds * PixelsPerSecond, 0, 0, 0); + set => _profileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / PixelsPerSecond); } public PropertyTreeViewModel PropertyTree { get; set; } @@ -91,15 +84,21 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties } } + private void ProfileEditorServiceOnCurrentTimeChanged(object sender, EventArgs e) + { + NotifyOfPropertyChange(() => FormattedCurrentTime); + NotifyOfPropertyChange(() => TimeCaretPosition); + } + #region Caret movement private double _caretStartMouseStartOffset; private bool _mouseOverCaret; private int _pixelsPerSecond; - private TimeSpan _currentTime; public void RightGridMouseDown(object sender, MouseButtonEventArgs e) { + // TODO Preserve mouse offset _caretStartMouseStartOffset = e.GetPosition((IInputElement) sender).X - TimeCaretPosition.Left; } @@ -124,14 +123,8 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties #region Events - public event EventHandler CurrentTimeChanged; public event EventHandler PixelsPerSecondChanged; - protected virtual void OnCurrentTimeChanged() - { - CurrentTimeChanged?.Invoke(this, EventArgs.Empty); - } - protected virtual void OnPixelsPerSecondChanged() { PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty); diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs index 37eb42814..af5eab74f 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs @@ -3,6 +3,7 @@ using System.Linq; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput; +using Artemis.UI.Services.Interfaces; using Ninject; using Stylet; @@ -11,11 +12,14 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties public class LayerPropertyViewModel : PropertyChangedBase { private readonly IKernel _kernel; + private readonly IProfileEditorService _profileEditorService; private bool _keyframesEnabled; - public LayerPropertyViewModel(BaseLayerProperty layerProperty, LayerPropertyViewModel parent, ILayerPropertyViewModelFactory layerPropertyViewModelFactory, IKernel kernel) + public LayerPropertyViewModel(BaseLayerProperty layerProperty, LayerPropertyViewModel parent, ILayerPropertyViewModelFactory layerPropertyViewModelFactory, IKernel kernel, IProfileEditorService profileEditorService) { _kernel = kernel; + _profileEditorService = profileEditorService; + _keyframesEnabled = layerProperty.UntypedKeyframes.Any(); LayerProperty = layerProperty; Parent = parent; @@ -44,6 +48,16 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties private void UpdateKeyframes() { + // Either create a new first keyframe or clear all the keyframes + if (_keyframesEnabled) + LayerProperty.CreateNewKeyframe(_profileEditorService.CurrentTime); + else + LayerProperty.ClearKeyframes(); + + // Force the keyframe engine to update, the new keyframe is the current keyframe + LayerProperty.KeyframeEngine.Update(0); + + _profileEditorService.UpdateSelectedProfileElement(); } public PropertyInputViewModel GetPropertyInputViewModel() diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputView.xaml index 8183ab3ec..0b0918fbe 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputView.xaml @@ -15,6 +15,7 @@ Padding="0 -1" materialDesign:ValidationAssist.UsePopup="True" HorizontalAlignment="Left" + Text="{Binding FloatInputValue}" Cursor="/Resources/aero_drag_ew.cur" /> diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputViewModel.cs index f25212a72..452b31514 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/FloatPropertyInputViewModel.cs @@ -1,26 +1,39 @@ using System; using System.Collections.Generic; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.UI.Services.Interfaces; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput { public class FloatPropertyInputViewModel : PropertyInputViewModel { + public FloatPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService) + { + } + public sealed override List CompatibleTypes { get; } = new List {typeof(float)}; + public float FloatInputValue + { + get => (float) InputValue; + set => InputValue = value; + } + + public override void Update() + { + NotifyOfPropertyChange(() => FloatInputValue); + } + protected override void UpdateBaseValue(object value) { - throw new NotImplementedException(); + var layerProperty = (LayerProperty) LayerPropertyViewModel.LayerProperty; + layerProperty.Value = (float) value; } - protected override void UpdateKeyframeValue(BaseKeyframe keyframe, object value) + protected override void UpdateKeyframeValue(BaseKeyframe baseKeyframe, object value) { - throw new NotImplementedException(); - } - - protected override void CreateKeyframeForValue(object value) - { - throw new NotImplementedException(); + var keyframe = (Keyframe) baseKeyframe; + keyframe.Value = (float) value; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputView.xaml index 63a2b3ea9..b7d6bc8f1 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputView.xaml @@ -15,6 +15,7 @@ Padding="0 -1" materialDesign:ValidationAssist.UsePopup="True" HorizontalAlignment="Left" + Text="{Binding IntInputValue}" Cursor="/Resources/aero_drag_ew.cur" /> diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputViewModel.cs index 4effe5539..185703805 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/IntPropertyInputViewModel.cs @@ -1,26 +1,39 @@ using System; using System.Collections.Generic; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.UI.Services.Interfaces; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput { public class IntPropertyInputViewModel : PropertyInputViewModel { + public IntPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService) + { + } + public sealed override List CompatibleTypes { get; } = new List {typeof(int)}; + public int IntInputValue + { + get => (int) InputValue; + set => InputValue = value; + } + + public override void Update() + { + NotifyOfPropertyChange(() => IntInputValue); + } + protected override void UpdateBaseValue(object value) { - throw new NotImplementedException(); + var layerProperty = (LayerProperty) LayerPropertyViewModel.LayerProperty; + layerProperty.Value = (int) value; } - protected override void UpdateKeyframeValue(BaseKeyframe keyframe, object value) + protected override void UpdateKeyframeValue(BaseKeyframe baseKeyframe, object value) { - throw new NotImplementedException(); - } - - protected override void CreateKeyframeForValue(object value) - { - throw new NotImplementedException(); + var keyframe = (Keyframe) baseKeyframe; + keyframe.Value = (int) value; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/PropertyInputViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/PropertyInputViewModel.cs index 528c05458..16c1179ef 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/PropertyInputViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/PropertyInputViewModel.cs @@ -3,18 +3,26 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.UI.Exceptions; +using Artemis.UI.Services.Interfaces; using Stylet; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput { public abstract class PropertyInputViewModel : PropertyChangedBase { + protected PropertyInputViewModel(IProfileEditorService profileEditorService) + { + ProfileEditorService = profileEditorService; + } + + protected IProfileEditorService ProfileEditorService { get; set; } + public bool Initialized { get; private set; } public abstract List CompatibleTypes { get; } public LayerPropertyViewModel LayerPropertyViewModel { get; private set; } - public object InputValue + protected object InputValue { get => LayerPropertyViewModel.LayerProperty.KeyframeEngine.GetCurrentValue(); set => UpdateInputValue(value); @@ -33,11 +41,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.P Initialized = true; } - public void Update() - { - NotifyOfPropertyChange(() => InputValue); - } - private void UpdateInputValue(object value) { // If keyframes are disabled, update the base value @@ -47,13 +50,22 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.P return; } - // If on a keyframe, update the keyframe TODO: Make decisions.. - // var currentKeyframe = LayerPropertyViewModel.LayerProperty.UntypedKeyframes.FirstOrDefault(k => k.Position == LayerPropertyViewModel.) - // Otherwise, add a new keyframe at the current position + // If on a keyframe, update the keyframe + var currentKeyframe = LayerPropertyViewModel.LayerProperty.UntypedKeyframes.FirstOrDefault(k => k.Position == ProfileEditorService.CurrentTime); + // Create a new keyframe if none found + if (currentKeyframe == null) + currentKeyframe = LayerPropertyViewModel.LayerProperty.CreateNewKeyframe(ProfileEditorService.CurrentTime); + + UpdateKeyframeValue(currentKeyframe, value); + // Force the keyframe engine to update, the edited keyframe might affect the current keyframe progress + LayerPropertyViewModel.LayerProperty.KeyframeEngine.Update(0); + + ProfileEditorService.UpdateSelectedProfileElement(); + } + public abstract void Update(); protected abstract void UpdateBaseValue(object value); - protected abstract void UpdateKeyframeValue(BaseKeyframe keyframe, object value); - protected abstract void CreateKeyframeForValue(object value); + protected abstract void UpdateKeyframeValue(BaseKeyframe baseKeyframe, object value); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputView.xaml index 24ec22465..815b7c16e 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputView.xaml @@ -8,7 +8,7 @@ mc:Ignorable="d" d:DesignHeight="25" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:SKPointPropertyInputViewModel}"> - + + Text="{Binding X}" + Cursor="/Resources/aero_drag_ew.cur" KeyboardNavigation.IsTabStop="True" TabIndex="1" /> , + Text="{Binding Y}" + Cursor="/Resources/aero_drag_ew.cur" KeyboardNavigation.IsTabStop="True" TabIndex="2" /> \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputViewModel.cs index 0977a9bf7..7a0616b60 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKPointPropertyInputViewModel.cs @@ -1,27 +1,51 @@ using System; using System.Collections.Generic; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.UI.Services.Interfaces; +using PropertyChanged; using SkiaSharp; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput { public class SKPointPropertyInputViewModel : PropertyInputViewModel { + public SKPointPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService) + { + } + public sealed override List CompatibleTypes { get; } = new List {typeof(SKPoint)}; + // Since SKPoint is immutable we need to create properties that replace the SKPoint entirely + [DependsOn(nameof(InputValue))] + public float X + { + get => ((SKPoint) InputValue).X; + set => InputValue = new SKPoint(value, Y); + } + + [DependsOn(nameof(InputValue))] + public float Y + { + get => ((SKPoint) InputValue).Y; + set => InputValue = new SKPoint(X, value); + } + + public override void Update() + { + NotifyOfPropertyChange(() => X); + NotifyOfPropertyChange(() => Y); + } + protected override void UpdateBaseValue(object value) { - throw new NotImplementedException(); + var layerProperty = (LayerProperty) LayerPropertyViewModel.LayerProperty; + layerProperty.Value = (SKPoint) value; } - protected override void UpdateKeyframeValue(BaseKeyframe keyframe, object value) + protected override void UpdateKeyframeValue(BaseKeyframe baseKeyframe, object value) { - throw new NotImplementedException(); - } - - protected override void CreateKeyframeForValue(object value) - { - throw new NotImplementedException(); + var keyframe = (Keyframe) baseKeyframe; + keyframe.Value = (SKPoint) value; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputView.xaml index db8eae794..94fc68a93 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputView.xaml @@ -16,6 +16,7 @@ materialDesign:ValidationAssist.UsePopup="True" HorizontalAlignment="Left" ToolTip="Height" + Text="{Binding Height}" Cursor="/Resources/aero_drag_ew.cur" /> , diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputViewModel.cs index 3b35a67b3..593214f35 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyInput/SKSizePropertyInputViewModel.cs @@ -1,27 +1,51 @@ using System; using System.Collections.Generic; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.UI.Services.Interfaces; +using PropertyChanged; using SkiaSharp; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput { public class SKSizePropertyInputViewModel : PropertyInputViewModel { + public SKSizePropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService) + { + } + public sealed override List CompatibleTypes { get; } = new List {typeof(SKSize)}; + // Since SKSize is immutable we need to create properties that replace the SKPoint entirely + [DependsOn(nameof(InputValue))] + public float Width + { + get => ((SKSize) InputValue).Width; + set => InputValue = new SKSize(value, Height); + } + + [DependsOn(nameof(InputValue))] + public float Height + { + get => ((SKSize) InputValue).Height; + set => InputValue = new SKSize(Width, value); + } + + public override void Update() + { + NotifyOfPropertyChange(() => Width); + NotifyOfPropertyChange(() => Height); + } + protected override void UpdateBaseValue(object value) { - throw new NotImplementedException(); + var layerProperty = (LayerProperty) LayerPropertyViewModel.LayerProperty; + layerProperty.Value = (SKSize) value; } - protected override void UpdateKeyframeValue(BaseKeyframe keyframe, object value) + protected override void UpdateKeyframeValue(BaseKeyframe baseKeyframe, object value) { - throw new NotImplementedException(); - } - - protected override void CreateKeyframeForValue(object value) - { - throw new NotImplementedException(); + var keyframe = (Keyframe) baseKeyframe; + keyframe.Value = (SKSize) value; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildView.xaml index 69fba2633..ae91ca1d9 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildView.xaml @@ -22,7 +22,7 @@ ToolTip="Toggle key-framing" Width="18" Height="18" - IsChecked="{Binding KeyframesEnabled}" + IsChecked="{Binding LayerPropertyViewModel.KeyframesEnabled}" VerticalAlignment="Center" Padding="-25"> diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildViewModel.cs index 2751bda99..c0a8cf009 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeChildViewModel.cs @@ -12,5 +12,17 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree public LayerPropertyViewModel LayerPropertyViewModel { get; } public PropertyInputViewModel PropertyInputViewModel { get; set; } + + public override void Update(bool forceUpdate) + { + if (forceUpdate) + PropertyInputViewModel.Update(); + else + { + // Only update if visible and if keyframes are enabled + if (LayerPropertyViewModel.Parent.IsExpanded && LayerPropertyViewModel.KeyframesEnabled) + PropertyInputViewModel.Update(); + } + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeItemViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeItemViewModel.cs index 1837a36d1..0cafdd660 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeItemViewModel.cs @@ -2,7 +2,12 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree { - public class PropertyTreeItemViewModel : PropertyChangedBase + public abstract class PropertyTreeItemViewModel : PropertyChangedBase { + /// + /// Updates the tree item's input if it is visible and has keyframes enabled + /// + /// Force update regardless of visibility and keyframes + public abstract void Update(bool forceUpdate); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeParentViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeParentViewModel.cs index 3ef565ab9..91e357e9c 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeParentViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeParentViewModel.cs @@ -21,5 +21,11 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree public LayerPropertyViewModel LayerPropertyViewModel { get; } public BindableCollection Children { get; set; } + + public override void Update(bool forceUpdate) + { + foreach (var child in Children) + child.Update(forceUpdate); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeViewModel.cs index 1d8ed7f48..12a200271 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/PropertyTree/PropertyTreeViewModel.cs @@ -1,15 +1,24 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Artemis.UI.Services.Interfaces; using Stylet; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree { public class PropertyTreeViewModel : PropertyChangedBase { - public PropertyTreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel) + private readonly IProfileEditorService _profileEditorService; + + public PropertyTreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IProfileEditorService profileEditorService) { + _profileEditorService = profileEditorService; + LayerPropertiesViewModel = layerPropertiesViewModel; PropertyTreeItemViewModels = new BindableCollection(); + + _profileEditorService.CurrentTimeChanged += (sender, args) => Update(false); + _profileEditorService.SelectedProfileElementUpdated += (sender, args) => Update(true); } public LayerPropertiesViewModel LayerPropertiesViewModel { get; } @@ -31,5 +40,15 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree { PropertyTreeItemViewModels.Clear(); } + + /// + /// Updates the tree item's input if it is visible and has keyframes enabled + /// + /// Force update regardless of visibility and keyframes + public void Update(bool forceUpdate) + { + foreach (var viewModel in PropertyTreeItemViewModels) + viewModel.Update(forceUpdate); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineView.xaml index e3e57b324..7c92513b7 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineView.xaml @@ -16,7 +16,7 @@ HorizontalAlignment="Left"> - + diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs index 63b4cdc1f..08a208eab 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs @@ -1,16 +1,24 @@ using System; using System.Collections.Generic; using System.Linq; +using Artemis.UI.Services.Interfaces; using Stylet; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline { public class PropertyTimelineViewModel : PropertyChangedBase { - public PropertyTimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel) + private readonly IProfileEditorService _profileEditorService; + + public PropertyTimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IProfileEditorService profileEditorService) { + _profileEditorService = profileEditorService; + LayerPropertiesViewModel = layerPropertiesViewModel; PropertyTrackViewModels = new BindableCollection(); + + _profileEditorService.SelectedProfileElementUpdated += (sender, args) => Update(); + LayerPropertiesViewModel.PixelsPerSecondChanged += (sender, args) => UpdateKeyframePositions(); } public LayerPropertiesViewModel LayerPropertiesViewModel { get; } @@ -27,8 +35,8 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline Width = endTime.TotalSeconds * LayerPropertiesViewModel.PixelsPerSecond; // Ensure the caret isn't outside the end time - if (LayerPropertiesViewModel.CurrentTime > endTime) - LayerPropertiesViewModel.CurrentTime = endTime; + if (_profileEditorService.CurrentTime > endTime) + _profileEditorService.CurrentTime = endTime; } public void PopulateProperties(List properties) @@ -51,5 +59,22 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline { PropertyTrackViewModels.Clear(); } + + public void UpdateKeyframePositions() + { + foreach (var viewModel in PropertyTrackViewModels) + viewModel.UpdateKeyframes(LayerPropertiesViewModel.PixelsPerSecond); + } + + /// + /// Updates the time line's keyframes + /// + public void Update() + { + foreach (var viewModel in PropertyTrackViewModels) + viewModel.PopulateKeyframes(); + + UpdateEndTime(); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs index b998f1512..2d1662077 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs @@ -12,7 +12,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline KeyframeViewModels = new BindableCollection(); PopulateKeyframes(); - UpdateKeyframes(propertyTimelineViewModel.LayerPropertiesViewModel.PixelsPerSecond); + UpdateKeyframes(PropertyTimelineViewModel.LayerPropertiesViewModel.PixelsPerSecond); } public PropertyTimelineViewModel PropertyTimelineViewModel { get; } @@ -21,12 +21,22 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline public void PopulateKeyframes() { + // Remove old keyframes + foreach (var viewModel in KeyframeViewModels.ToList()) + { + if (!LayerPropertyViewModel.LayerProperty.UntypedKeyframes.Contains(viewModel.Keyframe)) + KeyframeViewModels.Remove(viewModel); + } + + // Add new keyframes foreach (var keyframe in LayerPropertyViewModel.LayerProperty.UntypedKeyframes) { if (KeyframeViewModels.Any(k => k.Keyframe == keyframe)) continue; KeyframeViewModels.Add(new PropertyTrackKeyframeViewModel(keyframe)); } + + UpdateKeyframes(PropertyTimelineViewModel.LayerPropertiesViewModel.PixelsPerSecond); } public void UpdateKeyframes(int pixelsPerSecond) diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml index 961392118..eda1c8394 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml @@ -42,7 +42,7 @@ - + diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerViewModel.cs index 00bc0dfa4..72ecde229 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerViewModel.cs @@ -24,8 +24,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization Update(); Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated; _profileEditorService.SelectedProfileElementChanged += OnSelectedProfileElementChanged; + _profileEditorService.SelectedProfileElementUpdated += OnSelectedProfileElementUpdated; + _profileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged; } - + public Layer Layer { get; } public Geometry LayerGeometry { get; set; } @@ -119,8 +121,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization shapeGeometry.Freeze(); ShapeGeometry = shapeGeometry; } - - + private void CreateViewportRectangle() { if (!Layer.Leds.Any() || Layer.LayerShape == null) @@ -190,6 +191,20 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization IsSelected = _profileEditorService.SelectedProfileElement == Layer; } + private void OnSelectedProfileElementUpdated(object sender, EventArgs e) + { + if (IsSelected) + Update(); + } + + private void ProfileEditorServiceOnCurrentTimeChanged(object sender, EventArgs e) + { + if (!IsSelected) + return; + CreateShapeGeometry(); + CreateViewportRectangle(); + } + public void Dispose() { Layer.RenderPropertiesUpdated -= LayerOnRenderPropertiesUpdated; diff --git a/src/Artemis.UI/Screens/RootView.xaml b/src/Artemis.UI/Screens/RootView.xaml index 4675f580b..47f1e15d8 100644 --- a/src/Artemis.UI/Screens/RootView.xaml +++ b/src/Artemis.UI/Screens/RootView.xaml @@ -57,7 +57,7 @@ - + diff --git a/src/Artemis.UI/Services/Interfaces/IProfileEditorService.cs b/src/Artemis.UI/Services/Interfaces/IProfileEditorService.cs index 88b344969..90837eddd 100644 --- a/src/Artemis.UI/Services/Interfaces/IProfileEditorService.cs +++ b/src/Artemis.UI/Services/Interfaces/IProfileEditorService.cs @@ -7,15 +7,36 @@ namespace Artemis.UI.Services.Interfaces { Profile SelectedProfile { get; } ProfileElement SelectedProfileElement { get; } + TimeSpan CurrentTime { get; set; } void ChangeSelectedProfile(Profile profile); void UpdateSelectedProfile(); void ChangeSelectedProfileElement(ProfileElement profileElement); void UpdateSelectedProfileElement(); + /// + /// Occurs when a new profile is selected + /// event EventHandler SelectedProfileChanged; + + /// + /// Occurs then the currently selected profile is updated + /// event EventHandler SelectedProfileUpdated; + + /// + /// Occurs when a new profile element is selected + /// event EventHandler SelectedProfileElementChanged; + + /// + /// Occurs when the currently selected profile element is updated + /// event EventHandler SelectedProfileElementUpdated; + + /// + /// Occurs when the current editor time is changed + /// + event EventHandler CurrentTimeChanged; } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/ProfileEditorService.cs b/src/Artemis.UI/Services/ProfileEditorService.cs index dbf5faf76..f9c60dc85 100644 --- a/src/Artemis.UI/Services/ProfileEditorService.cs +++ b/src/Artemis.UI/Services/ProfileEditorService.cs @@ -8,6 +8,8 @@ namespace Artemis.UI.Services public class ProfileEditorService : IProfileEditorService { private readonly IProfileService _profileService; + private TimeSpan _currentTime; + private TimeSpan _lastUpdateTime; public ProfileEditorService(IProfileService profileService) { @@ -17,6 +19,19 @@ namespace Artemis.UI.Services public Profile SelectedProfile { get; private set; } public ProfileElement SelectedProfileElement { get; private set; } + public TimeSpan CurrentTime + { + get => _currentTime; + set + { + if (_currentTime.Equals(value)) + return; + _currentTime = value; + UpdateProfilePreview(); + OnCurrentTimeChanged(); + } + } + public void ChangeSelectedProfile(Profile profile) { SelectedProfile = profile; @@ -41,10 +56,30 @@ namespace Artemis.UI.Services OnSelectedProfileElementUpdated(); } + + private void UpdateProfilePreview() + { + var delta = CurrentTime - _lastUpdateTime; + foreach (var layer in SelectedProfile.GetAllLayers()) + { + // Override keyframe progress + foreach (var baseLayerProperty in layer.Properties) + baseLayerProperty.KeyframeEngine?.OverrideProgress(CurrentTime); + + // Force layer shape to redraw + layer.LayerShape?.CalculateRenderProperties(layer.PositionProperty.GetCurrentValue(), layer.SizeProperty.GetCurrentValue()); + // Update the brush with the delta (which can now be negative ^^) + layer.Update(delta.TotalSeconds); + } + + _lastUpdateTime = CurrentTime; + } + public event EventHandler SelectedProfileChanged; public event EventHandler SelectedProfileUpdated; public event EventHandler SelectedProfileElementChanged; public event EventHandler SelectedProfileElementUpdated; + public event EventHandler CurrentTimeChanged; protected virtual void OnSelectedProfileElementUpdated() { @@ -65,5 +100,10 @@ namespace Artemis.UI.Services { SelectedProfileChanged?.Invoke(this, EventArgs.Empty); } + + protected virtual void OnCurrentTimeChanged() + { + CurrentTimeChanged?.Invoke(this, EventArgs.Empty); + } } } \ No newline at end of file