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

Timeline - Implemented segments

This commit is contained in:
Robert 2022-01-25 00:23:26 +01:00
parent 913117ad0a
commit 642823add5
35 changed files with 1569 additions and 579 deletions

View File

@ -18,7 +18,7 @@ namespace Artemis.Core
/// Gets the description attribute applied to this property
/// </summary>
PropertyDescriptionAttribute PropertyDescription { get; }
/// <summary>
/// Gets the profile element (such as layer or folder) this property is applied to
/// </summary>
@ -68,7 +68,7 @@ namespace Artemis.Core
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary>
bool IsLoadedFromStorage { get; }
/// <summary>
/// Initializes the layer property
/// <para>
@ -102,6 +102,20 @@ namespace Artemis.Core
/// </summary>
void UpdateDataBinding();
/// <summary>
/// Removes a keyframe from the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Adds a keyframe to the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Occurs when the layer property is disposed
/// </summary>

View File

@ -96,6 +96,24 @@ namespace Artemis.Core
OnUpdated();
}
/// <inheritdoc />
public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe)
{
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}.");
RemoveKeyframe(typedKeyframe);
}
/// <inheritdoc />
public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe)
{
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}.");
AddKeyframe(typedKeyframe);
}
/// <inheritdoc />
public void Dispose()
{
@ -177,22 +195,25 @@ namespace Artemis.Core
/// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new
/// or existing keyframe.
/// </param>
public void SetCurrentValue(T value, TimeSpan? time)
/// <returns>The new keyframe if one was created.</returns>
public LayerPropertyKeyframe<T>? SetCurrentValue(T value, TimeSpan? time)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
LayerPropertyKeyframe<T>? newKeyframe = null;
if (time == null || !KeyframesEnabled || !KeyframesSupported)
{
BaseValue = value;
}
else
{
// If on a keyframe, update the keyframe
LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
// Create a new keyframe if none found
if (currentKeyframe == null)
AddKeyframe(new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this));
{
newKeyframe = new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this);
AddKeyframe(newKeyframe);
}
else
currentKeyframe.Value = value;
}
@ -200,6 +221,7 @@ namespace Artemis.Core
// 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
ReapplyUpdate();
return newKeyframe;
}
/// <inheritdoc />
@ -247,6 +269,7 @@ namespace Artemis.Core
{
if (_keyframesEnabled == value) return;
_keyframesEnabled = value;
ReapplyUpdate();
OnKeyframesToggled();
OnPropertyChanged(nameof(KeyframesEnabled));
}

View File

@ -2,517 +2,529 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a timeline used by profile elements
/// </summary>
public class Timeline : CorePropertyChanged, IStorageModel
{
private const int MaxExtraTimelines = 15;
private readonly object _lock = new();
/// <summary>
/// Represents a timeline used by profile elements
/// Creates a new instance of the <see cref="Timeline" /> class
/// </summary>
public class Timeline : CorePropertyChanged, IStorageModel
public Timeline()
{
private const int MaxExtraTimelines = 15;
private readonly object _lock = new();
Entity = new TimelineEntity();
MainSegmentLength = TimeSpan.FromSeconds(5);
/// <summary>
/// Creates a new instance of the <see cref="Timeline" /> class
/// </summary>
public Timeline()
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
Save();
}
internal Timeline(TimelineEntity entity)
{
Entity = entity;
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
Load();
}
private Timeline(Timeline parent)
{
Entity = new TimelineEntity();
Parent = parent;
StartSegmentLength = Parent.StartSegmentLength;
MainSegmentLength = Parent.MainSegmentLength;
EndSegmentLength = Parent.EndSegmentLength;
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
}
/// <inheritdoc />
public override string ToString()
{
return $"Progress: {Position}/{Length} - delta: {Delta}";
}
#region Extra timelines
/// <summary>
/// Adds an extra timeline to this timeline
/// </summary>
public void AddExtraTimeline()
{
_extraTimelines.Add(new Timeline(this));
if (_extraTimelines.Count > MaxExtraTimelines)
_extraTimelines.RemoveAt(0);
}
/// <summary>
/// Removes all extra timelines from this timeline
/// </summary>
public void ClearExtraTimelines()
{
_extraTimelines.Clear();
}
#endregion
#region Properties
private TimeSpan _position;
private TimeSpan _lastDelta;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private readonly List<Timeline> _extraTimelines;
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
/// <summary>
/// Gets the parent this timeline is an extra timeline of
/// </summary>
public Timeline? Parent { get; }
/// <summary>
/// Gets the current position of the timeline
/// </summary>
public TimeSpan Position
{
get => _position;
private set => SetAndNotify(ref _position, value);
}
/// <summary>
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to
/// <see cref="ClearDelta" />
/// <para>
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
/// </para>
/// </summary>
public TimeSpan Delta
{
get => Parent == null ? _lastDelta : DeltaToParent;
private set => SetAndNotify(ref _lastDelta, value);
}
/// <summary>
/// Gets the delta to this timeline's <see cref="Parent" />
/// </summary>
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public TimelinePlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public TimelineStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <summary>
/// Gets a list of extra copies of the timeline applied to this timeline
/// </summary>
public ReadOnlyCollection<Timeline> ExtraTimelines { get; }
/// <summary>
/// Gets a boolean indicating whether the timeline has finished its run
/// </summary>
public bool IsFinished => Position > Length && !ExtraTimelines.Any();
/// <summary>
/// Gets a boolean indicating whether the timeline progress has been overridden
/// </summary>
public bool IsOverridden { get; private set; }
#region Segments
/// <summary>
/// Gets the total length of this timeline
/// </summary>
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
/// <summary>
/// Gets or sets the length of the start segment
/// </summary>
public TimeSpan StartSegmentLength
{
get => _startSegmentLength;
set
{
Entity = new TimelineEntity();
MainSegmentLength = TimeSpan.FromSeconds(5);
if (SetAndNotify(ref _startSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
/// <summary>
/// Gets or sets the length of the main segment
/// </summary>
public TimeSpan MainSegmentLength
{
get => _mainSegmentLength;
set
{
if (SetAndNotify(ref _mainSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
Save();
/// <summary>
/// Gets or sets the length of the end segment
/// </summary>
public TimeSpan EndSegmentLength
{
get => _endSegmentLength;
set
{
if (SetAndNotify(ref _endSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
/// <summary>
/// Gets or sets the start position of the main segment
/// </summary>
public TimeSpan MainSegmentStartPosition
{
get => StartSegmentEndPosition;
set
{
StartSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.Main, true);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentStartPosition
{
get => MainSegmentEndPosition;
set
{
MainSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.End, true);
}
}
/// <summary>
/// Gets or sets the end position of the start segment
/// </summary>
public TimeSpan StartSegmentEndPosition
{
get => StartSegmentLength;
set
{
StartSegmentLength = value;
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the end position of the main segment
/// </summary>
public TimeSpan MainSegmentEndPosition
{
get => StartSegmentEndPosition + MainSegmentLength;
set
{
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentEndPosition
{
get => MainSegmentEndPosition + EndSegmentLength;
set
{
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
internal TimelineEntity Entity { get; set; }
/// <summary>
/// Notifies the right segments in a way that I don't have to think about it
/// </summary>
/// <param name="segment">The segment that was updated</param>
/// <param name="startUpdated">Whether the start point of the <paramref name="segment" /> was updated</param>
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
{
if (segment <= TimelineSegment.End)
{
if (startUpdated || segment < TimelineSegment.End)
OnPropertyChanged(nameof(EndSegmentStartPosition));
OnPropertyChanged(nameof(EndSegmentEndPosition));
}
internal Timeline(TimelineEntity entity)
if (segment <= TimelineSegment.Main)
{
Entity = entity;
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
Load();
if (startUpdated || segment < TimelineSegment.Main)
OnPropertyChanged(nameof(MainSegmentStartPosition));
OnPropertyChanged(nameof(MainSegmentEndPosition));
}
private Timeline(Timeline parent)
{
Entity = new TimelineEntity();
Parent = parent;
StartSegmentLength = Parent.StartSegmentLength;
MainSegmentLength = Parent.MainSegmentLength;
EndSegmentLength = Parent.EndSegmentLength;
if (segment <= TimelineSegment.Start)
OnPropertyChanged(nameof(StartSegmentEndPosition));
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
OnPropertyChanged(nameof(Length));
OnTimelineChanged();
}
/// <summary>
/// Occurs when changes have been made to any of the segments of the timeline.
/// </summary>
public event EventHandler? TimelineChanged;
private void OnTimelineChanged()
{
TimelineChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
#endregion
#region Updating
private TimeSpan _lastOverridePosition;
/// <summary>
/// Updates the timeline, applying the provided <paramref name="delta" /> to the <see cref="Position" />
/// </summary>
/// <param name="delta">The amount of time to apply to the position</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Update(TimeSpan delta, bool stickToMainSegment)
{
lock (_lock)
{
Delta += delta;
Position += delta;
IsOverridden = false;
_lastOverridePosition = Position;
if (stickToMainSegment && Position > MainSegmentEndPosition)
{
// If the main segment has no length, simply stick to the start of the segment
if (MainSegmentLength == TimeSpan.Zero)
Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
}
_extraTimelines.RemoveAll(t => t.IsFinished);
foreach (Timeline extraTimeline in _extraTimelines)
extraTimeline.Update(delta, false);
}
}
/// <inheritdoc />
public override string ToString()
/// <summary>
/// Moves the position of the timeline backwards to the very start of the timeline
/// </summary>
public void JumpToStart()
{
lock (_lock)
{
return $"Progress: {Position}/{Length} - delta: {Delta}";
if (Position == TimeSpan.Zero)
return;
Delta = TimeSpan.Zero - Position;
Position = TimeSpan.Zero;
}
}
#region Extra timelines
/// <summary>
/// Adds an extra timeline to this timeline
/// </summary>
public void AddExtraTimeline()
/// <summary>
/// Moves the position of the timeline forwards to the beginning of the end segment
/// </summary>
public void JumpToEndSegment()
{
lock (_lock)
{
_extraTimelines.Add(new Timeline(this));
if (_extraTimelines.Count > MaxExtraTimelines)
_extraTimelines.RemoveAt(0);
if (Position >= EndSegmentStartPosition)
return;
Delta = EndSegmentStartPosition - Position;
Position = EndSegmentStartPosition;
}
}
/// <summary>
/// Removes all extra timelines from this timeline
/// </summary>
public void ClearExtraTimelines()
/// <summary>
/// Moves the position of the timeline forwards to the very end of the timeline
/// </summary>
public void JumpToEnd()
{
lock (_lock)
{
if (Position >= EndSegmentEndPosition)
return;
Delta = EndSegmentEndPosition - Position;
Position = EndSegmentEndPosition;
}
}
/// <summary>
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines
/// </summary>
/// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Override(TimeSpan position, bool stickToMainSegment)
{
lock (_lock)
{
Delta += position - _lastOverridePosition;
Position = position;
IsOverridden = true;
_lastOverridePosition = position;
if (stickToMainSegment && Position >= MainSegmentStartPosition)
{
bool atSegmentStart = Position == MainSegmentStartPosition;
if (MainSegmentLength > TimeSpan.Zero)
{
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
// is actually at the start of the segment
if (Position == MainSegmentStartPosition && !atSegmentStart)
Position = MainSegmentEndPosition;
}
else
{
Position = MainSegmentStartPosition;
}
}
_extraTimelines.Clear();
}
#endregion
#region Properties
private TimeSpan _position;
private TimeSpan _lastDelta;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private readonly List<Timeline> _extraTimelines;
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
/// <summary>
/// Gets the parent this timeline is an extra timeline of
/// </summary>
public Timeline? Parent { get; }
/// <summary>
/// Gets the current position of the timeline
/// </summary>
public TimeSpan Position
{
get => _position;
private set => SetAndNotify(ref _position, value);
}
/// <summary>
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to
/// <see cref="ClearDelta" />
/// <para>
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
/// </para>
/// </summary>
public TimeSpan Delta
{
get => Parent == null ? _lastDelta : DeltaToParent;
private set => SetAndNotify(ref _lastDelta, value);
}
/// <summary>
/// Gets the delta to this timeline's <see cref="Parent" />
/// </summary>
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public TimelinePlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public TimelineStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <summary>
/// Gets a list of extra copies of the timeline applied to this timeline
/// </summary>
public ReadOnlyCollection<Timeline> ExtraTimelines { get; }
/// <summary>
/// Gets a boolean indicating whether the timeline has finished its run
/// </summary>
public bool IsFinished => Position > Length && !ExtraTimelines.Any();
/// <summary>
/// Gets a boolean indicating whether the timeline progress has been overridden
/// </summary>
public bool IsOverridden { get; private set; }
#region Segments
/// <summary>
/// Gets the total length of this timeline
/// </summary>
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
/// <summary>
/// Gets or sets the length of the start segment
/// </summary>
public TimeSpan StartSegmentLength
{
get => _startSegmentLength;
set
{
if (SetAndNotify(ref _startSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the length of the main segment
/// </summary>
public TimeSpan MainSegmentLength
{
get => _mainSegmentLength;
set
{
if (SetAndNotify(ref _mainSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the length of the end segment
/// </summary>
public TimeSpan EndSegmentLength
{
get => _endSegmentLength;
set
{
if (SetAndNotify(ref _endSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
/// <summary>
/// Gets or sets the start position of the main segment
/// </summary>
public TimeSpan MainSegmentStartPosition
{
get => StartSegmentEndPosition;
set
{
StartSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.Main, true);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentStartPosition
{
get => MainSegmentEndPosition;
set
{
MainSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.End, true);
}
}
/// <summary>
/// Gets or sets the end position of the start segment
/// </summary>
public TimeSpan StartSegmentEndPosition
{
get => StartSegmentLength;
set
{
StartSegmentLength = value;
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the end position of the main segment
/// </summary>
public TimeSpan MainSegmentEndPosition
{
get => StartSegmentEndPosition + MainSegmentLength;
set
{
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentEndPosition
{
get => MainSegmentEndPosition + EndSegmentLength;
set
{
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
internal TimelineEntity Entity { get; set; }
/// <summary>
/// Notifies the right segments in a way that I don't have to think about it
/// </summary>
/// <param name="segment">The segment that was updated</param>
/// <param name="startUpdated">Whether the start point of the <paramref name="segment" /> was updated</param>
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
{
if (segment <= TimelineSegment.End)
{
if (startUpdated || segment < TimelineSegment.End)
OnPropertyChanged(nameof(EndSegmentStartPosition));
OnPropertyChanged(nameof(EndSegmentEndPosition));
}
if (segment <= TimelineSegment.Main)
{
if (startUpdated || segment < TimelineSegment.Main)
OnPropertyChanged(nameof(MainSegmentStartPosition));
OnPropertyChanged(nameof(MainSegmentEndPosition));
}
if (segment <= TimelineSegment.Start) OnPropertyChanged(nameof(StartSegmentEndPosition));
OnPropertyChanged(nameof(Length));
}
#endregion
#endregion
#region Updating
private TimeSpan _lastOverridePosition;
/// <summary>
/// Updates the timeline, applying the provided <paramref name="delta" /> to the <see cref="Position" />
/// </summary>
/// <param name="delta">The amount of time to apply to the position</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Update(TimeSpan delta, bool stickToMainSegment)
{
lock (_lock)
{
Delta += delta;
Position += delta;
IsOverridden = false;
_lastOverridePosition = Position;
if (stickToMainSegment && Position > MainSegmentEndPosition)
{
// If the main segment has no length, simply stick to the start of the segment
if (MainSegmentLength == TimeSpan.Zero)
Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
}
_extraTimelines.RemoveAll(t => t.IsFinished);
foreach (Timeline extraTimeline in _extraTimelines)
extraTimeline.Update(delta, false);
}
}
/// <summary>
/// Moves the position of the timeline backwards to the very start of the timeline
/// </summary>
public void JumpToStart()
{
lock (_lock)
{
if (Position == TimeSpan.Zero)
return;
Delta = TimeSpan.Zero - Position;
Position = TimeSpan.Zero;
}
}
/// <summary>
/// Moves the position of the timeline forwards to the beginning of the end segment
/// </summary>
public void JumpToEndSegment()
{
lock (_lock)
{
if (Position >= EndSegmentStartPosition)
return;
Delta = EndSegmentStartPosition - Position;
Position = EndSegmentStartPosition;
}
}
/// <summary>
/// Moves the position of the timeline forwards to the very end of the timeline
/// </summary>
public void JumpToEnd()
{
lock (_lock)
{
if (Position >= EndSegmentEndPosition)
return;
Delta = EndSegmentEndPosition - Position;
Position = EndSegmentEndPosition;
}
}
/// <summary>
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines
/// </summary>
/// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Override(TimeSpan position, bool stickToMainSegment)
{
lock (_lock)
{
Delta += position - _lastOverridePosition;
Position = position;
IsOverridden = true;
_lastOverridePosition = position;
if (stickToMainSegment && Position >= MainSegmentStartPosition)
{
bool atSegmentStart = Position == MainSegmentStartPosition;
if (MainSegmentLength > TimeSpan.Zero)
{
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
// is actually at the start of the segment
if (Position == MainSegmentStartPosition && !atSegmentStart)
Position = MainSegmentEndPosition;
}
else
Position = MainSegmentStartPosition;
}
_extraTimelines.Clear();
}
}
/// <summary>
/// Sets the <see cref="Delta" /> to <see cref="TimeSpan.Zero" />
/// </summary>
public void ClearDelta()
{
lock (_lock)
{
Delta = TimeSpan.Zero;
}
}
#endregion
#region Storage
/// <inheritdoc />
public void Load()
{
StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
JumpToEnd();
}
/// <inheritdoc />
public void Save()
{
Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
}
#endregion
}
internal enum TimelineSegment
{
Start,
Main,
End
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// Sets the <see cref="Delta" /> to <see cref="TimeSpan.Zero" />
/// </summary>
public enum TimelinePlayMode
public void ClearDelta()
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
/// <summary>
/// Only play the timeline once when the condition is met
/// </summary>
Once
lock (_lock)
{
Delta = TimeSpan.Zero;
}
}
#endregion
#region Storage
/// <inheritdoc />
public void Load()
{
StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
JumpToEnd();
}
/// <inheritdoc />
public void Save()
{
Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
}
#endregion
}
internal enum TimelineSegment
{
Start,
Main,
End
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// </summary>
public enum TimelinePlayMode
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// Only play the timeline once when the condition is met
/// </summary>
public enum TimelineStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
Once
}
/// <summary>
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
SkipToEnd
}
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// </summary>
public enum TimelineStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions events are fired
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
public enum TimeLineEventOverlapMode
{
/// <summary>
/// Stop the current run and restart the timeline
/// </summary>
Restart,
SkipToEnd
}
/// <summary>
/// Ignore subsequent event fires until the timeline finishes
/// </summary>
Ignore,
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions events are fired
/// </summary>
public enum TimeLineEventOverlapMode
{
/// <summary>
/// Stop the current run and restart the timeline
/// </summary>
Restart,
/// <summary>
/// Play another copy of the timeline on top of the current run
/// </summary>
Copy,
/// <summary>
/// Ignore subsequent event fires until the timeline finishes
/// </summary>
Ignore,
/// <summary>
/// Repeat the timeline until the event fires again
/// </summary>
Toggle
}
/// <summary>
/// Play another copy of the timeline on top of the current run
/// </summary>
Copy,
/// <summary>
/// Repeat the timeline until the event fires again
/// </summary>
Toggle
}

View File

@ -337,7 +337,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
if (end < TimeSpan.FromMilliseconds(100))
end = TimeSpan.FromMilliseconds(100);
if (Segment == SegmentViewModelType.Start)
SelectedProfileElement.Timeline.StartSegmentEndPosition = end;
else if (Segment == SegmentViewModelType.Main)

View File

@ -5,14 +5,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to delete a keyframe.
/// </summary>
public class DeleteKeyframe<T> : IProfileEditorCommand
public class DeleteKeyframe : IProfileEditorCommand
{
private readonly LayerPropertyKeyframe<T> _keyframe;
private readonly ILayerPropertyKeyframe _keyframe;
/// <summary>
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
/// </summary>
public DeleteKeyframe(LayerPropertyKeyframe<T> keyframe)
public DeleteKeyframe(ILayerPropertyKeyframe keyframe)
{
_keyframe = keyframe;
}
@ -25,13 +25,13 @@ public class DeleteKeyframe<T> : IProfileEditorCommand
/// <inheritdoc />
public void Execute()
{
_keyframe.LayerProperty.RemoveKeyframe(_keyframe);
_keyframe.UntypedLayerProperty.RemoveUntypedKeyframe(_keyframe);
}
/// <inheritdoc />
public void Undo()
{
_keyframe.LayerProperty.AddKeyframe(_keyframe);
_keyframe.UntypedLayerProperty.AddUntypedKeyframe(_keyframe);
}
#endregion

View File

@ -0,0 +1,108 @@
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 length of a timeline segment.
/// </summary>
public class ResizeTimelineSegment : IProfileEditorCommand
{
private readonly TimeSpan _length;
private readonly TimeSpan _originalLength;
private readonly RenderProfileElement _profileElement;
private readonly SegmentType _segmentType;
/// <summary>
/// Creates a new instance of the <see cref="ResizeTimelineSegment" /> class.
/// </summary>
/// <param name="segmentType">The type of segment to resize.</param>
/// <param name="profileElement">The render profile element whose segment to resize.</param>
/// <param name="length">The new length of the segment</param>
public ResizeTimelineSegment(SegmentType segmentType, RenderProfileElement profileElement, TimeSpan length)
{
_segmentType = segmentType;
_profileElement = profileElement;
_length = length;
_originalLength = _segmentType switch
{
SegmentType.Start => _profileElement.Timeline.StartSegmentLength,
SegmentType.Main => _profileElement.Timeline.MainSegmentLength,
SegmentType.End => _profileElement.Timeline.EndSegmentLength,
_ => _originalLength
};
}
/// <summary>
/// Creates a new instance of the <see cref="ResizeTimelineSegment" /> class.
/// </summary>
/// <param name="segmentType">The type of segment to resize.</param>
/// <param name="profileElement">The render profile element whose segment to resize.</param>
/// <param name="length">The new length of the segment</param>
/// <param name="originalLength">The original length of the segment</param>
public ResizeTimelineSegment(SegmentType segmentType, RenderProfileElement profileElement, TimeSpan length, TimeSpan originalLength)
{
_segmentType = segmentType;
_profileElement = profileElement;
_length = length;
_originalLength = originalLength;
}
/// <inheritdoc />
public string DisplayName => $"Resize {_segmentType.ToString().ToLower()} segment";
/// <inheritdoc />
public void Execute()
{
switch (_segmentType)
{
case SegmentType.Start:
_profileElement.Timeline.StartSegmentLength = _length;
break;
case SegmentType.Main:
_profileElement.Timeline.MainSegmentLength = _length;
break;
case SegmentType.End:
_profileElement.Timeline.EndSegmentLength = _length;
break;
}
}
/// <inheritdoc />
public void Undo()
{
switch (_segmentType)
{
case SegmentType.Start:
_profileElement.Timeline.StartSegmentLength = _originalLength;
break;
case SegmentType.Main:
_profileElement.Timeline.MainSegmentLength = _originalLength;
break;
case SegmentType.End:
_profileElement.Timeline.EndSegmentLength = _originalLength;
break;
}
}
/// <summary>
/// Represents a type of segment on a timeline.
/// </summary>
public enum SegmentType
{
/// <summary>
/// The start segment.
/// </summary>
Start,
/// <summary>
/// The main segment.
/// </summary>
Main,
/// <summary>
/// The end segment.
/// </summary>
End
}
}

View File

@ -12,6 +12,7 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
private readonly T _newValue;
private readonly T _originalValue;
private readonly TimeSpan? _time;
private LayerPropertyKeyframe<T>? _newKeyframe;
/// <summary>
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
@ -43,13 +44,16 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
/// <inheritdoc />
public void Execute()
{
_layerProperty.SetCurrentValue(_newValue, _time);
_newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time);
}
/// <inheritdoc />
public void Undo()
{
_layerProperty.SetCurrentValue(_originalValue, _time);
if (_newKeyframe != null)
_layerProperty.RemoveKeyframe(_newKeyframe);
else
_layerProperty.SetCurrentValue(_originalValue, _time);
}
#endregion

View File

@ -4,6 +4,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdevice_005Ctabs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cplugins_005Cdialogs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels_005Cproperties_005Ctimeline_005Ckeyframes/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Ctools/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csettings_005Ctabs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Ccontentdialogs/@EntryIndexedValue">True</s:Boolean>

View File

@ -8,6 +8,7 @@ using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Properties;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Sidebar;
@ -73,14 +74,9 @@ namespace Artemis.UI.Ninject.Factories
PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect);
TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
// TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
// EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
// TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
// TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
}
public interface IPropertyVmFactory

View File

@ -6,6 +6,9 @@
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid">
<Grid RowDefinitions="48,*">
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" />
@ -31,76 +34,103 @@
Background="Transparent"
Margin="0 0 -5 0" />
<Grid Grid.Column="2" RowDefinitions="48,*">
<!-- Timeline header body -->
<controls:TimelineHeader Grid.Row="0"
Name="TimelineHeader"
Margin="0 18 0 0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
PixelsPerSecond="{Binding PixelsPerSecond}"
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
OffsetFirstValue="True"
PointerReleased="TimelineHeader_OnPointerReleased"
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
Cursor="Hand" />
<!-- Horizontal scrolling -->
<ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Grid RowDefinitions="48,*">
<!-- Timeline header body -->
<controls:TimelineHeader Grid.Row="0"
Name="TimelineHeader"
Margin="0 18 0 0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Left"
PixelsPerSecond="{Binding PixelsPerSecond}"
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
OffsetFirstValue="True"
PointerReleased="TimelineHeader_OnPointerReleased"
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
Cursor="Hand" />
<Canvas Grid.Row="0" ZIndex="2">
<!-- Timeline segments -->
<ContentControl Canvas.Left="{Binding EndTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding EndTimelineSegmentViewModel}" />
<ContentControl Canvas.Left="{Binding MainTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding MainTimelineSegmentViewModel}" />
<ContentControl Canvas.Left="{Binding StartTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding StartTimelineSegmentViewModel}" />
<Canvas Grid.Row="0" ZIndex="2">
<!-- Segment dividers -->
<Line Name="TimelineLine"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
StartPoint="0,0"
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}">
</Line>
<!-- Timeline caret -->
<Polygon Name="TimelineCaret"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
Points="-8,0 -8,8 0,20, 8,8 8,0"
Fill="{DynamicResource SystemAccentColorLight1}">
<!-- <Polygon.Transitions> -->
<!-- <Transitions> -->
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
<!-- </Transitions> -->
<!-- </Polygon.Transitions> -->
</Polygon>
<Line Name="TimelineLine"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
StartPoint="0,0"
EndPoint="0,1"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
RenderTransformOrigin="0,0">
<!-- <Line.Transitions> -->
<!-- <Transitions> -->
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
<!-- </Transitions> -->
<!-- </Line.Transitions> -->
<Line.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="{Binding #ContainerGrid.Bounds.Height}" />
</Line.RenderTransform>
</Line>
</Canvas>
<Line Name="StartSegmentLine"
Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.EndX}"
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddStart}"
StartPoint="0,0"
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5">
</Line>
<Line Name="MainSegmentLine"
Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.EndX}"
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddMain}"
StartPoint="0,0"
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5">
</Line>
<Line Name="EndSegmentLine"
Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.EndX}"
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddEnd}"
StartPoint="0,0"
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5">
</Line>
<!-- Horizontal scrolling -->
<ScrollViewer Grid.Row="1"
Name="TimelineScrollViewer"
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
<ContentControl Content="{Binding TimelineViewModel}" />
</ScrollViewer>
<!-- Timeline segments -->
<ContentControl Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{Binding TimelineViewModel.EndSegmentViewModel}" />
<ContentControl Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{Binding TimelineViewModel.MainSegmentViewModel}" />
<ContentControl Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{Binding TimelineViewModel.StartSegmentViewModel}" />
<!-- TODO: Databindings here -->
<!-- Timeline caret -->
<Polygon Name="TimelineCaret"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
Points="-8,0 -8,8 0,20, 8,8 8,0"
Fill="{DynamicResource SystemAccentColorLight1}">
</Polygon>
</Canvas>
</Grid>
<ScrollViewer Grid.Row="1"
Name="TimelineScrollViewer"
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden">
<ContentControl Content="{Binding TimelineViewModel}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
</ScrollViewer>
<!-- TODO: Databindings here -->
</Grid>
</ScrollViewer>
</Grid>

View File

@ -8,6 +8,7 @@ using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.PropertyInput;

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;

View File

@ -1,8 +1,7 @@
using System;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public interface ITimelineKeyframeViewModel
{

View File

@ -3,7 +3,7 @@
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">
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView">
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
StrokeThickness="1"

View File

@ -1,7 +1,7 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes
{
public partial class TimelineEasingView : UserControl
{

View File

@ -4,7 +4,7 @@ using Artemis.UI.Shared;
using Avalonia;
using Humanizer;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineEasingViewModel : ViewModelBase
{

View File

@ -5,7 +5,7 @@
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"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView"
ClipToBounds="False"
Height="{DynamicResource RailsHeight}">
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}"

View File

@ -4,7 +4,7 @@ using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
{

View File

@ -7,11 +7,10 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
@ -97,7 +96,7 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
public void Delete()
{
_profileEditorService.ExecuteCommand(new DeleteKeyframe<T>(LayerPropertyKeyframe));
_profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
}
#endregion

View File

@ -0,0 +1,56 @@
<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"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.EndSegmentView">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
Width="{Binding Width}"
ColumnDefinitions="Auto, Auto,*,Auto">
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
Classes="resize-visual" />
<Button Grid.Column="1"
Name="AddMainSegment"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Add main segment"
Command="{Binding AddMainSegment}"
IsVisible="{Binding ShowAddMain}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" ClipToBounds="True">
<TextBlock Name="SegmentTitle"
FontSize="13"
ToolTip.Tip="This segment is played once a condition is no longer met">
End
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{Binding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>
<Rectangle Name="KeyframeDragVisualRight"
Grid.Column="3"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="3"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{Binding EndTimestamp}"/>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,50 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
{
public partial class EndSegmentView : ReactiveUserControl<EndSegmentViewModel>
{
private readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public EndSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(_keyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class EndSegmentViewModel : TimelineSegmentViewModel
{
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private TimeSpan _time;
private ObservableAsPropertyHelper<double>? _start;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private readonly ObservableAsPropertyHelper<double> _width;
private TimeSpan _initialLength;
public EndSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_start = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentStartPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.StartX)
.DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => _profileElement?.Timeline.EndSegmentStartPosition ?? TimeSpan.Zero;
public override double StartX => _start?.Value ?? 0;
public override TimeSpan End => _profileElement?.Timeline.EndSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.EndSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.EndSegmentLength = value;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.End;
}

View File

@ -0,0 +1,78 @@
<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"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.MainSegmentView">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
Width="{Binding Width}"
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
IsVisible="{Binding !ShowAddStart}"
Classes="resize-visual" />
<Button Name="AddStartSegment"
Grid.Column="1"
Classes="AppBarButton icon-button icon-button-small"
VerticalAlignment="Center"
ToolTip.Tip="Add a start segment"
Command="{Binding AddStartSegment}"
IsVisible="{Binding ShowAddStart}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Name="SegmentTitle"
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontSize="13"
ToolTip.Tip="This segment is played while a condition is met, either once or on a repeating loop">
Main
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{Binding DisableSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
<ToggleButton Name="SegmentRepeat"
Classes="icon-button icon-button-small"
ToolTip.Tip="Repeat this segment"
IsChecked="{Binding RepeatSegment}"
Padding="0">
<avalonia:MaterialIcon Kind="Repeat" />
</ToggleButton>
</StackPanel>
<Button Name="AddEndSegment"
Grid.Column="3"
Classes="AppBarButton icon-button icon-button-small"
VerticalAlignment="Center"
ToolTip.Tip="Add an end segment"
Command="{Binding AddEndSegment}"
IsVisible="{Binding ShowAddEnd}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<Rectangle Name="KeyframeDragVisual"
Grid.Column="4"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="4"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{Binding EndTimestamp}"/>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,62 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
{
public partial class MainSegmentView : ReactiveUserControl<MainSegmentViewModel>
{
private readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public MainSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(_keyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragStartAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class MainSegmentViewModel : TimelineSegmentViewModel
{
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private TimeSpan _time;
private ObservableAsPropertyHelper<double>? _start;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private readonly ObservableAsPropertyHelper<double> _width;
private TimeSpan _initialLength;
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_start = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentStartPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.StartX)
.DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => _profileElement?.Timeline.MainSegmentStartPosition ?? TimeSpan.Zero;
public override double StartX => _start?.Value ?? 0;
public override TimeSpan End => _profileElement?.Timeline.MainSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.MainSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.MainSegmentLength = value;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main;
}

View File

@ -0,0 +1,28 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<Style Selector="ContentControl.segment-content-control">
</Style>
<Style Selector="Border.segment-container">
<Setter Property="Height" Value="18"></Setter>
</Style>
<Style Selector="Rectangle.resize-visual">
<Setter Property="Fill" Value="{DynamicResource SystemAccentColorLight2}"/>
<Setter Property="Width" Value="4"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style Selector="Rectangle.resize-anchor">
<Setter Property="Fill" Value="Transparent"/>
<Setter Property="Width" Value="10"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="Cursor" Value="SizeWestEast"/>
</Style>
</Styles>

View File

@ -0,0 +1,54 @@
<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"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.StartSegmentView">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
Width="{Binding Width}"
ColumnDefinitions="*,Auto,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Name="SegmentTitle"
FontSize="13"
ToolTip.Tip="This segment is played when a layer starts displaying because it's conditions are met">
Start
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{Binding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>
<Button Grid.Column="1"
Name="AddMainSegment"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Add main segment"
Command="{Binding AddMainSegment}"
IsVisible="{Binding ShowAddMain}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<Rectangle Name="KeyframeDragVisual"
Grid.Column="2"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="2"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{Binding EndTimestamp}"/>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,50 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
{
public partial class StartSegmentView : ReactiveUserControl<StartSegmentViewModel>
{
private readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public StartSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(_keyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using Castle.DynamicProxy.Generators;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class StartSegmentViewModel : TimelineSegmentViewModel
{
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private TimeSpan _time;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private readonly ObservableAsPropertyHelper<double> _width;
private TimeSpan _initialLength;
public StartSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => TimeSpan.Zero;
public override double StartX => 0;
public override TimeSpan End => _profileElement?.Timeline.StartSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.StartSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.StartSegmentLength = value;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Start;
}

View File

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
{
private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2);
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private Dictionary<ILayerPropertyKeyframe, TimeSpan> _originalKeyframePositions = new();
private ObservableAsPropertyHelper<bool>? _showAddStart;
private ObservableAsPropertyHelper<bool>? _showAddMain;
private ObservableAsPropertyHelper<bool>? _showAddEnd;
private TimeSpan _initialLength;
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_showAddStart = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddStart)
.DisposeWith(d);
_showAddMain = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddMain)
.DisposeWith(d);
_showAddEnd = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddEnd)
.DisposeWith(d);
});
}
public bool ShowAddStart => _showAddStart?.Value ?? false;
public bool ShowAddMain => _showAddMain?.Value ?? false;
public bool ShowAddEnd => _showAddEnd?.Value ?? false;
public abstract TimeSpan Start { get; }
public abstract double StartX { get; }
public abstract TimeSpan End { get; }
public abstract double EndX { get; }
public abstract TimeSpan Length { get; set; }
public abstract double Width { get; }
public abstract string? EndTimestamp { get; }
public abstract ResizeTimelineSegment.SegmentType Type { get; }
public void AddStartSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add start segment");
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes), NewSegmentLength);
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Start, _profileElement, NewSegmentLength));
}
public void AddMainSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add main segment");
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).Where(s => s.Position > _profileElement.Timeline.StartSegmentEndPosition), NewSegmentLength);
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Main, _profileElement, NewSegmentLength));
}
public void AddEndSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add end segment");
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.End, _profileElement, NewSegmentLength));
}
public void StartResize()
{
if (_profileElement == null)
return;
_initialLength = Length;
}
public void UpdateResize(double x)
{
if (_profileElement == null)
return;
TimeSpan difference = GetTimeFromX(x) - Length;
List<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList();
ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference);
Length = GetTimeFromX(x);
}
public void FinishResize(double x)
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment");
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, GetTimeFromX(x), _initialLength));
}
public void RemoveSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Remove segment");
IEnumerable<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes);
// Delete keyframes in the segment
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
if (layerPropertyKeyframe.Position > Start && layerPropertyKeyframe.Position <= End)
_profileEditorService.ExecuteCommand(new DeleteKeyframe(layerPropertyKeyframe));
// Move keyframes after the segment forwards
ShiftKeyframes(keyframes.Where(s => s.Position > End), new TimeSpan(Length.Ticks * -1));
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.Zero));
}
protected TimeSpan GetTimeFromX(double x)
{
TimeSpan length = TimeSpan.FromSeconds(x / _pixelsPerSecond);
if (length < TimeSpan.Zero)
length = TimeSpan.Zero;
return length;
}
protected void ShiftKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, TimeSpan amount)
{
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
{
if (!_originalKeyframePositions.ContainsKey(layerPropertyKeyframe))
_originalKeyframePositions[layerPropertyKeyframe] = layerPropertyKeyframe.Position;
layerPropertyKeyframe.Position = layerPropertyKeyframe.Position.Add(amount);
}
}
protected void ApplyPendingKeyframeMovement()
{
foreach ((ILayerPropertyKeyframe keyframe, TimeSpan originalPosition) in _originalKeyframePositions)
_profileEditorService.ExecuteCommand(new MoveKeyframe(keyframe, keyframe.Position, originalPosition));
_originalKeyframePositions.Clear();
}
}

View File

@ -12,8 +12,8 @@
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
</Style>
</ItemsControl.Styles>
</ItemsControl>

View File

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
@ -24,6 +25,9 @@ public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelineP
this.WhenActivated(d =>
{
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);

View File

@ -10,8 +10,11 @@
<x:Double x:Key="RailsHeight">28</x:Double>
<x:Double x:Key="RailsBorderHeight">29</x:Double>
</UserControl.Resources>
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased">
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
<Grid.KeyBindings>
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete"/>
</Grid.KeyBindings>
<ItemsControl Items="{Binding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TimelineGroupViewModel}" />

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared.Controls;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions;

View File

@ -3,6 +3,9 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
@ -17,10 +20,18 @@ public class TimelineViewModel : ActivatableViewModelBase
private ObservableAsPropertyHelper<double>? _caretPosition;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
private ObservableAsPropertyHelper<double> _minWidth;
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels, IProfileEditorService profileEditorService)
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels,
StartSegmentViewModel startSegmentViewModel,
MainSegmentViewModel mainSegmentViewModel,
EndSegmentViewModel endSegmentViewModel,
IProfileEditorService profileEditorService)
{
PropertyGroupViewModels = propertyGroupViewModels;
StartSegmentViewModel = startSegmentViewModel;
MainSegmentViewModel = mainSegmentViewModel;
EndSegmentViewModel = endSegmentViewModel;
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
@ -29,14 +40,24 @@ public class TimelineViewModel : ActivatableViewModelBase
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.CaretPosition)
.DisposeWith(d);
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
_minWidth = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.Length) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100)
.ToProperty(this, vm => vm.MinWidth)
.DisposeWith(d);
});
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public StartSegmentViewModel StartSegmentViewModel { get; }
public MainSegmentViewModel MainSegmentViewModel { get; }
public EndSegmentViewModel EndSegmentViewModel { get; }
public double CaretPosition => _caretPosition?.Value ?? 0.0;
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public double MinWidth => _minWidth?.Value ?? 0;
public void ChangeTime(TimeSpan newTime)
{
@ -118,10 +139,10 @@ public class TimelineViewModel : ActivatableViewModelBase
#region Keyframe actions
public void DuplicateKeyframes(ITimelineKeyframeViewModel source)
public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (!source.IsSelected)
source.Duplicate();
if (source is { IsSelected: false })
source.Delete();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
@ -131,9 +152,9 @@ public class TimelineViewModel : ActivatableViewModelBase
}
}
public void CopyKeyframes(ITimelineKeyframeViewModel source)
public void CopyKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (!source.IsSelected)
if (source is { IsSelected: false })
source.Copy();
else
{
@ -144,9 +165,9 @@ public class TimelineViewModel : ActivatableViewModelBase
}
}
public void PasteKeyframes(ITimelineKeyframeViewModel source)
public void PasteKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (!source.IsSelected)
if (source is { IsSelected: false })
source.Paste();
else
{
@ -157,9 +178,9 @@ public class TimelineViewModel : ActivatableViewModelBase
}
}
public void DeleteKeyframes(ITimelineKeyframeViewModel source)
public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (!source.IsSelected)
if (source is {IsSelected: false})
source.Delete();
else
{