• 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 "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->fMinTextSize,
273         fText->fMaxTextSize,
274         fText->fLineHeight,
275         fText->fLineShift,
276         fText->fAscent,
277         fText->fHAlign,
278         fText->fVAlign,
279         fText->fResize,
280         fText->fLineBreak,
281         fText->fDirection,
282         this->shaperFlags(),
283     };
284     const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
285 
286     if (fLogger && shape_result.fMissingGlyphCount > 0) {
287         const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
288                                         shape_result.fMissingGlyphCount,
289                                         fText->fText.c_str());
290         fLogger->log(Logger::Level::kWarning, msg.c_str());
291 
292         // This may trigger repeatedly when the text is animating.
293         // To avoid spamming, only log once.
294         fLogger = nullptr;
295     }
296 
297     // Rebuild all fragments.
298     // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
299 
300     fRoot->clear();
301     fFragments.clear();
302 
303     for (const auto& frag : shape_result.fFragments) {
304         this->addFragment(frag);
305     }
306 
307     if (!fAnimators.empty()) {
308         // Range selectors require fragment domain maps.
309         this->buildDomainMaps(shape_result);
310     }
311 
312 #if (0)
313     // Enable for text box debugging/visualization.
314     auto box_color = sksg::Color::Make(0xffff0000);
315     box_color->setStyle(SkPaint::kStroke_Style);
316     box_color->setStrokeWidth(1);
317     box_color->setAntiAlias(true);
318 
319     auto bounds_color = sksg::Color::Make(0xff00ff00);
320     bounds_color->setStyle(SkPaint::kStroke_Style);
321     bounds_color->setStrokeWidth(1);
322     bounds_color->setAntiAlias(true);
323 
324     fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
325                                      std::move(box_color)));
326     fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
327                                      std::move(bounds_color)));
328 #endif
329 }
330 
onSync()331 void TextAdapter::onSync() {
332     if (!fText->fHasFill && !fText->fHasStroke) {
333         return;
334     }
335 
336     if (fText.hasChanged()) {
337         this->reshape();
338     }
339 
340     if (fFragments.empty()) {
341         return;
342     }
343 
344     // Seed props from the current text value.
345     TextAnimator::ResolvedProps seed_props;
346     seed_props.fill_color   = fText->fFillColor;
347     seed_props.stroke_color = fText->fStrokeColor;
348 
349     TextAnimator::ModulatorBuffer buf;
350     buf.resize(fFragments.size(), { seed_props, 0 });
351 
352     // Apply all animators to the modulator buffer.
353     for (const auto& animator : fAnimators) {
354         animator->modulateProps(fMaps, buf);
355     }
356 
357     const TextAnimator::DomainMap* grouping_domain = nullptr;
358     switch (fAnchorPointGrouping) {
359         // for word/line grouping, we rely on domain map info
360         case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
361         case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
362         // remaining grouping modes (character/all) do not need (or have) domain map data
363         default: break;
364     }
365 
366     size_t grouping_span_index = 0;
367     SkV2           line_offset = { 0, 0 }; // cumulative line spacing
368 
369     // Finally, push all props to their corresponding fragment.
370     for (const auto& line_span : fMaps.fLinesMap) {
371         SkV2 line_spacing = { 0, 0 };
372         float line_tracking = 0;
373         bool line_has_tracking = false;
374 
375         // Tracking requires special treatment: unlike other props, its effect is not localized
376         // to a single fragment, but requires re-alignment of the whole line.
377         for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
378             // Track the grouping domain span in parallel.
379             if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
380                                         (*grouping_domain)[grouping_span_index].fCount) {
381                 grouping_span_index += 1;
382                 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
383                              (*grouping_domain)[grouping_span_index].fCount);
384             }
385 
386             const auto& props = buf[i].props;
387             const auto& frag  = fFragments[i];
388             this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage
389                                       grouping_domain ? &(*grouping_domain)[grouping_span_index]
390                                                         : nullptr);
391 
392             line_tracking += props.tracking;
393             line_has_tracking |= !SkScalarNearlyZero(props.tracking);
394 
395             line_spacing += props.line_spacing;
396         }
397 
398         // line spacing of the first line is ignored (nothing to "space" against)
399         if (&line_span != &fMaps.fLinesMap.front()) {
400             // For each line, the actual spacing is an average of individual fragment spacing
401             // (to preserve the "line").
402             line_offset += line_spacing / line_span.fCount;
403         }
404 
405         if (line_offset != SkV2{0, 0} || line_has_tracking) {
406             this->adjustLineProps(buf, line_span, line_offset, line_tracking);
407         }
408 
409     }
410 }
411 
fragmentAnchorPoint(const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const412 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
413                                       const SkV2& grouping_alignment,
414                                       const TextAnimator::DomainSpan* grouping_span) const {
415     // Construct the following 2x ascent box:
416     //
417     //      -------------
418     //     |             |
419     //     |             | ascent
420     //     |             |
421     // ----+-------------+---------- baseline
422     //   (pos)           |
423     //     |             | ascent
424     //     |             |
425     //      -------------
426     //         advance
427 
428     auto make_box = [](const SkPoint& pos, float advance, float ascent) {
429         // note: negative ascent
430         return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
431     };
432 
433     // Compute a grouping-dependent anchor point box.
434     // The default anchor point is at the center, and gets adjusted relative to the bounds
435     // based on |grouping_alignment|.
436     auto anchor_box = [&]() -> SkRect {
437         switch (fAnchorPointGrouping) {
438         case AnchorPointGrouping::kCharacter:
439             // Anchor box relative to each individual fragment.
440             return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
441         case AnchorPointGrouping::kWord:
442             // Fall through
443         case AnchorPointGrouping::kLine: {
444             SkASSERT(grouping_span);
445             // Anchor box relative to the first fragment in the word/line.
446             const auto& first_span_fragment = fFragments[grouping_span->fOffset];
447             return make_box(first_span_fragment.fOrigin,
448                             grouping_span->fAdvance,
449                             grouping_span->fAscent);
450         }
451         case AnchorPointGrouping::kAll:
452             // Anchor box is the same as the text box.
453             return fText->fBox;
454         }
455         SkUNREACHABLE;
456     };
457 
458     const auto ab = anchor_box();
459 
460     // Apply grouping alignment.
461     const auto ap = SkV2 { ab.centerX() + ab.width()  * 0.5f * grouping_alignment.x,
462                            ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
463 
464     // The anchor point is relative to the fragment position.
465     return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
466 }
467 
pushPropsToFragment(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const468 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
469                                       const FragmentRec& rec,
470                                       const SkV2& grouping_alignment,
471                                       const TextAnimator::DomainSpan* grouping_span) const {
472     const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
473 
474     rec.fMatrixNode->setMatrix(
475                 SkM44::Translate(props.position.x + rec.fOrigin.x() + anchor_point.x,
476                                  props.position.y + rec.fOrigin.y() + anchor_point.y,
477                                  props.position.z)
478               * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
479               * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
480               * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
481               * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
482               * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
483 
484     const auto scale_alpha = [](SkColor c, float o) {
485         return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
486     };
487 
488     if (rec.fFillColorNode) {
489         rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
490     }
491     if (rec.fStrokeColorNode) {
492         rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
493     }
494     if (rec.fBlur) {
495         rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
496                               props.blur.y * kBlurSizeToSigma });
497     }
498 }
499 
adjustLineProps(const TextAnimator::ModulatorBuffer & buf,const TextAnimator::DomainSpan & line_span,const SkV2 & line_offset,float total_tracking) const500 void TextAdapter::adjustLineProps(const TextAnimator::ModulatorBuffer& buf,
501                                   const TextAnimator::DomainSpan& line_span,
502                                   const SkV2& line_offset,
503                                   float total_tracking) const {
504     SkASSERT(line_span.fCount > 0);
505 
506     // AE tracking is defined per glyph, based on two components: |before| and |after|.
507     // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2.
508     //
509     // Tracking is applied as a local glyph offset, and contributes to the line width for alignment
510     // purposes.
511 
512     // The first glyph does not contribute |before| tracking, and the last one does not contribute
513     // |after| tracking.  Rather than spill this logic into applyAnimators, post-adjust here.
514     total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
515                               buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
516 
517     static const auto align_factor = [](SkTextUtils::Align a) {
518         switch (a) {
519         case SkTextUtils::kLeft_Align  : return  0.0f;
520         case SkTextUtils::kCenter_Align: return -0.5f;
521         case SkTextUtils::kRight_Align : return -1.0f;
522         }
523 
524         SkASSERT(false);
525         return 0.0f;
526     };
527 
528     const auto align_offset = total_tracking * align_factor(fText->fHAlign);
529 
530     float tracking_acc = 0;
531     for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
532         const auto& props = buf[i].props;
533 
534         // No |before| tracking for the first glyph, nor |after| tracking for the last one.
535         const auto track_before = i > line_span.fOffset
536                                     ? props.tracking * 0.5f : 0.0f,
537                    track_after  = i < line_span.fOffset + line_span.fCount - 1
538                                     ? props.tracking * 0.5f : 0.0f,
539                 fragment_offset = align_offset + tracking_acc + track_before;
540 
541         const auto& frag = fFragments[i];
542         const auto m = SkM44::Translate(line_offset.x + fragment_offset,
543                                         line_offset.y) *
544                        frag.fMatrixNode->getMatrix();
545         frag.fMatrixNode->setMatrix(m);
546 
547         tracking_acc += track_before + track_after;
548     }
549 }
550 
551 } // namespace internal
552 } // namespace skottie
553