mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 19:36:28 +00:00
Timeline - Implemented segments
This commit is contained in:
1 parent
913117ad0a
commit
642823add5
35 files changed
+1569
-579
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user