Skip to content

Commit df4131e

Browse files
authored
Added effects (blur/shadow) support for shape layers. (#455)
1 parent 7463321 commit df4131e

File tree

3 files changed

+330
-293
lines changed

3 files changed

+330
-293
lines changed

source/LottieToWinComp/Effects.cs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
#nullable enable
66

7+
using System;
8+
using System.Collections.Generic;
9+
using System.Diagnostics;
710
using System.Linq;
11+
using Microsoft.Toolkit.Uwp.UI.Lottie.Animatables;
812
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
13+
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
914

1015
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
1116
{
@@ -91,5 +96,304 @@ void EmitIssueAboutUnsupportedEffect(Effect? effect, string effectName)
9196
// Emit an issue about the effect not being supported on this layer.
9297
void EmitIssueAboutUnsupportedEffect(string effectName) =>
9398
_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+
}
94398
}
95399
}

0 commit comments

Comments
 (0)