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