// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.Previews
{
/// [Editor-Only] Utility for playing through transition previews.
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewPlayer
[Serializable]
public class TransitionPreviewPlayer : IDisposable
{
/************************************************************************************************************************/
[SerializeReference] private ITransition _FromTransition;
[SerializeReference] private ITransition _ToTransition;
/// The animation to play first.
public ref ITransition FromTransition
=> ref _FromTransition;
/// The animation to transition into.
public ref ITransition ToTransition
=> ref _ToTransition;
/************************************************************************************************************************/
private AnimancerGraph _Graph;
/// The graph used to play the animations.
public AnimancerGraph Graph
{
get => _Graph;
set
{
_Graph = value;
Evaluate();
}
}
/************************************************************************************************************************/
///
/// The minimum amount of time to play the
/// before the starts (in seconds).
///
public float FromDuration { get; set; }
///
/// The minimum amount of time to continue playing
/// after the started (in seconds).
///
public float ToDuration { get; set; }
/// The speed at which the preview plays.
public float Speed { get; set; } = 1;
/************************************************************************************************************************/
private float _FadeDuration = float.NaN;
/// The .
/// uses the value from the .
public float FadeDuration
{
get => float.IsNaN(_FadeDuration) && ToTransition.IsValid()
? ToTransition.FadeDuration
: _FadeDuration;
set => _FadeDuration = value;
}
/************************************************************************************************************************/
/// The lowest allowed .
///
/// This is the lower of the negative
/// or the negative duration of the .
///
public float MinTime { get; private set; }
/// The highest allowed .
///
/// This is the higher of the or
/// or the duration of the .
///
public float MaxTime { get; private set; }
/************************************************************************************************************************/
/// Recalculated the and .
public void RecalculateTimeBounds()
{
MinTime = -FromDuration;
if (_FromTransition.IsValid() &&
AnimancerUtilities.TryCalculateDuration(_FromTransition, out var duration))
MinTime = Math.Min(MinTime, -duration);
MaxTime = ToDuration;
var fadeDuration = FadeDuration;
if (!float.IsNaN(fadeDuration))
MaxTime = Math.Max(ToDuration, fadeDuration);
if (_ToTransition.IsValid() &&
AnimancerUtilities.TryCalculateDuration(_ToTransition, out duration))
MaxTime = Math.Max(MaxTime, duration);
}
/************************************************************************************************************************/
/// Converts normalized time to seconds.
public float LerpTimeUnclamped(float normalizedTime)
=> Mathf.LerpUnclamped(MinTime, MaxTime, normalizedTime);
/// Converts seconds to normalized time.
public float InverseLerpTimeUnclamped(float time)
=> AnimancerUtilities.InverseLerpUnclamped(MinTime, MaxTime, time);
/************************************************************************************************************************/
private float _CurrentTime = float.NaN;
/// The amount of time that has passed since the started (in seconds).
/// This value goes from the to the .
public float CurrentTime
{
get => float.IsNaN(_CurrentTime)
? MinTime
: _CurrentTime;
set
{
_CurrentTime = value;
Evaluate();
}
}
/// The amount of time that has passed since the started (normalized).
/// 0 is at the and 1 is at the .
public float NormalizedTime
{
get => InverseLerpTimeUnclamped(CurrentTime);
set => CurrentTime = LerpTimeUnclamped(value);
}
/************************************************************************************************************************/
private bool _IsPlaying;
/// Is the preview currently playing?
public bool IsPlaying
{
get => _IsPlaying;
set
{
if (_IsPlaying == value)
return;
_IsPlaying = value;
if (_IsPlaying)
{
_LastUpdateTime = TimeSinceStartup;
EditorApplication.update += Update;
}
else
{
EditorApplication.update -= Update;
}
}
}
/// Cleans up this player.
public void Dispose()
=> IsPlaying = false;
/************************************************************************************************************************/
///
private static double TimeSinceStartup
=> EditorApplication.timeSinceStartup;
private double _LastUpdateTime;
/// Updates the preview time while playing.
private void Update()
{
if (Graph == null || !Graph.IsValidOrDispose())
{
EditorApplication.update -= Update;
return;
}
var time = TimeSinceStartup;
var deltaTime = time - _LastUpdateTime;
_LastUpdateTime = time;
CurrentTime += ((float)deltaTime) * Speed;
AnimancerGUI.RepaintEverything();
}
/************************************************************************************************************************/
/// Applies the animations at the .
private void Evaluate()
{
if (Graph == null)
return;
Graph.PauseGraph();
Graph.Stop();
if (_FromTransition.IsValid())
{
if (_ToTransition.IsValid())
{
Apply(CurrentTime, _FromTransition, _ToTransition);
}
else
{
var minTime = MinTime;
Apply(CurrentTime - minTime, -minTime, _FromTransition);
}
}
else
{
if (_ToTransition.IsValid())
Apply(CurrentTime, MaxTime, _ToTransition);
else
return;
}
Graph.Evaluate();
}
/************************************************************************************************************************/
/// Applies the animations at the `currentTime`.
private void Apply(float currentTime, ITransition from, ITransition to)
{
var layer = Graph.Layers[0];
// Playing From.
if (currentTime < 0)
{
var state = layer.Play(from);
state.Time += state.NormalizedEndTime * state.Length + currentTime;
return;
}
var maxTime = MaxTime;
// Fading.
if (currentTime < maxTime)
{
var state = layer.Play(from);
state.Time += state.NormalizedEndTime * state.Length + currentTime;
state = layer.Play(to, FadeDuration, to.FadeMode);
state.Time += currentTime;
var fade = state.FadeGroup;
if (fade != null)
{
fade.NormalizedTime += currentTime * fade.FadeSpeed;
fade.ApplyWeights();
}
}
// Playing To.
else
{
var state = layer.Play(to);
state.Time += currentTime;
// Finished.
if (currentTime >= maxTime)
{
_CurrentTime = MinTime;
state.Time = _CurrentTime;
}
}
}
/************************************************************************************************************************/
/// Applies the animation at the `currentTime`.
private void Apply(float currentTime, float endTime, ITransition transition)
{
var state = Graph.Layers[0].Play(transition);
if (currentTime < endTime)// Playing.
{
state.Time = currentTime;
}
else// Finished.
{
_CurrentTime = MinTime;
state.Time = _CurrentTime;
}
}
/************************************************************************************************************************/
}
}
#endif