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
29namespace skottie::internal {
30
31static 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//
63struct 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
152private:
153    // Cached contour data.
154    ShapeValue              fCurrentPath;
155    sk_sp<SkContourMeasure> fCurrentMeasure;
156    bool                    fCurrentReversed = false,
157                            fCurrentClosed   = false;
158};
159
160sk_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
291TextAdapter::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
299TextAdapter::~TextAdapter() = default;
300
301void 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
375void 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
435void TextAdapter::setText(const TextValue& txt) {
436    fText.fCurrentValue = txt;
437    this->onSync();
438}
439
440uint32_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
451void 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
533void 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
619SkV2 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
675SkM44 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
707void 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
737void 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