Refactor to allow better modularization of the translator. (#337)
* Refactor to allow better modularization of the translator. Previously most of the translation code had to be in the LottieToWinCompTranslator class and that was making it difficult to maintain. The goal with this refactor is to allow translation code to be implemented in static methods grouped for convenience into static classes, and to avoid having to pass more than one context to a translation method. The state that is needed for translation is now in various "contexts". The LottieToWinCompTranslator is now just a static class that creates the top-level context and tells it to translate. Previously we had a "TranslationContext" that was the context for translating a layer, and a "ShapeContext" that was the context for translating a shape withing a shape layer. Now we have: TranslationContext - context for stuff that is global to a particular Lottie file's translation. CompositionContext - context for a list of layers. There is a CompositionContext for the root of the Lottie as well as one for each PreComp layer. LayerContext - context for stuff that is layer dependent. There are strongly-typed subclasses of this for each layer type. ShapeContext - as before - context for translating a shape. These contexts form a hierarchy: ShapeContext contains a LayerContext which contains a CompositionContext which contains a TranslationContext. Contexts inherit state from their containing context. Through the magic of implicit conversions, a context can always be passed to a method requiring a context type higher in the hierarchy, e.g. a LayerContext can be passed to a method that requires a TranslationContext.
This commit is contained in:
Родитель
53c52c76c5
Коммит
8608ab2fd5
|
@ -793,7 +793,7 @@ sealed class LottieFileProcessor
|
|||
// Optimize the code unless told not to.
|
||||
if (!_options.DisableTranslationOptimizer)
|
||||
{
|
||||
_translationResults = _translationResults.Select(tr => tr.WithDifferentRoot(Optimizer.Optimize(tr.RootVisual, ignoreCommentProperties: true))).ToArray();
|
||||
_translationResults = _translationResults.Select(tr => tr.WithDifferentRoot(Microsoft.Toolkit.Uwp.UI.Lottie.UIData.Tools.Optimizer.Optimize(tr.RootVisual, ignoreCommentProperties: true))).ToArray();
|
||||
_profiler.OnOptimizationFinished();
|
||||
|
||||
// NOTE: this is only reporting on the latest version in a multi-version translation.
|
||||
|
|
|
@ -0,0 +1,794 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Expr = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions.Expression;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Static methods for animating Windows Composition objects.
|
||||
/// </summary>
|
||||
static class Animate
|
||||
{
|
||||
/// <summary>
|
||||
/// Animates a property on <paramref name="compObject"/> using an expression animation.
|
||||
/// </summary>
|
||||
public static void WithExpression(
|
||||
CompositionObject compObject,
|
||||
ExpressionAnimation animation,
|
||||
string target) =>
|
||||
compObject.StartAnimation(target, animation);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a property on <paramref name="compObject"/> using a key frame animation.
|
||||
/// </summary>
|
||||
public static void WithKeyFrame(
|
||||
TranslationContext context,
|
||||
CompositionObject compObject,
|
||||
string target,
|
||||
KeyFrameAnimation_ animation,
|
||||
double scale = 1,
|
||||
double offset = 0)
|
||||
{
|
||||
Debug.Assert(offset >= 0, "Precondition");
|
||||
Debug.Assert(scale <= 1, "Precondition");
|
||||
Debug.Assert(animation.KeyFrameCount > 0, "Precondition");
|
||||
|
||||
var state = context.GetStateCache<StateCache>();
|
||||
|
||||
// Start the animation ...
|
||||
compObject.StartAnimation(target, animation);
|
||||
|
||||
// ... but pause it immediately so that it doesn't react to time. Instead, bind
|
||||
// its progress to the progress of the composition.
|
||||
var controller = compObject.TryGetAnimationController(target);
|
||||
controller.Pause();
|
||||
|
||||
// Bind it to the root visual's Progress property, scaling and offsetting if necessary.
|
||||
var key = new ScaleAndOffset(scale, offset);
|
||||
if (!state.ProgressBindingAnimations.TryGetValue(key, out var bindingAnimation))
|
||||
{
|
||||
bindingAnimation = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.ScaledAndOffsetRootProgress(scale, offset));
|
||||
bindingAnimation.SetReferenceParameter(ExpressionFactory.RootName, context.RootVisual);
|
||||
if (context.AddDescriptions)
|
||||
{
|
||||
// Give the animation a nice readable name in codegen.
|
||||
var name = key.Offset != 0 || key.Scale != 1
|
||||
? "RootProgressRemapped"
|
||||
: "RootProgress";
|
||||
|
||||
bindingAnimation.SetName(name);
|
||||
}
|
||||
|
||||
state.ProgressBindingAnimations.Add(key, bindingAnimation);
|
||||
}
|
||||
|
||||
// Bind the controller's Progress with a single Progress property on the scene root.
|
||||
// The Progress property provides the time reference for the animation.
|
||||
controller.StartAnimation("Progress", bindingAnimation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds and animates a <see cref="CompositionPropertySet"/> value on the target object.
|
||||
/// </summary>
|
||||
public static void ScalarPropertySetValue(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<double> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
if (targetObject.Properties == targetObject)
|
||||
{
|
||||
throw new ArgumentException("targetObject must not be a CompositionPropertySet");
|
||||
}
|
||||
|
||||
targetObject.Properties.InsertScalar(targetPropertyName, ConvertTo.Float(value.InitialValue));
|
||||
|
||||
if (value.IsAnimated)
|
||||
{
|
||||
ScaledScalar(context, value, 1, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds and animates a <see cref="CompositionPropertySet"/> value on the target object.
|
||||
/// </summary>
|
||||
public static void TrimStartOrTrimEndPropertySetValue(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Trim> value,
|
||||
CompositionGeometry targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
targetObject.Properties.InsertScalar(targetPropertyName, ConvertTo.Float(value.InitialValue));
|
||||
|
||||
if (value.IsAnimated)
|
||||
{
|
||||
TrimStartOrTrimEnd(context, value, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a rotation value.
|
||||
/// </summary>
|
||||
public static void Rotation(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Rotation> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
=> ScaledRotation(context, value, 1, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a scalar value.
|
||||
/// </summary>
|
||||
public static void Scalar(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<double> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
=> ScaledScalar(context, value, 1, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a percent value.
|
||||
/// </summary>
|
||||
public static void Percent(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<double> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
=> ScaledScalar(context, value, 0.01, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Animates an opacity value.
|
||||
/// </summary>
|
||||
public static void Opacity(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Opacity> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
=> ScaledOpacity(context, value, 1, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a trim start or trim end value.
|
||||
/// </summary>
|
||||
public static void TrimStartOrTrimEnd(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Trim> value,
|
||||
CompositionGeometry targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
=> ScaledTrimStartOrTrimEnd(context, value, 1, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a rotation value.
|
||||
/// </summary>
|
||||
public static void ScaledRotation(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Rotation> value,
|
||||
double scale,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription,
|
||||
string shortDescription)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateScalarKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, (float)(val.Degrees * scale), easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates an opacity value.
|
||||
/// </summary>
|
||||
public static void ScaledOpacity(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Opacity> value,
|
||||
double scale,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription,
|
||||
string shortDescription)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateScalarKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, (float)(val.Value * scale), easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a scalar value.
|
||||
/// </summary>
|
||||
public static void ScaledScalar(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<double> value,
|
||||
double scale,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateScalarKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, (float)(val * scale), easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a trim start or trim end value.
|
||||
/// </summary>
|
||||
static void ScaledTrimStartOrTrimEnd(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Trim> value,
|
||||
double scale,
|
||||
CompositionGeometry targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription,
|
||||
string shortDescription)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateScalarKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, (float)(val.Value * scale), easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a color using an expression animation.
|
||||
/// </summary>
|
||||
public static void ColorWithExpression(
|
||||
CompositionObject compObject,
|
||||
ExpressionAnimation animation,
|
||||
string target = "Color") =>
|
||||
WithExpression(compObject, animation, target);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a color value.
|
||||
/// </summary>
|
||||
public static void Color(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Color> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateColorKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, ConvertTo.Color(val), easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
public static void ColorWithExpressionKeyFrameAnimation(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<WinCompData.Expressions.Color> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
Action<ColorKeyFrameAnimation> beforeStartCallback,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateColorKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertExpressionKeyFrame(progress, val, easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription,
|
||||
beforeStartCallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a color expressed as a Vector4 value.
|
||||
/// </summary>
|
||||
public static void ColorAsVector4(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Color> value,
|
||||
CompositionPropertySet targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateVector4KeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, ConvertTo.Vector4(ConvertTo.Color(val)), easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a path value.
|
||||
/// </summary>
|
||||
public static void Path(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<PathGeometry> value,
|
||||
ShapeFill.PathFillType fillType,
|
||||
CompositionPathGeometry targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreatePathKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(
|
||||
progress,
|
||||
Paths.CompositionPathFromPathGeometry(
|
||||
context,
|
||||
val,
|
||||
fillType,
|
||||
|
||||
// Turn off the optimization that replaces cubic Beziers with
|
||||
// segments because it may result in different numbers of
|
||||
// control points in each path in the keyframes.
|
||||
optimizeLines: false),
|
||||
easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a path group value.
|
||||
/// </summary>
|
||||
public static void PathGroup(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<PathGeometryGroup> value,
|
||||
ShapeFill.PathFillType fillType,
|
||||
CompositionPathGeometry targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreatePathKeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(
|
||||
progress,
|
||||
Paths.CompositionPathFromPathGeometryGroup(
|
||||
context,
|
||||
val.Data,
|
||||
fillType,
|
||||
|
||||
// Turn off the optimization that replaces cubic Beziers with
|
||||
// segments because it may result in different numbers of
|
||||
// control points in each path in the keyframes.
|
||||
optimizeLines: false),
|
||||
easing),
|
||||
null,
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a Vector2 value.
|
||||
/// </summary>
|
||||
public static void Vector2(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Vector3> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
=> ScaledVector2(context, value, 1, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a Vector2 value.
|
||||
/// </summary>
|
||||
public static void ScaledVector2(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Vector3> value,
|
||||
double scale,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateVector2KeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, ConvertTo.Vector2(val * scale), easing),
|
||||
(ca, progress, expr, easing) => ca.InsertExpressionKeyFrame(progress, scale * expr, easing),
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates a Vector3 value.
|
||||
/// </summary>
|
||||
public static void Vector3(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Vector3> value,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
=> ScaledVector3(context, value, 1, targetObject, targetPropertyName, longDescription, shortDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Animates a Vector3 value.
|
||||
/// </summary>
|
||||
public static void ScaledVector3(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Vector3> value,
|
||||
double scale,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription = null,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
GenericCreateCompositionKeyFrameAnimation(
|
||||
context,
|
||||
value,
|
||||
context.ObjectFactory.CreateVector3KeyFrameAnimation,
|
||||
(ca, progress, val, easing) => ca.InsertKeyFrame(progress, ConvertTo.Vector3(val) * (float)scale, easing),
|
||||
(ca, progress, expr, easing) => ca.InsertExpressionKeyFrame(progress, scale * expr.AsVector3(), easing),
|
||||
targetObject,
|
||||
targetPropertyName,
|
||||
longDescription,
|
||||
shortDescription);
|
||||
}
|
||||
|
||||
static void GenericCreateCompositionKeyFrameAnimation<TCA, T>(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<T> value,
|
||||
Func<TCA> compositionAnimationFactory,
|
||||
Action<TCA, float, T, CompositionEasingFunction> keyFrameInserter,
|
||||
Action<TCA, float, CubicBezierFunction2, CompositionEasingFunction> expressionKeyFrameInserter,
|
||||
CompositionObject targetObject,
|
||||
string targetPropertyName,
|
||||
string longDescription,
|
||||
string shortDescription,
|
||||
Action<TCA> beforeStartCallback = null)
|
||||
where TCA : KeyFrameAnimation_
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
Debug.Assert(value.IsAnimated, "Precondition");
|
||||
|
||||
var translationContext = context.Translation;
|
||||
|
||||
var compositionAnimation = compositionAnimationFactory();
|
||||
|
||||
compositionAnimation.SetDescription(context, () => (longDescription ?? targetPropertyName, shortDescription ?? targetPropertyName));
|
||||
|
||||
compositionAnimation.Duration = translationContext.LottieComposition.Duration;
|
||||
|
||||
var trimmedKeyFrames = value.KeyFrames;
|
||||
|
||||
var firstKeyFrame = trimmedKeyFrames[0];
|
||||
var lastKeyFrame = trimmedKeyFrames[trimmedKeyFrames.Count - 1];
|
||||
|
||||
var animationStartTime = firstKeyFrame.Frame;
|
||||
var animationEndTime = lastKeyFrame.Frame;
|
||||
|
||||
var highestProgressValueSoFar = Float32.PreviousSmallerThan(0);
|
||||
|
||||
if (firstKeyFrame.Frame > context.CompositionContext.StartTime)
|
||||
{
|
||||
// The first key frame is after the start of the animation. Create an extra keyframe at 0 to
|
||||
// set and hold an initial value until the first specified keyframe.
|
||||
// Note that we could set an initial value for the property instead of using a key frame,
|
||||
// but seeing as we're creating key frames anyway, it will be fewer operations to
|
||||
// just use a first key frame and not set an initial value
|
||||
InsertKeyFrame(compositionAnimation, 0 /* progress */, firstKeyFrame.Value, context.ObjectFactory.CreateStepThenHoldEasingFunction() /*easing*/);
|
||||
|
||||
animationStartTime = context.CompositionContext.StartTime;
|
||||
}
|
||||
|
||||
if (lastKeyFrame.Frame < context.CompositionContext.EndTime)
|
||||
{
|
||||
// The last key frame is before the end of the animation.
|
||||
animationEndTime = context.CompositionContext.EndTime;
|
||||
}
|
||||
|
||||
var animationDuration = animationEndTime - animationStartTime;
|
||||
|
||||
// The Math.Min is to deal with rounding errors that cause the scale to be slightly more than 1.
|
||||
var scale = Math.Min(context.CompositionContext.DurationInFrames / animationDuration, 1.0);
|
||||
var offset = (context.CompositionContext.StartTime - animationStartTime) / animationDuration;
|
||||
|
||||
// Insert the keyframes with the progress adjusted so the first keyframe is at 0 and the remaining
|
||||
// progress values are scaled appropriately.
|
||||
var previousValue = firstKeyFrame.Value;
|
||||
var previousProgress = Float32.PreviousSmallerThan(0);
|
||||
var rootReferenceRequired = false;
|
||||
var previousKeyFrameWasExpression = false;
|
||||
|
||||
foreach (var keyFrame in trimmedKeyFrames)
|
||||
{
|
||||
// Convert the frame number to a progress value for the current key frame.
|
||||
var currentProgress = (float)((keyFrame.Frame - animationStartTime) / animationDuration);
|
||||
|
||||
if (keyFrame.SpatialBezier?.IsLinear == false)
|
||||
{
|
||||
// TODO - should only be on Vector3. In which case, should they be on Animatable, or on something else?
|
||||
if (typeof(T) != typeof(Vector3))
|
||||
{
|
||||
Debug.WriteLine("Spatial control point on non-Vector3 type");
|
||||
}
|
||||
|
||||
var spatialBezier = keyFrame.SpatialBezier.Value;
|
||||
|
||||
var cp0 = ConvertTo.Vector2((Vector3)(object)previousValue);
|
||||
var cp1 = ConvertTo.Vector2(spatialBezier.ControlPoint1);
|
||||
var cp2 = ConvertTo.Vector2(spatialBezier.ControlPoint2);
|
||||
var cp3 = ConvertTo.Vector2((Vector3)(object)keyFrame.Value);
|
||||
CubicBezierFunction2 cb;
|
||||
|
||||
switch (keyFrame.Easing.Type)
|
||||
{
|
||||
case Easing.EasingType.Linear:
|
||||
case Easing.EasingType.CubicBezier:
|
||||
cb = CubicBezierFunction2.Create(
|
||||
cp0,
|
||||
cp0 + cp1,
|
||||
cp2 + cp3,
|
||||
cp3,
|
||||
Expr.Scalar("dummy"));
|
||||
break;
|
||||
case Easing.EasingType.Hold:
|
||||
// Holds should never have interesting cubic Beziers, so replace with one that is definitely colinear.
|
||||
cb = CubicBezierFunction2.ZeroBezier;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (cb.IsEquivalentToLinear || currentProgress == 0)
|
||||
{
|
||||
// The cubic Bezier function is equivalent to a line, or its value starts at the start of the animation, so no need
|
||||
// for an expression to do spatial Beziers on it. Just use a regular key frame.
|
||||
if (previousKeyFrameWasExpression)
|
||||
{
|
||||
// Ensure the previous expression doesn't continue being evaluated during the current keyframe.
|
||||
// This is necessary because the expression is only defined from the previous progress to the current progress.
|
||||
InsertKeyFrame(compositionAnimation, currentProgress, previousValue, context.ObjectFactory.CreateStepThenHoldEasingFunction());
|
||||
}
|
||||
|
||||
// The easing for a keyframe at 0 is unimportant, so always use Hold.
|
||||
var easing = currentProgress == 0 ? HoldEasing.Instance : keyFrame.Easing;
|
||||
|
||||
InsertKeyFrame(compositionAnimation, currentProgress, keyFrame.Value, context.ObjectFactory.CreateCompositionEasingFunction(easing));
|
||||
previousKeyFrameWasExpression = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Expression key frame needed for a spatial Bezier.
|
||||
|
||||
// Make the progress value just before the requested progress value
|
||||
// so that there is room to add a key frame just after this to hold
|
||||
// the final value. This is necessary so that the expression we're about
|
||||
// to add won't get evaluated during the following segment.
|
||||
if (currentProgress > 0)
|
||||
{
|
||||
currentProgress = Float32.PreviousSmallerThan(currentProgress);
|
||||
}
|
||||
|
||||
if (previousProgress > 0)
|
||||
{
|
||||
previousProgress = Float32.NextLargerThan(previousProgress);
|
||||
}
|
||||
|
||||
// Re-create the cubic Bezier using the real variable name (it was created previously just to
|
||||
// see if it was linear).
|
||||
cb = CubicBezierFunction2.Create(
|
||||
cp0,
|
||||
cp0 + cp1,
|
||||
cp2 + cp3,
|
||||
cp3,
|
||||
ExpressionFactory.RootScalar(translationContext.ProgressMapFactory.GetVariableForProgressMapping(previousProgress, currentProgress, keyFrame.Easing, scale, offset)));
|
||||
|
||||
// Insert the cubic Bezier expression. The easing has to be a StepThenHold because otherwise
|
||||
// the value will be interpolated between the result of the expression, and the previous
|
||||
// key frame value. The StepThenHold will make it just evaluate the expression.
|
||||
InsertExpressionKeyFrame(
|
||||
compositionAnimation,
|
||||
currentProgress,
|
||||
cb, // Expression.
|
||||
context.ObjectFactory.CreateStepThenHoldEasingFunction()); // Jump to the final value so the expression is evaluated all the way through.
|
||||
|
||||
// Note that a reference to the root Visual is required by the animation because it
|
||||
// is used in the expression.
|
||||
rootReferenceRequired = true;
|
||||
previousKeyFrameWasExpression = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousKeyFrameWasExpression)
|
||||
{
|
||||
// Ensure the previous expression doesn't continue being evaluated during the current keyframe.
|
||||
var nextLargerThanPrevious = Float32.NextLargerThan(previousProgress);
|
||||
InsertKeyFrame(compositionAnimation, nextLargerThanPrevious, previousValue, context.ObjectFactory.CreateStepThenHoldEasingFunction());
|
||||
|
||||
if (currentProgress <= nextLargerThanPrevious)
|
||||
{
|
||||
// Prevent the next key frame from being inserted at the same progress value
|
||||
// as the one we just inserted.
|
||||
currentProgress = Float32.NextLargerThan(nextLargerThanPrevious);
|
||||
}
|
||||
}
|
||||
|
||||
InsertKeyFrame(compositionAnimation, currentProgress, keyFrame.Value, context.ObjectFactory.CreateCompositionEasingFunction(keyFrame.Easing));
|
||||
previousKeyFrameWasExpression = false;
|
||||
}
|
||||
|
||||
previousValue = keyFrame.Value;
|
||||
previousProgress = currentProgress;
|
||||
}
|
||||
|
||||
if (previousKeyFrameWasExpression && previousProgress < 1)
|
||||
{
|
||||
// Add a keyframe to hold the final value. Otherwise the expression on the last keyframe
|
||||
// will get evaluated outside the bounds of its keyframe.
|
||||
InsertKeyFrame(compositionAnimation, Float32.NextLargerThan(previousProgress), (T)(object)previousValue, context.ObjectFactory.CreateStepThenHoldEasingFunction());
|
||||
}
|
||||
|
||||
// Add a reference to the root Visual if needed (i.e. if an expression keyframe was added).
|
||||
if (rootReferenceRequired)
|
||||
{
|
||||
compositionAnimation.SetReferenceParameter(ExpressionFactory.RootName, translationContext.RootVisual);
|
||||
}
|
||||
|
||||
beforeStartCallback?.Invoke(compositionAnimation);
|
||||
|
||||
// Start the animation scaled and offset.
|
||||
Animate.WithKeyFrame(context, targetObject, targetPropertyName, compositionAnimation, scale, offset);
|
||||
|
||||
// If the given progress value is equal to a progress value that was already
|
||||
// inserted into the animation, adjust it up to ensure we never try to
|
||||
// insert a key frame on top of an existing key frame. This relies on the
|
||||
// key frames being inserted in order.
|
||||
void AdjustProgress(ref float progress)
|
||||
{
|
||||
if (progress == highestProgressValueSoFar)
|
||||
{
|
||||
progress = Float32.NextLargerThan(highestProgressValueSoFar);
|
||||
}
|
||||
|
||||
highestProgressValueSoFar = progress;
|
||||
}
|
||||
|
||||
// Local method to ensure we never insert more than 1 key frame with
|
||||
// the same progress value. This relies on the key frames being inserted
|
||||
// in order, so if we get a key frame with the same progress value as
|
||||
// the previous one we'll just adjust the progress value up slightly.
|
||||
void InsertKeyFrame(TCA animation, float progress, T value, CompositionEasingFunction easing)
|
||||
{
|
||||
AdjustProgress(ref progress);
|
||||
|
||||
// If progress is > 1 then we have no more room to add key frames.
|
||||
// This can happen as a result of extra key frames being added for
|
||||
// various reasons. The dropped key frames shouldn't matter as they
|
||||
// would only affect a very small amount of time at the end of the
|
||||
// animation.
|
||||
if (progress <= 1)
|
||||
{
|
||||
keyFrameInserter(animation, progress, value, easing);
|
||||
}
|
||||
}
|
||||
|
||||
// Local method to ensure we never insert more than 1 key frame with
|
||||
// the same progress value. This relies on the key frames being inserted
|
||||
// in order, so if we get a key frame with the same progress value as
|
||||
// the previous one we'll just adjust the progress value up slightly.
|
||||
void InsertExpressionKeyFrame(TCA animation, float progress, CubicBezierFunction2 expression, CompositionEasingFunction easing)
|
||||
{
|
||||
AdjustProgress(ref progress);
|
||||
|
||||
// If progress is > 1 then we have no more room to add key frames.
|
||||
// This can happen as a result of extra key frames being added for
|
||||
// various reasons. The dropped key frames shouldn't matter as they
|
||||
// would only affect a very small amount of time at the end of the
|
||||
// animation.
|
||||
if (progress <= 1)
|
||||
{
|
||||
expressionKeyFrameInserter(animation, progress, expression, easing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A pair of doubles used as a key in a dictionary.
|
||||
sealed class ScaleAndOffset
|
||||
{
|
||||
internal ScaleAndOffset(double scale, double offset)
|
||||
{
|
||||
Scale = scale;
|
||||
Offset = offset;
|
||||
}
|
||||
|
||||
internal double Scale { get; }
|
||||
|
||||
internal double Offset { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is ScaleAndOffset other &&
|
||||
other.Scale == Scale &&
|
||||
other.Offset == Offset;
|
||||
|
||||
public override int GetHashCode() => Scale.GetHashCode() ^ Offset.GetHashCode();
|
||||
}
|
||||
|
||||
sealed class StateCache
|
||||
{
|
||||
public Dictionary<ScaleAndOffset, ExpressionAnimation> ProgressBindingAnimations { get; }
|
||||
= new Dictionary<ScaleAndOffset, ExpressionAnimation>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,804 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Expr = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions.Expression;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates strokes and fills to Windows Composition brushes.
|
||||
/// </summary>
|
||||
static class Brushes
|
||||
{
|
||||
// Generates a sequence of ints from 0..int.MaxValue. Used to attach indexes to sequences using Zip.
|
||||
static readonly IEnumerable<int> PositiveInts = Enumerable.Range(0, int.MaxValue);
|
||||
|
||||
public static CompositionColorBrush CreateNonAnimatedColorBrush(TranslationContext context, Color color)
|
||||
{
|
||||
var nonAnimatedColorBrushes = context.GetStateCache<StateCache>().NonAnimatedColorBrushes;
|
||||
|
||||
if (!nonAnimatedColorBrushes.TryGetValue(color, out var result))
|
||||
{
|
||||
result = context.ObjectFactory.CreateNonAnimatedColorBrush(color);
|
||||
nonAnimatedColorBrushes.Add(color, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static CompositionColorBrush CreateAnimatedColorBrush(LayerContext context, Color color, in TrimmedAnimatable<Opacity> opacity)
|
||||
{
|
||||
var multipliedColor = MultiplyColorByAnimatableOpacity(color, in opacity);
|
||||
return CreateAnimatedColorBrush(context, multipliedColor);
|
||||
}
|
||||
|
||||
public static CompositionColorBrush CreateAnimatedColorBrush(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Color> color,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
// Opacity is pushed to the alpha channel of the brush. Translate this in the simplest
|
||||
// way depending on whether the color or the opacities are animated.
|
||||
if (!opacity.IsAnimated)
|
||||
{
|
||||
// The opacity isn't animated, so it can be simply multiplied into the color.
|
||||
var nonAnimatedOpacity = opacity.NonAnimatedValue;
|
||||
return color.IsAnimated
|
||||
? CreateAnimatedColorBrush(context, MultiplyAnimatableColorByOpacity(color, nonAnimatedOpacity))
|
||||
: CreateNonAnimatedColorBrush(context, color.InitialValue * nonAnimatedOpacity);
|
||||
}
|
||||
|
||||
// The opacity has animation. If it's a simple animation (i.e. not composed) and the color
|
||||
// is not animated then the color can simply be multiplied by the animation. Otherwise we
|
||||
// need to create an expression to relate the opacity value to the color value.
|
||||
var animatableOpacities =
|
||||
(from a in opacity.GetAnimatables().Zip(PositiveInts, (first, second) => (First: first, Second: second))
|
||||
select (animatable: a.First, name: $"Opacity{a.Second}")).ToArray();
|
||||
|
||||
if (animatableOpacities.Length == 1 && !color.IsAnimated)
|
||||
{
|
||||
// The color is not animated, so the opacity can be multiplied into the alpha channel.
|
||||
return CreateAnimatedColorBrush(
|
||||
context,
|
||||
MultiplyColorByAnimatableOpacity(color.InitialValue, Optimizer.TrimAnimatable(context, animatableOpacities[0].animatable)));
|
||||
}
|
||||
|
||||
// We can't simply multiply the opacity into the alpha channel because the opacity animation is not simple
|
||||
// or the color is animated. Create properties for the opacities and color and multiply them into a
|
||||
// color expression.
|
||||
var result = context.ObjectFactory.CreateColorBrush();
|
||||
|
||||
// Add a property for each opacity.
|
||||
foreach (var (animatable, name) in animatableOpacities)
|
||||
{
|
||||
var trimmed = Optimizer.TrimAnimatable(context, animatable);
|
||||
var propertyName = name;
|
||||
result.Properties.InsertScalar(propertyName, ConvertTo.Opacity(trimmed.InitialValue));
|
||||
|
||||
// The opacity is animated, but it might be non-animated after trimming.
|
||||
if (trimmed.IsAnimated)
|
||||
{
|
||||
Animate.Opacity(context, trimmed, result.Properties, propertyName, propertyName, null);
|
||||
}
|
||||
}
|
||||
|
||||
result.Properties.InsertVector4("Color", ConvertTo.Vector4(ConvertTo.Color(color.InitialValue)));
|
||||
if (color.IsAnimated)
|
||||
{
|
||||
Animate.ColorAsVector4(context, color, result.Properties, "Color", "Color", null);
|
||||
}
|
||||
|
||||
var opacityScalarExpressions = animatableOpacities.Select(a => Expr.Scalar($"my.{a.name}")).ToArray();
|
||||
var anim = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.MyColorAsVector4MultipliedByOpacity(opacityScalarExpressions));
|
||||
anim.SetReferenceParameter("my", result.Properties);
|
||||
Animate.ColorWithExpression(result, anim);
|
||||
return result;
|
||||
}
|
||||
|
||||
static CompositionColorBrush CreateAnimatedColorBrush(LayerContext context, in TrimmedAnimatable<Color> color)
|
||||
{
|
||||
if (color.IsAnimated)
|
||||
{
|
||||
var result = context.ObjectFactory.CreateColorBrush();
|
||||
|
||||
Animate.Color(
|
||||
context,
|
||||
color,
|
||||
result,
|
||||
targetPropertyName: nameof(result.Color),
|
||||
longDescription: "Color",
|
||||
shortDescription: null);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
return CreateNonAnimatedColorBrush(context, color.InitialValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TranslateAndApplyStroke(
|
||||
LayerContext context,
|
||||
ShapeStroke shapeStroke,
|
||||
CompositionSpriteShape sprite,
|
||||
CompositeOpacity contextOpacity)
|
||||
{
|
||||
if (shapeStroke is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (shapeStroke.StrokeWidth.IsAlways(0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (shapeStroke.StrokeKind)
|
||||
{
|
||||
case ShapeStroke.ShapeStrokeKind.SolidColor:
|
||||
TranslateAndApplySolidColorStroke(context, (SolidColorStroke)shapeStroke, sprite, contextOpacity);
|
||||
break;
|
||||
case ShapeStroke.ShapeStrokeKind.LinearGradient:
|
||||
TranslateAndApplyLinearGradientStroke(context, (LinearGradientStroke)shapeStroke, sprite, contextOpacity);
|
||||
break;
|
||||
case ShapeStroke.ShapeStrokeKind.RadialGradient:
|
||||
TranslateAndApplyRadialGradientStroke(context, (RadialGradientStroke)shapeStroke, sprite, contextOpacity);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
static void TranslateAndApplyLinearGradientStroke(
|
||||
LayerContext context,
|
||||
LinearGradientStroke shapeStroke,
|
||||
CompositionSpriteShape sprite,
|
||||
CompositeOpacity contextOpacity)
|
||||
{
|
||||
ApplyCommonStrokeProperties(
|
||||
context,
|
||||
shapeStroke,
|
||||
TranslateLinearGradient(context, shapeStroke, contextOpacity),
|
||||
sprite);
|
||||
}
|
||||
|
||||
static void TranslateAndApplyRadialGradientStroke(
|
||||
LayerContext context,
|
||||
RadialGradientStroke shapeStroke,
|
||||
CompositionSpriteShape sprite,
|
||||
CompositeOpacity contextOpacity)
|
||||
{
|
||||
ApplyCommonStrokeProperties(
|
||||
context,
|
||||
shapeStroke,
|
||||
TranslateRadialGradient(context, shapeStroke, contextOpacity),
|
||||
sprite);
|
||||
}
|
||||
|
||||
static void TranslateAndApplySolidColorStroke(
|
||||
LayerContext context,
|
||||
SolidColorStroke shapeStroke,
|
||||
CompositionSpriteShape sprite,
|
||||
CompositeOpacity contextOpacity)
|
||||
{
|
||||
ApplyCommonStrokeProperties(
|
||||
context,
|
||||
shapeStroke,
|
||||
TranslateSolidColorStrokeColor(context, shapeStroke, contextOpacity),
|
||||
sprite);
|
||||
|
||||
// NOTE: DashPattern animation (animating dash sizes) are not supported on CompositionSpriteShape.
|
||||
foreach (var dash in shapeStroke.DashPattern)
|
||||
{
|
||||
sprite.StrokeDashArray.Add((float)dash);
|
||||
}
|
||||
|
||||
// Set DashOffset
|
||||
var strokeDashOffset = Optimizer.TrimAnimatable(context, shapeStroke.DashOffset);
|
||||
if (strokeDashOffset.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, strokeDashOffset, sprite, nameof(sprite.StrokeDashOffset));
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.StrokeDashOffset = (float)strokeDashOffset.InitialValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the properties that are common to all Lottie ShapeStrokes to a CompositionSpriteShape.
|
||||
static void ApplyCommonStrokeProperties(
|
||||
LayerContext context,
|
||||
ShapeStroke shapeStroke,
|
||||
CompositionBrush brush,
|
||||
CompositionSpriteShape sprite)
|
||||
{
|
||||
var strokeThickness = Optimizer.TrimAnimatable(context, shapeStroke.StrokeWidth);
|
||||
|
||||
if (!ThemePropertyBindings.TryBindScalarPropertyToTheme(
|
||||
context: context,
|
||||
target: sprite,
|
||||
bindingSpec: shapeStroke.Name,
|
||||
lottiePropertyName: nameof(shapeStroke.StrokeWidth),
|
||||
compositionPropertyName: nameof(sprite.StrokeThickness),
|
||||
defaultValue: strokeThickness.InitialValue))
|
||||
{
|
||||
if (strokeThickness.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, strokeThickness, sprite, nameof(sprite.StrokeThickness));
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.StrokeThickness = ConvertTo.FloatDefaultIsOne(strokeThickness.InitialValue);
|
||||
}
|
||||
}
|
||||
|
||||
sprite.StrokeStartCap = sprite.StrokeEndCap = sprite.StrokeDashCap = ConvertTo.StrokeCapDefaultIsFlat(shapeStroke.CapType);
|
||||
|
||||
sprite.StrokeLineJoin = ConvertTo.StrokeLineJoinDefaultIsMiter(shapeStroke.JoinType);
|
||||
|
||||
// Lottie (and SVG/CSS) defines miter limit as (miter_length / stroke_thickness).
|
||||
// WUC defines miter limit as (miter_length / (2*stroke_thickness).
|
||||
// WUC requires the value not be < 1.
|
||||
sprite.StrokeMiterLimit = ConvertTo.FloatDefaultIsOne(Math.Max(shapeStroke.MiterLimit / 2, 1));
|
||||
|
||||
sprite.StrokeBrush = brush;
|
||||
}
|
||||
|
||||
public static CompositionBrush TranslateShapeFill(LayerContext context, ShapeFill shapeFill, CompositeOpacity opacity)
|
||||
{
|
||||
if (shapeFill is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (shapeFill.FillKind)
|
||||
{
|
||||
case ShapeFill.ShapeFillKind.SolidColor:
|
||||
return TranslateSolidColorFill(context, (SolidColorFill)shapeFill, opacity);
|
||||
case ShapeFill.ShapeFillKind.LinearGradient:
|
||||
return TranslateLinearGradient(context, (LinearGradientFill)shapeFill, opacity);
|
||||
case ShapeFill.ShapeFillKind.RadialGradient:
|
||||
return TranslateRadialGradient(context, (RadialGradientFill)shapeFill, opacity);
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
static CompositionColorBrush TranslateSolidColorStrokeColor(
|
||||
LayerContext context,
|
||||
SolidColorStroke shapeStroke,
|
||||
CompositeOpacity inheritedOpacity)
|
||||
=> TranslateSolidColorWithBindings(
|
||||
context,
|
||||
shapeStroke.Color,
|
||||
inheritedOpacity.ComposedWith(Optimizer.TrimAnimatable(context, shapeStroke.Opacity)),
|
||||
bindingSpec: shapeStroke.Name);
|
||||
|
||||
static CompositionColorBrush TranslateSolidColorFill(
|
||||
LayerContext context,
|
||||
SolidColorFill shapeFill,
|
||||
CompositeOpacity inheritedOpacity)
|
||||
=> TranslateSolidColorWithBindings(
|
||||
context,
|
||||
shapeFill.Color,
|
||||
inheritedOpacity.ComposedWith(Optimizer.TrimAnimatable(context, shapeFill.Opacity)),
|
||||
bindingSpec: shapeFill.Name);
|
||||
|
||||
// Returns a single color that can be used to represent the given animatable color.
|
||||
// This is used as the default color for property bindings. If the animatable color is
|
||||
// not animated then we return its value. If it's animated we return the value of the
|
||||
// keyframe with the highest alpha, so that it's likely to be visible.
|
||||
// The actual color we return here isn't all that important since it is expected to be set
|
||||
// to some other value at runtime via property binding, but it is handy to have a visible
|
||||
// color when testing, and even better if the color looks like what the designer saw.
|
||||
static Color DefaultValueOf(Animatable<Color> animatableColor)
|
||||
=> animatableColor.IsAnimated
|
||||
? animatableColor.KeyFrames.ToArray().OrderByDescending(kf => kf.Value.A).First().Value
|
||||
: animatableColor.InitialValue;
|
||||
|
||||
static CompositionColorBrush TranslateSolidColorWithBindings(
|
||||
LayerContext context,
|
||||
Animatable<Color> color,
|
||||
CompositeOpacity opacity,
|
||||
string bindingSpec)
|
||||
{
|
||||
// Look for a color binding embedded into the name of the fill or stroke.
|
||||
var bindingName = ThemePropertyBindings.GetThemeBindingNameForLottieProperty(context, bindingSpec, "Color");
|
||||
|
||||
if (bindingName != null)
|
||||
{
|
||||
// A color binding string was found. Bind the color to a property with the
|
||||
// name described by the binding string.
|
||||
return TranslateBoundSolidColor(context, opacity, bindingName, DefaultValueOf(color));
|
||||
}
|
||||
|
||||
if (context.Translation.ColorPalette != null && !color.IsAnimated)
|
||||
{
|
||||
// Color palette binding is enabled. Bind the color to a property with
|
||||
// the name of the color in the palette.
|
||||
var paletteColor = color.InitialValue;
|
||||
|
||||
if (!context.Translation.ColorPalette.TryGetValue(paletteColor, out bindingName))
|
||||
{
|
||||
bindingName = $"Color_{ConvertTo.Color(paletteColor).HexWithoutAlpha}";
|
||||
context.Translation.ColorPalette.Add(paletteColor, bindingName);
|
||||
}
|
||||
|
||||
return TranslateBoundSolidColor(context, opacity, bindingName, paletteColor);
|
||||
}
|
||||
|
||||
// Do not generate a binding for this color.
|
||||
return Brushes.CreateAnimatedColorBrush(context, Optimizer.TrimAnimatable(context, color), opacity);
|
||||
}
|
||||
|
||||
// Translates a SolidColorFill that gets its color value from a property set value with the given name.
|
||||
static CompositionColorBrush TranslateBoundSolidColor(
|
||||
LayerContext context,
|
||||
CompositeOpacity opacity,
|
||||
string bindingName,
|
||||
Color defaultColor)
|
||||
{
|
||||
// Ensure there is a property added to the theme property set.
|
||||
ThemePropertyBindings.EnsureColorThemePropertyExists(context, bindingName, defaultColor);
|
||||
|
||||
var result = context.ObjectFactory.CreateColorBrush();
|
||||
|
||||
if (context.Translation.AddDescriptions)
|
||||
{
|
||||
result.SetDescription(context, $"Color bound to theme property value: {bindingName}", bindingName);
|
||||
|
||||
// Name the brush with a name that includes the binding name. This will allow the code generator to
|
||||
// give its factory a more meaningful name.
|
||||
result.SetName($"ThemeColor_{bindingName}");
|
||||
}
|
||||
|
||||
if (opacity.IsAnimated)
|
||||
{
|
||||
// The opacity has animation. Create an expression to relate the opacity value to the color value.
|
||||
var animatableOpacities =
|
||||
(from a in opacity.GetAnimatables().Zip(PositiveInts, (first, second) => (First: first, Second: second))
|
||||
select (animatable: a.First, name: $"Opacity{a.Second}")).ToArray();
|
||||
|
||||
// Add a property for each opacity.
|
||||
foreach (var (animatable, name) in animatableOpacities)
|
||||
{
|
||||
var trimmed = Optimizer.TrimAnimatable(context, animatable);
|
||||
var propertyName = name;
|
||||
result.Properties.InsertScalar(propertyName, ConvertTo.Opacity(trimmed.InitialValue));
|
||||
|
||||
// The opacity is animated, but it might be non-animated after trimming.
|
||||
if (trimmed.IsAnimated)
|
||||
{
|
||||
Animate.Opacity(context, trimmed, result.Properties, propertyName, propertyName, null);
|
||||
}
|
||||
}
|
||||
|
||||
var opacityScalarExpressions = animatableOpacities.Select(a => Expr.Scalar($"my.{a.name}")).ToArray();
|
||||
var anim = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.ThemedColorAsVector4MultipliedByOpacities(bindingName, opacityScalarExpressions));
|
||||
anim.SetReferenceParameter("my", result.Properties);
|
||||
anim.SetReferenceParameter(ThemePropertyBindings.ThemePropertiesName, ThemePropertyBindings.GetThemePropertySet(context));
|
||||
|
||||
Animate.ColorWithExpression(result, anim);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Opacity isn't animated.
|
||||
// Create an expression that multiplies the alpha channel of the color by the opacity value.
|
||||
var anim = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.ThemedColorMultipliedByOpacity(bindingName, opacity.NonAnimatedValue));
|
||||
anim.SetReferenceParameter(ThemePropertyBindings.ThemePropertiesName, ThemePropertyBindings.GetThemePropertySet(context));
|
||||
Animate.ColorWithExpression(result, anim);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static CompositionLinearGradientBrush TranslateLinearGradient(
|
||||
LayerContext context,
|
||||
IGradient linearGradient,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
var result = context.ObjectFactory.CreateLinearGradientBrush();
|
||||
|
||||
// BodyMovin specifies start and end points in absolute values.
|
||||
result.MappingMode = CompositionMappingMode.Absolute;
|
||||
|
||||
var startPoint = Optimizer.TrimAnimatable(context, linearGradient.StartPoint);
|
||||
var endPoint = Optimizer.TrimAnimatable(context, linearGradient.EndPoint);
|
||||
|
||||
if (startPoint.IsAnimated)
|
||||
{
|
||||
Animate.Vector2(context, startPoint, result, nameof(result.StartPoint));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.StartPoint = ConvertTo.Vector2(startPoint.InitialValue);
|
||||
}
|
||||
|
||||
if (endPoint.IsAnimated)
|
||||
{
|
||||
Animate.Vector2(context, endPoint, result, nameof(result.EndPoint));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.EndPoint = ConvertTo.Vector2(endPoint.InitialValue);
|
||||
}
|
||||
|
||||
var gradientStops = Optimizer.TrimAnimatable(context, linearGradient.GradientStops);
|
||||
|
||||
if (gradientStops.InitialValue.IsEmpty)
|
||||
{
|
||||
// If there are no gradient stops then we can't create a brush.
|
||||
return null;
|
||||
}
|
||||
|
||||
TranslateAndApplyGradientStops(context, result, in gradientStops, opacity);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static CompositionGradientBrush TranslateRadialGradient(
|
||||
LayerContext context,
|
||||
IRadialGradient gradient,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
if (!context.ObjectFactory.IsUapApiAvailable(nameof(CompositionRadialGradientBrush), versionDependentFeatureDescription: "Radial gradient fill"))
|
||||
{
|
||||
// CompositionRadialGradientBrush didn't exist until UAP v8. If the target OS doesn't support
|
||||
// UAP v8 then fall back to linear gradients as a compromise.
|
||||
return TranslateLinearGradient(context, gradient, opacity);
|
||||
}
|
||||
|
||||
var result = context.ObjectFactory.CreateRadialGradientBrush();
|
||||
|
||||
// BodyMovin specifies start and end points in absolute values.
|
||||
result.MappingMode = CompositionMappingMode.Absolute;
|
||||
|
||||
var startPoint = Optimizer.TrimAnimatable(context, gradient.StartPoint);
|
||||
var endPoint = Optimizer.TrimAnimatable(context, gradient.EndPoint);
|
||||
|
||||
if (startPoint.IsAnimated)
|
||||
{
|
||||
Animate.Vector2(context, startPoint, result, nameof(result.EllipseCenter));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.EllipseCenter = ConvertTo.Vector2(startPoint.InitialValue);
|
||||
}
|
||||
|
||||
if (endPoint.IsAnimated)
|
||||
{
|
||||
// We don't yet support animated EndPoint.
|
||||
context.Issues.GradientFillIsNotSupported("Radial", "animated end point");
|
||||
}
|
||||
|
||||
result.EllipseRadius = new Sn.Vector2(Sn.Vector2.Distance(ConvertTo.Vector2(startPoint.InitialValue), ConvertTo.Vector2(endPoint.InitialValue)));
|
||||
|
||||
if (gradient.HighlightLength != null &&
|
||||
(gradient.HighlightLength.InitialValue != 0 || gradient.HighlightLength.IsAnimated))
|
||||
{
|
||||
// We don't yet support animated HighlightLength.
|
||||
context.Issues.GradientFillIsNotSupported("Radial", "animated highlight length");
|
||||
}
|
||||
|
||||
var gradientStops = Optimizer.TrimAnimatable(context, gradient.GradientStops);
|
||||
|
||||
if (gradientStops.InitialValue.IsEmpty)
|
||||
{
|
||||
// If there are no gradient stops then we can't create a brush.
|
||||
return null;
|
||||
}
|
||||
|
||||
TranslateAndApplyGradientStops(context, result, in gradientStops, opacity);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void TranslateAndApplyGradientStops(
|
||||
LayerContext context,
|
||||
CompositionGradientBrush brush,
|
||||
in TrimmedAnimatable<Sequence<GradientStop>> gradientStops,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
if (gradientStops.IsAnimated)
|
||||
{
|
||||
TranslateAndApplyAnimatedGradientStops(context, brush, gradientStops, opacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateAndApplyNonAnimatedGradientStops(context, brush, gradientStops.InitialValue, opacity);
|
||||
}
|
||||
}
|
||||
|
||||
static void TranslateAndApplyAnimatedGradientStops(
|
||||
LayerContext context,
|
||||
CompositionGradientBrush brush,
|
||||
in TrimmedAnimatable<Sequence<GradientStop>> gradientStops,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
if (opacity.IsAnimated)
|
||||
{
|
||||
TranslateAndApplyAnimatedGradientStopsWithAnimatedOpacity(context, brush, in gradientStops, opacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateAndApplyAnimatedColorGradientStopsWithStaticOpacity(context, brush, in gradientStops, opacity.NonAnimatedValue);
|
||||
}
|
||||
}
|
||||
|
||||
static void TranslateAndApplyAnimatedGradientStopsWithAnimatedOpacity(
|
||||
LayerContext context,
|
||||
CompositionGradientBrush brush,
|
||||
in TrimmedAnimatable<Sequence<GradientStop>> gradientStops,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
// Lottie represents animation of stops as a sequence of lists of stops.
|
||||
// WinComp uses a single list of stops where each stop is animated.
|
||||
|
||||
// Lottie represents stops as either color or opacity stops. Convert them all to color stops.
|
||||
var colorStopKeyFrames = gradientStops.KeyFrames.SelectToArray(kf => GradientStopOptimizer.Optimize(kf));
|
||||
colorStopKeyFrames = GradientStopOptimizer.RemoveRedundantStops(colorStopKeyFrames).ToArray();
|
||||
var stopsCount = colorStopKeyFrames[0].Value.Count();
|
||||
var keyframesCount = colorStopKeyFrames.Length;
|
||||
|
||||
// The opacity has animation. Create an expression to relate the opacity value to the color value.
|
||||
var animatableOpacities =
|
||||
(from a in opacity.GetAnimatables().Zip(PositiveInts, (first, second) => (First: first, Second: second))
|
||||
select (animatable: a.First, name: $"Opacity{a.Second}")).ToArray();
|
||||
|
||||
// Add a property for each opacity.
|
||||
foreach (var (animatable, name) in animatableOpacities)
|
||||
{
|
||||
var trimmedOpacity = Optimizer.TrimAnimatable(context, animatable);
|
||||
var propertyName = name;
|
||||
brush.Properties.InsertScalar(propertyName, ConvertTo.Opacity(trimmedOpacity.InitialValue * 255));
|
||||
|
||||
// Pre-multiply the opacities by 255 so we can use the simpler
|
||||
// expression for multiplying color by opacity.
|
||||
Animate.ScaledOpacity(context, trimmedOpacity, 255, brush.Properties, propertyName, propertyName, null);
|
||||
}
|
||||
|
||||
var opacityExpressions = animatableOpacities.Select(ao => Expr.Scalar($"my.{ao.name}")).ToArray();
|
||||
|
||||
// Create the Composition stops and animate them.
|
||||
for (var i = 0; i < stopsCount; i++)
|
||||
{
|
||||
var gradientStop = context.ObjectFactory.CreateColorGradientStop();
|
||||
|
||||
gradientStop.SetDescription(context, () => $"Stop {i}");
|
||||
|
||||
brush.ColorStops.Add(gradientStop);
|
||||
|
||||
// Extract the color key frames for this stop.
|
||||
var colorKeyFrames = ExtractKeyFramesFromColorStopKeyFrames(
|
||||
colorStopKeyFrames,
|
||||
i,
|
||||
gs => ExpressionFactory.ColorMultipliedByPreMultipliedOpacities(ConvertTo.Color(gs.Color), opacityExpressions)).ToArray();
|
||||
|
||||
// Bind the color to the opacities multiplied by the colors.
|
||||
Animate.ColorWithExpressionKeyFrameAnimation(
|
||||
context,
|
||||
new TrimmedAnimatable<WinCompData.Expressions.Color>(context, colorKeyFrames[0].Value, colorKeyFrames),
|
||||
gradientStop,
|
||||
nameof(gradientStop.Color),
|
||||
anim => anim.SetReferenceParameter("my", brush.Properties));
|
||||
|
||||
// Extract the offset key frames for this stop.
|
||||
var offsetKeyFrames = ExtractKeyFramesFromColorStopKeyFrames(colorStopKeyFrames, i, gs => gs.Offset).ToArray();
|
||||
Animate.Scalar(
|
||||
context,
|
||||
new TrimmedAnimatable<double>(context, offsetKeyFrames[0].Value, offsetKeyFrames),
|
||||
gradientStop,
|
||||
nameof(gradientStop.Offset));
|
||||
}
|
||||
}
|
||||
|
||||
static void TranslateAndApplyAnimatedColorGradientStopsWithStaticOpacity(
|
||||
LayerContext context,
|
||||
CompositionGradientBrush brush,
|
||||
in TrimmedAnimatable<Sequence<GradientStop>> gradientStops,
|
||||
Opacity opacity)
|
||||
{
|
||||
// Lottie represents animation of stops as a sequence of lists of stops.
|
||||
// WinComp uses a single list of stops where each stop is animated.
|
||||
|
||||
// Lottie represents stops as either color or opacity stops. Convert them all to color stops.
|
||||
var colorStopKeyFrames = gradientStops.KeyFrames.SelectToArray(kf => GradientStopOptimizer.Optimize(kf));
|
||||
colorStopKeyFrames = GradientStopOptimizer.RemoveRedundantStops(colorStopKeyFrames).ToArray();
|
||||
var stopsCount = colorStopKeyFrames[0].Value.Count();
|
||||
var keyframesCount = colorStopKeyFrames.Length;
|
||||
|
||||
// Create the Composition stops and animate them.
|
||||
for (var i = 0; i < stopsCount; i++)
|
||||
{
|
||||
var gradientStop = context.ObjectFactory.CreateColorGradientStop();
|
||||
|
||||
gradientStop.SetDescription(context, () => $"Stop {i}");
|
||||
|
||||
brush.ColorStops.Add(gradientStop);
|
||||
|
||||
// Extract the color key frames for this stop.
|
||||
var colorKeyFrames = ExtractKeyFramesFromColorStopKeyFrames(
|
||||
colorStopKeyFrames,
|
||||
i,
|
||||
gs => gs.Color * opacity).ToArray();
|
||||
|
||||
Animate.Color(
|
||||
context,
|
||||
new TrimmedAnimatable<Color>(context, colorKeyFrames[0].Value, colorKeyFrames),
|
||||
gradientStop,
|
||||
nameof(gradientStop.Color));
|
||||
|
||||
// Extract the offset key frames for this stop.
|
||||
var offsetKeyFrames = ExtractKeyFramesFromColorStopKeyFrames(colorStopKeyFrames, i, gs => gs.Offset).ToArray();
|
||||
Animate.Scalar(
|
||||
context,
|
||||
new TrimmedAnimatable<double>(context, offsetKeyFrames[0].Value, offsetKeyFrames),
|
||||
gradientStop,
|
||||
nameof(gradientStop.Offset));
|
||||
}
|
||||
}
|
||||
|
||||
static void TranslateAndApplyNonAnimatedGradientStops(
|
||||
LayerContext context,
|
||||
CompositionGradientBrush brush,
|
||||
Sequence<GradientStop> gradientStops,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
var optimizedGradientStops = GradientStopOptimizer.OptimizeColorStops(GradientStopOptimizer.Optimize(gradientStops));
|
||||
|
||||
if (opacity.IsAnimated)
|
||||
{
|
||||
TranslateAndApplyNonAnimatedColorGradientStopsWithAnimatedOpacity(context, brush, optimizedGradientStops, opacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateAndApplyNonAnimatedColorGradientStopsWithStaticOpacity(context, brush, optimizedGradientStops, opacity.NonAnimatedValue);
|
||||
}
|
||||
}
|
||||
|
||||
static void TranslateAndApplyNonAnimatedColorGradientStopsWithStaticOpacity(
|
||||
LayerContext context,
|
||||
CompositionGradientBrush brush,
|
||||
IEnumerable<ColorGradientStop> gradientStops,
|
||||
Opacity opacity)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var stop in gradientStops)
|
||||
{
|
||||
var color = stop.Color * opacity;
|
||||
|
||||
var gradientStop = context.ObjectFactory.CreateColorGradientStop(ConvertTo.Float(stop.Offset), color);
|
||||
|
||||
gradientStop.SetDescription(context, () => $"Stop {i}");
|
||||
|
||||
brush.ColorStops.Add(gradientStop);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
static void TranslateAndApplyNonAnimatedColorGradientStopsWithAnimatedOpacity(
|
||||
LayerContext context,
|
||||
CompositionGradientBrush brush,
|
||||
IEnumerable<ColorGradientStop> gradientStops,
|
||||
CompositeOpacity opacity)
|
||||
{
|
||||
// The opacity has animation. Create an expression to relate the opacity value to the color value.
|
||||
var animatableOpacities =
|
||||
(from a in opacity.GetAnimatables().Zip(PositiveInts, (first, second) => (First: first, Second: second))
|
||||
select (animatable: a.First, name: $"Opacity{a.Second}")).ToArray();
|
||||
|
||||
// Add a property for each opacity.
|
||||
foreach (var (animatable, name) in animatableOpacities)
|
||||
{
|
||||
var trimmedOpacity = Optimizer.TrimAnimatable(context, animatable);
|
||||
var propertyName = name;
|
||||
brush.Properties.InsertScalar(propertyName, ConvertTo.Opacity(trimmedOpacity.InitialValue * 255));
|
||||
|
||||
// The opacity is animated, but it might be non-animated after trimming.
|
||||
if (trimmedOpacity.IsAnimated)
|
||||
{
|
||||
// Pre-multiply the opacities by 255 so we can use the simpler
|
||||
// expression for multiplying color by opacity.
|
||||
Animate.ScaledOpacity(context, trimmedOpacity, 255, brush.Properties, propertyName, propertyName, null);
|
||||
}
|
||||
}
|
||||
|
||||
var opacityExpressions = animatableOpacities.Select(ao => Expr.Scalar($"my.{ao.name}")).ToArray();
|
||||
|
||||
var i = 0;
|
||||
foreach (var stop in gradientStops)
|
||||
{
|
||||
var gradientStop = context.ObjectFactory.CreateColorGradientStop();
|
||||
|
||||
gradientStop.SetDescription(context, () => $"Stop {i}");
|
||||
|
||||
gradientStop.Offset = ConvertTo.Float(stop.Offset);
|
||||
|
||||
if (stop.Color.A == 0)
|
||||
{
|
||||
// The stop has 0 alpha, so no point multiplying it by opacity.
|
||||
gradientStop.Color = ConvertTo.Color(stop.Color);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bind the color to the opacity multiplied by the color.
|
||||
var anim = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.ColorMultipliedByPreMultipliedOpacities(ConvertTo.Color(stop.Color), opacityExpressions));
|
||||
anim.SetReferenceParameter("my", brush.Properties);
|
||||
Animate.ColorWithExpression(gradientStop, anim);
|
||||
}
|
||||
|
||||
brush.ColorStops.Add(gradientStop);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
static IEnumerable<KeyFrame<TKeyFrame>> ExtractKeyFramesFromColorStopKeyFrames<TKeyFrame>(
|
||||
KeyFrame<Sequence<ColorGradientStop>>[] stops,
|
||||
int stopIndex,
|
||||
Func<ColorGradientStop, TKeyFrame> selector)
|
||||
where TKeyFrame : IEquatable<TKeyFrame>
|
||||
{
|
||||
for (var i = 0; i < stops.Length; i++)
|
||||
{
|
||||
var kf = stops[i];
|
||||
var value = kf.Value[stopIndex];
|
||||
var selected = selector(value);
|
||||
|
||||
yield return kf.CloneWithNewValue(selected);
|
||||
}
|
||||
}
|
||||
|
||||
static TrimmedAnimatable<Color> MultiplyColorByAnimatableOpacity(
|
||||
Color color,
|
||||
in TrimmedAnimatable<Opacity> opacity)
|
||||
{
|
||||
if (!opacity.IsAnimated)
|
||||
{
|
||||
return new TrimmedAnimatable<Color>(opacity.Context, color * opacity.InitialValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multiply the single color value by the opacity animation.
|
||||
return new TrimmedAnimatable<Color>(
|
||||
opacity.Context,
|
||||
initialValue: color * opacity.InitialValue,
|
||||
keyFrames: opacity.KeyFrames.SelectToArray(kf => kf.CloneWithNewValue(color * kf.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
static TrimmedAnimatable<Color> MultiplyAnimatableColorByOpacity(
|
||||
in TrimmedAnimatable<Color> color,
|
||||
Opacity opacity)
|
||||
{
|
||||
var initialColorValue = color.InitialValue * opacity;
|
||||
|
||||
if (color.IsAnimated)
|
||||
{
|
||||
// Multiply the color animation by the opacity.
|
||||
return new TrimmedAnimatable<Color>(
|
||||
color.Context,
|
||||
initialValue: initialColorValue,
|
||||
keyFrames: color.KeyFrames.SelectToArray(kf => kf.CloneWithNewValue(kf.Value * opacity)));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TrimmedAnimatable<Color>(color.Context, initialColorValue);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StateCache
|
||||
{
|
||||
/// <summary>
|
||||
/// A cache of color brushes that are not animated and can therefore be reused.
|
||||
/// </summary>
|
||||
public Dictionary<Color, CompositionColorBrush> NonAnimatedColorBrushes { get; } = new Dictionary<Color, CompositionColorBrush>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// The context in which the top-level layers of a <see cref="LottieComposition"/> or the layers
|
||||
/// of a <see cref="PreCompLayer"/> are translated.
|
||||
/// </summary>
|
||||
sealed class CompositionContext
|
||||
{
|
||||
internal CompositionContext(
|
||||
TranslationContext context,
|
||||
LayerCollection layers,
|
||||
Sn.Vector2 size,
|
||||
double startTime,
|
||||
double durationInFrames)
|
||||
{
|
||||
Translation = context;
|
||||
Layers = layers;
|
||||
Size = size;
|
||||
StartTime = startTime;
|
||||
DurationInFrames = durationInFrames;
|
||||
ObjectFactory = Translation.ObjectFactory;
|
||||
Issues = Translation.Issues;
|
||||
}
|
||||
|
||||
internal CompositionContext(
|
||||
TranslationContext context,
|
||||
LottieComposition lottieComposition)
|
||||
: this(
|
||||
context,
|
||||
lottieComposition.Layers,
|
||||
new Sn.Vector2((float)lottieComposition.Width, (float)lottieComposition.Height),
|
||||
startTime: lottieComposition.InPoint,
|
||||
durationInFrames: lottieComposition.OutPoint - lottieComposition.InPoint)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Translation"/> in which the contents are being translated.
|
||||
/// </summary>
|
||||
public TranslationContext Translation { get; }
|
||||
|
||||
public CompositionObjectFactory ObjectFactory { get; }
|
||||
|
||||
public TranslationIssues Issues { get; }
|
||||
|
||||
internal Sn.Vector2 Size { get; }
|
||||
|
||||
internal double StartTime { get; }
|
||||
|
||||
internal double EndTime => StartTime + DurationInFrames;
|
||||
|
||||
internal double DurationInFrames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The layers in this context.
|
||||
/// </summary>
|
||||
public LayerCollection Layers { get; }
|
||||
|
||||
public ImageLayerContext CreateLayerContext(ImageLayer layer) => new ImageLayerContext(this, layer);
|
||||
|
||||
public PreCompLayerContext CreateLayerContext(PreCompLayer layer) => new PreCompLayerContext(this, layer);
|
||||
|
||||
public ShapeLayerContext CreateLayerContext(ShapeLayer layer) => new ShapeLayerContext(this, layer);
|
||||
|
||||
public SolidLayerContext CreateLayerContext(SolidLayer layer) => new SolidLayerContext(this, layer);
|
||||
|
||||
public TextLayerContext CreateLayerContext(TextLayer layer) => new TextLayerContext(this, layer);
|
||||
|
||||
/// <summary>
|
||||
/// Allow a <see cref="CompositionContext"/> to be used wherever a <see cref="Translation"/> is required.
|
||||
/// </summary>
|
||||
public static implicit operator TranslationContext(CompositionContext obj) => obj.Translation;
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
{
|
||||
sealed class CompositionObjectFactory
|
||||
{
|
||||
readonly TranslationContext _context;
|
||||
readonly Compositor _compositor;
|
||||
|
||||
// The UAP version of the Compositor.
|
||||
|
@ -43,8 +44,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
// Holds a StepEasingFunction that can be reused in multiple animations.
|
||||
readonly StepEasingFunction _jumpStepEasingFunction;
|
||||
|
||||
internal CompositionObjectFactory(Compositor compositor, uint targetUapVersion)
|
||||
internal CompositionObjectFactory(TranslationContext context, Compositor compositor, uint targetUapVersion)
|
||||
{
|
||||
_context = context;
|
||||
_compositor = compositor;
|
||||
_targetUapVersion = targetUapVersion;
|
||||
|
||||
|
@ -86,6 +88,21 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given API is available for the current translation.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the given api is available in this translation.</returns>
|
||||
internal bool IsUapApiAvailable(string apiName, string versionDependentFeatureDescription)
|
||||
{
|
||||
if (!IsUapApiAvailable(apiName))
|
||||
{
|
||||
_context.Issues.UapVersionNotSupported(versionDependentFeatureDescription, GetUapVersionForApi(apiName).ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal CompositionEllipseGeometry CreateEllipseGeometry() => _compositor.CreateEllipseGeometry();
|
||||
|
||||
internal CompositionPathGeometry CreatePathGeometry() => _compositor.CreatePathGeometry();
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgcg;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Static methods for converting from Lottie types to Composition and CLR types.
|
||||
/// </summary>
|
||||
static class ConvertTo
|
||||
{
|
||||
public static WinCompData.Wui.Color Color(Color color) =>
|
||||
WinCompData.Wui.Color.FromArgb((byte)(255 * color.A), (byte)(255 * color.R), (byte)(255 * color.G), (byte)(255 * color.B));
|
||||
|
||||
public static Color Color(WinCompData.Wui.Color color) =>
|
||||
LottieData.Color.FromArgb(color.A / 255.0, color.R / 255.0, color.G / 255.0, color.B / 255.0);
|
||||
|
||||
public static float Float(double value) => (float)value;
|
||||
|
||||
public static float Float(Trim value) => (float)value.Value;
|
||||
|
||||
public static float? FloatDefaultIsZero(double value) => value == 0 ? null : (float?)value;
|
||||
|
||||
public static float? FloatDefaultIsOne(double value) => value == 1 ? null : (float?)value;
|
||||
|
||||
public static float Opacity(Opacity value) => (float)value.Value;
|
||||
|
||||
public static float PercentF(double value) => (float)value / 100F;
|
||||
|
||||
public static Sn.Vector2 Vector2(Vector3 vector3) => Vector2(vector3.X, vector3.Y);
|
||||
|
||||
public static Sn.Vector2 Vector2(Vector2 vector2) => Vector2(vector2.X, vector2.Y);
|
||||
|
||||
public static Sn.Vector2 Vector2(double x, double y) => new Sn.Vector2((float)x, (float)y);
|
||||
|
||||
public static Sn.Vector2 Vector2(float x, float y) => new Sn.Vector2(x, y);
|
||||
|
||||
public static Sn.Vector2 Vector2(float x) => new Sn.Vector2(x, x);
|
||||
|
||||
public static Sn.Vector2? Vector2DefaultIsOne(Vector3 vector2)
|
||||
=> vector2.X == 1 && vector2.Y == 1 ? null : (Sn.Vector2?)Vector2(vector2);
|
||||
|
||||
public static Sn.Vector2? Vector2DefaultIsZero(Sn.Vector2 vector2)
|
||||
=> vector2.X == 0 && vector2.Y == 0 ? null : (Sn.Vector2?)vector2;
|
||||
|
||||
public static Sn.Vector2 ClampedVector2(Vector3 vector3) => ClampedVector2((float)vector3.X, (float)vector3.Y);
|
||||
|
||||
public static Sn.Vector2 ClampedVector2(float x, float y) => Vector2(Clamp(x, 0, 1), Clamp(y, 0, 1));
|
||||
|
||||
public static Sn.Vector3 Vector3(double x, double y, double z) => new Sn.Vector3((float)x, (float)y, (float)z);
|
||||
|
||||
public static Sn.Vector3 Vector3(Vector3 vector3) => new Sn.Vector3((float)vector3.X, (float)vector3.Y, (float)vector3.Z);
|
||||
|
||||
public static Sn.Vector3? Vector3DefaultIsZero(Sn.Vector2 vector2)
|
||||
=> vector2.X == 0 && vector2.Y == 0 ? null : (Sn.Vector3?)Vector3(vector2);
|
||||
|
||||
public static Sn.Vector3? Vector3DefaultIsOne(Sn.Vector3 vector3)
|
||||
=> vector3.X == 1 && vector3.Y == 1 && vector3.Z == 1 ? null : (Sn.Vector3?)vector3;
|
||||
|
||||
public static Sn.Vector3? Vector3DefaultIsOne(Vector3 vector3)
|
||||
=> Vector3DefaultIsOne(new Sn.Vector3((float)vector3.X, (float)vector3.Y, (float)vector3.Z));
|
||||
|
||||
public static Sn.Vector3 Vector3(Sn.Vector2 vector2) => Vector3(vector2.X, vector2.Y, 0);
|
||||
|
||||
public static Sn.Vector4 Vector4(WinCompData.Wui.Color color) => new Sn.Vector4(color.R, color.G, color.B, color.A);
|
||||
|
||||
public static WinCompData.Wui.Color Color(Sn.Vector4 color) => WinCompData.Wui.Color.FromArgb((byte)color.W, (byte)color.X, (byte)color.Y, (byte)color.Z);
|
||||
|
||||
static float Clamp(float value, float min, float max)
|
||||
{
|
||||
Debug.Assert(min <= max, "Precondition");
|
||||
return Math.Min(Math.Max(min, value), max);
|
||||
}
|
||||
|
||||
public static CompositionStrokeCap? StrokeCapDefaultIsFlat(ShapeStroke.LineCapType lineCapType)
|
||||
{
|
||||
switch (lineCapType)
|
||||
{
|
||||
case ShapeStroke.LineCapType.Butt:
|
||||
return null;
|
||||
case ShapeStroke.LineCapType.Round:
|
||||
return CompositionStrokeCap.Round;
|
||||
case ShapeStroke.LineCapType.Projected:
|
||||
return CompositionStrokeCap.Square;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public static CompositionStrokeLineJoin? StrokeLineJoinDefaultIsMiter(ShapeStroke.LineJoinType lineJoinType)
|
||||
{
|
||||
switch (lineJoinType)
|
||||
{
|
||||
case ShapeStroke.LineJoinType.Bevel:
|
||||
return CompositionStrokeLineJoin.Bevel;
|
||||
case ShapeStroke.LineJoinType.Miter:
|
||||
return null;
|
||||
case ShapeStroke.LineJoinType.Round:
|
||||
default:
|
||||
return CompositionStrokeLineJoin.Round;
|
||||
}
|
||||
}
|
||||
|
||||
public static CanvasFilledRegionDetermination FilledRegionDetermination(ShapeFill.PathFillType fillType)
|
||||
{
|
||||
return (fillType == ShapeFill.PathFillType.Winding) ? CanvasFilledRegionDetermination.Winding : CanvasFilledRegionDetermination.Alternate;
|
||||
}
|
||||
|
||||
public static CanvasGeometryCombine GeometryCombine(MergePaths.MergeMode mergeMode)
|
||||
{
|
||||
switch (mergeMode)
|
||||
{
|
||||
case MergePaths.MergeMode.Add: return CanvasGeometryCombine.Union;
|
||||
case MergePaths.MergeMode.Subtract: return CanvasGeometryCombine.Exclude;
|
||||
case MergePaths.MergeMode.Intersect: return CanvasGeometryCombine.Intersect;
|
||||
|
||||
// TODO - find out what merge should be - maybe should be a Union.
|
||||
case MergePaths.MergeMode.Merge:
|
||||
case MergePaths.MergeMode.ExcludeIntersections: return CanvasGeometryCombine.Xor;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgcg;
|
||||
using Expr = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions.Expression;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates ellipses to <see cref="CompositionEllipseGeometry"/> and Win2D paths.
|
||||
/// </summary>
|
||||
static class Ellipses
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,9 +16,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
// The name used to bind to the property set that contains the Progress property.
|
||||
internal const string RootName = "_";
|
||||
|
||||
// The name used to bind to the property set that contains the theme properties.
|
||||
internal const string ThemePropertiesName = "_theme";
|
||||
|
||||
internal static readonly Vector2 MyAnchor = MyVector2("Anchor");
|
||||
internal static readonly Vector3 MyAnchor3 = Vector3(MyAnchor.X, MyAnchor.Y, 0);
|
||||
internal static readonly Vector4 MyColor = MyVector4("Color");
|
||||
|
@ -33,7 +30,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
static readonly Scalar MyTEnd = MyScalar("TEnd");
|
||||
|
||||
// An expression that refers to the name of the root property set and the Progress property on it.
|
||||
internal static readonly Scalar RootProgress = RootScalar(LottieToWinCompTranslator.ProgressPropertyName);
|
||||
internal static readonly Scalar RootProgress = RootScalar(TranslationContext.ProgressPropertyName);
|
||||
internal static readonly Scalar MaxTStartTEnd = Max(MyTStart, MyTEnd);
|
||||
internal static readonly Scalar MinTStartTEnd = Min(MyTStart, MyTEnd);
|
||||
static readonly Vector2 HalfMySize = MySize / Vector2(2, 2);
|
||||
|
@ -257,6 +254,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
|
||||
// A property on the theming property set. Used to bind to properties that can be
|
||||
// updated for theming purposes.
|
||||
static string ThemeProperty(string propertyName) => $"{ThemePropertiesName}.{propertyName}";
|
||||
static string ThemeProperty(string propertyName) => $"{ThemePropertyBindings.ThemePropertiesName}.{propertyName}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IDescribable"/>. These make it easier to access the
|
||||
/// members of the interface without requiring a cast, and they add some debug checks
|
||||
/// to help ensure correct usage.
|
||||
/// </summary>
|
||||
static class IDescribableExtensionMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a name on an object. This allows the code generator to give the object
|
||||
/// a more meaningful name.
|
||||
/// </summary>
|
||||
internal static void SetName(
|
||||
this IDescribable obj,
|
||||
string name)
|
||||
{
|
||||
Debug.Assert(obj.Name is null, "Names should never get set more than once.");
|
||||
obj.Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a description on an object.
|
||||
/// </summary>
|
||||
internal static void SetDescription(
|
||||
this IDescribable obj,
|
||||
TranslationContext context,
|
||||
string longDescription,
|
||||
string shortDescription = null)
|
||||
{
|
||||
Debug.Assert(context.AddDescriptions, "Descriptions should only be set when requested.");
|
||||
Debug.Assert(obj.ShortDescription is null, "Descriptions should never get set more than once.");
|
||||
Debug.Assert(obj.LongDescription is null, "Descriptions should never get set more than once.");
|
||||
|
||||
obj.ShortDescription = shortDescription ?? longDescription;
|
||||
obj.LongDescription = longDescription;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a description on an object.
|
||||
/// </summary>
|
||||
internal static void SetDescription(
|
||||
this IDescribable obj,
|
||||
TranslationContext context,
|
||||
Func<string> describer)
|
||||
{
|
||||
if (context.AddDescriptions)
|
||||
{
|
||||
var longDescription = describer();
|
||||
obj.SetDescription(context, longDescription, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a description on an object.
|
||||
/// </summary>
|
||||
internal static void SetDescription(
|
||||
this IDescribable obj,
|
||||
TranslationContext context,
|
||||
Func<(string longDescription, string shortDescription)> describer)
|
||||
{
|
||||
if (context.AddDescriptions)
|
||||
{
|
||||
var (longDescription, shortDescription) = describer();
|
||||
obj.SetDescription(context, longDescription, shortDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
sealed class ImageLayerContext : LayerContext
|
||||
{
|
||||
internal ImageLayerContext(CompositionContext compositionContext, ImageLayer layer)
|
||||
: base(compositionContext, layer)
|
||||
{
|
||||
Layer = layer;
|
||||
}
|
||||
|
||||
public new ImageLayer Layer { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinUIXamlMediaData;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates images.
|
||||
/// </summary>
|
||||
static class Images
|
||||
{
|
||||
public static LayerTranslator CreateImageLayerTranslator(ImageLayerContext context)
|
||||
{
|
||||
if (!Transforms.TryCreateContainerVisualTransformChain(context, out var containerVisualRootNode, out var containerVisualContentNode))
|
||||
{
|
||||
// The layer is never visible.
|
||||
return null;
|
||||
}
|
||||
|
||||
var imageAsset = GetImageAsset(context);
|
||||
if (imageAsset is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var content = context.ObjectFactory.CreateSpriteVisual();
|
||||
containerVisualContentNode.Children.Add(content);
|
||||
content.Size = new Sn.Vector2((float)imageAsset.Width, (float)imageAsset.Height);
|
||||
|
||||
LoadedImageSurface surface;
|
||||
var imageAssetWidth = imageAsset.Width;
|
||||
var imageAssetHeight = imageAsset.Height;
|
||||
|
||||
switch (imageAsset.ImageType)
|
||||
{
|
||||
case ImageAsset.ImageAssetType.Embedded:
|
||||
var embeddedImageAsset = (EmbeddedImageAsset)imageAsset;
|
||||
surface = LoadedImageSurface.StartLoadFromStream(embeddedImageAsset.Bytes);
|
||||
break;
|
||||
case ImageAsset.ImageAssetType.External:
|
||||
var externalImageAsset = (ExternalImageAsset)imageAsset;
|
||||
surface = LoadedImageSurface.StartLoadFromUri(new Uri($"file://localhost/{externalImageAsset.Path}{externalImageAsset.FileName}"));
|
||||
context.Issues.ImageFileRequired($"{externalImageAsset.Path}{externalImageAsset.FileName}");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
var imageBrush = context.ObjectFactory.CreateSurfaceBrush(surface);
|
||||
content.Brush = imageBrush;
|
||||
|
||||
surface.SetDescription(context, () => $"{context.Layer.Name}, {imageAssetWidth}x{imageAssetHeight}");
|
||||
|
||||
return new LayerTranslator.FromVisual(containerVisualRootNode);
|
||||
}
|
||||
|
||||
static ImageAsset GetImageAsset(ImageLayerContext context) =>
|
||||
(ImageAsset)context.Translation.GetAssetById(context, context.Layer.RefId, Asset.AssetType.Image);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization;
|
||||
using LottieOptimizer = Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization.Optimizer;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// The context in which to translate a layer. This is used to ensure that
|
||||
/// layers are translated in the context of the composition or their containing
|
||||
/// PreComp, and to carry around other context-specific state.
|
||||
/// </summary>
|
||||
abstract class LayerContext
|
||||
{
|
||||
FrameNumberEqualityComparer _frameNumberEqualityComparer;
|
||||
|
||||
private protected LayerContext(CompositionContext compositionContext, Layer layer)
|
||||
{
|
||||
CompositionContext = compositionContext;
|
||||
Layer = layer;
|
||||
|
||||
// Copy some frequently accessed properties so they can accessed more efficiently.
|
||||
Translation = compositionContext.Translation;
|
||||
ObjectFactory = Translation.ObjectFactory;
|
||||
Issues = Translation.Issues;
|
||||
}
|
||||
|
||||
public CompositionContext CompositionContext { get; }
|
||||
|
||||
public CompositionObjectFactory ObjectFactory { get; }
|
||||
|
||||
public TranslationContext Translation { get; }
|
||||
|
||||
public TranslationIssues Issues { get; }
|
||||
|
||||
internal Layer Layer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="Layer"/> from which the current layer inherits transforms
|
||||
/// or null if there is no transform parent.
|
||||
/// </summary>
|
||||
public Layer TransformParentLayer =>
|
||||
Layer.Parent.HasValue ? CompositionContext.Layers.GetLayerById(Layer.Parent.Value) : null;
|
||||
|
||||
public override string ToString() => $"{GetType().Name} - {Layer.Name}";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Layer"/>'s in point as a progress value.
|
||||
/// </summary>
|
||||
internal float InPointAsProgress =>
|
||||
(float)((Layer.InPoint - CompositionContext.StartTime) / CompositionContext.DurationInFrames);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Layer"/>'s out point as a progress value.
|
||||
/// </summary>
|
||||
internal float OutPointAsProgress =>
|
||||
(float)((Layer.OutPoint - CompositionContext.StartTime) / CompositionContext.DurationInFrames);
|
||||
|
||||
internal IEqualityComparer<double> FrameNumberComparer =>
|
||||
_frameNumberEqualityComparer ??= new FrameNumberEqualityComparer(this);
|
||||
|
||||
/// <summary>
|
||||
/// Compares frame numbers for equality. This takes into account the lossiness of the conversion
|
||||
/// that is done from <see cref="double"/> frame numbers to <see cref="float"/> progress values.
|
||||
/// </summary>
|
||||
sealed class FrameNumberEqualityComparer : IEqualityComparer<double>
|
||||
{
|
||||
readonly LayerContext _context;
|
||||
|
||||
internal FrameNumberEqualityComparer(LayerContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public bool Equals(double x, double y) => ProgressOf(x) == ProgressOf(y);
|
||||
|
||||
public int GetHashCode(double obj) => ProgressOf(obj).GetHashCode();
|
||||
|
||||
// Converts a frame number into a progress value.
|
||||
float ProgressOf(double value) =>
|
||||
(float)((value - _context.CompositionContext.StartTime) / _context.CompositionContext.DurationInFrames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow a <see cref="LayerContext"/> to be used wherever a <see cref="LottieToWinComp.CompositionContext"/> is required.
|
||||
/// </summary>
|
||||
public static implicit operator CompositionContext(LayerContext obj) => obj.CompositionContext;
|
||||
|
||||
/// <summary>
|
||||
/// Allow a <see cref="LayerContext"/> to be used wherever a <see cref="TranslationContext"/> is required.
|
||||
/// </summary>
|
||||
public static implicit operator TranslationContext(LayerContext obj) => obj.CompositionContext.Translation;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// A factory for a Composition graph that is the result of translating a Lottie subtree.
|
||||
/// The factory is used to create a <see cref="Visual"/>, an optionally also a
|
||||
/// <see cref="CompositionShape"/>.
|
||||
/// </summary>
|
||||
/// <remarks>We try to keep as much as possible of the overall translation as
|
||||
/// CompositionShapes as that should be the most efficient at runtime. However sometimes
|
||||
/// we have to use Visuals. A Shape graph can always be turned into a Visual (by
|
||||
/// wrapping it in a ShapeVisual) but a Visual cannot be turned into a Shape graph.
|
||||
/// </remarks>
|
||||
abstract class LayerTranslator : IDescribable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the translation of the layer as a <see cref="CompositionShape"/>.
|
||||
/// Only valid to call is <see cref="IsShape"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
/// <returns>The CompositionShape.</returns>
|
||||
internal virtual CompositionShape GetShapeRoot(TranslationContext context)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the translation of the layer as a <see cref="Visual"/>.
|
||||
/// </summary>
|
||||
/// <returns>The Visual.</returns>
|
||||
/// <remarks>
|
||||
/// The size (in the context) is needed in case a CompositionShape tree
|
||||
/// needs to be converted to a ShapeVisual. Shape trees need to know their
|
||||
/// maximum size.
|
||||
/// </remarks>
|
||||
internal abstract Visual GetVisualRoot(CompositionContext context);
|
||||
|
||||
/// <summary>
|
||||
/// True if the graph can be represented by a root CompositionShape.
|
||||
/// Otherwise the graph can only be represented by a root Visual.
|
||||
/// Note that all graphs can be represented by a root Visual but only
|
||||
/// some can be represented by a root CompositionShape.
|
||||
/// </summary>
|
||||
internal virtual bool IsShape => false;
|
||||
|
||||
public string LongDescription { get; set; }
|
||||
|
||||
public string ShortDescription { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
private protected void Describe(TranslationContext context, IDescribable obj)
|
||||
{
|
||||
if (context.AddDescriptions && obj.LongDescription is null && obj.ShortDescription is null && !(string.IsNullOrWhiteSpace(LongDescription) || string.IsNullOrWhiteSpace(ShortDescription)))
|
||||
{
|
||||
obj.SetDescription(context, LongDescription, ShortDescription);
|
||||
}
|
||||
|
||||
if (context.AddDescriptions && obj.Name is null && !string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
obj.SetName(Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="LayerTranslator"/> for an eagerly translated Visual.
|
||||
/// </summary>
|
||||
internal sealed class FromVisual : LayerTranslator
|
||||
{
|
||||
readonly Visual _root;
|
||||
|
||||
internal FromVisual(Visual root)
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
|
||||
internal override Visual GetVisualRoot(CompositionContext context)
|
||||
{
|
||||
Describe(context, _root);
|
||||
return _root;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="LayerTranslator"/> for an eagerly translated Shape.
|
||||
/// </summary>
|
||||
internal sealed class FromShape : LayerTranslator
|
||||
{
|
||||
readonly CompositionShape _root;
|
||||
|
||||
internal FromShape(CompositionShape root)
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
|
||||
internal override CompositionShape GetShapeRoot(TranslationContext context)
|
||||
{
|
||||
Describe(context, _root);
|
||||
return _root;
|
||||
}
|
||||
|
||||
internal override Visual GetVisualRoot(CompositionContext context)
|
||||
{
|
||||
// Create a ShapeVisual to hold the CompositionShape.
|
||||
var result = context.ObjectFactory.CreateShapeVisualWithChild(_root, context.Size);
|
||||
Describe(context, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal override bool IsShape => true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
static class Layers
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates each of the layers in the given <see cref="CompositionContext"/>
|
||||
/// to a <see cref="Visual"/>.
|
||||
/// </summary>
|
||||
/// <returns>The translated layers.</returns>
|
||||
public static Visual[] TranslateLayersToVisuals(CompositionContext context)
|
||||
{
|
||||
var layerTranslators =
|
||||
(from layer in context.Layers.GetLayersBottomToTop()
|
||||
let layerTranslator = CreateTranslatorForLayer(context, layer)
|
||||
where layerTranslator != null
|
||||
select (layerTranslator: layerTranslator, layer: layer)).ToArray();
|
||||
|
||||
// Set descriptions on each translate layer so that it's clear where the layer starts.
|
||||
if (context.Translation.AddDescriptions)
|
||||
{
|
||||
foreach (var (layerTranslator, layer) in layerTranslators)
|
||||
{
|
||||
// Add a description if not added already.
|
||||
if (layerTranslator.ShortDescription is null)
|
||||
{
|
||||
layerTranslator.SetDescription(context, $"{layer.Type} layer: {layer.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go through the layers and compose matte layer and layer to be matted into
|
||||
// the resulting visuals. Any layer that is not a matte or matted layer is
|
||||
// simply returned unmodified.
|
||||
var compositionGraphs = Masks.ComposeMattedLayers(context, layerTranslators).ToArray();
|
||||
|
||||
// Layers are translated into either a visual tree or a shape tree. Convert the list of Visual and
|
||||
// Shape roots to a list of Visual roots by wrapping the shape trees in ShapeVisuals.
|
||||
return VisualsAndShapesToVisuals(context, compositionGraphs).ToArray();
|
||||
}
|
||||
|
||||
// Combines 1 or more LayerTranslators as CompositionShape subgraphs under a ShapeVisual.
|
||||
static Visual GetVisualForLayerTranslators(CompositionContext context, IReadOnlyList<LayerTranslator> shapes)
|
||||
{
|
||||
Debug.Assert(shapes.All(s => s.IsShape), "Precondition");
|
||||
|
||||
var compositionShapes = shapes.Select(s => (shape: s.GetShapeRoot(context), subgraph: s)).Where(s => s.shape != null).ToArray();
|
||||
|
||||
switch (compositionShapes.Length)
|
||||
{
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
// There's only 1 shape. Get it to translate directly to a Visual.
|
||||
return compositionShapes[0].subgraph.GetVisualRoot(context);
|
||||
default:
|
||||
// There are multiple contiguous shapes. Group them under a ShapeVisual.
|
||||
// The ShapeVisual has to have a size (it clips to its size).
|
||||
// TODO - if the shape graphs share the same opacity and/or visiblity, get them
|
||||
// to translate without opacity/visiblity and we'll pull those
|
||||
// into the Visual.
|
||||
var shapeVisual = context.ObjectFactory.CreateShapeVisualWithChild(compositionShapes[0].shape, context.Size);
|
||||
|
||||
shapeVisual.SetDescription(context, () => "Layer aggregator");
|
||||
|
||||
for (var i = 1; i < compositionShapes.Length; i++)
|
||||
{
|
||||
shapeVisual.Shapes.Add(compositionShapes[i].shape);
|
||||
}
|
||||
|
||||
return shapeVisual;
|
||||
}
|
||||
}
|
||||
|
||||
// Takes a list of Visuals and Shapes and returns a list of Visuals by combining all direct
|
||||
// sibling shapes together into a ShapeVisual.
|
||||
static IEnumerable<Visual> VisualsAndShapesToVisuals(CompositionContext context, IEnumerable<LayerTranslator> items)
|
||||
{
|
||||
var shapeSubGraphs = new List<LayerTranslator>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.IsShape)
|
||||
{
|
||||
shapeSubGraphs.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (shapeSubGraphs.Count > 0)
|
||||
{
|
||||
var visual = GetVisualForLayerTranslators(context, shapeSubGraphs);
|
||||
|
||||
if (visual != null)
|
||||
{
|
||||
yield return visual;
|
||||
}
|
||||
|
||||
shapeSubGraphs.Clear();
|
||||
}
|
||||
|
||||
var visualRoot = item.GetVisualRoot(context);
|
||||
if (visualRoot != null)
|
||||
{
|
||||
yield return visualRoot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shapeSubGraphs.Count > 0)
|
||||
{
|
||||
var visual = GetVisualForLayerTranslators(context, shapeSubGraphs);
|
||||
if (visual != null)
|
||||
{
|
||||
yield return visual;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="LayerTranslator"/> for the given Lottie layer.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="LayerTranslator"/> that will translate the
|
||||
/// given Lottie layer to a Shape or a Visual.</returns>
|
||||
static LayerTranslator CreateTranslatorForLayer(CompositionContext context, Layer layer)
|
||||
{
|
||||
if (layer.Is3d)
|
||||
{
|
||||
context.Issues.ThreeDLayerIsNotSupported();
|
||||
}
|
||||
|
||||
if (layer.BlendMode != BlendMode.Normal)
|
||||
{
|
||||
context.Issues.BlendModeNotNormal(layer.Name, layer.BlendMode.ToString());
|
||||
}
|
||||
|
||||
if (layer.TimeStretch != 1)
|
||||
{
|
||||
context.Issues.TimeStretchIsNotSupported();
|
||||
}
|
||||
|
||||
if (layer.IsHidden)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (layer.Type)
|
||||
{
|
||||
case Layer.LayerType.Image:
|
||||
return Images.CreateImageLayerTranslator(context.CreateLayerContext((ImageLayer)layer));
|
||||
case Layer.LayerType.Null:
|
||||
// Null layers only exist to hold transforms when declared as parents of other layers.
|
||||
return null;
|
||||
case Layer.LayerType.PreComp:
|
||||
return PreComps.CreatePreCompLayerTranslator(context.CreateLayerContext((PreCompLayer)layer));
|
||||
case Layer.LayerType.Shape:
|
||||
return Shapes.CreateShapeLayerTranslator(context.CreateLayerContext((ShapeLayer)layer));
|
||||
case Layer.LayerType.Solid:
|
||||
return SolidLayers.CreateSolidLayerTranslator(context.CreateLayerContext((SolidLayer)layer));
|
||||
case Layer.LayerType.Text:
|
||||
return TextLayers.CreateTextLayerTranslator(context.CreateLayerContext((TextLayer)layer));
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,23 +7,47 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)AnimatableVector3Rewriter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Animate.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Brushes.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)CanvasGeometryCombiner.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)CompositionContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)CompositeOpacity.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)CompositionObjectFactory.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ContainerShapeOrVisual.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)RectangleOrRoundedRectangleGeometry.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ShapeContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ConvertTo.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)CubicBezierFunction2.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Ellipses.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ExpressionFactory.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Float32.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)LottieToMultiVersionWinCompTranslator.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)IDescribableExtensionMethods.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ImageLayerContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Images.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)LayerContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Layers.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)LayerTranslator.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)LottieToWinCompTranslator.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)LottieToWinCompTranslator.Rectangles.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)LottieToMultiVersionWinCompTranslator.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Masks.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)MultiVersionTranslationResult.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Optimizer.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)PathGeometryGroup.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Paths.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)PreCompLayerContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)PreComps.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ProgressMapFactory.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)PropertyBindings.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)PropertyBindingsParser.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)RectangleOrRoundedRectangleGeometry.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Rectangles.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ShapeContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ShapeLayerContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Shapes.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)SolidLayerContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)SolidLayers.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)TextLayerContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)TextLayers.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ThemePropertyBindings.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Transforms.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)TranslationContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)TranslationIssue.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)TranslationIssues.cs" />
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,461 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgc;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgce;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translation for Lottie masks and mattes.
|
||||
/// </summary>
|
||||
static class Masks
|
||||
{
|
||||
// Translate a mask into shapes for a shape visual. The mask is applied to the visual to be masked
|
||||
// using the VisualSurface. The VisualSurface can take the rendered contents of a visual tree and
|
||||
// use it as a brush. The final masked result is achieved by taking the visual to be masked, putting
|
||||
// it into a VisualSurface, then taking the mask and putting that in a VisualSurface and then combining
|
||||
// the result with a composite effect.
|
||||
public static Visual TranslateAndApplyMasksForLayer(
|
||||
LayerContext context,
|
||||
Visual visualToMask)
|
||||
{
|
||||
var result = visualToMask;
|
||||
var layer = context.Layer;
|
||||
|
||||
if (layer.Masks.Count > 0)
|
||||
{
|
||||
if (layer.Masks.Count == 1)
|
||||
{
|
||||
// Common case for masks: exactly one mask.
|
||||
var masks = layer.Masks.Slice(0, 1);
|
||||
|
||||
switch (masks[0].Mode)
|
||||
{
|
||||
// If there's only 1 mask, Difference and Intersect act the same as Add.
|
||||
case Mask.MaskMode.Add:
|
||||
case Mask.MaskMode.Difference:
|
||||
case Mask.MaskMode.Intersect:
|
||||
case Mask.MaskMode.None:
|
||||
// Composite using the mask.
|
||||
result = TranslateAndApplyMasks(context, masks, result, CanvasComposite.DestinationIn);
|
||||
break;
|
||||
|
||||
case Mask.MaskMode.Subtract:
|
||||
// Composite using the mask.
|
||||
result = TranslateAndApplyMasks(context, masks, result, CanvasComposite.DestinationOut);
|
||||
break;
|
||||
|
||||
default:
|
||||
context.Issues.MaskWithUnsupportedMode(masks[0].Mode.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Uncommon case for masks: multiple masks.
|
||||
// Get the contiguous segments of masks that have the same mode, create a shape tree for each
|
||||
// segment, and composite the shape trees.
|
||||
// The goal here is to use the smallest possible number of composites.
|
||||
// 1) Get the masks that have the same mode and are next to each other in the list of masks.
|
||||
// 2) Translate the masks to a ShapeVisual.
|
||||
// 3) Composite each ShapeVisual with the previous ShapeVisual.
|
||||
foreach (var (index, count) in EnumerateMaskListSegments(layer.Masks.ToArray()))
|
||||
{
|
||||
// Every mask in the segment has the same mode or None. The first mask is never None.
|
||||
var masksWithSameMode = layer.Masks.Slice(index, count);
|
||||
switch (masksWithSameMode[0].Mode)
|
||||
{
|
||||
case Mask.MaskMode.Add:
|
||||
// Composite using the mask, and apply to what has been already masked.
|
||||
result = TranslateAndApplyMasks(context, masksWithSameMode, result, CanvasComposite.DestinationIn);
|
||||
break;
|
||||
case Mask.MaskMode.Subtract:
|
||||
// Composite using the mask, and apply to what has been already masked.
|
||||
result = TranslateAndApplyMasks(context, masksWithSameMode, result, CanvasComposite.DestinationOut);
|
||||
break;
|
||||
default:
|
||||
// Only Add, Subtract, and None modes are currently supported.
|
||||
context.Issues.MaskWithUnsupportedMode(masksWithSameMode[0].Mode.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Translate a matte layer and the layer to be matted into the composited resulting brush.
|
||||
// This brush will be used to paint a sprite visual. The brush is created by using a mask brush
|
||||
// which will use the matted layer as a source and the matte layer as an alpha mask.
|
||||
// A visual tree is turned into a brush by using the CompositionVisualSurface.
|
||||
public static LayerTranslator TranslateMatteLayer(
|
||||
CompositionContext context,
|
||||
Visual matteLayer,
|
||||
Visual mattedLayer,
|
||||
bool invert)
|
||||
{
|
||||
// Calculate the context size which we will use as the size of the images we want to use
|
||||
// for the matte content and the content to be matted.
|
||||
var contextSize = context.Size;
|
||||
var objectFactory = context.ObjectFactory;
|
||||
|
||||
if (objectFactory.IsUapApiAvailable(nameof(CompositionVisualSurface), versionDependentFeatureDescription: "Matte"))
|
||||
{
|
||||
var matteLayerVisualSurface = objectFactory.CreateVisualSurface();
|
||||
matteLayerVisualSurface.SourceVisual = matteLayer;
|
||||
matteLayerVisualSurface.SourceSize = contextSize;
|
||||
var matteSurfaceBrush = objectFactory.CreateSurfaceBrush(matteLayerVisualSurface);
|
||||
|
||||
var mattedLayerVisualSurface = objectFactory.CreateVisualSurface();
|
||||
mattedLayerVisualSurface.SourceVisual = mattedLayer;
|
||||
mattedLayerVisualSurface.SourceSize = contextSize;
|
||||
var mattedSurfaceBrush = objectFactory.CreateSurfaceBrush(mattedLayerVisualSurface);
|
||||
|
||||
return new LayerTranslator.FromVisual(CompositeVisuals(
|
||||
context,
|
||||
matteLayer,
|
||||
mattedLayer,
|
||||
contextSize,
|
||||
Sn.Vector2.Zero,
|
||||
invert ? CanvasComposite.DestinationOut : CanvasComposite.DestinationIn));
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can't translate the matteing. Just return the layer that needed to be matted as a compromise.
|
||||
return new LayerTranslator.FromVisual(mattedLayer);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the collection of layer data and for each pair of matte layer and matted layer, compose them and return a visual
|
||||
// with the composed result. All other items are not touched.
|
||||
public static IEnumerable<LayerTranslator> ComposeMattedLayers(CompositionContext context, IEnumerable<(LayerTranslator translatedLayer, Layer layer)> items)
|
||||
{
|
||||
// Save off the visual for the layer to be matted when we encounter it. The very next
|
||||
// layer is the matte layer.
|
||||
Visual mattedVisual = null;
|
||||
Layer.MatteType matteType = Layer.MatteType.None;
|
||||
|
||||
// NOTE: The items appear in reverse order from how they appear in the original Lottie file.
|
||||
// This means that the layer to be matted appears right before the layer that is the matte.
|
||||
foreach (var (translatedLayer, layer) in items)
|
||||
{
|
||||
var layerIsMattedLayer = false;
|
||||
layerIsMattedLayer = layer.LayerMatteType != Layer.MatteType.None;
|
||||
|
||||
Visual visual = null;
|
||||
|
||||
if (translatedLayer.IsShape)
|
||||
{
|
||||
// If the layer is a shape then we need to wrap it
|
||||
// in a shape visual so that it can be used for matte
|
||||
// composition.
|
||||
if (layerIsMattedLayer || mattedVisual != null)
|
||||
{
|
||||
visual = translatedLayer.GetVisualRoot(context);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
visual = translatedLayer.GetVisualRoot(context);
|
||||
}
|
||||
|
||||
if (visual != null)
|
||||
{
|
||||
// The layer to be matted comes first. The matte layer is the very next layer.
|
||||
if (layerIsMattedLayer)
|
||||
{
|
||||
mattedVisual = visual;
|
||||
matteType = layer.LayerMatteType;
|
||||
}
|
||||
else if (mattedVisual != null)
|
||||
{
|
||||
var compositedMatteVisual = Masks.TranslateMatteLayer(context, visual, mattedVisual, matteType == Layer.MatteType.Invert);
|
||||
mattedVisual = null;
|
||||
matteType = Layer.MatteType.None;
|
||||
yield return compositedMatteVisual;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return the visual that was not a matte layer or a layer to be matted.
|
||||
yield return new LayerTranslator.FromVisual(visual);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return the shape which does not participate in mattes.
|
||||
yield return translatedLayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Takes the paths for the given masks and adds them as shapes on the maskContainerShape.
|
||||
// Requires at least one Mask.
|
||||
static void TranslateAndAddMaskPaths(
|
||||
LayerContext context,
|
||||
IReadOnlyList<Mask> masks,
|
||||
CompositionContainerShape resultContainer)
|
||||
{
|
||||
Debug.Assert(masks.Count > 0, "Precondition");
|
||||
|
||||
var maskMode = masks[0].Mode;
|
||||
|
||||
// Translate the mask paths
|
||||
foreach (var mask in masks)
|
||||
{
|
||||
if (mask.Inverted)
|
||||
{
|
||||
context.Issues.MaskWithInvertIsNotSupported();
|
||||
|
||||
// Mask inverted is not yet supported. Skip this mask.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mask.Opacity.IsAnimated ||
|
||||
!mask.Opacity.InitialValue.IsOpaque)
|
||||
{
|
||||
context.Issues.MaskWithAlphaIsNotSupported();
|
||||
|
||||
// Opacity on masks is not supported. Skip this mask.
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (mask.Mode)
|
||||
{
|
||||
case Mask.MaskMode.None:
|
||||
// Ignore None masks. They are just a way to disable a Mask in After Effects.
|
||||
continue;
|
||||
default:
|
||||
if (mask.Mode != maskMode)
|
||||
{
|
||||
// Every mask must have the same mode.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var path = Optimizer.TrimAnimatable(context, Optimizer.GetOptimized(context, mask.Points));
|
||||
|
||||
var maskSpriteShape = Paths.TranslatePath(context, path, ShapeFill.PathFillType.EvenOdd);
|
||||
|
||||
// The mask geometry needs to be colored with something so that it can be used
|
||||
// as a mask.
|
||||
maskSpriteShape.FillBrush = Brushes.CreateNonAnimatedColorBrush(context, LottieData.Color.Black);
|
||||
|
||||
resultContainer.Shapes.Add(maskSpriteShape);
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerates the segments of Masks with the same MaskMode.
|
||||
static IEnumerable<(int index, int count)> EnumerateMaskListSegments(Mask[] masks)
|
||||
{
|
||||
int i;
|
||||
|
||||
// Find the first non-None mask.
|
||||
for (i = 0; i < masks.Length && masks[i].Mode == Mask.MaskMode.None; i++)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == masks.Length)
|
||||
{
|
||||
// There were only None masks in the list.
|
||||
yield break;
|
||||
}
|
||||
|
||||
var currentMode = masks[i].Mode;
|
||||
var segmentIndex = i;
|
||||
|
||||
for (; i < masks.Length; i++)
|
||||
{
|
||||
var mode = masks[i].Mode;
|
||||
if (mode != currentMode && mode != Mask.MaskMode.None)
|
||||
{
|
||||
// Switching to a new mask mode. Output the segment for the previous mode.
|
||||
yield return (segmentIndex, i - segmentIndex);
|
||||
|
||||
currentMode = mode;
|
||||
segmentIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Output the last segment it's not empty.
|
||||
if (segmentIndex < i)
|
||||
{
|
||||
yield return (segmentIndex, i - segmentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Translates a list of masks to a Visual which can be used to mask another Visual.
|
||||
static Visual TranslateMasks(LayerContext context, IReadOnlyList<Mask> masks)
|
||||
{
|
||||
Debug.Assert(masks.Count > 0, "Precondition");
|
||||
|
||||
// Duplicate the transform chain used on the Layer being masked so
|
||||
// that the mask correctly overlays the Layer.
|
||||
if (!Transforms.TryCreateContainerShapeTransformChain(
|
||||
context,
|
||||
out var containerShapeMaskRootNode,
|
||||
out var containerShapeMaskContentNode))
|
||||
{
|
||||
// The layer is never visible. This should have been discovered already.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
// Create the mask tree from the masks.
|
||||
TranslateAndAddMaskPaths(context, masks, containerShapeMaskContentNode);
|
||||
|
||||
var result = context.ObjectFactory.CreateShapeVisualWithChild(containerShapeMaskRootNode, context.CompositionContext.Size);
|
||||
result.SetDescription(context, () => "Masks");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static Visual TranslateAndApplyMasks(LayerContext context, IReadOnlyList<Mask> masks, Visual visualToMask, CanvasComposite compositeMode)
|
||||
{
|
||||
Debug.Assert(masks.Count > 0, "Precondition");
|
||||
|
||||
if (context.ObjectFactory.IsUapApiAvailable(nameof(CompositionVisualSurface), versionDependentFeatureDescription: "Mask"))
|
||||
{
|
||||
var maskShapeVisual = TranslateMasks(context, masks);
|
||||
|
||||
return CompositeVisuals(
|
||||
context: context,
|
||||
source: maskShapeVisual,
|
||||
destination: visualToMask,
|
||||
size: context.CompositionContext.Size,
|
||||
offset: Sn.Vector2.Zero,
|
||||
compositeMode: compositeMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can't mask, so just return the unmasked visual as a compromise.
|
||||
return visualToMask;
|
||||
}
|
||||
}
|
||||
|
||||
// Combines two visual trees using a CompositeEffect. This is used for Masks and Mattes.
|
||||
// The way that the trees are combined is determined by the composite mode. The composition works as follows:
|
||||
// +--------------+
|
||||
// | SpriteVisual | -- Has the final composited result.
|
||||
// +--------------+
|
||||
// ^
|
||||
// |
|
||||
// +--------------+
|
||||
// | EffectBrush | -- Composition effect brush allows the composite effect result to be used as a brush.
|
||||
// +--------------+
|
||||
// ^
|
||||
// *
|
||||
// *
|
||||
// *
|
||||
// +-----------------+
|
||||
// | CompositeEffect | -- Composite effect does the work to combine the contents
|
||||
// +-----------------+ of the visual surfaces.
|
||||
// |
|
||||
// | +---------+
|
||||
// -> | Sources |
|
||||
// +---------+
|
||||
// ^ ^
|
||||
// | |
|
||||
// | |
|
||||
// | +----------------------+
|
||||
// | | Source Surface Brush | -- Surface brush that will paint with the output of the visual surface
|
||||
// | +----------------------+ that has the source visual assigned to it.
|
||||
// | |
|
||||
// | | +-----------------------+
|
||||
// | -> | Source VisualSurface | -- The visual surface captures the renderable contents of its source visual.
|
||||
// | +-----------------------+
|
||||
// | |
|
||||
// | | +------------------------+
|
||||
// | -> | Source Contents Visual | -- The source visual.
|
||||
// | +------------------------+
|
||||
// |
|
||||
// |
|
||||
// |
|
||||
// +--------------------------+
|
||||
// | Destination SurfaceBrush | -- Surface brush that will paint with the output of the visual surface
|
||||
// +--------------------------+ that has the destination visual assigned to it.
|
||||
// |
|
||||
// | +---------------------------+
|
||||
// -> | Destination VisualSurface | -- The visual surface captures the renderable contents of its source visual.
|
||||
// +---------------------------+
|
||||
// |
|
||||
// | +-----------------------------+
|
||||
// -> | Destination Contents Visual | -- The source visual.
|
||||
// +-----------------------------+
|
||||
static SpriteVisual CompositeVisuals(
|
||||
TranslationContext context,
|
||||
Visual source,
|
||||
Visual destination,
|
||||
Sn.Vector2 size,
|
||||
Sn.Vector2 offset,
|
||||
CanvasComposite compositeMode)
|
||||
{
|
||||
var objectFactory = context.ObjectFactory;
|
||||
|
||||
// The visual surface captures the contents of a visual and displays it in a brush.
|
||||
// If the visual has an offset, it will not be captured by the visual surface.
|
||||
// To capture any offsets we add an intermediate parent container visual so that
|
||||
// the visual we want captured by the visual surface has a parent to use as the
|
||||
// origin of its offsets.
|
||||
var sourceIntermediateParent = objectFactory.CreateContainerVisual();
|
||||
|
||||
// Because this is the root of a tree, the inherited BorderMode is Hard.
|
||||
// We want it to be Soft in order to enable anti-aliasing.
|
||||
// Note that the border mode for trees that are attached to the desktop do not
|
||||
// need to have their BorderMode set as they inherit Soft from the desktop.
|
||||
sourceIntermediateParent.BorderMode = CompositionBorderMode.Soft;
|
||||
sourceIntermediateParent.Children.Add(source);
|
||||
|
||||
var destinationIntermediateParent = objectFactory.CreateContainerVisual();
|
||||
|
||||
// Because this is the root of a tree, the inherited BorderMode is Hard.
|
||||
// We want it to be Soft in order to enable anti-aliasing.
|
||||
// Note that the border mode for trees that are attached to the desktop do not
|
||||
// need to have their BorderMode set as they inherit Soft from the desktop.
|
||||
destinationIntermediateParent.BorderMode = CompositionBorderMode.Soft;
|
||||
destinationIntermediateParent.Children.Add(destination);
|
||||
|
||||
var sourceVisualSurface = objectFactory.CreateVisualSurface();
|
||||
sourceVisualSurface.SourceVisual = sourceIntermediateParent;
|
||||
sourceVisualSurface.SourceSize = ConvertTo.Vector2DefaultIsZero(size);
|
||||
sourceVisualSurface.SourceOffset = ConvertTo.Vector2DefaultIsZero(offset);
|
||||
var sourceVisualSurfaceBrush = objectFactory.CreateSurfaceBrush(sourceVisualSurface);
|
||||
|
||||
var destinationVisualSurface = objectFactory.CreateVisualSurface();
|
||||
destinationVisualSurface.SourceVisual = destinationIntermediateParent;
|
||||
destinationVisualSurface.SourceSize = ConvertTo.Vector2DefaultIsZero(size);
|
||||
destinationVisualSurface.SourceOffset = ConvertTo.Vector2DefaultIsZero(offset);
|
||||
var destinationVisualSurfaceBrush = objectFactory.CreateSurfaceBrush(destinationVisualSurface);
|
||||
|
||||
var compositeEffect = new CompositeEffect();
|
||||
compositeEffect.Mode = compositeMode;
|
||||
|
||||
compositeEffect.Sources.Add(new CompositionEffectSourceParameter("destination"));
|
||||
compositeEffect.Sources.Add(new CompositionEffectSourceParameter("source"));
|
||||
|
||||
var compositionEffectFactory = objectFactory.CreateEffectFactory(compositeEffect);
|
||||
var effectBrush = compositionEffectFactory.CreateBrush();
|
||||
|
||||
effectBrush.SetSourceParameter("destination", destinationVisualSurfaceBrush);
|
||||
effectBrush.SetSourceParameter("source", sourceVisualSurfaceBrush);
|
||||
|
||||
var compositedVisual = objectFactory.CreateSpriteVisual();
|
||||
compositedVisual.Brush = effectBrush;
|
||||
compositedVisual.Size = size;
|
||||
compositedVisual.Offset = ConvertTo.Vector3(offset.X, offset.Y, 0);
|
||||
|
||||
return compositedVisual;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using LottieOptimizer = Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization.Optimizer;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
static class Optimizer
|
||||
{
|
||||
public static Animatable<PathGeometry> GetOptimized(TranslationContext context, Animatable<PathGeometry> value)
|
||||
{
|
||||
var lottieOptimizer = context.GetStateCache<StateCache>().LottieOptimizer;
|
||||
return lottieOptimizer.GetOptimized(value);
|
||||
}
|
||||
|
||||
public static Path OptimizePath(LayerContext context, Path path)
|
||||
{
|
||||
// Optimize the path data. This may result in a previously animated path
|
||||
// becoming non-animated.
|
||||
var optimizedPathData = TrimAnimatable(context, path.Data);
|
||||
|
||||
return path.CloneWithNewGeometry(
|
||||
optimizedPathData.IsAnimated
|
||||
? new Animatable<PathGeometry>(optimizedPathData.KeyFrames, path.Data.PropertyIndex)
|
||||
: new Animatable<PathGeometry>(optimizedPathData.InitialValue, path.Data.PropertyIndex));
|
||||
}
|
||||
|
||||
public static TrimmedAnimatable<Vector3> TrimAnimatable(LayerContext context, IAnimatableVector3 animatable)
|
||||
=> TrimAnimatable<Vector3>(context, (AnimatableVector3)animatable);
|
||||
|
||||
public static TrimmedAnimatable<T> TrimAnimatable<T>(LayerContext context, Animatable<T> animatable)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
if (animatable.IsAnimated)
|
||||
{
|
||||
var trimmedKeyFrames = LottieOptimizer.RemoveRedundantKeyFrames(
|
||||
LottieOptimizer.TrimKeyFrames(
|
||||
animatable,
|
||||
context.CompositionContext.StartTime,
|
||||
context.CompositionContext.EndTime));
|
||||
|
||||
return new TrimmedAnimatable<T>(
|
||||
context,
|
||||
trimmedKeyFrames.Count == 0
|
||||
? animatable.InitialValue
|
||||
: trimmedKeyFrames[0].Value,
|
||||
trimmedKeyFrames);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TrimmedAnimatable<T>(context, animatable.InitialValue, animatable.KeyFrames);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StateCache
|
||||
{
|
||||
public LottieOptimizer LottieOptimizer { get; } = new LottieOptimizer();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
// of PathGeometryGroups. Returns true if it succeeds without issues.
|
||||
// Even if false is returned a best-effort animatable is returned.
|
||||
internal static bool TryGroupPaths(
|
||||
TranslationContext context,
|
||||
ShapeLayerContext context,
|
||||
IEnumerable<Path> paths,
|
||||
out Animatable<PathGeometryGroup> result)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,410 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgcg;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates paths.
|
||||
/// </summary>
|
||||
static class Paths
|
||||
{
|
||||
// Translates a Lottie PathGeometry to a CompositionSpriteShape.
|
||||
public static CompositionSpriteShape TranslatePath(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<PathGeometry> path,
|
||||
ShapeFill.PathFillType fillType)
|
||||
{
|
||||
var result = context.ObjectFactory.CreateSpriteShape();
|
||||
var geometry = context.ObjectFactory.CreatePathGeometry();
|
||||
result.Geometry = geometry;
|
||||
|
||||
var isPathApplied = false;
|
||||
if (path.IsAnimated)
|
||||
{
|
||||
// In cases where the animated path is just being moved in position we can convert
|
||||
// to a static path with an offset animation. This is more efficient because it
|
||||
// results in fewer paths, and it works around the inability to support animated
|
||||
// paths before version 11.
|
||||
if (TryApplyPathAsStaticPathWithAnimatedOffset(context, path, geometry, result, fillType))
|
||||
{
|
||||
isPathApplied = true;
|
||||
}
|
||||
else if (context.ObjectFactory.IsUapApiAvailable(nameof(PathKeyFrameAnimation), versionDependentFeatureDescription: "Path animation"))
|
||||
{
|
||||
// PathKeyFrameAnimation was introduced in 6 but was unreliable until 11.
|
||||
Animate.Path(context, path, fillType, geometry, nameof(geometry.Path), nameof(geometry.Path));
|
||||
isPathApplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPathApplied)
|
||||
{
|
||||
// The Path is not animated, or it is animated but we failed to animate it.
|
||||
geometry.Path = Paths.CompositionPathFromPathGeometry(
|
||||
context,
|
||||
path.InitialValue,
|
||||
fillType,
|
||||
optimizeLines: true);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// If the given path is equivalent to a static path with an animated offset, convert
|
||||
// the path to that form and apply it to the given geometry and shape.
|
||||
static bool TryApplyPathAsStaticPathWithAnimatedOffset(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<PathGeometry> path,
|
||||
CompositionPathGeometry geometry,
|
||||
CompositionSpriteShape shape,
|
||||
ShapeFill.PathFillType fillType)
|
||||
{
|
||||
Debug.Assert(path.IsAnimated, "Precondition");
|
||||
|
||||
var offsets = new Vector2[path.KeyFrames.Count];
|
||||
for (var i = 1; i < path.KeyFrames.Count; i++)
|
||||
{
|
||||
if (!Paths.TryGetPathTranslation(path.KeyFrames[0].Value.BezierSegments, path.KeyFrames[i].Value.BezierSegments, out offsets[i]))
|
||||
{
|
||||
// The animation is not equivalent to a translation.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The path is equivalent to a translation. Apply the path described by the initial key frame
|
||||
// and apply an offset translation to the CompositionSpriteShape that contains it.
|
||||
geometry.Path = Paths.CompositionPathFromPathGeometry(
|
||||
context,
|
||||
path.InitialValue,
|
||||
fillType,
|
||||
optimizeLines: true);
|
||||
|
||||
// Create the offsets key frames.
|
||||
var keyFrames = new KeyFrame<Vector3>[offsets.Length];
|
||||
|
||||
for (var i = 0; i < path.KeyFrames.Count; i++)
|
||||
{
|
||||
ref var offset = ref offsets[i];
|
||||
var pathKeyFrame = path.KeyFrames[i];
|
||||
keyFrames[i] = new KeyFrame<Vector3>(pathKeyFrame.Frame, new Vector3(offset.X, offset.Y, 0), pathKeyFrame.Easing);
|
||||
}
|
||||
|
||||
var offsetAnimatable = new TrimmedAnimatable<Vector3>(context, new Vector3(offsets[0].X, offsets[0].Y, 0), keyFrames);
|
||||
|
||||
// Apply the offset animation.
|
||||
Animate.Vector2(context, offsetAnimatable, shape, nameof(shape.Offset), "Path animation as a translation.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static CompositionShape TranslatePathContent(ShapeContext context, Path path)
|
||||
{
|
||||
// A path is represented as a SpriteShape with a CompositionPathGeometry.
|
||||
var geometry = context.ObjectFactory.CreatePathGeometry();
|
||||
geometry.SetDescription(context, () => $"{path.Name}.PathGeometry");
|
||||
|
||||
var pathData = Optimizer.TrimAnimatable(context, Optimizer.GetOptimized(context, path.Data));
|
||||
|
||||
var compositionSpriteShape = TranslatePath(context, pathData, GetPathFillType(context.Fill));
|
||||
compositionSpriteShape.SetDescription(context, () => path.Name);
|
||||
|
||||
Shapes.TranslateAndApplyShapeContext(
|
||||
context,
|
||||
compositionSpriteShape,
|
||||
path.DrawingDirection == DrawingDirection.Reverse,
|
||||
trimOffsetDegrees: 0);
|
||||
|
||||
return compositionSpriteShape;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups multiple Shapes into a D2D geometry group.
|
||||
/// </summary>
|
||||
/// <returns>The shape.</returns>
|
||||
public static CompositionShape TranslatePathGroupContent(ShapeContext context, IEnumerable<Path> paths)
|
||||
{
|
||||
var groupingSucceeded = PathGeometryGroup.TryGroupPaths(context, paths, out var grouped);
|
||||
|
||||
// If any of the paths have different directions we may not get the translation
|
||||
// right, so check that case and warn the user.
|
||||
var directions = paths.Select(p => p.DrawingDirection).Distinct().ToArray();
|
||||
|
||||
if (!groupingSucceeded || directions.Length > 1)
|
||||
{
|
||||
context.Issues.CombiningMultipleAnimatedPathsIsNotSupported();
|
||||
}
|
||||
|
||||
// A path is represented as a SpriteShape with a CompositionPathGeometry.
|
||||
var compositionPathGeometry = context.ObjectFactory.CreatePathGeometry();
|
||||
|
||||
var compositionSpriteShape = context.ObjectFactory.CreateSpriteShape();
|
||||
compositionSpriteShape.Geometry = compositionPathGeometry;
|
||||
|
||||
var pathGroupData = Optimizer.TrimAnimatable(context, grouped);
|
||||
|
||||
ApplyPathGroup(context, compositionPathGeometry, pathGroupData, GetPathFillType(context.Fill));
|
||||
|
||||
if (context.Translation.AddDescriptions)
|
||||
{
|
||||
var shapeContentName = string.Join("+", paths.Select(sh => sh.Name).Where(a => a != null));
|
||||
compositionSpriteShape.SetDescription(context, shapeContentName);
|
||||
compositionPathGeometry.SetDescription(context, $"{shapeContentName}.PathGeometry");
|
||||
}
|
||||
|
||||
Shapes.TranslateAndApplyShapeContext(
|
||||
context,
|
||||
compositionSpriteShape,
|
||||
reverseDirection: directions[0] == DrawingDirection.Reverse,
|
||||
trimOffsetDegrees: 0);
|
||||
|
||||
return compositionSpriteShape;
|
||||
}
|
||||
|
||||
// Creates a CompositionPath from a single path.
|
||||
public static CompositionPath CompositionPathFromPathGeometry(
|
||||
TranslationContext context,
|
||||
PathGeometry pathGeometry,
|
||||
ShapeFill.PathFillType fillType,
|
||||
bool optimizeLines)
|
||||
{
|
||||
var cache = context.GetStateCache<StateCache>();
|
||||
|
||||
// CompositionPaths can be shared by many SpriteShapes so we cache them here.
|
||||
// Note that an optimizer that ran over the result could do the same job,
|
||||
// but paths are typically very large so it's preferable to cache them here.
|
||||
if (!cache.CompositionPaths.TryGetValue((pathGeometry, fillType, optimizeLines), out var result))
|
||||
{
|
||||
result = new CompositionPath(CreateWin2dPathGeometry(context, pathGeometry, fillType, Sn.Matrix3x2.Identity, optimizeLines));
|
||||
cache.CompositionPaths.Add((pathGeometry, fillType, optimizeLines), result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static CanvasGeometry CreateWin2dPathGeometryFromShape(
|
||||
ShapeContext context,
|
||||
Path path,
|
||||
ShapeFill.PathFillType fillType,
|
||||
bool optimizeLines)
|
||||
{
|
||||
var pathData = Optimizer.TrimAnimatable(context, path.Data);
|
||||
|
||||
if (pathData.IsAnimated)
|
||||
{
|
||||
context.Translation.Issues.CombiningAnimatedShapesIsNotSupported();
|
||||
}
|
||||
|
||||
var transform = Transforms.CreateMatrixFromTransform(context, context.Transform);
|
||||
|
||||
var result = CreateWin2dPathGeometry(
|
||||
context,
|
||||
pathData.InitialValue,
|
||||
fillType,
|
||||
transform,
|
||||
optimizeLines: optimizeLines);
|
||||
|
||||
result.SetDescription(context, () => path.Name);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CompositionPath from a group of paths.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="CompositionPath"/>.</returns>
|
||||
public static CompositionPath CompositionPathFromPathGeometryGroup(
|
||||
TranslationContext context,
|
||||
IEnumerable<PathGeometry> paths,
|
||||
ShapeFill.PathFillType fillType,
|
||||
bool optimizeLines)
|
||||
{
|
||||
var compositionPaths = paths.Select(p => CompositionPathFromPathGeometry(context, p, fillType, optimizeLines)).ToArray();
|
||||
|
||||
return compositionPaths.Length == 1
|
||||
? compositionPaths[0]
|
||||
: new CompositionPath(
|
||||
CanvasGeometry.CreateGroup(
|
||||
device: null,
|
||||
compositionPaths.Select(p => (CanvasGeometry)p.Source).ToArray(),
|
||||
ConvertTo.FilledRegionDetermination(fillType)));
|
||||
}
|
||||
|
||||
public static CanvasGeometry CreateWin2dPathGeometry(
|
||||
TranslationContext context,
|
||||
PathGeometry figure,
|
||||
ShapeFill.PathFillType fillType,
|
||||
Sn.Matrix3x2 transformMatrix,
|
||||
bool optimizeLines)
|
||||
{
|
||||
var beziers = figure.BezierSegments;
|
||||
using (var builder = new CanvasPathBuilder(null))
|
||||
{
|
||||
if (beziers.Count == 0)
|
||||
{
|
||||
builder.BeginFigure(ConvertTo.Vector2(0));
|
||||
builder.EndFigure(CanvasFigureLoop.Closed);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.SetFilledRegionDetermination(ConvertTo.FilledRegionDetermination(fillType));
|
||||
builder.BeginFigure(Sn.Vector2.Transform(ConvertTo.Vector2(beziers[0].ControlPoint0), transformMatrix));
|
||||
|
||||
foreach (var segment in beziers)
|
||||
{
|
||||
var cp0 = Sn.Vector2.Transform(ConvertTo.Vector2(segment.ControlPoint0), transformMatrix);
|
||||
var cp1 = Sn.Vector2.Transform(ConvertTo.Vector2(segment.ControlPoint1), transformMatrix);
|
||||
var cp2 = Sn.Vector2.Transform(ConvertTo.Vector2(segment.ControlPoint2), transformMatrix);
|
||||
var cp3 = Sn.Vector2.Transform(ConvertTo.Vector2(segment.ControlPoint3), transformMatrix);
|
||||
|
||||
// Add a line rather than a cubic Bezier if the segment is a straight line.
|
||||
if (optimizeLines && segment.IsALine)
|
||||
{
|
||||
// Ignore 0-length lines.
|
||||
if (!cp0.Equals(cp3))
|
||||
{
|
||||
builder.AddLine(cp3);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddCubicBezier(cp1, cp2, cp3);
|
||||
}
|
||||
}
|
||||
|
||||
// Closed tells D2D to synthesize a final segment. In many cases Closed
|
||||
// will have no effect because After Effects will have included the final
|
||||
// segment however it can make a difference because it determines whether
|
||||
// mitering or end caps will be used to join the end back to the start.
|
||||
builder.EndFigure(figure.IsClosed ? CanvasFigureLoop.Closed : CanvasFigureLoop.Open);
|
||||
}
|
||||
|
||||
return CanvasGeometry.CreatePath(builder);
|
||||
} // end using
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the given paths with MergeMode.Merge.
|
||||
/// </summary>
|
||||
/// <returns>The merged paths.</returns>
|
||||
public static CanvasGeometry MergePaths(CanvasGeometry.Path[] paths)
|
||||
{
|
||||
Debug.Assert(paths.Length > 1, "Precondition");
|
||||
var builder = new CanvasPathBuilder(null);
|
||||
var filledRegionDetermination = paths[0].FilledRegionDetermination;
|
||||
builder.SetFilledRegionDetermination(filledRegionDetermination);
|
||||
foreach (var path in paths)
|
||||
{
|
||||
Debug.Assert(filledRegionDetermination == path.FilledRegionDetermination, "Invariant");
|
||||
foreach (var command in path.Commands)
|
||||
{
|
||||
switch (command.Type)
|
||||
{
|
||||
case CanvasPathBuilder.CommandType.BeginFigure:
|
||||
builder.BeginFigure(((CanvasPathBuilder.Command.BeginFigure)command).StartPoint);
|
||||
break;
|
||||
case CanvasPathBuilder.CommandType.EndFigure:
|
||||
builder.EndFigure(((CanvasPathBuilder.Command.EndFigure)command).FigureLoop);
|
||||
break;
|
||||
case CanvasPathBuilder.CommandType.AddCubicBezier:
|
||||
var cb = (CanvasPathBuilder.Command.AddCubicBezier)command;
|
||||
builder.AddCubicBezier(cb.ControlPoint1, cb.ControlPoint2, cb.EndPoint);
|
||||
break;
|
||||
case CanvasPathBuilder.CommandType.AddLine:
|
||||
builder.AddLine(((CanvasPathBuilder.Command.AddLine)command).EndPoint);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CanvasGeometry.CreatePath(builder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iff the given paths are offsets translations of each other, gets the translation offset and returns true.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> iff the paths are offsets of each other.</returns>
|
||||
static bool TryGetPathTranslation(Sequence<BezierSegment> a, Sequence<BezierSegment> b, out Vector2 offset)
|
||||
{
|
||||
if (a.Count != b.Count)
|
||||
{
|
||||
// We could never animate this anyway.
|
||||
offset = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = b[0].ControlPoint0 - a[0].ControlPoint0;
|
||||
for (var i = 1; i < a.Count; i++)
|
||||
{
|
||||
var cp0Offset = b[i].ControlPoint0 - a[i].ControlPoint0;
|
||||
var cp1Offset = b[i].ControlPoint1 - a[i].ControlPoint1;
|
||||
var cp2Offset = b[i].ControlPoint2 - a[i].ControlPoint2;
|
||||
var cp3Offset = b[i].ControlPoint3 - a[i].ControlPoint3;
|
||||
|
||||
// Don't compare the values directly - there could be some rounding errors that
|
||||
// are acceptable. This value is just a guess about what is acceptable. We could
|
||||
// do something a lot more sophisticated (e.g. take into consideration the size
|
||||
// of the path) but this is probably good enough.
|
||||
const double acceptableError = 0.005;
|
||||
|
||||
if (!IsFuzzyEqual(cp0Offset, offset, acceptableError) ||
|
||||
!IsFuzzyEqual(cp1Offset, offset, acceptableError) ||
|
||||
!IsFuzzyEqual(cp2Offset, offset, acceptableError) ||
|
||||
!IsFuzzyEqual(cp3Offset, offset, acceptableError))
|
||||
{
|
||||
offset = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ApplyPathGroup(
|
||||
LayerContext context,
|
||||
CompositionPathGeometry targetGeometry,
|
||||
in TrimmedAnimatable<PathGeometryGroup> path,
|
||||
ShapeFill.PathFillType fillType)
|
||||
{
|
||||
// PathKeyFrameAnimation was introduced in 6 but was unreliable until 11.
|
||||
if (path.IsAnimated && context.ObjectFactory.IsUapApiAvailable(nameof(PathKeyFrameAnimation), versionDependentFeatureDescription: "Path animation"))
|
||||
{
|
||||
Animate.PathGroup(context, path, fillType, targetGeometry, nameof(targetGeometry.Path), nameof(targetGeometry.Path));
|
||||
}
|
||||
else
|
||||
{
|
||||
targetGeometry.Path = CompositionPathFromPathGeometryGroup(
|
||||
context,
|
||||
path.InitialValue.Data,
|
||||
fillType,
|
||||
optimizeLines: true);
|
||||
}
|
||||
}
|
||||
|
||||
static ShapeFill.PathFillType GetPathFillType(ShapeFill fill) => fill is null ? ShapeFill.PathFillType.EvenOdd : fill.FillType;
|
||||
|
||||
static bool IsFuzzyEqual(in Vector2 a, in Vector2 b, in double acceptableError)
|
||||
{
|
||||
var delta = a - b;
|
||||
return Math.Abs(delta.X) < acceptableError && Math.Abs(delta.Y) < acceptableError;
|
||||
}
|
||||
|
||||
sealed class StateCache
|
||||
{
|
||||
// Paths are shareable.
|
||||
public Dictionary<(PathGeometry, ShapeFill.PathFillType, bool), CompositionPath> CompositionPaths { get; }
|
||||
= new Dictionary<(PathGeometry, ShapeFill.PathFillType, bool), CompositionPath>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
sealed class PreCompLayerContext : LayerContext
|
||||
{
|
||||
internal PreCompLayerContext(CompositionContext compositionContext, PreCompLayer layer)
|
||||
: base(compositionContext, layer)
|
||||
{
|
||||
Layer = layer;
|
||||
|
||||
var referencedLayers = GetLayerCollectionByAssetId(this, layer.RefId);
|
||||
|
||||
// Precomps define a new temporal and spatial space for their child layers.
|
||||
ChildrenCompositionContext = new CompositionContext(
|
||||
compositionContext,
|
||||
referencedLayers,
|
||||
size: new Sn.Vector2((float)layer.Width, (float)layer.Height),
|
||||
startTime: compositionContext.StartTime - layer.StartTime,
|
||||
durationInFrames: compositionContext.DurationInFrames);
|
||||
}
|
||||
|
||||
public new PreCompLayer Layer { get; }
|
||||
|
||||
public CompositionContext ChildrenCompositionContext { get; }
|
||||
|
||||
static LayerCollection GetLayerCollectionByAssetId(PreCompLayerContext context, string assetId)
|
||||
=> ((LayerCollectionAsset)context.Translation.GetAssetById(context, assetId, Asset.AssetType.LayerCollection))?.Layers;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// 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.
|
||||
|
||||
using System.Linq;
|
||||
|
||||
#if DEBUG
|
||||
// For diagnosing issues, give nothing a clip.
|
||||
//#define NoClipping
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates Lottie PreComp layers.
|
||||
/// </summary>
|
||||
static class PreComps
|
||||
{
|
||||
public static LayerTranslator CreatePreCompLayerTranslator(PreCompLayerContext context)
|
||||
{
|
||||
// TODO - the animations produced inside a PreComp need to be time-mapped.
|
||||
|
||||
// Create the transform chain.
|
||||
if (!Transforms.TryCreateContainerVisualTransformChain(context, out var rootNode, out var contentsNode))
|
||||
{
|
||||
// The layer is never visible.
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = context.ObjectFactory.CreateContainerVisual();
|
||||
|
||||
#if !NoClipping
|
||||
// PreComps must clip to their size.
|
||||
// Create another ContainerVisual to apply clipping to.
|
||||
var clippingNode = context.ObjectFactory.CreateContainerVisual();
|
||||
contentsNode.Children.Add(clippingNode);
|
||||
contentsNode = clippingNode;
|
||||
contentsNode.Clip = context.ObjectFactory.CreateInsetClip();
|
||||
contentsNode.Size = context.ChildrenCompositionContext.Size;
|
||||
#endif
|
||||
|
||||
// Add the translations of each layer to the clipping node. This will recursively
|
||||
// add the tranlation of the layers in nested precomps.
|
||||
var contentsChildren = contentsNode.Children;
|
||||
foreach (var visual in Layers.TranslateLayersToVisuals(context.ChildrenCompositionContext))
|
||||
{
|
||||
contentsChildren.Add(visual);
|
||||
}
|
||||
|
||||
// Add mask if the layer has masks.
|
||||
// This must be done after all children are added to the content node.
|
||||
bool layerHasMasks = false;
|
||||
#if !NoClipping
|
||||
layerHasMasks = context.Layer.Masks.Any();
|
||||
#endif
|
||||
if (layerHasMasks)
|
||||
{
|
||||
var compositedVisual = Masks.TranslateAndApplyMasksForLayer(context, rootNode);
|
||||
|
||||
result.Children.Add(compositedVisual);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Children.Add(rootNode);
|
||||
}
|
||||
|
||||
return new LayerTranslator.FromVisual(result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,30 +7,28 @@ using System.Diagnostics;
|
|||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgcg;
|
||||
using static Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp.ExpressionFactory;
|
||||
using Expr = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions.Expression;
|
||||
using Expressions = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
#pragma warning disable SA1205 // Partial elements should declare access
|
||||
#pragma warning disable SA1601 // Partial elements should be documented
|
||||
// Translation for Lottie rectangles.
|
||||
sealed partial class LottieToWinCompTranslator
|
||||
/// <summary>
|
||||
/// Translation for Lottie rectangles.
|
||||
/// </summary>
|
||||
static class Rectangles
|
||||
{
|
||||
// Translates a Lottie rectangle to a CompositionShape.
|
||||
CompositionShape TranslateRectangleContent(TranslationContext context, ShapeContext shapeContext, Rectangle rectangle)
|
||||
public static CompositionShape TranslateRectangleContent(ShapeContext context, Rectangle rectangle)
|
||||
{
|
||||
var result = _c.CreateSpriteShape();
|
||||
var position = context.TrimAnimatable(rectangle.Position);
|
||||
var result = context.ObjectFactory.CreateSpriteShape();
|
||||
var position = Optimizer.TrimAnimatable(context, rectangle.Position);
|
||||
|
||||
if (IsNonRounded(shapeContext, rectangle))
|
||||
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,
|
||||
shapeContext,
|
||||
rectangle,
|
||||
position,
|
||||
result);
|
||||
|
@ -39,7 +37,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
{
|
||||
TranslateAndApplyRoundedRectangleContent(
|
||||
context,
|
||||
shapeContext,
|
||||
rectangle,
|
||||
position,
|
||||
result);
|
||||
|
@ -49,70 +46,68 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
|
||||
// Translates a non-rounded Lottie rectangle to a CompositionShape.
|
||||
void TranslateAndApplyNonRoundedRectangleContent(
|
||||
TranslationContext context,
|
||||
ShapeContext shapeContext,
|
||||
static void TranslateAndApplyNonRoundedRectangleContent(
|
||||
ShapeContext context,
|
||||
Rectangle rectangle,
|
||||
in TrimmedAnimatable<Vector3> position,
|
||||
CompositionSpriteShape compositionShape)
|
||||
{
|
||||
Debug.Assert(IsNonRounded(shapeContext, rectangle), "Precondition");
|
||||
Debug.Assert(IsNonRounded(context, rectangle), "Precondition");
|
||||
|
||||
var geometry = _c.CreateRectangleGeometry();
|
||||
var geometry = context.ObjectFactory.CreateRectangleGeometry();
|
||||
compositionShape.Geometry = geometry;
|
||||
|
||||
var size = AnimatableVector3Rewriter.EnsureOneEasingPerChannel(rectangle.Size);
|
||||
if (size is AnimatableXYZ sizeXYZ)
|
||||
{
|
||||
var width = context.TrimAnimatable(sizeXYZ.X);
|
||||
var height = context.TrimAnimatable(sizeXYZ.Y);
|
||||
var width = Optimizer.TrimAnimatable(context, sizeXYZ.X);
|
||||
var height = Optimizer.TrimAnimatable(context, sizeXYZ.Y);
|
||||
|
||||
if (!(width.IsAnimated || height.IsAnimated))
|
||||
{
|
||||
geometry.Size = Vector2(width.InitialValue, height.InitialValue);
|
||||
geometry.Size = ConvertTo.Vector2(width.InitialValue, height.InitialValue);
|
||||
}
|
||||
|
||||
geometry.Offset = InitialOffset(width, height, position: position);
|
||||
|
||||
ApplyRectangleContentCommonXY(context, shapeContext, rectangle, compositionShape, width, height, position, geometry);
|
||||
ApplyRectangleContentCommonXY(context, rectangle, compositionShape, width, height, position, geometry);
|
||||
}
|
||||
else
|
||||
{
|
||||
var size3 = context.TrimAnimatable<Vector3>((AnimatableVector3)size);
|
||||
var size3 = Optimizer.TrimAnimatable<Vector3>(context, (AnimatableVector3)size);
|
||||
|
||||
if (!size3.IsAnimated)
|
||||
{
|
||||
geometry.Size = Vector2(size3.InitialValue);
|
||||
geometry.Size = ConvertTo.Vector2(size3.InitialValue);
|
||||
}
|
||||
|
||||
geometry.Offset = InitialOffset(size: size3, position: position);
|
||||
|
||||
ApplyRectangleContentCommon(context, shapeContext, rectangle, compositionShape, size3, position, geometry);
|
||||
ApplyRectangleContentCommon(context, rectangle, compositionShape, size3, position, geometry);
|
||||
}
|
||||
}
|
||||
|
||||
// Translates a Lottie rectangle to a CompositionShape containing a RoundedRectangle.
|
||||
void TranslateAndApplyRoundedRectangleContent(
|
||||
TranslationContext context,
|
||||
ShapeContext shapeContext,
|
||||
static void TranslateAndApplyRoundedRectangleContent(
|
||||
ShapeContext context,
|
||||
Rectangle rectangle,
|
||||
in TrimmedAnimatable<Vector3> position,
|
||||
CompositionSpriteShape compositionShape)
|
||||
{
|
||||
// Use a rounded rectangle geometry.
|
||||
var geometry = _c.CreateRoundedRectangleGeometry();
|
||||
var geometry = context.ObjectFactory.CreateRoundedRectangleGeometry();
|
||||
compositionShape.Geometry = geometry;
|
||||
|
||||
// Get the corner radius. This will come from either Rectangle.Roundness
|
||||
// or RoundCorners.Radius.
|
||||
var cornerRadius = GetCornerRadius(context, shapeContext, rectangle, out var cornerRadiusIsRectangleRoundness);
|
||||
var cornerRadius = GetCornerRadius(context, rectangle, out var cornerRadiusIsRectangleRoundness);
|
||||
|
||||
// Get the size, converted to an AnimatableXYZ if necessary to handle different easings per channel.
|
||||
var size = AnimatableVector3Rewriter.EnsureOneEasingPerChannel(rectangle.Size);
|
||||
if (size is AnimatableXYZ sizeXYZ)
|
||||
{
|
||||
var width = context.TrimAnimatable(sizeXYZ.X);
|
||||
var height = context.TrimAnimatable(sizeXYZ.Y);
|
||||
var width = Optimizer.TrimAnimatable(context, sizeXYZ.X);
|
||||
var height = Optimizer.TrimAnimatable(context, sizeXYZ.Y);
|
||||
|
||||
ApplyCornerRadius(
|
||||
context,
|
||||
|
@ -125,11 +120,11 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
|
||||
geometry.Offset = InitialOffset(width, height, position: position);
|
||||
|
||||
ApplyRectangleContentCommonXY(context, shapeContext, rectangle, compositionShape, width, height, position, geometry);
|
||||
ApplyRectangleContentCommonXY(context, rectangle, compositionShape, width, height, position, geometry);
|
||||
}
|
||||
else
|
||||
{
|
||||
var size3 = context.TrimAnimatable<Vector3>((AnimatableVector3)size);
|
||||
var size3 = Optimizer.TrimAnimatable<Vector3>(context, (AnimatableVector3)size);
|
||||
|
||||
ApplyCornerRadius(
|
||||
context,
|
||||
|
@ -142,12 +137,12 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
|
||||
geometry.Offset = InitialOffset(size: size3, position: position);
|
||||
|
||||
ApplyRectangleContentCommon(context, shapeContext, rectangle, compositionShape, size3, position, geometry);
|
||||
ApplyRectangleContentCommon(context, rectangle, compositionShape, size3, position, geometry);
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyCornerRadius(
|
||||
TranslationContext context,
|
||||
static void ApplyCornerRadius(
|
||||
ShapeLayerContext context,
|
||||
CompositionRoundedRectangleGeometry geometry,
|
||||
in TrimmedAnimatable<double> cornerRadius,
|
||||
double initialWidth,
|
||||
|
@ -155,18 +150,18 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
bool isSizeAnimated,
|
||||
bool cornerRadiusIsRectangleRoundness)
|
||||
{
|
||||
var initialSize = Vector2(initialWidth, initialHeight);
|
||||
var initialSize = ConvertTo.Vector2(initialWidth, initialHeight);
|
||||
|
||||
// In After Effects Rectangle.Roundness and RoundCorners.Radius are clamped to a value
|
||||
// that depends on the size of the rectangle.
|
||||
// If size or corner radius are animated, handle this with an expression.
|
||||
if (cornerRadius.IsAnimated || isSizeAnimated)
|
||||
{
|
||||
WinCompData.Expressions.Vector2 cornerRadiusExpression;
|
||||
Expressions.Vector2 cornerRadiusExpression;
|
||||
|
||||
if (cornerRadius.IsAnimated)
|
||||
{
|
||||
InsertAndApplyScalarKeyFramePropertySetAnimation(
|
||||
Animate.ScalarPropertySetValue(
|
||||
context,
|
||||
cornerRadius,
|
||||
geometry,
|
||||
|
@ -176,28 +171,28 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
{
|
||||
// Both size and cornerRadius are animated.
|
||||
cornerRadiusExpression = cornerRadiusIsRectangleRoundness
|
||||
? RoundessToCornerRadius()
|
||||
: RadiusToCornerRadius();
|
||||
? ExpressionFactory.RoundessToCornerRadius()
|
||||
: ExpressionFactory.RadiusToCornerRadius();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only the cornerRadius is animated.
|
||||
cornerRadiusExpression = cornerRadiusIsRectangleRoundness
|
||||
? RoundnessToCornerRadius(initialSize)
|
||||
: RadiusToCornerRadius(initialSize);
|
||||
? ExpressionFactory.RoundnessToCornerRadius(initialSize)
|
||||
: ExpressionFactory.RadiusToCornerRadius(initialSize);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only the size is animated.
|
||||
cornerRadiusExpression = cornerRadiusIsRectangleRoundness
|
||||
? RoundnessToCornerRadius(cornerRadius.InitialValue)
|
||||
: RadiusToCornerRadius(cornerRadius.InitialValue);
|
||||
? ExpressionFactory.RoundnessToCornerRadius(cornerRadius.InitialValue)
|
||||
: ExpressionFactory.RadiusToCornerRadius(cornerRadius.InitialValue);
|
||||
}
|
||||
|
||||
var cornerRadiusAnimation = _c.CreateExpressionAnimation(cornerRadiusExpression);
|
||||
var cornerRadiusAnimation = context.ObjectFactory.CreateExpressionAnimation(cornerRadiusExpression);
|
||||
cornerRadiusAnimation.SetReferenceParameter("my", geometry);
|
||||
StartExpressionAnimation(geometry, "CornerRadius", cornerRadiusAnimation);
|
||||
Animate.WithExpression(geometry, cornerRadiusAnimation, "CornerRadius");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -206,12 +201,12 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
{
|
||||
// Rectangle.Roundness corner radius is constrained to half of the smaller side.
|
||||
var cornerRadiusValue = Math.Min(cornerRadius.InitialValue, Math.Min(initialWidth, initialHeight) / 2);
|
||||
geometry.CornerRadius = Vector2((float)cornerRadiusValue);
|
||||
geometry.CornerRadius = ConvertTo.Vector2((float)cornerRadiusValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// RoundCorners corner radii are constrained to half of the coresponding side.
|
||||
geometry.CornerRadius = Vector2(Math.Min(cornerRadius.InitialValue, initialWidth / 2), Math.Min(cornerRadius.InitialValue, initialHeight / 2));
|
||||
geometry.CornerRadius = ConvertTo.Vector2(Math.Min(cornerRadius.InitialValue, initialWidth / 2), Math.Min(cornerRadius.InitialValue, initialHeight / 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,9 +216,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
void ApplyRectangleContentCommon(
|
||||
TranslationContext context,
|
||||
ShapeContext shapeContext,
|
||||
static void ApplyRectangleContentCommon(
|
||||
ShapeContext context,
|
||||
Rectangle rectangle,
|
||||
CompositionSpriteShape compositionRectangle,
|
||||
in TrimmedAnimatable<Vector3> size,
|
||||
|
@ -232,33 +226,33 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
{
|
||||
if (position.IsAnimated || size.IsAnimated)
|
||||
{
|
||||
Expr offsetExpression;
|
||||
Expressions.Vector2 offsetExpression;
|
||||
if (position.IsAnimated)
|
||||
{
|
||||
ApplyVector2KeyFrameAnimation(context, position, geometry, nameof(Rectangle.Position));
|
||||
geometry.Properties.InsertVector2(nameof(Rectangle.Position), Vector2(position.InitialValue));
|
||||
Animate.Vector2(context, position, geometry, nameof(Rectangle.Position));
|
||||
geometry.Properties.InsertVector2(nameof(Rectangle.Position), ConvertTo.Vector2(position.InitialValue));
|
||||
if (size.IsAnimated)
|
||||
{
|
||||
// Size AND position are animated.
|
||||
offsetExpression = ExpressionFactory.PositionAndSizeToOffsetExpression;
|
||||
ApplyVector2KeyFrameAnimation(context, size, geometry, nameof(Rectangle.Size));
|
||||
Animate.Vector2(context, size, geometry, nameof(Rectangle.Size));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only Position is animated
|
||||
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(Vector2(size.InitialValue / 2));
|
||||
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(ConvertTo.Vector2(size.InitialValue / 2));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only Size is animated.
|
||||
offsetExpression = ExpressionFactory.PositionToOffsetExpression(Vector2(position.InitialValue));
|
||||
ApplyVector2KeyFrameAnimation(context, size, geometry, nameof(Rectangle.Size));
|
||||
offsetExpression = ExpressionFactory.PositionToOffsetExpression(ConvertTo.Vector2(position.InitialValue));
|
||||
Animate.Vector2(context, size, geometry, nameof(Rectangle.Size));
|
||||
}
|
||||
|
||||
var offsetExpressionAnimation = _c.CreateExpressionAnimation(offsetExpression);
|
||||
var offsetExpressionAnimation = context.ObjectFactory.CreateExpressionAnimation(offsetExpression);
|
||||
offsetExpressionAnimation.SetReferenceParameter("my", geometry);
|
||||
StartExpressionAnimation(geometry, "Offset", offsetExpressionAnimation);
|
||||
Animate.WithExpression(geometry, offsetExpressionAnimation, "Offset");
|
||||
}
|
||||
|
||||
// Lottie rectangles have 0,0 at top right. That causes problems for TrimPath which expects 0,0 to be top left.
|
||||
|
@ -266,37 +260,32 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
|
||||
// TODO - this only works correctly if Size and TrimOffset are not animated. A complete solution requires
|
||||
// adding another property.
|
||||
var isPartialTrimPath = shapeContext.TrimPath != null &&
|
||||
(shapeContext.TrimPath.Start.IsAnimated || shapeContext.TrimPath.End.IsAnimated || shapeContext.TrimPath.Offset.IsAnimated ||
|
||||
shapeContext.TrimPath.Start.InitialValue.Value != 0 || shapeContext.TrimPath.End.InitialValue.Value != 1);
|
||||
var isPartialTrimPath = context.TrimPath != null &&
|
||||
(context.TrimPath.Start.IsAnimated || context.TrimPath.End.IsAnimated || context.TrimPath.Offset.IsAnimated ||
|
||||
context.TrimPath.Start.InitialValue.Value != 0 || context.TrimPath.End.InitialValue.Value != 1);
|
||||
|
||||
if (size.IsAnimated && isPartialTrimPath)
|
||||
{
|
||||
// Warn that we might be getting things wrong
|
||||
_issues.AnimatedRectangleWithTrimPathIsNotSupported();
|
||||
context.Issues.AnimatedRectangleWithTrimPathIsNotSupported();
|
||||
}
|
||||
|
||||
var width = size.InitialValue.X;
|
||||
var height = size.InitialValue.Y;
|
||||
var trimOffsetDegrees = (width / (2 * (width + height))) * 360;
|
||||
|
||||
TranslateAndApplyShapeContext(
|
||||
Shapes.TranslateAndApplyShapeContext(
|
||||
context,
|
||||
shapeContext,
|
||||
compositionRectangle,
|
||||
rectangle.DrawingDirection == DrawingDirection.Reverse,
|
||||
trimOffsetDegrees: trimOffsetDegrees);
|
||||
|
||||
if (_addDescriptions)
|
||||
{
|
||||
Describe(compositionRectangle, rectangle.Name);
|
||||
Describe(compositionRectangle.Geometry, $"{rectangle.Name}.RectangleGeometry");
|
||||
}
|
||||
compositionRectangle.SetDescription(context, () => rectangle.Name);
|
||||
compositionRectangle.Geometry.SetDescription(context, () => $"{rectangle.Name}.RectangleGeometry");
|
||||
}
|
||||
|
||||
void ApplyRectangleContentCommonXY(
|
||||
TranslationContext context,
|
||||
ShapeContext shapeContext,
|
||||
static void ApplyRectangleContentCommonXY(
|
||||
ShapeContext context,
|
||||
Rectangle rectangle,
|
||||
CompositionSpriteShape compositionRectangle,
|
||||
in TrimmedAnimatable<double> width,
|
||||
|
@ -306,49 +295,49 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
{
|
||||
if (position.IsAnimated || width.IsAnimated || height.IsAnimated)
|
||||
{
|
||||
Expr offsetExpression;
|
||||
Expressions.Vector2 offsetExpression;
|
||||
if (position.IsAnimated)
|
||||
{
|
||||
ApplyVector2KeyFrameAnimation(context, position, geometry, nameof(Rectangle.Position));
|
||||
geometry.Properties.InsertVector2(nameof(Rectangle.Position), Vector2(position.InitialValue));
|
||||
Animate.Vector2(context, position, geometry, nameof(Rectangle.Position));
|
||||
geometry.Properties.InsertVector2(nameof(Rectangle.Position), ConvertTo.Vector2(position.InitialValue));
|
||||
if (width.IsAnimated || height.IsAnimated)
|
||||
{
|
||||
// Size AND position are animated.
|
||||
offsetExpression = ExpressionFactory.PositionAndSizeToOffsetExpression;
|
||||
if (width.IsAnimated)
|
||||
{
|
||||
ApplyScalarKeyFrameAnimation(context, width, geometry, $"{nameof(Rectangle.Size)}.X");
|
||||
Animate.Scalar(context, width, geometry, $"{nameof(Rectangle.Size)}.X");
|
||||
}
|
||||
|
||||
if (height.IsAnimated)
|
||||
{
|
||||
ApplyScalarKeyFrameAnimation(context, height, geometry, $"{nameof(Rectangle.Size)}.Y");
|
||||
Animate.Scalar(context, height, geometry, $"{nameof(Rectangle.Size)}.Y");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only Position is animated.
|
||||
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(Vector2(new Vector2(width.InitialValue, height.InitialValue) / 2));
|
||||
offsetExpression = ExpressionFactory.HalfSizeToOffsetExpression(ConvertTo.Vector2(new Vector2(width.InitialValue, height.InitialValue) / 2));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only Size is animated.
|
||||
offsetExpression = ExpressionFactory.PositionToOffsetExpression(Vector2(position.InitialValue));
|
||||
offsetExpression = ExpressionFactory.PositionToOffsetExpression(ConvertTo.Vector2(position.InitialValue));
|
||||
if (width.IsAnimated)
|
||||
{
|
||||
ApplyScalarKeyFrameAnimation(context, width, geometry, $"{nameof(Rectangle.Size)}.X");
|
||||
Animate.Scalar(context, width, geometry, $"{nameof(Rectangle.Size)}.X");
|
||||
}
|
||||
|
||||
if (height.IsAnimated)
|
||||
{
|
||||
ApplyScalarKeyFrameAnimation(context, height, geometry, $"{nameof(Rectangle.Size)}.Y");
|
||||
Animate.Scalar(context, height, geometry, $"{nameof(Rectangle.Size)}.Y");
|
||||
}
|
||||
}
|
||||
|
||||
var offsetExpressionAnimation = _c.CreateExpressionAnimation(offsetExpression);
|
||||
var offsetExpressionAnimation = context.ObjectFactory.CreateExpressionAnimation(offsetExpression);
|
||||
offsetExpressionAnimation.SetReferenceParameter("my", geometry);
|
||||
StartExpressionAnimation(geometry, "Offset", offsetExpressionAnimation);
|
||||
Animate.WithExpression(geometry, offsetExpressionAnimation, "Offset");
|
||||
}
|
||||
|
||||
// Lottie rectangles have 0,0 at top right. That causes problems for TrimPath which expects 0,0 to be top left.
|
||||
|
@ -356,47 +345,42 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
|
||||
// TODO - this only works correctly if Size and TrimOffset are not animated. A complete solution requires
|
||||
// adding another property.
|
||||
var isPartialTrimPath = shapeContext.TrimPath != null &&
|
||||
(shapeContext.TrimPath.Start.IsAnimated || shapeContext.TrimPath.End.IsAnimated || shapeContext.TrimPath.Offset.IsAnimated ||
|
||||
shapeContext.TrimPath.Start.InitialValue.Value != 0 || shapeContext.TrimPath.End.InitialValue.Value != 1);
|
||||
var isPartialTrimPath = context.TrimPath != null &&
|
||||
(context.TrimPath.Start.IsAnimated || context.TrimPath.End.IsAnimated || context.TrimPath.Offset.IsAnimated ||
|
||||
context.TrimPath.Start.InitialValue.Value != 0 || context.TrimPath.End.InitialValue.Value != 1);
|
||||
|
||||
if ((width.IsAnimated || height.IsAnimated) && isPartialTrimPath)
|
||||
{
|
||||
// Warn that we might be getting things wrong.
|
||||
_issues.AnimatedRectangleWithTrimPathIsNotSupported();
|
||||
context.Issues.AnimatedRectangleWithTrimPathIsNotSupported();
|
||||
}
|
||||
|
||||
var initialWidth = width.InitialValue;
|
||||
var initialHeight = height.InitialValue;
|
||||
var trimOffsetDegrees = (initialWidth / (2 * (initialWidth + initialHeight))) * 360;
|
||||
|
||||
TranslateAndApplyShapeContext(
|
||||
Shapes.TranslateAndApplyShapeContext(
|
||||
context,
|
||||
shapeContext,
|
||||
compositionRectangle,
|
||||
rectangle.DrawingDirection == DrawingDirection.Reverse,
|
||||
trimOffsetDegrees: trimOffsetDegrees);
|
||||
|
||||
if (_addDescriptions)
|
||||
{
|
||||
Describe(compositionRectangle, rectangle.Name);
|
||||
Describe(compositionRectangle.Geometry, $"{rectangle.Name}.RectangleGeometry");
|
||||
}
|
||||
compositionRectangle.SetDescription(context, () => rectangle.Name);
|
||||
compositionRectangle.Geometry.SetDescription(context, () => $"{rectangle.Name}.RectangleGeometry");
|
||||
}
|
||||
|
||||
CanvasGeometry CreateWin2dRectangleGeometry(
|
||||
TranslationContext context,
|
||||
ShapeContext shapeContext,
|
||||
public static CanvasGeometry CreateWin2dRectangleGeometry(
|
||||
ShapeContext context,
|
||||
Rectangle rectangle)
|
||||
{
|
||||
var position = context.TrimAnimatable(rectangle.Position);
|
||||
var size = context.TrimAnimatable(rectangle.Size);
|
||||
var position = Optimizer.TrimAnimatable(context, rectangle.Position);
|
||||
var size = Optimizer.TrimAnimatable(context, rectangle.Size);
|
||||
|
||||
var cornerRadius = GetCornerRadius(context, shapeContext, rectangle, out var cornerRadiusIsRectangleRoundness);
|
||||
var cornerRadius = GetCornerRadius(context, rectangle, out var cornerRadiusIsRectangleRoundness);
|
||||
|
||||
if (position.IsAnimated || size.IsAnimated || cornerRadius.IsAnimated)
|
||||
{
|
||||
_issues.CombiningAnimatedShapesIsNotSupported();
|
||||
context.Issues.CombiningAnimatedShapesIsNotSupported();
|
||||
}
|
||||
|
||||
var width = size.InitialValue.X;
|
||||
|
@ -430,25 +414,21 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
(float)radiusX,
|
||||
(float)radiusY);
|
||||
|
||||
var transformMatrix = CreateMatrixFromTransform(context, shapeContext.Transform);
|
||||
var transformMatrix = Transforms.CreateMatrixFromTransform(context, context.Transform);
|
||||
if (!transformMatrix.IsIdentity)
|
||||
{
|
||||
result = result.Transform(transformMatrix);
|
||||
}
|
||||
|
||||
if (_addDescriptions)
|
||||
{
|
||||
Describe(result, rectangle.Name);
|
||||
}
|
||||
result.SetDescription(context, () => rectangle.Name);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Gets the corner radius and indicates whether the value came from Rectangle.Roundness (as
|
||||
// opposed to RoundCorners.Radius).
|
||||
TrimmedAnimatable<double> GetCornerRadius(
|
||||
TranslationContext context,
|
||||
ShapeContext shapeContext,
|
||||
static TrimmedAnimatable<double> GetCornerRadius(
|
||||
ShapeContext context,
|
||||
Rectangle rectangle,
|
||||
out bool cornerRadiusIsRectangleRoundness)
|
||||
{
|
||||
|
@ -467,13 +447,13 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
// RoundCorners.Radius values.
|
||||
if (cornerRadiusIsRectangleRoundness &&
|
||||
rectangle.Roundness.IsEver(0) &&
|
||||
shapeContext.RoundCorners.Radius.IsEverNot(0))
|
||||
context.RoundCorners.Radius.IsEverNot(0))
|
||||
{
|
||||
// Report the issue about RoundCorners being ignored.
|
||||
_issues.ConflictingRoundnessAndRadiusIsNotSupported();
|
||||
context.Issues.ConflictingRoundnessAndRadiusIsNotSupported();
|
||||
}
|
||||
|
||||
return context.TrimAnimatable(cornerRadiusIsRectangleRoundness ? rectangle.Roundness : shapeContext.RoundCorners.Radius);
|
||||
return Optimizer.TrimAnimatable(context, cornerRadiusIsRectangleRoundness ? rectangle.Roundness : context.RoundCorners.Radius);
|
||||
}
|
||||
|
||||
// Convert the size and position for a geometry into an offset.
|
||||
|
@ -482,13 +462,13 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
static Sn.Vector2 InitialOffset(
|
||||
in TrimmedAnimatable<Vector3> size,
|
||||
in TrimmedAnimatable<Vector3> position)
|
||||
=> Vector2(position.InitialValue - (size.InitialValue / 2));
|
||||
=> ConvertTo.Vector2(position.InitialValue - (size.InitialValue / 2));
|
||||
|
||||
static Sn.Vector2 InitialOffset(
|
||||
in TrimmedAnimatable<double> width,
|
||||
in TrimmedAnimatable<double> height,
|
||||
in TrimmedAnimatable<Vector3> position)
|
||||
=> Vector2(position.InitialValue - (new Vector3(width.InitialValue, height.InitialValue, 0) / 2));
|
||||
=> ConvertTo.Vector2(position.InitialValue - (new Vector3(width.InitialValue, height.InitialValue, 0) / 2));
|
||||
|
||||
// Returns true if the given rectangle ever has rounded corners.
|
||||
static bool IsNonRounded(ShapeContext shapeContext, Rectangle rectangle) =>
|
|
@ -19,9 +19,19 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
static readonly RoundCorners s_defaultRoundCorners =
|
||||
new RoundCorners(new ShapeLayerContent.ShapeLayerContentArgs { }, new Animatable<double>(0, null));
|
||||
|
||||
readonly TranslationIssues _issues;
|
||||
internal ShapeContext(ShapeLayerContext layer)
|
||||
{
|
||||
LayerContext = layer;
|
||||
ObjectFactory = layer.ObjectFactory;
|
||||
}
|
||||
|
||||
internal ShapeContext(TranslationIssues issues) => _issues = issues;
|
||||
public ShapeLayerContext LayerContext { get; }
|
||||
|
||||
public CompositionObjectFactory ObjectFactory { get; }
|
||||
|
||||
public TranslationContext Translation => LayerContext.CompositionContext.Translation;
|
||||
|
||||
public TranslationIssues Issues => Translation.Issues;
|
||||
|
||||
internal ShapeStroke Stroke { get; private set; }
|
||||
|
||||
|
@ -76,14 +86,14 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
internal void UpdateOpacityFromTransform(TranslationContext context, Transform transform)
|
||||
internal void UpdateOpacityFromTransform(LayerContext context, Transform transform)
|
||||
{
|
||||
if (transform is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Opacity = Opacity.ComposedWith(context.TrimAnimatable(transform.Opacity));
|
||||
Opacity = Opacity.ComposedWith(Optimizer.TrimAnimatable(context, transform.Opacity));
|
||||
}
|
||||
|
||||
// Only used when translating geometries. Layers use an extra Shape or Visual to
|
||||
|
@ -95,7 +105,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
|
||||
internal ShapeContext Clone() =>
|
||||
new ShapeContext(_issues)
|
||||
new ShapeContext(LayerContext)
|
||||
{
|
||||
Fill = Fill,
|
||||
Stroke = Stroke,
|
||||
|
@ -118,7 +128,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
|
||||
if (a.FillKind != b.FillKind)
|
||||
{
|
||||
_issues.MultipleFillsIsNotSupported();
|
||||
Translation.Issues.MultipleFillsIsNotSupported();
|
||||
return b;
|
||||
}
|
||||
|
||||
|
@ -128,7 +138,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
return ComposeSolidColorFills((SolidColorFill)a, (SolidColorFill)b);
|
||||
}
|
||||
|
||||
_issues.MultipleFillsIsNotSupported();
|
||||
Translation.Issues.MultipleFillsIsNotSupported();
|
||||
return b;
|
||||
}
|
||||
|
||||
|
@ -149,7 +159,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
_issues.MultipleFillsIsNotSupported();
|
||||
Translation.Issues.MultipleFillsIsNotSupported();
|
||||
return b;
|
||||
}
|
||||
|
||||
|
@ -166,7 +176,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
|
||||
if (a.StrokeKind != b.StrokeKind)
|
||||
{
|
||||
_issues.MultipleStrokesIsNotSupported();
|
||||
Translation.Issues.MultipleStrokesIsNotSupported();
|
||||
return b;
|
||||
}
|
||||
|
||||
|
@ -197,7 +207,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
_issues.MultipleStrokesIsNotSupported();
|
||||
Translation.Issues.MultipleStrokesIsNotSupported();
|
||||
return a;
|
||||
}
|
||||
|
||||
|
@ -215,7 +225,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
_issues.MultipleStrokesIsNotSupported();
|
||||
Translation.Issues.MultipleStrokesIsNotSupported();
|
||||
return a;
|
||||
}
|
||||
|
||||
|
@ -235,7 +245,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
|
||||
// The new stroke should be in addition to the existing stroke. And colors should blend.
|
||||
_issues.MultipleStrokesIsNotSupported();
|
||||
Translation.Issues.MultipleStrokesIsNotSupported();
|
||||
return b;
|
||||
}
|
||||
|
||||
|
@ -264,7 +274,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
_issues.MultipleAnimatedRoundCornersIsNotSupported();
|
||||
Translation.Issues.MultipleAnimatedRoundCornersIsNotSupported();
|
||||
return b;
|
||||
}
|
||||
|
||||
|
@ -308,8 +318,18 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
}
|
||||
}
|
||||
|
||||
_issues.MultipleTrimPathsIsNotSupported();
|
||||
Translation.Issues.MultipleTrimPathsIsNotSupported();
|
||||
return b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow a <see cref="ShapeContext"/> to be used wherever a <see cref="ShapeLayerContext"/> is required.
|
||||
/// </summary>
|
||||
public static implicit operator ShapeLayerContext(ShapeContext obj) => obj.LayerContext;
|
||||
|
||||
/// <summary>
|
||||
/// Allow a <see cref="ShapeContext"/> to be used wherever a <see cref="TranslationContext"/> is required.
|
||||
/// </summary>
|
||||
public static implicit operator TranslationContext(ShapeContext obj) => obj.LayerContext;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
sealed class ShapeLayerContext : LayerContext
|
||||
{
|
||||
internal ShapeLayerContext(CompositionContext compositionContext, ShapeLayer layer)
|
||||
: base(compositionContext, layer)
|
||||
{
|
||||
Layer = layer;
|
||||
}
|
||||
|
||||
public new ShapeLayer Layer { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,675 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Mgcg;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translation for Lottie shapes.
|
||||
/// </summary>
|
||||
static class Shapes
|
||||
{
|
||||
public static LayerTranslator CreateShapeLayerTranslator(ShapeLayerContext context) =>
|
||||
new ShapeLayerTranslator(context);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static CompositionShape TranslateGroupShapeContent(ShapeContext context, ShapeGroup group)
|
||||
{
|
||||
var result = TranslateShapeLayerContents(context, group.Contents);
|
||||
result.SetDescription(context, () => $"ShapeGroup: {group.Name}");
|
||||
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 != 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;
|
||||
}
|
||||
|
||||
// Merge the stack into a single shape. Merging is done recursively - the top geometry on the
|
||||
// stack is merged with the merge of the remainder of the stack.
|
||||
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,
|
||||
trimOffsetDegrees: 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all of the given geometries into a single geometry.
|
||||
static CanvasGeometry CombineGeometries(
|
||||
TranslationContext context,
|
||||
CanvasGeometry[] geometries,
|
||||
MergePaths.MergeMode mergeMode)
|
||||
{
|
||||
switch (geometries.Length)
|
||||
{
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return geometries[0];
|
||||
}
|
||||
|
||||
// If MergeMode.Merge and they're all paths with the same FilledRegionDetermination,
|
||||
// combine into a single path.
|
||||
if (mergeMode == MergePaths.MergeMode.Merge &&
|
||||
geometries.All(g => g.Type == CanvasGeometry.GeometryType.Path) &&
|
||||
geometries.Select(g => ((CanvasGeometry.Path)g).FilledRegionDetermination).Distinct().Count() == 1)
|
||||
{
|
||||
return Paths.MergePaths(geometries.Cast<CanvasGeometry.Path>().ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (geometries.Length > 50)
|
||||
{
|
||||
// There will be stack overflows if the CanvasGeometry.Combine is too large.
|
||||
// Usually not a problem, but handle degenerate cases.
|
||||
context.Issues.MergingALargeNumberOfShapesIsNotSupported();
|
||||
geometries = geometries.Take(50).ToArray();
|
||||
}
|
||||
|
||||
var combineMode = ConvertTo.GeometryCombine(mergeMode);
|
||||
|
||||
#if PreCombineGeometries
|
||||
return CanvasGeometryCombiner.CombineGeometries(geometries, combineMode);
|
||||
#else
|
||||
var accumulator = geometries[0];
|
||||
for (var i = 1; i < geometries.Length; i++)
|
||||
{
|
||||
accumulator = accumulator.CombineWith(geometries[i], Sn.Matrix3x2.Identity, combineMode);
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static AnimatableOrder GetAnimatableOrder(in TrimmedAnimatable<Trim> a, in TrimmedAnimatable<Trim> b)
|
||||
{
|
||||
var initialA = a.InitialValue.Value;
|
||||
var initialB = b.InitialValue.Value;
|
||||
|
||||
var initialOrder = GetValueOrder(initialA, initialB);
|
||||
if (!a.IsAnimated && !b.IsAnimated)
|
||||
{
|
||||
return initialOrder;
|
||||
}
|
||||
|
||||
// TODO - recognize more cases. For now just handle a is always before b
|
||||
var aMin = initialA;
|
||||
var aMax = initialA;
|
||||
if (a.IsAnimated)
|
||||
{
|
||||
aMin = Math.Min(a.KeyFrames.Min(kf => kf.Value.Value), initialA);
|
||||
aMax = Math.Max(a.KeyFrames.Max(kf => kf.Value.Value), initialA);
|
||||
}
|
||||
|
||||
var bMin = initialB;
|
||||
var bMax = initialB;
|
||||
if (b.IsAnimated)
|
||||
{
|
||||
bMin = Math.Min(b.KeyFrames.Min(kf => kf.Value.Value), initialB);
|
||||
bMax = Math.Max(b.KeyFrames.Max(kf => kf.Value.Value), initialB);
|
||||
}
|
||||
|
||||
switch (initialOrder)
|
||||
{
|
||||
case AnimatableOrder.Before:
|
||||
return aMax <= bMin ? initialOrder : AnimatableOrder.BeforeAndAfter;
|
||||
case AnimatableOrder.After:
|
||||
return aMin >= bMax ? initialOrder : AnimatableOrder.BeforeAndAfter;
|
||||
case AnimatableOrder.Equal:
|
||||
{
|
||||
if (aMin == aMax && bMin == bMax && aMin == bMax)
|
||||
{
|
||||
return AnimatableOrder.Equal;
|
||||
}
|
||||
else if (aMin < bMax)
|
||||
{
|
||||
// Might be before, unless they cross over.
|
||||
return bMin < initialA || aMax > initialA ? AnimatableOrder.BeforeAndAfter : AnimatableOrder.Before;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Might be after, unless they cross over.
|
||||
return bMin > aMax ? AnimatableOrder.BeforeAndAfter : AnimatableOrder.After;
|
||||
}
|
||||
}
|
||||
|
||||
case AnimatableOrder.BeforeAndAfter:
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
enum AnimatableOrder
|
||||
{
|
||||
Before,
|
||||
After,
|
||||
Equal,
|
||||
BeforeAndAfter,
|
||||
}
|
||||
|
||||
static AnimatableOrder GetValueOrder(double a, double b)
|
||||
{
|
||||
if (a == b)
|
||||
{
|
||||
return AnimatableOrder.Equal;
|
||||
}
|
||||
else if (a < b)
|
||||
{
|
||||
return AnimatableOrder.Before;
|
||||
}
|
||||
else
|
||||
{
|
||||
return AnimatableOrder.After;
|
||||
}
|
||||
}
|
||||
|
||||
// Discover patterns that we don't yet support and report any issues.
|
||||
static void CheckForUnsupportedShapeGroup(TranslationContext context, IReadOnlyList<ShapeLayerContent> contents)
|
||||
{
|
||||
// Count the number of geometries. More than 1 geometry is currently not properly supported
|
||||
// unless they're all paths.
|
||||
var pathCount = 0;
|
||||
var geometryCount = 0;
|
||||
|
||||
for (var i = 0; i < contents.Count; i++)
|
||||
{
|
||||
switch (contents[i].ContentType)
|
||||
{
|
||||
case ShapeContentType.Ellipse:
|
||||
case ShapeContentType.Polystar:
|
||||
case ShapeContentType.Rectangle:
|
||||
geometryCount++;
|
||||
break;
|
||||
case ShapeContentType.Path:
|
||||
pathCount++;
|
||||
geometryCount++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (geometryCount > 1 && pathCount != geometryCount)
|
||||
{
|
||||
context.Issues.CombiningMultipleShapesIsNotSupported();
|
||||
}
|
||||
}
|
||||
|
||||
static void CheckForRoundCornersOnPath(ShapeContext context)
|
||||
{
|
||||
if (!Optimizer.TrimAnimatable(context, context.RoundCorners.Radius).IsAlways(0))
|
||||
{
|
||||
// TODO - can round corners be implemented by composing cubic Beziers?
|
||||
context.Issues.PathWithRoundCornersIsNotSupported();
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ShapeLayerTranslator : LayerTranslator
|
||||
{
|
||||
readonly ShapeLayerContext _context;
|
||||
|
||||
internal ShapeLayerTranslator(ShapeLayerContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
internal override bool IsShape => !_context.Layer.Masks.Any();
|
||||
|
||||
internal override CompositionShape GetShapeRoot(TranslationContext context)
|
||||
{
|
||||
bool layerHasMasks = false;
|
||||
#if !NoClipping
|
||||
layerHasMasks = _context.Layer.Masks.Any();
|
||||
#endif
|
||||
if (layerHasMasks)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (!Transforms.TryCreateContainerShapeTransformChain(_context, out var rootNode, out var contentsNode))
|
||||
{
|
||||
// The layer is never visible.
|
||||
return null;
|
||||
}
|
||||
|
||||
var shapeContext = new ShapeContext(_context);
|
||||
|
||||
// Update the opacity from the transform. This is necessary to push the opacity
|
||||
// to the leaves (because CompositionShape does not support opacity).
|
||||
shapeContext.UpdateOpacityFromTransform(_context, _context.Layer.Transform);
|
||||
contentsNode.Shapes.Add(TranslateShapeLayerContents(shapeContext, _context.Layer.Contents));
|
||||
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
internal override Visual GetVisualRoot(CompositionContext context)
|
||||
{
|
||||
bool layerHasMasks = false;
|
||||
#if !NoClipping
|
||||
layerHasMasks = _context.Layer.Masks.Any();
|
||||
#endif
|
||||
|
||||
if (!Transforms.TryCreateShapeVisualTransformChain(_context, out var rootNode, out var contentsNode))
|
||||
{
|
||||
// The layer is never visible.
|
||||
return null;
|
||||
}
|
||||
|
||||
var shapeContext = new ShapeContext(_context);
|
||||
|
||||
contentsNode.Shapes.Add(TranslateShapeLayerContents(shapeContext, _context.Layer.Contents));
|
||||
|
||||
return layerHasMasks
|
||||
? Masks.TranslateAndApplyMasksForLayer(_context, rootNode)
|
||||
: rootNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
sealed class SolidLayerContext : LayerContext
|
||||
{
|
||||
internal SolidLayerContext(CompositionContext compositionContext, SolidLayer layer)
|
||||
: base(compositionContext, layer)
|
||||
{
|
||||
Layer = layer;
|
||||
}
|
||||
|
||||
public new SolidLayer Layer { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
// 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.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
#if DEBUG
|
||||
// For diagnosing issues, give nothing a clip.
|
||||
//#define NoClipping
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
static class SolidLayers
|
||||
{
|
||||
public static LayerTranslator CreateSolidLayerTranslator(SolidLayerContext context)
|
||||
=> new SolidLayerTranslator(context);
|
||||
|
||||
sealed class SolidLayerTranslator : LayerTranslator
|
||||
{
|
||||
readonly SolidLayerContext _context;
|
||||
|
||||
internal SolidLayerTranslator(SolidLayerContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
internal override bool IsShape =>
|
||||
!_context.Layer.Masks.Any() || _context.Layer.IsHidden || _context.Layer.Transform.Opacity.IsAlways(LottieData.Opacity.Transparent);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
internal override Visual GetVisualRoot(CompositionContext context)
|
||||
{
|
||||
// Translate the SolidLayer to a Visual.
|
||||
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.TryCreateContainerVisualTransformChain(_context, out var containerRootNode, out var containerContentNode))
|
||||
{
|
||||
// The layer is never visible.
|
||||
return null;
|
||||
}
|
||||
|
||||
var rectangle = context.ObjectFactory.CreateSpriteVisual();
|
||||
rectangle.Size = ConvertTo.Vector2(_context.Layer.Width, _context.Layer.Height);
|
||||
|
||||
containerContentNode.Children.Add(rectangle);
|
||||
|
||||
var layerHasMasks = false;
|
||||
#if !NoClipping
|
||||
layerHasMasks = _context.Layer.Masks.Any();
|
||||
#endif
|
||||
rectangle.Brush = Brushes.CreateNonAnimatedColorBrush(_context, _context.Layer.Color);
|
||||
|
||||
rectangle.SetDescription(context, () => "SolidLayerRectangle");
|
||||
|
||||
var result = layerHasMasks
|
||||
? Masks.TranslateAndApplyMasksForLayer(_context, containerRootNode)
|
||||
: containerRootNode;
|
||||
|
||||
Describe(context, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
sealed class TextLayerContext : LayerContext
|
||||
{
|
||||
internal TextLayerContext(CompositionContext compositionContext, TextLayer layer)
|
||||
: base(compositionContext, layer)
|
||||
{
|
||||
Layer = layer;
|
||||
}
|
||||
|
||||
public new TextLayer Layer { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// 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.LottieToWinComp
|
||||
{
|
||||
static class TextLayers
|
||||
{
|
||||
public static LayerTranslator CreateTextLayerTranslator(TextLayerContext context)
|
||||
{
|
||||
// Text layers are not yet suported.
|
||||
context.Issues.TextLayerIsNotSupported();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.MetaData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates themable properties into propertyset values.
|
||||
/// </summary>
|
||||
static class ThemePropertyBindings
|
||||
{
|
||||
/// <summary>
|
||||
/// The name used in <see cref="ExpressionAnimation"/> expressions to bind to
|
||||
/// the <see cref="CompositionPropertySet"/> that contains the theme properties.
|
||||
/// </summary>
|
||||
public const string ThemePropertiesName = "_theme";
|
||||
|
||||
/// <summary>
|
||||
/// Parses the given bindingSpec, and returns the name of the property in the theme
|
||||
/// <see cref="CompositionPropertySet"/> that should be used for binding to, or null if the property bindings
|
||||
/// are currently disabled, or the bindingSpec doesn't mention the given property name.
|
||||
/// </summary>
|
||||
/// <returns>The name of the corresponding property in the theme <see cref="CompositionPropertySet"/>
|
||||
/// or null.</returns>
|
||||
public static string GetThemeBindingNameForLottieProperty(TranslationContext context, string bindingSpec, string propertyName)
|
||||
=> context.TranslatePropertyBindings
|
||||
? PropertyBindings.FindFirstBindingNameForProperty(bindingSpec, propertyName)
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the theme <see cref="CompositionPropertySet"/> for the given translation.
|
||||
/// </summary>
|
||||
/// <returns>The theme <see cref="CompositionPropertySet"/>.</returns>
|
||||
public static CompositionPropertySet GetThemePropertySet(TranslationContext context)
|
||||
{
|
||||
var cache = context.GetStateCache<StateCache>();
|
||||
return cache.GetThemePropertySet(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures there is a property in the theme property set with the given name and default value.
|
||||
/// </summary>
|
||||
public static void EnsureColorThemePropertyExists(LayerContext context, string bindingName, Color defaultValue)
|
||||
{
|
||||
var defaultValueAsWinUIColor = ConvertTo.Color(defaultValue);
|
||||
var defaultValueAsVector4 = ConvertTo.Vector4(defaultValueAsWinUIColor);
|
||||
var themePropertySet = GetThemePropertySet(context);
|
||||
|
||||
// Insert a property set value for the scalar if one hasn't yet been added.
|
||||
switch (themePropertySet.TryGetVector4(bindingName, out var existingColorAsVector4))
|
||||
{
|
||||
case CompositionGetValueStatus.NotFound:
|
||||
// The property hasn't been added yet. Add it.
|
||||
themePropertySet.InsertVector4(bindingName, ConvertTo.Vector4(defaultValueAsWinUIColor));
|
||||
context.Translation.PropertyBindings.AddPropertyBinding(
|
||||
bindingName,
|
||||
actualType: PropertySetValueType.Vector4,
|
||||
exposedType: PropertySetValueType.Color,
|
||||
defaultValue: defaultValueAsWinUIColor);
|
||||
break;
|
||||
|
||||
case CompositionGetValueStatus.Succeeded:
|
||||
// The property has already been added.
|
||||
var existingValue = ConvertTo.Color(ConvertTo.Color(existingColorAsVector4));
|
||||
|
||||
if (defaultValueAsVector4 != existingColorAsVector4)
|
||||
{
|
||||
context.Issues.ThemePropertyValuesAreInconsistent(bindingName, existingValue.ToString(), ConvertTo.Color(ConvertTo.Color(defaultValueAsVector4)).ToString());
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case CompositionGetValueStatus.TypeMismatch:
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures there is a property in the theme property set with the given name and default value.
|
||||
/// </summary>
|
||||
static void EnsureScalarThemePropertyExists(TranslationContext context, string bindingName, double defaultValue)
|
||||
{
|
||||
var defaultValueAsFloat = ConvertTo.Float(defaultValue);
|
||||
var themePropertySet = GetThemePropertySet(context);
|
||||
|
||||
// Insert a property set value for the scalar if one hasn't yet been added.
|
||||
switch (themePropertySet.TryGetScalar(bindingName, out var existingValueAsFloat))
|
||||
{
|
||||
case CompositionGetValueStatus.NotFound:
|
||||
// The property hasn't been added yet. Add it.
|
||||
themePropertySet.InsertScalar(bindingName, defaultValueAsFloat);
|
||||
context.PropertyBindings.AddPropertyBinding(
|
||||
bindingName,
|
||||
actualType: PropertySetValueType.Scalar,
|
||||
exposedType: PropertySetValueType.Scalar,
|
||||
defaultValue: ConvertTo.Float(defaultValue));
|
||||
break;
|
||||
|
||||
case CompositionGetValueStatus.Succeeded:
|
||||
// The property has already been added.
|
||||
if (existingValueAsFloat != defaultValueAsFloat)
|
||||
{
|
||||
context.Issues.ThemePropertyValuesAreInconsistent(bindingName, existingValueAsFloat.ToString(), defaultValueAsFloat.ToString());
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case CompositionGetValueStatus.TypeMismatch:
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryBindScalarPropertyToTheme(
|
||||
TranslationContext context,
|
||||
CompositionObject target,
|
||||
string bindingSpec,
|
||||
string lottiePropertyName,
|
||||
string compositionPropertyName,
|
||||
double defaultValue)
|
||||
{
|
||||
var bindingName = GetThemeBindingNameForLottieProperty(context, bindingSpec, lottiePropertyName);
|
||||
|
||||
if (bindingName is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure there is a property in the theme property set for this binding name.
|
||||
EnsureScalarThemePropertyExists(context, bindingName, defaultValue);
|
||||
|
||||
// Create an expression that binds property to the theme property set.
|
||||
var anim = context.ObjectFactory.CreateExpressionAnimation(ExpressionFactory.ThemedScalar(bindingName));
|
||||
anim.SetReferenceParameter(ThemePropertiesName, GetThemePropertySet(context));
|
||||
target.StartAnimation(compositionPropertyName, anim);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StateCache
|
||||
{
|
||||
CompositionPropertySet _themePropertySet;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="CompositionPropertySet"/> used for property bindings for themed Lotties.
|
||||
/// </summary>
|
||||
public CompositionPropertySet GetThemePropertySet(TranslationContext context) =>
|
||||
_themePropertySet ??= context.ObjectFactory.CreatePropertySet();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,733 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
using Expr = Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.Expressions.Expression;
|
||||
using Sn = System.Numerics;
|
||||
|
||||
#if DEBUG
|
||||
// For diagnosing issues, give nothing scale.
|
||||
//#define NoScaling
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates Lottie transforms.
|
||||
/// </summary>
|
||||
static class Transforms
|
||||
{
|
||||
public static Sn.Matrix3x2 CreateMatrixFromTransform(LayerContext context, Transform transform)
|
||||
{
|
||||
if (transform is null)
|
||||
{
|
||||
return Sn.Matrix3x2.Identity;
|
||||
}
|
||||
|
||||
if (transform.IsAnimated)
|
||||
{
|
||||
// TODO - report an issue. We can't handle an animated transform.
|
||||
// TODO - we could handle it if the only thing that is animated is the Opacity.
|
||||
}
|
||||
|
||||
var anchor = ConvertTo.Vector2(transform.Anchor.InitialValue);
|
||||
var position = ConvertTo.Vector2(transform.Position.InitialValue);
|
||||
var scale = ConvertTo.Vector2(transform.ScalePercent.InitialValue / 100.0);
|
||||
var rotation = (float)transform.Rotation.InitialValue.Radians;
|
||||
|
||||
// Calculate the matrix that is equivalent to the properties.
|
||||
var combinedMatrix =
|
||||
Sn.Matrix3x2.CreateScale(scale, anchor) *
|
||||
Sn.Matrix3x2.CreateRotation(rotation, anchor) *
|
||||
Sn.Matrix3x2.CreateTranslation(position + anchor);
|
||||
|
||||
return combinedMatrix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a chain with a Visual at the top and a CompositionContainerShape at the bottom.
|
||||
/// The nodes in between implement the transforms for the layer.
|
||||
/// This chain is used when a shape tree needs to be expressed as a visual tree. We take
|
||||
/// advantage of this case to do layer opacity and visibility using Visual nodes rather
|
||||
/// than pushing the opacity to the leaves and using Scale animations to do visibility.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the the chain was created.</returns>
|
||||
public static bool TryCreateShapeVisualTransformChain(
|
||||
LayerContext context,
|
||||
out ContainerVisual rootNode,
|
||||
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(LottieData.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a chain of ContainerShape that define the transforms for a layer.
|
||||
/// The top of the chain is the rootTransform, the bottom is the contentsNode.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the the chain was created.</returns>
|
||||
public static bool TryCreateContainerShapeTransformChain(
|
||||
LayerContext context,
|
||||
out CompositionContainerShape rootNode,
|
||||
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(LottieData.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a chain of ContainerVisual that define the transforms for a layer.
|
||||
/// The top of the chain is the rootTransform, the bottom is the leafTransform.
|
||||
/// Returns false if the layer is never visible.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the the chain was created.</returns>
|
||||
public static bool TryCreateContainerVisualTransformChain(
|
||||
LayerContext context,
|
||||
out ContainerVisual rootNode,
|
||||
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(LottieData.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;
|
||||
}
|
||||
|
||||
static void InsertVisibilityVisualIntoTransformChain(
|
||||
TranslationContext context,
|
||||
float inProgress,
|
||||
float outProgress,
|
||||
ref ContainerVisual root)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
// Insert a new node to control visibility at the top of the chain.
|
||||
var visibilityNode = context.ObjectFactory.CreateContainerVisual();
|
||||
visibilityNode.Children.Add(root);
|
||||
root = visibilityNode;
|
||||
|
||||
var visibilityAnimation = context.ObjectFactory.CreateBooleanKeyFrameAnimation();
|
||||
if (inProgress > 0)
|
||||
{
|
||||
// Set initial value to be non-visible.
|
||||
visibilityNode.IsVisible = false;
|
||||
visibilityAnimation.InsertKeyFrame(inProgress, true);
|
||||
}
|
||||
|
||||
if (outProgress < 1)
|
||||
{
|
||||
visibilityAnimation.InsertKeyFrame(outProgress, false);
|
||||
}
|
||||
|
||||
visibilityAnimation.Duration = context.LottieComposition.Duration;
|
||||
Animate.WithKeyFrame(context, visibilityNode, "IsVisible", visibilityAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
static void InsertOpacityVisualIntoTransformChain(
|
||||
LayerContext context,
|
||||
in TrimmedAnimatable<Opacity> opacity,
|
||||
ref ContainerVisual root)
|
||||
{
|
||||
// Implement opacity for the layer.
|
||||
if (opacity.IsAnimated || opacity.InitialValue < LottieData.Opacity.Opaque)
|
||||
{
|
||||
// Insert a new node to control opacity at the top of the chain.
|
||||
var opacityNode = context.ObjectFactory.CreateContainerVisual();
|
||||
|
||||
opacityNode.SetDescription(context, () => $"Opacity for layer: {context.Layer.Name}");
|
||||
|
||||
opacityNode.Children.Add(root);
|
||||
root = opacityNode;
|
||||
|
||||
if (opacity.IsAnimated)
|
||||
{
|
||||
Animate.Opacity(context, opacity, opacityNode, "Opacity", "Layer opacity animation");
|
||||
}
|
||||
else
|
||||
{
|
||||
opacityNode.Opacity = ConvertTo.Opacity(opacity.InitialValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a chain of ContainerVisual that define the transform for a layer.
|
||||
// The top of the chain is the rootTransform, the bottom is the leafTransform.
|
||||
static void TranslateTransformOnContainerVisualForLayer(
|
||||
LayerContext context,
|
||||
Layer layer,
|
||||
out ContainerVisual rootTransformNode,
|
||||
out ContainerVisual leafTransformNode)
|
||||
{
|
||||
// Create a ContainerVisual to apply the transform to.
|
||||
leafTransformNode = context.ObjectFactory.CreateContainerVisual();
|
||||
|
||||
// Apply the transform.
|
||||
TranslateAndApplyTransform(context, layer.Transform, leafTransformNode);
|
||||
leafTransformNode.SetDescription(context, () => $"Transforms for {layer.Name}");
|
||||
|
||||
// Translate the parent transform, if any.
|
||||
if (layer.Parent != null)
|
||||
{
|
||||
var parentLayer = context.CompositionContext.Layers.GetLayerById(layer.Parent.Value);
|
||||
TranslateTransformOnContainerVisualForLayer(context, parentLayer, out rootTransformNode, out var parentLeafTransform);
|
||||
parentLeafTransform.Children.Add(leafTransformNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
rootTransformNode = leafTransformNode;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a chain of CompositionContainerShape that define the transform for a layer.
|
||||
// The top of the chain is the rootTransform, the bottom is the leafTransform.
|
||||
static void TranslateTransformOnContainerShapeForLayer(
|
||||
LayerContext context,
|
||||
Layer layer,
|
||||
out CompositionContainerShape rootTransformNode,
|
||||
out CompositionContainerShape leafTransformNode)
|
||||
{
|
||||
// Create a ContainerVisual to apply the transform to.
|
||||
leafTransformNode = context.ObjectFactory.CreateContainerShape();
|
||||
|
||||
// Apply the transform from the layer.
|
||||
TranslateAndApplyTransform(context, layer.Transform, leafTransformNode);
|
||||
|
||||
// Recurse to translate the parent transform, if any.
|
||||
if (layer.Parent != null)
|
||||
{
|
||||
var parentLayer = context.CompositionContext.Layers.GetLayerById(layer.Parent.Value);
|
||||
TranslateTransformOnContainerShapeForLayer(context, parentLayer, out rootTransformNode, out var parentLeafTransform);
|
||||
parentLeafTransform.Shapes.Add(leafTransformNode);
|
||||
leafTransformNode.SetDescription(context, () => ($"Transforms for {layer.Name}", $"Transforms: {layer.Name}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
rootTransformNode = leafTransformNode;
|
||||
}
|
||||
}
|
||||
|
||||
public static void TranslateAndApplyTransform(
|
||||
LayerContext context,
|
||||
Transform transform,
|
||||
ContainerShapeOrVisual container)
|
||||
{
|
||||
TranslateAndApplyAnchorPositionRotationAndScale(
|
||||
context,
|
||||
transform.Anchor,
|
||||
transform.Position,
|
||||
Optimizer.TrimAnimatable(context, transform.Rotation),
|
||||
transform.ScalePercent,
|
||||
container);
|
||||
|
||||
// TODO: set Skew and Skew Axis
|
||||
}
|
||||
|
||||
static void TranslateAndApplyAnchorPositionRotationAndScale(
|
||||
LayerContext context,
|
||||
IAnimatableVector3 anchor,
|
||||
IAnimatableVector3 position,
|
||||
in TrimmedAnimatable<Rotation> rotation,
|
||||
IAnimatableVector3 scalePercent,
|
||||
ContainerShapeOrVisual container)
|
||||
{
|
||||
// There are many different cases to consider in order to do this optimally:
|
||||
// * Is the container a CompositionContainerShape (Vector2 properties)
|
||||
// or a ContainerVisual (Vector3 properties)
|
||||
// * Is the anchor animated?
|
||||
// * Is the anchor expressed as a Vector2 or as X and Y values?
|
||||
// * Is the position animated?
|
||||
// * Is the position expressed as a Vector2 or as X and Y values?
|
||||
// * Is rotation or scale specified? (If they're not and
|
||||
// the anchor is static then the anchor can be expressed
|
||||
// as just an offset)
|
||||
//
|
||||
// The current implementation doesn't take all cases into consideration yet.
|
||||
if (rotation.IsAnimated)
|
||||
{
|
||||
Animate.Rotation(context, rotation, container, nameof(container.RotationAngleInDegrees), "Rotation");
|
||||
}
|
||||
else
|
||||
{
|
||||
container.RotationAngleInDegrees = ConvertTo.FloatDefaultIsZero(rotation.InitialValue.Degrees);
|
||||
}
|
||||
|
||||
#if !NoScaling
|
||||
// If the channels have separate easings, convert to an AnimatableXYZ.
|
||||
var scale = AnimatableVector3Rewriter.EnsureOneEasingPerChannel(scalePercent);
|
||||
|
||||
if (scale is AnimatableXYZ scaleXYZ)
|
||||
{
|
||||
var trimmedX = Optimizer.TrimAnimatable(context, scaleXYZ.X);
|
||||
var trimmedY = Optimizer.TrimAnimatable(context, scaleXYZ.Y);
|
||||
|
||||
if (trimmedX.IsAnimated)
|
||||
{
|
||||
Animate.ScaledScalar(context, trimmedX, 1 / 100.0, container, $"{nameof(container.Scale)}.X", nameof(container.Scale));
|
||||
}
|
||||
|
||||
if (trimmedY.IsAnimated)
|
||||
{
|
||||
Animate.ScaledScalar(context, trimmedY, 1 / 100.0, container, $"{nameof(container.Scale)}.Y", nameof(container.Scale));
|
||||
}
|
||||
|
||||
if (!trimmedX.IsAnimated || !trimmedY.IsAnimated)
|
||||
{
|
||||
container.Scale = ConvertTo.Vector2DefaultIsOne(new Vector3(trimmedX.InitialValue, trimmedY.InitialValue, 0) * (1 / 100.0));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var trimmedScale = Optimizer.TrimAnimatable<Vector3>(context, (AnimatableVector3)scale);
|
||||
|
||||
if (trimmedScale.IsAnimated)
|
||||
{
|
||||
if (container.IsShape)
|
||||
{
|
||||
Animate.ScaledVector2(context, trimmedScale, 1 / 100.0, container, nameof(container.Scale), nameof(container.Scale));
|
||||
}
|
||||
else
|
||||
{
|
||||
Animate.ScaledVector3(context, trimmedScale, 1 / 100.0, container, nameof(container.Scale), nameof(container.Scale));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
container.Scale = ConvertTo.Vector2DefaultIsOne(trimmedScale.InitialValue * (1 / 100.0));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var anchorX = default(TrimmedAnimatable<double>);
|
||||
var anchorY = default(TrimmedAnimatable<double>);
|
||||
var anchor3 = default(TrimmedAnimatable<Vector3>);
|
||||
|
||||
var xyzAnchor = anchor as AnimatableXYZ;
|
||||
if (xyzAnchor != null)
|
||||
{
|
||||
anchorX = Optimizer.TrimAnimatable(context, xyzAnchor.X);
|
||||
anchorY = Optimizer.TrimAnimatable(context, xyzAnchor.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
anchor3 = Optimizer.TrimAnimatable(context, anchor);
|
||||
}
|
||||
|
||||
var positionX = default(TrimmedAnimatable<double>);
|
||||
var positionY = default(TrimmedAnimatable<double>);
|
||||
var position3 = default(TrimmedAnimatable<Vector3>);
|
||||
var positionWithSeparateEasings = AnimatableVector3Rewriter.EnsureOneEasingPerChannel(position);
|
||||
|
||||
var xyzPosition = positionWithSeparateEasings as AnimatableXYZ;
|
||||
if (xyzPosition != null)
|
||||
{
|
||||
positionX = Optimizer.TrimAnimatable(context, xyzPosition.X);
|
||||
positionY = Optimizer.TrimAnimatable(context, xyzPosition.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
position3 = Optimizer.TrimAnimatable<Vector3>(context, (AnimatableVector3)positionWithSeparateEasings);
|
||||
}
|
||||
|
||||
var anchorIsAnimated = anchorX.IsAnimated || anchorY.IsAnimated || anchor3.IsAnimated;
|
||||
var positionIsAnimated = positionX.IsAnimated || positionY.IsAnimated || position3.IsAnimated;
|
||||
|
||||
var initialAnchor = xyzAnchor != null ? ConvertTo.Vector2(anchorX.InitialValue, anchorY.InitialValue) : ConvertTo.Vector2(anchor3.InitialValue);
|
||||
var initialPosition = xyzPosition != null ? ConvertTo.Vector2(positionX.InitialValue, positionY.InitialValue) : ConvertTo.Vector2(position3.InitialValue);
|
||||
|
||||
// The Lottie Anchor is the centerpoint of the object and is used for rotation and scaling.
|
||||
if (anchorIsAnimated)
|
||||
{
|
||||
container.Properties.InsertVector2("Anchor", initialAnchor);
|
||||
var centerPointExpression = context.ObjectFactory.CreateExpressionAnimation(container.IsShape ? (Expr)ExpressionFactory.MyAnchor : (Expr)ExpressionFactory.MyAnchor3);
|
||||
centerPointExpression.SetReferenceParameter("my", container);
|
||||
Animate.WithExpression(container, centerPointExpression, nameof(container.CenterPoint));
|
||||
|
||||
if (xyzAnchor != null)
|
||||
{
|
||||
if (anchorX.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, anchorX, container.Properties, targetPropertyName: "Anchor.X");
|
||||
}
|
||||
|
||||
if (anchorY.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, anchorY, container.Properties, targetPropertyName: "Anchor.Y");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Animate.Vector2(context, anchor3, container.Properties, "Anchor");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
container.CenterPoint = ConvertTo.Vector2DefaultIsZero(initialAnchor);
|
||||
}
|
||||
|
||||
// If the position or anchor are animated, the offset needs to be calculated via an expression.
|
||||
ExpressionAnimation offsetExpression = null;
|
||||
if (positionIsAnimated && anchorIsAnimated)
|
||||
{
|
||||
// Both position and anchor are animated.
|
||||
offsetExpression = context.ObjectFactory.CreateExpressionAnimation(container.IsShape ? (Expr)ExpressionFactory.PositionMinusAnchor2 : (Expr)ExpressionFactory.PositionMinusAnchor3);
|
||||
}
|
||||
else if (positionIsAnimated)
|
||||
{
|
||||
// Only position is animated.
|
||||
if (initialAnchor == Sn.Vector2.Zero)
|
||||
{
|
||||
// Position and Offset are equivalent because the Anchor is not animated and is 0.
|
||||
// We don't need to animate a Position property - we can animate Offset directly.
|
||||
positionIsAnimated = false;
|
||||
|
||||
if (xyzPosition != null)
|
||||
{
|
||||
if (!positionX.IsAnimated || !positionY.IsAnimated)
|
||||
{
|
||||
container.Offset = ConvertTo.Vector2DefaultIsZero(initialPosition - initialAnchor);
|
||||
}
|
||||
|
||||
if (positionX.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, positionX, container, targetPropertyName: "Offset.X");
|
||||
}
|
||||
|
||||
if (positionY.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, positionY, container, targetPropertyName: "Offset.Y");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO - when we support spatial Bezier CubicBezierFunction3, we can enable this. For now this
|
||||
// may result in a CubicBezierFunction2 being applied to the Vector3 Offset property.
|
||||
//ApplyVector3KeyFrameAnimation(context, (AnimatableVector3)position, container, "Offset");
|
||||
offsetExpression = context.ObjectFactory.CreateExpressionAnimation(container.IsShape
|
||||
? (Expr)Expr.Vector2(
|
||||
ExpressionFactory.MyPosition.X - initialAnchor.X,
|
||||
ExpressionFactory.MyPosition.Y - initialAnchor.Y)
|
||||
: (Expr)Expr.Vector3(
|
||||
ExpressionFactory.MyPosition.X - initialAnchor.X,
|
||||
ExpressionFactory.MyPosition.Y - initialAnchor.Y,
|
||||
0));
|
||||
|
||||
positionIsAnimated = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-zero non-animated anchor. Subtract the anchor.
|
||||
offsetExpression = context.ObjectFactory.CreateExpressionAnimation(container.IsShape
|
||||
? (Expr)Expr.Vector2(
|
||||
ExpressionFactory.MyPosition.X - initialAnchor.X,
|
||||
ExpressionFactory.MyPosition.Y - initialAnchor.Y)
|
||||
: (Expr)Expr.Vector3(
|
||||
ExpressionFactory.MyPosition.X - initialAnchor.X,
|
||||
ExpressionFactory.MyPosition.Y - initialAnchor.Y,
|
||||
0));
|
||||
}
|
||||
}
|
||||
else if (anchorIsAnimated)
|
||||
{
|
||||
// Only anchor is animated.
|
||||
offsetExpression = context.ObjectFactory.CreateExpressionAnimation(container.IsShape
|
||||
? (Expr)Expr.Vector2(
|
||||
initialPosition.X - ExpressionFactory.MyAnchor.X,
|
||||
initialPosition.Y - ExpressionFactory.MyAnchor.Y)
|
||||
: (Expr)Expr.Vector3(
|
||||
initialPosition.X - ExpressionFactory.MyAnchor.X,
|
||||
initialPosition.Y - ExpressionFactory.MyAnchor.Y,
|
||||
0));
|
||||
}
|
||||
|
||||
if (!positionIsAnimated && !anchorIsAnimated)
|
||||
{
|
||||
// Position and Anchor are static. No expression needed.
|
||||
container.Offset = ConvertTo.Vector2DefaultIsZero(initialPosition - initialAnchor);
|
||||
}
|
||||
|
||||
// Position is a Lottie-only concept. It offsets the object relative to the Anchor.
|
||||
if (positionIsAnimated)
|
||||
{
|
||||
if (!anchorIsAnimated && xyzPosition is null)
|
||||
{
|
||||
// The anchor isn't animated and the position is an animated Vector3. This is a very
|
||||
// common case, and can be simplified to an Offset animation by subtracting the Anchor from the Position.
|
||||
offsetExpression = null;
|
||||
var anchoredPosition = PositionAndAnchorToOffset(context, position3, anchor.InitialValue);
|
||||
if (container.IsShape)
|
||||
{
|
||||
Animate.Vector2(context, anchoredPosition, container, "Offset");
|
||||
}
|
||||
else
|
||||
{
|
||||
Animate.Vector3(context, anchoredPosition, container, "Offset");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Anchor and Position are both animated.
|
||||
container.Properties.InsertVector2("Position", initialPosition);
|
||||
|
||||
if (xyzPosition != null)
|
||||
{
|
||||
if (positionX.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, positionX, container.Properties, targetPropertyName: "Position.X");
|
||||
}
|
||||
|
||||
if (positionY.IsAnimated)
|
||||
{
|
||||
Animate.Scalar(context, positionY, container.Properties, targetPropertyName: "Position.Y");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Animate.Vector2(context, position3, container.Properties, "Position");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (offsetExpression != null)
|
||||
{
|
||||
offsetExpression.SetReferenceParameter("my", container);
|
||||
Animate.WithExpression(container, offsetExpression, nameof(container.Offset));
|
||||
}
|
||||
}
|
||||
|
||||
static TrimmedAnimatable<Vector3> PositionAndAnchorToOffset(LayerContext context, in TrimmedAnimatable<Vector3> animation, Vector3 anchor)
|
||||
{
|
||||
var keyframes = new KeyFrame<Vector3>[animation.KeyFrames.Count];
|
||||
|
||||
for (var i = 0; i < animation.KeyFrames.Count; i++)
|
||||
{
|
||||
var kf = animation.KeyFrames[i];
|
||||
keyframes[i] = kf.CloneWithNewValue(kf.Value - anchor);
|
||||
}
|
||||
|
||||
return new TrimmedAnimatable<Vector3>(context, keyframes[0].Value, keyframes);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,176 +4,247 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization;
|
||||
using Sn = System.Numerics;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieMetadata;
|
||||
using Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
||||
{
|
||||
/// <summary>
|
||||
/// The context in which to translate a composition. This is used to ensure that
|
||||
/// layers are translated in the context of the composition or their containing
|
||||
/// PreComp, and to carry around other context-specific state.
|
||||
/// Translates a <see cref="LottieData.LottieComposition"/> to an equivalent <see cref="Visual"/>.
|
||||
/// </summary>
|
||||
abstract class TranslationContext
|
||||
sealed class TranslationContext
|
||||
{
|
||||
FrameNumberEqualityComparer _frameNumberEqualityComparer;
|
||||
// Identifies the Lottie metadata in TranslationResult.SourceMetadata.
|
||||
static readonly Guid s_lottieMetadataKey = new Guid("EA3D6538-361A-4B1C-960D-50A6C35563A5");
|
||||
|
||||
TranslationContext(TranslationContext containingContext, Layer layer)
|
||||
// Stores state on behalf of static classes. This allows static classes to
|
||||
// associate state they need for their methods with a particular TranslationContext instance.
|
||||
readonly Dictionary<Type, object> _stateCache = new Dictionary<Type, object>();
|
||||
|
||||
TranslationContext(
|
||||
LottieComposition lottieComposition,
|
||||
Compositor compositor,
|
||||
in TranslatorConfiguration configuration)
|
||||
{
|
||||
ContainingContext = containingContext;
|
||||
Layer = layer;
|
||||
}
|
||||
LottieComposition = lottieComposition;
|
||||
ObjectFactory = new CompositionObjectFactory(this, compositor, configuration.TargetUapVersion);
|
||||
Issues = new TranslationIssues(configuration.StrictTranslation);
|
||||
AddDescriptions = configuration.AddCodegenDescriptions;
|
||||
TranslatePropertyBindings = configuration.TranslatePropertyBindings;
|
||||
|
||||
// Constructs the root context.
|
||||
TranslationContext(LottieComposition lottieComposition)
|
||||
{
|
||||
Layers = lottieComposition.Layers;
|
||||
StartTime = lottieComposition.InPoint;
|
||||
DurationInFrames = lottieComposition.OutPoint - lottieComposition.InPoint;
|
||||
Size = new Sn.Vector2((float)lottieComposition.Width, (float)lottieComposition.Height);
|
||||
}
|
||||
|
||||
internal TranslationContext ContainingContext { get; }
|
||||
|
||||
internal Layer Layer { get; }
|
||||
|
||||
// A set of layers that can be referenced by id.
|
||||
internal LayerCollection Layers { get; private set; }
|
||||
|
||||
internal Sn.Vector2 Size { get; private set; }
|
||||
|
||||
// The start time of the current layer, in composition time.
|
||||
internal double StartTime { get; private set; }
|
||||
|
||||
internal double EndTime => StartTime + DurationInFrames;
|
||||
|
||||
internal double DurationInFrames { get; private set; }
|
||||
|
||||
public override string ToString() => Layer.Name ?? Layer.Type.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Layer"/>'s in point as a progress value.
|
||||
/// </summary>
|
||||
internal float InPointAsProgress =>
|
||||
(float)((Layer.InPoint - StartTime) / DurationInFrames);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Layer"/>'s out point as a progress value.
|
||||
/// </summary>
|
||||
internal float OutPointAsProgress =>
|
||||
(float)((Layer.OutPoint - StartTime) / DurationInFrames);
|
||||
|
||||
internal IEqualityComparer<double> FrameNumberComparer =>
|
||||
_frameNumberEqualityComparer ??= new FrameNumberEqualityComparer(this);
|
||||
|
||||
// Constructs a context for the given layer that is a child of this context.
|
||||
internal For<T> SubContext<T>(T layer)
|
||||
where T : Layer
|
||||
{
|
||||
var result = new For<T>(this, layer)
|
||||
if (configuration.GenerateColorBindings)
|
||||
{
|
||||
Size = Size,
|
||||
StartTime = StartTime,
|
||||
Layers = Layers,
|
||||
DurationInFrames = DurationInFrames,
|
||||
};
|
||||
ColorPalette = new Dictionary<Color, string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to translates the given <see cref="LottieData.LottieComposition"/>.
|
||||
/// </summary>
|
||||
/// <param name="lottieComposition">The <see cref="LottieData.LottieComposition"/> to translate.</param>
|
||||
/// <param name="configuration">Controls the configuration of the translator.</param>
|
||||
/// <returns>The result of the translation.</returns>
|
||||
internal static TranslationResult TryTranslateLottieComposition(
|
||||
LottieComposition lottieComposition,
|
||||
in TranslatorConfiguration configuration)
|
||||
{
|
||||
// Set up the translator.
|
||||
var translator = new TranslationContext(
|
||||
lottieComposition,
|
||||
new Compositor(),
|
||||
configuration: configuration);
|
||||
|
||||
// Translate the Lottie content to a Composition graph.
|
||||
translator.Translate();
|
||||
|
||||
var rootVisual = translator.RootVisual;
|
||||
|
||||
var resultRequiredUapVersion = translator.ObjectFactory.HighestUapVersionUsed;
|
||||
|
||||
// See if the version is compatible with what the caller requested.
|
||||
if (configuration.TargetUapVersion < resultRequiredUapVersion)
|
||||
{
|
||||
// We couldn't translate it and meet the requirement for the requested minimum version.
|
||||
rootVisual = null;
|
||||
}
|
||||
|
||||
// Add the metadata.
|
||||
var sourceMetadata = new Dictionary<Guid, object>();
|
||||
|
||||
// Metadata from the source.
|
||||
sourceMetadata.Add(
|
||||
s_lottieMetadataKey,
|
||||
new LottieCompositionMetadata(
|
||||
lottieComposition.Name,
|
||||
lottieComposition.FramesPerSecond,
|
||||
lottieComposition.InPoint,
|
||||
lottieComposition.OutPoint,
|
||||
lottieComposition.Markers.Select(m => (m.Name, m.Frame, m.DurationInFrames))));
|
||||
|
||||
// The list of property binding names.
|
||||
translator.PropertyBindings.AddToSourceMetadata(sourceMetadata);
|
||||
|
||||
return new TranslationResult(
|
||||
rootVisual: rootVisual,
|
||||
translationIssues: translator.Issues.GetIssues().Select(i =>
|
||||
new TranslationIssue(code: i.Code, description: i.Description)),
|
||||
minimumRequiredUapVersion: resultRequiredUapVersion,
|
||||
sourceMetadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, descriptions are added to the generated objects for use in generated source code.
|
||||
/// </summary>
|
||||
public bool AddDescriptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The palette of colors in fills and strokes. Null if color bindings are not enabled.
|
||||
/// </summary>
|
||||
public Dictionary<Color, string> ColorPalette { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating composition objects.
|
||||
/// </summary>
|
||||
public CompositionObjectFactory ObjectFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Manages the collection of issues that have been seen during the translation.
|
||||
/// </summary>
|
||||
public TranslationIssues Issues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="LottieData.LottieComposition"/> being translated.
|
||||
/// </summary>
|
||||
public LottieComposition LottieComposition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Factory used for creating CompositionPropertySet properties
|
||||
/// that map from the Progress value of the animated visual
|
||||
/// to another value. These are used to create properties
|
||||
/// that are required by cubic Bezier expressions used for
|
||||
/// spatial Beziers.
|
||||
/// </summary>
|
||||
public ProgressMapFactory ProgressMapFactory { get; } = new ProgressMapFactory();
|
||||
|
||||
/// <summary>
|
||||
/// The name of the property on the resulting <see cref="Visual"/> that controls the progress
|
||||
/// of the animation. Setting this property (directly or with an animation)
|
||||
/// between 0 and 1 controls the position of the animation.
|
||||
/// </summary>
|
||||
public static string ProgressPropertyName => "Progress";
|
||||
|
||||
/// <summary>
|
||||
/// The names that are bound to properties (such as the Color of a SolidColorFill).
|
||||
/// Keep track of them here so that codegen can generate properties for them.
|
||||
/// </summary>
|
||||
public PropertyBindings PropertyBindings { get; } = new PropertyBindings();
|
||||
|
||||
/// <summary>
|
||||
/// The root Visual of the resulting translation.
|
||||
/// </summary>
|
||||
public ContainerVisual RootVisual { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True iff theme property bindings are enabled.
|
||||
/// </summary>
|
||||
public bool TranslatePropertyBindings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the state cache of the given type, creating it if it doesn't
|
||||
/// already exist.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the cache.</typeparam>
|
||||
/// <returns>A state cache.</returns>
|
||||
/// <remarks>This is used to allow static classes to store and retrieve state
|
||||
/// for a translation.</remarks>
|
||||
public T GetStateCache<T>()
|
||||
where T : class, new()
|
||||
{
|
||||
// Look up the cache.
|
||||
if (_stateCache.TryGetValue(typeof(T), out var cached))
|
||||
{
|
||||
// The object has already been created. Return it.
|
||||
return (T)cached;
|
||||
}
|
||||
|
||||
// The object has not been created yet - create it now and cache
|
||||
// it for next time.
|
||||
var result = new T();
|
||||
_stateCache.Add(typeof(T), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Constructs a context for the given PreCompLayer that is a child of this context.
|
||||
internal For<PreCompLayer> PreCompSubContext(LayerCollection layers)
|
||||
{
|
||||
var layer = (PreCompLayer)Layer;
|
||||
var result = new For<PreCompLayer>(this, layer)
|
||||
{
|
||||
// Precomps define a new temporal and spatial space.
|
||||
Size = new Sn.Vector2((float)layer.Width, (float)layer.Height),
|
||||
StartTime = StartTime - layer.StartTime,
|
||||
Layers = layers,
|
||||
DurationInFrames = DurationInFrames,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal Path OptimizePath(Path path)
|
||||
{
|
||||
// Optimize the path data. This may result in a previously animated path
|
||||
// becoming non-animated.
|
||||
var optimizedPathData = TrimAnimatable(path.Data);
|
||||
|
||||
return path.CloneWithNewGeometry(
|
||||
optimizedPathData.IsAnimated
|
||||
? new Animatable<PathGeometry>(optimizedPathData.KeyFrames, path.Data.PropertyIndex)
|
||||
: new Animatable<PathGeometry>(optimizedPathData.InitialValue, path.Data.PropertyIndex));
|
||||
}
|
||||
|
||||
internal TrimmedAnimatable<Vector3> TrimAnimatable(IAnimatableVector3 animatable)
|
||||
=> TrimAnimatable<Vector3>((AnimatableVector3)animatable);
|
||||
|
||||
internal TrimmedAnimatable<T> TrimAnimatable<T>(Animatable<T> animatable)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
if (animatable.IsAnimated)
|
||||
{
|
||||
var trimmedKeyFrames = Optimizer.RemoveRedundantKeyFrames(Optimizer.TrimKeyFrames(animatable, StartTime, EndTime));
|
||||
return new TrimmedAnimatable<T>(
|
||||
this,
|
||||
trimmedKeyFrames.Count == 0
|
||||
? animatable.InitialValue
|
||||
: trimmedKeyFrames[0].Value,
|
||||
trimmedKeyFrames);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TrimmedAnimatable<T>(this, animatable.InitialValue, animatable.KeyFrames);
|
||||
}
|
||||
}
|
||||
|
||||
// The context for a Layer.
|
||||
internal sealed class For<T> : TranslationContext
|
||||
where T : Layer
|
||||
{
|
||||
internal For(TranslationContext containingContext, T layer)
|
||||
: base(containingContext, layer)
|
||||
{
|
||||
Layer = layer;
|
||||
}
|
||||
|
||||
internal new T Layer { get; }
|
||||
}
|
||||
|
||||
// The root context.
|
||||
internal sealed class Root : TranslationContext
|
||||
{
|
||||
internal Root(LottieComposition lottieComposition)
|
||||
: base(lottieComposition)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares frame numbers for equality. This takes into account the lossiness of the conversion
|
||||
/// that is done from <see cref="double"/> frame numbers to <see cref="float"/> progress values.
|
||||
/// Returns the asset with the given ID and type, or null if no such asset exists.
|
||||
/// </summary>
|
||||
sealed class FrameNumberEqualityComparer : IEqualityComparer<double>
|
||||
/// <returns>The asset or null.</returns>
|
||||
public Asset GetAssetById(LayerContext context, string assetId, Asset.AssetType expectedAssetType)
|
||||
{
|
||||
readonly TranslationContext _context;
|
||||
|
||||
internal FrameNumberEqualityComparer(TranslationContext context)
|
||||
var referencedAsset = LottieComposition.Assets.GetAssetById(assetId);
|
||||
if (referencedAsset is null)
|
||||
{
|
||||
_context = context;
|
||||
Issues.ReferencedAssetDoesNotExist(assetId);
|
||||
}
|
||||
else if (referencedAsset.Type != expectedAssetType)
|
||||
{
|
||||
Issues.InvalidAssetReferenceFromLayer(context.Layer.Type.ToString(), assetId, referencedAsset.Type.ToString(), expectedAssetType.ToString());
|
||||
referencedAsset = null;
|
||||
}
|
||||
|
||||
public bool Equals(double x, double y) => ProgressOf(x) == ProgressOf(y);
|
||||
|
||||
public int GetHashCode(double obj) => ProgressOf(obj).GetHashCode();
|
||||
|
||||
// Converts a frame number into a progress value.
|
||||
float ProgressOf(double value) =>
|
||||
(float)((value - _context.StartTime) / _context.DurationInFrames);
|
||||
return referencedAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Translates the LottieComposition. This must only be called once.
|
||||
void Translate()
|
||||
{
|
||||
Debug.Assert(RootVisual is null, "Translate() must only be called once.");
|
||||
|
||||
if (LottieComposition.Is3d)
|
||||
{
|
||||
// We don't yet support 3d, so report that as an issue for this Lottie.
|
||||
Issues.ThreeDIsNotSupported();
|
||||
}
|
||||
|
||||
// Create the root context.
|
||||
var context = new CompositionContext(this, LottieComposition);
|
||||
|
||||
// Create the root Visual.
|
||||
RootVisual = ObjectFactory.CreateContainerVisual();
|
||||
|
||||
RootVisual.SetDescription(this, () => ("The root of the composition.", string.Empty));
|
||||
RootVisual.SetName("Root");
|
||||
|
||||
// Add the master progress property to the visual.
|
||||
RootVisual.Properties.InsertScalar(ProgressPropertyName, 0);
|
||||
|
||||
// Add the translations of each layer to the root visual. This will recursively
|
||||
// add the tranlation of the layers in precomps.
|
||||
var contentsChildren = RootVisual.Children;
|
||||
foreach (var visual in Layers.TranslateLayersToVisuals(context))
|
||||
{
|
||||
contentsChildren.Add(visual);
|
||||
}
|
||||
|
||||
// Add and animate the properties that are used to create modified (scaled, eased)
|
||||
// versions of the Progress clock. These are necessary for the implementation of
|
||||
// spatial beziers and time remapping.
|
||||
foreach (var (name, scale, offset, ranges) in ProgressMapFactory.GetVariables())
|
||||
{
|
||||
RootVisual.Properties.InsertScalar(name, 0);
|
||||
var animation = ObjectFactory.CreateScalarKeyFrameAnimation();
|
||||
animation.Duration = LottieComposition.Duration;
|
||||
animation.SetReferenceParameter(ExpressionFactory.RootName, RootVisual);
|
||||
foreach (var keyframe in ranges)
|
||||
{
|
||||
animation.InsertKeyFrame(keyframe.Start, 0, ObjectFactory.CreateStepThenHoldEasingFunction());
|
||||
animation.InsertKeyFrame(keyframe.End, 1, ObjectFactory.CreateCompositionEasingFunction(keyframe.Easing));
|
||||
}
|
||||
|
||||
Animate.WithKeyFrame(this, RootVisual.Properties, name, animation, scale, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,14 +18,14 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
{
|
||||
readonly IReadOnlyList<KeyFrame<T>> _keyFrames;
|
||||
|
||||
internal TrimmedAnimatable(TranslationContext context, T initialValue, IReadOnlyList<KeyFrame<T>> keyFrames)
|
||||
internal TrimmedAnimatable(LayerContext context, T initialValue, IReadOnlyList<KeyFrame<T>> keyFrames)
|
||||
{
|
||||
Context = context;
|
||||
InitialValue = initialValue;
|
||||
_keyFrames = keyFrames;
|
||||
}
|
||||
|
||||
internal TrimmedAnimatable(TranslationContext context, T initialValue)
|
||||
internal TrimmedAnimatable(LayerContext context, T initialValue)
|
||||
{
|
||||
Context = context;
|
||||
InitialValue = initialValue;
|
||||
|
@ -53,6 +53,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
|
|||
/// <returns><c>true</c> if this value is always equal to the given value.</returns>
|
||||
internal bool IsAlways(T value) => !IsAnimated && value.Equals(InitialValue);
|
||||
|
||||
internal TranslationContext Context { get; }
|
||||
internal LayerContext Context { get; }
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче