Esempio n. 1
0
        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);
        }
Esempio n. 2
0
        static CompositionShape TranslateGroupShapeContent(ShapeContext context, ShapeGroup group)
        {
            var result = TranslateShapeLayerContents(context, group.Contents);

            result.SetDescription(context, () => $"ShapeGroup: {group.Name}");
            return(result);
        }
Esempio n. 3
0
        // 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);
        }
Esempio n. 4
0
        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),
            });
Esempio n. 5
0
 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);
 }
Esempio n. 6
0
        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));
            }
        }
Esempio n. 7
0
        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);
        }
Esempio n. 8
0
        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);
            }
        }
Esempio n. 9
0
        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);
        }
Esempio n. 10
0
        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);
        }
Esempio n. 11
0
        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);
                }
            }
        }
Esempio n. 12
0
        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();
                }
            }
        }
Esempio n. 13
0
 public static void TranslateAndApplyShapeContext(
     ShapeContext context,
     CompositionSpriteShape shape,
     bool reverseDirection) =>
 TranslateAndApplyShapeContextWithTrimOffset(context, shape, reverseDirection, 0);
Esempio n. 14
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);
        }
Esempio n. 15
0
 // Translates a non-rounded Lottie rectangle to a CompositionShape.
 static void TranslateAndApplyNonRoundedRectangleContent(
     ShapeContext context,
     Rectangle rectangle,
     in TrimmedAnimatable <Vector3> position,