LottieAnimation.swift 5.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
//
//  LottieAnimation.swift
//  lottie-swift
//
//  Created by Brandon Withrow on 1/7/19.
//

import Foundation

// MARK: - CoordinateSpace

public enum CoordinateSpace: Int, Codable, Sendable {
  case type2d
  case type3d
}

// MARK: - LottieAnimation

/// The `LottieAnimation` model is the top level model object in Lottie.
///
/// A `LottieAnimation` holds all of the animation data backing a Lottie Animation.
/// Codable, see JSON schema [here](https://github.com/airbnb/lottie-web/tree/master/docs/json).
public final class LottieAnimation: Codable, Sendable, DictionaryInitializable {

  // MARK: Lifecycle

  required public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: LottieAnimation.CodingKeys.self)
    version = try container.decode(String.self, forKey: .version)
    type = try container.decodeIfPresent(CoordinateSpace.self, forKey: .type) ?? .type2d
    startFrame = try container.decode(AnimationFrameTime.self, forKey: .startFrame)
    endFrame = try container.decode(AnimationFrameTime.self, forKey: .endFrame)
    framerate = try container.decode(Double.self, forKey: .framerate)
    width = try container.decode(Double.self, forKey: .width)
    height = try container.decode(Double.self, forKey: .height)
    layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
    glyphs = try container.decodeIfPresent([Glyph].self, forKey: .glyphs)
    fonts = try container.decodeIfPresent(FontList.self, forKey: .fonts)
    assetLibrary = try container.decodeIfPresent(AssetLibrary.self, forKey: .assetLibrary)
    markers = try container.decodeIfPresent([Marker].self, forKey: .markers)

    if let markers {
      var markerMap: [String: Marker] = [:]
      for marker in markers {
        markerMap[marker.name] = marker
      }
      self.markerMap = markerMap
    } else {
      markerMap = nil
    }
  }

  public init(dictionary: [String: Any]) throws {
    version = try dictionary.value(for: CodingKeys.version)
    if
      let typeRawValue = dictionary[CodingKeys.type.rawValue] as? Int,
      let type = CoordinateSpace(rawValue: typeRawValue)
    {
      self.type = type
    } else {
      type = .type2d
    }
    startFrame = try dictionary.value(for: CodingKeys.startFrame)
    endFrame = try dictionary.value(for: CodingKeys.endFrame)
    framerate = try dictionary.value(for: CodingKeys.framerate)
    width = try dictionary.value(for: CodingKeys.width)
    height = try dictionary.value(for: CodingKeys.height)
    let layerDictionaries: [[String: Any]] = try dictionary.value(for: CodingKeys.layers)
    layers = try [LayerModel].fromDictionaries(layerDictionaries)
    if let glyphDictionaries = dictionary[CodingKeys.glyphs.rawValue] as? [[String: Any]] {
      glyphs = try glyphDictionaries.map { try Glyph(dictionary: $0) }
    } else {
      glyphs = nil
    }
    if let fontsDictionary = dictionary[CodingKeys.fonts.rawValue] as? [String: Any] {
      fonts = try FontList(dictionary: fontsDictionary)
    } else {
      fonts = nil
    }
    if let assetLibraryDictionaries = dictionary[CodingKeys.assetLibrary.rawValue] as? [[String: Any]] {
      assetLibrary = try AssetLibrary(value: assetLibraryDictionaries)
    } else {
      assetLibrary = nil
    }
    if let markerDictionaries = dictionary[CodingKeys.markers.rawValue] as? [[String: Any]] {
      let markers = try markerDictionaries.map { try Marker(dictionary: $0) }
      var markerMap: [String: Marker] = [:]
      for marker in markers {
        markerMap[marker.name] = marker
      }
      self.markers = markers
      self.markerMap = markerMap
    } else {
      markers = nil
      markerMap = nil
    }
  }

  // MARK: Public

  /// The start time of the composition in frameTime.
  public let startFrame: AnimationFrameTime

  /// The end time of the composition in frameTime.
  public let endFrame: AnimationFrameTime

  /// The frame rate of the composition.
  public let framerate: Double

  /// Return all marker names, in order, or an empty list if none are specified
  public var markerNames: [String] {
    guard let markers else { return [] }
    return markers.map { $0.name }
  }

  // MARK: Internal

  enum CodingKeys: String, CodingKey {
    case version = "v"
    case type = "ddd"
    case startFrame = "ip"
    case endFrame = "op"
    case framerate = "fr"
    case width = "w"
    case height = "h"
    case layers
    case glyphs = "chars"
    case fonts
    case assetLibrary = "assets"
    case markers
  }

  /// The version of the JSON Schema.
  let version: String

  /// The coordinate space of the composition.
  let type: CoordinateSpace

  /// The height of the composition in points.
  let width: Double

  /// The width of the composition in points.
  let height: Double

  /// The list of animation layers
  let layers: [LayerModel]

  /// The list of glyphs used for text rendering
  let glyphs: [Glyph]?

  /// The list of fonts used for text rendering
  let fonts: FontList?

  /// Asset Library
  let assetLibrary: AssetLibrary?

  /// Markers
  let markers: [Marker]?
  let markerMap: [String: Marker]?

  /// The marker to use if "reduced motion" is enabled.
  /// Supported marker names are case insensitive, and include:
  ///  - reduced motion
  ///  - reducedMotion
  ///  - reduced_motion
  ///  - reduced-motion
  var reducedMotionMarker: Marker? {
    let allowedReducedMotionMarkerNames = Set([
      "reduced motion",
      "reduced_motion",
      "reduced-motion",
      "reducedmotion",
    ])

    return markers?.first(where: { marker in
      allowedReducedMotionMarkerNames.contains(marker.name.lowercased())
    })
  }
}