mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
529 lines
17 KiB
C#
529 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using Artemis.Storage.Entities.Profile;
|
|
|
|
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>
|
|
/// Creates a new instance of the <see cref="Timeline" /> class
|
|
/// </summary>
|
|
public Timeline()
|
|
{
|
|
Entity = new TimelineEntity();
|
|
MainSegmentLength = TimeSpan.FromSeconds(5);
|
|
|
|
_extraTimelines = new List<Timeline>();
|
|
ExtraTimelines = new(_extraTimelines);
|
|
|
|
Save();
|
|
}
|
|
|
|
internal Timeline(TimelineEntity entity)
|
|
{
|
|
Entity = entity;
|
|
_extraTimelines = new List<Timeline>();
|
|
ExtraTimelines = new(_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(_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 TimeLineEventOverlapMode _eventOverlapMode;
|
|
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 or sets the mode in which the render element responds to display condition events being fired before the
|
|
/// timeline finished
|
|
/// </summary>
|
|
public TimeLineEventOverlapMode EventOverlapMode
|
|
{
|
|
get => _eventOverlapMode;
|
|
set => SetAndNotify(ref _eventOverlapMode, 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;
|
|
EventOverlapMode = (TimeLineEventOverlapMode) Entity.EventOverlapMode;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Save()
|
|
{
|
|
Entity.StartSegmentLength = StartSegmentLength;
|
|
Entity.MainSegmentLength = MainSegmentLength;
|
|
Entity.EndSegmentLength = EndSegmentLength;
|
|
Entity.PlayMode = (int) PlayMode;
|
|
Entity.StopMode = (int) StopMode;
|
|
Entity.EventOverlapMode = (int) EventOverlapMode;
|
|
}
|
|
|
|
#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>
|
|
/// Only play the timeline once when the condition is met
|
|
/// </summary>
|
|
Once
|
|
}
|
|
|
|
/// <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>
|
|
/// When conditions are no longer met, skip to the end segment of the timeline
|
|
/// </summary>
|
|
SkipToEnd
|
|
}
|
|
|
|
/// <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>
|
|
/// Ignore subsequent event fires until the timeline finishes
|
|
/// </summary>
|
|
Ignore,
|
|
|
|
/// <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
|
|
}
|
|
} |