Skip to content

Commit 1b4e3d2

Browse files
authored
Added support for animated round corners modifier (#452)
* Added support for animated round corners modifier * Fixed typo and epsilon calculation * Typo fix [2] * Added new issue code LT0043 + more comments in the code
1 parent 96eeab4 commit 1b4e3d2

File tree

7 files changed

+111
-25
lines changed

7 files changed

+111
-25
lines changed

Lottie-Windows.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Issues", "Issues", "{07D6DF
131131
source\Issues\LT0040.md = source\Issues\LT0040.md
132132
source\Issues\LT0041.md = source\Issues\LT0041.md
133133
source\Issues\LT0042.md = source\Issues\LT0042.md
134+
source\Issues\LT0043.md = source\Issues\LT0043.md
134135
source\Issues\LV0001.md = source\Issues\LV0001.md
135136
source\Issues\LV0002.md = source\Issues\LV0002.md
136137
source\Issues\LV0003.md = source\Issues\LV0003.md

LottieGen/LottieGen.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Issues", "Issues", "{BDB88D
8686
..\source\Issues\LT0040.md = ..\source\Issues\LT0040.md
8787
..\source\Issues\LT0041.md = ..\source\Issues\LT0041.md
8888
..\source\Issues\LT0042.md = ..\source\Issues\LT0042.md
89+
..\source\Issues\LT0043.md = ..\source\Issues\LT0043.md
8990
..\source\Issues\LV0001.md = ..\source\Issues\LV0001.md
9091
..\source\Issues\LV0002.md = ..\source\Issues\LV0002.md
9192
..\source\Issues\LV0003.md = ..\source\Issues\LV0003.md

source/Issues/LT0016.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ by raising it as an issue [here](https://github.com/windows-toolkit/Lottie-Windo
1212
## Resources
1313

1414
* [Lottie-Windows repository](https://aka.ms/lottie)
15-
* [Questions and feedback via Github](https://github.com/windows-toolkit/Lottie-Windows/issues)
15+
* [Questions and feedback via Github](https://github.com/windows-toolkit/Lottie-Windows/issues)

source/Issues/LT0043.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[comment]: # (name:PathWithRoundCornersIsNotFullySupported)
2+
[comment]: # (text:Path with round corners is not fully supported.)
3+
4+
# Lottie-Windows Warning LT0043
5+
6+
The Lottie file specifies round corners on a path. This is not fully supported by Lottie-Windows.
7+
It works correctly if the radius and shape path are not animated. If both radius and shape path
8+
are animated, the round corner modifier will not be applied at all, otherwise if only one of them
9+
is animated Lottie-Windows will try to generate an animation but it is not guaranteed to look exactly
10+
the same as in After Effects.
11+
12+
## Remarks
13+
If support for this feature is important for your scenario please provide feedback
14+
by raising it as an issue [here](https://github.com/windows-toolkit/Lottie-Windows/issues).
15+
16+
## Resources
17+
18+
* [Lottie-Windows repository](https://aka.ms/lottie)
19+
* [Questions and feedback via Github](https://github.com/windows-toolkit/Lottie-Windows/issues)

source/LottieToWinComp/Paths.cs

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -132,17 +132,57 @@ public static CompositionShape TranslatePathContent(ShapeContext context, Path p
132132

133133
private static TrimmedAnimatable<PathGeometry> TranslateRoundCorners(ShapeContext context, TrimmedAnimatable<PathGeometry> pathGeometry)
134134
{
135-
if (context.RoundCorners.Radius.IsAlways(0) || context.RoundCorners.Radius.IsAnimated)
135+
var radius = context.RoundCorners.Radius;
136+
137+
if (radius.IsAlways(0))
136138
{
137139
return pathGeometry;
138140
}
139141

140-
if (!pathGeometry.IsAnimated)
142+
bool animated = radius.IsAnimated || pathGeometry.IsAnimated;
143+
144+
// Unfortunately, it is not 100% perfect when animated and does not produce animation identical to After Effects.
145+
// After Effects applies radius to the shape after the interpolation, but we are applying radius to the keyframes and
146+
// then Composition layer interpolates paths that are already rounded.
147+
if (animated)
141148
{
142-
return new TrimmedAnimatable<PathGeometry>(pathGeometry.Context, MakeRoundCorners(pathGeometry.InitialValue, context.RoundCorners.Radius.InitialValue));
149+
context.Issues.PathWithRoundCornersIsNotFullySupported();
143150
}
144151

145-
return pathGeometry;
152+
if (radius.IsAnimated && pathGeometry.IsAnimated)
153+
{
154+
// We do not support animating both yet.
155+
return pathGeometry;
156+
}
157+
158+
// If there is an animation we do not want to optimize number of points on resulting path because
159+
// we want the number of points to be the same for different keyframes.
160+
var initialValue = MakeRoundCorners(pathGeometry.InitialValue, radius.InitialValue, optimizeNumberOfPoints: !animated);
161+
162+
if (pathGeometry.IsAnimated)
163+
{
164+
List<KeyFrame<PathGeometry>> keyframes = new List<KeyFrame<PathGeometry>>();
165+
foreach (var keyframe in pathGeometry.KeyFrames)
166+
{
167+
var path = MakeRoundCorners(keyframe.Value, context.RoundCorners.Radius.InitialValue, optimizeNumberOfPoints: false);
168+
keyframes.Add(new KeyFrame<PathGeometry>(keyframe.Frame, path, keyframe.SpatialBezier, keyframe.Easing));
169+
}
170+
171+
return new TrimmedAnimatable<PathGeometry>(pathGeometry.Context, initialValue, keyframes);
172+
}
173+
else if (radius.IsAnimated)
174+
{
175+
List<KeyFrame<PathGeometry>> keyframes = new List<KeyFrame<PathGeometry>>();
176+
foreach (var keyframe in radius.KeyFrames)
177+
{
178+
var path = MakeRoundCorners(pathGeometry.InitialValue, keyframe.Value, optimizeNumberOfPoints: false);
179+
keyframes.Add(new KeyFrame<PathGeometry>(keyframe.Frame, path, keyframe.SpatialBezier, keyframe.Easing));
180+
}
181+
182+
return new TrimmedAnimatable<PathGeometry>(pathGeometry.Context, initialValue, keyframes);
183+
}
184+
185+
return new TrimmedAnimatable<PathGeometry>(pathGeometry.Context, initialValue);
146186
}
147187

148188
/// <summary>
@@ -435,21 +475,27 @@ static BezierSegment MakeCubicBezier(Vector2 cp0, Vector2 cp1, Vector2 cp2)
435475
return new BezierSegment(cp0, cp0 + ((cp1 - cp0) * 0.55), cp2 + ((cp1 - cp2) * 0.55), cp2);
436476
}
437477

438-
// The way this function works is it detects if two segments form a corner (if they do not have smooth connection)
439-
// Then it duplicates this point (shared by two segments) and moves newly generated points in different directions
440-
// for "radius" pixels along the segment.
441-
// After that we are joining both new points with a bezier curve to make the corner look rounded.
442-
//
443-
// There are 3 possible cases:
444-
// 1. When the segment is curved from both sides, then we can just keep it as it is
445-
// 2. When the segment is not curved from both sides, then we can make two rounded corners, from both ends.
446-
// 3. When the segment curved from one side (begin or end) we are making only one rounded corner.
447-
//
448-
// In order to make a rounded corner we also need two points on segments next to the current segment.
449-
// In this algortihm we are processing segments one by one, and passing one point from one segment to another,
450-
// so that currently processed segment can create rounded corner using this point, and pass new point
451-
// to the next segment, so that it can create next rounded corner and so on.
452-
static PathGeometry MakeRoundCorners(PathGeometry pathGeometry, double radius)
478+
/// <summary>
479+
/// The way this function works is it detects if two segments form a corner (if they do not have smooth connection)
480+
/// Then it duplicates this point (shared by two segments) and moves newly generated points in different directions
481+
/// for "radius" pixels along the segment.
482+
/// After that we are joining both new points with a bezier curve to make the corner look rounded.
483+
///
484+
/// There are 3 possible cases:
485+
/// 1. When the segment is curved from both sides, then we can just keep it as it is
486+
/// 2. When the segment is not curved from both sides, then we can make two rounded corners, from both ends.
487+
/// 3. When the segment curved from one side (begin or end) we are making only one rounded corner.
488+
///
489+
/// In order to make a rounded corner we also need two points on segments next to the current segment.
490+
/// In this algortihm we are processing segments one by one, and passing one point from one segment to another,
491+
/// so that currently processed segment can create rounded corner using this point, and pass new point
492+
/// to the next segment, so that it can create next rounded corner and so on.
493+
/// </summary>
494+
/// <param name="pathGeometry">Path.</param>
495+
/// <param name="radius">Radius of corners.</param>
496+
/// <param name="optimizeNumberOfPoints">Use optimizeNumberOfPoints = false if you need to keep the number of points constant,
497+
/// it can be needed for animated path.</param>
498+
static PathGeometry MakeRoundCorners(PathGeometry pathGeometry, double radius, bool optimizeNumberOfPoints = true)
453499
{
454500
// There is no corners if we have less than two segments.
455501
if (pathGeometry.BezierSegments.Count < 2)
@@ -517,7 +563,23 @@ static PathGeometry MakeRoundCorners(PathGeometry pathGeometry, double radius)
517563
// Case #2:
518564
if (length <= 2 * radius)
519565
{
520-
point0 = point1 = (segment.ControlPoint0 + segment.ControlPoint3) * 0.5;
566+
if (optimizeNumberOfPoints)
567+
{
568+
// Middle of the segment (ControlPoint0; ControlPoint3)
569+
point0 = point1 = (segment.ControlPoint0 + segment.ControlPoint3) * 0.5;
570+
}
571+
else
572+
{
573+
float halfPlusEpsilon = Float32.NextLargerThan(0.5f);
574+
float halfMinusEpsilon = 1 - halfPlusEpsilon;
575+
576+
// Generate two points instead of one, but place them close to each other.
577+
// These two points are placed on the segment (ControlPoint0; ControlPoint3)
578+
// Almost in the middle but point0 is a bit closer to the ControlPoint0 and
579+
// point1 is a bit closer po ControlPoint3.
580+
point0 = (segment.ControlPoint0 * halfPlusEpsilon) + (segment.ControlPoint3 * halfMinusEpsilon);
581+
point1 = (segment.ControlPoint0 * halfMinusEpsilon) + (segment.ControlPoint3 * halfPlusEpsilon);
582+
}
521583
}
522584

523585
// Rounded corner.

source/LottieToWinComp/Shapes.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,17 @@ static CompositionShape TranslateShapeLayerContents(
183183
paths.Add(Optimizer.OptimizePath(context, (Path)stack.Pop()));
184184
}
185185

186-
CheckForRoundCornersOnPath(context);
187-
188186
if (paths.Count == 1)
189187
{
190188
// There's a single path.
191189
container.Shapes.Add(Paths.TranslatePathContent(context, paths[0]));
192190
}
193191
else
194192
{
193+
// TODO: add support for round corners for multiple paths. I didn't find a way to generate
194+
// AE animation with multiple paths on the same shape.
195+
CheckForRoundCornersOnPath(context);
196+
195197
// There are multiple paths. They need to be grouped.
196198
container.Shapes.Add(Paths.TranslatePathGroupContent(context, paths));
197199
}
@@ -631,8 +633,7 @@ static void CheckForRoundCornersOnPath(ShapeContext context)
631633
{
632634
if (!Optimizer.TrimAnimatable(context, context.RoundCorners.Radius).IsAlways(0))
633635
{
634-
// TODO - can round corners be implemented by composing cubic Beziers?
635-
context.Issues.PathWithRoundCornersIsNotSupported();
636+
context.Issues.PathWithRoundCornersIsNotFullySupported();
636637
}
637638
}
638639

source/LottieToWinComp/TranslationIssues.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ internal TranslationIssues(bool throwOnIssue)
122122

123123
internal void UnsupportedLayerEffectParameter(string layerEffectType, string parameterName, string value) => Report("LT0042", $"Layer effects of type {layerEffectType} do not support {parameterName} values of {value}.");
124124

125+
internal void PathWithRoundCornersIsNotFullySupported() => Report("LT0043", "Using a path with rounded corners can lead to inaccurate results.");
126+
125127
void Report(string code, string description)
126128
{
127129
_issues.Add((code, description));

0 commit comments

Comments
 (0)