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 29namespace skottie::internal { 30 31static 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// 63struct TextAdapter::PathInfo { 64 ShapeValue fPath; 65 ScalarValue fPathFMargin = 0, 66 fPathLMargin = 0, 67 fPathPerpendicular = 0, 68 fPathReverse = 0; 69 70 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 93 float pathLength() const { 94 SkASSERT(fPath == fCurrentPath); 95 SkASSERT((fPathReverse != 0) == fCurrentReversed); 96 97 return fCurrentMeasure ? fCurrentMeasure->length() : 0; 98 } 99 100 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 152private: 153 // Cached contour data. 154 ShapeValue fCurrentPath; 155 sk_sp<SkContourMeasure> fCurrentMeasure; 156 bool fCurrentReversed = false, 157 fCurrentClosed = false; 158}; 159 160sk_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 291TextAdapter::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 299TextAdapter::~TextAdapter() = default; 300 301void 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 375void 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 435void TextAdapter::setText(const TextValue& txt) { 436 fText.fCurrentValue = txt; 437 this->onSync(); 438} 439 440uint32_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 451void 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 533void 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 619SkV2 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 675SkM44 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 707void 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 737void 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