public static bool TryCreateShapeVisualTransformChain( LayerContext context, [NotNullWhen(true)] out ContainerVisual?rootNode, [NotNullWhen(true)] out CompositionContainerShape?contentsNode) { // Create containers for the contents in the layer. // The rootNode is the root for the layer. // // +---------------+ // | rootNode |-- Root node, optionally with opacity animation for the layer. // +---------------+ // ^ // | // +-----------------+ // | visiblityNode |-- Optional visiblity node (only used if the visiblity is animated). // +-----------------+ // ^ // | // +-----------------+ // | opacityNode |-- Optional opacity node. // +-----------------+ // ^ // | // +-----------------+ // | ShapeVisual |-- Start of the shape tree. // +-----------------+ // ^ // | // +-------------------+ // | rootTransformNode |--Transform without opacity (inherited from root ancestor of the transform tree). // +-------------------+ // ^ // | // + - - - - - - - - - - - - + // | other transforms nodes |--Transform without opacity (inherited from the transform tree). // + - - - - - - - - - - - - + // ^ // | // +-------------------+ // | leafTransformNode |--Transform without opacity defined on the layer. // +-------------------+ // ^ ^ // | | // +---------+ +---------+ // | content | | content | ... // +---------+ +---------+ // // Get the opacity of the layer. var layerOpacity = Optimizer.TrimAnimatable(context, context.Layer.Transform.Opacity); // Convert the layer's in point and out point into absolute progress (0..1) values. var inProgress = context.InPointAsProgress; var outProgress = context.OutPointAsProgress; if (inProgress > 1 || outProgress <= 0 || inProgress >= outProgress || layerOpacity.IsAlways(Animatables.Opacity.Transparent)) { // The layer is never visible. Don't create anything. rootNode = null; contentsNode = null; return(false); } rootNode = context.ObjectFactory.CreateContainerVisual(); ContainerVisual contentsVisual = rootNode; // Implement opacity for the layer. InsertOpacityVisualIntoTransformChain(context, layerOpacity, ref rootNode); // Implement visibility for the layer. InsertVisibilityVisualIntoTransformChain(context, inProgress, outProgress, ref rootNode); // Create the transforms chain. TranslateTransformOnContainerShapeForLayer(context, context.Layer, out var transformsRoot, out contentsNode); // Create the shape visual. var shapeVisual = context.ObjectFactory.CreateShapeVisualWithChild(transformsRoot, context.CompositionContext.Size); shapeVisual.SetDescription(context, () => $"Shape tree root for layer: {context.Layer.Name}"); contentsVisual.Children.Add(shapeVisual); return(true); }
internal override CompositionShape?GetShapeRoot(TranslationContext context) { if (_context.Layer.IsHidden || _context.Layer.Transform.Opacity.IsAlways(LottieData.Opacity.Transparent)) { // The layer does not render anything. Nothing to translate. This can happen when someone // creates a solid layer to act like a Null layer. return(null); } if (!Transforms.TryCreateContainerShapeTransformChain(_context, out var containerRootNode, out var containerContentNode)) { // The layer is never visible. return(null); } var rectangle = context.ObjectFactory.CreateSpriteShape(); var rectangleGeometry = context.ObjectFactory.CreateRectangleGeometry(); rectangleGeometry.Size = new Sn.Vector2(_context.Layer.Width, _context.Layer.Height); rectangle.Geometry = rectangleGeometry; containerContentNode.Shapes.Add(rectangle); // Opacity is implemented via the alpha channel on the brush. rectangle.FillBrush = Brushes.CreateAnimatedColorBrush(_context, _context.Layer.Color, Optimizer.TrimAnimatable(_context, _context.Layer.Transform.Opacity)); rectangle.SetDescription(context, () => "SolidLayerRectangle"); rectangle.Geometry.SetDescription(context, () => "SolidLayerRectangle.RectangleGeometry"); Describe(context, containerRootNode); return(containerRootNode); }
public static bool TryCreateContainerShapeTransformChain( LayerContext context, [NotNullWhen(true)] out CompositionContainerShape?rootNode, [NotNullWhen(true)] out CompositionContainerShape?contentsNode) { // Create containers for the contents in the layer. // The rootNode is the root for the layer. It may be the same object // as the contentsNode if there are no inherited transforms and no visibility animation. // // +---------------+ // | ... | // +---------------+ // ^ // | // +-----------------+ // | visiblityNode |-- Optional visiblity node (only used if the visiblity is animated) // +-----------------+ // ^ // | // +-------------------+ // | rootTransformNode |--Transform (values are inherited from root ancestor of the transform tree) // +-------------------+ // ^ // | // + - - - - - - - - - - - - + // | other transforms nodes |--Transform (values inherited from the transform tree) // + - - - - - - - - - - - - + // ^ // | // +-------------------+ // | leafTransformNode |--Transform defined on the layer // +-------------------+ // ^ ^ // | | // +---------+ +---------+ // | content | | content | ... // +---------+ +---------+ // // Get the opacity of the layer. var layerOpacity = Optimizer.TrimAnimatable(context, context.Layer.Transform.Opacity); // Convert the layer's in point and out point into absolute progress (0..1) values. var inProgress = context.InPointAsProgress; var outProgress = context.OutPointAsProgress; if (inProgress > 1 || outProgress <= 0 || inProgress >= outProgress || layerOpacity.IsAlways(Animatables.Opacity.Transparent)) { // The layer is never visible. Don't create anything. rootNode = null; contentsNode = null; return(false); } // Create the transforms chain. TranslateTransformOnContainerShapeForLayer(context, context.Layer, out var transformsRoot, out contentsNode); // Implement the Visibility for the layer. Only needed if the layer becomes visible after // the LottieComposition's in point, or it becomes invisible before the LottieComposition's out point. if (inProgress > 0 || outProgress < 1) { // Create a node to control visibility. var visibilityNode = context.ObjectFactory.CreateContainerShape(); visibilityNode.Shapes.Add(transformsRoot); rootNode = visibilityNode; visibilityNode.SetDescription(context, () => $"Layer: {context.Layer.Name}"); // Animate between Scale(0,0) and Scale(1,1). var visibilityAnimation = context.ObjectFactory.CreateVector2KeyFrameAnimation(); visibilityAnimation.SetName("ShapeVisibilityAnimation"); if (inProgress > 0) { // Set initial value to be non-visible (default is visible). visibilityNode.Scale = Sn.Vector2.Zero; visibilityAnimation.InsertKeyFrame(inProgress, Sn.Vector2.One, context.ObjectFactory.CreateHoldThenStepEasingFunction()); } if (outProgress < 1) { visibilityAnimation.InsertKeyFrame(outProgress, Sn.Vector2.Zero, context.ObjectFactory.CreateHoldThenStepEasingFunction()); } visibilityAnimation.Duration = context.Translation.LottieComposition.Duration; Animate.WithKeyFrame(context, visibilityNode, nameof(visibilityNode.Scale), visibilityAnimation); } else { rootNode = transformsRoot; } return(true); }
public static bool TryCreateContainerVisualTransformChain( LayerContext context, [NotNullWhen(true)] out ContainerVisual?rootNode, [NotNullWhen(true)] out ContainerVisual?contentsNode) { // Create containers for the contents in the layer. // The rootTransformNode is the root for the layer. It may be the same object // as the contentsNode if there are no inherited transforms. // // +---------------+ // | ... | // +---------------+ // ^ // | // +-----------------+ // | visiblityNode |-- Optional visiblity node (only used if the visiblity is animated) // +-----------------+ // ^ // | // +-------------------+ // | rootTransformNode |--Transform (values are inherited from root ancestor of the transform tree) // +-------------------+ // ^ // | // + - - - - - - - - - - - - + // | other transforms nodes |--Transform (values inherited from the transform tree) // + - - - - - - - - - - - - + // ^ // | // +---------------+ // | contentsNode |--Transform defined on the layer // +---------------+ // ^ ^ // | | // +---------+ +---------+ // | content | | content | ... // +---------+ +---------+ // // Get the opacity of the layer. var layerOpacity = Optimizer.TrimAnimatable(context, context.Layer.Transform.Opacity); // Convert the layer's in point and out point into absolute progress (0..1) values. var inProgress = context.InPointAsProgress; var outProgress = context.OutPointAsProgress; if (inProgress > 1 || outProgress <= 0 || inProgress >= outProgress || layerOpacity.IsAlways(Animatables.Opacity.Transparent)) { // The layer is never visible. Don't create anything. rootNode = null; contentsNode = null; return(false); } // Create the transforms chain. TranslateTransformOnContainerVisualForLayer(context, context.Layer, out rootNode, out contentsNode); // Implement opacity for the layer. InsertOpacityVisualIntoTransformChain(context, layerOpacity, ref rootNode); // Implement visibility for the layer. InsertVisibilityVisualIntoTransformChain(context, inProgress, outProgress, ref rootNode); return(true); }
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, trimOffsetDegrees: 0); return(compositionSpriteShape); }
static LayerVisual ApplyDropShadow(PreCompLayerContext context, Visual visual, DropShadowEffect dropShadowEffect) { Debug.Assert(dropShadowEffect.IsEnabled, "Precondition"); // Create a LayerVisual so we can add a drop shadow. var result = context.ObjectFactory.CreateLayerVisual(); result.Children.Add(visual); // TODO: Due to a Composition bug, LayerVisual currently must be given a size for the drop // shadow to show up correctly. And even then it is not reliable. result.Size = context.CompositionContext.Size; var shadow = context.ObjectFactory.CreateDropShadow(); result.Shadow = shadow; shadow.SourcePolicy = CompositionDropShadowSourcePolicy.InheritFromVisualContent; var isShadowOnly = Optimizer.TrimAnimatable(context, dropShadowEffect.IsShadowOnly); if (!isShadowOnly.IsAlways(true)) { context.Issues.ShadowOnlyShadowEffect(); } // TODO - it's not clear whether BlurRadius and Softness are equivalent. We may // need to scale Softness to convert it to BlurRadius. var blurRadius = Optimizer.TrimAnimatable(context, dropShadowEffect.Softness); if (blurRadius.IsAnimated) { Animate.Scalar(context, blurRadius, shadow, nameof(shadow.BlurRadius)); } else { shadow.BlurRadius = (float)blurRadius.InitialValue; } var color = Optimizer.TrimAnimatable(context, dropShadowEffect.Color); if (color.IsAnimated) { Animate.Color(context, color, shadow, nameof(shadow.Color)); } else { shadow.Color = ConvertTo.Color(color.InitialValue); } var opacity = Optimizer.TrimAnimatable(context, dropShadowEffect.Opacity); if (opacity.IsAnimated) { Animate.Opacity(context, opacity, shadow, nameof(shadow.Opacity)); } else { shadow.Opacity = (float)opacity.InitialValue.Value; } // Convert direction and distance to a Vector3. var direction = Optimizer.TrimAnimatable(context, dropShadowEffect.Direction); var distance = Optimizer.TrimAnimatable(context, dropShadowEffect.Distance); if (direction.IsAnimated) { if (distance.IsAnimated) { // Direction and distance are animated. // NOTE: we could support this in some cases. The worst cases are // where the keyframes don't line up, and/or the easings are different // between distance and direction. context.Issues.AnimatedLayerEffectParameters("drop shadow"); } else { // Only direction is animated. var distanceValue = distance.InitialValue; var keyFrames = direction.KeyFrames.Select( kf => new KeyFrame <Vector3>(kf.Frame, VectorFromRotationAndDistance(kf.Value, distanceValue), kf.Easing)).ToArray(); var directionAnimation = new TrimmedAnimatable <Vector3>(context, keyFrames[0].Value, keyFrames); Animate.Vector3(context, directionAnimation, shadow, nameof(shadow.Offset)); } } else if (distance.IsAnimated) { // Only distance is animated. var directionRadians = direction.InitialValue.Radians; var keyFrames = distance.KeyFrames.Select( kf => new KeyFrame <Vector3>(kf.Frame, VectorFromRotationAndDistance(directionRadians, kf.Value), kf.Easing)).ToArray(); var distanceAnimation = new TrimmedAnimatable <Vector3>(context, keyFrames[0].Value, keyFrames); Animate.Vector3(context, distanceAnimation, shadow, nameof(shadow.Offset)); } else { // Direction and distance are both not animated. var directionRadians = direction.InitialValue.Radians; var distanceValue = distance.InitialValue; shadow.Offset = ConvertTo.Vector3(VectorFromRotationAndDistance(direction.InitialValue, distance.InitialValue)); } return(result); }
internal static ContainerVisual ApplyGaussianBlur( PreCompLayerContext context, ContainerVisual source, GaussianBlurEffect gaussianBlurEffect) { Debug.Assert(gaussianBlurEffect.IsEnabled, "Precondition"); var factory = context.ObjectFactory; if (!factory.IsUapApiAvailable(nameof(CompositionVisualSurface), versionDependentFeatureDescription: "Gaussian blur")) { // The effect can't be displayed on the targeted version. return(source); } // Gaussian blur: // +--------------+ // | SpriteVisual | -- Has the final composited result. // +--------------+ // ^ // | // +--------------+ // | EffectBrush | -- Composition effect brush allows the composite effect result to be used as a brush. // +--------------+ // ^ // | // +--------------------+ // | GaussianBlurEffect | // +--------------------+ // ^ Source // | // +--------------+ // | SurfaceBrush | -- Surface brush that will paint with the output of the VisualSurface // +--------------+ that has the source visual assigned to it. // ^ CompositionEffectSourceParameter("source") // | // +---------------+ // | VisualSurface | -- The visual surface captures the renderable contents of its source visual. // +---------------+ // ^ // | // +--------+ // | Visual | -- The layer translated to a Visual. // +--------+ var size = ConvertTo.Vector2(context.Layer.Width, context.Layer.Height); // Build from the bottom up. var visualSurface = factory.CreateVisualSurface(); visualSurface.SourceVisual = source; visualSurface.SourceSize = size; var surfaceBrush = factory.CreateSurfaceBrush(visualSurface); var effect = new WinCompData.Mgce.GaussianBlurEffect(); var blurriness = Optimizer.TrimAnimatable(context, gaussianBlurEffect.Blurriness); if (blurriness.IsAnimated) { context.Issues.AnimatedLayerEffectParameters("Gaussian blur"); } effect.BlurAmount = ConvertTo.Float(blurriness.InitialValue / 10.0); // We only support HorizontalAndVertical blur dimension. var blurDimensions = Optimizer.TrimAnimatable(context, gaussianBlurEffect.BlurDimensions); var unsupportedBlurDimensions = blurDimensions .KeyFrames .Select(kf => kf.Value) .Distinct() .Where(v => v.Value != BlurDimension.HorizontalAndVertical).ToArray(); foreach (var value in unsupportedBlurDimensions) { context.Issues.UnsupportedLayerEffectParameter("gaussian blur", "blur dimension", value.Value.ToString()); } effect.Source = new CompositionEffectSourceParameter("source"); var effectBrush = factory.CreateEffectFactory(effect).CreateBrush(); effectBrush.SetSourceParameter("source", surfaceBrush); var result = factory.CreateSpriteVisual(); result.Brush = effectBrush; result.Size = size; return(result); }
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 is not 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); } } }