[skottie] Shaper: fission glyphs into separate fragments when needed

In the presence of animated text properties, we want the Shaper result
glyphs to be split into separate fragments.

Add a Shaper flag to request this mode, and upate the logic to emit
one fragment per glyph, when active.  Otherwise fall back to the
current/consolidated blob mode.

Change-Id: If7440e5fa1ae2f8855984d3ae4d6852b10b2316c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/216879
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
This commit is contained in:
Florin Malita 2019-05-30 12:10:07 -04:00 committed by Skia Commit-Bot
parent f62e575bab
commit 0fe004c14c
6 changed files with 122 additions and 29 deletions

View File

@ -259,6 +259,7 @@ DEF_TEST(Skottie_Shaper_HAlign, reporter) {
tsize.text_size,
talign.align,
skottie::Shaper::VAlign::kTopBaseline,
Shaper::Flags::kNone
};
const auto shape_result = skottie::Shaper::Shape(text, desc, text_point);
@ -321,6 +322,7 @@ DEF_TEST(Skottie_Shaper_VAlign, reporter) {
tsize.text_size,
SkTextUtils::Align::kCenter_Align,
talign.align,
Shaper::Flags::kNone
};
const auto shape_result = skottie::Shaper::Shape(text, desc, text_box);
@ -346,3 +348,35 @@ DEF_TEST(Skottie_Shaper_VAlign, reporter) {
}
}
}
DEF_TEST(Skottie_Shaper_FragmentGlyphs, reporter) {
skottie::Shaper::TextDesc desc = {
SkTypeface::MakeDefault(),
18,
18,
SkTextUtils::Align::kCenter_Align,
Shaper::VAlign::kTop,
Shaper::Flags::kNone
};
const SkString text("Foo bar baz");
const auto text_box = SkRect::MakeWH(100, 100);
{
const auto shape_result = skottie::Shaper::Shape(text, desc, text_box);
// Default/consolidated mode => single blob result.
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
}
{
desc.fFlags = Shaper::Flags::kFragmentGlyphs;
const auto shape_result = skottie::Shaper::Shape(text, desc, text_box);
// Fragmented mode => one blob per glyph.
const size_t expectedSize = text.size();
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == expectedSize);
for (size_t i = 0; i < expectedSize; ++i) {
REPORTER_ASSERT(reporter, shape_result.fFragments[i].fBlob);
}
}
}

View File

@ -101,53 +101,97 @@ public:
void commitLine() override {
fOffset.fY += fDesc.fLineHeight;
// Commit all accumulated runs to the blob.
// TODO: justification adjustments
// TODO: multi-blob/fragmented results
using CommitProc = void(*)(const RunRec&,
const SkRect&,
const SkGlyphID*,
const SkPoint*,
SkTextBlobBuilder*,
Shaper::Result*);
static const CommitProc fragment_commit_proc = [](const RunRec& rec,
const SkRect& box,
const SkGlyphID* glyphs,
const SkPoint* pos,
SkTextBlobBuilder* builder,
Shaper::Result* result) {
// In fragmented mode we immediately push the glyphs to fResult,
// one fragment per glyph.
for (size_t i = 0; i < rec.fGlyphCount; ++i) {
const auto& blob_buffer = builder->allocRunPos(rec.fFont, 1);
blob_buffer.glyphs[0] = glyphs[i];
blob_buffer.pos[0] = pos[i].fX;
blob_buffer.pos[1] = pos[i].fY;
result->fFragments.push_back({builder->make(), {box.x(), box.y()}});
}
};
static const CommitProc consolidated_commit_proc = [](const RunRec& rec,
const SkRect& box,
const SkGlyphID* glyphs,
const SkPoint* pos,
SkTextBlobBuilder* builder,
Shaper::Result*) {
// In consolidated mode we just accumulate glyphs to the blob builder, then push
// to fResult as a single blob in finalize().
const auto& blob_buffer = builder->allocRunPos(rec.fFont, rec.fGlyphCount);
sk_careful_memcpy(blob_buffer.glyphs, glyphs, rec.fGlyphCount * sizeof(SkGlyphID));
sk_careful_memcpy(blob_buffer.pos , pos , rec.fGlyphCount * sizeof(SkPoint));
};
const auto commit_proc = (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)
? fragment_commit_proc : consolidated_commit_proc;
size_t run_offset = 0;
for (const auto& rec : fLineRuns) {
SkASSERT(run_offset < fLineGlyphCount);
const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, rec.fGlyphCount);
sk_careful_memcpy(blob_buffer.glyphs,
commit_proc(rec, fBox,
fLineGlyphs.get() + run_offset,
rec.fGlyphCount * sizeof(SkGlyphID));
sk_careful_memcpy(blob_buffer.pos,
fLinePos.get() + run_offset,
rec.fGlyphCount * sizeof(SkPoint));
&fBuilder, &fResult);
run_offset += rec.fGlyphCount;
}
}
Shaper::Result makeBlob() {
auto blob = fBuilder.make();
SkPoint pos {fBox.x(), fBox.y()};
Shaper::Result finalize() {
if (!(fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)) {
// All glyphs are pending in a single blob.
SkASSERT(fResult.fFragments.empty());
fResult.fFragments.reserve(1);
fResult.fFragments.push_back({fBuilder.make(), {fBox.x(), fBox.y()}});
}
// By default, first line is vertical-aligned on a baseline of 0.
// Perform additional adjustments based on VAlign.
float v_offset = 0;
switch (fDesc.fVAlign) {
case Shaper::VAlign::kTop:
pos.fY -= ComputeBlobBounds(blob).fTop;
v_offset = fBox.fTop - fResult.computeBounds().fTop;
break;
case Shaper::VAlign::kTopBaseline:
// Default behavior.
break;
case Shaper::VAlign::kCenter: {
const auto bounds = ComputeBlobBounds(blob).makeOffset(pos.x(), pos.y());
pos.fY += fBox.centerY() - bounds.centerY();
} break;
case Shaper::VAlign::kCenter:
v_offset = fBox.centerY() - fResult.computeBounds().centerY();
break;
case Shaper::VAlign::kBottom:
pos.fY += fBox.height() - ComputeBlobBounds(blob).fBottom;
v_offset = fBox.fBottom - fResult.computeBounds().fBottom;
break;
case Shaper::VAlign::kResizeToFit:
SkASSERT(false);
break;
}
// single blob for now
return { std::vector<Shaper::Fragment>(1ul, { std::move(blob), pos })};
if (v_offset) {
for (auto& fragment : fResult.fFragments) {
fragment.fPos.fY += v_offset;
}
}
return std::move(fResult);
}
void shapeLine(const char* start, const char* end) {
@ -194,6 +238,8 @@ private:
SkPoint fCurrentPosition{ 0, 0 };
SkPoint fOffset{ 0, 0 };
SkVector fPendingLineAdvance{ 0, 0 };
Shaper::Result fResult;
};
Shaper::Result ShapeImpl(const SkString& txt, const Shaper::TextDesc& desc, const SkRect& box) {
@ -217,7 +263,7 @@ Shaper::Result ShapeImpl(const SkString& txt, const Shaper::TextDesc& desc, cons
}
blobMaker.shapeLine(line_start, ptr);
return blobMaker.makeBlob();
return blobMaker.finalize();
}
Shaper::Result ShapeToFit(const SkString& txt, const Shaper::TextDesc& orig_desc,

View File

@ -45,12 +45,21 @@ public:
kResizeToFit,
};
enum Flags : uint32_t {
kNone = 0x00,
// Split out individual glyphs into separate Fragments
// (useful when the caller intends to manipulate glyphs independently).
kFragmentGlyphs = 0x01,
};
struct TextDesc {
const sk_sp<SkTypeface>& fTypeface;
SkScalar fTextSize,
fLineHeight;
SkTextUtils::Align fHAlign;
VAlign fVAlign;
uint32_t fFlags;
};
// Performs text layout along an infinite horizontal line, starting at |textPoint|.

View File

@ -15,7 +15,9 @@
namespace skottie {
TextAdapter::TextAdapter(sk_sp<sksg::Group> root) : fRoot(std::move(root)) {}
TextAdapter::TextAdapter(sk_sp<sksg::Group> root, bool hasAnimators)
: fRoot(std::move(root))
, fHasAnimators(hasAnimators) {}
TextAdapter::~TextAdapter() = default;
@ -76,6 +78,7 @@ void TextAdapter::apply() {
fText.fLineHeight,
fText.fHAlign,
fText.fVAlign,
fHasAnimators ? Shaper::Flags::kFragmentGlyphs : Shaper::Flags::kNone,
};
const auto shape_result = Shaper::Shape(fText.fText, text_desc, fText.fBox);

View File

@ -22,7 +22,7 @@ namespace skottie {
class TextAdapter final : public SkNVRefCnt<TextAdapter> {
public:
explicit TextAdapter(sk_sp<sksg::Group> root);
TextAdapter(sk_sp<sksg::Group> root, bool hasAnimators);
~TextAdapter();
ADAPTER_PROPERTY(Text, TextValue, TextValue())
@ -38,6 +38,8 @@ private:
sk_sp<sksg::Group> fRoot;
std::vector<FragmentRec> fFragments;
const bool fHasAnimators;
};
} // namespace skottie

View File

@ -289,9 +289,8 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectVa
}
const skjson::ArrayValue* animated_props = (*jt)["a"];
if (animated_props && animated_props->size() > 0) {
this->log(Logger::Level::kWarning, nullptr, "Unsupported animated text properties.");
}
const auto has_animators = (animated_props && animated_props->size() > 0);
// TODO: actually parse/implement animators.
const skjson::ObjectValue* jd = (*jt)["d"];
if (!jd) {
@ -299,7 +298,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectVa
}
auto text_root = sksg::Group::Make();
auto adapter = sk_make_sp<TextAdapter>(text_root);
auto adapter = sk_make_sp<TextAdapter>(text_root, has_animators);
this->bindProperty<TextValue>(*jd, ascope, [adapter] (const TextValue& txt) {
adapter->setText(txt);