mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
473 lines
13 KiB
C#
473 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using Artemis.Storage.Entities.Profile;
|
|
|
|
namespace Artemis.Core;
|
|
|
|
/// <summary>
|
|
/// Represents a timeline used by profile elements
|
|
/// </summary>
|
|
public class Timeline : CorePropertyChanged, IStorageModel
|
|
{
|
|
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);
|
|
|
|
Save();
|
|
}
|
|
|
|
internal Timeline(TimelineEntity entity)
|
|
{
|
|
Entity = entity;
|
|
|
|
Load();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString()
|
|
{
|
|
return $"Progress: {Position}/{Length} - delta: {Delta}";
|
|
}
|
|
|
|
#region Properties
|
|
|
|
private TimeSpan _position;
|
|
private TimeSpan _lastDelta;
|
|
private TimelinePlayMode _playMode;
|
|
private TimelineStopMode _stopMode;
|
|
private TimeSpan _startSegmentLength;
|
|
private TimeSpan _mainSegmentLength;
|
|
private TimeSpan _endSegmentLength;
|
|
private TimeSpan _lastOverride;
|
|
|
|
/// <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" />
|
|
/// </summary>
|
|
public TimeSpan Delta
|
|
{
|
|
get => _lastDelta;
|
|
private set => SetAndNotify(ref _lastDelta, value);
|
|
}
|
|
|
|
/// <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 boolean indicating whether the timeline has finished its run
|
|
/// </summary>
|
|
public bool IsFinished => Position > Length;
|
|
|
|
/// <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));
|
|
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
|
|
|
|
/// <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)
|
|
{
|
|
if (IsOverridden)
|
|
throw new ArtemisCoreException("Can't update an overridden timeline, call ClearOverride first.");
|
|
|
|
Delta += delta;
|
|
Position += delta;
|
|
|
|
if (!stickToMainSegment || Position <= MainSegmentEndPosition)
|
|
return;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/// <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
|
|
/// </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>
|
|
internal void Override(TimeSpan position, bool stickToMainSegment)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_lastOverride == TimeSpan.Zero)
|
|
Delta = Position - position;
|
|
else
|
|
Delta = position - _lastOverride;
|
|
|
|
Position = position;
|
|
IsOverridden = true;
|
|
_lastOverride = position;
|
|
|
|
if (!stickToMainSegment || Position < MainSegmentStartPosition)
|
|
return;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void ClearOverride()
|
|
{
|
|
IsOverridden = false;
|
|
_lastOverride = TimeSpan.Zero;
|
|
}
|
|
|
|
/// <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
|
|
/// </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
|
|
} |