| /* | |
| * Copyright 2019 Google LLC | |
| * | |
| * 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/SkTPin.h" | |
| #include "include/private/SkTo.h" | |
| #include "include/utils/SkTextUtils.h" | |
| #include "modules/jsonreader/SkJSONReader.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/core/SkPathPriv.h" | |
| #include <algorithm> | |
| #include <array> | |
| #include <cmath> | |
| #include <cstddef> | |
| #include <limits> | |
| #include <tuple> | |
| #include <utility> | |
| 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<GlyphDecorator> decorator, float scale) | |
| : fDecorator(std::move(decorator)) | |
| , fScale(scale) | |
| {} | |
| ~GlyphDecoratorNode() override = default; | |
| void updateFragmentData(const std::vector<TextAdapter::FragmentRec>& recs) { | |
| fFragCount = recs.size(); | |
| SkASSERT(!fFragInfo); | |
| fFragInfo = std::make_unique<FragmentInfo[]>(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<GlyphDecorator::GlyphInfo[]>(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<SkMatrix>(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<sksg::Matrix<SkM44>> fMatrixNode; | |
| float fAdvance; | |
| }; | |
| const sk_sp<GlyphDecorator> fDecorator; | |
| const float fScale; | |
| std::unique_ptr<FragmentInfo[]> fFragInfo; | |
| std::unique_ptr<GlyphDecorator::GlyphInfo[]> 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<SkPath>(fPath); | |
| if (reverse) { | |
| path = SkPathPriv::ReversePath(path); | |
| } | |
| 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<SkContourMeasure> fCurrentMeasure; | |
| bool fCurrentReversed = false, | |
| fCurrentClosed = false; | |
| }; | |
| sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer, | |
| const AnimationBuilder* abuilder, | |
| sk_sp<SkFontMgr> fontmgr, | |
| sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper, | |
| sk_sp<Logger> 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<const skjson::ObjectValue*>((*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 auto gGroupingMap = std::to_array<AnchorPointGrouping>({ | |
| AnchorPointGrouping::kCharacter, // 'g': 1 | |
| AnchorPointGrouping::kWord, // 'g': 2 | |
| AnchorPointGrouping::kLine, // 'g': 3 | |
| AnchorPointGrouping::kAll, // 'g': 4 | |
| }); | |
| const auto apg = jm | |
| ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, std::size(gGroupingMap)) | |
| : 1; | |
| auto adapter = sk_sp<TextAdapter>(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<PathInfo> { | |
| if (!jpath) { | |
| return nullptr; | |
| } | |
| // the actual path is identified as an index in the layer mask stack | |
| const auto mask_index = | |
| ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::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<PathInfo>(); | |
| 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<SkFontMgr> fontmgr, | |
| sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper, | |
| sk_sp<Logger> logger, | |
| sk_sp<SkShapers::Factory> 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<sk_sp<sksg::RenderNode>> | |
| TextAdapter::buildGlyphCompNodes(Shaper::ShapedGlyphs& glyphs) const { | |
| std::vector<sk_sp<sksg::RenderNode>> 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<SkM44>::Make(SkM44::Translate(frag.fOrigin.x(), | |
| frag.fOrigin.y())); | |
| // Start off substituting existing comp nodes for all composition-based glyphs. | |
| std::vector<sk_sp<sksg::RenderNode>> draws = this->buildGlyphCompNodes(frag.fGlyphs); | |
| // Use a regular GlyphTextNode for the remaining glyphs (backed by a real SkTypeface). | |
| auto text_node = sk_make_sp<GlyphTextNode>(std::move(frag.fGlyphs)); | |
| rec.fGlyphs = text_node->glyphs(); | |
| draws.reserve(draws.size() + | |
| static_cast<size_t>(fText->fHasFill) + | |
| static_cast<size_t>(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}); | |
| } | |
| for (auto& animator : fAnimators) { | |
| animator->updateDomainMaps(fMaps, shape_result.fFragments.size()); | |
| } | |
| } | |
| 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(), | |
| fText->fTextTracking, | |
| }; | |
| 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<SkPath>(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<GlyphDecoratorNode> decorator_node; | |
| if (fText->fDecorator) { | |
| decorator_node = sk_make_sp<GlyphDecoratorNode>(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(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 |