[LottieGen] New optimizer that collapses PreComp layers if they reference the same RefId for AnimatedIcon (#474)

This commit is contained in:
aborziak-ms 2021-10-06 16:49:11 -07:00 коммит произвёл GitHub
Родитель e937956fb9
Коммит 6d34478b01
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 216 добавлений и 0 удалений

Просмотреть файл

@ -35,6 +35,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Mask.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MergePaths.cs" />
<Compile Include="$(MSBuildThisFileDirectory)NullLayer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Optimization\CollapsePreCompsOptimizer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Optimization\LayerGroup.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Optimization\LayersGraph.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Optimization\MergeHelper.cs" />

Просмотреть файл

@ -31,5 +31,10 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData
/// <inheritdoc/>
public override LottieObjectType ObjectType => LottieObjectType.Marker;
public Marker WithTimeOffset(double offset)
{
return new Marker(Name, Frame + offset, DurationInFrames);
}
}
}

Просмотреть файл

@ -0,0 +1,193 @@
// 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.
#nullable enable
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization
{
/// <summary>
/// This optimizer is trying to optimize the most common Lottie scenario - usage for AnimatedIcon.
/// AnimatedIcons have many animation segments for different states of the icon and in most
/// cases they are represented by non-intersecting <see cref="PreCompLayer"/>s. Often these layers
/// are referencing the same RefId in Asset collection so it means that in fact we can use
/// only one <see cref="PreCompLayer"/> entry to display an animation for two (or more) identical segments.
///
/// This optimizer checks if this is a scenario described above, and if it is, it performs collapsing
/// of <see cref="PreCompLayer"/>s that have same RefId into one.
/// </summary>
#if PUBLIC_LottieData
public
#endif
sealed class CollapsePreCompsOptimizer
{
public static LottieComposition Optimize(LottieComposition composition)
{
// Example of how this optimization works:
//
// m - markers
//
// Before optimization:
//
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_0 __| |__ RefId : comp_2 __|
// ^ ^ ^ ^ ^ ^ ^ ^
// m0 m1 m2 m3 m4 m5 m6 m7
//
//
// Step 1 (delete duplicates)
//
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_2 __|
// ^ ^ ^ ^ ^ ^ ^ ^
// m0 m1 m2 m3 m4 m5 m6 m7
//
//
// Step 2 (shift all layers to form one contiguous section)
//
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_2 __|
// ^ ^ ^ ^ ^ ^ ^ ^
// m0 m1 m2 m3 m4 m5 m6 m7
//
//
// Step 3 (final, move all markers to corresponding layers):
//
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_2 __|
// ^ ^ ^ ^ ^ ^
// m0 m1 m2 m3 m6 m7
// m4 m5
//
// We deleted second entry of comp_0, shifted comp_2 to the left and moved markers m4 and m5 to the same spots where m0 and m1 are.
List<Layer> layers = composition.Layers.GetLayersBottomToTop().ToList();
// All layers should be pre-comp layers.
if (!layers.All(a => a is PreCompLayer) || layers.Count == 0)
{
return composition;
}
// Sort layers by beginning of their time range.
layers.Sort((a, b) => a.InPoint.CompareTo(b.InPoint));
// There should not be intersecting layers.
for (int i = 1; i < layers.Count; i++)
{
if (layers[i - 1].OutPoint > layers[i].InPoint)
{
return composition;
}
}
// AnimatedIcon uses pair of markers to represent animation segment.
var startMarkers = new Dictionary<string, Marker>();
var endMarkers = new Dictionary<string, Marker>();
foreach (var marker in composition.Markers)
{
if (marker.Name.EndsWith("_End"))
{
// End markers have %s_End format.
endMarkers.Add(marker.Name.Substring(0, marker.Name.Length - "_End".Length), marker);
}
else if (marker.Name.EndsWith("_Start"))
{
// Start markers have %s_Start format.
startMarkers.Add(marker.Name.Substring(0, marker.Name.Length - "_Start".Length), marker);
}
else
{
// All markers should have %s_Start or %s_End format.
return composition;
}
}
// Each Start should match to one End.
if (startMarkers.Count != endMarkers.Count)
{
return composition;
}
// Next part of this function will perform PreComps collapsing.
// We are iterating over layers in order and checking if we can find
// another layer that has been added to the result and referencing the same RefId.
//
// After all layers are collapsed we can end up with some gaps where there is no animations/layers.
// We can shift all the layers so that there will be no gaps, for this we need layerInPointOffset.
var layerInPointOffset = new Dictionary<int, double>();
var layersAfterCollapse = new List<Layer>();
for (int i = 0; i < layers.Count; i++)
{
// Find layer that is referencing the same RefId in already processed layers.
int previousSameLayer = layersAfterCollapse.FindIndex(layer => ((PreCompLayer)layer).RefId == ((PreCompLayer)layers[i]).RefId);
if (previousSameLayer == -1)
{
// If there were no processed layers, we will offset time of the first layer to start at 0.
if (layersAfterCollapse.Count == 0)
{
layerInPointOffset[i] = -layers[i].InPoint;
}
else
{
// Otherwise we will offset new layer to start right after previous processed layer.
layerInPointOffset[i] = layersAfterCollapse[layersAfterCollapse.Count - 1].OutPoint - layers[i].InPoint;
}
layersAfterCollapse.Add(layers[i].WithTimeOffset(layerInPointOffset[i]));
}
else
{
// If we found a layer that is referencing the same RefId we should offset new layer to start at the same point.
// But we do not need to add this to the layersAfterCompression, since the same layer is already there.
layerInPointOffset[i] = layersAfterCollapse[previousSameLayer].InPoint - layers[i].InPoint;
}
}
// Next part of this function will offset markers to match new layer positions.
var markersAfterOffset = new List<Marker>();
foreach (var key in startMarkers.Keys)
{
// For each start there should be an end.
if (!endMarkers.ContainsKey(key))
{
return composition;
}
// Each pair of start and end should correspond to some PreCompLayer and should be inside of its time segment.
int correspondingLayer = layers.FindIndex(
layer => layer.InPoint <= startMarkers[key].Frame &&
startMarkers[key].Frame <= endMarkers[key].Frame &&
endMarkers[key].Frame <= layer.OutPoint);
if (correspondingLayer == -1)
{
return composition;
}
// Offset each marker with the same shift as corresponding layer was shifted.
markersAfterOffset.Add(startMarkers[key].WithTimeOffset(layerInPointOffset[correspondingLayer]));
markersAfterOffset.Add(endMarkers[key].WithTimeOffset(layerInPointOffset[correspondingLayer]));
}
return new LottieComposition(
composition.Name,
composition.Width,
composition.Height,
layersAfterCollapse[0].InPoint,
layersAfterCollapse[layersAfterCollapse.Count - 1].OutPoint,
composition.FramesPerSecond,
composition.Is3d,
composition.Version,
composition.Assets,
composition.Chars,
composition.Fonts,
new LayerCollection(layersAfterCollapse),
markersAfterOffset,
composition.ExtraData);
}
}
}

Просмотреть файл

@ -23,6 +23,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieGen
internal bool DisableTranslationOptimizer { get; private set; }
internal bool EnableAnimatedIconOptimizer { get; private set; }
// The parse error, or null if the parse succeeded.
// The error should be a sentence (starts with a capital letter, and ends with a period).
internal string? ErrorDescription { get; private set; }
@ -97,6 +99,11 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieGen
sb.Append($" -{nameof(DisableTranslationOptimizer)}");
}
if (EnableAnimatedIconOptimizer)
{
sb.Append($" -{nameof(EnableAnimatedIconOptimizer)}");
}
if (GenerateColorBindings)
{
sb.Append($" -{nameof(GenerateColorBindings)}");
@ -176,6 +183,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieGen
DisableCodeGenOptimizer,
DisableLottieMergeOptimizer,
DisableTranslationOptimizer,
EnableAnimatedIconOptimizer,
GenerateColorBindings,
GenerateDependencyObject,
Help,
@ -246,6 +254,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieGen
.AddPrefixedKeyword(Keyword.DisableCodeGenOptimizer)
.AddPrefixedKeyword(Keyword.DisableLottieMergeOptimizer)
.AddPrefixedKeyword(Keyword.DisableTranslationOptimizer)
.AddPrefixedKeyword(Keyword.EnableAnimatedIconOptimizer)
.AddPrefixedKeyword(Keyword.GenerateColorBindings)
.AddPrefixedKeyword(Keyword.GenerateDependencyObject)
.AddPrefixedKeyword(Keyword.Help, "?")
@ -307,6 +316,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieGen
case Keyword.DisableTranslationOptimizer:
DisableTranslationOptimizer = true;
break;
case Keyword.EnableAnimatedIconOptimizer:
EnableAnimatedIconOptimizer = true;
break;
case Keyword.Public:
Public = true;
break;

Просмотреть файл

@ -131,6 +131,11 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieGen
return false;
}
if (_options.EnableAnimatedIconOptimizer)
{
lottieComposition = CollapsePreCompsOptimizer.Optimize(lottieComposition);
}
if (!_options.DisableLottieMergeOptimizer)
{
lottieComposition = LottieMergeOptimizer.Optimize(lottieComposition);