@@ -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