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
+ }
0 commit comments