[skottie] Text anchor point grouping support
Implement all AE grouping modes: character/word/line/all. -- character grouping was already supported (default mode) -- for word and line grouping, expand the existing domain mapping logic to also track cumulative advance and max(ascent) per span, then use this info to compute anchor point boxes -- for "all" grouping, the anchor point box coincides with the text box (https://helpx.adobe.com/after-effects/using/animating-text.html#text_anchor_point_properties) TBR= Change-Id: I8564f1349d167d82c31862d8f7e57615cdae0dcf Reviewed-on: https://skia-review.googlesource.com/c/skia/+/274201 Reviewed-by: Florin Malita <fmalita@chromium.org> Commit-Queue: Florin Malita <fmalita@chromium.org>
This commit is contained in:
parent
3705bf1b25
commit
960f3d4cd1
@ -50,7 +50,10 @@ sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// "m": {}, // "more options" (TODO)
|
||||
// "m": { // "more options"
|
||||
// "g": 1, // Anchor Point Grouping
|
||||
// "a": {...} // Grouping Alignment
|
||||
// },
|
||||
// "p": {} // "path options" (TODO)
|
||||
// },
|
||||
|
||||
@ -62,11 +65,24 @@ sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr), std::move(logger)));
|
||||
adapter->bind(*abuilder, jd, &adapter->fText.fCurrentValue);
|
||||
|
||||
// "More options"
|
||||
if (const skjson::ObjectValue* jm = (*jt)["m"]) {
|
||||
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, SK_ARRAY_COUNT(gGroupingMap))
|
||||
: 1;
|
||||
|
||||
auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
|
||||
std::move(logger),
|
||||
gGroupingMap[SkToSizeT(apg - 1)]));
|
||||
|
||||
adapter->bind(*abuilder, jd, &adapter->fText.fCurrentValue);
|
||||
if (jm) {
|
||||
adapter->bind(*abuilder, (*jm)["a"], &adapter->fGroupingAlignment);
|
||||
}
|
||||
|
||||
@ -89,10 +105,11 @@ sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
|
||||
return adapter;
|
||||
}
|
||||
|
||||
TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger)
|
||||
TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg)
|
||||
: fRoot(sksg::Group::Make())
|
||||
, fFontMgr(std::move(fontmgr))
|
||||
, fLogger(std::move(logger))
|
||||
, fAnchorPointGrouping(apg)
|
||||
, fHasBlurAnimator(false)
|
||||
, fRequiresAnchorPoint(false) {}
|
||||
|
||||
@ -168,6 +185,12 @@ void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
|
||||
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?
|
||||
@ -177,31 +200,39 @@ void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
|
||||
if (frag.fIsWhitespace) {
|
||||
if (in_word) {
|
||||
in_word = false;
|
||||
fMaps.fWordsMap.push_back({word_start, i - word_start});
|
||||
fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
|
||||
}
|
||||
} else {
|
||||
fMaps.fNonWhitespaceMap.push_back({i, 1});
|
||||
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});
|
||||
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});
|
||||
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});
|
||||
fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,6 +338,17 @@ void TextAdapter::onSync() {
|
||||
const auto grouping_alignment =
|
||||
ValueTraits<VectorValue>::As<SkVector>(fGroupingAlignment) * .01f; // percentage
|
||||
|
||||
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;
|
||||
|
||||
// Finally, push all props to their corresponding fragment.
|
||||
for (const auto& line_span : fMaps.fLinesMap) {
|
||||
float line_tracking = 0;
|
||||
@ -315,9 +357,19 @@ void TextAdapter::onSync() {
|
||||
// Tracking requires special treatment: unlike other props, its effect is not localized
|
||||
// to a single fragment, but requires re-alignment of the whole line.
|
||||
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];
|
||||
this->pushPropsToFragment(props, frag, grouping_alignment);
|
||||
this->pushPropsToFragment(props, frag, grouping_alignment,
|
||||
grouping_domain ? &(*grouping_domain)[grouping_span_index]
|
||||
: nullptr);
|
||||
|
||||
line_tracking += props.tracking;
|
||||
line_has_tracking |= !SkScalarNearlyZero(props.tracking);
|
||||
@ -330,22 +382,66 @@ void TextAdapter::onSync() {
|
||||
}
|
||||
|
||||
SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
|
||||
const SkVector& group_alignment) const {
|
||||
// TODO: implement word/line/paragraph alignment options.
|
||||
const SkVector& grouping_alignment,
|
||||
const TextAnimator::DomainSpan* grouping_span) const {
|
||||
// Construct the following 2x ascent box:
|
||||
//
|
||||
// -------------
|
||||
// | |
|
||||
// | | ascent
|
||||
// | |
|
||||
// ----+-------------+---------- baseline
|
||||
// (pos) |
|
||||
// | | ascent
|
||||
// | |
|
||||
// -------------
|
||||
// advance
|
||||
|
||||
// Character alignment: origin is at [advance/2,0] (mid-advance, baseline)
|
||||
const SkV2 ap = { rec.fAdvance * 0.5f, 0 };
|
||||
auto make_box = [](const SkPoint& pos, float advance, float ascent) {
|
||||
// note: negative ascent
|
||||
return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
|
||||
};
|
||||
|
||||
// Group alignment is an origin adjustment relative to the
|
||||
// ascent box [0, ascent, advance, 0]
|
||||
return ap + SkV2{group_alignment.fX * (ap.x - 0),
|
||||
group_alignment.fY * (ap.y - rec.fAscent)};
|
||||
// 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.fX,
|
||||
ab.centerY() + ab.height() * 0.5f * grouping_alignment.fY };
|
||||
|
||||
// The anchor point is relative to the fragment position.
|
||||
return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
|
||||
}
|
||||
|
||||
void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
|
||||
const FragmentRec& rec,
|
||||
const SkVector& grouping_alignment) const {
|
||||
const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment);
|
||||
const SkVector& grouping_alignment,
|
||||
const TextAnimator::DomainSpan* grouping_span) const {
|
||||
const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
|
||||
|
||||
rec.fMatrixNode->setMatrix(
|
||||
SkM44::Translate(props.position.x + rec.fOrigin.x() + anchor_point.x,
|
||||
|
@ -43,7 +43,14 @@ protected:
|
||||
void onSync() override;
|
||||
|
||||
private:
|
||||
TextAdapter(sk_sp<SkFontMgr>, sk_sp<Logger>);
|
||||
enum class AnchorPointGrouping : uint8_t {
|
||||
kCharacter,
|
||||
kWord,
|
||||
kLine,
|
||||
kAll,
|
||||
};
|
||||
|
||||
TextAdapter(sk_sp<SkFontMgr>, sk_sp<Logger>, AnchorPointGrouping);
|
||||
|
||||
struct FragmentRec {
|
||||
SkPoint fOrigin; // fragment position
|
||||
@ -62,18 +69,20 @@ private:
|
||||
void buildDomainMaps(const Shaper::Result&);
|
||||
|
||||
void pushPropsToFragment(const TextAnimator::ResolvedProps&, const FragmentRec&,
|
||||
const SkVector&) const;
|
||||
const SkVector&, const TextAnimator::DomainSpan*) const;
|
||||
|
||||
void adjustLineTracking(const TextAnimator::ModulatorBuffer&,
|
||||
const TextAnimator::DomainSpan&,
|
||||
float line_tracking) const;
|
||||
|
||||
SkV2 fragmentAnchorPoint(const FragmentRec&, const SkVector&) const;
|
||||
SkV2 fragmentAnchorPoint(const FragmentRec&, const SkVector&,
|
||||
const TextAnimator::DomainSpan*) const;
|
||||
uint32_t shaperFlags() const;
|
||||
|
||||
const sk_sp<sksg::Group> fRoot;
|
||||
const sk_sp<SkFontMgr> fFontMgr;
|
||||
sk_sp<Logger> fLogger;
|
||||
const AnchorPointGrouping fAnchorPointGrouping;
|
||||
|
||||
std::vector<sk_sp<TextAnimator>> fAnimators;
|
||||
std::vector<FragmentRec> fFragments;
|
||||
|
@ -66,7 +66,10 @@ public:
|
||||
// Each domain[i] represents a [domain[i].fOffset.. domain[i].fOffset+domain[i].fCount-1]
|
||||
// fragment subset.
|
||||
struct DomainSpan {
|
||||
size_t fOffset, fCount;
|
||||
size_t fOffset,
|
||||
fCount;
|
||||
float fAdvance, // cumulative advance for all fragments in span
|
||||
fAscent; // max ascent for all fragments in span
|
||||
};
|
||||
using DomainMap = std::vector<DomainSpan>;
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user