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