Skip to content

Commit 96eeab4

Browse files
authored
Add support for round corners if there is no animations (#451)
* Add support for round corners if there is no animations
1 parent d87f131 commit 96eeab4

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

source/Animatables/Vector2.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,9 @@ public Vector2(double x, double y)
6767

6868
/// <inheritdoc/>
6969
public override string ToString() => $"{{{X},{Y}}}";
70+
71+
public double Length() => Math.Sqrt((X * X) + (Y * Y));
72+
73+
public Vector2 Normalized() => this / Length();
7074
}
7175
}

source/LottieToWinComp/Paths.cs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public static CompositionShape TranslatePathContent(ShapeContext context, Path p
117117
geometry.SetDescription(context, () => $"{path.Name}.PathGeometry");
118118

119119
var pathData = Optimizer.TrimAnimatable(context, Optimizer.GetOptimized(context, path.Data));
120+
pathData = TranslateRoundCorners(context, pathData);
120121

121122
var compositionSpriteShape = TranslatePath(context, pathData, GetPathFillType(context.Fill));
122123
compositionSpriteShape.SetDescription(context, () => path.Name);
@@ -129,6 +130,21 @@ public static CompositionShape TranslatePathContent(ShapeContext context, Path p
129130
return compositionSpriteShape;
130131
}
131132

133+
private static TrimmedAnimatable<PathGeometry> TranslateRoundCorners(ShapeContext context, TrimmedAnimatable<PathGeometry> pathGeometry)
134+
{
135+
if (context.RoundCorners.Radius.IsAlways(0) || context.RoundCorners.Radius.IsAnimated)
136+
{
137+
return pathGeometry;
138+
}
139+
140+
if (!pathGeometry.IsAnimated)
141+
{
142+
return new TrimmedAnimatable<PathGeometry>(pathGeometry.Context, MakeRoundCorners(pathGeometry.InitialValue, context.RoundCorners.Radius.InitialValue));
143+
}
144+
145+
return pathGeometry;
146+
}
147+
132148
/// <summary>
133149
/// Groups multiple Shapes into a D2D geometry group.
134150
/// </summary>
@@ -410,5 +426,152 @@ sealed class StateCache
410426
public Dictionary<(PathGeometry, ShapeFill.PathFillType, bool), CompositionPath> CompositionPaths { get; }
411427
= new Dictionary<(PathGeometry, ShapeFill.PathFillType, bool), CompositionPath>();
412428
}
429+
430+
static BezierSegment MakeCubicBezier(Vector2 cp0, Vector2 cp1, Vector2 cp2)
431+
{
432+
// This is similar to converting QudraticBezier to CubicBezier, but instead of
433+
// coefficiet 2/3 we are using 0.55 . This coefficient was found experimentally by comparing
434+
// images from AfterEffects and LottieViewer.
435+
return new BezierSegment(cp0, cp0 + ((cp1 - cp0) * 0.55), cp2 + ((cp1 - cp2) * 0.55), cp2);
436+
}
437+
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)
453+
{
454+
// There is no corners if we have less than two segments.
455+
if (pathGeometry.BezierSegments.Count < 2)
456+
{
457+
return pathGeometry;
458+
}
459+
460+
var count = pathGeometry.BezierSegments.Count;
461+
462+
// We treat array of segments as a circular array, so that first and last elements are adjacent.
463+
Func<int, int> getCircularIndex = (i) => ((i % count) + count) % count;
464+
465+
// Initial value does not matter, it is guranteed that it will be reassigned before use.
466+
Vector2 prevControlPoint = Vector2.Zero;
467+
468+
List<BezierSegment> resultSegments = new List<BezierSegment>();
469+
470+
// If path is closed we are processing last segment at the beggining to get
471+
// proper value for prevControlPoint for first segment, so we are starting from -1.
472+
for (var i = pathGeometry.IsClosed ? -1 : 0; i < count; i++)
473+
{
474+
var segment = pathGeometry.BezierSegments[getCircularIndex(i)];
475+
var prevSegment = pathGeometry.BezierSegments[getCircularIndex(i - 1)];
476+
var nextSegment = pathGeometry.BezierSegments[getCircularIndex(i + 1)];
477+
478+
// We can make rounded corner at the beggining of the segment if we do not have any curvature
479+
// at point ControlPoint0 except if path is not closed and this segment is the first
480+
bool canRoundBegin =
481+
segment.ControlPoint0 == segment.ControlPoint1 &&
482+
prevSegment.ControlPoint2 == prevSegment.ControlPoint3 &&
483+
(getCircularIndex(i) != 0 || pathGeometry.IsClosed);
484+
485+
// We can make rounded corner at the end of the segment if we do not have any curvature
486+
// at point ControlPoint3 except if path is not closed and this segment is the last
487+
bool canRoundEnd = segment.ControlPoint2 == segment.ControlPoint3 &&
488+
nextSegment.ControlPoint0 == nextSegment.ControlPoint1 &&
489+
(getCircularIndex(i) != count - 1 || pathGeometry.IsClosed);
490+
491+
if (!canRoundBegin && !canRoundEnd)
492+
{
493+
// Both ends are curved, just adding current segment to the result.
494+
resultSegments.Add(segment);
495+
}
496+
else if (canRoundBegin && canRoundEnd)
497+
{
498+
// Both ends are not curved, so we can make them rounded.
499+
var cp0cp3 = segment.ControlPoint3 - segment.ControlPoint0;
500+
var length = cp0cp3.Length();
501+
var radiusVector = cp0cp3.Normalized() * radius;
502+
503+
// We are moving both ends of the segment, towards the center for "radius" pixels
504+
// Example:
505+
// Segment of length 13: p0-------------p1
506+
// #1 Radius 2: --p0---------p1--
507+
// #2 Radius 8: ------p0-p1------
508+
// In case #2 points has changed their relative order along the segment, so in this case
509+
// if doubled radius is greater than the length, we are moving both point to the middle:
510+
// -------p01------- (p0 = p1)
511+
// Case #1:
512+
Vector2 point0 = segment.ControlPoint0 + radiusVector;
513+
Vector2 point1 = segment.ControlPoint3 - radiusVector;
514+
515+
// If doubled radius is greater than length, then both points collapse into
516+
// one point right in the middle of the segment.
517+
// Case #2:
518+
if (length <= 2 * radius)
519+
{
520+
point0 = point1 = (segment.ControlPoint0 + segment.ControlPoint3) * 0.5;
521+
}
522+
523+
// Rounded corner.
524+
resultSegments.Add(MakeCubicBezier(prevControlPoint, segment.ControlPoint0, point0));
525+
526+
// Straigt line that connects point0 and point1.
527+
if (point0 != point1)
528+
{
529+
resultSegments.Add(new BezierSegment(point0, point0, point1, point1));
530+
}
531+
532+
// In the next iteration we will use this to make the next rounded corner.
533+
prevControlPoint = point1;
534+
}
535+
else if (canRoundBegin)
536+
{
537+
// Second end is curved, so we can make only one rounded corner.
538+
var cp0cp2 = segment.ControlPoint2 - segment.ControlPoint0;
539+
var length = cp0cp2.Length();
540+
var radiusVector = cp0cp2.Normalized() * radius;
541+
542+
Vector2 point = length > radius ? segment.ControlPoint0 + radiusVector : segment.ControlPoint2;
543+
544+
// Rounded corner.
545+
resultSegments.Add(MakeCubicBezier(prevControlPoint, segment.ControlPoint0, point));
546+
547+
// Adusted bezier segment.
548+
resultSegments.Add(MakeCubicBezier(point, segment.ControlPoint2, segment.ControlPoint3));
549+
}
550+
else if (canRoundEnd)
551+
{
552+
// Second end is curved, so we can make only one rounded corner.
553+
var cp3cp1 = segment.ControlPoint1 - segment.ControlPoint3;
554+
var length = cp3cp1.Length();
555+
var radiusVector = cp3cp1.Normalized() * radius;
556+
557+
Vector2 point = length > radius ? segment.ControlPoint3 + radiusVector : segment.ControlPoint1;
558+
559+
// Adjusted bezier segment.
560+
resultSegments.Add(MakeCubicBezier(segment.ControlPoint0, segment.ControlPoint1, point));
561+
562+
// In the next iteration we will use this to make the next rounded corner.
563+
prevControlPoint = point;
564+
}
565+
566+
if (i == -1)
567+
{
568+
// If i = -1 then it was special pre-pass to get the valid value of prevControlPoint,
569+
// all generated segments should be ignored.
570+
resultSegments.Clear();
571+
}
572+
}
573+
574+
return new PathGeometry(new Sequence<BezierSegment>(resultSegments), pathGeometry.IsClosed);
575+
}
413576
}
414577
}

0 commit comments

Comments
 (0)