• 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/SkContourMeasure.h"
11 #include "include/core/SkFontMgr.h"
12 #include "include/core/SkM44.h"
13 #include "include/private/SkTPin.h"
14 #include "modules/skottie/src/SkottieJson.h"
15 #include "modules/skottie/src/text/RangeSelector.h"
16 #include "modules/skottie/src/text/TextAnimator.h"
17 #include "modules/sksg/include/SkSGDraw.h"
18 #include "modules/sksg/include/SkSGGroup.h"
19 #include "modules/sksg/include/SkSGPaint.h"
20 #include "modules/sksg/include/SkSGPath.h"
21 #include "modules/sksg/include/SkSGRect.h"
22 #include "modules/sksg/include/SkSGRenderEffect.h"
23 #include "modules/sksg/include/SkSGText.h"
24 #include "modules/sksg/include/SkSGTransform.h"
25 
26 // Enable for text layout debugging.
27 #define SHOW_LAYOUT_BOXES 0
28 
29 namespace skottie::internal {
30 
align_factor(SkTextUtils::Align a)31 static float align_factor(SkTextUtils::Align a) {
32     switch (a) {
33         case SkTextUtils::kLeft_Align  : return 0.0f;
34         case SkTextUtils::kCenter_Align: return 0.5f;
35         case SkTextUtils::kRight_Align : return 1.0f;
36     }
37 
38     SkUNREACHABLE;
39 };
40 
41 // Text path semantics
42 //
43 //   * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
44 //     a distance along the path
45 //
46 //   * horizontal alignment is applied relative to the path start/end points
47 //
48 //   * "Reverse Path" allows reversing the path direction
49 //
50 //   * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular
51 //      to the path tangent, or not (just positioned).
52 //
53 //   * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path,
54 //     depending on horizontal alignement:
55 //       - left:   offset = first margin
56 //       - center: offset = first margin + last margin
57 //       - right:  offset = last margin
58 //
59 //   * extranormal path positions (d < 0, d > path len) are allowed
60 //       - closed path: the position wraps around in both directions
61 //       - open path: extrapolates from extremes' positions/slopes
62 //
63 struct TextAdapter::PathInfo {
64     ShapeValue  fPath;
65     ScalarValue fPathFMargin       = 0,
66                 fPathLMargin       = 0,
67                 fPathPerpendicular = 0,
68                 fPathReverse       = 0;
69 
updateContourDataskottie::internal::TextAdapter::PathInfo70     void updateContourData() {
71         const auto reverse = fPathReverse != 0;
72 
73         if (fPath != fCurrentPath || reverse != fCurrentReversed) {
74             // reinitialize cached contour data
75             auto path = static_cast<SkPath>(fPath);
76             if (reverse) {
77                 SkPath reversed;
78                 reversed.reverseAddPath(path);
79                 path = reversed;
80             }
81 
82             SkContourMeasureIter iter(path, /*forceClosed = */false);
83             fCurrentMeasure  = iter.next();
84             fCurrentClosed   = path.isLastContourClosed();
85             fCurrentReversed = reverse;
86             fCurrentPath     = fPath;
87 
88             // AE paths are always single-contour (no moves allowed).
89             SkASSERT(!iter.next());
90         }
91     }
92 
pathLengthskottie::internal::TextAdapter::PathInfo93     float pathLength() const {
94         SkASSERT(fPath == fCurrentPath);
95         SkASSERT((fPathReverse != 0) == fCurrentReversed);
96 
97         return fCurrentMeasure ? fCurrentMeasure->length() : 0;
98     }
99 
getMatrixskottie::internal::TextAdapter::PathInfo100     SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const {
101         SkASSERT(fPath == fCurrentPath);
102         SkASSERT((fPathReverse != 0) == fCurrentReversed);
103 
104         if (!fCurrentMeasure) {
105             return SkM44();
106         }
107 
108         const auto path_len = fCurrentMeasure->length();
109 
110         // First/last margin adjustment also depends on alignment.
111         switch (alignment) {
112             case SkTextUtils::Align::kLeft_Align:   distance += fPathFMargin; break;
113             case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
114                                                                 fPathLMargin; break;
115             case SkTextUtils::Align::kRight_Align:  distance += fPathLMargin; break;
116         }
117 
118         // For closed paths, extranormal distances wrap around the contour.
119         if (fCurrentClosed) {
120             distance = std::fmod(distance, path_len);
121             if (distance < 0) {
122                 distance += path_len;
123             }
124             SkASSERT(0 <= distance && distance <= path_len);
125         }
126 
127         SkPoint pos;
128         SkVector tan;
129         if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) {
130             return SkM44();
131         }
132 
133         // For open paths, extranormal distances are extrapolated from extremes.
134         // Note:
135         //   - getPosTan above clamps to the extremes
136         //   - the extrapolation below only kicks in for extranormal values
137         const auto underflow = std::min(0.0f, distance),
138                    overflow  = std::max(0.0f, distance - path_len);
139         pos += tan*(underflow + overflow);
140 
141         auto m = SkM44::Translate(pos.x(), pos.y());
142 
143         // The "perpendicular" flag controls whether fragments are positioned and rotated,
144         // or just positioned.
145         if (fPathPerpendicular != 0) {
146             m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x()));
147         }
148 
149         return m;
150     }
151 
152 private:
153     // Cached contour data.
154     ShapeValue              fCurrentPath;
155     sk_sp<SkContourMeasure> fCurrentMeasure;
156     bool                    fCurrentReversed = false,
157                             fCurrentClosed   = false;
158 };
159 
Make(const skjson::ObjectValue & jlayer,const AnimationBuilder * abuilder,sk_sp<SkFontMgr> fontmgr,sk_sp<Logger> logger)160 sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
161                                      const AnimationBuilder* abuilder,
162                                      sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger) {
163     // General text node format:
164     // "t": {
165     //    "a": [], // animators (see TextAnimator)
166     //    "d": {
167     //        "k": [
168     //            {
169     //                "s": {
170     //                    "f": "Roboto-Regular",
171     //                    "fc": [
172     //                        0.42,
173     //                        0.15,
174     //                        0.15
175     //                    ],
176     //                    "j": 1,
177     //                    "lh": 60,
178     //                    "ls": 0,
179     //                    "s": 50,
180     //                    "t": "text align right",
181     //                    "tr": 0
182     //                },
183     //                "t": 0
184     //            }
185     //        ]
186     //    },
187     //    "m": { // more options
188     //           "g": 1,     // Anchor Point Grouping
189     //           "a": {...}  // Grouping Alignment
190     //         },
191     //    "p": { // path options
192     //           "a": 0,   // force alignment
193     //           "f": {},  // first margin
194     //           "l": {},  // last margin
195     //           "m": 1,   // mask index
196     //           "p": 1,   // perpendicular
197     //           "r": 0    // reverse path
198     //         }
199 
200     // },
201 
202     const skjson::ObjectValue* jt = jlayer["t"];
203     const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
204                                        : nullptr;
205     if (!jd) {
206         abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
207         return nullptr;
208     }
209 
210     // "More options"
211     const skjson::ObjectValue* jm = (*jt)["m"];
212     static constexpr AnchorPointGrouping gGroupingMap[] = {
213         AnchorPointGrouping::kCharacter, // 'g': 1
214         AnchorPointGrouping::kWord,      // 'g': 2
215         AnchorPointGrouping::kLine,      // 'g': 3
216         AnchorPointGrouping::kAll,       // 'g': 4
217     };
218     const auto apg = jm
219             ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, SK_ARRAY_COUNT(gGroupingMap))
220             : 1;
221 
222     auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
223                                                       std::move(logger),
224                                                       gGroupingMap[SkToSizeT(apg - 1)]));
225 
226     adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
227     if (jm) {
228         adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
229     }
230 
231     // Animators
232     if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
233         adapter->fAnimators.reserve(janimators->size());
234 
235         for (const skjson::ObjectValue* janimator : *janimators) {
236             if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
237                 adapter->fHasBlurAnimator     |= animator->hasBlur();
238                 adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint();
239 
240                 adapter->fAnimators.push_back(std::move(animator));
241             }
242         }
243     }
244 
245     // Optional text path
246     const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
247         if (!jpath) {
248             return nullptr;
249         }
250 
251         // the actual path is identified as an index in the layer mask stack
252         const auto mask_index =
253                 ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
254         const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
255         if (!jmasks || mask_index >= jmasks->size()) {
256             return nullptr;
257         }
258 
259         const skjson::ObjectValue* mask = (*jmasks)[mask_index];
260         if (!mask) {
261             return nullptr;
262         }
263 
264         auto pinfo = std::make_unique<PathInfo>();
265         adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
266         adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
267         adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
268         adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
269         adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);
270 
271         // TODO: force align support
272 
273         // Historically, these used to be exported as static properties.
274         // Attempt parsing both ways, for backward compat.
275         skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
276         skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);
277 
278         // Path positioning requires anchor point info.
279         adapter->fRequiresAnchorPoint = true;
280 
281         return pinfo;
282     };
283 
284     adapter->fPathInfo = attach_path((*jt)["p"]);
285 
286     abuilder->dispatchTextProperty(adapter);
287 
288     return adapter;
289 }
290 
TextAdapter(sk_sp<SkFontMgr> fontmgr,sk_sp<Logger> logger,AnchorPointGrouping apg)291 TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg)
292     : fRoot(sksg::Group::Make())
293     , fFontMgr(std::move(fontmgr))
294     , fLogger(std::move(logger))
295     , fAnchorPointGrouping(apg)
296     , fHasBlurAnimator(false)
297     , fRequiresAnchorPoint(false) {}
298 
299 TextAdapter::~TextAdapter() = default;
300 
addFragment(const Shaper::Fragment & frag)301 void TextAdapter::addFragment(const Shaper::Fragment& frag) {
302     // For a given shaped fragment, build a corresponding SG fragment:
303     //
304     //   [TransformEffect] -> [Transform]
305     //     [Group]
306     //       [Draw] -> [TextBlob*] [FillPaint]
307     //       [Draw] -> [TextBlob*] [StrokePaint]
308     //
309     // * where the blob node is shared
310 
311     auto blob_node = sksg::TextBlob::Make(frag.fBlob);
312 
313     FragmentRec rec;
314     rec.fOrigin     = frag.fPos;
315     rec.fAdvance    = frag.fAdvance;
316     rec.fAscent     = frag.fAscent;
317     rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fPos.x(), frag.fPos.y()));
318 
319     std::vector<sk_sp<sksg::RenderNode>> draws;
320     draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke));
321 
322     SkASSERT(fText->fHasFill || fText->fHasStroke);
323 
324     auto add_fill = [&]() {
325         if (fText->fHasFill) {
326             rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
327             rec.fFillColorNode->setAntiAlias(true);
328             draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
329         }
330     };
331     auto add_stroke = [&] {
332         if (fText->fHasStroke) {
333             rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
334             rec.fStrokeColorNode->setAntiAlias(true);
335             rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
336             rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth);
337             draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
338         }
339     };
340 
341     if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
342         add_fill();
343         add_stroke();
344     } else {
345         add_stroke();
346         add_fill();
347     }
348 
349     SkASSERT(!draws.empty());
350 
351     if (SHOW_LAYOUT_BOXES) {
352         // visualize fragment ascent boxes
353         auto box_color = sksg::Color::Make(0xff0000ff);
354         box_color->setStyle(SkPaint::kStroke_Style);
355         box_color->setStrokeWidth(1);
356         box_color->setAntiAlias(true);
357         auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
358         draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
359     }
360 
361     auto draws_node = (draws.size() > 1)
362             ? sksg::Group::Make(std::move(draws))
363             : std::move(draws[0]);
364 
365     if (fHasBlurAnimator) {
366         // Optional blur effect.
367         rec.fBlur = sksg::BlurImageFilter::Make();
368         draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
369     }
370 
371     fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
372     fFragments.push_back(std::move(rec));
373 }
374 
buildDomainMaps(const Shaper::Result & shape_result)375 void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
376     fMaps.fNonWhitespaceMap.clear();
377     fMaps.fWordsMap.clear();
378     fMaps.fLinesMap.clear();
379 
380     size_t i          = 0,
381            line       = 0,
382            line_start = 0,
383            word_start = 0;
384 
385     float word_advance = 0,
386           word_ascent  = 0,
387           line_advance = 0,
388           line_ascent  = 0;
389 
390     bool in_word = false;
391 
392     // TODO: use ICU for building the word map?
393     for (; i  < shape_result.fFragments.size(); ++i) {
394         const auto& frag = shape_result.fFragments[i];
395 
396         if (frag.fIsWhitespace) {
397             if (in_word) {
398                 in_word = false;
399                 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
400             }
401         } else {
402             fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
403 
404             if (!in_word) {
405                 in_word = true;
406                 word_start = i;
407                 word_advance = word_ascent = 0;
408             }
409 
410             word_advance += frag.fAdvance;
411             word_ascent   = std::min(word_ascent, frag.fAscent); // negative ascent
412         }
413 
414         if (frag.fLineIndex != line) {
415             SkASSERT(frag.fLineIndex == line + 1);
416             fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
417             line = frag.fLineIndex;
418             line_start = i;
419             line_advance = line_ascent = 0;
420         }
421 
422         line_advance += frag.fAdvance;
423         line_ascent   = std::min(line_ascent, frag.fAscent); // negative ascent
424     }
425 
426     if (i > word_start) {
427         fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
428     }
429 
430     if (i > line_start) {
431         fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
432     }
433 }
434 
setText(const TextValue & txt)435 void TextAdapter::setText(const TextValue& txt) {
436     fText.fCurrentValue = txt;
437     this->onSync();
438 }
439 
shaperFlags() const440 uint32_t TextAdapter::shaperFlags() const {
441     uint32_t flags = Shaper::Flags::kNone;
442 
443     // We need granular fragments (as opposed to consolidated blobs) when animating, or when
444     // positioning on a path.
445     if (!fAnimators.empty()  || fPathInfo) flags |= Shaper::Flags::kFragmentGlyphs;
446     if (fRequiresAnchorPoint)              flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
447 
448     return flags;
449 }
450 
reshape()451 void TextAdapter::reshape() {
452     const Shaper::TextDesc text_desc = {
453         fText->fTypeface,
454         fText->fTextSize,
455         fText->fMinTextSize,
456         fText->fMaxTextSize,
457         fText->fLineHeight,
458         fText->fLineShift,
459         fText->fAscent,
460         fText->fHAlign,
461         fText->fVAlign,
462         fText->fResize,
463         fText->fLineBreak,
464         fText->fDirection,
465         fText->fCapitalization,
466         this->shaperFlags(),
467     };
468     const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
469 
470     if (fLogger) {
471         if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
472             const auto msg = SkStringPrintf("Text layout failed for '%s'.",
473                                             fText->fText.c_str());
474             fLogger->log(Logger::Level::kError, msg.c_str());
475         }
476 
477         if (shape_result.fMissingGlyphCount > 0) {
478             const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
479                                             shape_result.fMissingGlyphCount,
480                                             fText->fText.c_str());
481             fLogger->log(Logger::Level::kWarning, msg.c_str());
482         }
483 
484         // These may trigger repeatedly when the text is animating.
485         // To avoid spamming, only log once.
486         fLogger = nullptr;
487     }
488 
489     // Rebuild all fragments.
490     // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
491 
492     fRoot->clear();
493     fFragments.clear();
494 
495     for (const auto& frag : shape_result.fFragments) {
496         this->addFragment(frag);
497     }
498 
499     if (!fAnimators.empty() || fPathInfo) {
500         // Range selectors and text paths require fragment domain maps.
501         this->buildDomainMaps(shape_result);
502     }
503 
504     if (SHOW_LAYOUT_BOXES) {
505         auto box_color = sksg::Color::Make(0xffff0000);
506         box_color->setStyle(SkPaint::kStroke_Style);
507         box_color->setStrokeWidth(1);
508         box_color->setAntiAlias(true);
509 
510         auto bounds_color = sksg::Color::Make(0xff00ff00);
511         bounds_color->setStyle(SkPaint::kStroke_Style);
512         bounds_color->setStrokeWidth(1);
513         bounds_color->setAntiAlias(true);
514 
515         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
516                                          std::move(box_color)));
517         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
518                                          std::move(bounds_color)));
519 
520         if (fPathInfo) {
521             auto path_color = sksg::Color::Make(0xffffff00);
522             path_color->setStyle(SkPaint::kStroke_Style);
523             path_color->setStrokeWidth(1);
524             path_color->setAntiAlias(true);
525 
526             fRoot->addChild(
527                         sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
528                                          std::move(path_color)));
529         }
530     }
531 }
532 
onSync()533 void TextAdapter::onSync() {
534     if (!fText->fHasFill && !fText->fHasStroke) {
535         return;
536     }
537 
538     if (fText.hasChanged()) {
539         this->reshape();
540     }
541 
542     if (fFragments.empty()) {
543         return;
544     }
545 
546     // Update the path contour measure, if needed.
547     if (fPathInfo) {
548         fPathInfo->updateContourData();
549     }
550 
551     // Seed props from the current text value.
552     TextAnimator::ResolvedProps seed_props;
553     seed_props.fill_color   = fText->fFillColor;
554     seed_props.stroke_color = fText->fStrokeColor;
555 
556     TextAnimator::ModulatorBuffer buf;
557     buf.resize(fFragments.size(), { seed_props, 0 });
558 
559     // Apply all animators to the modulator buffer.
560     for (const auto& animator : fAnimators) {
561         animator->modulateProps(fMaps, buf);
562     }
563 
564     const TextAnimator::DomainMap* grouping_domain = nullptr;
565     switch (fAnchorPointGrouping) {
566         // for word/line grouping, we rely on domain map info
567         case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
568         case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
569         // remaining grouping modes (character/all) do not need (or have) domain map data
570         default: break;
571     }
572 
573     size_t grouping_span_index = 0;
574     SkV2           line_offset = { 0, 0 }; // cumulative line spacing
575 
576     // Finally, push all props to their corresponding fragment.
577     for (const auto& line_span : fMaps.fLinesMap) {
578         SkV2 line_spacing = { 0, 0 };
579         float line_tracking = 0;
580         bool line_has_tracking = false;
581 
582         // Tracking requires special treatment: unlike other props, its effect is not localized
583         // to a single fragment, but requires re-alignment of the whole line.
584         for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
585             // Track the grouping domain span in parallel.
586             if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
587                                         (*grouping_domain)[grouping_span_index].fCount) {
588                 grouping_span_index += 1;
589                 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
590                              (*grouping_domain)[grouping_span_index].fCount);
591             }
592 
593             const auto& props = buf[i].props;
594             const auto& frag  = fFragments[i];
595             this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage
596                                       grouping_domain ? &(*grouping_domain)[grouping_span_index]
597                                                         : nullptr);
598 
599             line_tracking += props.tracking;
600             line_has_tracking |= !SkScalarNearlyZero(props.tracking);
601 
602             line_spacing += props.line_spacing;
603         }
604 
605         // line spacing of the first line is ignored (nothing to "space" against)
606         if (&line_span != &fMaps.fLinesMap.front()) {
607             // For each line, the actual spacing is an average of individual fragment spacing
608             // (to preserve the "line").
609             line_offset += line_spacing / line_span.fCount;
610         }
611 
612         if (line_offset != SkV2{0, 0} || line_has_tracking) {
613             this->adjustLineProps(buf, line_span, line_offset, line_tracking);
614         }
615 
616     }
617 }
618 
fragmentAnchorPoint(const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const619 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
620                                       const SkV2& grouping_alignment,
621                                       const TextAnimator::DomainSpan* grouping_span) const {
622     // Construct the following 2x ascent box:
623     //
624     //      -------------
625     //     |             |
626     //     |             | ascent
627     //     |             |
628     // ----+-------------+---------- baseline
629     //   (pos)           |
630     //     |             | ascent
631     //     |             |
632     //      -------------
633     //         advance
634 
635     auto make_box = [](const SkPoint& pos, float advance, float ascent) {
636         // note: negative ascent
637         return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
638     };
639 
640     // Compute a grouping-dependent anchor point box.
641     // The default anchor point is at the center, and gets adjusted relative to the bounds
642     // based on |grouping_alignment|.
643     auto anchor_box = [&]() -> SkRect {
644         switch (fAnchorPointGrouping) {
645         case AnchorPointGrouping::kCharacter:
646             // Anchor box relative to each individual fragment.
647             return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
648         case AnchorPointGrouping::kWord:
649             // Fall through
650         case AnchorPointGrouping::kLine: {
651             SkASSERT(grouping_span);
652             // Anchor box relative to the first fragment in the word/line.
653             const auto& first_span_fragment = fFragments[grouping_span->fOffset];
654             return make_box(first_span_fragment.fOrigin,
655                             grouping_span->fAdvance,
656                             grouping_span->fAscent);
657         }
658         case AnchorPointGrouping::kAll:
659             // Anchor box is the same as the text box.
660             return fText->fBox;
661         }
662         SkUNREACHABLE;
663     };
664 
665     const auto ab = anchor_box();
666 
667     // Apply grouping alignment.
668     const auto ap = SkV2 { ab.centerX() + ab.width()  * 0.5f * grouping_alignment.x,
669                            ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
670 
671     // The anchor point is relative to the fragment position.
672     return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
673 }
674 
fragmentMatrix(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & anchor_point) const675 SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
676                                   const FragmentRec& rec, const SkV2& anchor_point) const {
677     const SkV3 pos = {
678         props.position.x + rec.fOrigin.fX + anchor_point.x,
679         props.position.y + rec.fOrigin.fY + anchor_point.y,
680         props.position.z
681     };
682 
683     if (!fPathInfo) {
684         return SkM44::Translate(pos.x, pos.y, pos.z);
685     }
686 
687     // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
688     const auto align_offset =
689             align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
690 
691     // Path positioning is based on the fragment position relative to the paragraph box
692     // upper-left corner:
693     //
694     //   - the horizontal component determines the distance on path
695     //
696     //   - the vertical component is post-applied after orienting on path
697     //
698     // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
699     //
700     const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
701     const auto path_distance = rel_pos.x + align_offset;
702 
703     return fPathInfo->getMatrix(path_distance, fText->fHAlign)
704          * SkM44::Translate(0, rel_pos.y, pos.z);
705 }
706 
pushPropsToFragment(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const707 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
708                                       const FragmentRec& rec,
709                                       const SkV2& grouping_alignment,
710                                       const TextAnimator::DomainSpan* grouping_span) const {
711     const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
712 
713     rec.fMatrixNode->setMatrix(
714                 this->fragmentMatrix(props, rec, anchor_point)
715               * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
716               * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
717               * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
718               * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
719               * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
720 
721     const auto scale_alpha = [](SkColor c, float o) {
722         return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
723     };
724 
725     if (rec.fFillColorNode) {
726         rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
727     }
728     if (rec.fStrokeColorNode) {
729         rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
730     }
731     if (rec.fBlur) {
732         rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
733                               props.blur.y * kBlurSizeToSigma });
734     }
735 }
736 
adjustLineProps(const TextAnimator::ModulatorBuffer & buf,const TextAnimator::DomainSpan & line_span,const SkV2 & line_offset,float total_tracking) const737 void TextAdapter::adjustLineProps(const TextAnimator::ModulatorBuffer& buf,
738                                   const TextAnimator::DomainSpan& line_span,
739                                   const SkV2& line_offset,
740                                   float total_tracking) const {
741     SkASSERT(line_span.fCount > 0);
742 
743     // AE tracking is defined per glyph, based on two components: |before| and |after|.
744     // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2.
745     //
746     // Tracking is applied as a local glyph offset, and contributes to the line width for alignment
747     // purposes.
748 
749     // The first glyph does not contribute |before| tracking, and the last one does not contribute
750     // |after| tracking.  Rather than spill this logic into applyAnimators, post-adjust here.
751     total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
752                               buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
753 
754     const auto align_offset = -total_tracking * align_factor(fText->fHAlign);
755 
756     float tracking_acc = 0;
757     for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
758         const auto& props = buf[i].props;
759 
760         // No |before| tracking for the first glyph, nor |after| tracking for the last one.
761         const auto track_before = i > line_span.fOffset
762                                     ? props.tracking * 0.5f : 0.0f,
763                    track_after  = i < line_span.fOffset + line_span.fCount - 1
764                                     ? props.tracking * 0.5f : 0.0f,
765                 fragment_offset = align_offset + tracking_acc + track_before;
766 
767         const auto& frag = fFragments[i];
768         const auto m = SkM44::Translate(line_offset.x + fragment_offset,
769                                         line_offset.y) *
770                        frag.fMatrixNode->getMatrix();
771         frag.fMatrixNode->setMatrix(m);
772 
773         tracking_acc += track_before + track_after;
774     }
775 }
776 
777 } // namespace skottie::internal
778