Update rive-cpp to 2.0 version
[platform/core/uifw/rive-tizen.git] / submodule / skia / modules / skottie / src / text / TextAdapter.cpp
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
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
70     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
93     float pathLength() const {
94         SkASSERT(fPath == fCurrentPath);
95         SkASSERT((fPathReverse != 0) == fCurrentReversed);
96
97         return fCurrentMeasure ? fCurrentMeasure->length() : 0;
98     }
99
100     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
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                 adapter->fRequiresLineAdjustments |= animator->requiresLineAdjustments();
240
241                 adapter->fAnimators.push_back(std::move(animator));
242             }
243         }
244     }
245
246     // Optional text path
247     const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
248         if (!jpath) {
249             return nullptr;
250         }
251
252         // the actual path is identified as an index in the layer mask stack
253         const auto mask_index =
254                 ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
255         const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
256         if (!jmasks || mask_index >= jmasks->size()) {
257             return nullptr;
258         }
259
260         const skjson::ObjectValue* mask = (*jmasks)[mask_index];
261         if (!mask) {
262             return nullptr;
263         }
264
265         auto pinfo = std::make_unique<PathInfo>();
266         adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
267         adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
268         adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
269         adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
270         adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);
271
272         // TODO: force align support
273
274         // Historically, these used to be exported as static properties.
275         // Attempt parsing both ways, for backward compat.
276         skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
277         skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);
278
279         // Path positioning requires anchor point info.
280         adapter->fRequiresAnchorPoint = true;
281
282         return pinfo;
283     };
284
285     adapter->fPathInfo = attach_path((*jt)["p"]);
286
287     abuilder->dispatchTextProperty(adapter);
288
289     return adapter;
290 }
291
292 TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg)
293     : fRoot(sksg::Group::Make())
294     , fFontMgr(std::move(fontmgr))
295     , fLogger(std::move(logger))
296     , fAnchorPointGrouping(apg)
297     , fHasBlurAnimator(false)
298     , fRequiresAnchorPoint(false)
299     , fRequiresLineAdjustments(false) {}
300
301 TextAdapter::~TextAdapter() = default;
302
303 void TextAdapter::addFragment(const Shaper::Fragment& frag, float scale) {
304     // For a given shaped fragment, build a corresponding SG fragment:
305     //
306     //   [TransformEffect] -> [Transform]
307     //     [Group]
308     //       [Draw] -> [TextBlob*] [FillPaint]
309     //       [Draw] -> [TextBlob*] [StrokePaint]
310     //
311     // * where the blob node is shared
312
313     auto blob_node = sksg::TextBlob::Make(frag.fBlob);
314
315     FragmentRec rec;
316     rec.fOrigin     = frag.fPos;
317     rec.fAdvance    = frag.fAdvance;
318     rec.fAscent     = frag.fAscent;
319     rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fPos.x(), frag.fPos.y()));
320
321     std::vector<sk_sp<sksg::RenderNode>> draws;
322     draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke));
323
324     SkASSERT(fText->fHasFill || fText->fHasStroke);
325
326     auto add_fill = [&]() {
327         if (fText->fHasFill) {
328             rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
329             rec.fFillColorNode->setAntiAlias(true);
330             draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
331         }
332     };
333     auto add_stroke = [&] {
334         if (fText->fHasStroke) {
335             rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
336             rec.fStrokeColorNode->setAntiAlias(true);
337             rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
338             rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth * scale);
339             rec.fStrokeColorNode->setStrokeJoin(fText->fStrokeJoin);
340             draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
341         }
342     };
343
344     if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
345         add_fill();
346         add_stroke();
347     } else {
348         add_stroke();
349         add_fill();
350     }
351
352     SkASSERT(!draws.empty());
353
354     if (SHOW_LAYOUT_BOXES) {
355         // visualize fragment ascent boxes
356         auto box_color = sksg::Color::Make(0xff0000ff);
357         box_color->setStyle(SkPaint::kStroke_Style);
358         box_color->setStrokeWidth(1);
359         box_color->setAntiAlias(true);
360         auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
361         draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
362     }
363
364     auto draws_node = (draws.size() > 1)
365             ? sksg::Group::Make(std::move(draws))
366             : std::move(draws[0]);
367
368     if (fHasBlurAnimator) {
369         // Optional blur effect.
370         rec.fBlur = sksg::BlurImageFilter::Make();
371         draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
372     }
373
374     fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
375     fFragments.push_back(std::move(rec));
376 }
377
378 void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
379     fMaps.fNonWhitespaceMap.clear();
380     fMaps.fWordsMap.clear();
381     fMaps.fLinesMap.clear();
382
383     size_t i          = 0,
384            line       = 0,
385            line_start = 0,
386            word_start = 0;
387
388     float word_advance = 0,
389           word_ascent  = 0,
390           line_advance = 0,
391           line_ascent  = 0;
392
393     bool in_word = false;
394
395     // TODO: use ICU for building the word map?
396     for (; i  < shape_result.fFragments.size(); ++i) {
397         const auto& frag = shape_result.fFragments[i];
398
399         if (frag.fIsWhitespace) {
400             if (in_word) {
401                 in_word = false;
402                 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
403             }
404         } else {
405             fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
406
407             if (!in_word) {
408                 in_word = true;
409                 word_start = i;
410                 word_advance = word_ascent = 0;
411             }
412
413             word_advance += frag.fAdvance;
414             word_ascent   = std::min(word_ascent, frag.fAscent); // negative ascent
415         }
416
417         if (frag.fLineIndex != line) {
418             SkASSERT(frag.fLineIndex == line + 1);
419             fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
420             line = frag.fLineIndex;
421             line_start = i;
422             line_advance = line_ascent = 0;
423         }
424
425         line_advance += frag.fAdvance;
426         line_ascent   = std::min(line_ascent, frag.fAscent); // negative ascent
427     }
428
429     if (i > word_start) {
430         fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
431     }
432
433     if (i > line_start) {
434         fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
435     }
436 }
437
438 void TextAdapter::setText(const TextValue& txt) {
439     fText.fCurrentValue = txt;
440     this->onSync();
441 }
442
443 uint32_t TextAdapter::shaperFlags() const {
444     uint32_t flags = Shaper::Flags::kNone;
445
446     // We need granular fragments (as opposed to consolidated blobs):
447     //   - when animating
448     //   - when positioning on a path
449     //   - when clamping the number or lines (for accurate line count)
450     if (!fAnimators.empty() || fPathInfo || fText->fMaxLines) {
451         flags |= Shaper::Flags::kFragmentGlyphs;
452     }
453
454     if (fRequiresAnchorPoint) {
455         flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
456     }
457
458     return flags;
459 }
460
461 void TextAdapter::reshape() {
462     const Shaper::TextDesc text_desc = {
463         fText->fTypeface,
464         fText->fTextSize,
465         fText->fMinTextSize,
466         fText->fMaxTextSize,
467         fText->fLineHeight,
468         fText->fLineShift,
469         fText->fAscent,
470         fText->fHAlign,
471         fText->fVAlign,
472         fText->fResize,
473         fText->fLineBreak,
474         fText->fDirection,
475         fText->fCapitalization,
476         fText->fMaxLines,
477         this->shaperFlags(),
478     };
479     const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
480
481     if (fLogger) {
482         if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
483             const auto msg = SkStringPrintf("Text layout failed for '%s'.",
484                                             fText->fText.c_str());
485             fLogger->log(Logger::Level::kError, msg.c_str());
486
487             // These may trigger repeatedly when the text is animating.
488             // To avoid spamming, only log once.
489             fLogger = nullptr;
490         }
491
492         if (shape_result.fMissingGlyphCount > 0) {
493             const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
494                                             shape_result.fMissingGlyphCount,
495                                             fText->fText.c_str());
496             fLogger->log(Logger::Level::kWarning, msg.c_str());
497             fLogger = nullptr;
498         }
499     }
500
501     // Rebuild all fragments.
502     // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
503
504     fRoot->clear();
505     fFragments.clear();
506
507     for (const auto& frag : shape_result.fFragments) {
508         this->addFragment(frag, shape_result.fScale);
509     }
510
511     if (!fAnimators.empty() || fPathInfo) {
512         // Range selectors and text paths require fragment domain maps.
513         this->buildDomainMaps(shape_result);
514     }
515
516     if (SHOW_LAYOUT_BOXES) {
517         auto box_color = sksg::Color::Make(0xffff0000);
518         box_color->setStyle(SkPaint::kStroke_Style);
519         box_color->setStrokeWidth(1);
520         box_color->setAntiAlias(true);
521
522         auto bounds_color = sksg::Color::Make(0xff00ff00);
523         bounds_color->setStyle(SkPaint::kStroke_Style);
524         bounds_color->setStrokeWidth(1);
525         bounds_color->setAntiAlias(true);
526
527         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
528                                          std::move(box_color)));
529         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
530                                          std::move(bounds_color)));
531
532         if (fPathInfo) {
533             auto path_color = sksg::Color::Make(0xffffff00);
534             path_color->setStyle(SkPaint::kStroke_Style);
535             path_color->setStrokeWidth(1);
536             path_color->setAntiAlias(true);
537
538             fRoot->addChild(
539                         sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
540                                          std::move(path_color)));
541         }
542     }
543 }
544
545 void TextAdapter::onSync() {
546     if (!fText->fHasFill && !fText->fHasStroke) {
547         return;
548     }
549
550     if (fText.hasChanged()) {
551         this->reshape();
552     }
553
554     if (fFragments.empty()) {
555         return;
556     }
557
558     // Update the path contour measure, if needed.
559     if (fPathInfo) {
560         fPathInfo->updateContourData();
561     }
562
563     // Seed props from the current text value.
564     TextAnimator::ResolvedProps seed_props;
565     seed_props.fill_color   = fText->fFillColor;
566     seed_props.stroke_color = fText->fStrokeColor;
567
568     TextAnimator::ModulatorBuffer buf;
569     buf.resize(fFragments.size(), { seed_props, 0 });
570
571     // Apply all animators to the modulator buffer.
572     for (const auto& animator : fAnimators) {
573         animator->modulateProps(fMaps, buf);
574     }
575
576     const TextAnimator::DomainMap* grouping_domain = nullptr;
577     switch (fAnchorPointGrouping) {
578         // for word/line grouping, we rely on domain map info
579         case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
580         case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
581         // remaining grouping modes (character/all) do not need (or have) domain map data
582         default: break;
583     }
584
585     size_t grouping_span_index = 0;
586     SkV2   current_line_offset = { 0, 0 }; // cumulative line spacing
587
588     auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf,
589                                          const TextAnimator::DomainSpan& line_span) {
590         SkV2  total_spacing  = {0,0};
591         float total_tracking = 0;
592
593         // Only compute these when needed.
594         if (fRequiresLineAdjustments) {
595             for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
596                 const auto& props = buf[i].props;
597                 total_spacing  += props.line_spacing;
598                 total_tracking += props.tracking;
599             }
600
601             // The first glyph does not contribute |before| tracking, and the last one does not
602             // contribute |after| tracking.
603             total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
604                                       buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
605         }
606
607         return std::make_tuple(total_spacing, total_tracking);
608     };
609
610     // Finally, push all props to their corresponding fragment.
611     for (const auto& line_span : fMaps.fLinesMap) {
612         const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span);
613         const auto align_offset = -line_tracking * align_factor(fText->fHAlign);
614
615         // line spacing of the first line is ignored (nothing to "space" against)
616         if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) {
617             // For each line, the actual spacing is an average of individual fragment spacing
618             // (to preserve the "line").
619             current_line_offset += line_spacing / line_span.fCount;
620         }
621
622         float tracking_acc = 0;
623         for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
624             // Track the grouping domain span in parallel.
625             if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
626                                         (*grouping_domain)[grouping_span_index].fCount) {
627                 grouping_span_index += 1;
628                 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
629                              (*grouping_domain)[grouping_span_index].fCount);
630             }
631
632             const auto& props = buf[i].props;
633             const auto& frag  = fFragments[i];
634
635             // AE tracking is defined per glyph, based on two components: |before| and |after|.
636             // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2.
637             //
638             // Tracking is applied as a local glyph offset, and contributes to the line width for
639             // alignment purposes.
640             //
641             // No |before| tracking for the first glyph, nor |after| tracking for the last one.
642             const auto track_before = i > line_span.fOffset
643                                         ? props.tracking * 0.5f : 0.0f,
644                        track_after  = i < line_span.fOffset + line_span.fCount - 1
645                                         ? props.tracking * 0.5f : 0.0f;
646
647             const auto frag_offset = current_line_offset +
648                                      SkV2{align_offset + tracking_acc + track_before, 0};
649
650             tracking_acc += track_before + track_after;
651
652             this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // %
653                                       grouping_domain ? &(*grouping_domain)[grouping_span_index]
654                                                         : nullptr);
655         }
656     }
657 }
658
659 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
660                                       const SkV2& grouping_alignment,
661                                       const TextAnimator::DomainSpan* grouping_span) const {
662     // Construct the following 2x ascent box:
663     //
664     //      -------------
665     //     |             |
666     //     |             | ascent
667     //     |             |
668     // ----+-------------+---------- baseline
669     //   (pos)           |
670     //     |             | ascent
671     //     |             |
672     //      -------------
673     //         advance
674
675     auto make_box = [](const SkPoint& pos, float advance, float ascent) {
676         // note: negative ascent
677         return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
678     };
679
680     // Compute a grouping-dependent anchor point box.
681     // The default anchor point is at the center, and gets adjusted relative to the bounds
682     // based on |grouping_alignment|.
683     auto anchor_box = [&]() -> SkRect {
684         switch (fAnchorPointGrouping) {
685         case AnchorPointGrouping::kCharacter:
686             // Anchor box relative to each individual fragment.
687             return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
688         case AnchorPointGrouping::kWord:
689             // Fall through
690         case AnchorPointGrouping::kLine: {
691             SkASSERT(grouping_span);
692             // Anchor box relative to the first fragment in the word/line.
693             const auto& first_span_fragment = fFragments[grouping_span->fOffset];
694             return make_box(first_span_fragment.fOrigin,
695                             grouping_span->fAdvance,
696                             grouping_span->fAscent);
697         }
698         case AnchorPointGrouping::kAll:
699             // Anchor box is the same as the text box.
700             return fText->fBox;
701         }
702         SkUNREACHABLE;
703     };
704
705     const auto ab = anchor_box();
706
707     // Apply grouping alignment.
708     const auto ap = SkV2 { ab.centerX() + ab.width()  * 0.5f * grouping_alignment.x,
709                            ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
710
711     // The anchor point is relative to the fragment position.
712     return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
713 }
714
715 SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
716                                   const FragmentRec& rec, const SkV2& frag_offset) const {
717     const SkV3 pos = {
718         props.position.x + rec.fOrigin.fX + frag_offset.x,
719         props.position.y + rec.fOrigin.fY + frag_offset.y,
720         props.position.z
721     };
722
723     if (!fPathInfo) {
724         return SkM44::Translate(pos.x, pos.y, pos.z);
725     }
726
727     // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
728     const auto align_offset =
729             align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
730
731     // Path positioning is based on the fragment position relative to the paragraph box
732     // upper-left corner:
733     //
734     //   - the horizontal component determines the distance on path
735     //
736     //   - the vertical component is post-applied after orienting on path
737     //
738     // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
739     //
740     const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
741     const auto path_distance = rel_pos.x + align_offset;
742
743     return fPathInfo->getMatrix(path_distance, fText->fHAlign)
744          * SkM44::Translate(0, rel_pos.y, pos.z);
745 }
746
747 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
748                                       const FragmentRec& rec,
749                                       const SkV2& frag_offset,
750                                       const SkV2& grouping_alignment,
751                                       const TextAnimator::DomainSpan* grouping_span) const {
752     const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
753
754     rec.fMatrixNode->setMatrix(
755                 this->fragmentMatrix(props, rec, anchor_point + frag_offset)
756               * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
757               * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
758               * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
759               * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
760               * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
761
762     const auto scale_alpha = [](SkColor c, float o) {
763         return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
764     };
765
766     if (rec.fFillColorNode) {
767         rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
768     }
769     if (rec.fStrokeColorNode) {
770         rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
771     }
772     if (rec.fBlur) {
773         rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
774                               props.blur.y * kBlurSizeToSigma });
775     }
776 }
777
778 } // namespace skottie::internal