• 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/TextAdapter.h"
9 
10 #include "include/core/SkFontMgr.h"
11 #include "modules/skottie/src/text/TextAnimator.h"
12 #include "modules/sksg/include/SkSGDraw.h"
13 #include "modules/sksg/include/SkSGGroup.h"
14 #include "modules/sksg/include/SkSGPaint.h"
15 #include "modules/sksg/include/SkSGRect.h"
16 #include "modules/sksg/include/SkSGText.h"
17 #include "modules/sksg/include/SkSGTransform.h"
18 
19 namespace skottie {
20 namespace internal {
21 
TextAdapter(sk_sp<sksg::Group> root,sk_sp<SkFontMgr> fontmgr,sk_sp<Logger> logger,bool hasAnimators)22 TextAdapter::TextAdapter(sk_sp<sksg::Group> root, sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger,
23                          bool hasAnimators)
24     : fRoot(std::move(root))
25     , fFontMgr(std::move(fontmgr))
26     , fLogger(std::move(logger))
27     , fHasAnimators(hasAnimators) {}
28 
29 TextAdapter::~TextAdapter() = default;
30 
addFragment(const Shaper::Fragment & frag)31 void TextAdapter::addFragment(const Shaper::Fragment& frag) {
32     // For a given shaped fragment, build a corresponding SG fragment:
33     //
34     //   [TransformEffect] -> [Transform]
35     //     [Group]
36     //       [Draw] -> [TextBlob*] [FillPaint]
37     //       [Draw] -> [TextBlob*] [StrokePaint]
38     //
39     // * where the blob node is shared
40 
41     auto blob_node = sksg::TextBlob::Make(frag.fBlob);
42 
43     FragmentRec rec;
44     rec.fOrigin = frag.fPos;
45     rec.fMatrixNode = sksg::Matrix<SkMatrix>::Make(SkMatrix::MakeTrans(frag.fPos.x(),
46                                                                        frag.fPos.y()));
47 
48     std::vector<sk_sp<sksg::RenderNode>> draws;
49     draws.reserve(static_cast<size_t>(fText.fHasFill) + static_cast<size_t>(fText.fHasStroke));
50 
51     SkASSERT(fText.fHasFill || fText.fHasStroke);
52 
53     if (fText.fHasFill) {
54         rec.fFillColorNode = sksg::Color::Make(fText.fFillColor);
55         rec.fFillColorNode->setAntiAlias(true);
56         draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
57     }
58     if (fText.fHasStroke) {
59         rec.fStrokeColorNode = sksg::Color::Make(fText.fStrokeColor);
60         rec.fStrokeColorNode->setAntiAlias(true);
61         rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
62         draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
63     }
64 
65     SkASSERT(!draws.empty());
66 
67     auto draws_node = (draws.size() > 1)
68             ? sksg::Group::Make(std::move(draws))
69             : std::move(draws[0]);
70 
71     fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
72     fFragments.push_back(std::move(rec));
73 }
74 
buildDomainMaps(const Shaper::Result & shape_result)75 void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
76     fMaps.fNonWhitespaceMap.clear();
77     fMaps.fWordsMap.clear();
78     fMaps.fLinesMap.clear();
79 
80     size_t i          = 0,
81            line       = 0,
82            line_start = 0,
83            word_start = 0;
84     bool in_word = false;
85 
86     // TODO: use ICU for building the word map?
87     for (; i  < shape_result.fFragments.size(); ++i) {
88         const auto& frag = shape_result.fFragments[i];
89 
90         if (frag.fIsWhitespace) {
91             if (in_word) {
92                 in_word = false;
93                 fMaps.fWordsMap.push_back({word_start, i - word_start});
94             }
95         } else {
96             fMaps.fNonWhitespaceMap.push_back({i, 1});
97 
98             if (!in_word) {
99                 in_word = true;
100                 word_start = i;
101             }
102         }
103 
104         if (frag.fLineIndex != line) {
105             SkASSERT(frag.fLineIndex == line + 1);
106             fMaps.fLinesMap.push_back({line_start, i - line_start});
107             line = frag.fLineIndex;
108             line_start = i;
109         }
110     }
111 
112     if (i > word_start) {
113         fMaps.fWordsMap.push_back({word_start, i - word_start});
114     }
115 
116     if (i > line_start) {
117         fMaps.fLinesMap.push_back({line_start, i - line_start});
118     }
119 }
120 
apply()121 void TextAdapter::apply() {
122     if (!fText.fHasFill && !fText.fHasStroke) {
123         return;
124     }
125 
126     const Shaper::TextDesc text_desc = {
127         fText.fTypeface,
128         fText.fTextSize,
129         fText.fLineHeight,
130         fText.fAscent,
131         fText.fHAlign,
132         fText.fVAlign,
133         fHasAnimators ? Shaper::Flags::kFragmentGlyphs : Shaper::Flags::kNone,
134     };
135     const auto shape_result = Shaper::Shape(fText.fText, text_desc, fText.fBox, fFontMgr);
136 
137     if (fLogger && shape_result.fMissingGlyphCount > 0) {
138         const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
139                                         shape_result.fMissingGlyphCount,
140                                         fText.fText.c_str());
141         fLogger->log(Logger::Level::kWarning, msg.c_str());
142 
143         // This may trigger repeatedly when the text is animating.
144         // To avoid spamming, only log once.
145         fLogger = nullptr;
146     }
147 
148     // Rebuild all fragments.
149     // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
150 
151     fRoot->clear();
152     fFragments.clear();
153 
154     for (const auto& frag : shape_result.fFragments) {
155         this->addFragment(frag);
156     }
157 
158     if (fHasAnimators) {
159         // Range selectors require fragment domain maps.
160         this->buildDomainMaps(shape_result);
161     }
162 
163 #if (0)
164     // Enable for text box debugging/visualization.
165     auto box_color = sksg::Color::Make(0xffff0000);
166     box_color->setStyle(SkPaint::kStroke_Style);
167     box_color->setStrokeWidth(1);
168     box_color->setAntiAlias(true);
169 
170     auto bounds_color = sksg::Color::Make(0xff00ff00);
171     bounds_color->setStyle(SkPaint::kStroke_Style);
172     bounds_color->setStrokeWidth(1);
173     bounds_color->setAntiAlias(true);
174 
175     fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText.fBox),
176                                      std::move(box_color)));
177     fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeBounds()),
178                                      std::move(bounds_color)));
179 #endif
180 }
181 
applyAnimators(const std::vector<sk_sp<TextAnimator>> & animators)182 void TextAdapter::applyAnimators(const std::vector<sk_sp<TextAnimator>>& animators) {
183     if (fFragments.empty()) {
184         return;
185     }
186 
187     const auto& txt_val = this->getText();
188 
189     // Seed props from the current text value.
190     TextAnimator::AnimatedProps seed_props;
191     seed_props.fill_color   = txt_val.fFillColor;
192     seed_props.stroke_color = txt_val.fStrokeColor;
193 
194     TextAnimator::ModulatorBuffer buf;
195     buf.resize(fFragments.size(), { seed_props, 0 });
196 
197     // Apply all animators to the modulator buffer.
198     for (const auto& animator : animators) {
199         animator->modulateProps(fMaps, buf);
200     }
201 
202     // Finally, push all props to their corresponding fragment.
203     for (const auto& line_span : fMaps.fLinesMap) {
204         float line_tracking = 0;
205         bool line_has_tracking = false;
206 
207         // Tracking requires special treatment: unlike other props, its effect is not localized
208         // to a single fragment, but requires re-alignment of the whole line.
209         for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
210             const auto& props = buf[i].props;
211             const auto& frag  = fFragments[i];
212             this->pushPropsToFragment(props, frag);
213 
214             line_tracking += props.tracking;
215             line_has_tracking |= !SkScalarNearlyZero(props.tracking);
216         }
217 
218         if (line_has_tracking) {
219             this->adjustLineTracking(buf, line_span, line_tracking);
220         }
221     }
222 }
223 
pushPropsToFragment(const TextAnimator::AnimatedProps & props,const FragmentRec & rec) const224 void TextAdapter::pushPropsToFragment(const TextAnimator::AnimatedProps& props,
225                                       const FragmentRec& rec) const {
226     // TODO: share this with TransformAdapter2D?
227     auto t = SkMatrix::MakeTrans(rec.fOrigin.x() + props.position.x(),
228                                  rec.fOrigin.y() + props.position.y());
229     t.preRotate(props.rotation);
230     t.preScale(props.scale, props.scale);
231     rec.fMatrixNode->setMatrix(t);
232 
233     const auto scale_alpha = [](SkColor c, float o) {
234         return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
235     };
236 
237     if (rec.fFillColorNode) {
238         rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
239     }
240     if (rec.fStrokeColorNode) {
241         rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
242     }
243 }
244 
adjustLineTracking(const TextAnimator::ModulatorBuffer & buf,const TextAnimator::DomainSpan & line_span,float total_tracking) const245 void TextAdapter::adjustLineTracking(const TextAnimator::ModulatorBuffer& buf,
246                                      const TextAnimator::DomainSpan& line_span,
247                                      float total_tracking) const {
248     SkASSERT(line_span.fCount > 0);
249 
250     // AE tracking is defined per glyph, based on two components: |before| and |after|.
251     // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2.
252     //
253     // Tracking is applied as a local glyph offset, and contributes to the line width for alignment
254     // purposes.
255 
256     // The first glyph does not contribute |before| tracking, and the last one does not contribute
257     // |after| tracking.  Rather than spill this logic into applyAnimators, post-adjust here.
258     total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
259                               buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
260 
261     static const auto align_factor = [](SkTextUtils::Align a) {
262         switch (a) {
263         case SkTextUtils::kLeft_Align  : return  0.0f;
264         case SkTextUtils::kCenter_Align: return -0.5f;
265         case SkTextUtils::kRight_Align : return -1.0f;
266         }
267 
268         SkASSERT(false);
269         return 0.0f;
270     };
271 
272     const auto align_offset = total_tracking * align_factor(fText.fHAlign);
273 
274     float tracking_acc = 0;
275     for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
276         const auto& props = buf[i].props;
277 
278         // No |before| tracking for the first glyph, nor |after| tracking for the last one.
279         const auto track_before = i > line_span.fOffset
280                                     ? props.tracking * 0.5f : 0.0f,
281                    track_after  = i < line_span.fOffset + line_span.fCount - 1
282                                     ? props.tracking * 0.5f : 0.0f,
283                 fragment_offset = align_offset + tracking_acc + track_before;
284 
285         const auto& frag = fFragments[i];
286         const auto m = SkMatrix::Concat(SkMatrix::MakeTrans(fragment_offset, 0),
287                                         frag.fMatrixNode->getMatrix());
288         frag.fMatrixNode->setMatrix(m);
289 
290         tracking_acc += track_before + track_after;
291     }
292 }
293 
294 } // namespace internal
295 } // namespace skottie
296