Handle rounded rectangles more accurately (#288)

* Rename to match After Effects and some initial refactoring.
* Correctly support roundness where size and roundness are not animated.
* Handle non-static rounded rectangles better.
* Also fixes a misunderstanding with how animations are stopped in Composition.
This commit is contained in:
Simeon 2020-06-11 16:18:08 -07:00 коммит произвёл GitHub
Родитель 29682524ff
Коммит 8432ee254d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 226 добавлений и 140 удалений

Просмотреть файл

@ -18,8 +18,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
Animatable<double> rotation,
Animatable<double> innerRadius,
Animatable<double> outerRadius,
Animatable<double> innerRoundedness,
Animatable<double> outerRoundedness)
Animatable<double> innerRoundness,
Animatable<double> outerRoundness)
: base(in args, drawingDirection)
{
StarType = starType;
@ -28,8 +28,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
Rotation = rotation;
InnerRadius = innerRadius;
OuterRadius = outerRadius;
InnerRoundedness = innerRoundedness;
OuterRoundedness = outerRoundedness;
InnerRoundness = innerRoundness;
OuterRoundness = outerRoundness;
}
internal PolyStarType StarType { get; }
@ -44,9 +44,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
internal Animatable<double> OuterRadius { get; }
internal Animatable<double> InnerRoundedness { get; }
internal Animatable<double> InnerRoundness { get; }
internal Animatable<double> OuterRoundedness { get; }
internal Animatable<double> OuterRoundness { get; }
/// <inheritdoc/>
public override ShapeContentType ContentType => ShapeContentType.Polystar;

Просмотреть файл

@ -14,15 +14,22 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
DrawingDirection drawingDirection,
IAnimatableVector3 position,
IAnimatableVector3 size,
Animatable<double> cornerRadius)
Animatable<double> roundness)
: base(in args, drawingDirection)
{
Position = position;
Size = size;
CornerRadius = cornerRadius;
Roundness = roundness;
}
public Animatable<double> CornerRadius { get; }
/// <summary>
/// Determines how round the corners of the rectangle are. If the rectangle
/// is a square and the roundness is equal to half of the width then the
/// rectangle will be rendered as a circle. Once the roundness value reaches
/// half of the minimum of the shortest dimension, increasing it has no
/// further effect.
/// </summary>
public Animatable<double> Roundness { get; }
public IAnimatableVector3 Size { get; }

Просмотреть файл

@ -17,6 +17,16 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
Radius = radius;
}
/// <summary>
/// The radius of the rounding.
/// </summary>
/// <remarks>
/// If the shape to which this applies is a rectangle, the rounding will
/// only apply if the rectangle has a 0 roundness value. Once the radius
/// value reaches half of the largest dimension of the rectangle, the
/// result will be equivalent to an ellipse of the same size, and
/// increasing the radius further will have no further effect.
/// </remarks>
public Animatable<double> Radius { get; }
/// <inheritdoc/>

Просмотреть файл

@ -616,7 +616,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
yield return FromAnimatable(nameof(content.Size), content.Size);
yield return FromAnimatable(nameof(content.Position), content.Position);
yield return FromAnimatable(nameof(content.CornerRadius), content.CornerRadius);
yield return FromAnimatable(nameof(content.Roundness), content.Roundness);
}
}

Просмотреть файл

@ -644,7 +644,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
var result = superclassContent;
result.Add(nameof(content.Size), FromAnimatable(content.Size));
result.Add(nameof(content.Position), FromAnimatable(content.Position));
result.Add(nameof(content.CornerRadius), FromAnimatable(content.CornerRadius));
result.Add(nameof(content.Roundness), FromAnimatable(content.Roundness));
return result;
}

Просмотреть файл

@ -326,23 +326,23 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
var position = ReadAnimatableVector3(obj.ObjectPropertyOrNull("p"));
var rotation = ReadAnimatableFloat(obj.ObjectPropertyOrNull("r"));
var outerRadius = ReadAnimatableFloat(obj.ObjectPropertyOrNull("or"));
var outerRoundedness = ReadAnimatableFloat(obj.ObjectPropertyOrNull("os"));
var outerRoundness = ReadAnimatableFloat(obj.ObjectPropertyOrNull("os"));
var polystarType = SyToPolystarType(obj.DoublePropertyOrNull("sy")) ?? Polystar.PolyStarType.Polygon;
Animatable<double> innerRadius;
Animatable<double> innerRoundedness;
Animatable<double> innerRoundness;
switch (polystarType)
{
case Polystar.PolyStarType.Star:
innerRadius = ReadAnimatableFloat(obj.ObjectPropertyOrNull("ir"));
innerRoundedness = ReadAnimatableFloat(obj.ObjectPropertyOrNull("is"));
innerRoundness = ReadAnimatableFloat(obj.ObjectPropertyOrNull("is"));
break;
default:
innerRadius = null;
innerRoundedness = null;
innerRoundness = null;
break;
}
@ -356,8 +356,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
rotation,
innerRadius,
outerRadius,
innerRoundedness,
outerRoundedness);
innerRoundness,
outerRoundness);
}
Rectangle ReadRectangle(
@ -370,10 +370,10 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
var drawingDirection = DToDrawingDirection(obj.DoublePropertyOrNull("d"));
var position = ReadAnimatableVector3(obj.ObjectPropertyOrNull("p"));
var size = ReadAnimatableVector3(obj.ObjectPropertyOrNull("s"));
var cornerRadius = ReadAnimatableFloat(obj.ObjectPropertyOrNull("r"));
var roundness = ReadAnimatableFloat(obj.ObjectPropertyOrNull("r"));
obj.AssertAllPropertiesRead();
return new Rectangle(in shapeLayerContentArgs, drawingDirection, position, size, cornerRadius);
return new Rectangle(in shapeLayerContentArgs, drawingDirection, position, size, roundness);
}
Path ReadPath(

Просмотреть файл

@ -27,6 +27,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
internal static readonly Vector2 MyPosition = MyVector2("Position");
internal static readonly Vector2 MySize = MyVector2("Size");
internal static readonly Matrix3x2 MyTransformMatrix = MyMatrix3x2("TransformMatrix");
static readonly Scalar MyRoundness = MyScalar("Roundness");
static readonly Scalar MyTStart = MyScalar("TStart");
static readonly Scalar MyTEnd = MyScalar("TEnd");
@ -67,6 +68,15 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
internal static Scalar RootScalar(string propertyName) => Scalar(RootProperty(propertyName));
internal static Vector2 ConstrainedCornerRadiusScalar(double roundness)
=> Vector2(Min(roundness, Min(MySize.X, MySize.Y) / 2), Min(roundness, Min(MySize.X, MySize.Y) / 2));
internal static Vector2 ConstrainedCornerRadiusScalar()
=> Vector2(Min(MyRoundness, Min(MySize.X, MySize.Y) / 2), Min(MyRoundness, Min(MySize.X, MySize.Y) / 2));
internal static Vector2 ConstrainedCornerRadiusScalar(Sn.Vector2 size)
=> Vector2(Min(MyRoundness, Math.Min(size.X, size.Y) / 2), Min(MyRoundness, Math.Min(size.X, size.Y) / 2));
// The value of a Color property stored as a Vector4 on the theming property set.
static Vector4 ThemedColor4Property(string propertyName) => Vector4(ThemeProperty(propertyName));

Просмотреть файл

@ -2152,7 +2152,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
var size = context.TrimAnimatable(rectangle.Size);
// If a Rectangle is in the context, use it to override the corner radius.
var cornerRadius = context.TrimAnimatable(shapeContext.RoundedCorner != null ? shapeContext.RoundedCorner.Radius : rectangle.CornerRadius);
var cornerRadius = context.TrimAnimatable(shapeContext.RoundedCorner != null ? shapeContext.RoundedCorner.Radius : rectangle.Roundness);
if (position.IsAnimated || size.IsAnimated || cornerRadius.IsAnimated)
{
@ -2231,141 +2231,199 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
CompositionShape TranslateRectangleContent(TranslationContext context, ShapeContentContext shapeContext, Rectangle shapeContent)
{
var compositionRectangle = _c.CreateSpriteShape();
var result = _c.CreateSpriteShape();
var position = context.TrimAnimatable(shapeContent.Position);
var size = context.TrimAnimatable(shapeContent.Size);
if (shapeContent.CornerRadius.AlwaysEquals(0) && shapeContext.RoundedCorner is null)
if (shapeContent.Roundness.AlwaysEquals(0) && shapeContext.RoundedCorner is null)
{
CompositionGeometry geometry;
TranslateAndApplyNonRoundedRectangleContent(
context,
shapeContext,
shapeContent,
position,
size,
result);
}
else
{
TranslateAndApplyRoundedRectangleContent(
context,
shapeContext,
shapeContent,
position,
size,
result);
}
// Use a non-rounded rectangle geometry.
if (_targetUapVersion <= 7)
return result;
}
void TranslateAndApplyNonRoundedRectangleContent(
TranslationContext context,
ShapeContentContext shapeContext,
Rectangle shapeContent,
in TrimmedAnimatable<Vector3> position,
in TrimmedAnimatable<Vector3> size,
CompositionSpriteShape compositionShape)
{
Debug.Assert(shapeContent.Roundness.AlwaysEquals(0) && shapeContext.RoundedCorner is null, "Precondition");
CompositionGeometry geometry;
// Use a non-rounded rectangle geometry.
if (_targetUapVersion <= 7)
{
// V7 did not reliably draw non-rounded rectangles.
// Work around the problem by using a rounded rectangle with a tiny corner radius.
var roundedRectangleGeometry = _c.CreateRoundedRectangleGeometry();
geometry = roundedRectangleGeometry;
// NOTE: magic tiny corner radius number - do not change!
roundedRectangleGeometry.CornerRadius = new Sn.Vector2(0.000001F);
roundedRectangleGeometry.Offset = InitialOffset(size: size, position: position);
if (!size.IsAnimated)
{
// V7 did not reliably draw non-rounded rectangles.
// Work around the problem by using a rounded rectangle with a tiny corner radius.
var roundedRectangleGeometry = _c.CreateRoundedRectangleGeometry();
geometry = roundedRectangleGeometry;
// NOTE: magic tiny corner radius number - do not change!
roundedRectangleGeometry.CornerRadius = new Sn.Vector2(0.000001F);
// Convert size and position into offset. This is necessary because a geometry's offset is for
// its top left corner, wherease a Lottie position is for its centerpoint.
roundedRectangleGeometry.Offset = Vector2(position.InitialValue - (size.InitialValue / 2));
if (!size.IsAnimated)
{
roundedRectangleGeometry.Size = Vector2(size.InitialValue);
}
}
else
{
// V8 and beyond doesn't need the rounded rectangle workaround.
var rectangleGeometry = _c.CreateRectangleGeometry();
geometry = rectangleGeometry;
// Convert size and position into offset. This is necessary because a geometry's offset is for
// its top left corner, wherease a Lottie position is for its centerpoint.
rectangleGeometry.Offset = Vector2(position.InitialValue - (size.InitialValue / 2));
if (!size.IsAnimated)
{
rectangleGeometry.Size = Vector2(size.InitialValue);
}
}
compositionRectangle.Geometry = geometry;
if (position.IsAnimated || size.IsAnimated)
{
Expr offsetExpression;
if (position.IsAnimated)
{
ApplyVector2KeyFrameAnimation(context, position, geometry, nameof(Rectangle.Position));
geometry.Properties.InsertVector2(nameof(Rectangle.Position), Vector2(position.InitialValue));
if (size.IsAnimated)
{
// Size AND position are animated.
offsetExpression = ExpressionFactory.PositionAndSizeToOffsetExpression;
ApplyVector2KeyFrameAnimation(context, size, geometry, nameof(Rectangle.Size));
}
else
{
// Only Position is animated
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(Vector2(size.InitialValue / 2));
}
}
else
{
// Only Size is animated.
offsetExpression = ExpressionFactory.PositionToOffsetExpression(Vector2(position.InitialValue));
ApplyVector2KeyFrameAnimation(context, size, geometry, nameof(Rectangle.Size));
}
var offsetExpressionAnimation = _c.CreateExpressionAnimation(offsetExpression);
offsetExpressionAnimation.SetReferenceParameter("my", geometry);
StartExpressionAnimation(geometry, "Offset", offsetExpressionAnimation);
roundedRectangleGeometry.Size = Vector2(size.InitialValue);
}
}
else
{
// Use a rounded rectangle geometry.
var geometry = _c.CreateRoundedRectangleGeometry();
compositionRectangle.Geometry = geometry;
// If a RoundedRectangle is in the context, use it to override the corner radius.
var cornerRadius = context.TrimAnimatable(shapeContext.RoundedCorner != null ? shapeContext.RoundedCorner.Radius : shapeContent.CornerRadius);
if (cornerRadius.IsAnimated)
{
ApplyScalarKeyFrameAnimation(context, cornerRadius, geometry, "CornerRadius.X");
ApplyScalarKeyFrameAnimation(context, cornerRadius, geometry, "CornerRadius.Y");
}
else
{
geometry.CornerRadius = Vector2((float)cornerRadius.InitialValue);
}
// V8 and beyond doesn't need the rounded rectangle workaround.
var rectangleGeometry = _c.CreateRectangleGeometry();
geometry = rectangleGeometry;
// Convert size and position into offset. This is necessary because a geometry's offset is for
// its top left corner, wherease a Lottie position is for its centerpoint.
geometry.Offset = Vector2(position.InitialValue - (size.InitialValue / 2));
// its top left corner, whereas a Lottie position is for its centerpoint.
rectangleGeometry.Offset = InitialOffset(size: size, position: position);
if (!size.IsAnimated)
{
geometry.Size = Vector2(size.InitialValue);
rectangleGeometry.Size = Vector2(size.InitialValue);
}
}
if (position.IsAnimated || size.IsAnimated)
compositionShape.Geometry = geometry;
ApplyRectangleContentCommon(context, shapeContext, shapeContent, compositionShape, size, position, geometry);
}
void TranslateAndApplyRoundedRectangleContent(
TranslationContext context,
ShapeContentContext shapeContext,
Rectangle shapeContent,
in TrimmedAnimatable<Vector3> position,
in TrimmedAnimatable<Vector3> size,
CompositionSpriteShape compositionShape)
{
// Use a rounded rectangle geometry.
var geometry = _c.CreateRoundedRectangleGeometry();
compositionShape.Geometry = geometry;
// If a RoundedRectangle is in the context, use it to override the roundness unless the roundness is non-0.
var cornerRadius = context.TrimAnimatable(
shapeContext.RoundedCorner != null && shapeContent.Roundness.AlwaysEquals(0)
? shapeContext.RoundedCorner.Radius
: shapeContent.Roundness);
// In After Effects, the rectangle Roundness has no further effect once it reaches min(Size.X, Size.Y)/2.
// In Composition, the cornerRadius continues to affect the shape even beyond min(Size.X, Size.Y)/2.
// If size or corner radius are animated, handle this with an expression.
if (cornerRadius.IsAnimated || size.IsAnimated)
{
if (cornerRadius.IsAnimated)
{
Expr offsetExpression;
if (position.IsAnimated)
{
ApplyVector2KeyFrameAnimation(context, position, geometry, nameof(Rectangle.Position));
geometry.Properties.InsertScalar("Roundness", Float(cornerRadius.InitialValue));
ApplyScalarKeyFrameAnimation(context, cornerRadius, geometry.Properties, "Roundness");
geometry.Properties.InsertVector2(nameof(Rectangle.Position), Vector2(position.InitialValue));
if (size.IsAnimated)
{
// Size AND position are animated.
offsetExpression = ExpressionFactory.PositionAndSizeToOffsetExpression;
ApplyVector2KeyFrameAnimation(context, size, geometry, nameof(Rectangle.Size));
}
else
{
// Only Position is animated
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(Vector2(size.InitialValue / 2));
}
if (size.IsAnimated)
{
// Both size and cornerRadius are animated.
var cornerRadiusExpression = _c.CreateExpressionAnimation(ConstrainedCornerRadiusScalar());
cornerRadiusExpression.SetReferenceParameter("my", geometry);
StartExpressionAnimation(geometry, "CornerRadius", cornerRadiusExpression);
}
else
{
// Only Size is animated.
offsetExpression = ExpressionFactory.PositionToOffsetExpression(Vector2(position.InitialValue));
// Only the cornerRadius is animated.
var cornerRadiusExpression = _c.CreateExpressionAnimation(ConstrainedCornerRadiusScalar(Vector2(size.InitialValue)));
cornerRadiusExpression.SetReferenceParameter("my", geometry);
StartExpressionAnimation(geometry, "CornerRadius", cornerRadiusExpression);
}
}
else
{
// Only the size is animated.
var cornerRadiusExpression = _c.CreateExpressionAnimation(ConstrainedCornerRadiusScalar(cornerRadius.InitialValue));
cornerRadiusExpression.SetReferenceParameter("my", geometry);
StartExpressionAnimation(geometry, "CornerRadius", cornerRadiusExpression);
}
}
else
{
// Static size and corner radius.
var cornerRadiusValue = Math.Min(cornerRadius.InitialValue, Math.Min(size.InitialValue.X, size.InitialValue.Y) / 2);
geometry.CornerRadius = Vector2((float)cornerRadiusValue);
}
geometry.Offset = InitialOffset(size:size, position:position);
if (!size.IsAnimated)
{
geometry.Size = Vector2(size.InitialValue);
}
ApplyRectangleContentCommon(context, shapeContext, shapeContent, compositionShape, size, position, geometry);
}
// Convert the size and position for a geometry into an offset.
// This is necessary because a geometry's offset describes its
// top left corner, whereas a Lottie position describes its centerpoint.
static Sn.Vector2 InitialOffset(
in TrimmedAnimatable<Vector3> size,
in TrimmedAnimatable<Vector3> position)
=> Vector2(position.InitialValue - (size.InitialValue / 2));
void ApplyRectangleContentCommon(
TranslationContext context,
ShapeContentContext shapeContext,
Rectangle shapeContent,
CompositionSpriteShape compositionRectangle,
in TrimmedAnimatable<Vector3> size,
in TrimmedAnimatable<Vector3> position,
CompositionGeometry geometry)
{
if (position.IsAnimated || size.IsAnimated)
{
Expr offsetExpression;
if (position.IsAnimated)
{
ApplyVector2KeyFrameAnimation(context, position, geometry, nameof(Rectangle.Position));
geometry.Properties.InsertVector2(nameof(Rectangle.Position), Vector2(position.InitialValue));
if (size.IsAnimated)
{
// Size AND position are animated.
offsetExpression = ExpressionFactory.PositionAndSizeToOffsetExpression;
ApplyVector2KeyFrameAnimation(context, size, geometry, nameof(Rectangle.Size));
}
var offsetExpressionAnimation = _c.CreateExpressionAnimation(offsetExpression);
offsetExpressionAnimation.SetReferenceParameter("my", geometry);
StartExpressionAnimation(geometry, nameof(geometry.Offset), offsetExpressionAnimation);
else
{
// Only Position is animated
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(Vector2(size.InitialValue / 2));
}
}
else
{
// Only Size is animated.
offsetExpression = ExpressionFactory.PositionToOffsetExpression(Vector2(position.InitialValue));
ApplyVector2KeyFrameAnimation(context, size, geometry, nameof(Rectangle.Size));
}
var offsetExpressionAnimation = _c.CreateExpressionAnimation(offsetExpression);
offsetExpressionAnimation.SetReferenceParameter("my", geometry);
StartExpressionAnimation(geometry, "Offset", offsetExpressionAnimation);
}
// Lottie rectangles have 0,0 at top right. That causes problems for TrimPath which expects 0,0 to be top left.
@ -2386,6 +2444,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
var width = size.InitialValue.X;
var height = size.InitialValue.Y;
var trimOffsetDegrees = (width / (2 * (width + height))) * 360;
TranslateAndApplyShapeContentContext(
context,
shapeContext,
@ -2398,8 +2457,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
Describe(compositionRectangle, shapeContent.Name);
Describe(compositionRectangle.Geometry, $"{shapeContent.Name}.RectangleGeometry");
}
return compositionRectangle;
}
void CheckForRoundedCornersOnPath(TranslationContext context, ShapeContentContext shapeContext)

Просмотреть файл

@ -141,22 +141,24 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData
/// <param name="propertyName">The name of the property.</param>
public void StopAnimation(string propertyName)
{
// We also need to stop animations on any sub-channels and super-channels.
// For example, if the property is TransformMatrix we must also stop animations
// on TransformMatrix.M11, TransformMatrix.M12, etc; and if the property is
// TransformMatrix.M11 we must also stop animations on TransformMatrix,
// TransformMatrix.M12, etc.
//
// We also need to stop animations on any sub-channels and the root property.
// Examples:
// Sub-channels: stopping Offset must also stop
// Offset.X, Offset.Y, etc..
// Root property: stopping Offset.X must also
// stop Offset.
// If there's a dot in the name it is a sub-channel name.
var subChannelPrefix = $"{propertyName}.";
var firstDotIndex = propertyName.IndexOf('.');
var rootPropertyPrefix = $"{(firstDotIndex >= 0 ? propertyName.Substring(0, firstDotIndex) : propertyName)}.";
var rootPropertyName = $"{(firstDotIndex >= 0 ? propertyName.Substring(0, firstDotIndex) : string.Empty)}.";
for (var i = 0; i < _animators.Count; i++)
{
var animatorPropertyName = _animators[i].AnimatedProperty;
if (animatorPropertyName == propertyName ||
animatorPropertyName.StartsWith(rootPropertyPrefix))
animatorPropertyName == rootPropertyName ||
animatorPropertyName.StartsWith(subChannelPrefix))
{
_animators.RemoveAt(i);