// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Animation.Utils; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Reactive; namespace Avalonia.Animation { /// /// Base class for KeyFrames objects /// public abstract class Animator : AvaloniaList, IAnimator { /// /// List of type-converted keyframes. /// private readonly List _convertedKeyframes = new List(); private bool _isVerifiedAndConverted; /// /// Gets or sets the target property for the keyframe. /// public AvaloniaProperty Property { get; set; } public Animator() { // Invalidate keyframes when changed. this.CollectionChanged += delegate { _isVerifiedAndConverted = false; }; } /// public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete); return match.Subscribe(subject); } /// /// Get the nearest pair of cue-time ordered keyframes /// according to the given time parameter that is relative to the /// 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 protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double animationTime) { AnimatorKeyFrame firstKeyframe, lastKeyframe; int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { if (animationTime <= 0.0) { firstKeyframe = _convertedKeyframes[0]; lastKeyframe = _convertedKeyframes[1]; } else if (animationTime >= 1.0) { firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2]; lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1]; } else { int index = FindClosestBeforeKeyFrame(animationTime); firstKeyframe = _convertedKeyframes[index]; lastKeyframe = _convertedKeyframes[index + 1]; } } else { firstKeyframe = _convertedKeyframes[0]; lastKeyframe = _convertedKeyframes[1]; } double t0 = firstKeyframe.Cue.CueValue; double t1 = lastKeyframe.Cue.CueValue; var intraframeTime = (animationTime - t0) / (t1 - t0); var firstFrameData = (firstKeyframe.GetTypedValue(), firstKeyframe.isNeutral); var lastFrameData = (lastKeyframe.GetTypedValue(), lastKeyframe.isNeutral); return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } private int FindClosestBeforeKeyFrame(double time) { int FindClosestBeforeKeyFrame(int startIndex, int length) { if (length == 0 || length == 1) { return startIndex; } int middle = startIndex + (length / 2); if (_convertedKeyframes[middle].Cue.CueValue < time) { return FindClosestBeforeKeyFrame(middle, length - middle); } else if (_convertedKeyframes[middle].Cue.CueValue > time) { return FindClosestBeforeKeyFrame(startIndex, middle - startIndex); } else { return middle; } } return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count); } /// /// Runs the KeyFrames Animation. /// internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete) { var instance = new AnimationInstance( animation, control, this, clock ?? control.Clock ?? Clock.GlobalClock, onComplete, DoInterpolation); return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } /// /// Interpolates a value given the desired time. /// protected abstract T DoInterpolation(double time, T neutralValue); /// /// Verifies, converts and sorts keyframe values according to this class's target type. /// private void VerifyConvertKeyFrames() { foreach (AnimatorKeyFrame keyframe in this) { _convertedKeyframes.Add(keyframe); } AddNeutralKeyFramesIfNeeded(); _isVerifiedAndConverted = true; } private void AddNeutralKeyFramesIfNeeded() { bool hasStartKey, hasEndKey; hasStartKey = hasEndKey = false; // Check if there's start and end keyframes. foreach (var frame in _convertedKeyframes) { if (frame.Cue.CueValue == 0.0d) { hasStartKey = true; } else if (frame.Cue.CueValue == 1.0d) { hasEndKey = true; } } if (!hasStartKey || !hasEndKey) AddNeutralKeyFrames(hasStartKey, hasEndKey); } private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey) { if (!hasStartKey) { _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true }); } if (!hasEndKey) { _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(1.0d)) { Value = default(T), isNeutral = true }); } } } }