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