Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
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) {
249 case SkTextUtils::Align::kLeft_Align: distance += fPathFMargin; break;
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
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,
301 sk_sp<Logger> logger,
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,
436 sk_sp<Logger> logger,
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
597 if (frag.fIsWhitespace) {
598 if (in_word) {
599 in_word = false;
600 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
601 }
602 } else {
603 fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
604
605 if (!in_word) {
606 in_word = true;
607 word_start = i;
608 word_advance = word_ascent = 0;
609 }
610
611 word_advance += frag.fAdvance;
612 word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent
613 }
614
615 if (frag.fLineIndex != line) {
616 SkASSERT(frag.fLineIndex == line + 1);
617 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
618 line = frag.fLineIndex;
619 line_start = i;
620 line_advance = line_ascent = 0;
621 }
622
623 line_advance += frag.fAdvance;
624 line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent
625 }
626
627 if (i > word_start) {
628 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
629 }
630
631 if (i > line_start) {
632 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
633 }
634}
635
637 fText.fCurrentValue = txt;
638 this->onSync();
639}
640
641uint32_t TextAdapter::shaperFlags() const {
642 uint32_t flags = Shaper::Flags::kNone;
643
644 // We need granular fragments (as opposed to consolidated blobs):
645 // - when animating
646 // - when positioning on a path
647 // - when clamping the number or lines (for accurate line count)
648 // - when a text decorator is present
649 if (!fAnimators.empty() || fPathInfo || fText->fMaxLines || fText->fDecorator) {
651 }
652
653 if (fRequiresAnchorPoint || fText->fDecorator) {
655 }
656
657 if (fText->fDecorator) {
659 }
660
661 return flags;
662}
663
664void TextAdapter::reshape() {
665 // AE clamps the font size to a reasonable range.
666 // We do the same, since HB is susceptible to int overflows for degenerate values.
667 static constexpr float kMinSize = 0.1f,
668 kMaxSize = 1296.0f;
669 const Shaper::TextDesc text_desc = {
670 fText->fTypeface,
671 SkTPin(fText->fTextSize, kMinSize, kMaxSize),
672 SkTPin(fText->fMinTextSize, kMinSize, kMaxSize),
673 SkTPin(fText->fMaxTextSize, kMinSize, kMaxSize),
674 fText->fLineHeight,
675 fText->fLineShift,
676 fText->fAscent,
677 fText->fHAlign,
678 fText->fVAlign,
679 fText->fResize,
680 fText->fLineBreak,
681 fText->fDirection,
682 fText->fCapitalization,
683 fText->fMaxLines,
684 this->shaperFlags(),
685 fText->fLocale.isEmpty() ? nullptr : fText->fLocale.c_str(),
686 fText->fFontFamily.isEmpty() ? nullptr : fText->fFontFamily.c_str(),
687 };
688 auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr,
689 fShapingFactory);
690
691 if (fLogger) {
692 if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
693 const auto msg = SkStringPrintf("Text layout failed for '%s'.",
694 fText->fText.c_str());
695 fLogger->log(Logger::Level::kError, msg.c_str());
696
697 // These may trigger repeatedly when the text is animating.
698 // To avoid spamming, only log once.
699 fLogger = nullptr;
700 }
701
702 if (shape_result.fMissingGlyphCount > 0) {
703 const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
704 shape_result.fMissingGlyphCount,
705 fText->fText.c_str());
706 fLogger->log(Logger::Level::kWarning, msg.c_str());
707 fLogger = nullptr;
708 }
709 }
710
711 // Save the text shaping scale for later adjustments.
712 fTextShapingScale = shape_result.fScale;
713
714 // Rebuild all fragments.
715 // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
716
717 fRoot->clear();
718 fFragments.clear();
719
720 if (SHOW_LAYOUT_BOXES) {
721 auto box_color = sksg::Color::Make(0xffff0000);
722 box_color->setStyle(SkPaint::kStroke_Style);
723 box_color->setStrokeWidth(1);
724 box_color->setAntiAlias(true);
725
726 auto bounds_color = sksg::Color::Make(0xff00ff00);
727 bounds_color->setStyle(SkPaint::kStroke_Style);
728 bounds_color->setStrokeWidth(1);
729 bounds_color->setAntiAlias(true);
730
731 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
732 std::move(box_color)));
733 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
734 std::move(bounds_color)));
735
736 if (fPathInfo) {
737 auto path_color = sksg::Color::Make(0xffffff00);
738 path_color->setStyle(SkPaint::kStroke_Style);
739 path_color->setStrokeWidth(1);
740 path_color->setAntiAlias(true);
741
742 fRoot->addChild(
743 sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
744 std::move(path_color)));
745 }
746 }
747
748 // Depending on whether a GlyphDecorator is present, we either add the glyph render nodes
749 // directly to the root group, or to an intermediate GlyphDecoratorNode container.
750 sksg::Group* container = fRoot.get();
751 sk_sp<GlyphDecoratorNode> decorator_node;
752 if (fText->fDecorator) {
753 decorator_node = sk_make_sp<GlyphDecoratorNode>(fText->fDecorator, fTextShapingScale);
754 container = decorator_node.get();
755 }
756
757 // N.B. addFragment moves shaped glyph data out of the fragment, so only the fragment
758 // metrics are valid after this block.
759 for (size_t i = 0; i < shape_result.fFragments.size(); ++i) {
760 this->addFragment(shape_result.fFragments[i], container);
761 }
762
763 if (decorator_node) {
764 decorator_node->updateFragmentData(fFragments);
765 fRoot->addChild(std::move(decorator_node));
766 }
767
768 if (!fAnimators.empty() || fPathInfo) {
769 // Range selectors and text paths require fragment domain maps.
770 this->buildDomainMaps(shape_result);
771 }
772}
773
775 if (!fText->fHasFill && !fText->fHasStroke) {
776 return;
777 }
778
779 if (fText.hasChanged()) {
780 this->reshape();
781 }
782
783 if (fFragments.empty()) {
784 return;
785 }
786
787 // Update the path contour measure, if needed.
788 if (fPathInfo) {
789 fPathInfo->updateContourData();
790 }
791
792 // Seed props from the current text value.
794 seed_props.fill_color = fText->fFillColor;
795 seed_props.stroke_color = fText->fStrokeColor;
796 seed_props.stroke_width = fText->fStrokeWidth;
797
799 buf.resize(fFragments.size(), { seed_props, 0 });
800
801 // Apply all animators to the modulator buffer.
802 for (const auto& animator : fAnimators) {
803 animator->modulateProps(fMaps, buf);
804 }
805
806 const TextAnimator::DomainMap* grouping_domain = nullptr;
807 switch (fAnchorPointGrouping) {
808 // for word/line grouping, we rely on domain map info
809 case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
810 case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
811 // remaining grouping modes (character/all) do not need (or have) domain map data
812 default: break;
813 }
814
815 size_t grouping_span_index = 0;
816 SkV2 current_line_offset = { 0, 0 }; // cumulative line spacing
817
818 auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf,
819 const TextAnimator::DomainSpan& line_span) {
820 SkV2 total_spacing = {0,0};
821 float total_tracking = 0;
822
823 // Only compute these when needed.
824 if (fRequiresLineAdjustments && line_span.fCount) {
825 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
826 const auto& props = buf[i].props;
827 total_spacing += props.line_spacing;
828 total_tracking += props.tracking;
829 }
830
831 // The first glyph does not contribute |before| tracking, and the last one does not
832 // contribute |after| tracking.
833 total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
834 buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
835 }
836
837 return std::make_tuple(total_spacing, total_tracking);
838 };
839
840 // Finally, push all props to their corresponding fragment.
841 for (const auto& line_span : fMaps.fLinesMap) {
842 const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span);
843 const auto align_offset = -line_tracking * align_factor(fText->fHAlign);
844
845 // line spacing of the first line is ignored (nothing to "space" against)
846 if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) {
847 // For each line, the actual spacing is an average of individual fragment spacing
848 // (to preserve the "line").
849 current_line_offset += line_spacing / line_span.fCount;
850 }
851
852 float tracking_acc = 0;
853 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
854 // Track the grouping domain span in parallel.
855 if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
856 (*grouping_domain)[grouping_span_index].fCount) {
857 grouping_span_index += 1;
858 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
859 (*grouping_domain)[grouping_span_index].fCount);
860 }
861
862 const auto& props = buf[i].props;
863 const auto& frag = fFragments[i];
864
865 // AE tracking is defined per glyph, based on two components: |before| and |after|.
866 // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2.
867 //
868 // Tracking is applied as a local glyph offset, and contributes to the line width for
869 // alignment purposes.
870 //
871 // No |before| tracking for the first glyph, nor |after| tracking for the last one.
872 const auto track_before = i > line_span.fOffset
873 ? props.tracking * 0.5f : 0.0f,
874 track_after = i < line_span.fOffset + line_span.fCount - 1
875 ? props.tracking * 0.5f : 0.0f;
876
877 const auto frag_offset = current_line_offset +
878 SkV2{align_offset + tracking_acc + track_before, 0};
879
880 tracking_acc += track_before + track_after;
881
882 this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // %
883 grouping_domain ? &(*grouping_domain)[grouping_span_index]
884 : nullptr);
885 }
886 }
887}
888
889SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
890 const SkV2& grouping_alignment,
891 const TextAnimator::DomainSpan* grouping_span) const {
892 // Construct the following 2x ascent box:
893 //
894 // -------------
895 // | |
896 // | | ascent
897 // | |
898 // ----+-------------+---------- baseline
899 // (pos) |
900 // | | ascent
901 // | |
902 // -------------
903 // advance
904
905 auto make_box = [](const SkPoint& pos, float advance, float ascent) {
906 // note: negative ascent
907 return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
908 };
909
910 // Compute a grouping-dependent anchor point box.
911 // The default anchor point is at the center, and gets adjusted relative to the bounds
912 // based on |grouping_alignment|.
913 auto anchor_box = [&]() -> SkRect {
914 switch (fAnchorPointGrouping) {
915 case AnchorPointGrouping::kCharacter:
916 // Anchor box relative to each individual fragment.
917 return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
918 case AnchorPointGrouping::kWord:
919 // Fall through
920 case AnchorPointGrouping::kLine: {
921 SkASSERT(grouping_span);
922 // Anchor box relative to the first fragment in the word/line.
923 const auto& first_span_fragment = fFragments[grouping_span->fOffset];
924 return make_box(first_span_fragment.fOrigin,
925 grouping_span->fAdvance,
926 grouping_span->fAscent);
927 }
928 case AnchorPointGrouping::kAll:
929 // Anchor box is the same as the text box.
930 return fText->fBox;
931 }
933 };
934
935 const auto ab = anchor_box();
936
937 // Apply grouping alignment.
938 const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x,
939 ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
940
941 // The anchor point is relative to the fragment position.
942 return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
943}
944
945SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
946 const FragmentRec& rec, const SkV2& frag_offset) const {
947 const SkV3 pos = {
948 props.position.x + rec.fOrigin.fX + frag_offset.x,
949 props.position.y + rec.fOrigin.fY + frag_offset.y,
950 props.position.z
951 };
952
953 if (!fPathInfo) {
954 return SkM44::Translate(pos.x, pos.y, pos.z);
955 }
956
957 // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
958 const auto align_offset =
959 align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
960
961 // Path positioning is based on the fragment position relative to the paragraph box
962 // upper-left corner:
963 //
964 // - the horizontal component determines the distance on path
965 //
966 // - the vertical component is post-applied after orienting on path
967 //
968 // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
969 //
970 const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
971 const auto path_distance = rel_pos.x + align_offset;
972
973 return fPathInfo->getMatrix(path_distance, fText->fHAlign)
974 * SkM44::Translate(0, rel_pos.y, pos.z);
975}
976
977void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
978 const FragmentRec& rec,
979 const SkV2& frag_offset,
980 const SkV2& grouping_alignment,
981 const TextAnimator::DomainSpan* grouping_span) const {
982 const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
983
984 rec.fMatrixNode->setMatrix(
985 this->fragmentMatrix(props, rec, anchor_point + frag_offset)
986 * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
987 * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
988 * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
989 * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
990 * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
991
992 const auto scale_alpha = [](SkColor c, float o) {
993 return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
994 };
995
996 if (rec.fFillColorNode) {
997 rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
998 }
999 if (rec.fStrokeColorNode) {
1000 rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
1001 rec.fStrokeColorNode->setStrokeWidth(props.stroke_width * fTextShapingScale);
1002 }
1003 if (rec.fBlur) {
1004 rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
1005 props.blur.y * kBlurSizeToSigma });
1006 }
1007}
1008
1009} // namespace skottie::internal
uint16_t glyphs[5]
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
static std::unique_ptr< SkEncoder > Make(SkWStream *dst, const SkPixmap *src, const SkYUVAPixmaps *srcYUVA, const SkColorSpace *srcYUVAColorSpace, const SkJpegEncoder::Options &options)
#define SkDegreesToRadians(degrees)
Definition SkScalar.h:77
#define SkScalarRoundToInt(x)
Definition SkScalar.h:37
SK_API SkString static SkString SkStringPrintf()
Definition SkString.h:287
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
SkMatrix getTotalMatrix() const
void clipPath(const SkPath &path, SkClipOp op, bool doAntiAlias)
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
SkPath & reverseAddPath(const SkPath &src)
Definition SkPath.cpp:1569
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 > &)
@ kTrackFragmentAdvanceAscent
Definition TextShaper.h:150
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
std::vector< AnimatedPropsModulator > ModulatorBuffer
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)
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)
const Paint & paint
struct MyStruct a[10]
FlutterSemanticsFlag flags
Definition ab.py:1
Definition run.py:1
static constexpr float kBlurSizeToSigma
Definition SkottiePriv.h:47
SkScalar ScalarValue
bool Parse(const skjson::Value &, T *)
Definition Skottie.h:32
Definition ref_ptr.h:256
const Scalar scale
float fX
x-axis value
float fY
y-axis value
constexpr float y() const
constexpr float x() const
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