KeyFrames.cs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using Avalonia.Collections;
  5. using System.ComponentModel;
  6. using Avalonia.Animation.Utils;
  7. using System.Reactive.Linq;
  8. using System.Linq;
  9. using Avalonia.Data;
  10. using System.Reactive.Disposables;
  11. namespace Avalonia.Animation.Keyframes
  12. {
  13. /// <summary>
  14. /// Base class for KeyFrames objects
  15. /// </summary>
  16. public abstract class KeyFrames<T> : AvaloniaList<KeyFrame>, IKeyFrames
  17. {
  18. /// <summary>
  19. /// Target property.
  20. /// </summary>
  21. public AvaloniaProperty Property { get; set; }
  22. /// <summary>
  23. /// List of type-converted keyframes.
  24. /// </summary>
  25. public Dictionary<double, T> ConvertedKeyframes = new Dictionary<double, T>();
  26. private bool IsVerfifiedAndConverted;
  27. /// <inheritdoc/>
  28. public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
  29. {
  30. if (!IsVerfifiedAndConverted)
  31. VerifyConvertKeyFrames(animation, typeof(T));
  32. return obsMatch
  33. .Where(p => p == true)
  34. // Ignore triggers when global timers are paused.
  35. .Where(p => Timing.GetGlobalPlayState() != PlayState.Pause)
  36. .Subscribe(_ =>
  37. {
  38. var timerObs = RunKeyFrames(animation, control);
  39. });
  40. }
  41. /// <summary>
  42. /// Get the nearest pair of cue-time ordered keyframes
  43. /// according to the given time parameter that is relative to
  44. /// total animation time and the normalized intra-keyframe pair time
  45. /// (i.e., the normalized time between the selected keyframes, relative to the
  46. /// time parameter).
  47. /// </summary>
  48. /// <param name="t">The time parameter, relative to the total animation time</param>
  49. public (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
  50. {
  51. KeyValuePair<double, T> firstCue, lastCue;
  52. int kvCount = ConvertedKeyframes.Count();
  53. if (kvCount > 2)
  54. {
  55. if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
  56. {
  57. firstCue = ConvertedKeyframes.First();
  58. lastCue = ConvertedKeyframes.Skip(1).First();
  59. }
  60. else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0)
  61. {
  62. firstCue = ConvertedKeyframes.Skip(kvCount - 2).First();
  63. lastCue = ConvertedKeyframes.Last();
  64. }
  65. else
  66. {
  67. firstCue = ConvertedKeyframes.Where(j => j.Key <= t).Last();
  68. lastCue = ConvertedKeyframes.Where(j => j.Key >= t).First();
  69. }
  70. }
  71. else
  72. {
  73. firstCue = ConvertedKeyframes.First();
  74. lastCue = ConvertedKeyframes.Last();
  75. }
  76. double t0 = firstCue.Key;
  77. double t1 = lastCue.Key;
  78. var intraframeTime = (t - t0) / (t1 - t0);
  79. return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue));
  80. }
  81. /// <summary>
  82. /// Runs the KeyFrames Animation.
  83. /// </summary>
  84. public IDisposable RunKeyFrames(Animation animation, Animatable control)
  85. {
  86. var _kfStateMach = new KeyFramesStateMachine<T>();
  87. _kfStateMach.Initialize(animation, control, this);
  88. Timing.AnimationStateTimer
  89. .TakeWhile(_ => !_kfStateMach._unsubscribe)
  90. .Subscribe(p =>
  91. {
  92. _kfStateMach.Step(p, DoInterpolation);
  93. });
  94. return control.Bind(Property, _kfStateMach, BindingPriority.Animation);
  95. }
  96. /// <summary>
  97. /// Interpolates a value given the desired time.
  98. /// </summary>
  99. public abstract T DoInterpolation(double time);
  100. // public abstract IObservable<T> DoInterpolation(IObservable<double> timer, Animation animation,
  101. // Animatable control);
  102. /// <summary>
  103. /// Verifies and converts keyframe values according to this class's target type.
  104. /// </summary>
  105. private void VerifyConvertKeyFrames(Animation animation, Type type)
  106. {
  107. var typeConv = TypeDescriptor.GetConverter(type);
  108. foreach (KeyFrame k in this)
  109. {
  110. if (k.Value == null)
  111. {
  112. throw new ArgumentNullException($"KeyFrame value can't be null.");
  113. }
  114. if (!typeConv.CanConvertTo(k.Value.GetType()))
  115. {
  116. throw new InvalidCastException($"KeyFrame value doesnt match property type.");
  117. }
  118. T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
  119. Cue _normalizedCue = k.Cue;
  120. if (k.timeSpanSet)
  121. {
  122. _normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
  123. }
  124. ConvertedKeyframes.Add(_normalizedCue.CueValue, convertedValue);
  125. }
  126. SortKeyFrameCues(ConvertedKeyframes);
  127. IsVerfifiedAndConverted = true;
  128. }
  129. private void SortKeyFrameCues(Dictionary<double, T> convertedValues)
  130. {
  131. bool hasStartKey, hasEndKey;
  132. hasStartKey = hasEndKey = false;
  133. // this can be optional later, by making the default start/end keyframes
  134. // to have a neutral value (a.k.a. the value prior to the animation).
  135. foreach (var converted in ConvertedKeyframes.Keys)
  136. {
  137. if (DoubleUtils.AboutEqual(converted, 0.0))
  138. {
  139. hasStartKey = true;
  140. }
  141. else if (DoubleUtils.AboutEqual(converted, 1.0))
  142. {
  143. hasEndKey = true;
  144. }
  145. }
  146. if (!hasStartKey && !hasEndKey)
  147. throw new InvalidOperationException
  148. ($"{this.GetType().Name} must have a starting (0% cue) and ending (100% cue) keyframe.");
  149. // Sort Cues, in case users don't order it by themselves.
  150. ConvertedKeyframes = ConvertedKeyframes.OrderBy(p => p.Key)
  151. .ToDictionary((k) => k.Key, (v) => v.Value);
  152. }
  153. }
  154. }