xref: /aosp_15_r20/external/skia/modules/skottie/src/text/TextAdapter.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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 #include "modules/skottie/src/text/TextAdapter.h"
8 
9 #include "include/core/SkCanvas.h"
10 #include "include/core/SkColor.h"
11 #include "include/core/SkContourMeasure.h"
12 #include "include/core/SkFont.h"
13 #include "include/core/SkFontMgr.h"
14 #include "include/core/SkM44.h"
15 #include "include/core/SkMatrix.h"
16 #include "include/core/SkPaint.h"
17 #include "include/core/SkPath.h"
18 #include "include/core/SkRect.h"
19 #include "include/core/SkScalar.h"
20 #include "include/core/SkSpan.h"
21 #include "include/core/SkString.h"
22 #include "include/core/SkTypes.h"
23 #include "include/private/base/SkTPin.h"
24 #include "include/private/base/SkTo.h"
25 #include "include/utils/SkTextUtils.h"
26 #include "modules/skottie/include/Skottie.h"
27 #include "modules/skottie/include/SkottieProperty.h"
28 #include "modules/skottie/src/SkottieJson.h"
29 #include "modules/skottie/src/SkottiePriv.h"
30 #include "modules/skottie/src/text/RangeSelector.h"  // IWYU pragma: keep
31 #include "modules/skottie/src/text/TextAnimator.h"
32 #include "modules/sksg/include/SkSGDraw.h"
33 #include "modules/sksg/include/SkSGGeometryNode.h"
34 #include "modules/sksg/include/SkSGGroup.h"
35 #include "modules/sksg/include/SkSGPaint.h"
36 #include "modules/sksg/include/SkSGPath.h"
37 #include "modules/sksg/include/SkSGRect.h"
38 #include "modules/sksg/include/SkSGRenderEffect.h"
39 #include "modules/sksg/include/SkSGRenderNode.h"
40 #include "modules/sksg/include/SkSGTransform.h"
41 #include "modules/sksg/src/SkSGTransformPriv.h"
42 #include "modules/skshaper/include/SkShaper_factory.h"
43 #include "src/utils/SkJSON.h"
44 
45 #include <algorithm>
46 #include <cmath>
47 #include <cstddef>
48 #include <limits>
49 #include <tuple>
50 #include <utility>
51 
52 namespace sksg {
53 class InvalidationController;
54 }
55 
56 // Enable for text layout debugging.
57 #define SHOW_LAYOUT_BOXES 0
58 
59 namespace skottie::internal {
60 
61 namespace {
62 
63 class GlyphTextNode final : public sksg::GeometryNode {
64 public:
GlyphTextNode(Shaper::ShapedGlyphs && glyphs)65     explicit GlyphTextNode(Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {}
66 
67     ~GlyphTextNode() override = default;
68 
glyphs() const69     const Shaper::ShapedGlyphs* glyphs() const { return &fGlyphs; }
70 
71 protected:
onRevalidate(sksg::InvalidationController *,const SkMatrix &)72     SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
73         return fGlyphs.computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight);
74     }
75 
onDraw(SkCanvas * canvas,const SkPaint & paint) const76     void onDraw(SkCanvas* canvas, const SkPaint& paint) const override {
77         fGlyphs.draw(canvas, {0,0}, paint);
78     }
79 
onClip(SkCanvas * canvas,bool antiAlias) const80     void onClip(SkCanvas* canvas, bool antiAlias) const override {
81         canvas->clipPath(this->asPath(), antiAlias);
82     }
83 
onContains(const SkPoint & p) const84     bool onContains(const SkPoint& p) const override {
85         return this->asPath().contains(p.x(), p.y());
86     }
87 
onAsPath() const88     SkPath onAsPath() const override {
89         // TODO
90         return SkPath();
91     }
92 
93 private:
94     const Shaper::ShapedGlyphs fGlyphs;
95 };
96 
align_factor(SkTextUtils::Align a)97 static float align_factor(SkTextUtils::Align a) {
98     switch (a) {
99         case SkTextUtils::kLeft_Align  : return 0.0f;
100         case SkTextUtils::kCenter_Align: return 0.5f;
101         case SkTextUtils::kRight_Align : return 1.0f;
102     }
103 
104     SkUNREACHABLE;
105 }
106 
107 } // namespace
108 
109 class TextAdapter::GlyphDecoratorNode final : public sksg::Group {
110 public:
GlyphDecoratorNode(sk_sp<GlyphDecorator> decorator,float scale)111     GlyphDecoratorNode(sk_sp<GlyphDecorator> decorator, float scale)
112         : fDecorator(std::move(decorator))
113         , fScale(scale)
114     {}
115 
116     ~GlyphDecoratorNode() override = default;
117 
updateFragmentData(const std::vector<TextAdapter::FragmentRec> & recs)118     void updateFragmentData(const std::vector<TextAdapter::FragmentRec>& recs) {
119         fFragCount = recs.size();
120 
121         SkASSERT(!fFragInfo);
122         fFragInfo = std::make_unique<FragmentInfo[]>(recs.size());
123 
124         for (size_t i = 0; i < recs.size(); ++i) {
125             const auto& rec = recs[i];
126             fFragInfo[i] = {rec.fGlyphs, rec.fMatrixNode, rec.fAdvance};
127         }
128 
129         SkASSERT(!fDecoratorInfo);
130         fDecoratorInfo = std::make_unique<GlyphDecorator::GlyphInfo[]>(recs.size());
131     }
132 
onRevalidate(sksg::InvalidationController * ic,const SkMatrix & ctm)133     SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
134         const auto child_bounds = INHERITED::onRevalidate(ic, ctm);
135 
136         for (size_t i = 0; i < fFragCount; ++i) {
137             const auto* glyphs = fFragInfo[i].fGlyphs;
138             fDecoratorInfo[i].fBounds =
139                     glyphs->computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight);
140             fDecoratorInfo[i].fMatrix = sksg::TransformPriv::As<SkMatrix>(fFragInfo[i].fMatrixNode);
141 
142             fDecoratorInfo[i].fCluster = glyphs->fClusters.empty() ? 0 : glyphs->fClusters.front();
143             fDecoratorInfo[i].fAdvance = fFragInfo[i].fAdvance;
144         }
145 
146         return child_bounds;
147     }
148 
onRender(SkCanvas * canvas,const RenderContext * ctx) const149     void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
150         auto local_ctx = ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(),
151                                                                        canvas->getTotalMatrix(),
152                                                                        true);
153         this->INHERITED::onRender(canvas, local_ctx);
154 
155         fDecorator->onDecorate(canvas, {
156             SkSpan(fDecoratorInfo.get(), fFragCount),
157             fScale
158         });
159     }
160 
161 private:
162     struct FragmentInfo {
163         const Shaper::ShapedGlyphs* fGlyphs;
164         sk_sp<sksg::Matrix<SkM44>>  fMatrixNode;
165         float                       fAdvance;
166     };
167 
168     const sk_sp<GlyphDecorator>                  fDecorator;
169     const float                                  fScale;
170 
171     std::unique_ptr<FragmentInfo[]>              fFragInfo;
172     std::unique_ptr<GlyphDecorator::GlyphInfo[]> fDecoratorInfo;
173     size_t                                       fFragCount;
174 
175     using INHERITED = Group;
176 };
177 
178 // Text path semantics
179 //
180 //   * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
181 //     a distance along the path
182 //
183 //   * horizontal alignment is applied relative to the path start/end points
184 //
185 //   * "Reverse Path" allows reversing the path direction
186 //
187 //   * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular
188 //      to the path tangent, or not (just positioned).
189 //
190 //   * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path,
191 //     depending on horizontal alignement:
192 //       - left:   offset = first margin
193 //       - center: offset = first margin + last margin
194 //       - right:  offset = last margin
195 //
196 //   * extranormal path positions (d < 0, d > path len) are allowed
197 //       - closed path: the position wraps around in both directions
198 //       - open path: extrapolates from extremes' positions/slopes
199 //
200 struct TextAdapter::PathInfo {
201     ShapeValue  fPath;
202     ScalarValue fPathFMargin       = 0,
203                 fPathLMargin       = 0,
204                 fPathPerpendicular = 0,
205                 fPathReverse       = 0;
206 
updateContourDataskottie::internal::TextAdapter::PathInfo207     void updateContourData() {
208         const auto reverse = fPathReverse != 0;
209 
210         if (fPath != fCurrentPath || reverse != fCurrentReversed) {
211             // reinitialize cached contour data
212             auto path = static_cast<SkPath>(fPath);
213             if (reverse) {
214                 SkPath reversed;
215                 reversed.reverseAddPath(path);
216                 path = reversed;
217             }
218 
219             SkContourMeasureIter iter(path, /*forceClosed = */false);
220             fCurrentMeasure  = iter.next();
221             fCurrentClosed   = path.isLastContourClosed();
222             fCurrentReversed = reverse;
223             fCurrentPath     = fPath;
224 
225             // AE paths are always single-contour (no moves allowed).
226             SkASSERT(!iter.next());
227         }
228     }
229 
pathLengthskottie::internal::TextAdapter::PathInfo230     float pathLength() const {
231         SkASSERT(fPath == fCurrentPath);
232         SkASSERT((fPathReverse != 0) == fCurrentReversed);
233 
234         return fCurrentMeasure ? fCurrentMeasure->length() : 0;
235     }
236 
getMatrixskottie::internal::TextAdapter::PathInfo237     SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const {
238         SkASSERT(fPath == fCurrentPath);
239         SkASSERT((fPathReverse != 0) == fCurrentReversed);
240 
241         if (!fCurrentMeasure) {
242             return SkM44();
243         }
244 
245         const auto path_len = fCurrentMeasure->length();
246 
247         // First/last margin adjustment also depends on alignment.
248         switch (alignment) {
249             case SkTextUtils::Align::kLeft_Align:   distance += fPathFMargin; break;
250             case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
251                                                                 fPathLMargin; break;
252             case SkTextUtils::Align::kRight_Align:  distance += fPathLMargin; break;
253         }
254 
255         // For closed paths, extranormal distances wrap around the contour.
256         if (fCurrentClosed) {
257             distance = std::fmod(distance, path_len);
258             if (distance < 0) {
259                 distance += path_len;
260             }
261             SkASSERT(0 <= distance && distance <= path_len);
262         }
263 
264         SkPoint pos;
265         SkVector tan;
266         if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) {
267             return SkM44();
268         }
269 
270         // For open paths, extranormal distances are extrapolated from extremes.
271         // Note:
272         //   - getPosTan above clamps to the extremes
273         //   - the extrapolation below only kicks in for extranormal values
274         const auto underflow = std::min(0.0f, distance),
275                    overflow  = std::max(0.0f, distance - path_len);
276         pos += tan*(underflow + overflow);
277 
278         auto m = SkM44::Translate(pos.x(), pos.y());
279 
280         // The "perpendicular" flag controls whether fragments are positioned and rotated,
281         // or just positioned.
282         if (fPathPerpendicular != 0) {
283             m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x()));
284         }
285 
286         return m;
287     }
288 
289 private:
290     // Cached contour data.
291     ShapeValue              fCurrentPath;
292     sk_sp<SkContourMeasure> fCurrentMeasure;
293     bool                    fCurrentReversed = false,
294                             fCurrentClosed   = false;
295 };
296 
Make(const skjson::ObjectValue & jlayer,const AnimationBuilder * abuilder,sk_sp<SkFontMgr> fontmgr,sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,sk_sp<Logger> logger,sk_sp<::SkShapers::Factory> factory)297 sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
298                                      const AnimationBuilder* abuilder,
299                                      sk_sp<SkFontMgr> fontmgr,
300                                      sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
301                                      sk_sp<Logger> logger,
302                                      sk_sp<::SkShapers::Factory> factory) {
303     // General text node format:
304     // "t": {
305     //    "a": [], // animators (see TextAnimator)
306     //    "d": {
307     //        "k": [
308     //            {
309     //                "s": {
310     //                    "f": "Roboto-Regular",
311     //                    "fc": [
312     //                        0.42,
313     //                        0.15,
314     //                        0.15
315     //                    ],
316     //                    "j": 1,
317     //                    "lh": 60,
318     //                    "ls": 0,
319     //                    "s": 50,
320     //                    "t": "text align right",
321     //                    "tr": 0
322     //                },
323     //                "t": 0
324     //            }
325     //        ],
326     //        "sid": "optionalSlotID"
327     //    },
328     //    "m": { // more options
329     //           "g": 1,     // Anchor Point Grouping
330     //           "a": {...}  // Grouping Alignment
331     //         },
332     //    "p": { // path options
333     //           "a": 0,   // force alignment
334     //           "f": {},  // first margin
335     //           "l": {},  // last margin
336     //           "m": 1,   // mask index
337     //           "p": 1,   // perpendicular
338     //           "r": 0    // reverse path
339     //         }
340 
341     // },
342 
343     const skjson::ObjectValue* jt = jlayer["t"];
344     const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
345                                        : nullptr;
346     if (!jd) {
347         abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
348         return nullptr;
349     }
350 
351     // "More options"
352     const skjson::ObjectValue* jm = (*jt)["m"];
353     static constexpr AnchorPointGrouping gGroupingMap[] = {
354         AnchorPointGrouping::kCharacter, // 'g': 1
355         AnchorPointGrouping::kWord,      // 'g': 2
356         AnchorPointGrouping::kLine,      // 'g': 3
357         AnchorPointGrouping::kAll,       // 'g': 4
358     };
359     const auto apg = jm
360             ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, std::size(gGroupingMap))
361             : 1;
362 
363     auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
364                                                       std::move(custom_glyph_mapper),
365                                                       std::move(logger),
366                                                       std::move(factory),
367                                                       gGroupingMap[SkToSizeT(apg - 1)]));
368 
369     adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
370     if (jm) {
371         adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
372     }
373 
374     // Animators
375     if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
376         adapter->fAnimators.reserve(janimators->size());
377 
378         for (const skjson::ObjectValue* janimator : *janimators) {
379             if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
380                 adapter->fHasBlurAnimator         |= animator->hasBlur();
381                 adapter->fRequiresAnchorPoint     |= animator->requiresAnchorPoint();
382                 adapter->fRequiresLineAdjustments |= animator->requiresLineAdjustments();
383 
384                 adapter->fAnimators.push_back(std::move(animator));
385             }
386         }
387     }
388 
389     // Optional text path
390     const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
391         if (!jpath) {
392             return nullptr;
393         }
394 
395         // the actual path is identified as an index in the layer mask stack
396         const auto mask_index =
397                 ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
398         const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
399         if (!jmasks || mask_index >= jmasks->size()) {
400             return nullptr;
401         }
402 
403         const skjson::ObjectValue* mask = (*jmasks)[mask_index];
404         if (!mask) {
405             return nullptr;
406         }
407 
408         auto pinfo = std::make_unique<PathInfo>();
409         adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
410         adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
411         adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
412         adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
413         adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);
414 
415         // TODO: force align support
416 
417         // Historically, these used to be exported as static properties.
418         // Attempt parsing both ways, for backward compat.
419         skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
420         skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);
421 
422         // Path positioning requires anchor point info.
423         adapter->fRequiresAnchorPoint = true;
424 
425         return pinfo;
426     };
427 
428     adapter->fPathInfo = attach_path((*jt)["p"]);
429     abuilder->dispatchTextProperty(adapter, jd);
430 
431     return adapter;
432 }
433 
TextAdapter(sk_sp<SkFontMgr> fontmgr,sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,sk_sp<Logger> logger,sk_sp<SkShapers::Factory> factory,AnchorPointGrouping apg)434 TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr,
435                          sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
436                          sk_sp<Logger> logger,
437                          sk_sp<SkShapers::Factory> factory,
438                          AnchorPointGrouping apg)
439     : fRoot(sksg::Group::Make())
440     , fFontMgr(std::move(fontmgr))
441     , fCustomGlyphMapper(std::move(custom_glyph_mapper))
442     , fLogger(std::move(logger))
443     , fShapingFactory(std::move(factory))
444     , fAnchorPointGrouping(apg)
445     , fHasBlurAnimator(false)
446     , fRequiresAnchorPoint(false)
447     , fRequiresLineAdjustments(false) {}
448 
449 TextAdapter::~TextAdapter() = default;
450 
451 std::vector<sk_sp<sksg::RenderNode>>
buildGlyphCompNodes(Shaper::ShapedGlyphs & glyphs) const452 TextAdapter::buildGlyphCompNodes(Shaper::ShapedGlyphs& glyphs) const {
453     std::vector<sk_sp<sksg::RenderNode>> draws;
454 
455     if (fCustomGlyphMapper) {
456         size_t run_offset = 0;
457         for (auto& run : glyphs.fRuns) {
458             for (size_t i = 0; i < run.fSize; ++i) {
459                 const size_t goffset = run_offset + i;
460                 const SkGlyphID  gid = glyphs.fGlyphIDs[goffset];
461 
462                 if (auto gcomp = fCustomGlyphMapper->getGlyphComp(run.fFont.getTypeface(), gid)) {
463                     // Position and scale the "glyph".
464                     const auto m = SkMatrix::Translate(glyphs.fGlyphPos[goffset])
465                                  * SkMatrix::Scale(fText->fTextSize*fTextShapingScale,
466                                                    fText->fTextSize*fTextShapingScale);
467 
468                     draws.push_back(sksg::TransformEffect::Make(std::move(gcomp), m));
469 
470                     // Remove all related data from the fragment, so we don't attempt to render
471                     // this as a regular glyph.
472                     SkASSERT(glyphs.fGlyphIDs.size() > goffset);
473                     glyphs.fGlyphIDs.erase(glyphs.fGlyphIDs.begin() + goffset);
474                     SkASSERT(glyphs.fGlyphPos.size() > goffset);
475                     glyphs.fGlyphPos.erase(glyphs.fGlyphPos.begin() + goffset);
476                     if (!glyphs.fClusters.empty()) {
477                         SkASSERT(glyphs.fClusters.size() > goffset);
478                         glyphs.fClusters.erase(glyphs.fClusters.begin() + goffset);
479                     }
480                     i         -= 1;
481                     run.fSize -= 1;
482                 }
483             }
484             run_offset += run.fSize;
485         }
486     }
487 
488     return draws;
489 }
490 
addFragment(Shaper::Fragment & frag,sksg::Group * container)491 void TextAdapter::addFragment(Shaper::Fragment& frag, sksg::Group* container) {
492     // For a given shaped fragment, build a corresponding SG fragment:
493     //
494     //   [TransformEffect] -> [Transform]
495     //     [Group]
496     //       [Draw] -> [GlyphTextNode*] [FillPaint]    // SkTypeface-based glyph.
497     //       [Draw] -> [GlyphTextNode*] [StrokePaint]  // SkTypeface-based glyph.
498     //       [CompRenderTree]                          // Comp glyph.
499     //       ...
500     //
501 
502     FragmentRec rec;
503     rec.fOrigin     = frag.fOrigin;
504     rec.fAdvance    = frag.fAdvance;
505     rec.fAscent     = frag.fAscent;
506     rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fOrigin.x(),
507                                                                  frag.fOrigin.y()));
508 
509     // Start off substituting existing comp nodes for all composition-based glyphs.
510     std::vector<sk_sp<sksg::RenderNode>> draws = this->buildGlyphCompNodes(frag.fGlyphs);
511 
512     // Use a regular GlyphTextNode for the remaining glyphs (backed by a real SkTypeface).
513     auto text_node = sk_make_sp<GlyphTextNode>(std::move(frag.fGlyphs));
514     rec.fGlyphs = text_node->glyphs();
515 
516     draws.reserve(draws.size() +
517                   static_cast<size_t>(fText->fHasFill) +
518                   static_cast<size_t>(fText->fHasStroke));
519 
520     SkASSERT(fText->fHasFill || fText->fHasStroke);
521 
522     auto add_fill = [&]() {
523         if (fText->fHasFill) {
524             rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
525             rec.fFillColorNode->setAntiAlias(true);
526             draws.push_back(sksg::Draw::Make(text_node, rec.fFillColorNode));
527         }
528     };
529     auto add_stroke = [&] {
530         if (fText->fHasStroke) {
531             rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
532             rec.fStrokeColorNode->setAntiAlias(true);
533             rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
534             rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth * fTextShapingScale);
535             rec.fStrokeColorNode->setStrokeJoin(fText->fStrokeJoin);
536             draws.push_back(sksg::Draw::Make(text_node, rec.fStrokeColorNode));
537         }
538     };
539 
540     if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
541         add_fill();
542         add_stroke();
543     } else {
544         add_stroke();
545         add_fill();
546     }
547 
548     SkASSERT(!draws.empty());
549 
550     if (SHOW_LAYOUT_BOXES) {
551         // visualize fragment ascent boxes
552         auto box_color = sksg::Color::Make(0xff0000ff);
553         box_color->setStyle(SkPaint::kStroke_Style);
554         box_color->setStrokeWidth(1);
555         box_color->setAntiAlias(true);
556         auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
557         draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
558     }
559 
560     draws.shrink_to_fit();
561 
562     auto draws_node = (draws.size() > 1)
563             ? sksg::Group::Make(std::move(draws))
564             : std::move(draws[0]);
565 
566     if (fHasBlurAnimator) {
567         // Optional blur effect.
568         rec.fBlur = sksg::BlurImageFilter::Make();
569         draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
570     }
571 
572     container->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
573     fFragments.push_back(std::move(rec));
574 }
575 
buildDomainMaps(const Shaper::Result & shape_result)576 void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
577     fMaps.fNonWhitespaceMap.clear();
578     fMaps.fWordsMap.clear();
579     fMaps.fLinesMap.clear();
580 
581     size_t i          = 0,
582            line       = 0,
583            line_start = 0,
584            word_start = 0;
585 
586     float word_advance = 0,
587           word_ascent  = 0,
588           line_advance = 0,
589           line_ascent  = 0;
590 
591     bool in_word = false;
592 
593     // TODO: use ICU for building the word map?
594     for (; i  < shape_result.fFragments.size(); ++i) {
595         const auto& frag = shape_result.fFragments[i];
596         const bool is_new_line = frag.fLineIndex != line;
597 
598         if (frag.fIsWhitespace || is_new_line) {
599             // Both whitespace and new lines break words.
600             if (in_word) {
601                 in_word = false;
602                 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
603             }
604         }
605 
606         if (!frag.fIsWhitespace) {
607             fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
608 
609             if (!in_word) {
610                 in_word = true;
611                 word_start = i;
612                 word_advance = word_ascent = 0;
613             }
614 
615             word_advance += frag.fAdvance;
616             word_ascent   = std::min(word_ascent, frag.fAscent); // negative ascent
617         }
618 
619         if (is_new_line) {
620             SkASSERT(frag.fLineIndex == line + 1);
621             fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
622             line = frag.fLineIndex;
623             line_start = i;
624             line_advance = line_ascent = 0;
625         }
626 
627         line_advance += frag.fAdvance;
628         line_ascent   = std::min(line_ascent, frag.fAscent); // negative ascent
629     }
630 
631     if (i > word_start) {
632         fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
633     }
634 
635     if (i > line_start) {
636         fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
637     }
638 }
639 
setText(const TextValue & txt)640 void TextAdapter::setText(const TextValue& txt) {
641     fText.fCurrentValue = txt;
642     this->onSync();
643 }
644 
shaperFlags() const645 uint32_t TextAdapter::shaperFlags() const {
646     uint32_t flags = Shaper::Flags::kNone;
647 
648     // We need granular fragments (as opposed to consolidated blobs):
649     //   - when animating
650     //   - when positioning on a path
651     //   - when clamping the number or lines (for accurate line count)
652     //   - when a text decorator is present
653     if (!fAnimators.empty() || fPathInfo || fText->fMaxLines || fText->fDecorator) {
654         flags |= Shaper::Flags::kFragmentGlyphs;
655     }
656 
657     if (fRequiresAnchorPoint || fText->fDecorator) {
658         flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
659     }
660 
661     if (fText->fDecorator) {
662         flags |= Shaper::Flags::kClusters;
663     }
664 
665     return flags;
666 }
667 
reshape()668 void TextAdapter::reshape() {
669     // AE clamps the font size to a reasonable range.
670     // We do the same, since HB is susceptible to int overflows for degenerate values.
671     static constexpr float kMinSize =    0.1f,
672                            kMaxSize = 1296.0f;
673     const Shaper::TextDesc text_desc = {
674         fText->fTypeface,
675         SkTPin(fText->fTextSize,    kMinSize, kMaxSize),
676         SkTPin(fText->fMinTextSize, kMinSize, kMaxSize),
677         SkTPin(fText->fMaxTextSize, kMinSize, kMaxSize),
678         fText->fLineHeight,
679         fText->fLineShift,
680         fText->fAscent,
681         fText->fHAlign,
682         fText->fVAlign,
683         fText->fResize,
684         fText->fLineBreak,
685         fText->fDirection,
686         fText->fCapitalization,
687         fText->fMaxLines,
688         this->shaperFlags(),
689         fText->fLocale.isEmpty()     ? nullptr : fText->fLocale.c_str(),
690         fText->fFontFamily.isEmpty() ? nullptr : fText->fFontFamily.c_str(),
691     };
692     auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr,
693         fShapingFactory);
694 
695     if (fLogger) {
696         if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
697             const auto msg = SkStringPrintf("Text layout failed for '%s'.",
698                                             fText->fText.c_str());
699             fLogger->log(Logger::Level::kError, msg.c_str());
700 
701             // These may trigger repeatedly when the text is animating.
702             // To avoid spamming, only log once.
703             fLogger = nullptr;
704         }
705 
706         if (shape_result.fMissingGlyphCount > 0) {
707             const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
708                                             shape_result.fMissingGlyphCount,
709                                             fText->fText.c_str());
710             fLogger->log(Logger::Level::kWarning, msg.c_str());
711             fLogger = nullptr;
712         }
713     }
714 
715     // Save the text shaping scale for later adjustments.
716     fTextShapingScale = shape_result.fScale;
717 
718     // Rebuild all fragments.
719     // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
720 
721     fRoot->clear();
722     fFragments.clear();
723 
724     if (SHOW_LAYOUT_BOXES) {
725         auto box_color = sksg::Color::Make(0xffff0000);
726         box_color->setStyle(SkPaint::kStroke_Style);
727         box_color->setStrokeWidth(1);
728         box_color->setAntiAlias(true);
729 
730         auto bounds_color = sksg::Color::Make(0xff00ff00);
731         bounds_color->setStyle(SkPaint::kStroke_Style);
732         bounds_color->setStrokeWidth(1);
733         bounds_color->setAntiAlias(true);
734 
735         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
736                                          std::move(box_color)));
737         fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
738                                          std::move(bounds_color)));
739 
740         if (fPathInfo) {
741             auto path_color = sksg::Color::Make(0xffffff00);
742             path_color->setStyle(SkPaint::kStroke_Style);
743             path_color->setStrokeWidth(1);
744             path_color->setAntiAlias(true);
745 
746             fRoot->addChild(
747                         sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
748                                          std::move(path_color)));
749         }
750     }
751 
752     // Depending on whether a GlyphDecorator is present, we either add the glyph render nodes
753     // directly to the root group, or to an intermediate GlyphDecoratorNode container.
754     sksg::Group* container = fRoot.get();
755     sk_sp<GlyphDecoratorNode> decorator_node;
756     if (fText->fDecorator) {
757         decorator_node = sk_make_sp<GlyphDecoratorNode>(fText->fDecorator, fTextShapingScale);
758         container = decorator_node.get();
759     }
760 
761     // N.B. addFragment moves shaped glyph data out of the fragment, so only the fragment
762     // metrics are valid after this block.
763     for (size_t i = 0; i < shape_result.fFragments.size(); ++i) {
764         this->addFragment(shape_result.fFragments[i], container);
765     }
766 
767     if (decorator_node) {
768         decorator_node->updateFragmentData(fFragments);
769         fRoot->addChild(std::move(decorator_node));
770     }
771 
772     if (!fAnimators.empty() || fPathInfo) {
773         // Range selectors and text paths require fragment domain maps.
774         this->buildDomainMaps(shape_result);
775     }
776 }
777 
onSync()778 void TextAdapter::onSync() {
779     if (!fText->fHasFill && !fText->fHasStroke) {
780         return;
781     }
782 
783     if (fText.hasChanged()) {
784         this->reshape();
785     }
786 
787     if (fFragments.empty()) {
788         return;
789     }
790 
791     // Update the path contour measure, if needed.
792     if (fPathInfo) {
793         fPathInfo->updateContourData();
794     }
795 
796     // Seed props from the current text value.
797     TextAnimator::ResolvedProps seed_props;
798     seed_props.fill_color   = fText->fFillColor;
799     seed_props.stroke_color = fText->fStrokeColor;
800     seed_props.stroke_width = fText->fStrokeWidth;
801 
802     TextAnimator::ModulatorBuffer buf;
803     buf.resize(fFragments.size(), { seed_props, 0 });
804 
805     // Apply all animators to the modulator buffer.
806     for (const auto& animator : fAnimators) {
807         animator->modulateProps(fMaps, buf);
808     }
809 
810     const TextAnimator::DomainMap* grouping_domain = nullptr;
811     switch (fAnchorPointGrouping) {
812         // for word/line grouping, we rely on domain map info
813         case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
814         case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
815         // remaining grouping modes (character/all) do not need (or have) domain map data
816         default: break;
817     }
818 
819     size_t grouping_span_index = 0;
820     SkV2   current_line_offset = { 0, 0 }; // cumulative line spacing
821 
822     auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf,
823                                          const TextAnimator::DomainSpan& line_span) {
824         SkV2  total_spacing  = {0,0};
825         float total_tracking = 0;
826 
827         // Only compute these when needed.
828         if (fRequiresLineAdjustments && line_span.fCount) {
829             for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
830                 const auto& props = buf[i].props;
831                 total_spacing  += props.line_spacing;
832                 total_tracking += props.tracking;
833             }
834 
835             // The first glyph does not contribute |before| tracking, and the last one does not
836             // contribute |after| tracking.
837             total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
838                                       buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
839         }
840 
841         return std::make_tuple(total_spacing, total_tracking);
842     };
843 
844     // Finally, push all props to their corresponding fragment.
845     for (const auto& line_span : fMaps.fLinesMap) {
846         const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span);
847         const auto align_offset = -line_tracking * align_factor(fText->fHAlign);
848 
849         // line spacing of the first line is ignored (nothing to "space" against)
850         if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) {
851             // For each line, the actual spacing is an average of individual fragment spacing
852             // (to preserve the "line").
853             current_line_offset += line_spacing / line_span.fCount;
854         }
855 
856         float tracking_acc = 0;
857         for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
858             // Track the grouping domain span in parallel.
859             if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
860                                         (*grouping_domain)[grouping_span_index].fCount) {
861                 grouping_span_index += 1;
862                 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
863                              (*grouping_domain)[grouping_span_index].fCount);
864             }
865 
866             const auto& props = buf[i].props;
867             const auto& frag  = fFragments[i];
868 
869             // AE tracking is defined per glyph, based on two components: |before| and |after|.
870             // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2.
871             //
872             // Tracking is applied as a local glyph offset, and contributes to the line width for
873             // alignment purposes.
874             //
875             // No |before| tracking for the first glyph, nor |after| tracking for the last one.
876             const auto track_before = i > line_span.fOffset
877                                         ? props.tracking * 0.5f : 0.0f,
878                        track_after  = i < line_span.fOffset + line_span.fCount - 1
879                                         ? props.tracking * 0.5f : 0.0f;
880 
881             const auto frag_offset = current_line_offset +
882                                      SkV2{align_offset + tracking_acc + track_before, 0};
883 
884             tracking_acc += track_before + track_after;
885 
886             this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // %
887                                       grouping_domain ? &(*grouping_domain)[grouping_span_index]
888                                                         : nullptr);
889         }
890     }
891 }
892 
fragmentAnchorPoint(const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const893 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
894                                       const SkV2& grouping_alignment,
895                                       const TextAnimator::DomainSpan* grouping_span) const {
896     // Construct the following 2x ascent box:
897     //
898     //      -------------
899     //     |             |
900     //     |             | ascent
901     //     |             |
902     // ----+-------------+---------- baseline
903     //   (pos)           |
904     //     |             | ascent
905     //     |             |
906     //      -------------
907     //         advance
908 
909     auto make_box = [](const SkPoint& pos, float advance, float ascent) {
910         // note: negative ascent
911         return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
912     };
913 
914     // Compute a grouping-dependent anchor point box.
915     // The default anchor point is at the center, and gets adjusted relative to the bounds
916     // based on |grouping_alignment|.
917     auto anchor_box = [&]() -> SkRect {
918         switch (fAnchorPointGrouping) {
919         case AnchorPointGrouping::kCharacter:
920             // Anchor box relative to each individual fragment.
921             return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
922         case AnchorPointGrouping::kWord:
923             // Fall through
924         case AnchorPointGrouping::kLine: {
925             SkASSERT(grouping_span);
926             // Anchor box relative to the first fragment in the word/line.
927             const auto& first_span_fragment = fFragments[grouping_span->fOffset];
928             return make_box(first_span_fragment.fOrigin,
929                             grouping_span->fAdvance,
930                             grouping_span->fAscent);
931         }
932         case AnchorPointGrouping::kAll:
933             // Anchor box is the same as the text box.
934             return fText->fBox;
935         }
936         SkUNREACHABLE;
937     };
938 
939     const auto ab = anchor_box();
940 
941     // Apply grouping alignment.
942     const auto ap = SkV2 { ab.centerX() + ab.width()  * 0.5f * grouping_alignment.x,
943                            ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
944 
945     // The anchor point is relative to the fragment position.
946     return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
947 }
948 
fragmentMatrix(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & frag_offset) const949 SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
950                                   const FragmentRec& rec, const SkV2& frag_offset) const {
951     const SkV3 pos = {
952         props.position.x + rec.fOrigin.fX + frag_offset.x,
953         props.position.y + rec.fOrigin.fY + frag_offset.y,
954         props.position.z
955     };
956 
957     if (!fPathInfo) {
958         return SkM44::Translate(pos.x, pos.y, pos.z);
959     }
960 
961     // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
962     const auto align_offset =
963             align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
964 
965     // Path positioning is based on the fragment position relative to the paragraph box
966     // upper-left corner:
967     //
968     //   - the horizontal component determines the distance on path
969     //
970     //   - the vertical component is post-applied after orienting on path
971     //
972     // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
973     //
974     const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
975     const auto path_distance = rel_pos.x + align_offset;
976 
977     return fPathInfo->getMatrix(path_distance, fText->fHAlign)
978          * SkM44::Translate(0, rel_pos.y, pos.z);
979 }
980 
pushPropsToFragment(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & frag_offset,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const981 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
982                                       const FragmentRec& rec,
983                                       const SkV2& frag_offset,
984                                       const SkV2& grouping_alignment,
985                                       const TextAnimator::DomainSpan* grouping_span) const {
986     const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
987 
988     rec.fMatrixNode->setMatrix(
989                 this->fragmentMatrix(props, rec, anchor_point + frag_offset)
990               * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
991               * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
992               * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
993               * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
994               * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
995 
996     const auto scale_alpha = [](SkColor c, float o) {
997         return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
998     };
999 
1000     if (rec.fFillColorNode) {
1001         rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
1002     }
1003     if (rec.fStrokeColorNode) {
1004         rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
1005         rec.fStrokeColorNode->setStrokeWidth(props.stroke_width * fTextShapingScale);
1006     }
1007     if (rec.fBlur) {
1008         rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
1009                               props.blur.y * kBlurSizeToSigma });
1010     }
1011 }
1012 
1013 } // namespace skottie::internal
1014