Browse Source
*Implement DoubleKeyFrames for Properties such as Opacity, etc. *Implement TransformKeyFrames with initial implementation of specialized logic for selecting RenderTransform of the target control properly. *Ported RenderTest to .NET Core for testing and to remove the crufty old .csproj format. *Replaced AnimationsPage with some samples of hover-activated animated red rectangles.pull/1461/head
65 changed files with 756 additions and 247 deletions
@ -1,2 +1,72 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui"> |
|||
<Grid> |
|||
<Grid.Styles> |
|||
<Styles> |
|||
<Style Selector="Rectangle.Test"> |
|||
<Setter Property="Fill" Value="Red"/> |
|||
<Setter Property="Margin" Value="15"/> |
|||
<Setter Property="Width" Value="100"/> |
|||
<Setter Property="Height" Value="100"/> |
|||
</Style> |
|||
|
|||
<Style Selector="Rectangle.Rect1:pointerover"> |
|||
<Style.Animations> |
|||
<Animation Duration="0:0:2.5" Easing="BounceEaseInOut"> |
|||
<TransformKeyFrames Property="RotateTransform.Angle"> |
|||
<KeyFrame Cue="0%" Value="0"/> |
|||
<KeyFrame Cue="100%" Value="360"/> |
|||
</TransformKeyFrames> |
|||
</Animation> |
|||
</Style.Animations> |
|||
</Style> |
|||
|
|||
<Style Selector="Rectangle.Rect2:pointerover"> |
|||
<Style.Animations> |
|||
<Animation Duration="0:0:0.5" Easing="SineEaseInOut"> |
|||
<TransformKeyFrames Property="ScaleTransform.ScaleX"> |
|||
<KeyFrame Cue="0%" Value="0.8"/> |
|||
<KeyFrame Cue="100%" Value="1"/> |
|||
</TransformKeyFrames> |
|||
<TransformKeyFrames Property="ScaleTransform.ScaleY"> |
|||
<KeyFrame Cue="0%" Value="0.8"/> |
|||
<KeyFrame Cue="100%" Value="1"/> |
|||
</TransformKeyFrames> |
|||
</Animation> |
|||
</Style.Animations> |
|||
</Style> |
|||
|
|||
<Style Selector="Rectangle.Rect3:pointerover"> |
|||
<Style.Animations> |
|||
<Animation Duration="0:0:3" Easing="BounceEaseInOut"> |
|||
<TransformKeyFrames Property="TranslateTransform.Y"> |
|||
<KeyFrame Cue="0%" Value="0"/> |
|||
<KeyFrame Cue="50%" Value="-100"/> |
|||
<KeyFrame Cue="100%" Value="0"/> |
|||
</TransformKeyFrames> |
|||
</Animation> |
|||
</Style.Animations> |
|||
</Style> |
|||
</Styles> |
|||
</Grid.Styles> |
|||
<StackPanel VerticalAlignment="Center" |
|||
HorizontalAlignment="Center" |
|||
Orientation="Horizontal" |
|||
ClipToBounds="False"> |
|||
<Rectangle Classes="Test Rect1"> |
|||
<Rectangle.RenderTransform> |
|||
<RotateTransform/> |
|||
</Rectangle.RenderTransform> |
|||
</Rectangle> |
|||
<Rectangle Classes="Test Rect2"> |
|||
<Rectangle.RenderTransform> |
|||
<ScaleTransform ScaleX="0.8" ScaleY="0.8"/> |
|||
</Rectangle.RenderTransform> |
|||
</Rectangle> |
|||
<Rectangle Classes="Test Rect3"> |
|||
<Rectangle.RenderTransform> |
|||
<TranslateTransform/> |
|||
</Rectangle.RenderTransform> |
|||
</Rectangle> |
|||
</StackPanel> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -1,55 +1,72 @@ |
|||
// 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 Avalonia.Animation.Easings; |
|||
using Avalonia.Animation.Keyframes; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Metadata; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
/// <summary>
|
|||
/// Tracks the progress of an animation.
|
|||
/// </summary>
|
|||
public class Animation : IObservable<object>, IDisposable |
|||
public class Animation : IDisposable, IAnimation |
|||
{ |
|||
private List<IDisposable>_subscription = new List<IDisposable>(); |
|||
|
|||
/// <summary>
|
|||
/// The animation being tracked.
|
|||
/// Run time of this animation.
|
|||
/// </summary>
|
|||
private readonly IObservable<object> _inner; |
|||
public TimeSpan Duration { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The disposable used to cancel the animation.
|
|||
/// Delay time for animation.
|
|||
/// </summary>
|
|||
private readonly IDisposable _subscription; |
|||
public TimeSpan Delay { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Easing function to be used.
|
|||
/// </summary>
|
|||
public Easing Easing { get; set; } = new LinearEasing(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Animation"/> class.
|
|||
/// A list of <see cref="IKeyFrames"/> objects.
|
|||
/// </summary>
|
|||
/// <param name="inner">The animation observable being tracked.</param>
|
|||
/// <param name="subscription">A disposable used to cancel the animation.</param>
|
|||
public Animation(IObservable<object> inner, IDisposable subscription) |
|||
{ |
|||
_inner = inner; |
|||
_subscription = subscription; |
|||
} |
|||
[Content] |
|||
public AvaloniaList<IKeyFrames> Children { get; set; } = new AvaloniaList<IKeyFrames>(); |
|||
|
|||
/// <summary>
|
|||
/// Cancels the animation.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
_subscription.Dispose(); |
|||
foreach(var sub in _subscription) sub.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Notifies the provider that an observer is to receive notifications.
|
|||
/// </summary>
|
|||
/// <param name="observer">The observer.</param>
|
|||
/// <returns>
|
|||
/// A reference to an interface that allows observers to stop receiving notifications
|
|||
/// before the provider has finished sending them.
|
|||
/// </returns>
|
|||
public IDisposable Subscribe(IObserver<object> observer) |
|||
/// <inheritdocs/>
|
|||
public IDisposable Apply(Animatable control, IObservable<bool> matchObs) |
|||
{ |
|||
return _inner.Subscribe(observer); |
|||
foreach (IKeyFrames keyframes in Children) |
|||
{ |
|||
_subscription.Add(keyframes.Apply(this, control, matchObs)); |
|||
} |
|||
return this; |
|||
} |
|||
|
|||
///// <summary>
|
|||
///// Notifies the provider that an observer is to receive notifications.
|
|||
///// </summary>
|
|||
///// <param name="observer">The observer.</param>
|
|||
///// <returns>
|
|||
///// A reference to an interface that allows observers to stop receiving notifications
|
|||
///// before the provider has finished sending them.
|
|||
///// </returns>
|
|||
//public IDisposable Subscribe(IObserver<object> observer)
|
|||
//{
|
|||
// return _inner.Subscribe(observer);
|
|||
//}
|
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
/// <summary>
|
|||
/// Interface for Animation objects
|
|||
/// </summary>
|
|||
public interface IAnimation |
|||
{ |
|||
/// <summary>
|
|||
/// Apply the animation to the specified control
|
|||
/// </summary>
|
|||
IDisposable Apply(Animatable control, IObservable<bool> match); |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Animation.Keyframes |
|||
{ |
|||
/// <summary>
|
|||
/// A Cue object for <see cref="KeyFrame"/>.
|
|||
/// </summary>
|
|||
[TypeConverter(typeof(CueTypeConverter))] |
|||
public struct Cue : IEquatable<Cue>, IEquatable<double> |
|||
{ |
|||
/// <summary>
|
|||
/// The normalized percent value, ranging from 0.0 to 1.0
|
|||
/// </summary>
|
|||
public double CueValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Sets a new <see cref="Cue"/> object.
|
|||
/// </summary>
|
|||
/// <param name="value"></param>
|
|||
public Cue(double value) |
|||
{ |
|||
if (value <= 1 && value >= 0) |
|||
CueValue = value; |
|||
else |
|||
throw new ArgumentException($"This cue object's value should be within or equal to 0.0 and 1.0"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses a string to a <see cref="Cue"/> object.
|
|||
/// </summary>
|
|||
public static object Parse(string value, CultureInfo culture) |
|||
{ |
|||
string v = value; |
|||
|
|||
if (value.EndsWith("%")) |
|||
{ |
|||
v = v.TrimEnd('%'); |
|||
} |
|||
|
|||
if (double.TryParse(v, NumberStyles.Float, culture, out double res)) |
|||
{ |
|||
return new Cue(res / 100d); |
|||
} |
|||
else |
|||
{ |
|||
throw new FormatException($"Invalid Cue string \"{value}\""); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks for equality between two <see cref="Cue"/>s.
|
|||
/// </summary>
|
|||
/// <param name="other">The second cue.</param>
|
|||
public bool Equals(Cue other) |
|||
{ |
|||
return CueValue == other.CueValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks for equality between a <see cref="Cue"/>
|
|||
/// and a <see cref="double"/> value.
|
|||
/// </summary>
|
|||
/// <param name="other"></param>
|
|||
/// <returns></returns>
|
|||
public bool Equals(double other) |
|||
{ |
|||
return CueValue == other; |
|||
} |
|||
} |
|||
|
|||
public class CueTypeConverter : TypeConverter |
|||
{ |
|||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|||
{ |
|||
return sourceType == typeof(string); |
|||
} |
|||
|
|||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|||
{ |
|||
return Cue.Parse((string)value, culture); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using System.Diagnostics; |
|||
using Avalonia.Animation.Utils; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Animation.Keyframes |
|||
{ |
|||
/// <summary>
|
|||
/// Key frames that handles <see cref="double"/> properties.
|
|||
/// </summary>
|
|||
public class DoubleKeyFrames : KeyFrames<double> |
|||
{ |
|||
/// <inheritdocs/>
|
|||
public override IDisposable DoInterpolation(Animation animation, Animatable control, Dictionary<double, double> sortedkeyValues) |
|||
{ |
|||
var timer = Timing.GetTimer(animation.Duration, animation.Delay); |
|||
|
|||
|
|||
var interp = timer.Select(p => |
|||
{ |
|||
// Handle the errors rather naively, for now.
|
|||
try |
|||
{ |
|||
var x = animation.Easing.Ease(p); |
|||
|
|||
// Get a pair of keyframes to make the interpolation.
|
|||
KeyValuePair<double, double> firstCue, lastCue; |
|||
|
|||
firstCue = sortedkeyValues.First(); |
|||
lastCue = sortedkeyValues.Last(); |
|||
|
|||
// This should be changed later for a much more efficient one
|
|||
if (sortedkeyValues.Count() > 2) |
|||
{ |
|||
bool isWithinRange_Start = DoubleUtils.AboutEqual(x, 0.0) || x > 0.0; |
|||
bool isWithinRange_End = DoubleUtils.AboutEqual(x, 1.0) || x < 1.0; |
|||
|
|||
if (isWithinRange_Start && isWithinRange_End) |
|||
{ |
|||
|
|||
firstCue = sortedkeyValues.Where(j => j.Key <= x).Last(); |
|||
lastCue = sortedkeyValues.Where(j=> j.Key >= firstCue.Key).First(); |
|||
} |
|||
else if (!isWithinRange_Start) |
|||
{ |
|||
firstCue = sortedkeyValues.First(); |
|||
lastCue = sortedkeyValues.Skip(1).First(); |
|||
} |
|||
else if (!isWithinRange_End) |
|||
{ |
|||
firstCue = sortedkeyValues.Skip(sortedkeyValues.Count() - 1).First(); |
|||
lastCue = sortedkeyValues.Last(); |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException |
|||
($"Can't find KeyFrames within the specified Easing time {x}"); |
|||
} |
|||
} |
|||
|
|||
// Piecewise Linear interpolation, courtesy of wikipedia
|
|||
var y0 = firstCue.Value; |
|||
var x0 = firstCue.Key; |
|||
var y1 = lastCue.Value; |
|||
var x1 = lastCue.Key; |
|||
var y = ((y0 * (x1 - x)) + (y1 * (x - x0))) / x1 - x0; |
|||
|
|||
return y; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Debug.WriteLine(e); |
|||
return 1; |
|||
} |
|||
}); |
|||
|
|||
|
|||
return control.Bind(Property, interp.Select(p => (object)p), BindingPriority.Animation); |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Animation.Keyframes |
|||
{ |
|||
/// <summary>
|
|||
/// Interface for Keyframe group object
|
|||
/// </summary>
|
|||
public interface IKeyFrames |
|||
{ |
|||
/// <summary>
|
|||
/// Applies the current KeyFrame group to the specified control.
|
|||
/// </summary>
|
|||
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch); |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.ComponentModel; |
|||
|
|||
namespace Avalonia.Animation.Keyframes |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Stores data regarding a specific key
|
|||
/// point and value in an animation.
|
|||
/// </summary>
|
|||
public class KeyFrame |
|||
{ |
|||
internal bool timeSpanSet, cueSet; |
|||
|
|||
private TimeSpan _ktimeSpan; |
|||
private Cue _kCue; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the key time of this <see cref="KeyFrame"/>.
|
|||
/// </summary>
|
|||
/// <value>The key time.</value>
|
|||
public TimeSpan KeyTime |
|||
{ |
|||
get |
|||
{ |
|||
|
|||
return _ktimeSpan; |
|||
} |
|||
set |
|||
{ |
|||
if (cueSet) |
|||
{ |
|||
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); |
|||
} |
|||
timeSpanSet = true; |
|||
_ktimeSpan = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the cue of this <see cref="KeyFrame"/>.
|
|||
/// </summary>
|
|||
/// <value>The cue.</value>
|
|||
public Cue Cue |
|||
{ |
|||
get |
|||
{ |
|||
|
|||
return _kCue; |
|||
} |
|||
set |
|||
{ |
|||
if (timeSpanSet) |
|||
{ |
|||
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); |
|||
} |
|||
cueSet = true; |
|||
_kCue = value; |
|||
} |
|||
} |
|||
|
|||
|
|||
public object Value { get; set; } |
|||
|
|||
|
|||
///// <summary>
|
|||
///// Initializes a new instance of the <see cref="KeyFrame"/> class.
|
|||
///// </summary>
|
|||
//public KeyFrame()
|
|||
//{
|
|||
|
|||
//}
|
|||
|
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
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; |
|||
|
|||
namespace Avalonia.Animation.Keyframes |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for KeyFrames
|
|||
/// </summary>
|
|||
public abstract class KeyFrames<T> : AvaloniaList<KeyFrame>, IKeyFrames |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Target property.
|
|||
/// </summary>
|
|||
public AvaloniaProperty Property { get; set; } |
|||
|
|||
/// Enable if the derived class will do the verification of
|
|||
/// its keyframes.
|
|||
internal bool IsVerfifiedAndConverted; |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) |
|||
{ |
|||
if(obsMatch == null) return null; |
|||
|
|||
if (!IsVerfifiedAndConverted) |
|||
VerifyKeyFrames(animation, typeof(T)); |
|||
|
|||
return obsMatch |
|||
.Where(p => p == true) |
|||
.Subscribe(_ => DoInterpolation(animation, control, ConvertedValues)); |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Interpolates the given keyframes to the control.
|
|||
/// </summary>
|
|||
public abstract IDisposable DoInterpolation(Animation animation, |
|||
Animatable control, |
|||
Dictionary<double, T> keyValues); |
|||
|
|||
internal Dictionary<double, T> ConvertedValues = new Dictionary<double, T>(); |
|||
|
|||
/// <summary>
|
|||
/// Verifies keyframe value types.
|
|||
/// </summary>
|
|||
private void VerifyKeyFrames(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); |
|||
} |
|||
|
|||
ConvertedValues.Add(_normalizedCue.CueValue, convertedValue); |
|||
|
|||
} |
|||
|
|||
// This can be optional if we ever try to make
|
|||
// the default start and end values to be the
|
|||
// property's prior value.
|
|||
SortKeyFrameCues(ConvertedValues); |
|||
|
|||
IsVerfifiedAndConverted = true; |
|||
|
|||
} |
|||
|
|||
private void SortKeyFrameCues(Dictionary<double, T> convertedValues) |
|||
{ |
|||
SortKeyFrameCues(convertedValues.ToDictionary((k) => k.Key, (v) => (object)v.Value)); |
|||
} |
|||
|
|||
internal void SortKeyFrameCues(Dictionary<double, object> convertedValues) |
|||
{ |
|||
bool hasStartKey, hasEndKey; |
|||
hasStartKey = hasEndKey = false; |
|||
|
|||
foreach (var converted in ConvertedValues.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 they don't order it by themselves.
|
|||
ConvertedValues = ConvertedValues.OrderBy(p => p.Key) |
|||
.ToDictionary((k) => k.Key, (v) => v.Value); |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -1,12 +1,12 @@ |
|||
// 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.
|
|||
|
|||
namespace Avalonia.Animation |
|||
namespace Avalonia.Animation.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// Helper static class for BounceEase classes.
|
|||
/// </summary>
|
|||
internal static class BounceEaseHelper |
|||
internal static class BounceEaseUtils |
|||
{ |
|||
/// <summary>
|
|||
/// Returns the consequent <see cref="double"/> value of
|
|||
@ -0,0 +1,77 @@ |
|||
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.Media; |
|||
|
|||
namespace Avalonia.Animation.Keyframes |
|||
{ |
|||
/// <summary>
|
|||
/// Key frames that handles <see cref="double"/> properties.
|
|||
/// </summary>
|
|||
public class TransformKeyFrames : KeyFrames<double> |
|||
{ |
|||
DoubleKeyFrames childKeyFrames; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) |
|||
{ |
|||
var ctrl = (Visual)control; |
|||
|
|||
// Check if the AvaloniaProperty is Transform derived.
|
|||
if (typeof(Transform).IsAssignableFrom(Property.OwnerType)) |
|||
{ |
|||
var renderTransformType = ctrl.RenderTransform.GetType(); |
|||
|
|||
// It's only 1 transform object so let's target that.
|
|||
if (renderTransformType == Property.OwnerType) |
|||
{ |
|||
var targetTransform = Convert.ChangeType(ctrl.RenderTransform, Property.OwnerType); |
|||
|
|||
if (childKeyFrames == null) |
|||
{ |
|||
childKeyFrames = new DoubleKeyFrames(); |
|||
|
|||
foreach (KeyFrame k in this) |
|||
{ |
|||
childKeyFrames.Add(k); |
|||
} |
|||
|
|||
childKeyFrames.Property = Property; |
|||
} |
|||
|
|||
return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch); |
|||
} |
|||
if (renderTransformType == typeof(TransformGroup)) |
|||
{ |
|||
foreach (Transform t in ((TransformGroup)ctrl.RenderTransform).Children) |
|||
{ |
|||
if (renderTransformType == Property.OwnerType) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
// not existing in the transform
|
|||
|
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidProgramException($"Unsupported property {Property}"); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <inheritdocs/>
|
|||
public override IDisposable DoInterpolation(Animation animation, Animatable control, Dictionary<double, double> keyValues) |
|||
{ |
|||
return Timing.GetTimer(animation.Duration, animation.Delay).Subscribe(); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue