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/SkContourMeasure.h"
11 #include "include/core/SkFontMgr.h"
12 #include "include/core/SkM44.h"
13 #include "include/private/SkTPin.h"
14 #include "modules/skottie/src/SkottieJson.h"
15 #include "modules/skottie/src/text/RangeSelector.h"
16 #include "modules/skottie/src/text/TextAnimator.h"
17 #include "modules/sksg/include/SkSGDraw.h"
18 #include "modules/sksg/include/SkSGGroup.h"
19 #include "modules/sksg/include/SkSGPaint.h"
20 #include "modules/sksg/include/SkSGPath.h"
21 #include "modules/sksg/include/SkSGRect.h"
22 #include "modules/sksg/include/SkSGRenderEffect.h"
23 #include "modules/sksg/include/SkSGText.h"
24 #include "modules/sksg/include/SkSGTransform.h"
25
26 // Enable for text layout debugging.
27 #define SHOW_LAYOUT_BOXES 0
28
29 namespace skottie::internal {
30
align_factor(SkTextUtils::Align a)31 static float align_factor(SkTextUtils::Align a) {
32 switch (a) {
33 case SkTextUtils::kLeft_Align : return 0.0f;
34 case SkTextUtils::kCenter_Align: return 0.5f;
35 case SkTextUtils::kRight_Align : return 1.0f;
36 }
37
38 SkUNREACHABLE;
39 };
40
41 // Text path semantics
42 //
43 // * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
44 // a distance along the path
45 //
46 // * horizontal alignment is applied relative to the path start/end points
47 //
48 // * "Reverse Path" allows reversing the path direction
49 //
50 // * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular
51 // to the path tangent, or not (just positioned).
52 //
53 // * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path,
54 // depending on horizontal alignement:
55 // - left: offset = first margin
56 // - center: offset = first margin + last margin
57 // - right: offset = last margin
58 //
59 // * extranormal path positions (d < 0, d > path len) are allowed
60 // - closed path: the position wraps around in both directions
61 // - open path: extrapolates from extremes' positions/slopes
62 //
63 struct TextAdapter::PathInfo {
64 ShapeValue fPath;
65 ScalarValue fPathFMargin = 0,
66 fPathLMargin = 0,
67 fPathPerpendicular = 0,
68 fPathReverse = 0;
69
updateContourDataskottie::internal::TextAdapter::PathInfo70 void updateContourData() {
71 const auto reverse = fPathReverse != 0;
72
73 if (fPath != fCurrentPath || reverse != fCurrentReversed) {
74 // reinitialize cached contour data
75 auto path = static_cast<SkPath>(fPath);
76 if (reverse) {
77 SkPath reversed;
78 reversed.reverseAddPath(path);
79 path = reversed;
80 }
81
82 SkContourMeasureIter iter(path, /*forceClosed = */false);
83 fCurrentMeasure = iter.next();
84 fCurrentClosed = path.isLastContourClosed();
85 fCurrentReversed = reverse;
86 fCurrentPath = fPath;
87
88 // AE paths are always single-contour (no moves allowed).
89 SkASSERT(!iter.next());
90 }
91 }
92
pathLengthskottie::internal::TextAdapter::PathInfo93 float pathLength() const {
94 SkASSERT(fPath == fCurrentPath);
95 SkASSERT((fPathReverse != 0) == fCurrentReversed);
96
97 return fCurrentMeasure ? fCurrentMeasure->length() : 0;
98 }
99
getMatrixskottie::internal::TextAdapter::PathInfo100 SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const {
101 SkASSERT(fPath == fCurrentPath);
102 SkASSERT((fPathReverse != 0) == fCurrentReversed);
103
104 if (!fCurrentMeasure) {
105 return SkM44();
106 }
107
108 const auto path_len = fCurrentMeasure->length();
109
110 // First/last margin adjustment also depends on alignment.
111 switch (alignment) {
112 case SkTextUtils::Align::kLeft_Align: distance += fPathFMargin; break;
113 case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
114 fPathLMargin; break;
115 case SkTextUtils::Align::kRight_Align: distance += fPathLMargin; break;
116 }
117
118 // For closed paths, extranormal distances wrap around the contour.
119 if (fCurrentClosed) {
120 distance = std::fmod(distance, path_len);
121 if (distance < 0) {
122 distance += path_len;
123 }
124 SkASSERT(0 <= distance && distance <= path_len);
125 }
126
127 SkPoint pos;
128 SkVector tan;
129 if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) {
130 return SkM44();
131 }
132
133 // For open paths, extranormal distances are extrapolated from extremes.
134 // Note:
135 // - getPosTan above clamps to the extremes
136 // - the extrapolation below only kicks in for extranormal values
137 const auto underflow = std::min(0.0f, distance),
138 overflow = std::max(0.0f, distance - path_len);
139 pos += tan*(underflow + overflow);
140
141 auto m = SkM44::Translate(pos.x(), pos.y());
142
143 // The "perpendicular" flag controls whether fragments are positioned and rotated,
144 // or just positioned.
145 if (fPathPerpendicular != 0) {
146 m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x()));
147 }
148
149 return m;
150 }
151
152 private:
153 // Cached contour data.
154 ShapeValue fCurrentPath;
155 sk_sp<SkContourMeasure> fCurrentMeasure;
156 bool fCurrentReversed = false,
157 fCurrentClosed = false;
158 };
159
Make(const skjson::ObjectValue & jlayer,const AnimationBuilder * abuilder,sk_sp<SkFontMgr> fontmgr,sk_sp<Logger> logger)160 sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
161 const AnimationBuilder* abuilder,
162 sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger) {
163 // General text node format:
164 // "t": {
165 // "a": [], // animators (see TextAnimator)
166 // "d": {
167 // "k": [
168 // {
169 // "s": {
170 // "f": "Roboto-Regular",
171 // "fc": [
172 // 0.42,
173 // 0.15,
174 // 0.15
175 // ],
176 // "j": 1,
177 // "lh": 60,
178 // "ls": 0,
179 // "s": 50,
180 // "t": "text align right",
181 // "tr": 0
182 // },
183 // "t": 0
184 // }
185 // ]
186 // },
187 // "m": { // more options
188 // "g": 1, // Anchor Point Grouping
189 // "a": {...} // Grouping Alignment
190 // },
191 // "p": { // path options
192 // "a": 0, // force alignment
193 // "f": {}, // first margin
194 // "l": {}, // last margin
195 // "m": 1, // mask index
196 // "p": 1, // perpendicular
197 // "r": 0 // reverse path
198 // }
199
200 // },
201
202 const skjson::ObjectValue* jt = jlayer["t"];
203 const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
204 : nullptr;
205 if (!jd) {
206 abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
207 return nullptr;
208 }
209
210 // "More options"
211 const skjson::ObjectValue* jm = (*jt)["m"];
212 static constexpr AnchorPointGrouping gGroupingMap[] = {
213 AnchorPointGrouping::kCharacter, // 'g': 1
214 AnchorPointGrouping::kWord, // 'g': 2
215 AnchorPointGrouping::kLine, // 'g': 3
216 AnchorPointGrouping::kAll, // 'g': 4
217 };
218 const auto apg = jm
219 ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, SK_ARRAY_COUNT(gGroupingMap))
220 : 1;
221
222 auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
223 std::move(logger),
224 gGroupingMap[SkToSizeT(apg - 1)]));
225
226 adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
227 if (jm) {
228 adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
229 }
230
231 // Animators
232 if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
233 adapter->fAnimators.reserve(janimators->size());
234
235 for (const skjson::ObjectValue* janimator : *janimators) {
236 if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
237 adapter->fHasBlurAnimator |= animator->hasBlur();
238 adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint();
239
240 adapter->fAnimators.push_back(std::move(animator));
241 }
242 }
243 }
244
245 // Optional text path
246 const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
247 if (!jpath) {
248 return nullptr;
249 }
250
251 // the actual path is identified as an index in the layer mask stack
252 const auto mask_index =
253 ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
254 const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
255 if (!jmasks || mask_index >= jmasks->size()) {
256 return nullptr;
257 }
258
259 const skjson::ObjectValue* mask = (*jmasks)[mask_index];
260 if (!mask) {
261 return nullptr;
262 }
263
264 auto pinfo = std::make_unique<PathInfo>();
265 adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
266 adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
267 adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
268 adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
269 adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);
270
271 // TODO: force align support
272
273 // Historically, these used to be exported as static properties.
274 // Attempt parsing both ways, for backward compat.
275 skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
276 skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);
277
278 // Path positioning requires anchor point info.
279 adapter->fRequiresAnchorPoint = true;
280
281 return pinfo;
282 };
283
284 adapter->fPathInfo = attach_path((*jt)["p"]);
285
286 abuilder->dispatchTextProperty(adapter);
287
288 return adapter;
289 }
290
TextAdapter(sk_sp<SkFontMgr> fontmgr,sk_sp<Logger> logger,AnchorPointGrouping apg)291 TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg)
292 : fRoot(sksg::Group::Make())
293 , fFontMgr(std::move(fontmgr))
294 , fLogger(std::move(logger))
295 , fAnchorPointGrouping(apg)
296 , fHasBlurAnimator(false)
297 , fRequiresAnchorPoint(false) {}
298
299 TextAdapter::~TextAdapter() = default;
300
addFragment(const Shaper::Fragment & frag)301 void TextAdapter::addFragment(const Shaper::Fragment& frag) {
302 // For a given shaped fragment, build a corresponding SG fragment:
303 //
304 // [TransformEffect] -> [Transform]
305 // [Group]
306 // [Draw] -> [TextBlob*] [FillPaint]
307 // [Draw] -> [TextBlob*] [StrokePaint]
308 //
309 // * where the blob node is shared
310
311 auto blob_node = sksg::TextBlob::Make(frag.fBlob);
312
313 FragmentRec rec;
314 rec.fOrigin = frag.fPos;
315 rec.fAdvance = frag.fAdvance;
316 rec.fAscent = frag.fAscent;
317 rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fPos.x(), frag.fPos.y()));
318
319 std::vector<sk_sp<sksg::RenderNode>> draws;
320 draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke));
321
322 SkASSERT(fText->fHasFill || fText->fHasStroke);
323
324 auto add_fill = [&]() {
325 if (fText->fHasFill) {
326 rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
327 rec.fFillColorNode->setAntiAlias(true);
328 draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
329 }
330 };
331 auto add_stroke = [&] {
332 if (fText->fHasStroke) {
333 rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
334 rec.fStrokeColorNode->setAntiAlias(true);
335 rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
336 rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth);
337 draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
338 }
339 };
340
341 if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
342 add_fill();
343 add_stroke();
344 } else {
345 add_stroke();
346 add_fill();
347 }
348
349 SkASSERT(!draws.empty());
350
351 if (SHOW_LAYOUT_BOXES) {
352 // visualize fragment ascent boxes
353 auto box_color = sksg::Color::Make(0xff0000ff);
354 box_color->setStyle(SkPaint::kStroke_Style);
355 box_color->setStrokeWidth(1);
356 box_color->setAntiAlias(true);
357 auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
358 draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
359 }
360
361 auto draws_node = (draws.size() > 1)
362 ? sksg::Group::Make(std::move(draws))
363 : std::move(draws[0]);
364
365 if (fHasBlurAnimator) {
366 // Optional blur effect.
367 rec.fBlur = sksg::BlurImageFilter::Make();
368 draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
369 }
370
371 fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
372 fFragments.push_back(std::move(rec));
373 }
374
buildDomainMaps(const Shaper::Result & shape_result)375 void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
376 fMaps.fNonWhitespaceMap.clear();
377 fMaps.fWordsMap.clear();
378 fMaps.fLinesMap.clear();
379
380 size_t i = 0,
381 line = 0,
382 line_start = 0,
383 word_start = 0;
384
385 float word_advance = 0,
386 word_ascent = 0,
387 line_advance = 0,
388 line_ascent = 0;
389
390 bool in_word = false;
391
392 // TODO: use ICU for building the word map?
393 for (; i < shape_result.fFragments.size(); ++i) {
394 const auto& frag = shape_result.fFragments[i];
395
396 if (frag.fIsWhitespace) {
397 if (in_word) {
398 in_word = false;
399 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
400 }
401 } else {
402 fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
403
404 if (!in_word) {
405 in_word = true;
406 word_start = i;
407 word_advance = word_ascent = 0;
408 }
409
410 word_advance += frag.fAdvance;
411 word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent
412 }
413
414 if (frag.fLineIndex != line) {
415 SkASSERT(frag.fLineIndex == line + 1);
416 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
417 line = frag.fLineIndex;
418 line_start = i;
419 line_advance = line_ascent = 0;
420 }
421
422 line_advance += frag.fAdvance;
423 line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent
424 }
425
426 if (i > word_start) {
427 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
428 }
429
430 if (i > line_start) {
431 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
432 }
433 }
434
setText(const TextValue & txt)435 void TextAdapter::setText(const TextValue& txt) {
436 fText.fCurrentValue = txt;
437 this->onSync();
438 }
439
shaperFlags() const440 uint32_t TextAdapter::shaperFlags() const {
441 uint32_t flags = Shaper::Flags::kNone;
442
443 // We need granular fragments (as opposed to consolidated blobs) when animating, or when
444 // positioning on a path.
445 if (!fAnimators.empty() || fPathInfo) flags |= Shaper::Flags::kFragmentGlyphs;
446 if (fRequiresAnchorPoint) flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
447
448 return flags;
449 }
450
reshape()451 void TextAdapter::reshape() {
452 const Shaper::TextDesc text_desc = {
453 fText->fTypeface,
454 fText->fTextSize,
455 fText->fMinTextSize,
456 fText->fMaxTextSize,
457 fText->fLineHeight,
458 fText->fLineShift,
459 fText->fAscent,
460 fText->fHAlign,
461 fText->fVAlign,
462 fText->fResize,
463 fText->fLineBreak,
464 fText->fDirection,
465 fText->fCapitalization,
466 this->shaperFlags(),
467 };
468 const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
469
470 if (fLogger) {
471 if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
472 const auto msg = SkStringPrintf("Text layout failed for '%s'.",
473 fText->fText.c_str());
474 fLogger->log(Logger::Level::kError, msg.c_str());
475 }
476
477 if (shape_result.fMissingGlyphCount > 0) {
478 const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
479 shape_result.fMissingGlyphCount,
480 fText->fText.c_str());
481 fLogger->log(Logger::Level::kWarning, msg.c_str());
482 }
483
484 // These may trigger repeatedly when the text is animating.
485 // To avoid spamming, only log once.
486 fLogger = nullptr;
487 }
488
489 // Rebuild all fragments.
490 // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
491
492 fRoot->clear();
493 fFragments.clear();
494
495 for (const auto& frag : shape_result.fFragments) {
496 this->addFragment(frag);
497 }
498
499 if (!fAnimators.empty() || fPathInfo) {
500 // Range selectors and text paths require fragment domain maps.
501 this->buildDomainMaps(shape_result);
502 }
503
504 if (SHOW_LAYOUT_BOXES) {
505 auto box_color = sksg::Color::Make(0xffff0000);
506 box_color->setStyle(SkPaint::kStroke_Style);
507 box_color->setStrokeWidth(1);
508 box_color->setAntiAlias(true);
509
510 auto bounds_color = sksg::Color::Make(0xff00ff00);
511 bounds_color->setStyle(SkPaint::kStroke_Style);
512 bounds_color->setStrokeWidth(1);
513 bounds_color->setAntiAlias(true);
514
515 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
516 std::move(box_color)));
517 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
518 std::move(bounds_color)));
519
520 if (fPathInfo) {
521 auto path_color = sksg::Color::Make(0xffffff00);
522 path_color->setStyle(SkPaint::kStroke_Style);
523 path_color->setStrokeWidth(1);
524 path_color->setAntiAlias(true);
525
526 fRoot->addChild(
527 sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
528 std::move(path_color)));
529 }
530 }
531 }
532
onSync()533 void TextAdapter::onSync() {
534 if (!fText->fHasFill && !fText->fHasStroke) {
535 return;
536 }
537
538 if (fText.hasChanged()) {
539 this->reshape();
540 }
541
542 if (fFragments.empty()) {
543 return;
544 }
545
546 // Update the path contour measure, if needed.
547 if (fPathInfo) {
548 fPathInfo->updateContourData();
549 }
550
551 // Seed props from the current text value.
552 TextAnimator::ResolvedProps seed_props;
553 seed_props.fill_color = fText->fFillColor;
554 seed_props.stroke_color = fText->fStrokeColor;
555
556 TextAnimator::ModulatorBuffer buf;
557 buf.resize(fFragments.size(), { seed_props, 0 });
558
559 // Apply all animators to the modulator buffer.
560 for (const auto& animator : fAnimators) {
561 animator->modulateProps(fMaps, buf);
562 }
563
564 const TextAnimator::DomainMap* grouping_domain = nullptr;
565 switch (fAnchorPointGrouping) {
566 // for word/line grouping, we rely on domain map info
567 case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
568 case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
569 // remaining grouping modes (character/all) do not need (or have) domain map data
570 default: break;
571 }
572
573 size_t grouping_span_index = 0;
574 SkV2 line_offset = { 0, 0 }; // cumulative line spacing
575
576 // Finally, push all props to their corresponding fragment.
577 for (const auto& line_span : fMaps.fLinesMap) {
578 SkV2 line_spacing = { 0, 0 };
579 float line_tracking = 0;
580 bool line_has_tracking = false;
581
582 // Tracking requires special treatment: unlike other props, its effect is not localized
583 // to a single fragment, but requires re-alignment of the whole line.
584 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
585 // Track the grouping domain span in parallel.
586 if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
587 (*grouping_domain)[grouping_span_index].fCount) {
588 grouping_span_index += 1;
589 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
590 (*grouping_domain)[grouping_span_index].fCount);
591 }
592
593 const auto& props = buf[i].props;
594 const auto& frag = fFragments[i];
595 this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage
596 grouping_domain ? &(*grouping_domain)[grouping_span_index]
597 : nullptr);
598
599 line_tracking += props.tracking;
600 line_has_tracking |= !SkScalarNearlyZero(props.tracking);
601
602 line_spacing += props.line_spacing;
603 }
604
605 // line spacing of the first line is ignored (nothing to "space" against)
606 if (&line_span != &fMaps.fLinesMap.front()) {
607 // For each line, the actual spacing is an average of individual fragment spacing
608 // (to preserve the "line").
609 line_offset += line_spacing / line_span.fCount;
610 }
611
612 if (line_offset != SkV2{0, 0} || line_has_tracking) {
613 this->adjustLineProps(buf, line_span, line_offset, line_tracking);
614 }
615
616 }
617 }
618
fragmentAnchorPoint(const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const619 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
620 const SkV2& grouping_alignment,
621 const TextAnimator::DomainSpan* grouping_span) const {
622 // Construct the following 2x ascent box:
623 //
624 // -------------
625 // | |
626 // | | ascent
627 // | |
628 // ----+-------------+---------- baseline
629 // (pos) |
630 // | | ascent
631 // | |
632 // -------------
633 // advance
634
635 auto make_box = [](const SkPoint& pos, float advance, float ascent) {
636 // note: negative ascent
637 return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
638 };
639
640 // Compute a grouping-dependent anchor point box.
641 // The default anchor point is at the center, and gets adjusted relative to the bounds
642 // based on |grouping_alignment|.
643 auto anchor_box = [&]() -> SkRect {
644 switch (fAnchorPointGrouping) {
645 case AnchorPointGrouping::kCharacter:
646 // Anchor box relative to each individual fragment.
647 return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
648 case AnchorPointGrouping::kWord:
649 // Fall through
650 case AnchorPointGrouping::kLine: {
651 SkASSERT(grouping_span);
652 // Anchor box relative to the first fragment in the word/line.
653 const auto& first_span_fragment = fFragments[grouping_span->fOffset];
654 return make_box(first_span_fragment.fOrigin,
655 grouping_span->fAdvance,
656 grouping_span->fAscent);
657 }
658 case AnchorPointGrouping::kAll:
659 // Anchor box is the same as the text box.
660 return fText->fBox;
661 }
662 SkUNREACHABLE;
663 };
664
665 const auto ab = anchor_box();
666
667 // Apply grouping alignment.
668 const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x,
669 ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
670
671 // The anchor point is relative to the fragment position.
672 return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
673 }
674
fragmentMatrix(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & anchor_point) const675 SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
676 const FragmentRec& rec, const SkV2& anchor_point) const {
677 const SkV3 pos = {
678 props.position.x + rec.fOrigin.fX + anchor_point.x,
679 props.position.y + rec.fOrigin.fY + anchor_point.y,
680 props.position.z
681 };
682
683 if (!fPathInfo) {
684 return SkM44::Translate(pos.x, pos.y, pos.z);
685 }
686
687 // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
688 const auto align_offset =
689 align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
690
691 // Path positioning is based on the fragment position relative to the paragraph box
692 // upper-left corner:
693 //
694 // - the horizontal component determines the distance on path
695 //
696 // - the vertical component is post-applied after orienting on path
697 //
698 // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
699 //
700 const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
701 const auto path_distance = rel_pos.x + align_offset;
702
703 return fPathInfo->getMatrix(path_distance, fText->fHAlign)
704 * SkM44::Translate(0, rel_pos.y, pos.z);
705 }
706
pushPropsToFragment(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const707 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
708 const FragmentRec& rec,
709 const SkV2& grouping_alignment,
710 const TextAnimator::DomainSpan* grouping_span) const {
711 const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
712
713 rec.fMatrixNode->setMatrix(
714 this->fragmentMatrix(props, rec, anchor_point)
715 * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
716 * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
717 * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
718 * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
719 * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
720
721 const auto scale_alpha = [](SkColor c, float o) {
722 return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
723 };
724
725 if (rec.fFillColorNode) {
726 rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
727 }
728 if (rec.fStrokeColorNode) {
729 rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
730 }
731 if (rec.fBlur) {
732 rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
733 props.blur.y * kBlurSizeToSigma });
734 }
735 }
736
adjustLineProps(const TextAnimator::ModulatorBuffer & buf,const TextAnimator::DomainSpan & line_span,const SkV2 & line_offset,float total_tracking) const737 void TextAdapter::adjustLineProps(const TextAnimator::ModulatorBuffer& buf,
738 const TextAnimator::DomainSpan& line_span,
739 const SkV2& line_offset,
740 float total_tracking) const {
741 SkASSERT(line_span.fCount > 0);
742
743 // AE tracking is defined per glyph, based on two components: |before| and |after|.
744 // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2.
745 //
746 // Tracking is applied as a local glyph offset, and contributes to the line width for alignment
747 // purposes.
748
749 // The first glyph does not contribute |before| tracking, and the last one does not contribute
750 // |after| tracking. Rather than spill this logic into applyAnimators, post-adjust here.
751 total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
752 buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
753
754 const auto align_offset = -total_tracking * align_factor(fText->fHAlign);
755
756 float tracking_acc = 0;
757 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
758 const auto& props = buf[i].props;
759
760 // No |before| tracking for the first glyph, nor |after| tracking for the last one.
761 const auto track_before = i > line_span.fOffset
762 ? props.tracking * 0.5f : 0.0f,
763 track_after = i < line_span.fOffset + line_span.fCount - 1
764 ? props.tracking * 0.5f : 0.0f,
765 fragment_offset = align_offset + tracking_acc + track_before;
766
767 const auto& frag = fFragments[i];
768 const auto m = SkM44::Translate(line_offset.x + fragment_offset,
769 line_offset.y) *
770 frag.fMatrixNode->getMatrix();
771 frag.fMatrixNode->setMatrix(m);
772
773 tracking_acc += track_before + track_after;
774 }
775 }
776
777 } // namespace skottie::internal
778