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