blob: 97d51902bee74ec2db216bb8ea65a38e21a16272 [file] [log] [blame]
/*
* 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/SkContourMeasure.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkM44.h"
#include "include/private/base/SkTPin.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/text/RangeSelector.h"
#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"
// 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::kConservative);
}
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) {
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<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) {
// 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 AnchorPointGrouping gGroupingMap[] = {
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),
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,
AnchorPointGrouping apg)
: fRoot(sksg::Group::Make())
, fFontMgr(std::move(fontmgr))
, fCustomGlyphMapper(std::move(custom_glyph_mapper))
, fLogger(std::move(logger))
, 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];
if (frag.fIsWhitespace) {
if (in_word) {
in_word = false;
fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
}
} else {
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 (frag.fLineIndex != 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.size() ? fText->fLocale.c_str() : nullptr,
};
auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
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(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