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 "include/core/SkM44.h" |
12 | #include "modules/skottie/src/SkottieJson.h" |
13 | #include "modules/skottie/src/text/RangeSelector.h" |
14 | #include "modules/skottie/src/text/TextAnimator.h" |
15 | #include "modules/sksg/include/SkSGDraw.h" |
16 | #include "modules/sksg/include/SkSGGroup.h" |
17 | #include "modules/sksg/include/SkSGPaint.h" |
18 | #include "modules/sksg/include/SkSGRect.h" |
19 | #include "modules/sksg/include/SkSGRenderEffect.h" |
20 | #include "modules/sksg/include/SkSGText.h" |
21 | #include "modules/sksg/include/SkSGTransform.h" |
22 | |
23 | namespace skottie { |
24 | namespace internal { |
25 | |
26 | sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer, |
27 | const AnimationBuilder* abuilder, |
28 | sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger) { |
29 | // General text node format: |
30 | // "t": { |
31 | // "a": [], // animators (see TextAnimator) |
32 | // "d": { |
33 | // "k": [ |
34 | // { |
35 | // "s": { |
36 | // "f": "Roboto-Regular", |
37 | // "fc": [ |
38 | // 0.42, |
39 | // 0.15, |
40 | // 0.15 |
41 | // ], |
42 | // "j": 1, |
43 | // "lh": 60, |
44 | // "ls": 0, |
45 | // "s": 50, |
46 | // "t": "text align right", |
47 | // "tr": 0 |
48 | // }, |
49 | // "t": 0 |
50 | // } |
51 | // ] |
52 | // }, |
53 | // "m": { // "more options" |
54 | // "g": 1, // Anchor Point Grouping |
55 | // "a": {...} // Grouping Alignment |
56 | // }, |
57 | // "p": {} // "path options" (TODO) |
58 | // }, |
59 | |
60 | const skjson::ObjectValue* jt = jlayer["t" ]; |
61 | const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d" ]) |
62 | : nullptr; |
63 | if (!jd) { |
64 | abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer." ); |
65 | return nullptr; |
66 | } |
67 | |
68 | // "More options" |
69 | const skjson::ObjectValue* jm = (*jt)["m" ]; |
70 | static constexpr AnchorPointGrouping gGroupingMap[] = { |
71 | AnchorPointGrouping::kCharacter, // 'g': 1 |
72 | AnchorPointGrouping::kWord, // 'g': 2 |
73 | AnchorPointGrouping::kLine, // 'g': 3 |
74 | AnchorPointGrouping::kAll, // 'g': 4 |
75 | }; |
76 | const auto apg = jm |
77 | ? SkTPin<int>(ParseDefault<int>((*jm)["g" ], 1), 1, SK_ARRAY_COUNT(gGroupingMap)) |
78 | : 1; |
79 | |
80 | auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr), |
81 | std::move(logger), |
82 | gGroupingMap[SkToSizeT(apg - 1)])); |
83 | |
84 | adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue); |
85 | if (jm) { |
86 | adapter->bind(*abuilder, (*jm)["a" ], adapter->fGroupingAlignment); |
87 | } |
88 | |
89 | // Animators |
90 | if (const skjson::ArrayValue* janimators = (*jt)["a" ]) { |
91 | adapter->fAnimators.reserve(janimators->size()); |
92 | |
93 | for (const skjson::ObjectValue* janimator : *janimators) { |
94 | if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) { |
95 | adapter->fHasBlurAnimator |= animator->hasBlur(); |
96 | adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint(); |
97 | |
98 | adapter->fAnimators.push_back(std::move(animator)); |
99 | } |
100 | } |
101 | } |
102 | |
103 | abuilder->dispatchTextProperty(adapter); |
104 | |
105 | return adapter; |
106 | } |
107 | |
108 | TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg) |
109 | : fRoot(sksg::Group::Make()) |
110 | , fFontMgr(std::move(fontmgr)) |
111 | , fLogger(std::move(logger)) |
112 | , fAnchorPointGrouping(apg) |
113 | , fHasBlurAnimator(false) |
114 | , fRequiresAnchorPoint(false) {} |
115 | |
116 | TextAdapter::~TextAdapter() = default; |
117 | |
118 | void TextAdapter::addFragment(const Shaper::Fragment& frag) { |
119 | // For a given shaped fragment, build a corresponding SG fragment: |
120 | // |
121 | // [TransformEffect] -> [Transform] |
122 | // [Group] |
123 | // [Draw] -> [TextBlob*] [FillPaint] |
124 | // [Draw] -> [TextBlob*] [StrokePaint] |
125 | // |
126 | // * where the blob node is shared |
127 | |
128 | auto blob_node = sksg::TextBlob::Make(frag.fBlob); |
129 | |
130 | FragmentRec rec; |
131 | rec.fOrigin = frag.fPos; |
132 | rec.fAdvance = frag.fAdvance; |
133 | rec.fAscent = frag.fAscent; |
134 | rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fPos.x(), frag.fPos.y())); |
135 | |
136 | std::vector<sk_sp<sksg::RenderNode>> draws; |
137 | draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke)); |
138 | |
139 | SkASSERT(fText->fHasFill || fText->fHasStroke); |
140 | |
141 | if (fText->fHasFill) { |
142 | rec.fFillColorNode = sksg::Color::Make(fText->fFillColor); |
143 | rec.fFillColorNode->setAntiAlias(true); |
144 | draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode)); |
145 | } |
146 | if (fText->fHasStroke) { |
147 | rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor); |
148 | rec.fStrokeColorNode->setAntiAlias(true); |
149 | rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style); |
150 | draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode)); |
151 | } |
152 | |
153 | SkASSERT(!draws.empty()); |
154 | |
155 | if (0) { |
156 | // enable to visualize fragment ascent boxes |
157 | auto box_color = sksg::Color::Make(0xff0000ff); |
158 | box_color->setStyle(SkPaint::kStroke_Style); |
159 | box_color->setStrokeWidth(1); |
160 | box_color->setAntiAlias(true); |
161 | auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0); |
162 | draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color))); |
163 | } |
164 | |
165 | auto draws_node = (draws.size() > 1) |
166 | ? sksg::Group::Make(std::move(draws)) |
167 | : std::move(draws[0]); |
168 | |
169 | if (fHasBlurAnimator) { |
170 | // Optional blur effect. |
171 | rec.fBlur = sksg::BlurImageFilter::Make(); |
172 | draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur); |
173 | } |
174 | |
175 | fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode)); |
176 | fFragments.push_back(std::move(rec)); |
177 | } |
178 | |
179 | void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) { |
180 | fMaps.fNonWhitespaceMap.clear(); |
181 | fMaps.fWordsMap.clear(); |
182 | fMaps.fLinesMap.clear(); |
183 | |
184 | size_t i = 0, |
185 | line = 0, |
186 | line_start = 0, |
187 | word_start = 0; |
188 | |
189 | float word_advance = 0, |
190 | word_ascent = 0, |
191 | line_advance = 0, |
192 | line_ascent = 0; |
193 | |
194 | bool in_word = false; |
195 | |
196 | // TODO: use ICU for building the word map? |
197 | for (; i < shape_result.fFragments.size(); ++i) { |
198 | const auto& frag = shape_result.fFragments[i]; |
199 | |
200 | if (frag.fIsWhitespace) { |
201 | if (in_word) { |
202 | in_word = false; |
203 | fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent}); |
204 | } |
205 | } else { |
206 | fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0}); |
207 | |
208 | if (!in_word) { |
209 | in_word = true; |
210 | word_start = i; |
211 | word_advance = word_ascent = 0; |
212 | } |
213 | |
214 | word_advance += frag.fAdvance; |
215 | word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent |
216 | } |
217 | |
218 | if (frag.fLineIndex != line) { |
219 | SkASSERT(frag.fLineIndex == line + 1); |
220 | fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent}); |
221 | line = frag.fLineIndex; |
222 | line_start = i; |
223 | line_advance = line_ascent = 0; |
224 | } |
225 | |
226 | line_advance += frag.fAdvance; |
227 | line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent |
228 | } |
229 | |
230 | if (i > word_start) { |
231 | fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent}); |
232 | } |
233 | |
234 | if (i > line_start) { |
235 | fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent}); |
236 | } |
237 | } |
238 | |
239 | void TextAdapter::setText(const TextValue& txt) { |
240 | fText.fCurrentValue = txt; |
241 | this->onSync(); |
242 | } |
243 | |
244 | uint32_t TextAdapter::shaperFlags() const { |
245 | uint32_t flags = Shaper::Flags::kNone; |
246 | |
247 | SkASSERT(!(fRequiresAnchorPoint && fAnimators.empty())); |
248 | if (!fAnimators.empty() ) flags |= Shaper::Flags::kFragmentGlyphs; |
249 | if (fRequiresAnchorPoint) flags |= Shaper::Flags::kTrackFragmentAdvanceAscent; |
250 | |
251 | return flags; |
252 | } |
253 | |
254 | void TextAdapter::reshape() { |
255 | const Shaper::TextDesc text_desc = { |
256 | fText->fTypeface, |
257 | fText->fTextSize, |
258 | fText->fLineHeight, |
259 | fText->fAscent, |
260 | fText->fHAlign, |
261 | fText->fVAlign, |
262 | fText->fResize, |
263 | this->shaperFlags(), |
264 | }; |
265 | const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr); |
266 | |
267 | if (fLogger && shape_result.fMissingGlyphCount > 0) { |
268 | const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'." , |
269 | shape_result.fMissingGlyphCount, |
270 | fText->fText.c_str()); |
271 | fLogger->log(Logger::Level::kWarning, msg.c_str()); |
272 | |
273 | // This may trigger repeatedly when the text is animating. |
274 | // To avoid spamming, only log once. |
275 | fLogger = nullptr; |
276 | } |
277 | |
278 | // Rebuild all fragments. |
279 | // TODO: we can be smarter here and try to reuse the existing SG structure if needed. |
280 | |
281 | fRoot->clear(); |
282 | fFragments.clear(); |
283 | |
284 | for (const auto& frag : shape_result.fFragments) { |
285 | this->addFragment(frag); |
286 | } |
287 | |
288 | if (!fAnimators.empty()) { |
289 | // Range selectors require fragment domain maps. |
290 | this->buildDomainMaps(shape_result); |
291 | } |
292 | |
293 | #if (0) |
294 | // Enable for text box debugging/visualization. |
295 | auto box_color = sksg::Color::Make(0xffff0000); |
296 | box_color->setStyle(SkPaint::kStroke_Style); |
297 | box_color->setStrokeWidth(1); |
298 | box_color->setAntiAlias(true); |
299 | |
300 | auto bounds_color = sksg::Color::Make(0xff00ff00); |
301 | bounds_color->setStyle(SkPaint::kStroke_Style); |
302 | bounds_color->setStrokeWidth(1); |
303 | bounds_color->setAntiAlias(true); |
304 | |
305 | fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText.fBox), |
306 | std::move(box_color))); |
307 | fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeBounds()), |
308 | std::move(bounds_color))); |
309 | #endif |
310 | } |
311 | |
312 | void TextAdapter::onSync() { |
313 | if (!fText->fHasFill && !fText->fHasStroke) { |
314 | return; |
315 | } |
316 | |
317 | if (fText.hasChanged()) { |
318 | this->reshape(); |
319 | } |
320 | |
321 | if (fFragments.empty()) { |
322 | return; |
323 | } |
324 | |
325 | // Seed props from the current text value. |
326 | TextAnimator::ResolvedProps seed_props; |
327 | seed_props.fill_color = fText->fFillColor; |
328 | seed_props.stroke_color = fText->fStrokeColor; |
329 | |
330 | TextAnimator::ModulatorBuffer buf; |
331 | buf.resize(fFragments.size(), { seed_props, 0 }); |
332 | |
333 | // Apply all animators to the modulator buffer. |
334 | for (const auto& animator : fAnimators) { |
335 | animator->modulateProps(fMaps, buf); |
336 | } |
337 | |
338 | const TextAnimator::DomainMap* grouping_domain = nullptr; |
339 | switch (fAnchorPointGrouping) { |
340 | // for word/line grouping, we rely on domain map info |
341 | case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break; |
342 | case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break; |
343 | // remaining grouping modes (character/all) do not need (or have) domain map data |
344 | default: break; |
345 | } |
346 | |
347 | size_t grouping_span_index = 0; |
348 | |
349 | // Finally, push all props to their corresponding fragment. |
350 | for (const auto& line_span : fMaps.fLinesMap) { |
351 | float line_tracking = 0; |
352 | bool line_has_tracking = false; |
353 | |
354 | // Tracking requires special treatment: unlike other props, its effect is not localized |
355 | // to a single fragment, but requires re-alignment of the whole line. |
356 | for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) { |
357 | // Track the grouping domain span in parallel. |
358 | if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset + |
359 | (*grouping_domain)[grouping_span_index].fCount) { |
360 | grouping_span_index += 1; |
361 | SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset + |
362 | (*grouping_domain)[grouping_span_index].fCount); |
363 | } |
364 | |
365 | const auto& props = buf[i].props; |
366 | const auto& frag = fFragments[i]; |
367 | this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage |
368 | grouping_domain ? &(*grouping_domain)[grouping_span_index] |
369 | : nullptr); |
370 | |
371 | line_tracking += props.tracking; |
372 | line_has_tracking |= !SkScalarNearlyZero(props.tracking); |
373 | } |
374 | |
375 | if (line_has_tracking) { |
376 | this->adjustLineTracking(buf, line_span, line_tracking); |
377 | } |
378 | } |
379 | } |
380 | |
381 | SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec, |
382 | const SkV2& grouping_alignment, |
383 | const TextAnimator::DomainSpan* grouping_span) const { |
384 | // Construct the following 2x ascent box: |
385 | // |
386 | // ------------- |
387 | // | | |
388 | // | | ascent |
389 | // | | |
390 | // ----+-------------+---------- baseline |
391 | // (pos) | |
392 | // | | ascent |
393 | // | | |
394 | // ------------- |
395 | // advance |
396 | |
397 | auto make_box = [](const SkPoint& pos, float advance, float ascent) { |
398 | // note: negative ascent |
399 | return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent); |
400 | }; |
401 | |
402 | // Compute a grouping-dependent anchor point box. |
403 | // The default anchor point is at the center, and gets adjusted relative to the bounds |
404 | // based on |grouping_alignment|. |
405 | auto anchor_box = [&]() -> SkRect { |
406 | switch (fAnchorPointGrouping) { |
407 | case AnchorPointGrouping::kCharacter: |
408 | // Anchor box relative to each individual fragment. |
409 | return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent); |
410 | case AnchorPointGrouping::kWord: |
411 | // Fall through |
412 | case AnchorPointGrouping::kLine: { |
413 | SkASSERT(grouping_span); |
414 | // Anchor box relative to the first fragment in the word/line. |
415 | const auto& first_span_fragment = fFragments[grouping_span->fOffset]; |
416 | return make_box(first_span_fragment.fOrigin, |
417 | grouping_span->fAdvance, |
418 | grouping_span->fAscent); |
419 | } |
420 | case AnchorPointGrouping::kAll: |
421 | // Anchor box is the same as the text box. |
422 | return fText->fBox; |
423 | } |
424 | SkUNREACHABLE; |
425 | }; |
426 | |
427 | const auto ab = anchor_box(); |
428 | |
429 | // Apply grouping alignment. |
430 | const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x, |
431 | ab.centerY() + ab.height() * 0.5f * grouping_alignment.y }; |
432 | |
433 | // The anchor point is relative to the fragment position. |
434 | return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY }; |
435 | } |
436 | |
437 | void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props, |
438 | const FragmentRec& rec, |
439 | const SkV2& grouping_alignment, |
440 | const TextAnimator::DomainSpan* grouping_span) const { |
441 | const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span); |
442 | |
443 | rec.fMatrixNode->setMatrix( |
444 | SkM44::Translate(props.position.x + rec.fOrigin.x() + anchor_point.x, |
445 | props.position.y + rec.fOrigin.y() + anchor_point.y, |
446 | props.position.z) |
447 | * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x)) |
448 | * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y)) |
449 | * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z)) |
450 | * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z) |
451 | * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0)); |
452 | |
453 | const auto scale_alpha = [](SkColor c, float o) { |
454 | return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c))); |
455 | }; |
456 | |
457 | if (rec.fFillColorNode) { |
458 | rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity)); |
459 | } |
460 | if (rec.fStrokeColorNode) { |
461 | rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity)); |
462 | } |
463 | if (rec.fBlur) { |
464 | rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma, |
465 | props.blur.y * kBlurSizeToSigma }); |
466 | } |
467 | } |
468 | |
469 | void TextAdapter::adjustLineTracking(const TextAnimator::ModulatorBuffer& buf, |
470 | const TextAnimator::DomainSpan& line_span, |
471 | float total_tracking) const { |
472 | SkASSERT(line_span.fCount > 0); |
473 | |
474 | // AE tracking is defined per glyph, based on two components: |before| and |after|. |
475 | // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2. |
476 | // |
477 | // Tracking is applied as a local glyph offset, and contributes to the line width for alignment |
478 | // purposes. |
479 | |
480 | // The first glyph does not contribute |before| tracking, and the last one does not contribute |
481 | // |after| tracking. Rather than spill this logic into applyAnimators, post-adjust here. |
482 | total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking + |
483 | buf[line_span.fOffset + line_span.fCount - 1].props.tracking); |
484 | |
485 | static const auto align_factor = [](SkTextUtils::Align a) { |
486 | switch (a) { |
487 | case SkTextUtils::kLeft_Align : return 0.0f; |
488 | case SkTextUtils::kCenter_Align: return -0.5f; |
489 | case SkTextUtils::kRight_Align : return -1.0f; |
490 | } |
491 | |
492 | SkASSERT(false); |
493 | return 0.0f; |
494 | }; |
495 | |
496 | const auto align_offset = total_tracking * align_factor(fText->fHAlign); |
497 | |
498 | float tracking_acc = 0; |
499 | for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) { |
500 | const auto& props = buf[i].props; |
501 | |
502 | // No |before| tracking for the first glyph, nor |after| tracking for the last one. |
503 | const auto track_before = i > line_span.fOffset |
504 | ? props.tracking * 0.5f : 0.0f, |
505 | track_after = i < line_span.fOffset + line_span.fCount - 1 |
506 | ? props.tracking * 0.5f : 0.0f, |
507 | fragment_offset = align_offset + tracking_acc + track_before; |
508 | |
509 | const auto& frag = fFragments[i]; |
510 | const auto m = SkM44::Translate(fragment_offset, 0) * frag.fMatrixNode->getMatrix(); |
511 | frag.fMatrixNode->setMatrix(m); |
512 | |
513 | tracking_acc += track_before + track_after; |
514 | } |
515 | } |
516 | |
517 | } // namespace internal |
518 | } // namespace skottie |
519 | |