|
4 | 4 |
|
5 | 5 | #nullable enable |
6 | 6 |
|
| 7 | +using System; |
| 8 | +using System.Collections.Generic; |
| 9 | +using System.Diagnostics; |
7 | 10 | using System.Linq; |
| 11 | +using Microsoft.Toolkit.Uwp.UI.Lottie.Animatables; |
8 | 12 | using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData; |
| 13 | +using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData; |
9 | 14 |
|
10 | 15 | namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp |
11 | 16 | { |
@@ -91,5 +96,304 @@ void EmitIssueAboutUnsupportedEffect(Effect? effect, string effectName) |
91 | 96 | // Emit an issue about the effect not being supported on this layer. |
92 | 97 | void EmitIssueAboutUnsupportedEffect(string effectName) => |
93 | 98 | _context.Issues.LayerEffectNotSupportedOnLayer(effectName, _context.Layer.Type.ToString()); |
| 99 | + |
| 100 | + /// <summary> |
| 101 | + /// Applies the given <see cref="DropShadowEffect"/>. |
| 102 | + /// This is only designed to work on a <see cref="PreCompLayer"/> and <see cref="ShapeLayer"/> |
| 103 | + /// because the bounds of the <paramref name="source"/> tree must be known. |
| 104 | + /// In case of <see cref="ShapeLayer"/> we are using parent context size instead. |
| 105 | + /// </summary> |
| 106 | + /// <returns>Visual node with shadow.</returns> |
| 107 | + public static Visual ApplyDropShadow( |
| 108 | + LayerContext context, |
| 109 | + Visual source, |
| 110 | + DropShadowEffect dropShadowEffect) |
| 111 | + { |
| 112 | + Debug.Assert(dropShadowEffect.IsEnabled, "Precondition"); |
| 113 | + Debug.Assert(context is PreCompLayerContext || context is ShapeLayerContext, "Precondition"); |
| 114 | + |
| 115 | + // Shadow: |
| 116 | + // +------------------+ |
| 117 | + // | Container Visual | -- Has the final composited result. |
| 118 | + // +------------------+ < |
| 119 | + // ^ Child #1 \ Child #2 (original layer) |
| 120 | + // | (shadow layer) \ |
| 121 | + // | \ |
| 122 | + // +---------------------+ \ |
| 123 | + // | ApplyGaussianBlur() | \ |
| 124 | + // +---------------------+ +-----------------+ |
| 125 | + // ^ | ContainerVisual | - Original Visual node. |
| 126 | + // | +-----------------+ |
| 127 | + // +----------------+ . |
| 128 | + // | SpriteVisual | . |
| 129 | + // +----------------+ . |
| 130 | + // ^ Source . |
| 131 | + // | . |
| 132 | + // +--------------+ . |
| 133 | + // | MaskBrush | . |
| 134 | + // +--------------+ . |
| 135 | + // ^ Source ^ Mask . Source |
| 136 | + // | \ V |
| 137 | + // +----------+ +---------------+ |
| 138 | + // |ColorBrush| | VisualSurface | |
| 139 | + // +----------+ +---------------+ |
| 140 | + GaussianBlurEffect gaussianBlurEffect = new GaussianBlurEffect( |
| 141 | + name: dropShadowEffect.Name + "_blur", |
| 142 | + isEnabled: true, |
| 143 | + blurriness: dropShadowEffect.Softness, |
| 144 | + blurDimensions: new Animatable<Enum<BlurDimension>>(BlurDimension.HorizontalAndVertical), |
| 145 | + repeatEdgePixels: new Animatable<bool>(true), |
| 146 | + forceGpuRendering: true); |
| 147 | + |
| 148 | + var factory = context.ObjectFactory; |
| 149 | + var size = context.CompositionContext.Size; |
| 150 | + |
| 151 | + if (context is PreCompLayerContext) |
| 152 | + { |
| 153 | + size = ConvertTo.Vector2(((PreCompLayerContext)context).Layer.Width, ((PreCompLayerContext)context).Layer.Height); |
| 154 | + } |
| 155 | + |
| 156 | + var visualSurface = factory.CreateVisualSurface(); |
| 157 | + visualSurface.SourceSize = size; |
| 158 | + visualSurface.SourceVisual = source; |
| 159 | + |
| 160 | + var maskBrush = factory.CreateMaskBrush(); |
| 161 | + |
| 162 | + var colorBrush = factory.CreateColorBrush(dropShadowEffect.Color.InitialValue); |
| 163 | + |
| 164 | + var color = Optimizer.TrimAnimatable(context, dropShadowEffect.Color); |
| 165 | + if (color.IsAnimated) |
| 166 | + { |
| 167 | + Animate.Color(context, color, colorBrush, nameof(colorBrush.Color)); |
| 168 | + } |
| 169 | + else |
| 170 | + { |
| 171 | + colorBrush.Color = ConvertTo.Color(color.InitialValue); |
| 172 | + } |
| 173 | + |
| 174 | + maskBrush.Source = colorBrush; |
| 175 | + maskBrush.Mask = factory.CreateSurfaceBrush(visualSurface); |
| 176 | + |
| 177 | + var shadowSpriteVisual = factory.CreateSpriteVisual(); |
| 178 | + shadowSpriteVisual.Size = size; |
| 179 | + shadowSpriteVisual.Brush = maskBrush; |
| 180 | + |
| 181 | + var blurResult = ApplyGaussianBlur(context, shadowSpriteVisual, gaussianBlurEffect); |
| 182 | + |
| 183 | + var opacity = Optimizer.TrimAnimatable(context, dropShadowEffect.Opacity); |
| 184 | + if (opacity.IsAnimated) |
| 185 | + { |
| 186 | + Animate.Opacity(context, opacity, blurResult, nameof(blurResult.Opacity)); |
| 187 | + } |
| 188 | + else |
| 189 | + { |
| 190 | + blurResult.Opacity = (float)opacity.InitialValue.Value; |
| 191 | + } |
| 192 | + |
| 193 | + // Convert direction and distance to a Vector3. |
| 194 | + var direction = Optimizer.TrimAnimatable(context, dropShadowEffect.Direction); |
| 195 | + var distance = Optimizer.TrimAnimatable(context, dropShadowEffect.Distance); |
| 196 | + |
| 197 | + if (direction.IsAnimated) |
| 198 | + { |
| 199 | + if (distance.IsAnimated) |
| 200 | + { |
| 201 | + // Direction and distance are animated. |
| 202 | + // NOTE: we could support this in some cases. The worst cases are |
| 203 | + // where the keyframes don't line up, and/or the easings are different |
| 204 | + // between distance and direction. |
| 205 | + context.Issues.AnimatedLayerEffectParameters("drop shadow"); |
| 206 | + } |
| 207 | + else |
| 208 | + { |
| 209 | + // Only direction is animated. |
| 210 | + var distanceValue = distance.InitialValue; |
| 211 | + var keyFrames = direction.KeyFrames.Select( |
| 212 | + kf => new KeyFrame<Vector3>(kf.Frame, VectorFromRotationAndDistance(kf.Value, distanceValue), kf.Easing)).ToArray(); |
| 213 | + var directionAnimation = new TrimmedAnimatable<Vector3>(context, keyFrames[0].Value, keyFrames); |
| 214 | + Animate.Vector3(context, directionAnimation, blurResult, nameof(blurResult.Offset)); |
| 215 | + } |
| 216 | + } |
| 217 | + else if (distance.IsAnimated) |
| 218 | + { |
| 219 | + // Only distance is animated. |
| 220 | + var directionRadians = direction.InitialValue.Radians; |
| 221 | + var keyFrames = distance.KeyFrames.Select( |
| 222 | + kf => new KeyFrame<Vector3>(kf.Frame, VectorFromRotationAndDistance(directionRadians, kf.Value), kf.Easing)).ToArray(); |
| 223 | + var distanceAnimation = new TrimmedAnimatable<Vector3>(context, keyFrames[0].Value, keyFrames); |
| 224 | + Animate.Vector3(context, distanceAnimation, blurResult, nameof(blurResult.Offset)); |
| 225 | + } |
| 226 | + else |
| 227 | + { |
| 228 | + // Direction and distance are both not animated. |
| 229 | + var directionRadians = direction.InitialValue.Radians; |
| 230 | + var distanceValue = distance.InitialValue; |
| 231 | + |
| 232 | + blurResult.Offset = ConvertTo.Vector3(VectorFromRotationAndDistance(direction.InitialValue, distance.InitialValue)); |
| 233 | + } |
| 234 | + |
| 235 | + var result = factory.CreateContainerVisual(); |
| 236 | + result.Size = size; |
| 237 | + result.Children.Add(blurResult); |
| 238 | + |
| 239 | + // Check if ShadowOnly can be false |
| 240 | + if (!dropShadowEffect.IsShadowOnly.IsAlways(true)) |
| 241 | + { |
| 242 | + // Check if ShadowOnly can be true |
| 243 | + if (!dropShadowEffect.IsShadowOnly.IsAlways(false)) |
| 244 | + { |
| 245 | + var isVisible = FlipBoolAnimatable(dropShadowEffect.IsShadowOnly); // isVisible = !isShadowOnly |
| 246 | + |
| 247 | + source.IsVisible = isVisible.InitialValue; |
| 248 | + if (isVisible.IsAnimated) |
| 249 | + { |
| 250 | + Animate.Boolean( |
| 251 | + context, |
| 252 | + Optimizer.TrimAnimatable(context, isVisible), |
| 253 | + source, |
| 254 | + nameof(blurResult.IsVisible)); |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + result.Children.Add(source); |
| 259 | + } |
| 260 | + |
| 261 | + return result; |
| 262 | + } |
| 263 | + |
| 264 | + static Vector3 VectorFromRotationAndDistance(Rotation direction, double distance) => |
| 265 | + VectorFromRotationAndDistance(direction.Radians, distance); |
| 266 | + |
| 267 | + /// <summary> |
| 268 | + /// Construct a 2D vector with a given rotation and length. |
| 269 | + /// Note: In After Effects 0 degrees angle corresponds to UP direction |
| 270 | + /// and 90 degrees angle corresponds to RIGHT direction. |
| 271 | + /// </summary> |
| 272 | + /// <param name="directionRadians">Rotation in radians.</param> |
| 273 | + /// <param name="distance">Vector length.</param> |
| 274 | + /// <returns>Vector with given parameters.</returns> |
| 275 | + static Vector3 VectorFromRotationAndDistance(double directionRadians, double distance) => |
| 276 | + new Vector3( |
| 277 | + x: Math.Sin(directionRadians) * distance, |
| 278 | + y: -Math.Cos(directionRadians) * distance, |
| 279 | + z: 1); |
| 280 | + |
| 281 | + static Animatable<bool> FlipBoolAnimatable(Animatable<bool> animatable) |
| 282 | + { |
| 283 | + if (!animatable.IsAnimated) |
| 284 | + { |
| 285 | + return new Animatable<bool>(!animatable.InitialValue); |
| 286 | + } |
| 287 | + |
| 288 | + var keyFrames = new List<KeyFrame<bool>>(); |
| 289 | + |
| 290 | + foreach (var keyFrame in animatable.KeyFrames) |
| 291 | + { |
| 292 | + keyFrames.Add(new KeyFrame<bool>(keyFrame.Frame, !keyFrame.Value, keyFrame.Easing)); |
| 293 | + } |
| 294 | + |
| 295 | + return new Animatable<bool>(!animatable.InitialValue, keyFrames); |
| 296 | + } |
| 297 | + |
| 298 | + /// <summary> |
| 299 | + /// Applies a Gaussian blur effect to the given <paramref name="source"/> and |
| 300 | + /// returns a new root. This is only designed to work on a <see cref="PreCompLayer"/> and <see cref="ShapeLayer"/> |
| 301 | + /// because the bounds of the <paramref name="source"/> tree must be known. |
| 302 | + /// In case of <see cref="ShapeLayer"/> we are using parent context size instead. |
| 303 | + /// </summary> |
| 304 | + /// <returns>A new subtree that contains <paramref name="source"/>.</returns> |
| 305 | + public static Visual ApplyGaussianBlur( |
| 306 | + LayerContext context, |
| 307 | + Visual source, |
| 308 | + GaussianBlurEffect gaussianBlurEffect) |
| 309 | + { |
| 310 | + Debug.Assert(gaussianBlurEffect.IsEnabled, "Precondition"); |
| 311 | + Debug.Assert(context is PreCompLayerContext || context is ShapeLayerContext, "Precondition"); |
| 312 | + |
| 313 | + var factory = context.ObjectFactory; |
| 314 | + |
| 315 | + if (!factory.IsUapApiAvailable(nameof(CompositionVisualSurface), versionDependentFeatureDescription: "Gaussian blur")) |
| 316 | + { |
| 317 | + // The effect can't be displayed on the targeted version. |
| 318 | + return source; |
| 319 | + } |
| 320 | + |
| 321 | + // Gaussian blur: |
| 322 | + // +--------------+ |
| 323 | + // | SpriteVisual | -- Has the final composited result. |
| 324 | + // +--------------+ |
| 325 | + // ^ |
| 326 | + // | |
| 327 | + // +--------------+ |
| 328 | + // | EffectBrush | -- Composition effect brush allows the composite effect result to be used as a brush. |
| 329 | + // +--------------+ |
| 330 | + // ^ |
| 331 | + // | |
| 332 | + // +--------------------+ |
| 333 | + // | GaussianBlurEffect | |
| 334 | + // +--------------------+ |
| 335 | + // ^ Source |
| 336 | + // | |
| 337 | + // +--------------+ |
| 338 | + // | SurfaceBrush | -- Surface brush that will paint with the output of the VisualSurface |
| 339 | + // +--------------+ that has the source visual assigned to it. |
| 340 | + // ^ CompositionEffectSourceParameter("source") |
| 341 | + // | |
| 342 | + // +---------------+ |
| 343 | + // | VisualSurface | -- The visual surface captures the renderable contents of its source visual. |
| 344 | + // +---------------+ |
| 345 | + // ^ |
| 346 | + // | |
| 347 | + // +--------+ |
| 348 | + // | Visual | -- The layer translated to a Visual. |
| 349 | + // +--------+ |
| 350 | + var size = context.CompositionContext.Size; |
| 351 | + |
| 352 | + if (context is PreCompLayerContext) |
| 353 | + { |
| 354 | + size = ConvertTo.Vector2(((PreCompLayerContext)context).Layer.Width, ((PreCompLayerContext)context).Layer.Height); |
| 355 | + } |
| 356 | + |
| 357 | + // Build from the bottom up. |
| 358 | + var visualSurface = factory.CreateVisualSurface(); |
| 359 | + visualSurface.SourceVisual = source; |
| 360 | + visualSurface.SourceSize = size; |
| 361 | + |
| 362 | + var surfaceBrush = factory.CreateSurfaceBrush(visualSurface); |
| 363 | + |
| 364 | + var effect = new WinCompData.Mgce.GaussianBlurEffect(); |
| 365 | + |
| 366 | + var blurriness = Optimizer.TrimAnimatable(context, gaussianBlurEffect.Blurriness); |
| 367 | + if (blurriness.IsAnimated) |
| 368 | + { |
| 369 | + context.Issues.AnimatedLayerEffectParameters("Gaussian blur"); |
| 370 | + } |
| 371 | + |
| 372 | + effect.BlurAmount = ConvertTo.Float(blurriness.InitialValue / 10.0); |
| 373 | + |
| 374 | + // We only support HorizontalAndVertical blur dimension. |
| 375 | + var blurDimensions = Optimizer.TrimAnimatable(context, gaussianBlurEffect.BlurDimensions); |
| 376 | + var unsupportedBlurDimensions = blurDimensions |
| 377 | + .KeyFrames |
| 378 | + .Select(kf => kf.Value) |
| 379 | + .Distinct() |
| 380 | + .Where(v => v.Value != BlurDimension.HorizontalAndVertical).ToArray(); |
| 381 | + |
| 382 | + foreach (var value in unsupportedBlurDimensions) |
| 383 | + { |
| 384 | + context.Issues.UnsupportedLayerEffectParameter("gaussian blur", "blur dimension", value.Value.ToString()); |
| 385 | + } |
| 386 | + |
| 387 | + effect.Source = new CompositionEffectSourceParameter("source"); |
| 388 | + |
| 389 | + var effectBrush = factory.CreateEffectFactory(effect).CreateBrush(); |
| 390 | + effectBrush.SetSourceParameter("source", surfaceBrush); |
| 391 | + |
| 392 | + var result = factory.CreateSpriteVisual(); |
| 393 | + result.Brush = effectBrush; |
| 394 | + result.Size = size; |
| 395 | + |
| 396 | + return result; |
| 397 | + } |
94 | 398 | } |
95 | 399 | } |
0 commit comments