using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using LitMotion.Collections; using Unity.Burst.CompilerServices; using Unity.Mathematics; namespace LitMotion { [StructLayout(LayoutKind.Sequential)] internal struct MotionData { public struct MotionState { public MotionStatus Status; public MotionStatus PrevStatus; public bool IsPreserved; public bool IsInSequence; public ushort CompletedLoops; public ushort PrevCompletedLoops; public double Time; public float PlaybackSpeed; public readonly bool WasStatusChanged => Status != PrevStatus; public readonly bool WasLoopCompleted => CompletedLoops > PrevCompletedLoops; } public struct MotionParameters { public float Duration; public Ease Ease; #if LITMOTION_COLLECTIONS_2_0_OR_NEWER public NativeAnimationCurve AnimationCurve; #else public UnsafeAnimationCurve AnimationCurve; #endif public MotionTimeKind TimeKind; public float Delay; public int Loops; public DelayType DelayType; public LoopType LoopType; public readonly double TotalDuration { get { if (Loops < 0) return double.PositiveInfinity; return Delay * (DelayType == DelayType.EveryLoop ? Loops : 1) + Duration * Loops; } } } public MotionState State; public MotionParameters Parameters; public readonly double TimeSinceStart => State.Time - Parameters.Delay; public void Update(double time, out float progress) { State.PrevCompletedLoops = State.CompletedLoops; State.PrevStatus = State.Status; State.Time = time; time = math.max(time, 0.0); double t; bool isCompleted; bool isDelayed; int completedLoops; int clampedCompletedLoops; if (Hint.Unlikely(Parameters.Duration <= 0f)) { if (Parameters.DelayType == DelayType.FirstLoop || Parameters.Delay == 0f) { isCompleted = Parameters.Loops >= 0 && TimeSinceStart > 0f; if (isCompleted) { t = 1f; completedLoops = Parameters.Loops; } else { t = 0f; completedLoops = TimeSinceStart < 0f ? -1 : 0; } clampedCompletedLoops = GetClampedCompletedLoops(completedLoops); isDelayed = TimeSinceStart < 0; } else { completedLoops = (int)math.floor(time / Parameters.Delay); clampedCompletedLoops = GetClampedCompletedLoops(completedLoops); isCompleted = Parameters.Loops >= 0 && clampedCompletedLoops > Parameters.Loops - 1; isDelayed = !isCompleted; t = isCompleted ? 1f : 0f; } } else { if (Parameters.DelayType == DelayType.FirstLoop) { completedLoops = (int)math.floor(TimeSinceStart / Parameters.Duration); clampedCompletedLoops = GetClampedCompletedLoops(completedLoops); isCompleted = Parameters.Loops >= 0 && clampedCompletedLoops > Parameters.Loops - 1; isDelayed = TimeSinceStart < 0f; if (isCompleted) { t = 1f; } else { var currentLoopTime = TimeSinceStart - Parameters.Duration * clampedCompletedLoops; t = math.clamp(currentLoopTime / Parameters.Duration, 0f, 1f); } } else { var currentLoopTime = math.fmod(time, Parameters.Duration + Parameters.Delay) - Parameters.Delay; completedLoops = (int)math.floor(time / (Parameters.Duration + Parameters.Delay)); clampedCompletedLoops = GetClampedCompletedLoops(completedLoops); isCompleted = Parameters.Loops >= 0 && clampedCompletedLoops > Parameters.Loops - 1; isDelayed = currentLoopTime < 0; if (isCompleted) { t = 1f; } else { t = math.clamp(currentLoopTime / Parameters.Duration, 0f, 1f); } } } State.CompletedLoops = (ushort)clampedCompletedLoops; switch (Parameters.LoopType) { default: case LoopType.Restart: progress = GetEasedValue((float)t); break; case LoopType.Flip: progress = GetEasedValue((float)t); if ((clampedCompletedLoops + (int)t) % 2 == 1) progress = 1f - progress; break; case LoopType.Incremental: progress = GetEasedValue(1f) * clampedCompletedLoops + GetEasedValue((float)math.fmod(t, 1f)); break; case LoopType.Yoyo: progress = (clampedCompletedLoops + (int)t) % 2 == 1 ? GetEasedValue((float)(1f - t)) : GetEasedValue((float)t); break; } if (isCompleted) { State.Status = MotionStatus.Completed; } else if (isDelayed || State.Time < 0) { State.Status = MotionStatus.Delayed; } else { State.Status = MotionStatus.Playing; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Complete(out float progress) { State.Status = MotionStatus.Completed; State.Time = Parameters.TotalDuration; State.CompletedLoops = (ushort)Parameters.Loops; progress = GetEasedValue(Parameters.LoopType switch { LoopType.Restart => 1f, LoopType.Flip or LoopType.Yoyo => Parameters.Loops % 2 == 0 ? 0f : 1f, LoopType.Incremental => Parameters.Loops, _ => 1f }); } [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly int GetClampedCompletedLoops(int completedLoops) { return Parameters.Loops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, Parameters.Loops); } [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly float GetEasedValue(float value) { return Parameters.Ease switch { Ease.CustomAnimationCurve => Parameters.AnimationCurve.Evaluate(value), _ => EaseUtility.Evaluate(value, Parameters.Ease) }; } } /// /// A structure representing motion data. /// /// The type of value to animate /// The type of special parameters given to the motion data [StructLayout(LayoutKind.Sequential)] internal struct MotionData where TValue : unmanaged where TOptions : unmanaged, IMotionOptions { // Because of pointer casting, this field must always be placed at the beginning. public MotionData Core; public TValue StartValue; public TValue EndValue; public TOptions Options; public void Update(double time, out TValue result) where TAdapter : unmanaged, IMotionAdapter { Core.Update(time, out var progress); result = default(TAdapter).Evaluate(ref StartValue, ref EndValue, ref Options, new MotionEvaluationContext() { Progress = progress, Time = time, }); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Complete(out TValue result) where TAdapter : unmanaged, IMotionAdapter { Core.Complete(out var progress); result = default(TAdapter).Evaluate( ref StartValue, ref EndValue, ref Options, new() { Progress = progress, Time = Core.State.Time, } ); } } }