Flutter Engine
The Flutter Engine
TextAdapter.cpp
Go to the documentation of this file.
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 */
8
12#include "include/core/SkFont.h"
14#include "include/core/SkM44.h"
17#include "include/core/SkPath.h"
18#include "include/core/SkRect.h"
20#include "include/core/SkSpan.h"
30#include "modules/skottie/src/text/RangeSelector.h" // IWYU pragma: keep
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
52namespace sksg {
53class InvalidationController;
54}
55
56// Enable for text layout debugging.
57#define SHOW_LAYOUT_BOXES 0
58
59namespace skottie::internal {
60
61namespace {
62
63class GlyphTextNode final : public sksg::GeometryNode {
64public:
65 explicit GlyphTextNode(Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {}
66
67 ~GlyphTextNode() override = default;
68
69 const Shaper::ShapedGlyphs* glyphs() const { return &fGlyphs; }
70
71protected:
72 SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
73 return fGlyphs.computeBounds(Shaper::ShapedGlyphs::BoundsType::kConservative);
74 }
75
76 void onDraw(SkCanvas* canvas, const SkPaint& paint) const override {
77 fGlyphs.draw(canvas, {0,0}, paint);
78 }
79
80 void onClip(SkCanvas* canvas, bool antiAlias) const override {
81 canvas->clipPath(this->asPath(), antiAlias);
82 }
83
84 bool onContains(const SkPoint& p) const override {
85 return this->asPath().contains(p.x(), p.y());
86 }
87
88 SkPath onAsPath() const override {
89 // TODO
90 return SkPath();
91 }
92
93private:
94 const Shaper::ShapedGlyphs fGlyphs;
95};
96
97static 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
105}
106
107} // namespace
108
110public:
112 : fDecorator(std::move(decorator))
113 , fScale(scale)
114 {}
115
116 ~GlyphDecoratorNode() override = default;
117
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
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 =
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
149 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
161private:
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//
206
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
230 float pathLength() const {
231 SkASSERT(fPath == fCurrentPath);
232 SkASSERT((fPathReverse != 0) == fCurrentReversed);
233
234 return fCurrentMeasure ? fCurrentMeasure->length() : 0;
235 }
236
237 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) {
250 case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
251 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
289private:
290 // Cached contour data.
291 ShapeValue fCurrentPath;
292 sk_sp<SkContourMeasure> fCurrentMeasure;
293 bool fCurrentReversed = false,
294 fCurrentClosed = false;
295};
296
298 const AnimationBuilder* abuilder,
299 sk_sp<SkFontMgr> fontmgr,
300 sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
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
434TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr,
435 sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
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
449TextAdapter::~TextAdapter() = default;
450
451std::vector<sk_sp<sksg::RenderNode>>
452TextAdapter::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
491void 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
576void 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
641 fText.fCurrentValue = txt;
642 this->onSync();
643}
644
645uint32_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
668void 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
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.
798 seed_props.fill_color = fText->fFillColor;
799 seed_props.stroke_color = fText->fStrokeColor;
800 seed_props.stroke_width = fText->fStrokeWidth;
801
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
893SkV2 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 }
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
949SkM44 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
981void 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
@ kRight_Align
@ kLeft_Align
uint16_t glyphs[5]
Definition: FontMgrTest.cpp:46
SkPoint pos
#define SkUNREACHABLE
Definition: SkAssert.h:135
#define SkASSERT(cond)
Definition: SkAssert.h:116
uint32_t SkColor
Definition: SkColor.h:37
static constexpr SkColor SkColorSetA(SkColor c, U8CPU a)
Definition: SkColor.h:82
#define SkColorGetA(color)
Definition: SkColor.h:61
#define SkDegreesToRadians(degrees)
Definition: SkScalar.h:77
#define SkScalarRoundToInt(x)
Definition: SkScalar.h:37
SkSpan(Container &&) -> SkSpan< std::remove_pointer_t< decltype(std::data(std::declval< Container >()))> >
SK_API SkString SkStringPrintf(const char *format,...) SK_PRINTF_LIKE(1
Creates a new string and writes into it using a printf()-style format.
static constexpr const T & SkTPin(const T &x, const T &lo, const T &hi)
Definition: SkTPin.h:19
constexpr size_t SkToSizeT(S x)
Definition: SkTo.h:31
uint16_t SkGlyphID
Definition: SkTypes.h:179
#define SHOW_LAYOUT_BOXES
Definition: TextAdapter.cpp:57
SkMatrix getTotalMatrix() const
Definition: SkCanvas.cpp:1629
void clipPath(const SkPath &path, SkClipOp op, bool doAntiAlias)
Definition: SkCanvas.cpp:1456
sk_sp< SkContourMeasure > next()
bool getPosTan(SkScalar distance, SkPoint *position, SkVector *tangent) const
SkScalar length() const
Definition: SkM44.h:150
static SkM44 Rotate(SkV3 axis, SkScalar radians)
Definition: SkM44.h:239
static SkM44 Translate(SkScalar x, SkScalar y, SkScalar z=0)
Definition: SkM44.h:225
static SkM44 Scale(SkScalar x, SkScalar y, SkScalar z=1)
Definition: SkM44.h:232
static SkMatrix Scale(SkScalar sx, SkScalar sy)
Definition: SkMatrix.h:75
static SkMatrix Translate(SkScalar dx, SkScalar dy)
Definition: SkMatrix.h:91
@ kStroke_Style
set to stroke geometry
Definition: SkPaint.h:194
Definition: SkPath.h:59
SkPath & reverseAddPath(const SkPath &src)
Definition: SkPath.cpp:1633
T * get() const
Definition: SkRefCnt.h:303
size_t size() const
Definition: SkJSON.h:262
static Result Shape(const SkString &text, const TextDesc &desc, const SkPoint &point, const sk_sp< SkFontMgr > &, const sk_sp< SkShapers::Factory > &)
Definition: TextShaper.cpp:643
void log(Logger::Level, const skjson::Value *, const char fmt[],...) const SK_PRINTF_LIKE(4
Definition: Skottie.cpp:71
bool dispatchTextProperty(const sk_sp< TextAdapter > &, const skjson::ObjectValue *jtext) const
Definition: Skottie.cpp:256
void updateFragmentData(const std::vector< TextAdapter::FragmentRec > &recs)
void onRender(SkCanvas *canvas, const RenderContext *ctx) const override
GlyphDecoratorNode(sk_sp< GlyphDecorator > decorator, float scale)
SkRect onRevalidate(sksg::InvalidationController *ic, const SkMatrix &ctm) override
static sk_sp< TextAdapter > Make(const skjson::ObjectValue &, const AnimationBuilder *, sk_sp< SkFontMgr >, sk_sp< CustomFont::GlyphCompMapper >, sk_sp< Logger >, sk_sp<::SkShapers::Factory >)
void setText(const TextValue &)
std::vector< DomainSpan > DomainMap
Definition: TextAnimator.h:82
std::vector< AnimatedPropsModulator > ModulatorBuffer
Definition: TextAnimator.h:69
static sk_sp< TextAnimator > Make(const skjson::ObjectValue *, const AnimationBuilder *, AnimatablePropertyContainer *acontainer)
static sk_sp< BlurImageFilter > Make()
static sk_sp< Color > Make(SkColor c)
Definition: SkSGPaint.cpp:44
static sk_sp< Draw > Make(sk_sp< GeometryNode > geo, sk_sp< PaintNode > paint)
Definition: SkSGDraw.h:35
void addChild(sk_sp< RenderNode >)
Definition: SkSGGroup.cpp:45
static sk_sp< Group > Make()
Definition: SkSGGroup.h:31
void clear()
Definition: SkSGGroup.cpp:38
static sk_sp< RenderNode > Make(sk_sp< RenderNode > child, sk_sp< ImageFilter > filter)
static sk_sp< Matrix > Make(const T &m)
Definition: SkSGTransform.h:70
const SkRect & bounds() const
Definition: SkSGNode.h:55
static sk_sp< Path > Make()
Definition: SkSGPath.h:31
static sk_sp< Rect > Make()
Definition: SkSGRect.h:36
ScopedRenderContext && setIsolation(const SkRect &bounds, const SkMatrix &ctm, bool do_isolate)
static sk_sp< TransformEffect > Make(sk_sp< RenderNode > child, sk_sp< Transform > transform)
Definition: SkSGTransform.h:97
const Paint & paint
Definition: color_source.cc:38
struct MyStruct a[10]
FlutterSemanticsFlag flags
static float max(float r, float g, float b)
Definition: hsl.cpp:49
static float min(float r, float g, float b)
Definition: hsl.cpp:48
SK_API sk_sp< SkDocument > Make(SkWStream *dst, const SkSerialProcs *=nullptr, std::function< void(const SkPicture *)> onEndPage=nullptr)
Definition: ab.py:1
@ kNone
Definition: layer.h:53
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
Definition: switches.h:57
it will be possible to load the file into Perfetto s trace viewer disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
Definition: switches.h:259
Definition: run.py:1
static constexpr float kBlurSizeToSigma
Definition: SkottiePriv.h:47
SkScalar ScalarValue
Definition: SkottieValue.h:22
bool Parse(const skjson::Value &, T *)
Definition: Skottie.h:32
Definition: ref_ptr.h:256
const Scalar scale
float fX
x-axis value
Definition: SkPoint_impl.h:164
float fY
y-axis value
Definition: SkPoint_impl.h:165
constexpr float y() const
Definition: SkPoint_impl.h:187
constexpr float x() const
Definition: SkPoint_impl.h:181
static constexpr SkRect MakeXYWH(float x, float y, float w, float h)
Definition: SkRect.h:659
static constexpr SkRect MakeLTRB(float l, float t, float r, float b)
Definition: SkRect.h:646
Definition: SkM44.h:19
float x
Definition: SkM44.h:20
float y
Definition: SkM44.h:20
Definition: SkM44.h:56
SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const