public static CanvasGeometry CreateWin2dEllipseGeometry(ShapeContext context, Ellipse ellipse) { var ellipsePosition = Optimizer.TrimAnimatable(context, ellipse.Position); var ellipseDiameter = Optimizer.TrimAnimatable(context, ellipse.Diameter); if (ellipsePosition.IsAnimated || ellipseDiameter.IsAnimated) { context.Translation.Issues.CombiningAnimatedShapesIsNotSupported(); } var xRadius = ellipseDiameter.InitialValue.X / 2; var yRadius = ellipseDiameter.InitialValue.Y / 2; var result = CanvasGeometry.CreateEllipse( null, (float)(ellipsePosition.InitialValue.X - (xRadius / 2)), (float)(ellipsePosition.InitialValue.Y - (yRadius / 2)), (float)xRadius, (float)yRadius); var transformMatrix = Transforms.CreateMatrixFromTransform(context, context.Transform); if (!transformMatrix.IsIdentity) { result = result.Transform(transformMatrix); } result.SetDescription(context, () => ellipse.Name); return(result); }
static CompositionShape TranslateGroupShapeContent(ShapeContext context, ShapeGroup group) { var result = TranslateShapeLayerContents(context, group.Contents); result.SetDescription(context, () => $"ShapeGroup: {group.Name}"); return(result); }
// NOTES ABOUT RECTANGLE DRAWING AND CORNERS: // ========================================== // A rectangle can be thought of as having 8 components - // 4 sides (1,3,5,7) and 4 corners (2,4,6,8): // // 1 // 8╭ ─────────── ╮2 // ╷ ╷ // 7│ │3 // ╵ ╵ // 6╰ ─────────── ╯4 // 5 // // Windows.Composition draws in order 1,2,3,4,5,6,7,8. // // Lottie draws in one of two different ways depending on // whether the corners are controlled by RoundCorners or by // Rectangle.CornerRadius. // If RoundCorners the order is 2,3,4,5,6,7,8,1. // If Rectangle.CornerRadius the order is 3,4,5,6,7,8,1,2. // // If the corners have 0 radius, the corners are irrelevant // resulting in: // Windows.Composition: 1,3,5,7. // Lottie: 3,5,7,1. // // The order of drawing matters only if there is a TrimPath, and in // that case: // a) If there are no RoundCorners, a TrimOffset equivalent to 90 degrees // must be added. // b) If there are RoundCorners, swap width and height, rotate the rectangle // by 90 degrees around the center, and transform the trim path so that // it effectively draws in the reverse direction. // // TODO - the RoundCorners case with TrimPath is currently not handled correctly // and will cause the trim to appear to be rotated by 90 degrees. // // // Translates a Lottie rectangle to a CompositionShape. public static CompositionShape TranslateRectangleContent(ShapeContext context, Rectangle rectangle) { var result = context.ObjectFactory.CreateSpriteShape(); var position = Optimizer.TrimAnimatable(context, rectangle.Position); if (IsNonRounded(context, rectangle)) { // Non-rounded rectangles are slightly more efficient, but they can only be used // if there is no roundness or Round Corners. TranslateAndApplyNonRoundedRectangleContent( context, rectangle, position, result); } else { TranslateAndApplyRoundedRectangleContent( context, rectangle, position, result); } return(result); }
static CanvasGeometry?MergeShapeLayerContent(ShapeContext context, Stack <ShapeLayerContent> stack, MergePaths.MergeMode mergeMode) { var pathFillType = context.Fill is null ? ShapeFill.PathFillType.EvenOdd : context.Fill.FillType; var geometries = CreateCanvasGeometries(context, stack, pathFillType).ToArray(); return(geometries.Length switch { 0 => null, 1 => geometries[0], _ => CombineGeometries(context, geometries, mergeMode), });
public static void TranslateAndApplyShapeContext( ShapeContext context, CompositionSpriteShape shape, bool reverseDirection, double trimOffsetDegrees) { shape.FillBrush = Brushes.TranslateShapeFill(context, context.Fill, context.Opacity); Brushes.TranslateAndApplyStroke(context, context.Stroke, shape, context.Opacity); TranslateAndApplyTrimPath( context, shape.Geometry, reverseDirection, trimOffsetDegrees); }
static CanvasGeometry MergeShapeLayerContent(ShapeContext context, Stack <ShapeLayerContent> stack, MergePaths.MergeMode mergeMode) { var pathFillType = context.Fill is null ? ShapeFill.PathFillType.EvenOdd : context.Fill.FillType; var geometries = CreateCanvasGeometries(context, stack, pathFillType).ToArray(); switch (geometries.Length) { case 0: return(null); case 1: return(geometries[0]); default: return(CombineGeometries(context, geometries, mergeMode)); } }
public static void TranslateAndApplyShapeContextWithTrimOffset( ShapeContext context, CompositionSpriteShape shape, bool reverseDirection, double trimOffsetDegrees) { Debug.Assert(shape.Geometry != null, "Precondition"); shape.FillBrush = Brushes.TranslateShapeFill(context, context.Fill, context.Opacity); Brushes.TranslateAndApplyStroke(context, context.Stroke, shape, context.Opacity); TranslateAndApplyTrimPath( context, geometry: shape.Geometry !, reverseDirection, trimOffsetDegrees); }
static CompositionShape?TranslateMergePathsContent(ShapeContext context, Stack <ShapeLayerContent> stack, MergePaths.MergeMode mergeMode) { var mergedGeometry = MergeShapeLayerContent(context, stack, mergeMode); if (mergedGeometry != null) { var result = context.ObjectFactory.CreateSpriteShape(); result.Geometry = context.ObjectFactory.CreatePathGeometry(new CompositionPath(mergedGeometry)); TranslateAndApplyShapeContext( context, result, reverseDirection: false); return(result); } else { return(null); } }
public static void TranslateAndApplyShapeContextWithTrimOffset( ShapeContext context, CompositionSpriteShape shape, bool reverseDirection, double trimOffsetDegrees) { Debug.Assert(shape.Geometry is not null, "Precondition"); shape.FillBrush = Brushes.TranslateShapeFill(context, context.Fill, context.Opacity); // OriginOffset is used to adjust cordinates of FillBrush for Rectangle shapes. // It is not needed afterwards, so we clean it up to not affect other code. context.LayerContext.OriginOffset = null; Brushes.TranslateAndApplyStroke(context, context.Stroke, shape, context.Opacity); TranslateAndApplyTrimPath( context, geometry: shape.Geometry !, reverseDirection, trimOffsetDegrees); }
static CompositionShape TranslateShapeLayerContents( ShapeContext context, IReadOnlyList <ShapeLayerContent> contents) { // The Contents of a ShapeLayer is a list of instructions for a stack machine. // When evaluated, the stack of ShapeLayerContent produces a list of CompositionShape. // Some ShapeLayerContent modify the evaluation context (e.g. stroke, fill, trim) // Some ShapeLayerContent evaluate to geometries (e.g. any geometry, merge path) // Create a container to hold the contents. var container = context.ObjectFactory.CreateContainerShape(); // This is the object that will be returned. Containers may be added above this // as necessary to hold transforms. var result = container; // If the contents contains a repeater, generate repeated contents if (contents.Any(slc => slc.ContentType == ShapeContentType.Repeater)) { // The contents contains a repeater. Treat it as if there are n sets of items (where n // equals the Count of the repeater). In each set, replace the repeater with // the transform of the repeater, multiplied. // Find the index of the repeater var repeaterIndex = 0; while (contents[repeaterIndex].ContentType != ShapeContentType.Repeater) { // Keep going until the first repeater is found. repeaterIndex++; } // Get the repeater. var repeater = (Repeater)contents[repeaterIndex]; var repeaterCount = Optimizer.TrimAnimatable(context, repeater.Count); var repeaterOffset = Optimizer.TrimAnimatable(context, repeater.Offset); // Make sure we can handle it. if (repeaterCount.IsAnimated || repeaterOffset.IsAnimated || repeaterOffset.InitialValue != 0) { // TODO - handle all cases. context.Issues.RepeaterIsNotSupported(); } else { // Get the items before the repeater, and the items after the repeater. var itemsBeforeRepeater = contents.Slice(0, repeaterIndex).ToArray(); var itemsAfterRepeater = contents.Slice(repeaterIndex + 1).ToArray(); var nonAnimatedRepeaterCount = (int)Math.Round(repeaterCount.InitialValue); for (var i = 0; i < nonAnimatedRepeaterCount; i++) { // Treat each repeated value as a list of items where the repeater is replaced // by n transforms. // TODO - currently ignoring the StartOpacity and EndOpacity - should generate a new transform // that interpolates that. var generatedItems = itemsBeforeRepeater.Concat(Enumerable.Repeat(repeater.Transform, i + 1)).Concat(itemsAfterRepeater).ToArray(); // Recurse to translate the synthesized items. container.Shapes.Add(TranslateShapeLayerContents(context, generatedItems)); } return(result); } } CheckForUnsupportedShapeGroup(context, contents); var stack = new Stack <ShapeLayerContent>(contents.ToArray()); while (true) { context.UpdateFromStack(stack); if (stack.Count == 0) { break; } var shapeContent = stack.Pop(); // Complain if the BlendMode is not supported. if (shapeContent.BlendMode != BlendMode.Normal) { context.Issues.BlendModeNotNormal(context.LayerContext.Layer.Name, shapeContent.BlendMode.ToString()); } switch (shapeContent.ContentType) { case ShapeContentType.Ellipse: container.Shapes.Add(Ellipses.TranslateEllipseContent(context, (Ellipse)shapeContent)); break; case ShapeContentType.Group: container.Shapes.Add(TranslateGroupShapeContent(context.Clone(), (ShapeGroup)shapeContent)); break; case ShapeContentType.MergePaths: var mergedPaths = TranslateMergePathsContent(context, stack, ((MergePaths)shapeContent).Mode); if (mergedPaths != null) { container.Shapes.Add(mergedPaths); } break; case ShapeContentType.Path: { var paths = new List <Path>(); paths.Add(Optimizer.OptimizePath(context, (Path)shapeContent)); // Get all the paths that are part of the same group. while (stack.TryPeek(out var item) && item.ContentType == ShapeContentType.Path) { // Optimize the paths as they are added. Optimized paths have redundant keyframes // removed. Optimizing here increases the chances that an animated path will be // turned into a non-animated path which will allow us to group the paths. paths.Add(Optimizer.OptimizePath(context, (Path)stack.Pop())); } CheckForRoundCornersOnPath(context); if (paths.Count == 1) { // There's a single path. container.Shapes.Add(Paths.TranslatePathContent(context, paths[0])); } else { // There are multiple paths. They need to be grouped. container.Shapes.Add(Paths.TranslatePathGroupContent(context, paths)); } } break; case ShapeContentType.Polystar: context.Issues.PolystarIsNotSupported(); break; case ShapeContentType.Rectangle: container.Shapes.Add(Rectangles.TranslateRectangleContent(context, (Rectangle)shapeContent)); break; case ShapeContentType.Transform: { var transform = (Transform)shapeContent; // Multiply the opacity in the transform. context.UpdateOpacityFromTransform(context, transform); // Insert a new container at the top. The transform will be applied to it. var newContainer = context.ObjectFactory.CreateContainerShape(); newContainer.Shapes.Add(result); result = newContainer; // Apply the transform to the new container at the top. Transforms.TranslateAndApplyTransform(context, transform, result); } break; case ShapeContentType.Repeater: // TODO - handle all cases. Not clear whether this is valid. Seen on 0605.traffic_light. context.Issues.RepeaterIsNotSupported(); break; default: case ShapeContentType.SolidColorStroke: case ShapeContentType.LinearGradientStroke: case ShapeContentType.RadialGradientStroke: case ShapeContentType.SolidColorFill: case ShapeContentType.LinearGradientFill: case ShapeContentType.RadialGradientFill: case ShapeContentType.TrimPath: case ShapeContentType.RoundCorners: throw new InvalidOperationException(); } } return(result); }
static void TranslateAndApplyTrimPath( ShapeContext context, CompositionGeometry geometry, bool reverseDirection, double trimOffsetDegrees) { var trimPath = context.TrimPath; if (trimPath is null) { return; } if (reverseDirection) { trimPath = trimPath.CloneWithReversedDirection(); } var startTrim = Optimizer.TrimAnimatable(context, trimPath.Start); var endTrim = Optimizer.TrimAnimatable(context, trimPath.End); var trimPathOffset = Optimizer.TrimAnimatable(context, trimPath.Offset); if (!startTrim.IsAnimated && !endTrim.IsAnimated) { // Handle some well-known static cases. if (startTrim.InitialValue.Value == 0 && endTrim.InitialValue.Value == 1) { // The trim does nothing. return; } else if (startTrim.InitialValue == endTrim.InitialValue) { // TODO - the trim trims away all of the path. } } var order = GetAnimatableOrder(in startTrim, in endTrim); switch (order) { case AnimatableOrder.Before: case AnimatableOrder.Equal: break; case AnimatableOrder.After: { // Swap is necessary to match the WinComp semantics. var temp = startTrim; startTrim = endTrim; endTrim = temp; } break; case AnimatableOrder.BeforeAndAfter: break; default: throw new InvalidOperationException(); } if (order == AnimatableOrder.BeforeAndAfter) { // Add properties that will be animated. The TrimStart and TrimEnd properties // will be set by these values through an expression. Animate.TrimStartOrTrimEndPropertySetValue(context, startTrim, geometry, "TStart"); var trimStartExpression = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.MinTStartTEnd); trimStartExpression.SetReferenceParameter("my", geometry); Animate.WithExpression(geometry, trimStartExpression, nameof(geometry.TrimStart)); Animate.TrimStartOrTrimEndPropertySetValue(context, endTrim, geometry, "TEnd"); var trimEndExpression = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.MaxTStartTEnd); trimEndExpression.SetReferenceParameter("my", geometry); Animate.WithExpression(geometry, trimEndExpression, nameof(geometry.TrimEnd)); } else { // Directly animate the TrimStart and TrimEnd properties. if (startTrim.IsAnimated) { Animate.TrimStartOrTrimEnd(context, startTrim, geometry, nameof(geometry.TrimStart), "TrimStart", null); } else { geometry.TrimStart = ConvertTo.Float(startTrim.InitialValue); } if (endTrim.IsAnimated) { Animate.TrimStartOrTrimEnd(context, endTrim, geometry, nameof(geometry.TrimEnd), "TrimEnd", null); } else { geometry.TrimEnd = ConvertTo.Float(endTrim.InitialValue); } } if (trimOffsetDegrees != 0 && !trimPathOffset.IsAnimated) { // Rectangle shapes are treated specially here to account for Lottie rectangle 0,0 being // top right and WinComp rectangle 0,0 being top left. As long as the TrimOffset isn't // being animated we can simply add an offset to the trim path. geometry.TrimOffset = (float)((trimPathOffset.InitialValue.Degrees + trimOffsetDegrees) / 360); } else { if (trimOffsetDegrees != 0) { // TODO - can be handled with another property. context.Issues.AnimatedTrimOffsetWithStaticTrimOffsetIsNotSupported(); } if (trimPathOffset.IsAnimated) { Animate.ScaledRotation(context, trimPathOffset, 1 / 360.0, geometry, nameof(geometry.TrimOffset), "TrimOffset", null); } else { geometry.TrimOffset = ConvertTo.Float(trimPathOffset.InitialValue.Degrees / 360); } } }
static IEnumerable <CanvasGeometry> CreateCanvasGeometries( ShapeContext context, Stack <ShapeLayerContent> stack, ShapeFill.PathFillType pathFillType) { while (stack.Count > 0) { // Ignore context on the stack - we only want geometries. var shapeContent = stack.Pop(); switch (shapeContent.ContentType) { case ShapeContentType.Group: { // Convert all the shapes in the group to a list of geometries var group = (ShapeGroup)shapeContent; var groupedGeometries = CreateCanvasGeometries(context.Clone(), new Stack <ShapeLayerContent>(group.Contents.ToArray()), pathFillType).ToArray(); foreach (var geometry in groupedGeometries) { yield return(geometry); } } break; case ShapeContentType.MergePaths: yield return(MergeShapeLayerContent(context, stack, ((MergePaths)shapeContent).Mode)); break; case ShapeContentType.Repeater: context.Issues.RepeaterIsNotSupported(); break; case ShapeContentType.Transform: // TODO - do we need to clear out the transform when we've finished with this call to CreateCanvasGeometries?? Maybe the caller should clone the context. context.SetTransform((Transform)shapeContent); break; case ShapeContentType.SolidColorStroke: case ShapeContentType.LinearGradientStroke: case ShapeContentType.RadialGradientStroke: case ShapeContentType.SolidColorFill: case ShapeContentType.RadialGradientFill: case ShapeContentType.LinearGradientFill: case ShapeContentType.TrimPath: case ShapeContentType.RoundCorners: // Ignore commands that set the context - we only want geometries. break; case ShapeContentType.Path: yield return(Paths.CreateWin2dPathGeometryFromShape(context, (Path)shapeContent, pathFillType, optimizeLines: true)); break; case ShapeContentType.Ellipse: yield return(Ellipses.CreateWin2dEllipseGeometry(context, (Ellipse)shapeContent)); break; case ShapeContentType.Rectangle: yield return(Rectangles.CreateWin2dRectangleGeometry(context, (Rectangle)shapeContent)); break; case ShapeContentType.Polystar: context.Issues.PolystarIsNotSupported(); break; default: throw new InvalidOperationException(); } } }
public static void TranslateAndApplyShapeContext( ShapeContext context, CompositionSpriteShape shape, bool reverseDirection) => TranslateAndApplyShapeContextWithTrimOffset(context, shape, reverseDirection, 0);
public static CompositionShape TranslateEllipseContent(ShapeContext context, Ellipse shapeContent) { // An ellipse is represented as a SpriteShape with a CompositionEllipseGeometry. var compositionSpriteShape = context.ObjectFactory.CreateSpriteShape(); compositionSpriteShape.SetDescription(context, () => shapeContent.Name); var compositionEllipseGeometry = context.ObjectFactory.CreateEllipseGeometry(); compositionEllipseGeometry.SetDescription(context, () => $"{shapeContent.Name}.EllipseGeometry"); compositionSpriteShape.Geometry = compositionEllipseGeometry; var position = Optimizer.TrimAnimatable(context, shapeContent.Position); if (position.IsAnimated) { Animate.Vector2(context, position, compositionEllipseGeometry, "Center"); } else { compositionEllipseGeometry.Center = ConvertTo.Vector2(position.InitialValue); } // Ensure that the diameter is expressed in a form that has only one easing per channel. var diameter = AnimatableVector3Rewriter.EnsureOneEasingPerChannel(shapeContent.Diameter); if (diameter is AnimatableXYZ diameterXYZ) { var diameterX = Optimizer.TrimAnimatable(context, diameterXYZ.X); var diameterY = Optimizer.TrimAnimatable(context, diameterXYZ.Y); if (diameterX.IsAnimated) { Animate.ScaledScalar(context, diameterX, 0.5, compositionEllipseGeometry, $"{nameof(CompositionEllipseGeometry.Radius)}.X"); } if (diameterY.IsAnimated) { Animate.ScaledScalar(context, diameterY, 0.5, compositionEllipseGeometry, $"{nameof(CompositionEllipseGeometry.Radius)}.Y"); } if (!diameterX.IsAnimated || !diameterY.IsAnimated) { compositionEllipseGeometry.Radius = ConvertTo.Vector2(diameter.InitialValue) * 0.5F; } } else { var diameter3 = Optimizer.TrimAnimatable <Vector3>(context, (AnimatableVector3)diameter); if (diameter3.IsAnimated) { Animate.ScaledVector2(context, diameter3, 0.5, compositionEllipseGeometry, nameof(CompositionEllipseGeometry.Radius)); } else { compositionEllipseGeometry.Radius = ConvertTo.Vector2(diameter.InitialValue) * 0.5F; } } Shapes.TranslateAndApplyShapeContext( context, compositionSpriteShape, reverseDirection: shapeContent.DrawingDirection == DrawingDirection.Reverse); return(compositionSpriteShape); }
// Translates a non-rounded Lottie rectangle to a CompositionShape. static void TranslateAndApplyNonRoundedRectangleContent( ShapeContext context, Rectangle rectangle, in TrimmedAnimatable <Vector3> position,