| 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/SkFontMgr.h" |
| 11 | #include "include/core/SkM44.h" |
| 12 | #include "modules/skottie/src/SkottieJson.h" |
| 13 | #include "modules/skottie/src/text/RangeSelector.h" |
| 14 | #include "modules/skottie/src/text/TextAnimator.h" |
| 15 | #include "modules/sksg/include/SkSGDraw.h" |
| 16 | #include "modules/sksg/include/SkSGGroup.h" |
| 17 | #include "modules/sksg/include/SkSGPaint.h" |
| 18 | #include "modules/sksg/include/SkSGRect.h" |
| 19 | #include "modules/sksg/include/SkSGRenderEffect.h" |
| 20 | #include "modules/sksg/include/SkSGText.h" |
| 21 | #include "modules/sksg/include/SkSGTransform.h" |
| 22 | |
| 23 | namespace skottie { |
| 24 | namespace internal { |
| 25 | |
| 26 | sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer, |
| 27 | const AnimationBuilder* abuilder, |
| 28 | sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger) { |
| 29 | // General text node format: |
| 30 | // "t": { |
| 31 | // "a": [], // animators (see TextAnimator) |
| 32 | // "d": { |
| 33 | // "k": [ |
| 34 | // { |
| 35 | // "s": { |
| 36 | // "f": "Roboto-Regular", |
| 37 | // "fc": [ |
| 38 | // 0.42, |
| 39 | // 0.15, |
| 40 | // 0.15 |
| 41 | // ], |
| 42 | // "j": 1, |
| 43 | // "lh": 60, |
| 44 | // "ls": 0, |
| 45 | // "s": 50, |
| 46 | // "t": "text align right", |
| 47 | // "tr": 0 |
| 48 | // }, |
| 49 | // "t": 0 |
| 50 | // } |
| 51 | // ] |
| 52 | // }, |
| 53 | // "m": { // "more options" |
| 54 | // "g": 1, // Anchor Point Grouping |
| 55 | // "a": {...} // Grouping Alignment |
| 56 | // }, |
| 57 | // "p": {} // "path options" (TODO) |
| 58 | // }, |
| 59 | |
| 60 | const skjson::ObjectValue* jt = jlayer["t" ]; |
| 61 | const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d" ]) |
| 62 | : nullptr; |
| 63 | if (!jd) { |
| 64 | abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer." ); |
| 65 | return nullptr; |
| 66 | } |
| 67 | |
| 68 | // "More options" |
| 69 | const skjson::ObjectValue* jm = (*jt)["m" ]; |
| 70 | static constexpr AnchorPointGrouping gGroupingMap[] = { |
| 71 | AnchorPointGrouping::kCharacter, // 'g': 1 |
| 72 | AnchorPointGrouping::kWord, // 'g': 2 |
| 73 | AnchorPointGrouping::kLine, // 'g': 3 |
| 74 | AnchorPointGrouping::kAll, // 'g': 4 |
| 75 | }; |
| 76 | const auto apg = jm |
| 77 | ? SkTPin<int>(ParseDefault<int>((*jm)["g" ], 1), 1, SK_ARRAY_COUNT(gGroupingMap)) |
| 78 | : 1; |
| 79 | |
| 80 | auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr), |
| 81 | std::move(logger), |
| 82 | gGroupingMap[SkToSizeT(apg - 1)])); |
| 83 | |
| 84 | adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue); |
| 85 | if (jm) { |
| 86 | adapter->bind(*abuilder, (*jm)["a" ], adapter->fGroupingAlignment); |
| 87 | } |
| 88 | |
| 89 | // Animators |
| 90 | if (const skjson::ArrayValue* janimators = (*jt)["a" ]) { |
| 91 | adapter->fAnimators.reserve(janimators->size()); |
| 92 | |
| 93 | for (const skjson::ObjectValue* janimator : *janimators) { |
| 94 | if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) { |
| 95 | adapter->fHasBlurAnimator |= animator->hasBlur(); |
| 96 | adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint(); |
| 97 | |
| 98 | adapter->fAnimators.push_back(std::move(animator)); |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | abuilder->dispatchTextProperty(adapter); |
| 104 | |
| 105 | return adapter; |
| 106 | } |
| 107 | |
| 108 | TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg) |
| 109 | : fRoot(sksg::Group::Make()) |
| 110 | , fFontMgr(std::move(fontmgr)) |
| 111 | , fLogger(std::move(logger)) |
| 112 | , fAnchorPointGrouping(apg) |
| 113 | , fHasBlurAnimator(false) |
| 114 | , fRequiresAnchorPoint(false) {} |
| 115 | |
| 116 | TextAdapter::~TextAdapter() = default; |
| 117 | |
| 118 | void TextAdapter::addFragment(const Shaper::Fragment& frag) { |
| 119 | // For a given shaped fragment, build a corresponding SG fragment: |
| 120 | // |
| 121 | // [TransformEffect] -> [Transform] |
| 122 | // [Group] |
| 123 | // [Draw] -> [TextBlob*] [FillPaint] |
| 124 | // [Draw] -> [TextBlob*] [StrokePaint] |
| 125 | // |
| 126 | // * where the blob node is shared |
| 127 | |
| 128 | auto blob_node = sksg::TextBlob::Make(frag.fBlob); |
| 129 | |
| 130 | FragmentRec rec; |
| 131 | rec.fOrigin = frag.fPos; |
| 132 | rec.fAdvance = frag.fAdvance; |
| 133 | rec.fAscent = frag.fAscent; |
| 134 | rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fPos.x(), frag.fPos.y())); |
| 135 | |
| 136 | std::vector<sk_sp<sksg::RenderNode>> draws; |
| 137 | draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke)); |
| 138 | |
| 139 | SkASSERT(fText->fHasFill || fText->fHasStroke); |
| 140 | |
| 141 | auto add_fill = [&]() { |
| 142 | if (fText->fHasFill) { |
| 143 | rec.fFillColorNode = sksg::Color::Make(fText->fFillColor); |
| 144 | rec.fFillColorNode->setAntiAlias(true); |
| 145 | draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode)); |
| 146 | } |
| 147 | }; |
| 148 | auto add_stroke = [&] { |
| 149 | if (fText->fHasStroke) { |
| 150 | rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor); |
| 151 | rec.fStrokeColorNode->setAntiAlias(true); |
| 152 | rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style); |
| 153 | rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth); |
| 154 | draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode)); |
| 155 | } |
| 156 | }; |
| 157 | |
| 158 | if (fText->fPaintOrder == TextPaintOrder::kFillStroke) { |
| 159 | add_fill(); |
| 160 | add_stroke(); |
| 161 | } else { |
| 162 | add_stroke(); |
| 163 | add_fill(); |
| 164 | } |
| 165 | |
| 166 | SkASSERT(!draws.empty()); |
| 167 | |
| 168 | if (0) { |
| 169 | // enable to visualize fragment ascent boxes |
| 170 | auto box_color = sksg::Color::Make(0xff0000ff); |
| 171 | box_color->setStyle(SkPaint::kStroke_Style); |
| 172 | box_color->setStrokeWidth(1); |
| 173 | box_color->setAntiAlias(true); |
| 174 | auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0); |
| 175 | draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color))); |
| 176 | } |
| 177 | |
| 178 | auto draws_node = (draws.size() > 1) |
| 179 | ? sksg::Group::Make(std::move(draws)) |
| 180 | : std::move(draws[0]); |
| 181 | |
| 182 | if (fHasBlurAnimator) { |
| 183 | // Optional blur effect. |
| 184 | rec.fBlur = sksg::BlurImageFilter::Make(); |
| 185 | draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur); |
| 186 | } |
| 187 | |
| 188 | fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode)); |
| 189 | fFragments.push_back(std::move(rec)); |
| 190 | } |
| 191 | |
| 192 | void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) { |
| 193 | fMaps.fNonWhitespaceMap.clear(); |
| 194 | fMaps.fWordsMap.clear(); |
| 195 | fMaps.fLinesMap.clear(); |
| 196 | |
| 197 | size_t i = 0, |
| 198 | line = 0, |
| 199 | line_start = 0, |
| 200 | word_start = 0; |
| 201 | |
| 202 | float word_advance = 0, |
| 203 | word_ascent = 0, |
| 204 | line_advance = 0, |
| 205 | line_ascent = 0; |
| 206 | |
| 207 | bool in_word = false; |
| 208 | |
| 209 | // TODO: use ICU for building the word map? |
| 210 | for (; i < shape_result.fFragments.size(); ++i) { |
| 211 | const auto& frag = shape_result.fFragments[i]; |
| 212 | |
| 213 | if (frag.fIsWhitespace) { |
| 214 | if (in_word) { |
| 215 | in_word = false; |
| 216 | fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent}); |
| 217 | } |
| 218 | } else { |
| 219 | fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0}); |
| 220 | |
| 221 | if (!in_word) { |
| 222 | in_word = true; |
| 223 | word_start = i; |
| 224 | word_advance = word_ascent = 0; |
| 225 | } |
| 226 | |
| 227 | word_advance += frag.fAdvance; |
| 228 | word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent |
| 229 | } |
| 230 | |
| 231 | if (frag.fLineIndex != line) { |
| 232 | SkASSERT(frag.fLineIndex == line + 1); |
| 233 | fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent}); |
| 234 | line = frag.fLineIndex; |
| 235 | line_start = i; |
| 236 | line_advance = line_ascent = 0; |
| 237 | } |
| 238 | |
| 239 | line_advance += frag.fAdvance; |
| 240 | line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent |
| 241 | } |
| 242 | |
| 243 | if (i > word_start) { |
| 244 | fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent}); |
| 245 | } |
| 246 | |
| 247 | if (i > line_start) { |
| 248 | fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent}); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | void TextAdapter::setText(const TextValue& txt) { |
| 253 | fText.fCurrentValue = txt; |
| 254 | this->onSync(); |
| 255 | } |
| 256 | |
| 257 | uint32_t TextAdapter::shaperFlags() const { |
| 258 | uint32_t flags = Shaper::Flags::kNone; |
| 259 | |
| 260 | SkASSERT(!(fRequiresAnchorPoint && fAnimators.empty())); |
| 261 | if (!fAnimators.empty() ) flags |= Shaper::Flags::kFragmentGlyphs; |
| 262 | if (fRequiresAnchorPoint) flags |= Shaper::Flags::kTrackFragmentAdvanceAscent; |
| 263 | |
| 264 | return flags; |
| 265 | } |
| 266 | |
| 267 | void TextAdapter::reshape() { |
| 268 | const Shaper::TextDesc text_desc = { |
| 269 | fText->fTypeface, |
| 270 | fText->fTextSize, |
| 271 | fText->fLineHeight, |
| 272 | fText->fAscent, |
| 273 | fText->fHAlign, |
| 274 | fText->fVAlign, |
| 275 | fText->fResize, |
| 276 | this->shaperFlags(), |
| 277 | }; |
| 278 | const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr); |
| 279 | |
| 280 | if (fLogger && shape_result.fMissingGlyphCount > 0) { |
| 281 | const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'." , |
| 282 | shape_result.fMissingGlyphCount, |
| 283 | fText->fText.c_str()); |
| 284 | fLogger->log(Logger::Level::kWarning, msg.c_str()); |
| 285 | |
| 286 | // This may trigger repeatedly when the text is animating. |
| 287 | // To avoid spamming, only log once. |
| 288 | fLogger = nullptr; |
| 289 | } |
| 290 | |
| 291 | // Rebuild all fragments. |
| 292 | // TODO: we can be smarter here and try to reuse the existing SG structure if needed. |
| 293 | |
| 294 | fRoot->clear(); |
| 295 | fFragments.clear(); |
| 296 | |
| 297 | for (const auto& frag : shape_result.fFragments) { |
| 298 | this->addFragment(frag); |
| 299 | } |
| 300 | |
| 301 | if (!fAnimators.empty()) { |
| 302 | // Range selectors require fragment domain maps. |
| 303 | this->buildDomainMaps(shape_result); |
| 304 | } |
| 305 | |
| 306 | #if (0) |
| 307 | // Enable for text box debugging/visualization. |
| 308 | auto box_color = sksg::Color::Make(0xffff0000); |
| 309 | box_color->setStyle(SkPaint::kStroke_Style); |
| 310 | box_color->setStrokeWidth(1); |
| 311 | box_color->setAntiAlias(true); |
| 312 | |
| 313 | auto bounds_color = sksg::Color::Make(0xff00ff00); |
| 314 | bounds_color->setStyle(SkPaint::kStroke_Style); |
| 315 | bounds_color->setStrokeWidth(1); |
| 316 | bounds_color->setAntiAlias(true); |
| 317 | |
| 318 | fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText.fBox), |
| 319 | std::move(box_color))); |
| 320 | fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeBounds()), |
| 321 | std::move(bounds_color))); |
| 322 | #endif |
| 323 | } |
| 324 | |
| 325 | void TextAdapter::onSync() { |
| 326 | if (!fText->fHasFill && !fText->fHasStroke) { |
| 327 | return; |
| 328 | } |
| 329 | |
| 330 | if (fText.hasChanged()) { |
| 331 | this->reshape(); |
| 332 | } |
| 333 | |
| 334 | if (fFragments.empty()) { |
| 335 | return; |
| 336 | } |
| 337 | |
| 338 | // Seed props from the current text value. |
| 339 | TextAnimator::ResolvedProps seed_props; |
| 340 | seed_props.fill_color = fText->fFillColor; |
| 341 | seed_props.stroke_color = fText->fStrokeColor; |
| 342 | |
| 343 | TextAnimator::ModulatorBuffer buf; |
| 344 | buf.resize(fFragments.size(), { seed_props, 0 }); |
| 345 | |
| 346 | // Apply all animators to the modulator buffer. |
| 347 | for (const auto& animator : fAnimators) { |
| 348 | animator->modulateProps(fMaps, buf); |
| 349 | } |
| 350 | |
| 351 | const TextAnimator::DomainMap* grouping_domain = nullptr; |
| 352 | switch (fAnchorPointGrouping) { |
| 353 | // for word/line grouping, we rely on domain map info |
| 354 | case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break; |
| 355 | case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break; |
| 356 | // remaining grouping modes (character/all) do not need (or have) domain map data |
| 357 | default: break; |
| 358 | } |
| 359 | |
| 360 | size_t grouping_span_index = 0; |
| 361 | SkV2 line_offset = { 0, 0 }; // cumulative line spacing |
| 362 | |
| 363 | // Finally, push all props to their corresponding fragment. |
| 364 | for (const auto& line_span : fMaps.fLinesMap) { |
| 365 | SkV2 line_spacing = { 0, 0 }; |
| 366 | float line_tracking = 0; |
| 367 | bool line_has_tracking = false; |
| 368 | |
| 369 | // Tracking requires special treatment: unlike other props, its effect is not localized |
| 370 | // to a single fragment, but requires re-alignment of the whole line. |
| 371 | for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) { |
| 372 | // Track the grouping domain span in parallel. |
| 373 | if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset + |
| 374 | (*grouping_domain)[grouping_span_index].fCount) { |
| 375 | grouping_span_index += 1; |
| 376 | SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset + |
| 377 | (*grouping_domain)[grouping_span_index].fCount); |
| 378 | } |
| 379 | |
| 380 | const auto& props = buf[i].props; |
| 381 | const auto& frag = fFragments[i]; |
| 382 | this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage |
| 383 | grouping_domain ? &(*grouping_domain)[grouping_span_index] |
| 384 | : nullptr); |
| 385 | |
| 386 | line_tracking += props.tracking; |
| 387 | line_has_tracking |= !SkScalarNearlyZero(props.tracking); |
| 388 | |
| 389 | line_spacing += props.line_spacing; |
| 390 | } |
| 391 | |
| 392 | // line spacing of the first line is ignored (nothing to "space" against) |
| 393 | if (&line_span != &fMaps.fLinesMap.front()) { |
| 394 | // For each line, the actual spacing is an average of individual fragment spacing |
| 395 | // (to preserve the "line"). |
| 396 | line_offset += line_spacing / line_span.fCount; |
| 397 | } |
| 398 | |
| 399 | if (line_offset != SkV2{0, 0} || line_has_tracking) { |
| 400 | this->adjustLineProps(buf, line_span, line_offset, line_tracking); |
| 401 | } |
| 402 | |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec, |
| 407 | const SkV2& grouping_alignment, |
| 408 | const TextAnimator::DomainSpan* grouping_span) const { |
| 409 | // Construct the following 2x ascent box: |
| 410 | // |
| 411 | // ------------- |
| 412 | // | | |
| 413 | // | | ascent |
| 414 | // | | |
| 415 | // ----+-------------+---------- baseline |
| 416 | // (pos) | |
| 417 | // | | ascent |
| 418 | // | | |
| 419 | // ------------- |
| 420 | // advance |
| 421 | |
| 422 | auto make_box = [](const SkPoint& pos, float advance, float ascent) { |
| 423 | // note: negative ascent |
| 424 | return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent); |
| 425 | }; |
| 426 | |
| 427 | // Compute a grouping-dependent anchor point box. |
| 428 | // The default anchor point is at the center, and gets adjusted relative to the bounds |
| 429 | // based on |grouping_alignment|. |
| 430 | auto anchor_box = [&]() -> SkRect { |
| 431 | switch (fAnchorPointGrouping) { |
| 432 | case AnchorPointGrouping::kCharacter: |
| 433 | // Anchor box relative to each individual fragment. |
| 434 | return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent); |
| 435 | case AnchorPointGrouping::kWord: |
| 436 | // Fall through |
| 437 | case AnchorPointGrouping::kLine: { |
| 438 | SkASSERT(grouping_span); |
| 439 | // Anchor box relative to the first fragment in the word/line. |
| 440 | const auto& first_span_fragment = fFragments[grouping_span->fOffset]; |
| 441 | return make_box(first_span_fragment.fOrigin, |
| 442 | grouping_span->fAdvance, |
| 443 | grouping_span->fAscent); |
| 444 | } |
| 445 | case AnchorPointGrouping::kAll: |
| 446 | // Anchor box is the same as the text box. |
| 447 | return fText->fBox; |
| 448 | } |
| 449 | SkUNREACHABLE; |
| 450 | }; |
| 451 | |
| 452 | const auto ab = anchor_box(); |
| 453 | |
| 454 | // Apply grouping alignment. |
| 455 | const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x, |
| 456 | ab.centerY() + ab.height() * 0.5f * grouping_alignment.y }; |
| 457 | |
| 458 | // The anchor point is relative to the fragment position. |
| 459 | return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY }; |
| 460 | } |
| 461 | |
| 462 | void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props, |
| 463 | const FragmentRec& rec, |
| 464 | const SkV2& grouping_alignment, |
| 465 | const TextAnimator::DomainSpan* grouping_span) const { |
| 466 | const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span); |
| 467 | |
| 468 | rec.fMatrixNode->setMatrix( |
| 469 | SkM44::Translate(props.position.x + rec.fOrigin.x() + anchor_point.x, |
| 470 | props.position.y + rec.fOrigin.y() + anchor_point.y, |
| 471 | props.position.z) |
| 472 | * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x)) |
| 473 | * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y)) |
| 474 | * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z)) |
| 475 | * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z) |
| 476 | * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0)); |
| 477 | |
| 478 | const auto scale_alpha = [](SkColor c, float o) { |
| 479 | return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c))); |
| 480 | }; |
| 481 | |
| 482 | if (rec.fFillColorNode) { |
| 483 | rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity)); |
| 484 | } |
| 485 | if (rec.fStrokeColorNode) { |
| 486 | rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity)); |
| 487 | } |
| 488 | if (rec.fBlur) { |
| 489 | rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma, |
| 490 | props.blur.y * kBlurSizeToSigma }); |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | void TextAdapter::adjustLineProps(const TextAnimator::ModulatorBuffer& buf, |
| 495 | const TextAnimator::DomainSpan& line_span, |
| 496 | const SkV2& line_offset, |
| 497 | float total_tracking) const { |
| 498 | SkASSERT(line_span.fCount > 0); |
| 499 | |
| 500 | // AE tracking is defined per glyph, based on two components: |before| and |after|. |
| 501 | // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2. |
| 502 | // |
| 503 | // Tracking is applied as a local glyph offset, and contributes to the line width for alignment |
| 504 | // purposes. |
| 505 | |
| 506 | // The first glyph does not contribute |before| tracking, and the last one does not contribute |
| 507 | // |after| tracking. Rather than spill this logic into applyAnimators, post-adjust here. |
| 508 | total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking + |
| 509 | buf[line_span.fOffset + line_span.fCount - 1].props.tracking); |
| 510 | |
| 511 | static const auto align_factor = [](SkTextUtils::Align a) { |
| 512 | switch (a) { |
| 513 | case SkTextUtils::kLeft_Align : return 0.0f; |
| 514 | case SkTextUtils::kCenter_Align: return -0.5f; |
| 515 | case SkTextUtils::kRight_Align : return -1.0f; |
| 516 | } |
| 517 | |
| 518 | SkASSERT(false); |
| 519 | return 0.0f; |
| 520 | }; |
| 521 | |
| 522 | const auto align_offset = total_tracking * align_factor(fText->fHAlign); |
| 523 | |
| 524 | float tracking_acc = 0; |
| 525 | for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) { |
| 526 | const auto& props = buf[i].props; |
| 527 | |
| 528 | // No |before| tracking for the first glyph, nor |after| tracking for the last one. |
| 529 | const auto track_before = i > line_span.fOffset |
| 530 | ? props.tracking * 0.5f : 0.0f, |
| 531 | track_after = i < line_span.fOffset + line_span.fCount - 1 |
| 532 | ? props.tracking * 0.5f : 0.0f, |
| 533 | fragment_offset = align_offset + tracking_acc + track_before; |
| 534 | |
| 535 | const auto& frag = fFragments[i]; |
| 536 | const auto m = SkM44::Translate(line_offset.x + fragment_offset, |
| 537 | line_offset.y) * |
| 538 | frag.fMatrixNode->getMatrix(); |
| 539 | frag.fMatrixNode->setMatrix(m); |
| 540 | |
| 541 | tracking_acc += track_before + track_after; |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | } // namespace internal |
| 546 | } // namespace skottie |
| 547 | |