xref: /aosp_15_r20/external/skia/modules/skottie/src/text/TextAnimator.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2019 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "modules/skottie/src/text/TextAnimator.h"
9 
10 #include "include/core/SkColor.h"
11 #include "include/private/base/SkCPUTypes.h"
12 #include "modules/skottie/src/SkottieValue.h"
13 #include "modules/skottie/src/animator/Animator.h"
14 #include "modules/skottie/src/text/RangeSelector.h"
15 #include "src/base/SkVx.h"
16 #include "src/core/SkSwizzlePriv.h"
17 #include "src/utils/SkJSON.h"
18 
19 #include <algorithm>
20 #include <cmath>
21 #include <cstdint>
22 #include <utility>
23 
24 namespace skottie {
25 namespace internal {
26 
27 /*
28  * Text layers can have optional text property animators.
29  *
30  * Each animator consists of
31  *
32  *   1) a list of animated properties (e.g. position, fill color, etc)
33  *
34  *   2) a list of range selectors
35  *
36  * Animated properties yield new values to be applied to the text, while range selectors
37  * determine the text subset these new values are applied to.
38  *
39  * The best way to think of range selectors is in terms of coverage: they combine to generate
40  * a coverage value [0..1] for each text fragment/glyph.  This coverage is then used to modulate
41  * how the new property value is applied to a given fragment (interpolation weight).
42  *
43  * Note: Bodymovin currently only supports a single selector.
44  *
45  * JSON structure:
46  *
47  * "t": {              // text node
48  *   "a": [            // animators list
49  *     {               // animator node
50  *       "s": {...},   // selector node
51  *       "a": {        // animator properties node
52  *         "a":  {}    // optional anchor point value
53  *         "p":  {},   // optional position value
54  *         "s":  {},   // optional scale value
55  *         "o":  {},   // optional opacity
56  *         "fc": {},   // optional fill color value
57  *         "sc": {},   // optional stroke color value
58  *
59  *         // TODO: more props?
60  *       }
61  *     },
62  *     ...
63  *   ],
64  *   ...
65  * }
66  */
Make(const skjson::ObjectValue * janimator,const AnimationBuilder * abuilder,AnimatablePropertyContainer * acontainer)67 sk_sp<TextAnimator> TextAnimator::Make(const skjson::ObjectValue* janimator,
68                                        const AnimationBuilder* abuilder,
69                                        AnimatablePropertyContainer* acontainer) {
70     if (!janimator) {
71         return nullptr;
72     }
73 
74     const skjson::ObjectValue* jprops = (*janimator)["a"];
75     if (!jprops) {
76         return nullptr;
77     }
78 
79     std::vector<sk_sp<RangeSelector>> selectors;
80 
81     // Depending on compat mode and whether more than one selector is present,
82     // BM exports either an array or a single object.
83     if (const skjson::ArrayValue* jselectors = (*janimator)["s"]) {
84         selectors.reserve(jselectors->size());
85         for (const skjson::ObjectValue* jselector : *jselectors) {
86             if (auto sel = RangeSelector::Make(*jselector, abuilder, acontainer)) {
87                 selectors.push_back(std::move(sel));
88             }
89         }
90     } else {
91         if (auto sel = RangeSelector::Make((*janimator)["s"], abuilder, acontainer)) {
92             selectors.reserve(1);
93             selectors.push_back(std::move(sel));
94         }
95     }
96 
97     return sk_sp<TextAnimator>(
98                 new TextAnimator(std::move(selectors), *jprops, abuilder, acontainer));
99 }
100 
modulateProps(const DomainMaps & maps,ModulatorBuffer & buf) const101 void TextAnimator::modulateProps(const DomainMaps& maps, ModulatorBuffer& buf) const {
102     // No selectors -> full coverage.
103     const auto initial_coverage = fSelectors.empty() ? 1.f : 0.f;
104 
105     // Coverage is scoped per animator.
106     for (auto& mod : buf) {
107         mod.coverage = initial_coverage;
108     }
109 
110     // Accumulate selector coverage.
111     for (const auto& selector : fSelectors) {
112         selector->modulateCoverage(maps, buf);
113     }
114 
115     // Modulate animated props.
116     for (auto& mod : buf) {
117         mod.props = this->modulateProps(mod.props, mod.coverage);
118     }
119 }
120 
modulateProps(const ResolvedProps & props,float amount) const121 TextAnimator::ResolvedProps TextAnimator::modulateProps(const ResolvedProps& props,
122                                                         float amount) const {
123     auto modulated_props = props;
124 
125     // Transform props compose.
126     modulated_props.position += static_cast<SkV3>(fTextProps.position) * amount;
127     modulated_props.rotation += fTextProps.rotation * amount;
128     modulated_props.tracking += fTextProps.tracking * amount;
129     modulated_props.scale    *= SkV3{1,1,1} +
130             (static_cast<SkV3>(fTextProps.scale) * 0.01f - SkV3{1,1,1}) * amount;
131 
132     // ... as do blur, line spacing, and stroke width.
133     modulated_props.blur         += fTextProps.blur         * amount;
134     modulated_props.line_spacing += fTextProps.line_spacing * amount;
135     modulated_props.stroke_width += fTextProps.stroke_width * amount;
136 
137     const auto lerp = [](float v0, float v1, float t) {
138         return v0 + (v1 - v0)*t;
139     };
140     const auto lerp_color = [](SkColor c0, SkColor c1, float t) {
141         const auto c0_4f = Sk4f_fromL32(c0),
142                    c1_4f = Sk4f_fromL32(c1),
143                     c_4f = c0_4f + (c1_4f - c0_4f) * t;
144 
145         return Sk4f_toL32(c_4f);
146     };
147 
148     // Colors and opacity are interpolated, and use a clamped amount value.
149     const auto clamped_amount = std::max(amount, 0.0f);
150     if (fHasFillColor) {
151         modulated_props.fill_color = lerp_color(props.fill_color,
152                                                 fTextProps.fill_color,
153                                                 clamped_amount);
154     }
155     if (fHasStrokeColor) {
156         modulated_props.stroke_color = lerp_color(props.stroke_color,
157                                                   fTextProps.stroke_color,
158                                                   clamped_amount);
159     }
160     if (fHasFillOpacity) {
161         // 255-based
162         const auto alpha = lerp(SkColorGetA(props.fill_color),
163                                 fTextProps.fill_opacity*2.55f,
164                                 clamped_amount);
165         modulated_props.fill_color = SkColorSetA(modulated_props.fill_color,
166                                                  static_cast<U8CPU>(std::round(alpha)));
167     }
168     if (fHasStrokeOpacity) {
169         // 255-based
170         const auto alpha = lerp(SkColorGetA(props.stroke_color),
171                                 fTextProps.stroke_opacity*2.55f,
172                                 clamped_amount);
173         modulated_props.stroke_color = SkColorSetA(modulated_props.stroke_color,
174                                                    static_cast<U8CPU>(std::round(alpha)));
175     }
176     if (fHasOpacity) {
177         modulated_props.opacity = lerp(props.opacity, fTextProps.opacity*0.01f, clamped_amount);
178     }
179 
180     return modulated_props;
181 }
182 
TextAnimator(std::vector<sk_sp<RangeSelector>> && selectors,const skjson::ObjectValue & jprops,const AnimationBuilder * abuilder,AnimatablePropertyContainer * acontainer)183 TextAnimator::TextAnimator(std::vector<sk_sp<RangeSelector>>&& selectors,
184                            const skjson::ObjectValue& jprops,
185                            const AnimationBuilder* abuilder,
186                            AnimatablePropertyContainer* acontainer)
187     : fSelectors(std::move(selectors))
188     , fRequiresAnchorPoint(false)
189     , fRequiresLineAdjustments(false) {
190 
191     acontainer->bind(*abuilder, jprops["p" ], fTextProps.position);
192 
193     // Tracking and line spacing affect all line fragments.
194     fRequiresLineAdjustments |= acontainer->bind(*abuilder, jprops["t" ], fTextProps.tracking);
195     fRequiresLineAdjustments |= acontainer->bind(*abuilder, jprops["ls"], fTextProps.line_spacing);
196 
197     // Scale and rotation are anchor-point-dependent.
198     fRequiresAnchorPoint |= acontainer->bind(*abuilder, jprops["s"], fTextProps.scale);
199 
200     // Depending on whether we're in 2D/3D mode, some of these will stick and some will not.
201     // It's fine either way.
202     fRequiresAnchorPoint |= acontainer->bind(*abuilder, jprops["rx"], fTextProps.rotation.x);
203     fRequiresAnchorPoint |= acontainer->bind(*abuilder, jprops["ry"], fTextProps.rotation.y);
204     fRequiresAnchorPoint |= acontainer->bind(*abuilder, jprops["r" ], fTextProps.rotation.z);
205 
206     fHasFillColor     = acontainer->bind(*abuilder, jprops["fc"], fTextProps.fill_color    );
207     fHasStrokeColor   = acontainer->bind(*abuilder, jprops["sc"], fTextProps.stroke_color  );
208     fHasFillOpacity   = acontainer->bind(*abuilder, jprops["fo"], fTextProps.fill_opacity  );
209     fHasStrokeOpacity = acontainer->bind(*abuilder, jprops["so"], fTextProps.stroke_opacity);
210     fHasOpacity       = acontainer->bind(*abuilder, jprops["o" ], fTextProps.opacity       );
211     fHasBlur          = acontainer->bind(*abuilder, jprops["bl"], fTextProps.blur          );
212 
213     acontainer->bind(*abuilder, jprops["sw"], fTextProps.stroke_width);
214 }
215 
216 } // namespace internal
217 } // namespace skottie
218