524 lines
18 KiB
C#
524 lines
18 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
using System.Windows;
|
|
using System.Windows.Markup;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using HandyControl.Tools;
|
|
|
|
// ReSharper disable PossibleInvalidOperationException
|
|
|
|
namespace HandyControl.Media.Animation
|
|
{
|
|
[ContentProperty("KeyFrames")]
|
|
public class GeometryAnimationUsingKeyFrames : GeometryAnimationBase, IKeyFrameAnimation, IAddChild
|
|
{
|
|
private string[] _strings;
|
|
|
|
public string[] Strings
|
|
{
|
|
get
|
|
{
|
|
if (_keyFrames == null || _keyFrames.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return _strings ??= Regex.Split(_keyFrames[0].Value.ToString(CultureInfo.InvariantCulture), RegularPatterns.DigitsPattern);
|
|
}
|
|
}
|
|
|
|
private GeometryKeyFrameCollection _keyFrames;
|
|
|
|
private ResolvedKeyFrameEntry[] _sortedResolvedKeyFrames;
|
|
|
|
private bool _areKeyTimesValid;
|
|
|
|
public GeometryAnimationUsingKeyFrames()
|
|
{
|
|
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
|
_areKeyTimesValid = true;
|
|
}
|
|
|
|
public new GeometryAnimationUsingKeyFrames Clone() => (GeometryAnimationUsingKeyFrames)base.Clone();
|
|
|
|
public new GeometryAnimationUsingKeyFrames CloneCurrentValue() => (GeometryAnimationUsingKeyFrames)base.CloneCurrentValue();
|
|
|
|
protected override bool FreezeCore(bool isChecking)
|
|
{
|
|
var canFreeze = base.FreezeCore(isChecking);
|
|
|
|
canFreeze &= Freeze(_keyFrames, isChecking);
|
|
|
|
if (canFreeze & !_areKeyTimesValid)
|
|
{
|
|
ResolveKeyTimes();
|
|
}
|
|
|
|
return canFreeze;
|
|
}
|
|
|
|
protected override void OnChanged()
|
|
{
|
|
_areKeyTimesValid = false;
|
|
|
|
base.OnChanged();
|
|
}
|
|
|
|
protected override Freezable CreateInstanceCore() => new GeometryAnimationUsingKeyFrames();
|
|
|
|
protected override void CloneCore(Freezable sourceFreezable)
|
|
{
|
|
var sourceAnimation = (GeometryAnimationUsingKeyFrames)sourceFreezable;
|
|
base.CloneCore(sourceFreezable);
|
|
|
|
CopyCommon(sourceAnimation, false);
|
|
}
|
|
|
|
protected override void CloneCurrentValueCore(Freezable sourceFreezable)
|
|
{
|
|
var sourceAnimation = (GeometryAnimationUsingKeyFrames)sourceFreezable;
|
|
base.CloneCurrentValueCore(sourceFreezable);
|
|
|
|
CopyCommon(sourceAnimation, true);
|
|
}
|
|
|
|
protected override void GetAsFrozenCore(Freezable source)
|
|
{
|
|
var sourceAnimation = (GeometryAnimationUsingKeyFrames)source;
|
|
base.GetAsFrozenCore(source);
|
|
|
|
CopyCommon(sourceAnimation, false);
|
|
}
|
|
|
|
protected override void GetCurrentValueAsFrozenCore(Freezable source)
|
|
{
|
|
var sourceAnimation = (GeometryAnimationUsingKeyFrames)source;
|
|
base.GetCurrentValueAsFrozenCore(source);
|
|
|
|
CopyCommon(sourceAnimation, true);
|
|
}
|
|
|
|
private void CopyCommon(GeometryAnimationUsingKeyFrames sourceAnimation, bool isCurrentValueClone)
|
|
{
|
|
_areKeyTimesValid = sourceAnimation._areKeyTimesValid;
|
|
|
|
if (_areKeyTimesValid && sourceAnimation._sortedResolvedKeyFrames != null)
|
|
{
|
|
_sortedResolvedKeyFrames = (ResolvedKeyFrameEntry[])sourceAnimation._sortedResolvedKeyFrames.Clone();
|
|
}
|
|
|
|
if (sourceAnimation._keyFrames != null)
|
|
{
|
|
if (isCurrentValueClone)
|
|
{
|
|
_keyFrames = (GeometryKeyFrameCollection)sourceAnimation._keyFrames.CloneCurrentValue();
|
|
}
|
|
else
|
|
{
|
|
_keyFrames = sourceAnimation._keyFrames.Clone();
|
|
}
|
|
|
|
OnFreezablePropertyChanged(null, _keyFrames);
|
|
}
|
|
}
|
|
|
|
void IAddChild.AddChild(object child)
|
|
{
|
|
WritePreamble();
|
|
|
|
if (child == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(child));
|
|
}
|
|
|
|
AddChild(child);
|
|
|
|
WritePostscript();
|
|
}
|
|
|
|
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
|
protected virtual void AddChild(object child)
|
|
{
|
|
if (child is GeometryKeyFrame keyFrame)
|
|
{
|
|
KeyFrames.Add(keyFrame);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException("Animation_ChildMustBeKeyFrame", nameof(child));
|
|
}
|
|
}
|
|
|
|
void IAddChild.AddText(string childText)
|
|
{
|
|
if (childText == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(childText));
|
|
}
|
|
|
|
AddText(childText);
|
|
}
|
|
|
|
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
|
protected virtual void AddText(string childText) => throw new InvalidOperationException("Animation_NoTextChildren");
|
|
|
|
protected override Geometry GetCurrentValueCore(Geometry defaultOriginValue, Geometry defaultDestinationValue, AnimationClock animationClock)
|
|
{
|
|
if (_keyFrames == null)
|
|
{
|
|
return defaultDestinationValue;
|
|
}
|
|
|
|
if (!_areKeyTimesValid)
|
|
{
|
|
ResolveKeyTimes();
|
|
}
|
|
|
|
if (_sortedResolvedKeyFrames == null)
|
|
{
|
|
return defaultDestinationValue;
|
|
}
|
|
|
|
var currentTime = animationClock.CurrentTime.Value;
|
|
var keyFrameCount = _sortedResolvedKeyFrames.Length;
|
|
var maxKeyFrameIndex = keyFrameCount - 1;
|
|
|
|
double[] currentIterationValue;
|
|
|
|
var currentResolvedKeyFrameIndex = 0;
|
|
|
|
while (currentResolvedKeyFrameIndex < keyFrameCount && currentTime > _sortedResolvedKeyFrames[currentResolvedKeyFrameIndex]._resolvedKeyTime)
|
|
{
|
|
currentResolvedKeyFrameIndex++;
|
|
}
|
|
|
|
while (currentResolvedKeyFrameIndex < maxKeyFrameIndex && currentTime == _sortedResolvedKeyFrames[currentResolvedKeyFrameIndex + 1]._resolvedKeyTime)
|
|
{
|
|
currentResolvedKeyFrameIndex++;
|
|
}
|
|
|
|
if (currentResolvedKeyFrameIndex == keyFrameCount)
|
|
{
|
|
currentIterationValue = GetResolvedKeyFrameValue(maxKeyFrameIndex);
|
|
}
|
|
else if (currentTime == _sortedResolvedKeyFrames[currentResolvedKeyFrameIndex]._resolvedKeyTime)
|
|
{
|
|
currentIterationValue = GetResolvedKeyFrameValue(currentResolvedKeyFrameIndex);
|
|
}
|
|
else
|
|
{
|
|
double currentSegmentProgress;
|
|
double[] fromValue;
|
|
|
|
if (currentResolvedKeyFrameIndex == 0)
|
|
{
|
|
AnimationHelper.DecomposeGeometryStr(defaultOriginValue.ToString(CultureInfo.InvariantCulture), out fromValue);
|
|
|
|
currentSegmentProgress = currentTime.TotalMilliseconds / _sortedResolvedKeyFrames[0]._resolvedKeyTime.TotalMilliseconds;
|
|
}
|
|
else
|
|
{
|
|
var previousResolvedKeyFrameIndex = currentResolvedKeyFrameIndex - 1;
|
|
var previousResolvedKeyTime = _sortedResolvedKeyFrames[previousResolvedKeyFrameIndex]._resolvedKeyTime;
|
|
|
|
fromValue = GetResolvedKeyFrameValue(previousResolvedKeyFrameIndex);
|
|
|
|
var segmentCurrentTime = currentTime - previousResolvedKeyTime;
|
|
var segmentDuration = _sortedResolvedKeyFrames[currentResolvedKeyFrameIndex]._resolvedKeyTime - previousResolvedKeyTime;
|
|
|
|
currentSegmentProgress = segmentCurrentTime.TotalMilliseconds / segmentDuration.TotalMilliseconds;
|
|
}
|
|
|
|
currentIterationValue = GetResolvedKeyFrame(currentResolvedKeyFrameIndex).InterpolateValue(fromValue, currentSegmentProgress);
|
|
}
|
|
|
|
return AnimationHelper.ComposeGeometry(Strings, currentIterationValue);
|
|
}
|
|
|
|
protected sealed override Duration GetNaturalDurationCore(Clock clock) => new Duration(LargestTimeSpanKeyTime);
|
|
|
|
IList IKeyFrameAnimation.KeyFrames
|
|
{
|
|
get => KeyFrames;
|
|
set => KeyFrames = (GeometryKeyFrameCollection)value;
|
|
}
|
|
|
|
public GeometryKeyFrameCollection KeyFrames
|
|
{
|
|
get
|
|
{
|
|
ReadPreamble();
|
|
|
|
if (_keyFrames == null)
|
|
{
|
|
if (IsFrozen)
|
|
{
|
|
_keyFrames = GeometryKeyFrameCollection.Empty;
|
|
}
|
|
else
|
|
{
|
|
WritePreamble();
|
|
|
|
_keyFrames = new GeometryKeyFrameCollection();
|
|
|
|
OnFreezablePropertyChanged(null, _keyFrames);
|
|
|
|
WritePostscript();
|
|
}
|
|
}
|
|
|
|
return _keyFrames;
|
|
}
|
|
set
|
|
{
|
|
if (value == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(value));
|
|
}
|
|
|
|
WritePreamble();
|
|
|
|
if (value != _keyFrames)
|
|
{
|
|
OnFreezablePropertyChanged(_keyFrames, value);
|
|
_keyFrames = value;
|
|
|
|
WritePostscript();
|
|
}
|
|
}
|
|
}
|
|
|
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
public bool ShouldSerializeKeyFrames()
|
|
{
|
|
ReadPreamble();
|
|
|
|
return _keyFrames != null && _keyFrames.Count > 0;
|
|
}
|
|
|
|
private struct KeyTimeBlock
|
|
{
|
|
public int BeginIndex;
|
|
public int EndIndex;
|
|
}
|
|
|
|
private double[] GetResolvedKeyFrameValue(int resolvedKeyFrameIndex) =>
|
|
GetResolvedKeyFrame(resolvedKeyFrameIndex).Numbers;
|
|
|
|
private GeometryKeyFrame GetResolvedKeyFrame(int resolvedKeyFrameIndex) =>
|
|
_keyFrames[_sortedResolvedKeyFrames[resolvedKeyFrameIndex]._originalKeyFrameIndex];
|
|
|
|
private TimeSpan LargestTimeSpanKeyTime
|
|
{
|
|
get
|
|
{
|
|
var hasTimeSpanKeyTime = false;
|
|
var largestTimeSpanKeyTime = TimeSpan.Zero;
|
|
|
|
if (_keyFrames != null)
|
|
{
|
|
var keyFrameCount = _keyFrames.Count;
|
|
|
|
for (var index = 0; index < keyFrameCount; index++)
|
|
{
|
|
var keyTime = _keyFrames[index].KeyTime;
|
|
|
|
if (keyTime.Type == KeyTimeType.TimeSpan)
|
|
{
|
|
hasTimeSpanKeyTime = true;
|
|
|
|
if (keyTime.TimeSpan > largestTimeSpanKeyTime)
|
|
{
|
|
largestTimeSpanKeyTime = keyTime.TimeSpan;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return hasTimeSpanKeyTime ? largestTimeSpanKeyTime : TimeSpan.FromSeconds(1.0);
|
|
}
|
|
}
|
|
|
|
private void ResolveKeyTimes()
|
|
{
|
|
var keyFrameCount = 0;
|
|
|
|
if (_keyFrames != null)
|
|
{
|
|
keyFrameCount = _keyFrames.Count;
|
|
}
|
|
|
|
if (keyFrameCount == 0)
|
|
{
|
|
_sortedResolvedKeyFrames = null;
|
|
_areKeyTimesValid = true;
|
|
return;
|
|
}
|
|
|
|
_sortedResolvedKeyFrames = new ResolvedKeyFrameEntry[keyFrameCount];
|
|
|
|
var index = 0;
|
|
|
|
for (; index < keyFrameCount; index++)
|
|
{
|
|
_sortedResolvedKeyFrames[index]._originalKeyFrameIndex = index;
|
|
}
|
|
|
|
var duration = Duration;
|
|
var calculationDuration = duration.HasTimeSpan ? duration.TimeSpan : LargestTimeSpanKeyTime;
|
|
var maxKeyFrameIndex = keyFrameCount - 1;
|
|
var unspecifiedBlocks = new List<KeyTimeBlock>();
|
|
var hasPacedKeyTimes = false;
|
|
|
|
index = 0;
|
|
while (index < keyFrameCount)
|
|
{
|
|
// ReSharper disable once PossibleNullReferenceException
|
|
var keyTime = _keyFrames[index].KeyTime;
|
|
|
|
switch (keyTime.Type)
|
|
{
|
|
case KeyTimeType.Percent:
|
|
|
|
_sortedResolvedKeyFrames[index]._resolvedKeyTime = TimeSpan.FromMilliseconds(keyTime.Percent * calculationDuration.TotalMilliseconds);
|
|
index++;
|
|
break;
|
|
|
|
case KeyTimeType.TimeSpan:
|
|
|
|
_sortedResolvedKeyFrames[index]._resolvedKeyTime = keyTime.TimeSpan;
|
|
|
|
index++;
|
|
break;
|
|
|
|
case KeyTimeType.Paced:
|
|
case KeyTimeType.Uniform:
|
|
|
|
if (index == maxKeyFrameIndex)
|
|
{
|
|
_sortedResolvedKeyFrames[index]._resolvedKeyTime = calculationDuration;
|
|
index++;
|
|
}
|
|
else if (index == 0 && keyTime.Type == KeyTimeType.Paced)
|
|
{
|
|
_sortedResolvedKeyFrames[index]._resolvedKeyTime = TimeSpan.Zero;
|
|
index++;
|
|
}
|
|
else
|
|
{
|
|
if (keyTime.Type == KeyTimeType.Paced)
|
|
{
|
|
hasPacedKeyTimes = true;
|
|
}
|
|
|
|
var block = new KeyTimeBlock
|
|
{
|
|
BeginIndex = index
|
|
};
|
|
|
|
while (++index < maxKeyFrameIndex)
|
|
{
|
|
var type = _keyFrames[index].KeyTime.Type;
|
|
|
|
if (type == KeyTimeType.Percent || type == KeyTimeType.TimeSpan)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (type == KeyTimeType.Paced)
|
|
{
|
|
hasPacedKeyTimes = true;
|
|
}
|
|
}
|
|
|
|
block.EndIndex = index;
|
|
unspecifiedBlocks.Add(block);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach (var block in unspecifiedBlocks)
|
|
{
|
|
var blockBeginTime = TimeSpan.Zero;
|
|
|
|
if (block.BeginIndex > 0)
|
|
{
|
|
blockBeginTime = _sortedResolvedKeyFrames[block.BeginIndex - 1]._resolvedKeyTime;
|
|
}
|
|
|
|
long segmentCount = block.EndIndex - block.BeginIndex + 1;
|
|
var uniformTimeStep = TimeSpan.FromTicks((_sortedResolvedKeyFrames[block.EndIndex]._resolvedKeyTime - blockBeginTime).Ticks / segmentCount);
|
|
|
|
index = block.BeginIndex;
|
|
var resolvedTime = blockBeginTime + uniformTimeStep;
|
|
|
|
while (index < block.EndIndex)
|
|
{
|
|
_sortedResolvedKeyFrames[index]._resolvedKeyTime = resolvedTime;
|
|
|
|
resolvedTime += uniformTimeStep;
|
|
index++;
|
|
}
|
|
}
|
|
|
|
if (hasPacedKeyTimes)
|
|
{
|
|
ResolvePacedKeyTimes();
|
|
}
|
|
|
|
Array.Sort(_sortedResolvedKeyFrames);
|
|
|
|
_areKeyTimesValid = true;
|
|
}
|
|
|
|
private void ResolvePacedKeyTimes()
|
|
{
|
|
var index = 1;
|
|
var maxKeyFrameIndex = _sortedResolvedKeyFrames.Length - 1;
|
|
|
|
do
|
|
{
|
|
if (_keyFrames[index].KeyTime.Type == KeyTimeType.Paced)
|
|
{
|
|
var firstPacedBlockKeyFrameIndex = index;
|
|
|
|
var segmentLengths = new List<double>();
|
|
var prePacedBlockKeyTime = _sortedResolvedKeyFrames[index - 1]._resolvedKeyTime;
|
|
var totalLength = 0.0;
|
|
var prevKeyValue = _keyFrames[index - 1].Numbers;
|
|
|
|
do
|
|
{
|
|
var currentKeyValue = _keyFrames[index].Numbers;
|
|
totalLength += Math.Abs(currentKeyValue[0] - prevKeyValue[0]);
|
|
segmentLengths.Add(totalLength);
|
|
prevKeyValue = currentKeyValue;
|
|
index++;
|
|
}
|
|
while (index < maxKeyFrameIndex && _keyFrames[index].KeyTime.Type == KeyTimeType.Paced);
|
|
totalLength += Math.Abs(_keyFrames[index].Numbers[0] - prevKeyValue[0]);
|
|
|
|
var pacedBlockDuration = _sortedResolvedKeyFrames[index]._resolvedKeyTime - prePacedBlockKeyTime;
|
|
|
|
for (int i = 0, currentKeyFrameIndex = firstPacedBlockKeyFrameIndex; i < segmentLengths.Count; i++, currentKeyFrameIndex++)
|
|
{
|
|
_sortedResolvedKeyFrames[currentKeyFrameIndex]._resolvedKeyTime = prePacedBlockKeyTime + TimeSpan.FromMilliseconds(segmentLengths[i] / totalLength * pacedBlockDuration.TotalMilliseconds);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
index++;
|
|
}
|
|
}
|
|
while (index < maxKeyFrameIndex);
|
|
}
|
|
}
|
|
}
|