Skip to content

Commit 6d34478

Browse files
authored
[LottieGen] New optimizer that collapses PreComp layers if they reference the same RefId for AnimatedIcon (#474)
1 parent e937956 commit 6d34478

File tree

5 files changed

+216
-0
lines changed

5 files changed

+216
-0
lines changed

source/LottieData/LottieData.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Compile Include="$(MSBuildThisFileDirectory)Mask.cs" />
3636
<Compile Include="$(MSBuildThisFileDirectory)MergePaths.cs" />
3737
<Compile Include="$(MSBuildThisFileDirectory)NullLayer.cs" />
38+
<Compile Include="$(MSBuildThisFileDirectory)Optimization\CollapsePreCompsOptimizer.cs" />
3839
<Compile Include="$(MSBuildThisFileDirectory)Optimization\LayerGroup.cs" />
3940
<Compile Include="$(MSBuildThisFileDirectory)Optimization\LayersGraph.cs" />
4041
<Compile Include="$(MSBuildThisFileDirectory)Optimization\MergeHelper.cs" />

source/LottieData/Marker.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,10 @@ public Marker(
3131

3232
/// <inheritdoc/>
3333
public override LottieObjectType ObjectType => LottieObjectType.Marker;
34+
35+
public Marker WithTimeOffset(double offset)
36+
{
37+
return new Marker(Name, Frame + offset, DurationInFrames);
38+
}
3439
}
3540
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#nullable enable
6+
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
10+
namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Optimization
11+
{
12+
/// <summary>
13+
/// This optimizer is trying to optimize the most common Lottie scenario - usage for AnimatedIcon.
14+
/// AnimatedIcons have many animation segments for different states of the icon and in most
15+
/// cases they are represented by non-intersecting <see cref="PreCompLayer"/>s. Often these layers
16+
/// are referencing the same RefId in Asset collection so it means that in fact we can use
17+
/// only one <see cref="PreCompLayer"/> entry to display an animation for two (or more) identical segments.
18+
///
19+
/// This optimizer checks if this is a scenario described above, and if it is, it performs collapsing
20+
/// of <see cref="PreCompLayer"/>s that have same RefId into one.
21+
/// </summary>
22+
#if PUBLIC_LottieData
23+
public
24+
#endif
25+
26+
sealed class CollapsePreCompsOptimizer
27+
{
28+
public static LottieComposition Optimize(LottieComposition composition)
29+
{
30+
// Example of how this optimization works:
31+
//
32+
// m - markers
33+
//
34+
// Before optimization:
35+
//
36+
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_0 __| |__ RefId : comp_2 __|
37+
// ^ ^ ^ ^ ^ ^ ^ ^
38+
// m0 m1 m2 m3 m4 m5 m6 m7
39+
//
40+
//
41+
// Step 1 (delete duplicates)
42+
//
43+
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_2 __|
44+
// ^ ^ ^ ^ ^ ^ ^ ^
45+
// m0 m1 m2 m3 m4 m5 m6 m7
46+
//
47+
//
48+
// Step 2 (shift all layers to form one contiguous section)
49+
//
50+
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_2 __|
51+
// ^ ^ ^ ^ ^ ^ ^ ^
52+
// m0 m1 m2 m3 m4 m5 m6 m7
53+
//
54+
//
55+
// Step 3 (final, move all markers to corresponding layers):
56+
//
57+
// |__ RefId: comp_0 __| |________ RefId: comp_1 ________| |__ RefId : comp_2 __|
58+
// ^ ^ ^ ^ ^ ^
59+
// m0 m1 m2 m3 m6 m7
60+
// m4 m5
61+
//
62+
// 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.
63+
List<Layer> layers = composition.Layers.GetLayersBottomToTop().ToList();
64+
65+
// All layers should be pre-comp layers.
66+
if (!layers.All(a => a is PreCompLayer) || layers.Count == 0)
67+
{
68+
return composition;
69+
}
70+
71+
// Sort layers by beginning of their time range.
72+
layers.Sort((a, b) => a.InPoint.CompareTo(b.InPoint));
73+
74+
// There should not be intersecting layers.
75+
for (int i = 1; i < layers.Count; i++)
76+
{
77+
if (layers[i - 1].OutPoint > layers[i].InPoint)
78+
{
79+
return composition;
80+
}
81+
}
82+
83+
// AnimatedIcon uses pair of markers to represent animation segment.
84+
var startMarkers = new Dictionary<string, Marker>();
85+
var endMarkers = new Dictionary<string, Marker>();
86+
87+
foreach (var marker in composition.Markers)
88+
{
89+
if (marker.Name.EndsWith("_End"))
90+
{
91+
// End markers have %s_End format.
92+
endMarkers.Add(marker.Name.Substring(0, marker.Name.Length - "_End".Length), marker);
93+
}
94+
else if (marker.Name.EndsWith("_Start"))
95+
{
96+
// Start markers have %s_Start format.
97+
startMarkers.Add(marker.Name.Substring(0, marker.Name.Length - "_Start".Length), marker);
98+
}
99+
else
100+
{
101+
// All markers should have %s_Start or %s_End format.
102+
return composition;
103+
}
104+
}
105+
106+
// Each Start should match to one End.
107+
if (startMarkers.Count != endMarkers.Count)
108+
{
109+
return composition;
110+
}
111+
112+
// Next part of this function will perform PreComps collapsing.
113+
// We are iterating over layers in order and checking if we can find
114+
// another layer that has been added to the result and referencing the same RefId.
115+
//
116+
// After all layers are collapsed we can end up with some gaps where there is no animations/layers.
117+
// We can shift all the layers so that there will be no gaps, for this we need layerInPointOffset.
118+
var layerInPointOffset = new Dictionary<int, double>();
119+
var layersAfterCollapse = new List<Layer>();
120+
121+
for (int i = 0; i < layers.Count; i++)
122+
{
123+
// Find layer that is referencing the same RefId in already processed layers.
124+
int previousSameLayer = layersAfterCollapse.FindIndex(layer => ((PreCompLayer)layer).RefId == ((PreCompLayer)layers[i]).RefId);
125+
126+
if (previousSameLayer == -1)
127+
{
128+
// If there were no processed layers, we will offset time of the first layer to start at 0.
129+
if (layersAfterCollapse.Count == 0)
130+
{
131+
layerInPointOffset[i] = -layers[i].InPoint;
132+
}
133+
else
134+
{
135+
// Otherwise we will offset new layer to start right after previous processed layer.
136+
layerInPointOffset[i] = layersAfterCollapse[layersAfterCollapse.Count - 1].OutPoint - layers[i].InPoint;
137+
}
138+
139+
layersAfterCollapse.Add(layers[i].WithTimeOffset(layerInPointOffset[i]));
140+
}
141+
else
142+
{
143+
// If we found a layer that is referencing the same RefId we should offset new layer to start at the same point.
144+
// But we do not need to add this to the layersAfterCompression, since the same layer is already there.
145+
layerInPointOffset[i] = layersAfterCollapse[previousSameLayer].InPoint - layers[i].InPoint;
146+
}
147+
}
148+
149+
// Next part of this function will offset markers to match new layer positions.
150+
var markersAfterOffset = new List<Marker>();
151+
152+
foreach (var key in startMarkers.Keys)
153+
{
154+
// For each start there should be an end.
155+
if (!endMarkers.ContainsKey(key))
156+
{
157+
return composition;
158+
}
159+
160+
// Each pair of start and end should correspond to some PreCompLayer and should be inside of its time segment.
161+
int correspondingLayer = layers.FindIndex(
162+
layer => layer.InPoint <= startMarkers[key].Frame &&
163+
startMarkers[key].Frame <= endMarkers[key].Frame &&
164+
endMarkers[key].Frame <= layer.OutPoint);
165+
166+
if (correspondingLayer == -1)
167+
{
168+
return composition;
169+
}
170+
171+
// Offset each marker with the same shift as corresponding layer was shifted.
172+
markersAfterOffset.Add(startMarkers[key].WithTimeOffset(layerInPointOffset[correspondingLayer]));
173+
markersAfterOffset.Add(endMarkers[key].WithTimeOffset(layerInPointOffset[correspondingLayer]));
174+
}
175+
176+
return new LottieComposition(
177+
composition.Name,
178+
composition.Width,
179+
composition.Height,
180+
layersAfterCollapse[0].InPoint,
181+
layersAfterCollapse[layersAfterCollapse.Count - 1].OutPoint,
182+
composition.FramesPerSecond,
183+
composition.Is3d,
184+
composition.Version,
185+
composition.Assets,
186+
composition.Chars,
187+
composition.Fonts,
188+
new LayerCollection(layersAfterCollapse),
189+
markersAfterOffset,
190+
composition.ExtraData);
191+
}
192+
}
193+
}

source/LottieGen/CommandLineOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ sealed class CommandLineOptions
2323

2424
internal bool DisableTranslationOptimizer { get; private set; }
2525

26+
internal bool EnableAnimatedIconOptimizer { get; private set; }
27+
2628
// The parse error, or null if the parse succeeded.
2729
// The error should be a sentence (starts with a capital letter, and ends with a period).
2830
internal string? ErrorDescription { get; private set; }
@@ -97,6 +99,11 @@ internal string ToConfigurationCommandLine(Language languageSwitch)
9799
sb.Append($" -{nameof(DisableTranslationOptimizer)}");
98100
}
99101

102+
if (EnableAnimatedIconOptimizer)
103+
{
104+
sb.Append($" -{nameof(EnableAnimatedIconOptimizer)}");
105+
}
106+
100107
if (GenerateColorBindings)
101108
{
102109
sb.Append($" -{nameof(GenerateColorBindings)}");
@@ -176,6 +183,7 @@ enum Keyword
176183
DisableCodeGenOptimizer,
177184
DisableLottieMergeOptimizer,
178185
DisableTranslationOptimizer,
186+
EnableAnimatedIconOptimizer,
179187
GenerateColorBindings,
180188
GenerateDependencyObject,
181189
Help,
@@ -246,6 +254,7 @@ void ParseCommandLineStrings(string[] args)
246254
.AddPrefixedKeyword(Keyword.DisableCodeGenOptimizer)
247255
.AddPrefixedKeyword(Keyword.DisableLottieMergeOptimizer)
248256
.AddPrefixedKeyword(Keyword.DisableTranslationOptimizer)
257+
.AddPrefixedKeyword(Keyword.EnableAnimatedIconOptimizer)
249258
.AddPrefixedKeyword(Keyword.GenerateColorBindings)
250259
.AddPrefixedKeyword(Keyword.GenerateDependencyObject)
251260
.AddPrefixedKeyword(Keyword.Help, "?")
@@ -307,6 +316,9 @@ void ParseCommandLineStrings(string[] args)
307316
case Keyword.DisableTranslationOptimizer:
308317
DisableTranslationOptimizer = true;
309318
break;
319+
case Keyword.EnableAnimatedIconOptimizer:
320+
EnableAnimatedIconOptimizer = true;
321+
break;
310322
case Keyword.Public:
311323
Public = true;
312324
break;

source/LottieGen/LottieJsonFileProcessor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ bool Run(Stream jsonStream)
131131
return false;
132132
}
133133

134+
if (_options.EnableAnimatedIconOptimizer)
135+
{
136+
lottieComposition = CollapsePreCompsOptimizer.Optimize(lottieComposition);
137+
}
138+
134139
if (!_options.DisableLottieMergeOptimizer)
135140
{
136141
lottieComposition = LottieMergeOptimizer.Optimize(lottieComposition);

0 commit comments

Comments
 (0)