diff --git a/source/Lottie/Instantiator.cs b/source/Lottie/Instantiator.cs index b180a60..743bac7 100644 --- a/source/Lottie/Instantiator.cs +++ b/source/Lottie/Instantiator.cs @@ -436,6 +436,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie Wd.CompositionObjectType.CompositionEllipseGeometry => GetCompositionEllipseGeometry((Wd.CompositionEllipseGeometry)obj), Wd.CompositionObjectType.CompositionGeometricClip => GetCompositionGeometricClip((Wd.CompositionGeometricClip)obj), Wd.CompositionObjectType.CompositionLinearGradientBrush => GetCompositionLinearGradientBrush((Wd.CompositionLinearGradientBrush)obj), + Wd.CompositionObjectType.CompositionMaskBrush => GetCompositionMaskBrush((Wd.CompositionMaskBrush)obj), Wd.CompositionObjectType.CompositionPathGeometry => GetCompositionPathGeometry((Wd.CompositionPathGeometry)obj), Wd.CompositionObjectType.CompositionPropertySet => GetCompositionPropertySet((Wd.CompositionPropertySet)obj), Wd.CompositionObjectType.CompositionRadialGradientBrush => GetCompositionRadialGradientBrush((Wd.CompositionRadialGradientBrush)obj), @@ -1229,6 +1230,29 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie return result; } + Wc.CompositionMaskBrush GetCompositionMaskBrush(Wd.CompositionMaskBrush obj) + { + if (GetExisting(obj, out var result)) + { + return result; + } + + result = CacheAndInitializeCompositionObject(obj, _c.CreateMaskBrush()); + + if (obj.Mask is not null) + { + result.Mask = GetCompositionBrush(obj.Mask); + } + + if (obj.Source is not null) + { + result.Source = GetCompositionBrush(obj.Source); + } + + StartAnimations(obj, result); + return result; + } + [return: NotNullIfNotNull("obj")] Wc.CompositionGeometry? GetCompositionGeometry(Wd.CompositionGeometry? obj) { @@ -1429,6 +1453,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie return GetCompositionEffectBrush((Wd.CompositionEffectBrush)obj); case Wd.CompositionObjectType.CompositionSurfaceBrush: return GetCompositionSurfaceBrush((Wd.CompositionSurfaceBrush)obj); + case Wd.CompositionObjectType.CompositionMaskBrush: + return GetCompositionMaskBrush((Wd.CompositionMaskBrush)obj); case Wd.CompositionObjectType.CompositionLinearGradientBrush: case Wd.CompositionObjectType.CompositionRadialGradientBrush: return GetCompositionGradientBrush((Wd.CompositionGradientBrush)obj); diff --git a/source/LottieToWinComp/Animate.cs b/source/LottieToWinComp/Animate.cs index ced72c9..fe12952 100644 --- a/source/LottieToWinComp/Animate.cs +++ b/source/LottieToWinComp/Animate.cs @@ -255,6 +255,30 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp shortDescription); } + /// + /// Animates a boolean value. + /// + public static void Boolean( + LayerContext context, + in TrimmedAnimatable value, + CompositionObject targetObject, + string targetPropertyName, + string? longDescription = null, + string? shortDescription = null) + { + Debug.Assert(value.IsAnimated, "Precondition"); + GenericCreateCompositionKeyFrameAnimation( + context, + value, + context.ObjectFactory.CreateBooleanKeyFrameAnimation, + (ca, progress, val, easing) => ca.InsertKeyFrame(progress, val), + null, + targetObject, + targetPropertyName, + longDescription, + shortDescription); + } + /// /// Animates a trim start or trim end value. /// diff --git a/source/LottieToWinComp/CompositionObjectFactory.cs b/source/LottieToWinComp/CompositionObjectFactory.cs index 506fb1e..ceaa7c1 100644 --- a/source/LottieToWinComp/CompositionObjectFactory.cs +++ b/source/LottieToWinComp/CompositionObjectFactory.cs @@ -167,6 +167,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp return result; } + internal CompositionMaskBrush CreateMaskBrush() => _compositor.CreateMaskBrush(); + internal CompositionColorGradientStop CreateColorGradientStop() => _compositor.CreateColorGradientStop(); internal CompositionColorGradientStop CreateColorGradientStop(float offset, Color color) => _compositor.CreateColorGradientStop(offset, Color(color)); diff --git a/source/LottieToWinComp/PreComps.cs b/source/LottieToWinComp/PreComps.cs index ea37789..3fc65c0 100644 --- a/source/LottieToWinComp/PreComps.cs +++ b/source/LottieToWinComp/PreComps.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -// DropShadows are currently disabled because of LayerVisual bugs. -//#define EnableDropShadow #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.Toolkit.Uwp.UI.Lottie.Animatables; @@ -73,16 +72,12 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp result.Children.Add(rootNode); } -#if EnableDropShadow var dropShadowEffect = context.Effects.DropShadowEffect; if (dropShadowEffect is not null) { result = ApplyDropShadow(context, result, dropShadowEffect); } -#else - context.Effects.EmitIssueIfDropShadow(); -#endif var gaussianBlurEffect = context.Effects.GaussianBlurEffect; @@ -97,59 +92,84 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp /// /// Applies the given . /// - static LayerVisual ApplyDropShadow(PreCompLayerContext context, Visual visual, DropShadowEffect dropShadowEffect) + static ContainerVisual ApplyDropShadow( + PreCompLayerContext context, + ContainerVisual source, + 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); + // Shadow: + // +------------------+ + // | Container Visual | -- Has the final composited result. + // +------------------+ < + // ^ Child #1 \ Child #2 (original layer) + // | (shadow layer) \ + // | \ + // +---------------------+ \ + // | ApplyGaussianBlur() | \ + // +---------------------+ +-----------------+ + // ^ | ContainerVisual | - Original Visual node. + // | +-----------------+ + // +----------------+ . + // | SpriteVisual | . + // +----------------+ . + // ^ Source . + // | . + // +--------------+ . + // | MaskBrush | . + // +--------------+ . + // ^ Source ^ Mask . Source + // | \ V + // +----------+ +---------------+ + // |ColorBrush| | VisualSurface | + // +----------+ +---------------+ + GaussianBlurEffect gaussianBlurEffect = new GaussianBlurEffect( + name: dropShadowEffect.Name + "_blur", + isEnabled: true, + blurriness: dropShadowEffect.Softness, + blurDimensions: new Animatable>(BlurDimension.HorizontalAndVertical), + repeatEdgePixels: new Animatable(true), + forceGpuRendering: true); - // 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 factory = context.ObjectFactory; + var size = ConvertTo.Vector2(context.Layer.Width, context.Layer.Height); - var shadow = context.ObjectFactory.CreateDropShadow(); + var visualSurface = factory.CreateVisualSurface(); + visualSurface.SourceSize = size; + visualSurface.SourceVisual = source; - result.Shadow = shadow; - shadow.SourcePolicy = CompositionDropShadowSourcePolicy.InheritFromVisualContent; + var maskBrush = factory.CreateMaskBrush(); - 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 colorBrush = factory.CreateColorBrush(dropShadowEffect.Color.InitialValue); var color = Optimizer.TrimAnimatable(context, dropShadowEffect.Color); if (color.IsAnimated) { - Animate.Color(context, color, shadow, nameof(shadow.Color)); + Animate.Color(context, color, colorBrush, nameof(colorBrush.Color)); } else { - shadow.Color = ConvertTo.Color(color.InitialValue); + colorBrush.Color = ConvertTo.Color(color.InitialValue); } + maskBrush.Source = colorBrush; + maskBrush.Mask = factory.CreateSurfaceBrush(visualSurface); + + var shadowSpriteVisual = factory.CreateSpriteVisual(); + shadowSpriteVisual.Size = size; + shadowSpriteVisual.Brush = maskBrush; + + var blurResult = ApplyGaussianBlur(context, shadowSpriteVisual, gaussianBlurEffect); + var opacity = Optimizer.TrimAnimatable(context, dropShadowEffect.Opacity); if (opacity.IsAnimated) { - Animate.Opacity(context, opacity, shadow, nameof(shadow.Opacity)); + Animate.Opacity(context, opacity, blurResult, nameof(blurResult.Opacity)); } else { - shadow.Opacity = (float)opacity.InitialValue.Value; + blurResult.Opacity = (float)opacity.InitialValue.Value; } // Convert direction and distance to a Vector3. @@ -173,7 +193,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp var keyFrames = direction.KeyFrames.Select( kf => new KeyFrame(kf.Frame, VectorFromRotationAndDistance(kf.Value, distanceValue), kf.Easing)).ToArray(); var directionAnimation = new TrimmedAnimatable(context, keyFrames[0].Value, keyFrames); - Animate.Vector3(context, directionAnimation, shadow, nameof(shadow.Offset)); + Animate.Vector3(context, directionAnimation, blurResult, nameof(blurResult.Offset)); } } else if (distance.IsAnimated) @@ -183,7 +203,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp var keyFrames = distance.KeyFrames.Select( kf => new KeyFrame(kf.Frame, VectorFromRotationAndDistance(directionRadians, kf.Value), kf.Easing)).ToArray(); var distanceAnimation = new TrimmedAnimatable(context, keyFrames[0].Value, keyFrames); - Animate.Vector3(context, distanceAnimation, shadow, nameof(shadow.Offset)); + Animate.Vector3(context, distanceAnimation, blurResult, nameof(blurResult.Offset)); } else { @@ -191,7 +211,33 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp var directionRadians = direction.InitialValue.Radians; var distanceValue = distance.InitialValue; - shadow.Offset = ConvertTo.Vector3(VectorFromRotationAndDistance(direction.InitialValue, distance.InitialValue)); + blurResult.Offset = ConvertTo.Vector3(VectorFromRotationAndDistance(direction.InitialValue, distance.InitialValue)); + } + + var result = factory.CreateContainerVisual(); + result.Size = size; + result.Children.Add(blurResult); + + // Check if ShadowOnly can be true + if (!dropShadowEffect.IsShadowOnly.IsAlways(false)) + { + // Check if ShadowOnly can be false + if (!dropShadowEffect.IsShadowOnly.IsAlways(true)) + { + var isVisible = FlipBoolAnimatable(dropShadowEffect.IsShadowOnly); // isVisible = !isShadowOnly + + source.IsVisible = isVisible.InitialValue; + if (isVisible.IsAnimated) + { + Animate.Boolean( + context, + Optimizer.TrimAnimatable(context, isVisible), + source, + nameof(blurResult.IsVisible)); + } + } + + result.Children.Add(source); } return result; @@ -200,12 +246,37 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp static Vector3 VectorFromRotationAndDistance(Rotation direction, double distance) => VectorFromRotationAndDistance(direction.Radians, distance); + /// + /// Construct a 2D vector with a given rotation and length. + /// Note: In After Effects 0 degrees angle corresponds to UP direction + /// and 90 degrees angle corresponds to RIGHT direction. + /// + /// Rotation in radians. + /// Vector length. + /// Vector with given parameters. static Vector3 VectorFromRotationAndDistance(double directionRadians, double distance) => new Vector3( x: Math.Sin(directionRadians) * distance, - y: Math.Cos(directionRadians) * distance, + y: -Math.Cos(directionRadians) * distance, z: 1); + static Animatable FlipBoolAnimatable(Animatable animatable) + { + if (!animatable.IsAnimated) + { + return new Animatable(!animatable.InitialValue); + } + + var keyFrames = new List>(); + + foreach (var keyFrame in animatable.KeyFrames) + { + keyFrames.Add(new KeyFrame(keyFrame.Frame, !keyFrame.Value, keyFrame.Easing)); + } + + return new Animatable(!animatable.InitialValue, keyFrames); + } + /// /// Applies a Gaussian blur effect to the given and /// returns a new root. This is only designed to work on a diff --git a/source/UIData/Tools/GraphCompactor.cs b/source/UIData/Tools/GraphCompactor.cs index ebea643..1043aa6 100644 --- a/source/UIData/Tools/GraphCompactor.cs +++ b/source/UIData/Tools/GraphCompactor.cs @@ -101,6 +101,14 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools graph[child].Parent = node.Object; } + break; + case CompositionObjectType.CompositionVisualSurface: + Visual? source = ((CompositionVisualSurface)node.Object).SourceVisual; + if (source is not null) + { + graph[source].AllowCoalesing = false; + } + break; } } @@ -1074,7 +1082,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools // The parent may have been removed already. let parent = n.Node.Parent - where parent is not null + where parent is not null && n.Node.AllowCoalesing select ((ContainerVisual)parent, containerVisual)).ToArray(); // Pull the children of the container into the parent of the container. Remove the unnecessary containers. @@ -1443,6 +1451,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools sealed class Node : Graph.Node { internal CompositionObject? Parent { get; set; } + + internal bool AllowCoalesing { get; set; } = true; } } } diff --git a/source/UIData/Tools/ObjectGraph.cs b/source/UIData/Tools/ObjectGraph.cs index 0c7cbb9..4d26226 100644 --- a/source/UIData/Tools/ObjectGraph.cs +++ b/source/UIData/Tools/ObjectGraph.cs @@ -142,6 +142,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools case CompositionObjectType.CompositionLinearGradientBrush: VisitCompositionLinearGradientBrush((CompositionLinearGradientBrush)obj, node); break; + case CompositionObjectType.CompositionMaskBrush: + VisitCompositionMaskBrush((CompositionMaskBrush)obj, node); + break; case CompositionObjectType.CompositionPathGeometry: VisitCompositionPathGeometry((CompositionPathGeometry)obj, node); break; @@ -603,6 +606,23 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools return true; } + bool VisitCompositionMaskBrush(CompositionMaskBrush obj, T node) + { + VisitCompositionBrush(obj, node); + + if (obj.Mask is not null) + { + Reference(node, obj.Mask); + } + + if (obj.Source is not null) + { + Reference(node, obj.Source); + } + + return true; + } + bool VisitCompositionRadialGradientBrush(CompositionRadialGradientBrush obj, T node) { return VisitCompositionGradientBrush(obj, node); diff --git a/source/UIData/Tools/Optimizer.cs b/source/UIData/Tools/Optimizer.cs index e1f2882..736ae65 100644 --- a/source/UIData/Tools/Optimizer.cs +++ b/source/UIData/Tools/Optimizer.cs @@ -487,6 +487,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools CompositionObjectType.CompositionEllipseGeometry => GetCompositionEllipseGeometry((CompositionEllipseGeometry)obj), CompositionObjectType.CompositionGeometricClip => GetCompositionGeometricClip((CompositionGeometricClip)obj), CompositionObjectType.CompositionLinearGradientBrush => GetCompositionLinearGradientBrush((CompositionLinearGradientBrush)obj), + CompositionObjectType.CompositionMaskBrush => GetCompositionMaskBrush((CompositionMaskBrush)obj), CompositionObjectType.CompositionPathGeometry => GetCompositionPathGeometry((CompositionPathGeometry)obj), CompositionObjectType.CompositionPropertySet => GetCompositionPropertySet((CompositionPropertySet)obj), CompositionObjectType.CompositionRadialGradientBrush => GetCompositionRadialGradientBrush((CompositionRadialGradientBrush)obj), @@ -1179,6 +1180,29 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools return result; } + CompositionMaskBrush GetCompositionMaskBrush(CompositionMaskBrush obj) + { + if (GetExisting(obj, out var result)) + { + return result; + } + + result = CacheAndInitializeCompositionObject(obj, _c.CreateMaskBrush()); + + if (obj.Mask is not null) + { + result.Mask = GetCompositionBrush(obj.Mask); + } + + if (obj.Source is not null) + { + result.Source = GetCompositionBrush(obj.Source); + } + + StartAnimationsAndFreeze(obj, result); + return result; + } + [return: NotNullIfNotNull("obj")] CompositionGeometry? GetCompositionGeometry(CompositionGeometry? obj) { @@ -1386,6 +1410,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools return GetCompositionGradientBrush((CompositionGradientBrush)obj); case CompositionObjectType.CompositionSurfaceBrush: return GetCompositionSurfaceBrush((CompositionSurfaceBrush)obj); + case CompositionObjectType.CompositionMaskBrush: + return GetCompositionMaskBrush((CompositionMaskBrush)obj); default: throw new InvalidOperationException(); } diff --git a/source/UIData/Tools/Stats.cs b/source/UIData/Tools/Stats.cs index e108568..9de39cc 100644 --- a/source/UIData/Tools/Stats.cs +++ b/source/UIData/Tools/Stats.cs @@ -68,6 +68,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools case CompositionObjectType.CompositionLinearGradientBrush: LinearGradientBrushCount++; break; + case CompositionObjectType.CompositionMaskBrush: + MaskBrushCount++; + break; case CompositionObjectType.CompositionPathGeometry: PathGeometryCount++; break; @@ -184,6 +187,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools public int LinearGradientBrushCount { get; } + public int MaskBrushCount { get; } + public int PathGeometryCount { get; } public int PropertySetPropertyCount { get; } diff --git a/source/UIDataCodeGen/CodeGen/InstantiatorGeneratorBase.cs b/source/UIDataCodeGen/CodeGen/InstantiatorGeneratorBase.cs index 6859429..357f3a8 100644 --- a/source/UIDataCodeGen/CodeGen/InstantiatorGeneratorBase.cs +++ b/source/UIDataCodeGen/CodeGen/InstantiatorGeneratorBase.cs @@ -1672,6 +1672,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.CodeGen CompositionObjectType.CompositionEllipseGeometry => GenerateCompositionEllipseGeometryFactory(builder, (CompositionEllipseGeometry)obj, node), CompositionObjectType.CompositionGeometricClip => GenerateCompositionGeometricClipFactory(builder, (CompositionGeometricClip)obj, node), CompositionObjectType.CompositionLinearGradientBrush => GenerateCompositionLinearGradientBrushFactory(builder, (CompositionLinearGradientBrush)obj, node), + CompositionObjectType.CompositionMaskBrush => GenerateCompositionMaskBrushFactory(builder, (CompositionMaskBrush)obj, node), CompositionObjectType.CompositionPathGeometry => GenerateCompositionPathGeometryFactory(builder, (CompositionPathGeometry)obj, node), // Do not generate code for property sets. It is done inline in the CompositionObject initialization. @@ -1754,6 +1755,19 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.CodeGen return true; } + bool GenerateCompositionMaskBrushFactory(CodeBuilder builder, CompositionMaskBrush obj, ObjectData node) + { + WriteObjectFactoryStart(builder, node); + WriteCreateAssignment(builder, node, $"_c{Deref}CreateMaskBrush()"); + InitializeCompositionBrush(builder, obj, node); + + WriteSetPropertyStatement(builder, "Mask", CallFactoryFromFor(node, obj.Mask)); + WriteSetPropertyStatement(builder, "Source", CallFactoryFromFor(node, obj.Source)); + + WriteCompositionObjectFactoryEnd(builder, obj, node); + return true; + } + bool GenerateCompositionRadialGradientBrushFactory(CodeBuilder builder, CompositionRadialGradientBrush obj, ObjectData node) { WriteObjectFactoryStart(builder, node); diff --git a/source/WinCompData/CompositionMaskBrush.cs b/source/WinCompData/CompositionMaskBrush.cs new file mode 100644 index 0000000..af0aa14 --- /dev/null +++ b/source/WinCompData/CompositionMaskBrush.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData +{ + [MetaData.UapVersion(3)] +#if PUBLIC_WinCompData + public +#endif + sealed class CompositionMaskBrush : CompositionBrush + { + public CompositionBrush? Source { get; set; } + + public CompositionBrush? Mask { get; set; } + + internal CompositionMaskBrush() + { + } + + public override CompositionObjectType Type => CompositionObjectType.CompositionMaskBrush; + } +} diff --git a/source/WinCompData/CompositionObjectType.cs b/source/WinCompData/CompositionObjectType.cs index 6e5aa87..ca7f416 100644 --- a/source/WinCompData/CompositionObjectType.cs +++ b/source/WinCompData/CompositionObjectType.cs @@ -20,6 +20,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData CompositionEllipseGeometry, CompositionGeometricClip, CompositionLinearGradientBrush, + CompositionMaskBrush, CompositionPathGeometry, CompositionPropertySet, CompositionRadialGradientBrush, diff --git a/source/WinCompData/Compositor.cs b/source/WinCompData/Compositor.cs index 6833898..08ad53a 100644 --- a/source/WinCompData/Compositor.cs +++ b/source/WinCompData/Compositor.cs @@ -17,6 +17,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData public CompositionColorBrush CreateColorBrush() => new CompositionColorBrush(); + public CompositionMaskBrush CreateMaskBrush() => new CompositionMaskBrush(); + public CompositionColorBrush CreateColorBrush(Wui.Color color) => new CompositionColorBrush(color); public CompositionColorGradientStop CreateColorGradientStop() => new CompositionColorGradientStop(); diff --git a/source/WinCompData/WinCompData.projitems b/source/WinCompData/WinCompData.projitems index 6864a52..2913dec 100644 --- a/source/WinCompData/WinCompData.projitems +++ b/source/WinCompData/WinCompData.projitems @@ -19,6 +19,7 @@ +