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

Profile editor - Implemented keyframe easing selection

This commit is contained in:
Robert 2022-01-21 00:42:06 +01:00
parent 6f269af8d4
commit a1f7f6dff8
11 changed files with 237 additions and 61 deletions

View File

@ -14,12 +14,12 @@ namespace Artemis.Core
ILayerProperty UntypedLayerProperty { get; }
/// <summary>
/// The position of this keyframe in the timeline
/// Gets or sets the position of this keyframe in the timeline
/// </summary>
TimeSpan Position { get; set; }
/// <summary>
/// The easing function applied on the value of the keyframe
/// Gets or sets the easing function applied on the value of the keyframe
/// </summary>
Easings.Functions EasingFunction { get; set; }

View File

@ -0,0 +1,43 @@
using Artemis.Core;
using Humanizer;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to change the easing function of a keyframe.
/// </summary>
public class ChangeKeyframeEasing : IProfileEditorCommand
{
private readonly ILayerPropertyKeyframe _keyframe;
private readonly Easings.Functions _easingFunction;
private readonly Easings.Functions _originalEasingFunction;
/// <summary>
/// Creates a new instance of the <see cref="ChangeKeyframeEasing"/> class.
/// </summary>
public ChangeKeyframeEasing(ILayerPropertyKeyframe keyframe, Easings.Functions easingFunction)
{
_keyframe = keyframe;
_easingFunction = easingFunction;
_originalEasingFunction = keyframe.EasingFunction;
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName => "Change easing to " + _easingFunction.Humanize(LetterCasing.LowerCase);
/// <inheritdoc />
public void Execute()
{
_keyframe.EasingFunction = _easingFunction;
}
/// <inheritdoc />
public void Undo()
{
_keyframe.EasingFunction = _originalEasingFunction;
}
#endregion
}

View File

@ -0,0 +1,43 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to change the position of a keyframe.
/// </summary>
public class MoveKeyframe : IProfileEditorCommand
{
private readonly ILayerPropertyKeyframe _keyframe;
private readonly TimeSpan _originalPosition;
private readonly TimeSpan _position;
/// <summary>
/// Creates a new instance of the <see cref="MoveKeyframe" /> class.
/// </summary>
public MoveKeyframe(ILayerPropertyKeyframe keyframe, TimeSpan position)
{
_keyframe = keyframe;
_position = position;
_originalPosition = keyframe.Position;
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName => "Move keyframe";
/// <inheritdoc />
public void Execute()
{
_keyframe.Position = _position;
}
/// <inheritdoc />
public void Undo()
{
_keyframe.Position = _originalPosition;
}
#endregion
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Artemis.UI.Shared.Services.ProfileEditor;
/// <summary>
/// Represents a profile editor command that can be used to combine multiple commands into one.
/// </summary>
public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable
{
private readonly List<IProfileEditorCommand> _commands;
/// <summary>
/// Creates a new instance of the <see cref="CompositeProfileEditorCommand" /> class.
/// </summary>
/// <param name="commands">The commands to execute.</param>
/// <param name="displayName">The display name of the composite command.</param>
public CompositeProfileEditorCommand(IEnumerable<IProfileEditorCommand> commands, string displayName)
{
if (commands == null)
throw new ArgumentNullException(nameof(commands));
_commands = commands.ToList();
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
}
/// <inheritdoc />
public void Dispose()
{
foreach (IProfileEditorCommand profileEditorCommand in _commands)
if (profileEditorCommand is IDisposable disposable)
disposable.Dispose();
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public void Execute()
{
foreach (IProfileEditorCommand profileEditorCommand in _commands)
profileEditorCommand.Execute();
}
/// <inheritdoc />
public void Undo()
{
// Undo in reverse by iterating from the back
for (int index = _commands.Count; index >= 0; index--)
_commands[index].Undo();
}
#endregion
}

View File

@ -25,7 +25,6 @@ public interface ITimelineKeyframeViewModel
#region Context menu actions
void PopulateEasingViewModels();
void ClearEasingViewModels();
void Delete(bool save = true);
#endregion

View File

@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineEasingView">
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
StrokeThickness="1"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline
{
public partial class TimelineEasingView : UserControl
{
public TimelineEasingView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Artemis.Core;
using Artemis.UI.Shared;
using Avalonia;
@ -9,11 +8,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineEasingViewModel : ViewModelBase
{
private bool _isEasingModeSelected;
private readonly ILayerPropertyKeyframe _keyframe;
public TimelineEasingViewModel(Easings.Functions easingFunction, bool isSelected)
public TimelineEasingViewModel(Easings.Functions easingFunction, ILayerPropertyKeyframe keyframe)
{
_isEasingModeSelected = isSelected;
_keyframe = keyframe;
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
@ -30,21 +29,5 @@ public class TimelineEasingViewModel : ViewModelBase
public Easings.Functions EasingFunction { get; }
public List<Point> EasingPoints { get; }
public string Description { get; }
public bool IsEasingModeSelected
{
get => _isEasingModeSelected;
set
{
_isEasingModeSelected = value;
if (value) OnEasingModeSelected();
}
}
public event EventHandler EasingModeSelected;
protected virtual void OnEasingModeSelected()
{
EasingModeSelected?.Invoke(this, EventArgs.Empty);
}
public bool IsEasingModeSelected => _keyframe.EasingFunction == EasingFunction;
}

View File

@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineKeyframeView"
ClipToBounds="False"
@ -31,25 +32,24 @@
</Transitions>
</Ellipse.Transitions>
<Ellipse.ContextFlyout>
<MenuFlyout>
<MenuFlyout Opening="FlyoutBase_OnOpening">
<MenuItem Header="Easing" Items="{Binding EasingViewModels}">
<MenuItem.Styles>
<Style Selector="MenuItem > MenuItem">
<Setter Property="Icon">
<Setter.Value>
<Template>
<CheckBox IsHitTestVisible="False" IsChecked="{Binding IsEasingModeSelected}" />
</Template>
</Setter.Value>
</Setter>
<Setter Property="Command" Value="{Binding $parent[UserControl].DataContext.SelectEasingFunction}"></Setter>
<Setter Property="CommandParameter" Value="{Binding EasingFunction}"></Setter>
</Style>
</MenuItem.Styles>
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Creation" />
</MenuItem.Icon>
<MenuItem.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource MaterialDesignBody}"
StrokeThickness="1"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
<Separator />
<MenuItem Header="Duplicate" Command="{Binding DuplicateKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+D">

View File

@ -1,3 +1,4 @@
using System;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
@ -43,4 +44,9 @@ public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewMod
if (!_moved)
ViewModel?.Select(e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
private void FlyoutBase_OnOpening(object? sender, EventArgs e)
{
ViewModel?.PopulateEasingViewModels();
}
}

View File

@ -6,7 +6,9 @@ using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using DynamicData;
using Humanizer;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
@ -32,11 +34,6 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
{
_pixelsPerSecond = p;
profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
System.Reactive.Disposables.Disposable.Create(() =>
{
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected;
}).DisposeWith(d);
}).DisposeWith(d);
_isSelected = profileEditorService.ConnectToKeyframes().ToCollection().Select(keyframes => keyframes.Contains(LayerPropertyKeyframe)).ToProperty(this, vm => vm.IsSelected).DisposeWith(d);
@ -142,31 +139,45 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions))
.Cast<Easings.Functions>()
.Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction)));
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected;
.Select(e => new TimelineEasingViewModel(e, Keyframe)));
}
public void ClearEasingViewModels()
public void SelectEasingFunction(Easings.Functions easingFunction)
{
EasingViewModels.Clear();
}
private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e)
{
if (sender is TimelineEasingViewModel timelineEasingViewModel)
SelectEasingMode(timelineEasingViewModel);
}
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
throw new NotImplementedException();
LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
_profileEditorService.ExecuteCommand(new ChangeKeyframeEasing(Keyframe, easingFunction));
}
#endregion
}
public class ChangeKeyframeEasing : IProfileEditorCommand
{
private readonly ILayerPropertyKeyframe _keyframe;
private readonly Easings.Functions _easingFunction;
private readonly Easings.Functions _originalEasingFunction;
public ChangeKeyframeEasing(ILayerPropertyKeyframe keyframe, Easings.Functions easingFunction)
{
_keyframe = keyframe;
_easingFunction = easingFunction;
_originalEasingFunction = keyframe.EasingFunction;
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName => "Change easing to " + _easingFunction.Humanize(LetterCasing.LowerCase);
/// <inheritdoc />
public void Execute()
{
_keyframe.EasingFunction = _easingFunction;
}
/// <inheritdoc />
public void Undo()
{
_keyframe.EasingFunction = _originalEasingFunction;
}
#endregion