• 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             // These may trigger repeatedly when the text is animating.
477             // To avoid spamming, only log once.
478             fLogger = nullptr;
479         }
480 
481         if (shape_result.fMissingGlyphCount > 0) {
482             const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
483                                             shape_result.fMissingGlyphCount,
484                                             fText->fText.c_str());
485             fLogger->log(Logger::Level::kWarning, msg.c_str());
486             fLogger = nullptr;
487         }
488     }
489 
490     // Rebuild all fragments.
491     // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
492 
493     fRoot->clear();
494     fFragments.clear();
495 
496     for (const auto& frag : shape_result.fFragments) {
497         this->addFragment(frag);
498     }
499 
500     if (!fAnimators.empty() || fPathInfo) {
501         // Range selectors and text paths require fragment domain maps.
502         this->buildDomainMaps(shape_result);
503     }
504 
505     if (SHOW_LAYOUT_BOXES) {
506         auto box_color = sksg::Color::Make(0xffff0000);
507         box_color->setStyle(SkPaint::kStroke_Style);
508         box_color->setStrokeWidth(1);
509         box_color->setAntiAlias(true);
510 
511         auto bounds_color = sksg::Color::Make(0xff00ff00);
512         bounds_color->setStyle(SkPaint::kStroke_Style);
513         bounds_color->setStrokeWidth(1);
514         bounds_color->setAntiAlias(true);
515 
516         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
517                                          std::move(box_color)));
518         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
519                                          std::move(bounds_color)));
520 
521         if (fPathInfo) {
522             auto path_color = sksg::Color::Make(0xffffff00);
523             path_color->setStyle(SkPaint::kStroke_Style);
524             path_color->setStrokeWidth(1);
525             path_color->setAntiAlias(true);
526 
527             fRoot->addChild(
528                         sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
529                                          std::move(path_color)));
530         }
531     }
532 }
533 
onSync()534 void TextAdapter::onSync() {
535     if (!fText->fHasFill && !fText->fHasStroke) {
536         return;
537     }
538 
539     if (fText.hasChanged()) {
540         this->reshape();
541     }
542 
543     if (fFragments.empty()) {
544         return;
545     }
546 
547     // Update the path contour measure, if needed.
548     if (fPathInfo) {
549         fPathInfo->updateContourData();
550     }
551 
552     // Seed props from the current text value.
553     TextAnimator::ResolvedProps seed_props;
554     seed_props.fill_color   = fText->fFillColor;
555     seed_props.stroke_color = fText->fStrokeColor;
556 
557     TextAnimator::ModulatorBuffer buf;
558     buf.resize(fFragments.size(), { seed_props, 0 });
559 
560     // Apply all animators to the modulator buffer.
561     for (const auto& animator : fAnimators) {
562         animator->modulateProps(fMaps, buf);
563     }
564 
565     const TextAnimator::DomainMap* grouping_domain = nullptr;
566     switch (fAnchorPointGrouping) {
567         // for word/line grouping, we rely on domain map info
568         case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
569         case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
570         // remaining grouping modes (character/all) do not need (or have) domain map data
571         default: break;
572     }
573 
574     size_t grouping_span_index = 0;
575     SkV2           line_offset = { 0, 0 }; // cumulative line spacing
576 
577     // Finally, push all props to their corresponding fragment.
578     for (const auto& line_span : fMaps.fLinesMap) {
579         SkV2 line_spacing = { 0, 0 };
580         float line_tracking = 0;
581         bool line_has_tracking = false;
582 
583         // Tracking requires special treatment: unlike other props, its effect is not localized
584         // to a single fragment, but requires re-alignment of the whole line.
585         for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
586             // Track the grouping domain span in parallel.
587             if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
588                                         (*grouping_domain)[grouping_span_index].fCount) {
589                 grouping_span_index += 1;
590                 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
591                              (*grouping_domain)[grouping_span_index].fCount);
592             }
593 
594             const auto& props = buf[i].props;
595             const auto& frag  = fFragments[i];
596             this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage
597                                       grouping_domain ? &(*grouping_domain)[grouping_span_index]
598                                                         : nullptr);
599 
600             line_tracking += props.tracking;
601             line_has_tracking |= !SkScalarNearlyZero(props.tracking);
602 
603             line_spacing += props.line_spacing;
604         }
605 
606         // line spacing of the first line is ignored (nothing to "space" against)
607         if (&line_span != &fMaps.fLinesMap.front()) {
608             // For each line, the actual spacing is an average of individual fragment spacing
609             // (to preserve the "line").
610             line_offset += line_spacing / line_span.fCount;
611         }
612 
613         if (line_offset != SkV2{0, 0} || line_has_tracking) {
614             this->adjustLineProps(buf, line_span, line_offset, line_tracking);
615         }
616 
617     }
618 }
619 
fragmentAnchorPoint(const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const620 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
621                                       const SkV2& grouping_alignment,
622                                       const TextAnimator::DomainSpan* grouping_span) const {
623     // Construct the following 2x ascent box:
624     //
625     //      -------------
626     //     |             |
627     //     |             | ascent
628     //     |             |
629     // ----+-------------+---------- baseline
630     //   (pos)           |
631     //     |             | ascent
632     //     |             |
633     //      -------------
634     //         advance
635 
636     auto make_box = [](const SkPoint& pos, float advance, float ascent) {
637         // note: negative ascent
638         return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
639     };
640 
641     // Compute a grouping-dependent anchor point box.
642     // The default anchor point is at the center, and gets adjusted relative to the bounds
643     // based on |grouping_alignment|.
644     auto anchor_box = [&]() -> SkRect {
645         switch (fAnchorPointGrouping) {
646         case AnchorPointGrouping::kCharacter:
647             // Anchor box relative to each individual fragment.
648             return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
649         case AnchorPointGrouping::kWord:
650             // Fall through
651         case AnchorPointGrouping::kLine: {
652             SkASSERT(grouping_span);
653             // Anchor box relative to the first fragment in the word/line.
654             const auto& first_span_fragment = fFragments[grouping_span->fOffset];
655             return make_box(first_span_fragment.fOrigin,
656                             grouping_span->fAdvance,
657                             grouping_span->fAscent);
658         }
659         case AnchorPointGrouping::kAll:
660             // Anchor box is the same as the text box.
661             return fText->fBox;
662         }
663         SkUNREACHABLE;
664     };
665 
666     const auto ab = anchor_box();
667 
668     // Apply grouping alignment.
669     const auto ap = SkV2 { ab.centerX() + ab.width()  * 0.5f * grouping_alignment.x,
670                            ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
671 
672     // The anchor point is relative to the fragment position.
673     return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
674 }
675 
fragmentMatrix(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & anchor_point) const676 SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
677                                   const FragmentRec& rec, const SkV2& anchor_point) const {
678     const SkV3 pos = {
679         props.position.x + rec.fOrigin.fX + anchor_point.x,
680         props.position.y + rec.fOrigin.fY + anchor_point.y,
681         props.position.z
682     };
683 
684     if (!fPathInfo) {
685         return SkM44::Translate(pos.x, pos.y, pos.z);
686     }
687 
688     // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
689     const auto align_offset =
690             align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
691 
692     // Path positioning is based on the fragment position relative to the paragraph box
693     // upper-left corner:
694     //
695     //   - the horizontal component determines the distance on path
696     //
697     //   - the vertical component is post-applied after orienting on path
698     //
699     // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
700     //
701     const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
702     const auto path_distance = rel_pos.x + align_offset;
703 
704     return fPathInfo->getMatrix(path_distance, fText->fHAlign)
705          * SkM44::Translate(0, rel_pos.y, pos.z);
706 }
707 
pushPropsToFragment(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const708 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
709                                       const FragmentRec& rec,
710                                       const SkV2& grouping_alignment,
711                                       const TextAnimator::DomainSpan* grouping_span) const {
712     const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
713 
714     rec.fMatrixNode->setMatrix(
715                 this->fragmentMatrix(props, rec, anchor_point)
716               * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
717               * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
718               * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
719               * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
720               * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
721 
722     const auto scale_alpha = [](SkColor c, float o) {
723         return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
724     };
725 
726     if (rec.fFillColorNode) {
727         rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
728     }
729     if (rec.fStrokeColorNode) {
730         rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
731     }
732     if (rec.fBlur) {
733         rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
734                               props.blur.y * kBlurSizeToSigma });
735     }
736 }
737 
adjustLineProps(const TextAnimator::ModulatorBuffer & buf,const TextAnimator::DomainSpan & line_span,const SkV2 & line_offset,float total_tracking) const738 void TextAdapter::adjustLineProps(const TextAnimator::ModulatorBuffer& buf,
739                                   const TextAnimator::DomainSpan& line_span,
740                                   const SkV2& line_offset,
741                                   float total_tracking) const {
742     SkASSERT(line_span.fCount > 0);
743 
744     // AE tracking is defined per glyph, based on two components: |before| and |after|.
745     // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2.
746     //
747     // Tracking is applied as a local glyph offset, and contributes to the line width for alignment
748     // purposes.
749 
750     // The first glyph does not contribute |before| tracking, and the last one does not contribute
751     // |after| tracking.  Rather than spill this logic into applyAnimators, post-adjust here.
752     total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
753                               buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
754 
755     const auto align_offset = -total_tracking * align_factor(fText->fHAlign);
756 
757     float tracking_acc = 0;
758     for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
759         const auto& props = buf[i].props;
760 
761         // No |before| tracking for the first glyph, nor |after| tracking for the last one.
762         const auto track_before = i > line_span.fOffset
763                                     ? props.tracking * 0.5f : 0.0f,
764                    track_after  = i < line_span.fOffset + line_span.fCount - 1
765                                     ? props.tracking * 0.5f : 0.0f,
766                 fragment_offset = align_offset + tracking_acc + track_before;
767 
768         const auto& frag = fFragments[i];
769         const auto m = SkM44::Translate(line_offset.x + fragment_offset,
770                                         line_offset.y) *
771                        frag.fMatrixNode->getMatrix();
772         frag.fMatrixNode->setMatrix(m);
773 
774         tracking_acc += track_before + track_after;
775     }
776 }
777 
778 } // namespace skottie::internal
779