Fix translation of rectangles with RoundCorners. (#326)

We were not handling the distinction between RoundCorners.Radius and Rectangle.Roundness. This change does a much better job.
Note that it gets the Trimming wrong for RoundCorners - that will come in a later fix. I'm getting this in now because it has so much refactoring that it will be hard to focus on the trimming algorithm alongside this.

These are the After Effects rules for how RoundCorners.Radius and Rectangle.Roundness interact:
RoundCorners.Radius is ignored if Rectangle.Roundness != 0.
RoundCorners.Radius is able to produce an ellipse if the radius is large enough.
Rectangle.Roundness creates round corners with equal X and Y radii, so it can never create an ellipse.
This commit is contained in:
Simeon 2020-08-06 15:50:09 -07:00 коммит произвёл GitHub
Родитель 9ddf5e8113
Коммит 8d6e56ef3a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 1062 добавлений и 798 удалений

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

@ -1,18 +1,18 @@
[comment]: # (name:MultipleAnimatedRoundedCornersIsNotSupported)
[comment]: # (text:Multiple animated rounded corners is not supported.)
[comment]: # (name:MultipleAnimatedRoundCornersIsNotSupported)
[comment]: # (text:Multiple animated round corners is not supported.)
# Lottie-Windows Warning LT0011
The Lottie file contains a shape with multiple rounded corner properties, and at least
The Lottie file contains a shape with multiple round corners properties, and at least
one of them is animated.
## Remarks
After Effects allows arbitrary numbers of rounded corners to be applied to a shape. Currently
Lottie-Windows will ignore animations on a rounded corner if there are other rounded corners
After Effects allows arbitrary numbers of round corners to be applied to a shape. Currently
Lottie-Windows will ignore animations on a round corners if there are other round corners
applied to the shape.
In most cases the After Effects project can be modified to use a single animated rounded
corner in order to avoid this issue.
In most cases the After Effects project can be modified to use a single animated round
corners in order to avoid this issue.
If support for this feature is important for your scenario please provide feedback
by raising it as an issue [here](https://github.com/windows-toolkit/Lottie-Windows/issues).

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

@ -1,9 +1,9 @@
[comment]: # (name:PathWithRoundedCornersIsNotSupported)
[comment]: # (text:Path with rounded corners is not supported.)
[comment]: # (name:PathWithRoundCornersIsNotSupported)
[comment]: # (text:Path with round corners is not supported.)
# Lottie-Windows Warning LT0016
The Lottie file specifies rounded corners on a path. This is not currently supported by Lottie-Windows.
The Lottie file specifies round corners on a path. This is not currently supported by Lottie-Windows.
## Remarks
If support for this feature is important for your scenario please provide feedback

17
source/Issues/LT0037.md Normal file
Просмотреть файл

@ -0,0 +1,17 @@
[comment]: # (name:ConflictingRoundnessAndRadiusIsNotSupported)
[comment]: # (text:Rectangle roundness with round corners is not supported.)
# Lottie-Windows Warning LT0037
A Lottie file contains a rectangle that has rounded corners via a corner radius, and also has round corners applied to it.
## Remarks
Currently Lottie-Windows ignores the round corners applied to a rectangle that ever has a non-0 corner radius.
If support for this feature is important for your scenario please provide feedback
by raising it as an issue [here](https://github.com/windows-toolkit/Lottie-Windows/issues).
## Resources
* [Lottie-Windows repository](https://aka.ms/lottie)
* [Questions and feedback via Github](https://github.com/windows-toolkit/Lottie-Windows/issues)

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

@ -101,7 +101,19 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
/// Returns <c>true</c> if this value is always equal to the given value.
/// </summary>
/// <returns><c>true</c> if this value is always equal to the given value.</returns>
public bool AlwaysEquals(T value) => !IsAnimated && value.Equals(InitialValue);
public bool Always(T value) => !IsAnimated && value.Equals(InitialValue);
/// <summary>
/// Returns <c>true</c> if this value is ever equal to the given value.
/// </summary>
/// <returns><c>true</c> if this value is ever equal to the given value.</returns>
public bool Ever(T value) => value.Equals(InitialValue) || KeyFrames.Any(kf => value.Equals(kf.Value));
/// <summary>
/// Returns <c>true</c> if this value is ever not equal to the given value.
/// </summary>
/// <returns><c>true</c> if this value is ever not equal to the given value.</returns>
public bool EverNot(T value) => !Always(value);
/// <inheritdoc/>
// Not a great hash code because it ignore the KeyFrames, but quick.

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

@ -62,7 +62,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Repeater.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RepeaterTransform.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Rotation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RoundedCorner.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RoundCorners.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Sequence.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Serialization\LottieCompositionYamlSerializer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Shape.cs" />

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

@ -23,7 +23,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
RadialGradientStroke,
Rectangle,
Repeater,
RoundedCorner,
RoundCorners,
Shape,
ShapeGroup,
ShapeLayer,

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

@ -7,9 +7,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
#if PUBLIC_LottieData
public
#endif
sealed class RoundedCorner : ShapeLayerContent
sealed class RoundCorners : ShapeLayerContent
{
public RoundedCorner(
public RoundCorners(
in ShapeLayerContentArgs args,
Animatable<double> radius)
: base(in args)
@ -30,9 +30,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
public Animatable<double> Radius { get; }
/// <inheritdoc/>
public override ShapeContentType ContentType => ShapeContentType.RoundedCorner;
public override ShapeContentType ContentType => ShapeContentType.RoundCorners;
/// <inheritdoc/>
public override LottieObjectType ObjectType => LottieObjectType.RoundedCorner;
public override LottieObjectType ObjectType => LottieObjectType.RoundCorners;
}
}

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

@ -93,7 +93,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
case LottieObjectType.RadialGradientStroke:
case LottieObjectType.Rectangle:
case LottieObjectType.Repeater:
case LottieObjectType.RoundedCorner:
case LottieObjectType.RoundCorners:
case LottieObjectType.Shape:
case LottieObjectType.ShapeGroup:
case LottieObjectType.SolidColorFill:
@ -314,8 +314,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
return FromMergePaths((MergePaths)content, superclassContent);
case ShapeContentType.Repeater:
return FromRepeater((Repeater)content, superclassContent);
case ShapeContentType.RoundedCorner:
return FromRoundedCorner((RoundedCorner)content, superclassContent);
case ShapeContentType.RoundCorners:
return FromRoundCorners((RoundCorners)content, superclassContent);
default:
throw Unreachable;
}
@ -674,7 +674,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
return result;
}
YamlObject FromRoundedCorner(RoundedCorner content, YamlMap superclassContent)
YamlObject FromRoundCorners(RoundCorners content, YamlMap superclassContent)
{
var result = superclassContent;
result.Add(nameof(content.Radius), FromAnimatable(content.Radius));

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

@ -23,7 +23,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
RadialGradientStroke,
Rectangle,
Repeater,
RoundedCorner,
RoundCorners,
SolidColorFill,
SolidColorStroke,
Transform,

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

@ -182,7 +182,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Tools
break;
case ShapeContentType.Repeater:
break;
case ShapeContentType.RoundedCorner:
case ShapeContentType.RoundCorners:
break;
case ShapeContentType.SolidColorFill:
break;

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

@ -45,7 +45,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
case "mm":
return ReadMergePaths(obj, in args);
case "rd":
return ReadRoundedCorner(obj, in args);
return ReadRoundCorners(obj, in args);
case "rp":
return ReadRepeater(obj, in args);
@ -436,7 +436,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
mergeMode);
}
RoundedCorner ReadRoundedCorner(
RoundCorners ReadRoundCorners(
in LottieJsonObjectElement obj,
in ShapeLayerContent.ShapeLayerContentArgs shapeLayerContentArgs)
{
@ -445,7 +445,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization
var radius = ReadAnimatableFloat(obj.ObjectPropertyOrNull("r"));
obj.AssertAllPropertiesRead();
return new RoundedCorner(
return new RoundCorners(
in shapeLayerContentArgs,
radius);
}

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

@ -38,7 +38,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
/// this <see cref="CompositeOpacity"/>.</returns>
internal CompositeOpacity ComposedWith(in TrimmedAnimatable<Opacity> opacity)
{
if (opacity.AlwaysEquals(Opacity.Opaque))
if (opacity.Always(Opacity.Opaque))
{
// Nothing to do.
return this;

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

@ -94,11 +94,12 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
internal CompositionPropertySet CreatePropertySet() => _compositor.CreatePropertySet();
internal CompositionGeometry CreateRectangleGeometry(Sn.Vector2? size, Sn.Vector2? offset)
// Returns either a CompositionRectangleGeometry or a CompositionRoundedRectangleGeometry.
internal RectangleOrRoundedRectangleGeometry CreateRectangleGeometry()
{
const int c_rectangleGeometryIsUnreliableUntil = 12;
CompositionGeometry result;
RectangleOrRoundedRectangleGeometry result;
if (_targetUapVersion < c_rectangleGeometryIsUnreliableUntil)
{
@ -109,8 +110,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
// NOTE: magic tiny corner radius number - do not change!
roundedRectangleGeometry.CornerRadius = new Sn.Vector2(0.000001F);
roundedRectangleGeometry.Size = size;
roundedRectangleGeometry.Offset = offset;
result = roundedRectangleGeometry;
}
@ -119,11 +118,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
// Later versions do not need the rounded rectangle workaround.
ConsumeVersionFeature(c_rectangleGeometryIsUnreliableUntil);
var rectangleGeometry = _compositor.CreateRectangleGeometry();
rectangleGeometry.Size = size;
rectangleGeometry.Offset = offset;
result = rectangleGeometry;
result = _compositor.CreateRectangleGeometry();
}
return result;

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

@ -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 MyRadius = MyScalar("Radius");
static readonly Scalar MyRoundness = MyScalar("Roundness");
static readonly Scalar MyTStart = MyScalar("TStart");
static readonly Scalar MyTEnd = MyScalar("TEnd");
@ -68,13 +69,34 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
internal static Scalar RootScalar(string propertyName) => Scalar(RootProperty(propertyName));
internal static Vector2 ConstrainedCornerRadiusScalar(double roundness)
// Converts a RoundCorners.Radius value to CompositionRoundedRectangleGeometry.CornerRadius.
// RoundCorners corner radii are constrained to half of the coresponding side.
internal static Vector2 RadiusToCornerRadius(double radius)
=> Vector2(Min(radius, MySize.X / 2), Min(radius, MySize.Y / 2));
// Converts a RoundCorners.Radius value to CompositionRoundedRectangleGeometry.CornerRadius.
// RoundCorners corner radii are constrained to half of the coresponding side.
internal static Vector2 RadiusToCornerRadius()
=> Vector2(Min(MyRadius, MySize.X / 2), Min(MyRadius, MySize.Y / 2));
// Converts a RoundCorners.Radius value to CompositionRoundedRectangleGeometry.CornerRadius.
// RoundCorners corner radii are constrained to half of the coresponding side.
internal static Vector2 RadiusToCornerRadius(Sn.Vector2 size)
=> Vector2(Min(MyRadius, size.X / 2), Min(MyRadius, size.Y / 2));
// Converts a Rectangle.Roundness value to CompositionRoundedRectangleGeometry.CornerRadius.
// Rectangle.Roundness corner radius is constrained to half of the smaller side.
internal static Vector2 RoundnessToCornerRadius(double roundness)
=> Vector2(Min(roundness, Min(MySize.X, MySize.Y) / 2), Min(roundness, Min(MySize.X, MySize.Y) / 2));
internal static Vector2 ConstrainedCornerRadiusScalar()
// Converts a Rectangle.Roundness value to CompositionRoundedRectangleGeometry.CornerRadius.
// Rectangle.Roundness corner radius is constrained to half of the smaller side.
internal static Vector2 RoundessToCornerRadius()
=> Vector2(Min(MyRoundness, Min(MySize.X, MySize.Y) / 2), Min(MyRoundness, Min(MySize.X, MySize.Y) / 2));
internal static Vector2 ConstrainedCornerRadiusScalar(Sn.Vector2 size)
// Converts a Rectangle.Roundness value to CompositionRoundedRectangleGeometry.CornerRadius.
// Rectangle.Roundness corner radius is constrained to half of the smaller side.
internal static Vector2 RoundnessToCornerRadius(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.

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

@ -11,11 +11,14 @@
<Compile Include="$(MSBuildThisFileDirectory)CompositeOpacity.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CompositionObjectFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ContainerShapeOrVisual.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RectangleOrRoundedRectangleGeometry.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ShapeContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CubicBezierFunction2.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExpressionFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Float32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LottieToMultiVersionWinCompTranslator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LottieToWinCompTranslator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LottieToWinCompTranslator.Rectangles.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MultiVersionTranslationResult.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PathGeometryGroup.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ProgressMapFactory.cs" />

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

@ -0,0 +1,497 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgcg;
using static Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp.ExpressionFactory;
using Expr = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions.Expression;
using Sn = System.Numerics;
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
{
#pragma warning disable SA1205 // Partial elements should declare access
#pragma warning disable SA1601 // Partial elements should be documented
// Translation for Lottie rectangles.
sealed partial class LottieToWinCompTranslator
{
// Translates a Lottie rectangle to a CompositionShape.
CompositionShape TranslateRectangleContent(TranslationContext context, ShapeContext shapeContext, Rectangle rectangle)
{
var result = _c.CreateSpriteShape();
var position = context.TrimAnimatable(rectangle.Position);
if (IsNonRounded(shapeContext, rectangle))
{
// Non-rounded rectangles are slightly more efficient, but they can only be used
// if there is no roundness or Round Corners.
TranslateAndApplyNonRoundedRectangleContent(
context,
shapeContext,
rectangle,
position,
result);
}
else
{
TranslateAndApplyRoundedRectangleContent(
context,
shapeContext,
rectangle,
position,
result);
}
return result;
}
// Translates a non-rounded Lottie rectangle to a CompositionShape.
void TranslateAndApplyNonRoundedRectangleContent(
TranslationContext context,
ShapeContext shapeContext,
Rectangle rectangle,
in TrimmedAnimatable<Vector3> position,
CompositionSpriteShape compositionShape)
{
Debug.Assert(IsNonRounded(shapeContext, rectangle), "Precondition");
var geometry = _c.CreateRectangleGeometry();
compositionShape.Geometry = geometry;
var size = AnimatableVector3Rewriter.EnsureOneEasingPerChannel(rectangle.Size);
if (size is AnimatableXYZ sizeXYZ)
{
var width = context.TrimAnimatable(sizeXYZ.X);
var height = context.TrimAnimatable(sizeXYZ.Y);
if (!(width.IsAnimated || height.IsAnimated))
{
geometry.Size = Vector2(width.InitialValue, height.InitialValue);
}
geometry.Offset = InitialOffset(width, height, position: position);
ApplyRectangleContentCommonXY(context, shapeContext, rectangle, compositionShape, width, height, position, geometry);
}
else
{
var size3 = context.TrimAnimatable<Vector3>((AnimatableVector3)size);
if (!size3.IsAnimated)
{
geometry.Size = Vector2(size3.InitialValue);
}
geometry.Offset = InitialOffset(size: size3, position: position);
ApplyRectangleContentCommon(context, shapeContext, rectangle, compositionShape, size3, position, geometry);
}
}
// Translates a Lottie rectangle to a CompositionShape containing a RoundedRectangle.
void TranslateAndApplyRoundedRectangleContent(
TranslationContext context,
ShapeContext shapeContext,
Rectangle rectangle,
in TrimmedAnimatable<Vector3> position,
CompositionSpriteShape compositionShape)
{
// Use a rounded rectangle geometry.
var geometry = _c.CreateRoundedRectangleGeometry();
compositionShape.Geometry = geometry;
// Get the corner radius. This will come from either Rectangle.Roundness
// or RoundCorners.Radius.
var cornerRadius = GetCornerRadius(context, shapeContext, rectangle, out var cornerRadiusIsRectangleRoundness);
// Get the size, converted to an AnimatableXYZ if necessary to handle different easings per channel.
var size = AnimatableVector3Rewriter.EnsureOneEasingPerChannel(rectangle.Size);
if (size is AnimatableXYZ sizeXYZ)
{
var width = context.TrimAnimatable(sizeXYZ.X);
var height = context.TrimAnimatable(sizeXYZ.Y);
ApplyCornerRadius(
context,
geometry,
cornerRadius,
initialWidth: width.InitialValue,
initialHeight: height.InitialValue,
isSizeAnimated: width.IsAnimated || height.IsAnimated,
cornerRadiusIsRectangleRoundness: cornerRadiusIsRectangleRoundness);
geometry.Offset = InitialOffset(width, height, position: position);
ApplyRectangleContentCommonXY(context, shapeContext, rectangle, compositionShape, width, height, position, geometry);
}
else
{
var size3 = context.TrimAnimatable<Vector3>((AnimatableVector3)size);
ApplyCornerRadius(
context,
geometry,
cornerRadius,
initialWidth: size3.InitialValue.X,
initialHeight: size3.InitialValue.Y,
isSizeAnimated: size3.IsAnimated,
cornerRadiusIsRectangleRoundness: cornerRadiusIsRectangleRoundness);
geometry.Offset = InitialOffset(size: size3, position: position);
ApplyRectangleContentCommon(context, shapeContext, rectangle, compositionShape, size3, position, geometry);
}
}
void ApplyCornerRadius(
TranslationContext context,
CompositionRoundedRectangleGeometry geometry,
in TrimmedAnimatable<double> cornerRadius,
double initialWidth,
double initialHeight,
bool isSizeAnimated,
bool cornerRadiusIsRectangleRoundness)
{
var initialSize = Vector2(initialWidth, initialHeight);
// In After Effects Rectangle.Roundness and RoundCorners.Radius are clamped to a value
// that depends on the size of the rectangle.
// If size or corner radius are animated, handle this with an expression.
if (cornerRadius.IsAnimated || isSizeAnimated)
{
WinCompData.Expressions.Vector2 cornerRadiusExpression;
if (cornerRadius.IsAnimated)
{
InsertAndApplyScalarKeyFramePropertySetAnimation(
context,
cornerRadius,
geometry,
cornerRadiusIsRectangleRoundness ? "Roundness" : "Radius");
if (isSizeAnimated)
{
// Both size and cornerRadius are animated.
cornerRadiusExpression = cornerRadiusIsRectangleRoundness
? RoundessToCornerRadius()
: RadiusToCornerRadius();
}
else
{
// Only the cornerRadius is animated.
cornerRadiusExpression = cornerRadiusIsRectangleRoundness
? RoundnessToCornerRadius(initialSize)
: RadiusToCornerRadius(initialSize);
}
}
else
{
// Only the size is animated.
cornerRadiusExpression = cornerRadiusIsRectangleRoundness
? RoundnessToCornerRadius(cornerRadius.InitialValue)
: RadiusToCornerRadius(cornerRadius.InitialValue);
}
var cornerRadiusAnimation = _c.CreateExpressionAnimation(cornerRadiusExpression);
cornerRadiusAnimation.SetReferenceParameter("my", geometry);
StartExpressionAnimation(geometry, "CornerRadius", cornerRadiusAnimation);
}
else
{
// Static size and corner radius.
if (cornerRadiusIsRectangleRoundness)
{
// Rectangle.Roundness corner radius is constrained to half of the smaller side.
var cornerRadiusValue = Math.Min(cornerRadius.InitialValue, Math.Min(initialWidth, initialHeight) / 2);
geometry.CornerRadius = Vector2((float)cornerRadiusValue);
}
else
{
// RoundCorners corner radii are constrained to half of the coresponding side.
geometry.CornerRadius = Vector2(Math.Min(cornerRadius.InitialValue, initialWidth / 2), Math.Min(cornerRadius.InitialValue, initialHeight / 2));
}
}
if (!isSizeAnimated)
{
geometry.Size = initialSize;
}
}
void ApplyRectangleContentCommon(
TranslationContext context,
ShapeContext shapeContext,
Rectangle rectangle,
CompositionSpriteShape compositionRectangle,
in TrimmedAnimatable<Vector3> size,
in TrimmedAnimatable<Vector3> position,
RectangleOrRoundedRectangleGeometry 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);
}
// Lottie rectangles have 0,0 at top right. That causes problems for TrimPath which expects 0,0 to be top left.
// Add an offset to the trim path.
// TODO - this only works correctly if Size and TrimOffset are not animated. A complete solution requires
// adding another property.
var isPartialTrimPath = shapeContext.TrimPath != null &&
(shapeContext.TrimPath.Start.IsAnimated || shapeContext.TrimPath.End.IsAnimated || shapeContext.TrimPath.Offset.IsAnimated ||
shapeContext.TrimPath.Start.InitialValue.Value != 0 || shapeContext.TrimPath.End.InitialValue.Value != 1);
if (size.IsAnimated && isPartialTrimPath)
{
// Warn that we might be getting things wrong
_issues.AnimatedRectangleWithTrimPathIsNotSupported();
}
var width = size.InitialValue.X;
var height = size.InitialValue.Y;
var trimOffsetDegrees = (width / (2 * (width + height))) * 360;
TranslateAndApplyShapeContext(
context,
shapeContext,
compositionRectangle,
rectangle.DrawingDirection == DrawingDirection.Reverse,
trimOffsetDegrees: trimOffsetDegrees);
if (_addDescriptions)
{
Describe(compositionRectangle, rectangle.Name);
Describe(compositionRectangle.Geometry, $"{rectangle.Name}.RectangleGeometry");
}
}
void ApplyRectangleContentCommonXY(
TranslationContext context,
ShapeContext shapeContext,
Rectangle rectangle,
CompositionSpriteShape compositionRectangle,
in TrimmedAnimatable<double> width,
in TrimmedAnimatable<double> height,
in TrimmedAnimatable<Vector3> position,
RectangleOrRoundedRectangleGeometry geometry)
{
if (position.IsAnimated || width.IsAnimated || height.IsAnimated)
{
Expr offsetExpression;
if (position.IsAnimated)
{
ApplyVector2KeyFrameAnimation(context, position, geometry, nameof(Rectangle.Position));
geometry.Properties.InsertVector2(nameof(Rectangle.Position), Vector2(position.InitialValue));
if (width.IsAnimated || height.IsAnimated)
{
// Size AND position are animated.
offsetExpression = ExpressionFactory.PositionAndSizeToOffsetExpression;
if (width.IsAnimated)
{
ApplyScalarKeyFrameAnimation(context, width, geometry, $"{nameof(Rectangle.Size)}.X");
}
if (height.IsAnimated)
{
ApplyScalarKeyFrameAnimation(context, height, geometry, $"{nameof(Rectangle.Size)}.Y");
}
}
else
{
// Only Position is animated.
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(Vector2(new Vector2(width.InitialValue, height.InitialValue) / 2));
}
}
else
{
// Only Size is animated.
offsetExpression = ExpressionFactory.PositionToOffsetExpression(Vector2(position.InitialValue));
if (width.IsAnimated)
{
ApplyScalarKeyFrameAnimation(context, width, geometry, $"{nameof(Rectangle.Size)}.X");
}
if (height.IsAnimated)
{
ApplyScalarKeyFrameAnimation(context, height, geometry, $"{nameof(Rectangle.Size)}.Y");
}
}
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.
// Add an offset to the trim path.
// TODO - this only works correctly if Size and TrimOffset are not animated. A complete solution requires
// adding another property.
var isPartialTrimPath = shapeContext.TrimPath != null &&
(shapeContext.TrimPath.Start.IsAnimated || shapeContext.TrimPath.End.IsAnimated || shapeContext.TrimPath.Offset.IsAnimated ||
shapeContext.TrimPath.Start.InitialValue.Value != 0 || shapeContext.TrimPath.End.InitialValue.Value != 1);
if ((width.IsAnimated || height.IsAnimated) && isPartialTrimPath)
{
// Warn that we might be getting things wrong.
_issues.AnimatedRectangleWithTrimPathIsNotSupported();
}
var initialWidth = width.InitialValue;
var initialHeight = height.InitialValue;
var trimOffsetDegrees = (initialWidth / (2 * (initialWidth + initialHeight))) * 360;
TranslateAndApplyShapeContext(
context,
shapeContext,
compositionRectangle,
rectangle.DrawingDirection == DrawingDirection.Reverse,
trimOffsetDegrees: trimOffsetDegrees);
if (_addDescriptions)
{
Describe(compositionRectangle, rectangle.Name);
Describe(compositionRectangle.Geometry, $"{rectangle.Name}.RectangleGeometry");
}
}
CanvasGeometry CreateWin2dRectangleGeometry(
TranslationContext context,
ShapeContext shapeContext,
Rectangle rectangle)
{
var position = context.TrimAnimatable(rectangle.Position);
var size = context.TrimAnimatable(rectangle.Size);
var cornerRadius = GetCornerRadius(context, shapeContext, rectangle, out var cornerRadiusIsRectangleRoundness);
if (position.IsAnimated || size.IsAnimated || cornerRadius.IsAnimated)
{
_issues.CombiningAnimatedShapesIsNotSupported();
}
var width = size.InitialValue.X;
var height = size.InitialValue.Y;
var radiusX = cornerRadius.InitialValue;
var radiusY = cornerRadius.InitialValue;
// The radius is treated differently depending on whether it came from Rectangle.Roundness
// or RoundCorners.Radius.
if (cornerRadiusIsRectangleRoundness)
{
// Radius came from Rectangle.Radius.
// X and Y have the same radius (the corners are round) which is capped at half
// the length of the smallest side.
radiusX = radiusY = Math.Min(Math.Min(width, height) / 2, radiusY);
}
else
{
// Radius came from RoundCorners.Radius.
// X and Y radii are capped at half the length of their corresponding sides.
radiusX = Math.Min(width / 2, radiusX);
radiusY = Math.Min(width / 2, radiusY);
}
var result = CanvasGeometry.CreateRoundedRectangle(
null,
(float)(position.InitialValue.X - (width / 2)),
(float)(position.InitialValue.Y - (height / 2)),
(float)width,
(float)height,
(float)radiusX,
(float)radiusY);
var transformMatrix = CreateMatrixFromTransform(context, shapeContext.Transform);
if (!transformMatrix.IsIdentity)
{
result = result.Transform(transformMatrix);
}
if (_addDescriptions)
{
Describe(result, rectangle.Name);
}
return result;
}
// Gets the corner radius and indicates whether the value came from Rectangle.Roundness (as
// opposed to RoundCorners.Radius).
TrimmedAnimatable<double> GetCornerRadius(
TranslationContext context,
ShapeContext shapeContext,
Rectangle rectangle,
out bool cornerRadiusIsRectangleRoundness)
{
// Choose either Rectangle.Roundness or RoundCorners.Radius to control corner rounding.
// After Effects ignores RoundCorners.Radius when Rectangle.Roundness is non-0.
//
// If Rectangle.Roundness is ever non-0 and RoundCorners.Radius is ever non-0, we'd need to
// switch between Rectangle.Roundness and RoundCorners.Radius behaviors as RoundCorners.Radius
// switches between 0 and non-0. That is a rare case and would require a much more complicated
// set of expressions, so for now we don't support that case.
//
// If Rectangle.Roundness is ever non-0, choose it to define the rounding of the corners.
cornerRadiusIsRectangleRoundness = rectangle.Roundness.EverNot(0);
// If we're using Rectangle.Roundness, check whether that might interfere with the
// RoundCorners.Radius values.
if (cornerRadiusIsRectangleRoundness &&
rectangle.Roundness.Ever(0) &&
shapeContext.RoundCorners.Radius.EverNot(0))
{
// Report the issue about RoundCorners being ignored.
_issues.ConflictingRoundnessAndRadiusIsNotSupported();
}
return context.TrimAnimatable(cornerRadiusIsRectangleRoundness ? rectangle.Roundness : shapeContext.RoundCorners.Radius);
}
// 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));
static Sn.Vector2 InitialOffset(
in TrimmedAnimatable<double> width,
in TrimmedAnimatable<double> height,
in TrimmedAnimatable<Vector3> position)
=> Vector2(position.InitialValue - (new Vector3(width.InitialValue, height.InitialValue, 0) / 2));
// Returns true if the given rectangle ever has rounded corners.
static bool IsNonRounded(ShapeContext shapeContext, Rectangle rectangle) =>
rectangle.Roundness.Always(0) && shapeContext.RoundCorners.Radius.Always(0);
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Numerics;
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
{
/// <summary>
/// Helper for abstracting <see cref="CompositionRectangleGeometry"/> and
/// <see cref="CompositionRoundedRectangleGeometry"/> so that they can be
/// treated the same in some circumstances.
/// </summary>
abstract class RectangleOrRoundedRectangleGeometry
{
readonly CompositionGeometry _compositionGeometry;
RectangleOrRoundedRectangleGeometry(CompositionGeometry compositionGeometry)
{
_compositionGeometry = compositionGeometry;
}
public abstract bool IsRoundedRectangle { get; }
public CompositionPropertySet Properties => _compositionGeometry.Properties;
public abstract Vector2? Offset { get; set; }
public abstract Vector2? Size { get; set; }
sealed class WrappedRectangleGeometry : RectangleOrRoundedRectangleGeometry
{
readonly CompositionRectangleGeometry _wrapped;
internal WrappedRectangleGeometry(CompositionRectangleGeometry wrapped)
: base(wrapped)
{
_wrapped = wrapped;
}
public override bool IsRoundedRectangle => false;
public override Vector2? Offset { get => _wrapped.Offset; set => _wrapped.Offset = value; }
public override Vector2? Size { get => _wrapped.Size; set => _wrapped.Size = value; }
}
sealed class WrappedRoundedRectangleGeometry : RectangleOrRoundedRectangleGeometry
{
readonly CompositionRoundedRectangleGeometry _wrapped;
internal WrappedRoundedRectangleGeometry(CompositionRoundedRectangleGeometry wrapped)
: base(wrapped)
{
_wrapped = wrapped;
}
public override bool IsRoundedRectangle => true;
public override Vector2? Offset { get => _wrapped.Offset; set => _wrapped.Offset = value; }
public override Vector2? Size { get => _wrapped.Size; set => _wrapped.Size = value; }
}
public static implicit operator RectangleOrRoundedRectangleGeometry(CompositionRectangleGeometry rectangle) => new WrappedRectangleGeometry(rectangle);
public static implicit operator RectangleOrRoundedRectangleGeometry(CompositionRoundedRectangleGeometry roundedRectangle) => new WrappedRoundedRectangleGeometry(roundedRectangle);
public static implicit operator CompositionGeometry(RectangleOrRoundedRectangleGeometry rectangle) => rectangle._compositionGeometry;
}
}

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

@ -0,0 +1,315 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
{
/// <summary>
/// Describes the environment in which a Lottie shape should be interpreted.
/// </summary>
sealed class ShapeContext
{
// A RoundCorners with a radius of 0.
static readonly RoundCorners s_defaultRoundCorners =
new RoundCorners(new ShapeLayerContent.ShapeLayerContentArgs { }, new Animatable<double>(0, null));
readonly TranslationIssues _issues;
internal ShapeContext(TranslationIssues issues) => _issues = issues;
internal ShapeStroke Stroke { get; private set; }
internal ShapeFill Fill { get; private set; }
internal TrimPath TrimPath { get; private set; }
/// <summary>
/// Never null. If there is no <see cref="RoundCorners"/> set, a default
/// 0 <see cref="RoundCorners"/> will be returned.
/// </summary>
internal RoundCorners RoundCorners { get; private set; } = s_defaultRoundCorners;
internal Transform Transform { get; private set; }
// Opacity is not part of the Lottie context for shapes. But because WinComp
// doesn't support opacity on shapes, the opacity is inherited from
// the Transform and passed through to the brushes here.
internal CompositeOpacity Opacity { get; private set; } = CompositeOpacity.Opaque;
internal void UpdateFromStack(Stack<ShapeLayerContent> stack)
{
while (stack.Count > 0)
{
var popped = stack.Peek();
switch (popped.ContentType)
{
case ShapeContentType.LinearGradientFill:
case ShapeContentType.RadialGradientFill:
case ShapeContentType.SolidColorFill:
Fill = ComposeFills(Fill, (ShapeFill)popped);
break;
case ShapeContentType.LinearGradientStroke:
case ShapeContentType.RadialGradientStroke:
case ShapeContentType.SolidColorStroke:
Stroke = ComposeStrokes(Stroke, (ShapeStroke)popped);
break;
case ShapeContentType.RoundCorners:
RoundCorners = ComposeRoundCorners(RoundCorners, (RoundCorners)popped);
break;
case ShapeContentType.TrimPath:
TrimPath = ComposeTrimPaths(TrimPath, (TrimPath)popped);
break;
default: return;
}
stack.Pop();
}
}
internal void UpdateOpacityFromTransform(TranslationContext context, Transform transform)
{
if (transform is null)
{
return;
}
Opacity = Opacity.ComposedWith(context.TrimAnimatable(transform.Opacity));
}
// Only used when translating geometries. Layers use an extra Shape or Visual to
// apply the transform, but geometries need to take the transform into account when
// they're created.
internal void SetTransform(Transform transform)
{
Transform = transform;
}
internal ShapeContext Clone() =>
new ShapeContext(_issues)
{
Fill = Fill,
Stroke = Stroke,
TrimPath = TrimPath,
RoundCorners = RoundCorners,
Opacity = Opacity,
Transform = Transform,
};
ShapeFill ComposeFills(ShapeFill a, ShapeFill b)
{
if (a is null)
{
return b;
}
else if (b is null)
{
return a;
}
if (a.FillKind != b.FillKind)
{
_issues.MultipleFillsIsNotSupported();
return b;
}
switch (a.FillKind)
{
case ShapeFill.ShapeFillKind.SolidColor:
return ComposeSolidColorFills((SolidColorFill)a, (SolidColorFill)b);
}
_issues.MultipleFillsIsNotSupported();
return b;
}
SolidColorFill ComposeSolidColorFills(SolidColorFill a, SolidColorFill b)
{
if (!b.Color.IsAnimated && !b.Opacity.IsAnimated)
{
if (b.Opacity.InitialValue == LottieData.Opacity.Opaque &&
b.Color.InitialValue.A == 1)
{
// b overrides a.
return b;
}
else if (b.Opacity.InitialValue.IsTransparent || b.Color.InitialValue.A == 0)
{
// b is transparent, so a wins.
return a;
}
}
_issues.MultipleFillsIsNotSupported();
return b;
}
ShapeStroke ComposeStrokes(ShapeStroke a, ShapeStroke b)
{
if (a is null)
{
return b;
}
else if (b is null)
{
return a;
}
if (a.StrokeKind != b.StrokeKind)
{
_issues.MultipleStrokesIsNotSupported();
return b;
}
switch (a.StrokeKind)
{
case ShapeStroke.ShapeStrokeKind.SolidColor:
return ComposeSolidColorStrokes((SolidColorStroke)a, (SolidColorStroke)b);
case ShapeStroke.ShapeStrokeKind.LinearGradient:
return ComposeLinearGradientStrokes((LinearGradientStroke)a, (LinearGradientStroke)b);
case ShapeStroke.ShapeStrokeKind.RadialGradient:
return ComposeRadialGradientStrokes((RadialGradientStroke)a, (RadialGradientStroke)b);
default:
throw new InvalidOperationException();
}
}
LinearGradientStroke ComposeLinearGradientStrokes(LinearGradientStroke a, LinearGradientStroke b)
{
Debug.Assert(a != null && b != null, "Precondition");
if (!a.StrokeWidth.IsAnimated && !b.StrokeWidth.IsAnimated &&
a.Opacity.Always(LottieData.Opacity.Opaque) && b.Opacity.Always(LottieData.Opacity.Opaque))
{
if (a.StrokeWidth.InitialValue >= b.StrokeWidth.InitialValue)
{
// a occludes b, so b can be ignored.
return a;
}
}
_issues.MultipleStrokesIsNotSupported();
return a;
}
RadialGradientStroke ComposeRadialGradientStrokes(RadialGradientStroke a, RadialGradientStroke b)
{
Debug.Assert(a != null && b != null, "Precondition");
if (!a.StrokeWidth.IsAnimated && !b.StrokeWidth.IsAnimated &&
a.Opacity.Always(LottieData.Opacity.Opaque) && b.Opacity.Always(LottieData.Opacity.Opaque))
{
if (a.StrokeWidth.InitialValue >= b.StrokeWidth.InitialValue)
{
// a occludes b, so b can be ignored.
return a;
}
}
_issues.MultipleStrokesIsNotSupported();
return a;
}
SolidColorStroke ComposeSolidColorStrokes(SolidColorStroke a, SolidColorStroke b)
{
Debug.Assert(a != null && b != null, "Precondition");
if (!a.StrokeWidth.IsAnimated && !b.StrokeWidth.IsAnimated &&
!a.DashPattern.Any() && !b.DashPattern.Any() &&
a.Opacity.Always(LottieData.Opacity.Opaque) && b.Opacity.Always(LottieData.Opacity.Opaque))
{
if (a.StrokeWidth.InitialValue >= b.StrokeWidth.InitialValue)
{
// a occludes b, so b can be ignored.
return a;
}
}
// The new stroke should be in addition to the existing stroke. And colors should blend.
_issues.MultipleStrokesIsNotSupported();
return b;
}
RoundCorners ComposeRoundCorners(RoundCorners a, RoundCorners b)
{
if (a is null)
{
return b;
}
else if (b is null)
{
return a;
}
if (!b.Radius.IsAnimated)
{
if (b.Radius.InitialValue >= 0)
{
// If b has a non-0 value, it wins.
return b;
}
else
{
// b is always 0. A wins.
return a;
}
}
_issues.MultipleAnimatedRoundCornersIsNotSupported();
return b;
}
TrimPath ComposeTrimPaths(TrimPath a, TrimPath b)
{
if (a is null)
{
return b;
}
else if (b is null)
{
return a;
}
if (!a.Start.IsAnimated && !a.Start.IsAnimated && !a.Offset.IsAnimated)
{
// a is not animated.
if (!b.Start.IsAnimated && !b.Start.IsAnimated && !b.Offset.IsAnimated)
{
// Both are not animated.
if (a.Start.InitialValue == b.End.InitialValue)
{
// a trims out everything. b is unnecessary.
return a;
}
else if (b.Start.InitialValue == b.End.InitialValue)
{
// b trims out everything. a is unnecessary.
return b;
}
else if (a.Start.InitialValue.Value == 0 && a.End.InitialValue.Value == 1 && a.Offset.InitialValue.Degrees == 0)
{
// a is trimming nothing. a is unnecessary.
return b;
}
else if (b.Start.InitialValue.Value == 0 && b.End.InitialValue.Value == 1 && b.Offset.InitialValue.Degrees == 0)
{
// b is trimming nothing. b is unnecessary.
return a;
}
}
}
_issues.MultipleTrimPathsIsNotSupported();
return b;
}
}
}

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

@ -53,7 +53,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
internal void MergingALargeNumberOfShapesIsNotSupported() => Report("LT0010", "Merging a large number of shape is not supported.");
internal void MultipleAnimatedRoundedCornersIsNotSupported() => Report("LT0011", "Multiple animated rounded corners is not supported.");
internal void MultipleAnimatedRoundCornersIsNotSupported() => Report("LT0011", "Multiple animated round corners is not supported.");
internal void MultipleFillsIsNotSupported() => Report("LT0012", "Multiple fills is not supported.");
@ -64,7 +64,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
// LT0015 has been deprecated.
// Was: Opacity and color animated at the same time is not supported.
internal void PathWithRoundedCornersIsNotSupported() => Report("LT0016", "Path with rounded corners is not supported.");
internal void PathWithRoundCornersIsNotSupported() => Report("LT0016", "Path with round corners is not supported.");
internal void PolystarIsNotSupported() => Report("LT0017", "Polystar is not supported.");
@ -110,6 +110,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
internal void CombiningMultipleAnimatedPathsIsNotSupported() => Report("LT0036", "Combining multiple animated paths is not supported.");
internal void ConflictingRoundnessAndRadiusIsNotSupported() => Report("LT0037", "Rectangle roundness with round corners is not supported.");
void Report(string code, string description)
{
_issues.Add((code, description));

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

@ -51,7 +51,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
/// Returns <c>true</c> if this value is always equal to the given value.
/// </summary>
/// <returns><c>true</c> if this value is always equal to the given value.</returns>
internal bool AlwaysEquals(T value) => !IsAnimated && value.Equals(InitialValue);
internal bool Always(T value) => !IsAnimated && value.Equals(InitialValue);
internal TranslationContext Context { get; }
}