committed by
GitHub
57 changed files with 1873 additions and 463 deletions
@ -1,6 +1,6 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="SkiaSharp" Version="1.68.1" /> |
<PackageReference Include="SkiaSharp" Version="1.68.2" /> |
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" /> |
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -0,0 +1,349 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel; |
||||
|
using System.Globalization; |
||||
|
using System.Text; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
// Ported from WPF open-source code.
|
||||
|
// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
|
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Determines how an animation is used based on a cubic bezier curve.
|
||||
|
/// X1 and X2 must be between 0.0 and 1.0, inclusive.
|
||||
|
/// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline
|
||||
|
/// </summary>
|
||||
|
[TypeConverter(typeof(KeySplineTypeConverter))] |
||||
|
public class KeySpline : AvaloniaObject |
||||
|
{ |
||||
|
// Control points
|
||||
|
private double _controlPointX1; |
||||
|
private double _controlPointY1; |
||||
|
private double _controlPointX2; |
||||
|
private double _controlPointY2; |
||||
|
private bool _isSpecified; |
||||
|
private bool _isDirty; |
||||
|
|
||||
|
// The parameter that corresponds to the most recent time
|
||||
|
private double _parameter; |
||||
|
|
||||
|
// Cached coefficients
|
||||
|
private double _Bx; // 3*points[0].X
|
||||
|
private double _Cx; // 3*points[1].X
|
||||
|
private double _Cx_Bx; // 2*(Cx - Bx)
|
||||
|
private double _three_Cx; // 3 - Cx
|
||||
|
|
||||
|
private double _By; // 3*points[0].Y
|
||||
|
private double _Cy; // 3*points[1].Y
|
||||
|
|
||||
|
// constants
|
||||
|
private const double _accuracy = .001; // 1/3 the desired accuracy in X
|
||||
|
private const double _fuzz = .000001; // computational zero
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Create a <see cref="KeySpline"/> with X1 = Y1 = 0 and X2 = Y2 = 1.
|
||||
|
/// </summary>
|
||||
|
public KeySpline() |
||||
|
{ |
||||
|
_controlPointX1 = 0.0; |
||||
|
_controlPointY1 = 0.0; |
||||
|
_controlPointX2 = 1.0; |
||||
|
_controlPointY2 = 1.0; |
||||
|
_isDirty = true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Create a <see cref="KeySpline"/> with the given parameters
|
||||
|
/// </summary>
|
||||
|
/// <param name="x1">X coordinate for the first control point</param>
|
||||
|
/// <param name="y1">Y coordinate for the first control point</param>
|
||||
|
/// <param name="x2">X coordinate for the second control point</param>
|
||||
|
/// <param name="y2">Y coordinate for the second control point</param>
|
||||
|
public KeySpline(double x1, double y1, double x2, double y2) |
||||
|
{ |
||||
|
_controlPointX1 = x1; |
||||
|
_controlPointY1 = y1; |
||||
|
_controlPointX2 = x2; |
||||
|
_controlPointY2 = y2; |
||||
|
_isDirty = true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parse a <see cref="KeySpline"/> from a string. The string
|
||||
|
/// needs to contain 4 values in it for the 2 control points.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">string with 4 values in it</param>
|
||||
|
/// <param name="culture">culture of the string</param>
|
||||
|
/// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
|
||||
|
/// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
|
||||
|
public static KeySpline Parse(string value, CultureInfo culture) |
||||
|
{ |
||||
|
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) |
||||
|
{ |
||||
|
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// X coordinate of the first control point
|
||||
|
/// </summary>
|
||||
|
public double ControlPointX1 |
||||
|
{ |
||||
|
get => _controlPointX1; |
||||
|
set |
||||
|
{ |
||||
|
if (IsValidXValue(value)) |
||||
|
{ |
||||
|
_controlPointX1 = value; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Y coordinate of the first control point
|
||||
|
/// </summary>
|
||||
|
public double ControlPointY1 |
||||
|
{ |
||||
|
get => _controlPointY1; |
||||
|
set => _controlPointY1 = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// X coordinate of the second control point
|
||||
|
/// </summary>
|
||||
|
public double ControlPointX2 |
||||
|
{ |
||||
|
get => _controlPointX2; |
||||
|
set |
||||
|
{ |
||||
|
if (IsValidXValue(value)) |
||||
|
{ |
||||
|
_controlPointX2 = value; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Y coordinate of the second control point
|
||||
|
/// </summary>
|
||||
|
public double ControlPointY2 |
||||
|
{ |
||||
|
get => _controlPointY2; |
||||
|
set => _controlPointY2 = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Calculates spline progress from a linear progress.
|
||||
|
/// </summary>
|
||||
|
/// <param name="linearProgress">the linear progress</param>
|
||||
|
/// <returns>the spline progress</returns>
|
||||
|
public double GetSplineProgress(double linearProgress) |
||||
|
{ |
||||
|
if (_isDirty) |
||||
|
{ |
||||
|
Build(); |
||||
|
} |
||||
|
|
||||
|
if (!_isSpecified) |
||||
|
{ |
||||
|
return linearProgress; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
SetParameterFromX(linearProgress); |
||||
|
|
||||
|
return GetBezierValue(_By, _Cy, _parameter); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Check to see whether the <see cref="KeySpline"/> is valid by looking
|
||||
|
/// at its X values.
|
||||
|
/// </summary>
|
||||
|
/// <returns>true if the X values for this <see cref="KeySpline"/> fall in
|
||||
|
/// acceptable range; false otherwise.</returns>
|
||||
|
public bool IsValid() |
||||
|
{ |
||||
|
return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
///
|
||||
|
/// </summary>
|
||||
|
/// <param name="value"></param>
|
||||
|
/// <returns></returns>
|
||||
|
private bool IsValidXValue(double value) |
||||
|
{ |
||||
|
return value >= 0.0 && value <= 1.0; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compute cached coefficients.
|
||||
|
/// </summary>
|
||||
|
private void Build() |
||||
|
{ |
||||
|
if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1) |
||||
|
{ |
||||
|
// This KeySpline would have no effect on the progress.
|
||||
|
_isSpecified = false; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_isSpecified = true; |
||||
|
|
||||
|
_parameter = 0; |
||||
|
|
||||
|
// X coefficients
|
||||
|
_Bx = 3 * _controlPointX1; |
||||
|
_Cx = 3 * _controlPointX2; |
||||
|
_Cx_Bx = 2 * (_Cx - _Bx); |
||||
|
_three_Cx = 3 - _Cx; |
||||
|
|
||||
|
// Y coefficients
|
||||
|
_By = 3 * _controlPointY1; |
||||
|
_Cy = 3 * _controlPointY2; |
||||
|
} |
||||
|
|
||||
|
_isDirty = false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get an X or Y value with the Bezier formula.
|
||||
|
/// </summary>
|
||||
|
/// <param name="b">the second Bezier coefficient</param>
|
||||
|
/// <param name="c">the third Bezier coefficient</param>
|
||||
|
/// <param name="t">the parameter value to evaluate at</param>
|
||||
|
/// <returns>the value of the Bezier function at the given parameter</returns>
|
||||
|
static private double GetBezierValue(double b, double c, double t) |
||||
|
{ |
||||
|
double s = 1.0 - t; |
||||
|
double t2 = t * t; |
||||
|
|
||||
|
return b * t * s * s + c * t2 * s + t2 * t; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get X and dX/dt at a given parameter
|
||||
|
/// </summary>
|
||||
|
/// <param name="t">the parameter value to evaluate at</param>
|
||||
|
/// <param name="x">the value of x there</param>
|
||||
|
/// <param name="dx">the value of dx/dt there</param>
|
||||
|
private void GetXAndDx(double t, out double x, out double dx) |
||||
|
{ |
||||
|
double s = 1.0 - t; |
||||
|
double t2 = t * t; |
||||
|
double s2 = s * s; |
||||
|
|
||||
|
x = _Bx * t * s2 + _Cx * t2 * s + t2 * t; |
||||
|
dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compute the parameter value that corresponds to a given X value, using a modified
|
||||
|
/// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make
|
||||
|
/// use of some known properties of this particular function:
|
||||
|
/// * We are only interested in solutions in the interval [0,1]
|
||||
|
/// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use
|
||||
|
/// that to clamp down the search interval with every probe.
|
||||
|
/// * The derivative of X and Y are between 0 and 3.
|
||||
|
/// </summary>
|
||||
|
/// <param name="time">the time, scaled to fit in [0,1]</param>
|
||||
|
private void SetParameterFromX(double time) |
||||
|
{ |
||||
|
// Dynamic search interval to clamp with
|
||||
|
double bottom = 0; |
||||
|
double top = 1; |
||||
|
|
||||
|
if (time == 0) |
||||
|
{ |
||||
|
_parameter = 0; |
||||
|
} |
||||
|
else if (time == 1) |
||||
|
{ |
||||
|
_parameter = 1; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Loop while improving the guess
|
||||
|
while (top - bottom > _fuzz) |
||||
|
{ |
||||
|
double x, dx, absdx; |
||||
|
|
||||
|
// Get x and dx/dt at the current parameter
|
||||
|
GetXAndDx(_parameter, out x, out dx); |
||||
|
absdx = Math.Abs(dx); |
||||
|
|
||||
|
// Clamp down the search interval, relying on the monotonicity of X(t)
|
||||
|
if (x > time) |
||||
|
{ |
||||
|
top = _parameter; // because parameter > solution
|
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
bottom = _parameter; // because parameter < solution
|
||||
|
} |
||||
|
|
||||
|
// The desired accuracy is in ultimately in y, not in x, so the
|
||||
|
// accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt).
|
||||
|
// But dy/dt <=3, so we omit that
|
||||
|
if (Math.Abs(x - time) < _accuracy * absdx) |
||||
|
{ |
||||
|
break; // We're there
|
||||
|
} |
||||
|
|
||||
|
if (absdx > _fuzz) |
||||
|
{ |
||||
|
// Nonzero derivative, use Newton-Raphson to obtain the next guess
|
||||
|
double next = _parameter - (x - time) / dx; |
||||
|
|
||||
|
// If next guess is out of the search interval then clamp it in
|
||||
|
if (next >= top) |
||||
|
{ |
||||
|
_parameter = (_parameter + top) / 2; |
||||
|
} |
||||
|
else if (next <= bottom) |
||||
|
{ |
||||
|
_parameter = (_parameter + bottom) / 2; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Next guess is inside the search interval, accept it
|
||||
|
_parameter = next; |
||||
|
} |
||||
|
} |
||||
|
else // Zero derivative, halve the search interval
|
||||
|
{ |
||||
|
_parameter = (bottom + top) / 2; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts string values to <see cref="KeySpline"/> values
|
||||
|
/// </summary>
|
||||
|
public class KeySplineTypeConverter : TypeConverter |
||||
|
{ |
||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
||||
|
{ |
||||
|
return sourceType == typeof(string); |
||||
|
} |
||||
|
|
||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
||||
|
{ |
||||
|
return KeySpline.Parse((string)value, culture); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,44 +0,0 @@ |
|||||
namespace Avalonia.Media.TextFormatting.Unicode |
|
||||
{ |
|
||||
public enum UnicodeGeneralCategory : byte |
|
||||
{ |
|
||||
Other, //C# Cc | Cf | Cn | Co | Cs
|
|
||||
Control, //Cc
|
|
||||
Format, //Cf
|
|
||||
Unassigned, //Cn
|
|
||||
PrivateUse, //Co
|
|
||||
Surrogate, //Cs
|
|
||||
Letter, //L# Ll | Lm | Lo | Lt | Lu
|
|
||||
CasedLetter, //LC# Ll | Lt | Lu
|
|
||||
LowercaseLetter, //Ll
|
|
||||
ModifierLetter, //Lm
|
|
||||
OtherLetter, //Lo
|
|
||||
TitlecaseLetter, //Lt
|
|
||||
UppercaseLetter, //Lu
|
|
||||
Mark, //M
|
|
||||
SpacingMark, //Mc
|
|
||||
EnclosingMark, //Me
|
|
||||
NonspacingMark, //Mn
|
|
||||
Number, //N# Nd | Nl | No
|
|
||||
DecimalNumber, //Nd
|
|
||||
LetterNumber, //Nl
|
|
||||
OtherNumber, //No
|
|
||||
Punctuation, //P
|
|
||||
ConnectorPunctuation, //Pc
|
|
||||
DashPunctuation, //Pd
|
|
||||
ClosePunctuation, //Pe
|
|
||||
FinalPunctuation, //Pf
|
|
||||
InitialPunctuation, //Pi
|
|
||||
OtherPunctuation, //Po
|
|
||||
OpenPunctuation, //Ps
|
|
||||
Symbol, //S# Sc | Sk | Sm | So
|
|
||||
CurrencySymbol, //Sc
|
|
||||
ModifierSymbol, //Sk
|
|
||||
MathSymbol, //Sm
|
|
||||
OtherSymbol, //So
|
|
||||
Separator, //Z# Zl | Zp | Zs
|
|
||||
LineSeparator, //Zl
|
|
||||
ParagraphSeparator, //Zp
|
|
||||
SpaceSeparator, //Zs
|
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,55 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using static Avalonia.Win32.Interop.UnmanagedMethods; |
||||
|
|
||||
|
namespace Avalonia.Win32.Interop |
||||
|
{ |
||||
|
internal class TaskBarList |
||||
|
{ |
||||
|
private static IntPtr s_taskBarList; |
||||
|
private static HrInit s_hrInitDelegate; |
||||
|
private static MarkFullscreenWindow s_markFullscreenWindowDelegate; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
|
||||
|
/// </summary>
|
||||
|
/// <param name="fullscreen">Fullscreen state.</param>
|
||||
|
public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen) |
||||
|
{ |
||||
|
if (s_taskBarList == IntPtr.Zero) |
||||
|
{ |
||||
|
Guid clsid = ShellIds.TaskBarList; |
||||
|
Guid iid = ShellIds.ITaskBarList2; |
||||
|
|
||||
|
int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList); |
||||
|
|
||||
|
if (s_taskBarList != IntPtr.Zero) |
||||
|
{ |
||||
|
var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); |
||||
|
|
||||
|
if (s_hrInitDelegate is null) |
||||
|
{ |
||||
|
s_hrInitDelegate = Marshal.GetDelegateForFunctionPointer<HrInit>((*ptr)->HrInit); |
||||
|
} |
||||
|
|
||||
|
if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK) |
||||
|
{ |
||||
|
s_taskBarList = IntPtr.Zero; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (s_taskBarList != IntPtr.Zero) |
||||
|
{ |
||||
|
var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); |
||||
|
|
||||
|
if (s_markFullscreenWindowDelegate is null) |
||||
|
{ |
||||
|
s_markFullscreenWindowDelegate = Marshal.GetDelegateForFunctionPointer<MarkFullscreenWindow>((*ptr)->MarkFullscreenWindow); |
||||
|
} |
||||
|
|
||||
|
s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using static Avalonia.Win32.Interop.UnmanagedMethods; |
||||
|
|
||||
|
namespace Avalonia.Win32 |
||||
|
{ |
||||
|
internal static class Win32TypeExtensions |
||||
|
{ |
||||
|
public static PixelRect ToPixelRect(this RECT rect) |
||||
|
{ |
||||
|
return new PixelRect(rect.left, rect.top, rect.right - rect.left, |
||||
|
rect.bottom - rect.top); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,145 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls.Shapes; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Styling; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Animation.UnitTests |
||||
|
{ |
||||
|
public class KeySplineTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData("1,2 3,4")] |
||||
|
[InlineData("1 2 3 4")] |
||||
|
[InlineData("1 2,3 4")] |
||||
|
[InlineData("1,2,3,4")] |
||||
|
public void Can_Parse_KeySpline_Via_TypeConverter(string input) |
||||
|
{ |
||||
|
var conv = new KeySplineTypeConverter(); |
||||
|
|
||||
|
var keySpline = (KeySpline)conv.ConvertFrom(input); |
||||
|
|
||||
|
Assert.Equal(1, keySpline.ControlPointX1); |
||||
|
Assert.Equal(2, keySpline.ControlPointY1); |
||||
|
Assert.Equal(3, keySpline.ControlPointX2); |
||||
|
Assert.Equal(4, keySpline.ControlPointY2); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0.00)] |
||||
|
[InlineData(0.50)] |
||||
|
[InlineData(1.00)] |
||||
|
public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input) |
||||
|
{ |
||||
|
var keySpline = new KeySpline(); |
||||
|
keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown
|
||||
|
keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown
|
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(-0.01)] |
||||
|
[InlineData(1.01)] |
||||
|
public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input) |
||||
|
{ |
||||
|
var keySpline = new KeySpline(); |
||||
|
Assert.Throws<ArgumentException>(() => keySpline.ControlPointX1 = input); |
||||
|
Assert.Throws<ArgumentException>(() => keySpline.ControlPointX2 = input); |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
To get the test values for the KeySpline test, you can: |
||||
|
1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations
|
||||
|
2) Add the following xaml somewhere: |
||||
|
<Button Content="Capture" |
||||
|
Click="Button_Click"/> |
||||
|
<ScrollViewer VerticalScrollBarVisibility="Visible"> |
||||
|
<TextBlock Name="CaptureData" |
||||
|
Text="---" |
||||
|
TextWrapping="Wrap" /> |
||||
|
</ScrollViewer> |
||||
|
3) Add the following code to the code behind: |
||||
|
private void Button_Click(object sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
CaptureData.Text += string.Format("\n{0} | {1}", myTranslateTransform3D.OffsetX, (TimeSpan)ExampleStoryboard.GetCurrentTime(this)); |
||||
|
CaptureData.Text += |
||||
|
"\nKeySpline=\"" + mySplineKeyFrame.KeySpline.ControlPoint1.X.ToString() + "," + |
||||
|
mySplineKeyFrame.KeySpline.ControlPoint1.Y.ToString() + " " + |
||||
|
mySplineKeyFrame.KeySpline.ControlPoint2.X.ToString() + "," + |
||||
|
mySplineKeyFrame.KeySpline.ControlPoint2.Y.ToString() + "\""; |
||||
|
CaptureData.Text += "\n-----"; |
||||
|
} |
||||
|
4) Run the app, mess with the slider values, then click the button to capture output values |
||||
|
**/ |
||||
|
|
||||
|
[Fact] |
||||
|
public void Check_KeySpline_Handled_properly() |
||||
|
{ |
||||
|
var keyframe1 = new KeyFrame() |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(RotateTransform.AngleProperty, -2.5d), |
||||
|
}, |
||||
|
KeyTime = TimeSpan.FromSeconds(0) |
||||
|
}; |
||||
|
|
||||
|
var keyframe2 = new KeyFrame() |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(RotateTransform.AngleProperty, 2.5d), |
||||
|
}, |
||||
|
KeyTime = TimeSpan.FromSeconds(5), |
||||
|
KeySpline = new KeySpline(0.1123555056179775, |
||||
|
0.657303370786517, |
||||
|
0.8370786516853934, |
||||
|
0.499999999999999999) |
||||
|
}; |
||||
|
|
||||
|
var animation = new Animation() |
||||
|
{ |
||||
|
Duration = TimeSpan.FromSeconds(5), |
||||
|
Children = |
||||
|
{ |
||||
|
keyframe1, |
||||
|
keyframe2 |
||||
|
}, |
||||
|
IterationCount = new IterationCount(5), |
||||
|
PlaybackDirection = PlaybackDirection.Alternate |
||||
|
}; |
||||
|
|
||||
|
var rotateTransform = new RotateTransform(-2.5); |
||||
|
var rect = new Rectangle() |
||||
|
{ |
||||
|
RenderTransform = rotateTransform |
||||
|
}; |
||||
|
|
||||
|
var clock = new TestClock(); |
||||
|
var animationRun = animation.RunAsync(rect, clock); |
||||
|
|
||||
|
// position is what you'd expect at end and beginning
|
||||
|
clock.Step(TimeSpan.Zero); |
||||
|
Assert.Equal(rotateTransform.Angle, -2.5); |
||||
|
clock.Step(TimeSpan.FromSeconds(5)); |
||||
|
Assert.Equal(rotateTransform.Angle, 2.5); |
||||
|
|
||||
|
// test some points in between end and beginning
|
||||
|
var tolerance = 0.01; |
||||
|
clock.Step(TimeSpan.Parse("00:00:10.0153932")); |
||||
|
var expected = -2.4122350198982545; |
||||
|
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); |
||||
|
|
||||
|
clock.Step(TimeSpan.Parse("00:00:11.2655407")); |
||||
|
expected = -0.37153223002125113; |
||||
|
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); |
||||
|
|
||||
|
clock.Step(TimeSpan.Parse("00:00:12.6158773")); |
||||
|
expected = 0.3967885416786294; |
||||
|
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); |
||||
|
|
||||
|
clock.Step(TimeSpan.Parse("00:00:14.6495256")); |
||||
|
expected = 1.8016358493761722; |
||||
|
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue