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