/* * Copyright 2019 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "modules/skottie/src/text/TextAdapter.h" #include "include/core/SkCanvas.h" #include "include/core/SkColor.h" #include "include/core/SkContourMeasure.h" #include "include/core/SkFont.h" #include "include/core/SkFontMgr.h" #include "include/core/SkM44.h" #include "include/core/SkMatrix.h" #include "include/core/SkPaint.h" #include "include/core/SkPath.h" #include "include/core/SkRect.h" #include "include/core/SkScalar.h" #include "include/core/SkSpan.h" #include "include/core/SkString.h" #include "include/core/SkTypes.h" #include "include/private/base/SkTPin.h" #include "include/private/base/SkTo.h" #include "include/utils/SkTextUtils.h" #include "modules/skottie/include/Skottie.h" #include "modules/skottie/include/SkottieProperty.h" #include "modules/skottie/src/SkottieJson.h" #include "modules/skottie/src/SkottiePriv.h" #include "modules/skottie/src/text/RangeSelector.h" // IWYU pragma: keep #include "modules/skottie/src/text/TextAnimator.h" #include "modules/sksg/include/SkSGDraw.h" #include "modules/sksg/include/SkSGGeometryNode.h" #include "modules/sksg/include/SkSGGroup.h" #include "modules/sksg/include/SkSGPaint.h" #include "modules/sksg/include/SkSGPath.h" #include "modules/sksg/include/SkSGRect.h" #include "modules/sksg/include/SkSGRenderEffect.h" #include "modules/sksg/include/SkSGRenderNode.h" #include "modules/sksg/include/SkSGTransform.h" #include "modules/sksg/src/SkSGTransformPriv.h" #include "modules/skshaper/include/SkShaper_factory.h" #include "src/utils/SkJSON.h" #include #include #include #include #include #include namespace sksg { class InvalidationController; } // Enable for text layout debugging. #define SHOW_LAYOUT_BOXES 0 namespace skottie::internal { namespace { class GlyphTextNode final : public sksg::GeometryNode { public: explicit GlyphTextNode(Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {} ~GlyphTextNode() override = default; const Shaper::ShapedGlyphs* glyphs() const { return &fGlyphs; } protected: SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override { return fGlyphs.computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight); } void onDraw(SkCanvas* canvas, const SkPaint& paint) const override { fGlyphs.draw(canvas, {0,0}, paint); } void onClip(SkCanvas* canvas, bool antiAlias) const override { canvas->clipPath(this->asPath(), antiAlias); } bool onContains(const SkPoint& p) const override { return this->asPath().contains(p.x(), p.y()); } SkPath onAsPath() const override { // TODO return SkPath(); } private: const Shaper::ShapedGlyphs fGlyphs; }; static float align_factor(SkTextUtils::Align a) { switch (a) { case SkTextUtils::kLeft_Align : return 0.0f; case SkTextUtils::kCenter_Align: return 0.5f; case SkTextUtils::kRight_Align : return 1.0f; } SkUNREACHABLE; } } // namespace class TextAdapter::GlyphDecoratorNode final : public sksg::Group { public: GlyphDecoratorNode(sk_sp decorator, float scale) : fDecorator(std::move(decorator)) , fScale(scale) {} ~GlyphDecoratorNode() override = default; void updateFragmentData(const std::vector& recs) { fFragCount = recs.size(); SkASSERT(!fFragInfo); fFragInfo = std::make_unique(recs.size()); for (size_t i = 0; i < recs.size(); ++i) { const auto& rec = recs[i]; fFragInfo[i] = {rec.fGlyphs, rec.fMatrixNode, rec.fAdvance}; } SkASSERT(!fDecoratorInfo); fDecoratorInfo = std::make_unique(recs.size()); } SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override { const auto child_bounds = INHERITED::onRevalidate(ic, ctm); for (size_t i = 0; i < fFragCount; ++i) { const auto* glyphs = fFragInfo[i].fGlyphs; fDecoratorInfo[i].fBounds = glyphs->computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight); fDecoratorInfo[i].fMatrix = sksg::TransformPriv::As(fFragInfo[i].fMatrixNode); fDecoratorInfo[i].fCluster = glyphs->fClusters.empty() ? 0 : glyphs->fClusters.front(); fDecoratorInfo[i].fAdvance = fFragInfo[i].fAdvance; } return child_bounds; } void onRender(SkCanvas* canvas, const RenderContext* ctx) const override { auto local_ctx = ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(), canvas->getTotalMatrix(), true); this->INHERITED::onRender(canvas, local_ctx); fDecorator->onDecorate(canvas, { SkSpan(fDecoratorInfo.get(), fFragCount), fScale }); } private: struct FragmentInfo { const Shaper::ShapedGlyphs* fGlyphs; sk_sp> fMatrixNode; float fAdvance; }; const sk_sp fDecorator; const float fScale; std::unique_ptr fFragInfo; std::unique_ptr fDecoratorInfo; size_t fFragCount; using INHERITED = Group; }; // Text path semantics // // * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as // a distance along the path // // * horizontal alignment is applied relative to the path start/end points // // * "Reverse Path" allows reversing the path direction // // * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular // to the path tangent, or not (just positioned). // // * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path, // depending on horizontal alignement: // - left: offset = first margin // - center: offset = first margin + last margin // - right: offset = last margin // // * extranormal path positions (d < 0, d > path len) are allowed // - closed path: the position wraps around in both directions // - open path: extrapolates from extremes' positions/slopes // struct TextAdapter::PathInfo { ShapeValue fPath; ScalarValue fPathFMargin = 0, fPathLMargin = 0, fPathPerpendicular = 0, fPathReverse = 0; void updateContourData() { const auto reverse = fPathReverse != 0; if (fPath != fCurrentPath || reverse != fCurrentReversed) { // reinitialize cached contour data auto path = static_cast(fPath); if (reverse) { SkPath reversed; reversed.reverseAddPath(path); path = reversed; } SkContourMeasureIter iter(path, /*forceClosed = */false); fCurrentMeasure = iter.next(); fCurrentClosed = path.isLastContourClosed(); fCurrentReversed = reverse; fCurrentPath = fPath; // AE paths are always single-contour (no moves allowed). SkASSERT(!iter.next()); } } float pathLength() const { SkASSERT(fPath == fCurrentPath); SkASSERT((fPathReverse != 0) == fCurrentReversed); return fCurrentMeasure ? fCurrentMeasure->length() : 0; } SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const { SkASSERT(fPath == fCurrentPath); SkASSERT((fPathReverse != 0) == fCurrentReversed); if (!fCurrentMeasure) { return SkM44(); } const auto path_len = fCurrentMeasure->length(); // First/last margin adjustment also depends on alignment. switch (alignment) { case SkTextUtils::Align::kLeft_Align: distance += fPathFMargin; break; case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin + fPathLMargin; break; case SkTextUtils::Align::kRight_Align: distance += fPathLMargin; break; } // For closed paths, extranormal distances wrap around the contour. if (fCurrentClosed) { distance = std::fmod(distance, path_len); if (distance < 0) { distance += path_len; } SkASSERT(0 <= distance && distance <= path_len); } SkPoint pos; SkVector tan; if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) { return SkM44(); } // For open paths, extranormal distances are extrapolated from extremes. // Note: // - getPosTan above clamps to the extremes // - the extrapolation below only kicks in for extranormal values const auto underflow = std::min(0.0f, distance), overflow = std::max(0.0f, distance - path_len); pos += tan*(underflow + overflow); auto m = SkM44::Translate(pos.x(), pos.y()); // The "perpendicular" flag controls whether fragments are positioned and rotated, // or just positioned. if (fPathPerpendicular != 0) { m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x())); } return m; } private: // Cached contour data. ShapeValue fCurrentPath; sk_sp fCurrentMeasure; bool fCurrentReversed = false, fCurrentClosed = false; }; sk_sp TextAdapter::Make(const skjson::ObjectValue& jlayer, const AnimationBuilder* abuilder, sk_sp fontmgr, sk_sp custom_glyph_mapper, sk_sp logger, sk_sp<::SkShapers::Factory> factory) { // General text node format: // "t": { // "a": [], // animators (see TextAnimator) // "d": { // "k": [ // { // "s": { // "f": "Roboto-Regular", // "fc": [ // 0.42, // 0.15, // 0.15 // ], // "j": 1, // "lh": 60, // "ls": 0, // "s": 50, // "t": "text align right", // "tr": 0 // }, // "t": 0 // } // ], // "sid": "optionalSlotID" // }, // "m": { // more options // "g": 1, // Anchor Point Grouping // "a": {...} // Grouping Alignment // }, // "p": { // path options // "a": 0, // force alignment // "f": {}, // first margin // "l": {}, // last margin // "m": 1, // mask index // "p": 1, // perpendicular // "r": 0 // reverse path // } // }, const skjson::ObjectValue* jt = jlayer["t"]; const skjson::ObjectValue* jd = jt ? static_cast((*jt)["d"]) : nullptr; if (!jd) { abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer."); return nullptr; } // "More options" const skjson::ObjectValue* jm = (*jt)["m"]; static constexpr AnchorPointGrouping gGroupingMap[] = { AnchorPointGrouping::kCharacter, // 'g': 1 AnchorPointGrouping::kWord, // 'g': 2 AnchorPointGrouping::kLine, // 'g': 3 AnchorPointGrouping::kAll, // 'g': 4 }; const auto apg = jm ? SkTPin(ParseDefault((*jm)["g"], 1), 1, std::size(gGroupingMap)) : 1; auto adapter = sk_sp(new TextAdapter(std::move(fontmgr), std::move(custom_glyph_mapper), std::move(logger), std::move(factory), gGroupingMap[SkToSizeT(apg - 1)])); adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue); if (jm) { adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment); } // Animators if (const skjson::ArrayValue* janimators = (*jt)["a"]) { adapter->fAnimators.reserve(janimators->size()); for (const skjson::ObjectValue* janimator : *janimators) { if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) { adapter->fHasBlurAnimator |= animator->hasBlur(); adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint(); adapter->fRequiresLineAdjustments |= animator->requiresLineAdjustments(); adapter->fAnimators.push_back(std::move(animator)); } } } // Optional text path const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr { if (!jpath) { return nullptr; } // the actual path is identified as an index in the layer mask stack const auto mask_index = ParseDefault((*jpath)["m"], std::numeric_limits::max()); const skjson::ArrayValue* jmasks = jlayer["masksProperties"]; if (!jmasks || mask_index >= jmasks->size()) { return nullptr; } const skjson::ObjectValue* mask = (*jmasks)[mask_index]; if (!mask) { return nullptr; } auto pinfo = std::make_unique(); adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath); adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin); adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin); adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular); adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse); // TODO: force align support // Historically, these used to be exported as static properties. // Attempt parsing both ways, for backward compat. skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular); skottie::Parse((*jpath)["r"], &pinfo->fPathReverse); // Path positioning requires anchor point info. adapter->fRequiresAnchorPoint = true; return pinfo; }; adapter->fPathInfo = attach_path((*jt)["p"]); abuilder->dispatchTextProperty(adapter, jd); return adapter; } TextAdapter::TextAdapter(sk_sp fontmgr, sk_sp custom_glyph_mapper, sk_sp logger, sk_sp factory, AnchorPointGrouping apg) : fRoot(sksg::Group::Make()) , fFontMgr(std::move(fontmgr)) , fCustomGlyphMapper(std::move(custom_glyph_mapper)) , fLogger(std::move(logger)) , fShapingFactory(std::move(factory)) , fAnchorPointGrouping(apg) , fHasBlurAnimator(false) , fRequiresAnchorPoint(false) , fRequiresLineAdjustments(false) {} TextAdapter::~TextAdapter() = default; std::vector> TextAdapter::buildGlyphCompNodes(Shaper::ShapedGlyphs& glyphs) const { std::vector> draws; if (fCustomGlyphMapper) { size_t run_offset = 0; for (auto& run : glyphs.fRuns) { for (size_t i = 0; i < run.fSize; ++i) { const size_t goffset = run_offset + i; const SkGlyphID gid = glyphs.fGlyphIDs[goffset]; if (auto gcomp = fCustomGlyphMapper->getGlyphComp(run.fFont.getTypeface(), gid)) { // Position and scale the "glyph". const auto m = SkMatrix::Translate(glyphs.fGlyphPos[goffset]) * SkMatrix::Scale(fText->fTextSize*fTextShapingScale, fText->fTextSize*fTextShapingScale); draws.push_back(sksg::TransformEffect::Make(std::move(gcomp), m)); // Remove all related data from the fragment, so we don't attempt to render // this as a regular glyph. SkASSERT(glyphs.fGlyphIDs.size() > goffset); glyphs.fGlyphIDs.erase(glyphs.fGlyphIDs.begin() + goffset); SkASSERT(glyphs.fGlyphPos.size() > goffset); glyphs.fGlyphPos.erase(glyphs.fGlyphPos.begin() + goffset); if (!glyphs.fClusters.empty()) { SkASSERT(glyphs.fClusters.size() > goffset); glyphs.fClusters.erase(glyphs.fClusters.begin() + goffset); } i -= 1; run.fSize -= 1; } } run_offset += run.fSize; } } return draws; } void TextAdapter::addFragment(Shaper::Fragment& frag, sksg::Group* container) { // For a given shaped fragment, build a corresponding SG fragment: // // [TransformEffect] -> [Transform] // [Group] // [Draw] -> [GlyphTextNode*] [FillPaint] // SkTypeface-based glyph. // [Draw] -> [GlyphTextNode*] [StrokePaint] // SkTypeface-based glyph. // [CompRenderTree] // Comp glyph. // ... // FragmentRec rec; rec.fOrigin = frag.fOrigin; rec.fAdvance = frag.fAdvance; rec.fAscent = frag.fAscent; rec.fMatrixNode = sksg::Matrix::Make(SkM44::Translate(frag.fOrigin.x(), frag.fOrigin.y())); // Start off substituting existing comp nodes for all composition-based glyphs. std::vector> draws = this->buildGlyphCompNodes(frag.fGlyphs); // Use a regular GlyphTextNode for the remaining glyphs (backed by a real SkTypeface). auto text_node = sk_make_sp(std::move(frag.fGlyphs)); rec.fGlyphs = text_node->glyphs(); draws.reserve(draws.size() + static_cast(fText->fHasFill) + static_cast(fText->fHasStroke)); SkASSERT(fText->fHasFill || fText->fHasStroke); auto add_fill = [&]() { if (fText->fHasFill) { rec.fFillColorNode = sksg::Color::Make(fText->fFillColor); rec.fFillColorNode->setAntiAlias(true); draws.push_back(sksg::Draw::Make(text_node, rec.fFillColorNode)); } }; auto add_stroke = [&] { if (fText->fHasStroke) { rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor); rec.fStrokeColorNode->setAntiAlias(true); rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style); rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth * fTextShapingScale); rec.fStrokeColorNode->setStrokeJoin(fText->fStrokeJoin); draws.push_back(sksg::Draw::Make(text_node, rec.fStrokeColorNode)); } }; if (fText->fPaintOrder == TextPaintOrder::kFillStroke) { add_fill(); add_stroke(); } else { add_stroke(); add_fill(); } SkASSERT(!draws.empty()); if (SHOW_LAYOUT_BOXES) { // visualize fragment ascent boxes auto box_color = sksg::Color::Make(0xff0000ff); box_color->setStyle(SkPaint::kStroke_Style); box_color->setStrokeWidth(1); box_color->setAntiAlias(true); auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0); draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color))); } draws.shrink_to_fit(); auto draws_node = (draws.size() > 1) ? sksg::Group::Make(std::move(draws)) : std::move(draws[0]); if (fHasBlurAnimator) { // Optional blur effect. rec.fBlur = sksg::BlurImageFilter::Make(); draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur); } container->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode)); fFragments.push_back(std::move(rec)); } void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) { fMaps.fNonWhitespaceMap.clear(); fMaps.fWordsMap.clear(); fMaps.fLinesMap.clear(); size_t i = 0, line = 0, line_start = 0, word_start = 0; float word_advance = 0, word_ascent = 0, line_advance = 0, line_ascent = 0; bool in_word = false; // TODO: use ICU for building the word map? for (; i < shape_result.fFragments.size(); ++i) { const auto& frag = shape_result.fFragments[i]; const bool is_new_line = frag.fLineIndex != line; if (frag.fIsWhitespace || is_new_line) { // Both whitespace and new lines break words. if (in_word) { in_word = false; fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent}); } } if (!frag.fIsWhitespace) { fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0}); if (!in_word) { in_word = true; word_start = i; word_advance = word_ascent = 0; } word_advance += frag.fAdvance; word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent } if (is_new_line) { SkASSERT(frag.fLineIndex == line + 1); fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent}); line = frag.fLineIndex; line_start = i; line_advance = line_ascent = 0; } line_advance += frag.fAdvance; line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent } if (i > word_start) { fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent}); } if (i > line_start) { fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent}); } } void TextAdapter::setText(const TextValue& txt) { fText.fCurrentValue = txt; this->onSync(); } uint32_t TextAdapter::shaperFlags() const { uint32_t flags = Shaper::Flags::kNone; // We need granular fragments (as opposed to consolidated blobs): // - when animating // - when positioning on a path // - when clamping the number or lines (for accurate line count) // - when a text decorator is present if (!fAnimators.empty() || fPathInfo || fText->fMaxLines || fText->fDecorator) { flags |= Shaper::Flags::kFragmentGlyphs; } if (fRequiresAnchorPoint || fText->fDecorator) { flags |= Shaper::Flags::kTrackFragmentAdvanceAscent; } if (fText->fDecorator) { flags |= Shaper::Flags::kClusters; } return flags; } void TextAdapter::reshape() { // AE clamps the font size to a reasonable range. // We do the same, since HB is susceptible to int overflows for degenerate values. static constexpr float kMinSize = 0.1f, kMaxSize = 1296.0f; const Shaper::TextDesc text_desc = { fText->fTypeface, SkTPin(fText->fTextSize, kMinSize, kMaxSize), SkTPin(fText->fMinTextSize, kMinSize, kMaxSize), SkTPin(fText->fMaxTextSize, kMinSize, kMaxSize), fText->fLineHeight, fText->fLineShift, fText->fAscent, fText->fHAlign, fText->fVAlign, fText->fResize, fText->fLineBreak, fText->fDirection, fText->fCapitalization, fText->fMaxLines, this->shaperFlags(), fText->fLocale.isEmpty() ? nullptr : fText->fLocale.c_str(), fText->fFontFamily.isEmpty() ? nullptr : fText->fFontFamily.c_str(), }; auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr, fShapingFactory); if (fLogger) { if (shape_result.fFragments.empty() && fText->fText.size() > 0) { const auto msg = SkStringPrintf("Text layout failed for '%s'.", fText->fText.c_str()); fLogger->log(Logger::Level::kError, msg.c_str()); // These may trigger repeatedly when the text is animating. // To avoid spamming, only log once. fLogger = nullptr; } if (shape_result.fMissingGlyphCount > 0) { const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.", shape_result.fMissingGlyphCount, fText->fText.c_str()); fLogger->log(Logger::Level::kWarning, msg.c_str()); fLogger = nullptr; } } // Save the text shaping scale for later adjustments. fTextShapingScale = shape_result.fScale; // Rebuild all fragments. // TODO: we can be smarter here and try to reuse the existing SG structure if needed. fRoot->clear(); fFragments.clear(); if (SHOW_LAYOUT_BOXES) { auto box_color = sksg::Color::Make(0xffff0000); box_color->setStyle(SkPaint::kStroke_Style); box_color->setStrokeWidth(1); box_color->setAntiAlias(true); auto bounds_color = sksg::Color::Make(0xff00ff00); bounds_color->setStyle(SkPaint::kStroke_Style); bounds_color->setStrokeWidth(1); bounds_color->setAntiAlias(true); fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox), std::move(box_color))); fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()), std::move(bounds_color))); if (fPathInfo) { auto path_color = sksg::Color::Make(0xffffff00); path_color->setStyle(SkPaint::kStroke_Style); path_color->setStrokeWidth(1); path_color->setAntiAlias(true); fRoot->addChild( sksg::Draw::Make(sksg::Path::Make(static_cast(fPathInfo->fPath)), std::move(path_color))); } } // Depending on whether a GlyphDecorator is present, we either add the glyph render nodes // directly to the root group, or to an intermediate GlyphDecoratorNode container. sksg::Group* container = fRoot.get(); sk_sp decorator_node; if (fText->fDecorator) { decorator_node = sk_make_sp(fText->fDecorator, fTextShapingScale); container = decorator_node.get(); } // N.B. addFragment moves shaped glyph data out of the fragment, so only the fragment // metrics are valid after this block. for (size_t i = 0; i < shape_result.fFragments.size(); ++i) { this->addFragment(shape_result.fFragments[i], container); } if (decorator_node) { decorator_node->updateFragmentData(fFragments); fRoot->addChild(std::move(decorator_node)); } if (!fAnimators.empty() || fPathInfo) { // Range selectors and text paths require fragment domain maps. this->buildDomainMaps(shape_result); } } void TextAdapter::onSync() { if (!fText->fHasFill && !fText->fHasStroke) { return; } if (fText.hasChanged()) { this->reshape(); } if (fFragments.empty()) { return; } // Update the path contour measure, if needed. if (fPathInfo) { fPathInfo->updateContourData(); } // Seed props from the current text value. TextAnimator::ResolvedProps seed_props; seed_props.fill_color = fText->fFillColor; seed_props.stroke_color = fText->fStrokeColor; seed_props.stroke_width = fText->fStrokeWidth; TextAnimator::ModulatorBuffer buf; buf.resize(fFragments.size(), { seed_props, 0 }); // Apply all animators to the modulator buffer. for (const auto& animator : fAnimators) { animator->modulateProps(fMaps, buf); } const TextAnimator::DomainMap* grouping_domain = nullptr; switch (fAnchorPointGrouping) { // for word/line grouping, we rely on domain map info case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break; case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break; // remaining grouping modes (character/all) do not need (or have) domain map data default: break; } size_t grouping_span_index = 0; SkV2 current_line_offset = { 0, 0 }; // cumulative line spacing auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf, const TextAnimator::DomainSpan& line_span) { SkV2 total_spacing = {0,0}; float total_tracking = 0; // Only compute these when needed. if (fRequiresLineAdjustments && line_span.fCount) { for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) { const auto& props = buf[i].props; total_spacing += props.line_spacing; total_tracking += props.tracking; } // The first glyph does not contribute |before| tracking, and the last one does not // contribute |after| tracking. total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking + buf[line_span.fOffset + line_span.fCount - 1].props.tracking); } return std::make_tuple(total_spacing, total_tracking); }; // Finally, push all props to their corresponding fragment. for (const auto& line_span : fMaps.fLinesMap) { const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span); const auto align_offset = -line_tracking * align_factor(fText->fHAlign); // line spacing of the first line is ignored (nothing to "space" against) if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) { // For each line, the actual spacing is an average of individual fragment spacing // (to preserve the "line"). current_line_offset += line_spacing / line_span.fCount; } float tracking_acc = 0; for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) { // Track the grouping domain span in parallel. if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset + (*grouping_domain)[grouping_span_index].fCount) { grouping_span_index += 1; SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset + (*grouping_domain)[grouping_span_index].fCount); } const auto& props = buf[i].props; const auto& frag = fFragments[i]; // AE tracking is defined per glyph, based on two components: |before| and |after|. // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2. // // Tracking is applied as a local glyph offset, and contributes to the line width for // alignment purposes. // // No |before| tracking for the first glyph, nor |after| tracking for the last one. const auto track_before = i > line_span.fOffset ? props.tracking * 0.5f : 0.0f, track_after = i < line_span.fOffset + line_span.fCount - 1 ? props.tracking * 0.5f : 0.0f; const auto frag_offset = current_line_offset + SkV2{align_offset + tracking_acc + track_before, 0}; tracking_acc += track_before + track_after; this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // % grouping_domain ? &(*grouping_domain)[grouping_span_index] : nullptr); } } } SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec, const SkV2& grouping_alignment, const TextAnimator::DomainSpan* grouping_span) const { // Construct the following 2x ascent box: // // ------------- // | | // | | ascent // | | // ----+-------------+---------- baseline // (pos) | // | | ascent // | | // ------------- // advance auto make_box = [](const SkPoint& pos, float advance, float ascent) { // note: negative ascent return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent); }; // Compute a grouping-dependent anchor point box. // The default anchor point is at the center, and gets adjusted relative to the bounds // based on |grouping_alignment|. auto anchor_box = [&]() -> SkRect { switch (fAnchorPointGrouping) { case AnchorPointGrouping::kCharacter: // Anchor box relative to each individual fragment. return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent); case AnchorPointGrouping::kWord: // Fall through case AnchorPointGrouping::kLine: { SkASSERT(grouping_span); // Anchor box relative to the first fragment in the word/line. const auto& first_span_fragment = fFragments[grouping_span->fOffset]; return make_box(first_span_fragment.fOrigin, grouping_span->fAdvance, grouping_span->fAscent); } case AnchorPointGrouping::kAll: // Anchor box is the same as the text box. return fText->fBox; } SkUNREACHABLE; }; const auto ab = anchor_box(); // Apply grouping alignment. const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x, ab.centerY() + ab.height() * 0.5f * grouping_alignment.y }; // The anchor point is relative to the fragment position. return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY }; } SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props, const FragmentRec& rec, const SkV2& frag_offset) const { const SkV3 pos = { props.position.x + rec.fOrigin.fX + frag_offset.x, props.position.y + rec.fOrigin.fY + frag_offset.y, props.position.z }; if (!fPathInfo) { return SkM44::Translate(pos.x, pos.y, pos.z); } // "Align" the paragraph box left/center/right to path start/mid/end, respectively. const auto align_offset = align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width()); // Path positioning is based on the fragment position relative to the paragraph box // upper-left corner: // // - the horizontal component determines the distance on path // // - the vertical component is post-applied after orienting on path // // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}. // const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop}; const auto path_distance = rel_pos.x + align_offset; return fPathInfo->getMatrix(path_distance, fText->fHAlign) * SkM44::Translate(0, rel_pos.y, pos.z); } void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props, const FragmentRec& rec, const SkV2& frag_offset, const SkV2& grouping_alignment, const TextAnimator::DomainSpan* grouping_span) const { const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span); rec.fMatrixNode->setMatrix( this->fragmentMatrix(props, rec, anchor_point + frag_offset) * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x)) * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y)) * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z)) * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z) * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0)); const auto scale_alpha = [](SkColor c, float o) { return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c))); }; if (rec.fFillColorNode) { rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity)); } if (rec.fStrokeColorNode) { rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity)); rec.fStrokeColorNode->setStrokeWidth(props.stroke_width * fTextShapingScale); } if (rec.fBlur) { rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma, props.blur.y * kBlurSizeToSigma }); } } } // namespace skottie::internal