UNPKG

19 kBMarkdownView Raw
1# Lottie-iOS Render System Documentation
2
3The purpose of this document is to explain how Lottie's render system works.
4
5For questions reach out to the author: [Brandon Withrow](https://twitter.com/theWithra)
6
7## Check Before Submitting
8
9Before submitting a PR to Lottie please run through the following checklist.
10
11 - Run 'pod install' in `/Example`
12 - Ensure that all targets in `/Example/lottie-swift.xcworkspace` build
13 - Add any new files to all of the targets in `/Lottie.xcodeproj`
14 - Ensure that all targets build in `/Lottie.xcodeproj`
15
16 After making a PR please watch for PR notifications. We will run a series of tests on the PR to ensure that it does not break existing animations.
17
18 NOTE: PRs must be approved by the Maintainer of Lottie-ios before they can be merged.
19
20## Project Structure
21
22Lottie is available on iOS and MacOS via CocoaPods, NPM, and Carthage. Because of this, there are some things to consider when adding files to the project. All of the under-the-hood code in Lottie is written to compile in all environments. Specialty wrappers for both `iOS` and `MacOS` are written to give access to Lottie in each environment. These wrappers are designed to be as thin as possible, to avoid code fragmentation.
23
24For example, `UIKit` is only available on iOS, whereas MacOS uses `AppKit`.
25
26### Source Code Directory Structure
27All of the source code for Lottie is located in `/lottie-swift/src` in the repo. Here's a quick run down of the directory structure:
28
29 - `src`: The Root directory for all Lottie source files
30 - `Public`: Public facing files.
31 - `Animation`: Files relating to `Animation` and `AnimationView`. *Files in this directory are complied on both iOS and MacOS.*
32 - `AnimationCache`: Files relating to `AnimationCache`. *Files in this directory are complied on both iOS and MacOS.*
33 - `DynamicProperties`: All public facing files relating to the Dynamic Properties API. *Files in this directory are complied on both iOS and MacOS.*
34 - `ImageProvider`: Holds the `ImageProvider` protocol. *Files in this directory are complied on both iOS and MacOS.*
35 - `MacOS`: Files that are **only** compiled for MacOS. *Files in this directory are complied on MacOS.*
36 - `Primitives`: Primitive data structures. *Files in this directory are complied on both iOS and MacOS.*
37 - `iOS`: Files that are **only** compiled for iOS. *Files in this directory are complied on iOS.*
38 - `Private`: Private `internal` files. *Files in this directory are complied on both iOS and MacOS.*
39
40### Adding a file to the project.
41
42Because Lottie supports multiple distributions/platforms, adding a file to the project takes a couple of steps.
43
441. Add the new source file into the appropriate directory. Think about the new source file's purpose and what platform it will be available on.
452. After adding the new file, test that it can install and compile with CocoaPods. Navigate to `/Example` in terminal and run `pod install`. Afterwards open [lottie-swift.xcworkspace](/Example/lottie-swift.xcworkspace "lottie-swift.xcworkspace") and build all of the target platforms to ensure that nothing is broken.
463. Add the files to the Carthage build. Open [Lottie.xcodeproj](/Lottie.xcodeproj "Lottie.xcodeproj"). Add the new file to the project. **Uncheck Copy File when adding new files to this project**. Check the files Target Membership in the right panel and make sure it is added to the appropriate targets. There are two targets, on dynamic and one static, for each platform (iOS, tvOS, macOS). After adding the targets run through and build all of the targets.
474. Celebrate! You've done it!
48
49## After Effects Primer
50Before digging into Lottie, let's take a look at how After Effects builds animation. Lottie structures a lot of its render system in a similar way to After Effects.
51
52### Layers
53An After Effects `Composition` is a top level object that holds an animation timeline and several `Layers`. These `Layers` are different from Layers in iOS, which can describe any rendered frame. A `Layer` is more of a top level container, that holds its contents and can transform it. There are a couple of types of `Layer`, each with it's own unique contents. They are:
54
55- `Image layer`: Contains an image and a `Transform`
56- `Null Layer`: Contains only a`Transform`
57- `Shape Layer`: Contains a group of `Shape Objects`
58- `Solid Layer`: Contains a colored rectangle
59- `Text Layer`: Contains rendered `Text`
60- `Precomp Layer`: Contains another composition, with its own group of `Layers`
61
62A `Composition` has a timeline, and almost every property in After Effects can be keyframed to change over time. As the current time on a timeline is changed every property with keyframe data is interpolated and updated, creating animation.
63
64Each `Layer` has a `Transform` which transforms the layer's contents in space. A `Transform` can position, rotate, scale, skew, and change the opacity of a layer's contents. Each of these properties can be animated.
65
66`Layers` can be parented to another layer. A layer is affect not only by it's own `Transform` but also by its `Parent`. `Layers` also each hold a list of `Masks` that affect their appearance. `Masks` are bezier shape paths that are used to cut out portions of the layer.
67
68### Shape Layers
69The `Shape Layer` is the bread and butter of Lottie. A `Shape Layer` can hold several `Shape Objects`. You can think of a `Shape Object` as a single render instruction. At render time, each `Shape Object` is read in order and together create a rendering on screen. Each `Shape Object` has its own list of animatable properties that defines it's output. There are three basic classes of `Shape Objects`, each with a handful of subtypes.
70
71- **Path Generators**: Create bezier path data to be rendered and adds it to the current state. Some path generators are `Ellipse` `Rect` `Polystar`
72
73- **Modifiers**: Alters bezier path data from the current state. Some Modifiers are `Trim Path` `Merge Path` `Transform Path`
74
75- **Renderers**: Renders the current bezier path data on screen. Some Renderers are `Fill Path` `Stroke path`
76
77A set of `Shape Objects` are held in a `Group`, which can be nested inside other `Groups`. A `Shape Layer` can hold an unlimited number of `Shape Objects`
78
79## Lottie's Node System
80
81The simplest way to recreate After Effects' render system would be to nest all of the render instruction into a `CALayer` and have the layer draw it's contents. This wouldn't be very performant however, as a layer would have to redraw it's entire contents if there was even the slightest of updates. In fact this is how After Effects works, each frame is entirely redrawn when anything changes. After Affects can afford to work this way, as it is not a realtime renderer.
82
83Lottie works in a different way. Every renderable instruction is nested in it's own `CALayer`. Every frame a `Node Tree` determines which properties have updates, and only updates the affected layers. If a renderer doesn't have any updates, it is not redrawn. This greatly improves performance and allows for realtime rendering of animations.
84
85The Node System is designed to efficiently updates render contents. Additionally the Node System was designed to be as clean and composable as possible.
86
87### Animator Node
88
89![AnimatorNode](images/animatorNode.png)
90
91The `Animator Node` is a protocol that defines an object with a group of animatable properties. Every `Shape Object` is an `Animator Node`. A `Shape Layer` holds a linked-list tree of `Animator Nodes` that it updates each frame. `Animator Nodes` are not directly responsible for rendering on screen, they are only responsible for checking for updates and building the data used for rendering.
92
93An `Animator Node` will build it's output and put it into an `Output Node`. This node is later referenced by a `ShapeRenderLayer` which is responsible for rendering the output.
94
95Every frame a `Shape Layer` asks it's root `Animator Node` to update. The `Animator Node` check's if it has any updates, updates its properties if necessary, and then recursively updates it's parent `Animator Node`. If an `Animator Node` has any updates it will rebuild its outputs, and mark them as updates. At render time anything marked with an update is rendered.
96
97### Node Property Map
98
99![NodePropertyMap](images/nodePropertyMap.png)
100
101An `Animator Node` holds reference to a `Node Property Map`. The `Node Property Map` holds a list of `Node Property` objects, and is responsible for updating them each frame. Additionally the `Node Property Map` can map to its `Node Property` objects by a key.
102
103### Node Property
104
105![NodeProperty](images/nodeProperty.png)
106
107A `Node Property` holds both a `Value Provider` and a `Value Container`. During an update the `Node Property` will ask the `Value Provider` if it has an update. If it does the `Node Property` will get the new value from the `Value Provider` and store it in the `Value Container`. The property and the container will be marked as having an update.
108
109Additionally, the `Value Provider` of a `Node Property` can be dynamically changed, allowing animations to be altered at runtime.
110
111### Value Provider
112
113![ValueProvider](images/valueProvider.png)
114
115The `Value Provider` protocol defines a handful of methods for retrieving a typed value over time. Each frame a `Value Provider` is asked if it has an update, and then is asked for it's value.
116
117A `Value Provider` can be a list of keyframes that interpolates over time, a single unchanging value, or a dynamic object that is changed from outside of Lottie.
118
119### Value Container
120
121![ValueContainer](images/valueContainer.png)
122
123The `Value Container` holds reference to a single output value. The node is marked if it has been updated, and can be references from many sources. Ultimately an `Animator Node` will read the value from the container and build its output.
124
125### Output Node
126
127![OutputNode](images/outputNode.png)
128
129The `Output Node` holds reference to the final output of an `Animator Node`. `Output Node` objects are linked together into their own tree that is held by the render layers. After the `Animator Node` tree is updated the `Output Node` tree is used by the render layers to redraw it's contents. Every `Output Node` has a parent node and an `outputPath`. The `outputPath` is the sum of its parent's output and its out data.
130
131An `Output Node` generally falls into one of three types, which match the three classes of `Shape Objects` in After Effects.
132
133- `Path Output Node`: Holds a generated bezier path
134- `Path Modifier`: Modifies its input path and set the output.
135- `Renderer`: Holds instructions for rendering the current path data.
136
137
138## The Update Cycle
139
140![renderUpdate](images/renderMap.png)
141
142A `ShapeCompositionLayer` is a top level `CALayer` that holds a `Node Tree` and a `Render Container`. Each frame of animation the `ShapeCompositionLayer` is given a frame. Render updates happen in two passes:
143 1. The Node Tree is updated
144 2. The child Render Layers are updated.
145
146
147![nodeUpdate](images/nodeUpdate.png)
148
149*The Animator Node update cycle*
150
151This is the update cycle for a single `Animator Node`. When the `ShapeCompositionLayer` receives a frame it tells its root `Animator Node` to update with the frame. The `Animator Node` calls recursively upstream to start updates at the top of the tree. An `Animator Node` asks its `Node Property Map` if there are updates. The property map holds a list of `Node Property` objects. Each one is asked if it has an update. That call is passed through to the `Value Provider`, and also the `Value Container` if either return `true` then the property is marked for update. Next the `Node Property Map` loops through its properties and asks them to update if necessary. The `Node Property` asks its `Value Provider` for its value and then stores it in the `Value Container`.
152
153After all of the `Node Properties` have updates the `Animator Node` passes its update state down stream. Once the entire tree has updated its properties it starts to rebuild its outputs. Outputs are rebuilt from the bottom of the tree up to the top. If an `Animator Node` was marked as updated during its update pass it rebuilds its output. `updateOutputs` is called. Here an `Animator Node` executes its custom code for building its outputs. It reads the values of its properties `Value Container` and builds the output that is stored in its `Output Node`. Afterwards it calls up the tree to continue the update process.
154
155Once all of the nodes have marked themselves, and updated their outputs, the `ShapeCompositionLayer` moves on to the render side of the update.
156
157![renderUpdate](images/renderMap.png)
158
159*The Render Node update cycle*
160
161The `Shape Composition Layer` tells it's `Shape Container Layer` to mark it's updates. The `Shape Container Layer` loops through its child `Shape Render Layers`.
162
163Each `Shape Render Layer` holds reference to a `Renderer`. A `Renderer` is a type of `Output Node` that has render instructions in addition to an `outputPath`. The `Shape Render Layer` asks its renderer if there are updates for the frame. If the renderer returns `true` the `Shape Render Layer` calls `setNeedsDisplay` on itself which loops into `CALayer` update system.
164
165When `display` is called on the `Shape Render Layer` it asks its render for render instructions and the layer is redrawn.
166
167🎉🎉🎉
168
169## Current Animator Nodes
170
171### Modifier Nodes
172- `TrimPathNode`: Trims a collection of paths by a percentage of their length
173### Render Nodes
174- `FillNode`: Fills all input paths with a solid color
175- `StrokeNode`: Strokes all input paths with a solid color
176- `GradientFillNode`: Fills all input paths with a gradient color
177- `GradientStrokeNode`: Strokes all input paths with a gradient color
178### Path Nodes
179- `EllipseNode`: Generates an Ellipse Path
180- `PolygonNode`: Generates a Polygon Path with N sides
181- `RectNode`: Generates an Rectangular Path
182- `ShapeNode`: Generates an Path with bezier path data
183- `StarNode`: Generates an Star Path
184### Container Nodes
185- `GroupNode`: Holds and renders a group of node objects
186### Specialty Nodes
187- `TransformNode`: Supplies top level Layers with transforms
188- `TextAnimatorNode`: Supplies a Text Layer with its text contents
189
190## Example Animator Node
191
192For example, let us implement one of the simplest `Animator Nodes`, the `Fill Node`.
193
194The `Fill Node` is a node that renders a filled shape with a solid color. It only has a few properties: `color` `opacity` and `fillRule`.
195
196An `Animator Node` has a `Node Property Map` that maps its properties. Lets create a property map fot the `Fill Node`:
197
198```swift
199class FillNodeProperties: NodePropertyMap, KeypathSearchable {
200
201 var keypathName: String
202
203 init(fill: Fill) {
204 /// The node is initialized with a `Fill` model.
205 self.keypathName = fill.name
206 /// Create a Node Property with a group of Color Keyframes
207 self.color = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.color.keyframes))
208 /// Create a Node Property with a group of Float Keyframes
209 self.opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.opacity.keyframes))
210 /// Set the fill rule.
211 self.type = fill.fillRule
212 /// Make a key map of the properties, enabling dynamic property setting.
213 self.keypathProperties = [
214 "Opacity" : opacity,
215 "Color" : color
216 ]
217 /// Set the properties.
218 self.properties = Array(keypathProperties.values)
219 }
220
221 let opacity: NodeProperty<Vector1D>
222 let color: NodeProperty<Color>
223
224 let type: FillRule
225
226 let keypathProperties: [String : AnyNodeProperty]
227 let properties: [AnyNodeProperty]
228
229}
230```
231
232Now we have created a robust property map for our Fill Node.
233An `Animator Node` also needs an `OutputNode`. The Fill Node has a Renderer output type, that renders path objects with a fill. Let's create the Renderer `OutputNode`
234```Swift
235/// An OutputNode that holds render instructions for Fill
236class FillRenderer: PassThroughOutputNode, Renderable {
237
238 /// A Render Node can either update a CAShapeLayer, or render directly into a context.
239 /// This node can accomplish its rendering with a CAShapeLayer
240 let shouldRenderInContext: Bool = false
241
242 /// Output Node Properties. Notice how setting these properties sets hasUpdate to `true`
243
244 /// The fill color.
245 var color: CGColor? {
246 didSet {
247 hasUpdate = true
248 }
249 }
250
251 /// The fill opacity.
252 var opacity: CGFloat = 0 {
253 didSet {
254 hasUpdate = true
255 }
256 }
257
258 //// The fill rule.
259 var fillRule: FillRule = .none {
260 didSet {
261 hasUpdate = true
262 }
263 }
264
265 /// The function that is called when render updates happen.
266 func updateShapeLayer(layer: CAShapeLayer) {
267 layer.fillColor = color
268 layer.opacity = Float(opacity)
269 layer.fillRule = fillRule.caFillRule
270 /// Clear the update flag. The job is done.
271 hasUpdate = false
272 }
273
274 /// Optional, the context renderer.
275 /// setting shouldRenderInContext to `true` would cause this method to be called.
276 func render(_ inContext: CGContext) {
277 guard inContext.path != nil && inContext.path!.isEmpty == false else {
278 return
279 }
280 guard let color = color else { return }
281 hasUpdate = false
282 inContext.setAlpha(opacity * 0.01)
283 inContext.setFillColor(color)
284 inContext.fillPath(using: fillRule.cgFillRule)
285 }
286}
287```
288
289Now we have a `Renderer` `OutputNode` capable of rendering a fill. We are now ready to create our `FillNode`
290
291```swift
292
293/// An `Animator Node` capable of fill rendering.
294class FillNode: AnimatorNode, RenderNode {
295
296 /// The fill renderer.
297 let fillRender: FillRenderer
298
299 /// Protocol RenderNode requires a `Renderable`
300 var renderer: NodeOutput & Renderable {
301 return fillRender
302 }
303
304 /// The Fill properties.
305 let fillProperties: FillNodeProperties
306
307 /// Initialized with a `Fill` model.
308 init(parentNode: AnimatorNode?, fill: Fill) {
309 /// Create the Renderer
310 self.fillRender = FillRenderer(parent: parentNode?.outputNode)
311 /// Create the Properties
312 self.fillProperties = FillNodeProperties(fill: fill)
313 /// Set the upstream parent node.
314 self.parentNode = parentNode
315 }
316
317 // MARK: Animator Node Protocol
318
319 var propertyMap: NodePropertyMap & KeypathSearchable {
320 return fillProperties
321 }
322
323 let parentNode: AnimatorNode?
324 var hasLocalUpdates: Bool = false
325 var hasUpstreamUpdates: Bool = false
326 var lastUpdateFrame: CGFloat? = nil
327
328 /// Changes to this node do not affect downstream nodes.
329 func localUpdatesPermeateDownstream() -> Bool {
330 return false
331 }
332
333 /// Set up the renderer.
334 func rebuildOutputs(frame: CGFloat) {
335 fillRender.color = fillProperties.color.value.cgColorValue
336 fillRender.opacity = fillProperties.opacity.value.cgFloatValue * 0.01
337 fillRender.fillRule = fillProperties.type
338 }
339
340}
341```
342
343And that's that!
344Now, when connected to a Node Tree, the fill node will render its contents only when its contents, or its upstream nodes, have updated.
345
346
347
348<!--stackedit_data:
349eyJoaXN0b3J5IjpbNDAyMzc2OTcwLC02MjE5NjI3MDEsMTg0OT
350c1ODQ2Niw0ODc1MjM0NzAsLTEwNzk3Mjk1NzksLTE5Mjc4OTQ3
351OTEsMTg1MDI5NDQyNywxNTI2MjA4OTY3LC01Mzg5NzExMjIsLT
352gyNzI5NjA5NSwtMTk3ODAwNjA4NywzNjU5MjYzMDBdfQ==
353-->