Skip to content

Commit

Permalink
Add support for opacity text animators
Browse files Browse the repository at this point in the history
  • Loading branch information
calda committed Jun 17, 2024
1 parent 0f00e64 commit d30a60b
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 35 deletions.
18 changes: 14 additions & 4 deletions Example/Example/AnimationPreviewView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ struct AnimationPreviewView: View {
currentRenderingEngine = animationView.currentRenderingEngine
}
}
.getRealtimeAnimationProgress(animationPlaying ? $sliderValue : nil)
// TODO: REVERT
// .getRealtimeAnimationProgress(animationPlaying ? $sliderValue : nil)
.getRealtimeAnimationFrame(animationPlaying ? $currentFrame : nil)

Spacer()

HStack {
#if !os(tvOS)
Slider(value: $sliderValue, in: 0...1, onEditingChanged: { editing in
Slider(value: $currentFrame, in: 0...300, step: 1, onEditingChanged: { editing in
if animationPlaying, editing {
animationPlaying = false
}
Expand All @@ -93,6 +95,13 @@ struct AnimationPreviewView: View {
Image(systemName: "play.fill")
}
}

// TODO: REVERT

Spacer(minLength: 16)

Text("\(Int(currentFrame))")
.frame(minWidth: 30)
}
.padding(.all, 16)
}
Expand All @@ -115,7 +124,8 @@ struct AnimationPreviewView: View {
private let urls: [URL]

@State private var animationPlaying = true
@State private var sliderValue: AnimationProgressTime = 0
// @State private var sliderValue: AnimationProgressTime = 0
@State private var currentFrame: AnimationFrameTime = 0
@State private var currentURLIndex: Int
@State private var renderingEngine: RenderingEngineOption = .automatic
@State private var loopMode: LottieLoopMode = .loop
Expand All @@ -127,7 +137,7 @@ struct AnimationPreviewView: View {
if animationPlaying {
.playing(.fromProgress(playFromProgress, toProgress: playToProgress, loopMode: loopMode))
} else {
.paused(at: .progress(sliderValue))
.paused(at: .frame(currentFrame))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,20 +133,26 @@ final class TextCompositionLayer: CompositionLayer {
let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue
let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
let start = rootNode?.textOutputNode.start.flatMap { Int($0) }
let end = rootNode?.textOutputNode.end.flatMap { Int($0) }
let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
let start = rootNode?.textOutputNode.start.flatMap { Int($0) }
let end = rootNode?.textOutputNode.end.flatMap { Int($0) }
let selectedRangeOpacity = rootNode?.textOutputNode.selectedRangeOpacity
let textRangeUnit = rootNode?.textAnimatorProperties.textRangeUnit

// Set all of the text layer options
textLayer.text = textString
textLayer.start = start
textLayer.end = end
textLayer.font = ctFont
textLayer.alignment = text.justification.textAlignment
textLayer.lineHeight = CGFloat(text.lineHeight)
textLayer.tracking = tracking

// Configure the text animators
textLayer.start = start
textLayer.end = end
textLayer.textRangeUnit = textRangeUnit
textLayer.selectedRangeOpacity = selectedRangeOpacity

if let fillColor = rootNode?.textOutputNode.fillColor {
textLayer.fillColor = fillColor
} else if let fillColor = text.fillColorData?.cgColorValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,86 +31,104 @@ final class CoreTextRenderLayer: CALayer {
}
}

public var start: Int? {
public var font: CTFont? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var end: Int? {
public var alignment = NSTextAlignment.left {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var font: CTFont? {
public var lineHeight: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var alignment: NSTextAlignment = .left {
public var tracking: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var lineHeight: CGFloat = 0 {
public var fillColor: CGColor? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var tracking: CGFloat = 0 {
public var strokeColor: CGColor? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var fillColor: CGColor? {
public var strokeWidth: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var strokeColor: CGColor? {
public var strokeOnTop = false {
didSet {
setNeedsLayout()
setNeedsDisplay()
}
}

public var preferredSize: CGSize? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var strokeWidth: CGFloat = 0 {
public var start: Int? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var strokeOnTop = false {
public var end: Int? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

public var preferredSize: CGSize? {
/// The type of unit to use when computing the `start` / `end` range within the text string
public var textRangeUnit: TextRangeUnit? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}

/// The opacity to apply to the range between `start` and `end`
public var selectedRangeOpacity: CGFloat? {
didSet {
needsContentUpdate = true
setNeedsLayout()
Expand Down Expand Up @@ -192,8 +210,8 @@ final class CoreTextRenderLayer: CALayer {

// MARK: Private

private var drawingRect: CGRect = .zero
private var drawingAnchor: CGPoint = .zero
private var drawingRect = CGRect.zero
private var drawingAnchor = CGPoint.zero
private var fillFrameSetter: CTFramesetter?
private var attributedString: NSAttributedString?
private var strokeFrameSetter: CTFramesetter?
Expand Down Expand Up @@ -277,9 +295,54 @@ final class CoreTextRenderLayer: CALayer {

let attrString = NSMutableAttributedString(string: text, attributes: attributes)

// Proof of concept showing that we're at least able to apply different attributed string stylings each frame:
if let start {
attrString.addAttribute(NSAttributedString.Key.foregroundColor, value: CGColor.rgba(0, 0, 0, 0), range: NSRange(location: start, length: text.count - start))
// Apply the text animator within between the `start` and `end` indices
if let start, let end, let selectedRangeOpacity {
// The start and end of a text animator refer to the portions of the text
// where that animator is applies. In the schema these can be represented
// in absolute index value, or as percentages relative to the dynamic string length.
var startIndex: Int
var endIndex: Int

switch textRangeUnit ?? .percentage {
case .index:
startIndex = start
endIndex = end

case .percentage:
let startPercentage = Double(start) / 100
let endPercentage = Double(end) / 100

startIndex = Int(round(Double(attrString.length) * startPercentage))
endIndex = Int(round(Double(attrString.length) * endPercentage))
}

// Carefully cap the indices, since passing invalid indices
// to `NSAttributedString` will crash the app.
startIndex = startIndex.clamp(0, attrString.length)
endIndex = endIndex.clamp(0, attrString.length)

// Make sure the end index actually comes after the start index
if endIndex < startIndex {
swap(&startIndex, &endIndex)
}

// Apply the `selectedRangeOpacity` to the current `fillColor` if provided
let textRangeColor: CGColor
if let fillColor {
if let (r, g, b) = fillColor.rgb {
textRangeColor = .rgba(r, g, b, selectedRangeOpacity)
} else {
LottieLogger.shared.warn("Could not convert color \(fillColor) to RGB values.")
textRangeColor = .rgba(0, 0, 0, selectedRangeOpacity)
}
} else {
textRangeColor = .rgba(0, 0, 0, selectedRangeOpacity)
}

attrString.addAttribute(
NSAttributedString.Key.foregroundColor,
value: textRangeColor,
range: NSRange(location: startIndex, length: endIndex - startIndex))
}

attributedString = attrString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {

init(textAnimator: TextAnimator) {
keypathName = textAnimator.name
textRangeUnit = textAnimator.textRangeUnit
var properties = [String : AnyNodeProperty]()

if let keyframeGroup = textAnimator.anchor {
Expand Down Expand Up @@ -123,6 +124,13 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
end = nil
}

if let selectedRangeOpacityKeyframes = textAnimator.opacity {
selectedRangeOpacity = NodeProperty(provider: KeyframeInterpolator(keyframes: selectedRangeOpacityKeyframes.keyframes))
properties["SelectedRangeOpacity"] = selectedRangeOpacity
} else {
selectedRangeOpacity = nil
}

keypathProperties = properties

self.properties = Array(keypathProperties.values)
Expand All @@ -147,6 +155,8 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
let tracking: NodeProperty<LottieVector1D>?
let start: NodeProperty<LottieVector1D>?
let end: NodeProperty<LottieVector1D>?
let selectedRangeOpacity: NodeProperty<LottieVector1D>?
let textRangeUnit: TextRangeUnit?

let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
Expand Down Expand Up @@ -257,6 +267,15 @@ final class TextOutputNode: NodeOutput {
}
}

var selectedRangeOpacity: CGFloat? {
get {
_selectedRangeOpacity
}
set {
_selectedRangeOpacity = newValue
}
}

func hasOutputUpdates(_: CGFloat) -> Bool {
// TODO Fix This
true
Expand All @@ -272,6 +291,7 @@ final class TextOutputNode: NodeOutput {
fileprivate var _strokeWidth: CGFloat?
fileprivate var _start: CGFloat?
fileprivate var _end: CGFloat?
fileprivate var _selectedRangeOpacity: CGFloat?
}

// MARK: - TextAnimatorNode
Expand Down Expand Up @@ -314,12 +334,13 @@ class TextAnimatorNode: AnimatorNode {

func rebuildOutputs(frame _: CGFloat) {
textOutputNode.xform = textAnimatorProperties.caTransform
textOutputNode.opacity = 1.0//(textAnimatorProperties.opacity?.value.cgFloatValue ?? 100) * 0.01
textOutputNode.opacity = 1.0
textOutputNode.strokeColor = textAnimatorProperties.strokeColor?.value.cgColorValue
textOutputNode.fillColor = textAnimatorProperties.fillColor?.value.cgColorValue
textOutputNode.tracking = textAnimatorProperties.tracking?.value.cgFloatValue ?? 1
textOutputNode.strokeWidth = textAnimatorProperties.strokeWidth?.value.cgFloatValue ?? 0
textOutputNode.start = textAnimatorProperties.start?.value.cgFloatValue
textOutputNode.end = textAnimatorProperties.end?.value.cgFloatValue
textOutputNode.selectedRangeOpacity = (textAnimatorProperties.opacity?.value.cgFloatValue).flatMap { $0 * 0.01 }
}
}
20 changes: 20 additions & 0 deletions Sources/Private/Model/Text/TextAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
// Created by Brandon Withrow on 1/9/19.
//

// MARK: - TextRangeUnit

enum TextRangeUnit: Int, RawRepresentable, Codable {
case percentage = 1
case index = 2
}

// MARK: - TextAnimator

final class TextAnimator: Codable, DictionaryInitializable {

// MARK: Lifecycle
Expand Down Expand Up @@ -37,6 +46,7 @@ final class TextAnimator: Codable, DictionaryInitializable {
let selectorContainer = try? container.nestedContainer(keyedBy: TextSelectorKeys.self, forKey: .textSelector)
start = try? selectorContainer?.decodeIfPresent(KeyframeGroup<LottieVector1D>.self, forKey: .start)
end = try? selectorContainer?.decodeIfPresent(KeyframeGroup<LottieVector1D>.self, forKey: .start)
textRangeUnit = try? selectorContainer?.decodeIfPresent(TextRangeUnit.self, forKey: .textRangeUnits)
}

init(dictionary: [String: Any]) throws {
Expand Down Expand Up @@ -127,6 +137,12 @@ final class TextAnimator: Codable, DictionaryInitializable {
} else {
end = nil
}

if let textRangeUnitValue = selectorDictionary[TextSelectorKeys.textRangeUnits.rawValue] as? Int {
textRangeUnit = TextRangeUnit(rawValue: textRangeUnitValue)
} else {
textRangeUnit = nil
}
}

// MARK: Internal
Expand Down Expand Up @@ -178,6 +194,9 @@ final class TextAnimator: Codable, DictionaryInitializable {
/// End
let end: KeyframeGroup<LottieVector1D>?

/// The type of unit used by the start/end ranges
let textRangeUnit: TextRangeUnit?

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var animatorContainer = container.nestedContainer(keyedBy: TextAnimatorKeys.self, forKey: .textAnimator)
Expand All @@ -199,6 +218,7 @@ final class TextAnimator: Codable, DictionaryInitializable {
case start = "s"
case end = "e"
case offset = "o"
case textRangeUnits = "r"
}

private enum TextAnimatorKeys: String, CodingKey {
Expand Down
Loading

0 comments on commit d30a60b

Please sign in to comment.