Skip to content

Commit d87f131

Browse files
authored
Merge pull request #450 from windows-toolkit/user/aborziak-ms/rectangles-gradient-fill-fix
Gradient fill for rectangle(rounded rectangle) shape fixed
2 parents ee3344e + 60a2c6b commit d87f131

File tree

6 files changed

+137
-20
lines changed

6 files changed

+137
-20
lines changed

source/LottieToWinComp/Brushes.cs

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,60 @@ static CompositionColorBrush TranslateBoundSolidColor(
406406
return result;
407407
}
408408

409+
// Animate Vector2 property of object with TrimmedAnimatable while applying OriginOffset to it.
410+
// Returns non-null Sn.Vector2 value if no animation is needed (and no animation was applied).
411+
static Sn.Vector2? AnimateVector2WithOriginOffsetOrGetValue(
412+
LayerContext context,
413+
CompositionObject obj,
414+
string propertyName,
415+
TrimmedAnimatable<Vector2> value)
416+
{
417+
if (context is not ShapeLayerContext || ((ShapeLayerContext)context).OriginOffset is null)
418+
{
419+
if (value.IsAnimated)
420+
{
421+
Animate.Vector2(context, value, obj, propertyName);
422+
return null;
423+
}
424+
else
425+
{
426+
return ConvertTo.Vector2(value.InitialValue);
427+
}
428+
}
429+
430+
var offset = ((ShapeLayerContext)context).OriginOffset!;
431+
432+
if (!offset.IsAnimated && !value.IsAnimated)
433+
{
434+
return ConvertTo.Vector2(value.InitialValue) + offset.OffsetValue;
435+
}
436+
437+
// Animate source property first.
438+
// We are using this auxiliary property to store original animation,
439+
// so that its value can be used in expression animation of property itself.
440+
string sourcePropertyName = propertyName + "Source";
441+
obj.Properties.InsertVector2(sourcePropertyName, ConvertTo.Vector2(value.InitialValue));
442+
Animate.Vector2(context, value, obj, sourcePropertyName);
443+
444+
// Create expression that offsets source property by origin offset.
445+
WinCompData.Expressions.Vector2 expression = offset.IsAnimated ?
446+
ExpressionFactory.OriginOffsetExressionAdded(sourcePropertyName, offset.OffsetExpression) :
447+
ExpressionFactory.OriginOffsetValueAdded(sourcePropertyName, offset.OffsetValue);
448+
449+
var expressionAnimation = context.ObjectFactory.CreateExpressionAnimation(expression);
450+
expressionAnimation.SetReferenceParameter("my", obj);
451+
if (offset.IsAnimated)
452+
{
453+
// Expression can use geometry.
454+
expressionAnimation.SetReferenceParameter("geometry", offset.Geometry);
455+
}
456+
457+
// Animate original property with expression that applies origin offset to it.
458+
Animate.WithExpression(obj, expressionAnimation, propertyName);
459+
460+
return null;
461+
}
462+
409463
static CompositionLinearGradientBrush? TranslateLinearGradient(
410464
LayerContext context,
411465
IGradient linearGradient,
@@ -419,22 +473,16 @@ static CompositionColorBrush TranslateBoundSolidColor(
419473
var startPoint = Optimizer.TrimAnimatable(context, linearGradient.StartPoint);
420474
var endPoint = Optimizer.TrimAnimatable(context, linearGradient.EndPoint);
421475

422-
if (startPoint.IsAnimated)
423-
{
424-
Animate.Vector2(context, startPoint, result, nameof(result.StartPoint));
425-
}
426-
else
476+
var startPointValue = AnimateVector2WithOriginOffsetOrGetValue(context, result, nameof(result.StartPoint), startPoint);
477+
if (startPointValue is not null)
427478
{
428-
result.StartPoint = ConvertTo.Vector2(startPoint.InitialValue);
479+
result.StartPoint = startPointValue!;
429480
}
430481

431-
if (endPoint.IsAnimated)
432-
{
433-
Animate.Vector2(context, endPoint, result, nameof(result.EndPoint));
434-
}
435-
else
482+
var endPointValue = AnimateVector2WithOriginOffsetOrGetValue(context, result, nameof(result.EndPoint), endPoint);
483+
if (endPointValue is not null)
436484
{
437-
result.EndPoint = ConvertTo.Vector2(endPoint.InitialValue);
485+
result.EndPoint = endPointValue!;
438486
}
439487

440488
var gradientStops = Optimizer.TrimAnimatable(context, linearGradient.GradientStops);
@@ -470,13 +518,10 @@ static CompositionColorBrush TranslateBoundSolidColor(
470518
var startPoint = Optimizer.TrimAnimatable(context, gradient.StartPoint);
471519
var endPoint = Optimizer.TrimAnimatable(context, gradient.EndPoint);
472520

473-
if (startPoint.IsAnimated)
474-
{
475-
Animate.Vector2(context, startPoint, result, nameof(result.EllipseCenter));
476-
}
477-
else
521+
var startPointValue = AnimateVector2WithOriginOffsetOrGetValue(context, result, nameof(result.EllipseCenter), startPoint);
522+
if (startPointValue is not null)
478523
{
479-
result.EllipseCenter = ConvertTo.Vector2(startPoint.InitialValue);
524+
result.EllipseCenter = startPointValue!;
480525
}
481526

482527
if (endPoint.IsAnimated)

source/LottieToWinComp/ExpressionFactory.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ static class ExpressionFactory
3333
internal static readonly Scalar RootProgress = RootScalar(TranslationContext.ProgressPropertyName);
3434
internal static readonly Scalar MaxTStartTEnd = Max(MyTStart, MyTEnd);
3535
internal static readonly Scalar MinTStartTEnd = Min(MyTStart, MyTEnd);
36-
static readonly Vector2 HalfMySize = MySize / Vector2(2, 2);
36+
internal static readonly Vector2 HalfMySize = MySize / Vector2(2, 2);
37+
internal static readonly Vector2 GeometryHalfSize = NamedVector2("geometry", "Size") / Vector2(2, 2);
3738
internal static readonly Color AnimatedColorWithAnimatedOpacity =
3839
ColorAsVector4MultipliedByOpacities(MyColor, new[] { MyOpacity });
3940

@@ -46,6 +47,10 @@ static class ExpressionFactory
4647
MyPosition.Y - MyAnchor.Y,
4748
0);
4849

50+
internal static Vector2 OriginOffsetExressionAdded(string property, Vector2 offsetExpression) => MyVector2(property) + offsetExpression;
51+
52+
internal static Vector2 OriginOffsetValueAdded(string property, Sn.Vector2 offsetValue) => MyVector2(property) + Vector2(offsetValue);
53+
4954
internal static Color ThemedColorMultipliedByOpacity(string bindingName, Animatables.Opacity opacity)
5055
=> ColorAsVector4MultipliedByOpacity(ThemedColor4Property(bindingName), opacity.Value);
5156

@@ -248,10 +253,14 @@ static Scalar CreateProgressExpression(ArraySegment<Segment> segments, Scalar pr
248253

249254
static Vector2 MyVector2(string propertyName) => Vector2(My(propertyName));
250255

256+
static Vector2 NamedVector2(string name, string propertyName) => Vector2(Named(name, propertyName));
257+
251258
static Vector4 MyVector4(string propertyName) => Vector4(My(propertyName));
252259

253260
static string My(string propertyName) => $"my.{propertyName}";
254261

262+
static string Named(string name, string propertyName) => $"{name}.{propertyName}";
263+
255264
// A property on the root property set. Used to bind to the property set that contains the Progress property.
256265
static string RootProperty(string propertyName) => $"{RootName}.{propertyName}";
257266

source/LottieToWinComp/Rectangles.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
99
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
1010
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgcg;
11+
using static Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp.ShapeLayerContext;
1112
using Expressions = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions;
1213
using Sn = System.Numerics;
1314

@@ -318,6 +319,11 @@ static void ApplyRectangleContentCommon(
318319
var height = size.InitialValue.Y;
319320
var trimOffsetDegrees = (width / (2 * (width + height))) * 360;
320321

322+
// If offset is not animated then other computations for fill brush can be optimized.
323+
context.LayerContext.OriginOffset = size.IsAnimated ?
324+
new OriginOffsetContainer(geometry, ExpressionFactory.GeometryHalfSize) :
325+
new OriginOffsetContainer(geometry, ConvertTo.Vector2(size.InitialValue / 2));
326+
321327
Shapes.TranslateAndApplyShapeContextWithTrimOffset(
322328
context,
323329
compositionRectangle,
@@ -408,6 +414,11 @@ static void ApplyRectangleContentCommonXY(
408414
var initialHeight = height.InitialValue;
409415
var trimOffsetDegrees = (initialWidth / (2 * (initialWidth + initialHeight))) * 360;
410416

417+
// If offset is not animated then other computations for fill brush can be optimized.
418+
context.LayerContext.OriginOffset = width.IsAnimated || height.IsAnimated ?
419+
new OriginOffsetContainer(geometry, ExpressionFactory.GeometryHalfSize) :
420+
new OriginOffsetContainer(geometry, ConvertTo.Vector2(width.InitialValue / 2, height.InitialValue / 2));
421+
411422
Shapes.TranslateAndApplyShapeContextWithTrimOffset(
412423
context,
413424
compositionRectangle,

source/LottieToWinComp/ShapeLayerContext.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44

55
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
6+
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions;
7+
using Sn = System.Numerics;
68

79
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
810
{
@@ -14,6 +16,45 @@ internal ShapeLayerContext(CompositionContext compositionContext, ShapeLayer lay
1416
Layer = layer;
1517
}
1618

19+
// Rectangles are implemented differently in WinComp API
20+
// and Lottie. In WinComp API coordinates inside rectangle start in
21+
// top left corner and in Lottie they start in the middle
22+
// To account for this we need to offset all the points inside
23+
// the rectangle for (Rectangle.Size / 2).
24+
// This class represents this offset (static or animated)
25+
public class OriginOffsetContainer
26+
{
27+
public RectangleOrRoundedRectangleGeometry Geometry { get; }
28+
29+
// Use expression if size is animated
30+
public WinCompData.Expressions.Vector2 OffsetExpression { get; }
31+
32+
// Use constant value if size is static
33+
public Sn.Vector2 OffsetValue { get; }
34+
35+
// IsAnimated = true means that we have to use OffsetExpression.
36+
// IsAnimated = false means that we can use OffsetValue instead of OffsetExpression to optimize the code.
37+
public bool IsAnimated { get; }
38+
39+
public OriginOffsetContainer(RectangleOrRoundedRectangleGeometry geometry, WinCompData.Expressions.Vector2 expression)
40+
{
41+
IsAnimated = true;
42+
Geometry = geometry;
43+
OffsetExpression = expression;
44+
OffsetValue = new Sn.Vector2(0, 0);
45+
}
46+
47+
public OriginOffsetContainer(RectangleOrRoundedRectangleGeometry geometry, Sn.Vector2 value)
48+
{
49+
IsAnimated = false;
50+
Geometry = geometry;
51+
OffsetExpression = Expression.Vector2(value.X, value.Y);
52+
OffsetValue = value;
53+
}
54+
}
55+
56+
internal OriginOffsetContainer? OriginOffset { get; set; }
57+
1758
public new ShapeLayer Layer { get; }
1859
}
1960
}

source/LottieToWinComp/Shapes.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public static void TranslateAndApplyShapeContextWithTrimOffset(
4545
Debug.Assert(shape.Geometry is not null, "Precondition");
4646

4747
shape.FillBrush = Brushes.TranslateShapeFill(context, context.Fill, context.Opacity);
48+
49+
// OriginOffset is used to adjust cordinates of FillBrush for Rectangle shapes.
50+
// It is not needed afterwards, so we clean it up to not affect other code.
51+
context.LayerContext.OriginOffset = null;
52+
4853
Brushes.TranslateAndApplyStroke(context, context.Stroke, shape, context.Opacity);
4954

5055
TranslateAndApplyTrimPath(

source/UIDataCodeGen/CodeGen/InstantiatorGeneratorBase.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2130,7 +2130,13 @@ void WriteCallHelperCreateSpriteShapeWithFillBrush(
21302130

21312131
// Call the helper and initialize the remaining CompositionShape properties.
21322132
WriteMatrixComment(builder, obj.TransformMatrix);
2133-
builder.WriteLine($"{ConstVar} result = CreateSpriteShape({CallFactoryFromFor(node, obj.Geometry)}, {Matrix3x2(transformMatrix)}, {CallFactoryFromFor(node, obj.FillBrush)});");
2133+
2134+
// We need to instantiate geometry first because sometimes it initializes fields
2135+
// that are used in FillBrush, but CreateSpriteShape(GetGeometry(), ..., GetFillBrush()) code
2136+
// will result in evaluating GetFillBrush() first which may cause null dereferencing
2137+
builder.WriteLine($"{ConstVar} geometry = {CallFactoryFromFor(node, obj.Geometry)};");
2138+
2139+
builder.WriteLine($"{ConstVar} result = CreateSpriteShape(geometry, {Matrix3x2(transformMatrix)}, {CallFactoryFromFor(node, obj.FillBrush)});");
21342140
InitializeCompositionObject(builder, obj, node);
21352141
WriteSetPropertyStatement(builder, nameof(obj.CenterPoint), obj.CenterPoint);
21362142
WriteSetPropertyStatement(builder, nameof(obj.Offset), obj.Offset);

0 commit comments

Comments
 (0)