using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Collections;
using System.ComponentModel;
using Avalonia.Animation.Utils;
using System.Reactive.Linq;
using System.Linq;
using Avalonia.Data;
using System.Reactive.Disposables;
namespace Avalonia.Animation.Keyframes
{
///
/// Base class for KeyFrames objects
///
public abstract class KeyFrames : AvaloniaList, IKeyFrames
{
///
/// Target property.
///
public AvaloniaProperty Property { get; set; }
///
/// List of type-converted keyframes.
///
public Dictionary ConvertedKeyframes = new Dictionary();
private bool IsVerfifiedAndConverted;
///
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch)
{
if (!IsVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T));
return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control);
});
}
///
/// Get the nearest pair of cue-time ordered keyframes
/// according to the given time parameter that is relative to
/// total animation time and the normalized intra-keyframe pair time
/// (i.e., the normalized time between the selected keyframes, relative to the
/// time parameter).
///
/// The time parameter, relative to the total animation time
public (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t)
{
KeyValuePair firstCue, lastCue;
int kvCount = ConvertedKeyframes.Count();
if (kvCount > 2)
{
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
{
firstCue = ConvertedKeyframes.First();
lastCue = ConvertedKeyframes.Skip(1).First();
}
else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0)
{
firstCue = ConvertedKeyframes.Skip(kvCount - 2).First();
lastCue = ConvertedKeyframes.Last();
}
else
{
firstCue = ConvertedKeyframes.Where(j => j.Key <= t).Last();
lastCue = ConvertedKeyframes.Where(j => j.Key >= t).First();
}
}
else
{
firstCue = ConvertedKeyframes.First();
lastCue = ConvertedKeyframes.Last();
}
double t0 = firstCue.Key;
double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0);
return (intraframeTime, new KeyFramePair(firstCue, lastCue));
}
///
/// Runs the KeyFrames Animation.
///
public IDisposable RunKeyFrames(Animation animation, Animatable control)
{
var _kfStateMach = new KeyFramesStateMachine();
_kfStateMach.Initialize(animation, control, this);
Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe)
.Subscribe(p =>
{
_kfStateMach.Step(p, DoInterpolation);
});
return control.Bind(Property, _kfStateMach, BindingPriority.Animation);
}
///
/// Interpolates a value given the desired time.
///
public abstract T DoInterpolation(double time);
// public abstract IObservable DoInterpolation(IObservable timer, Animation animation,
// Animatable control);
///
/// Verifies and converts keyframe values according to this class's target type.
///
private void VerifyConvertKeyFrames(Animation animation, Type type)
{
var typeConv = TypeDescriptor.GetConverter(type);
foreach (KeyFrame k in this)
{
if (k.Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(k.Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
Cue _normalizedCue = k.Cue;
if (k.timeSpanSet)
{
_normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
}
ConvertedKeyframes.Add(_normalizedCue.CueValue, convertedValue);
}
SortKeyFrameCues(ConvertedKeyframes);
IsVerfifiedAndConverted = true;
}
private void SortKeyFrameCues(Dictionary convertedValues)
{
bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false;
// this can be optional later, by making the default start/end keyframes
// to have a neutral value (a.k.a. the value prior to the animation).
foreach (var converted in ConvertedKeyframes.Keys)
{
if (DoubleUtils.AboutEqual(converted, 0.0))
{
hasStartKey = true;
}
else if (DoubleUtils.AboutEqual(converted, 1.0))
{
hasEndKey = true;
}
}
if (!hasStartKey && !hasEndKey)
throw new InvalidOperationException
($"{this.GetType().Name} must have a starting (0% cue) and ending (100% cue) keyframe.");
// Sort Cues, in case users don't order it by themselves.
ConvertedKeyframes = ConvertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
}
}
}