1 | # Lottie-iOS Render System Documentation
|
2 |
|
3 | The purpose of this document is to explain how Lottie's render system works.
|
4 |
|
5 | For questions reach out to the author: [Brandon Withrow](https://twitter.com/theWithra)
|
6 |
|
7 | ## Check Before Submitting
|
8 |
|
9 | Before 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 |
|
22 | Lottie 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 |
|
24 | For example, `UIKit` is only available on iOS, whereas MacOS uses `AppKit`.
|
25 |
|
26 | ### Source Code Directory Structure
|
27 | All 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 |
|
42 | Because Lottie supports multiple distributions/platforms, adding a file to the project takes a couple of steps.
|
43 |
|
44 | 1. 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.
|
45 | 2. 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.
|
46 | 3. 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.
|
47 | 4. Celebrate! You've done it!
|
48 |
|
49 | ## After Effects Primer
|
50 | Before 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
|
53 | An 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 |
|
62 | A `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 |
|
64 | Each `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
|
69 | The `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 |
|
77 | A 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 |
|
81 | The 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 |
|
83 | Lottie 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 |
|
85 | The 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 |
|
91 | The `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 |
|
93 | An `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 |
|
95 | Every 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 |
|
101 | An `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 |
|
107 | A `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 |
|
109 | Additionally, 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 |
|
115 | The `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 |
|
117 | A `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 |
|
123 | The `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 |
|
129 | The `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 |
|
131 | An `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 |
|
142 | A `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 |
|
151 | This 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 |
|
153 | After 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 |
|
155 | Once 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 |
|
161 | The `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 |
|
163 | Each `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 |
|
165 | When `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 |
|
192 | For example, let us implement one of the simplest `Animator Nodes`, the `Fill Node`.
|
193 |
|
194 | The `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 |
|
196 | An `Animator Node` has a `Node Property Map` that maps its properties. Lets create a property map fot the `Fill Node`:
|
197 |
|
198 | ```swift
|
199 | class 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 |
|
232 | Now we have created a robust property map for our Fill Node.
|
233 | An `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
|
236 | class 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 |
|
289 | Now 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.
|
294 | class 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 |
|
343 | And that's that!
|
344 | Now, 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 |
|
349 | eyJoaXN0b3J5IjpbNDAyMzc2OTcwLC02MjE5NjI3MDEsMTg0OT
|
350 | c1ODQ2Niw0ODc1MjM0NzAsLTEwNzk3Mjk1NzksLTE5Mjc4OTQ3
|
351 | OTEsMTg1MDI5NDQyNywxNTI2MjA4OTY3LC01Mzg5NzExMjIsLT
|
352 | gyNzI5NjA5NSwtMTk3ODAwNjA4NywzNjU5MjYzMDBdfQ==
|
353 | -->
|