[skottie] Add support for range selector domains

Range selector's "Based On" property controls how range indices map
to glyphs: characters, characters-excluding-spaces, words, lines.

To support this feature:

  - update SkottieShaper to track domain-relevant info per fragment
    (fLineIndex, fIsWhitespace)

  - update TextAdapter to build domain maps
    (domain index -> fragment span)

  - update RangeSelector to run its range indices through a domain map,
    if present.

Change-Id: I80e713f6beaa2578aa0eae1d1ddae8e1e47d8d10
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/219859
Commit-Queue: Florin Malita <fmalita@chromium.org>
Reviewed-by: Ben Wagner <bungeman@google.com>
This commit is contained in:
Florin Malita 2019-06-10 13:21:28 -04:00 committed by Skia Commit-Bot
parent 22ea7e994b
commit e695e53f1c
9 changed files with 257 additions and 118 deletions

View File

@ -63,19 +63,76 @@ struct UnitTraits<RangeSelector::Units::kIndex> {
}
};
using CoverageProcT = void(*)(float amount,
TextAnimator::AnimatedPropsModulator* dst,
size_t count);
class CoverageProcessor {
public:
CoverageProcessor(const TextAnimator::DomainMaps& maps,
RangeSelector::Domain domain,
RangeSelector::Mode mode,
TextAnimator::ModulatorBuffer& dst)
: fDst(dst)
, fDomainSize(dst.size()) {
static const CoverageProcT gCoverageProcs[] = {
// Mode::kAdd
[](float amount, TextAnimator::AnimatedPropsModulator* dst, size_t count) {
SkASSERT(mode == RangeSelector::Mode::kAdd);
fProc = &CoverageProcessor::add_proc;
switch (domain) {
case RangeSelector::Domain::kChars:
// Direct (1-to-1) index mapping.
break;
case RangeSelector::Domain::kCharsExcludingSpaces:
fMap = &maps.fNonWhitespaceMap;
break;
case RangeSelector::Domain::kWords:
fMap = &maps.fWordsMap;
break;
case RangeSelector::Domain::kLines:
fMap = &maps.fLinesMap;
break;
}
// When no domain map is active, fProc points directly to the mode proc.
// Otherwise, we punt through a domain mapper proxy.
if (fMap) {
fMappedProc = fProc;
fProc = &CoverageProcessor::domain_map_proc;
fDomainSize = fMap->size();
}
}
size_t size() const { return fDomainSize; }
void operator()(float amount, size_t offset, size_t count) const {
(this->*fProc)(amount, offset, count);
}
private:
// mode: kAdd
void add_proc(float amount, size_t offset, size_t count) const {
if (!amount || !count) return;
for (size_t i = 0; i < count; ++i) {
dst[i].coverage = SkTPin<float>(dst[i].coverage + amount, -1, 1);
for (auto* dst = fDst.data() + offset; dst < fDst.data() + offset + count; ++dst) {
dst->coverage = SkTPin<float>(dst->coverage + amount, -1, 1);
}
},
}
// A proxy for mapping domain indices to the target buffer.
void domain_map_proc(float amount, size_t offset, size_t count) const {
SkASSERT(fMap);
SkASSERT(fMappedProc);
for (auto i = offset; i < offset + count; ++i) {
const auto& span = (*fMap)[i];
(this->*fMappedProc)(amount, span.fOffset, span.fCount);
}
}
using ProcT = void(CoverageProcessor::*)(float amount, size_t offset, size_t count) const;
TextAnimator::ModulatorBuffer& fDst;
ProcT fProc,
fMappedProc = nullptr;
const TextAnimator::DomainMap* fMap = nullptr;
size_t fDomainSize;
};
// Each shape generator is defined in a normalized domain, over three |t| intervals:
@ -128,7 +185,10 @@ sk_sp<RangeSelector> RangeSelector::Make(const skjson::ObjectValue* jrange,
};
static constexpr Domain gDomainMap[] = {
Domain::kChars, // 'b': 1
Domain::kChars, // 'b': 1
Domain::kCharsExcludingSpaces, // 'b': 2
Domain::kWords, // 'b': 3
Domain::kLines, // 'b': 4
};
static constexpr Mode gModeMap[] = {
@ -220,23 +280,23 @@ std::tuple<float, float> RangeSelector::resolve(size_t len) const {
* 4) Finally, the resulting coverage is accumulated to existing fragment coverage based on
* the specified Mode (add, difference, etc).
*/
void RangeSelector::modulateCoverage(TextAnimator::ModulatorBuffer& buf) const {
SkASSERT(!buf.empty());
void RangeSelector::modulateCoverage(const TextAnimator::DomainMaps& maps,
TextAnimator::ModulatorBuffer& mbuf) const {
const CoverageProcessor coverage_proc(maps, fDomain, fMode, mbuf);
if (coverage_proc.size() == 0) {
return;
}
// Amount is percentage based [-100% .. 100%].
const auto amount = SkTPin<float>(fAmount / 100, -1, 1);
// First, resolve to a float range in the given domain.
SkAssertResult(fDomain == Domain::kChars);
const auto f_range = this->resolve(buf.size());
const auto f_range = this->resolve(coverage_proc.size());
// f_range pinned to [0..size].
const auto f_buf_size = static_cast<float>(buf.size()),
f0 = SkTPin(std::get<0>(f_range), 0.0f, f_buf_size),
f1 = SkTPin(std::get<1>(f_range), 0.0f, f_buf_size);
SkASSERT(static_cast<size_t>(fMode) < SK_ARRAY_COUNT(gCoverageProcs));
const auto& coverage_proc = gCoverageProcs[static_cast<size_t>(fMode)];
// f_range pinned to [0..domain_size].
const auto f_dom_size = static_cast<float>(coverage_proc.size()),
f0 = SkTPin(std::get<0>(f_range), 0.0f, f_dom_size),
f1 = SkTPin(std::get<1>(f_range), 0.0f, f_dom_size);
SkASSERT(static_cast<size_t>(fShape) < SK_ARRAY_COUNT(gShapeGenerators));
const auto& generator = gShapeGenerators[static_cast<size_t>(fShape)];
@ -245,22 +305,22 @@ void RangeSelector::modulateCoverage(TextAnimator::ModulatorBuffer& buf) const {
{
// Constant coverage count before the shape left edge, and after the right edge.
const auto count_lo = static_cast<size_t>(std::floor(f0)),
count_hi = static_cast<size_t>(f_buf_size - std::ceil (f1));
SkASSERT(count_lo <= buf.size());
SkASSERT(count_hi <= buf.size());
count_hi = static_cast<size_t>(f_dom_size - std::ceil (f1));
SkASSERT(count_lo <= coverage_proc.size());
SkASSERT(count_hi <= coverage_proc.size());
coverage_proc(amount * generator.lo, buf.data() , count_lo);
coverage_proc(amount * generator.hi, buf.data() + buf.size() - count_hi, count_hi);
coverage_proc(amount * generator.lo, 0 , count_lo);
coverage_proc(amount * generator.hi, coverage_proc.size() - count_hi, count_hi);
if (count_lo == buf.size() || count_hi == buf.size()) {
if (count_lo == coverage_proc.size() || count_hi == coverage_proc.size()) {
// The shape is completely outside the domain - we're done.
return;
}
}
// Integral/index range.
const auto i0 = std::min<size_t>(f0, buf.size() - 1),
i1 = std::min<size_t>(f1, buf.size() - 1);
const auto i0 = std::min<size_t>(f0, coverage_proc.size() - 1),
i1 = std::min<size_t>(f1, coverage_proc.size() - 1);
SkASSERT(i0 <= i1);
const auto range_span = std::get<1>(f_range) - std::get<0>(f_range);
@ -269,7 +329,7 @@ void RangeSelector::modulateCoverage(TextAnimator::ModulatorBuffer& buf) const {
SkASSERT(i0 == i1);
const auto ratio = f0 - i0,
coverage = Lerp(generator.lo, generator.hi, ratio);
coverage_proc(amount * coverage, buf.data() + i0, 1);
coverage_proc(amount * coverage, i0, 1);
return;
}
@ -315,7 +375,7 @@ void RangeSelector::modulateCoverage(TextAnimator::ModulatorBuffer& buf) const {
auto t = (i0 + 0.5f - std::get<0>(f_range)) / range_span;
// [i0] may have partial coverage.
coverage_proc(amount * partial_coverage(generator(std::max(t, 0.0f)), i0), buf.data() + i0, 1);
coverage_proc(amount * partial_coverage(generator(std::max(t, 0.0f)), i0), i0, 1);
// If the whole range falls within a single fragment, we're done.
if (i0 == i1) {
@ -325,14 +385,14 @@ void RangeSelector::modulateCoverage(TextAnimator::ModulatorBuffer& buf) const {
t += dt;
// [i0+1..i1-1] has full coverage.
for (auto* dst = buf.data() + i0 + 1; dst < buf.data() + i1; ++dst) {
for (auto i = i0 + 1; i < i1; ++i) {
SkASSERT(0 <= t && t <= 1);
coverage_proc(amount * generator(t), dst, 1);
coverage_proc(amount * generator(t), i, 1);
t += dt;
}
// [i1] may have partial coverage.
coverage_proc(amount * partial_coverage(generator(std::min(t, 1.0f)), i1), buf.data() + i1, 1);
coverage_proc(amount * partial_coverage(generator(std::min(t, 1.0f)), i1), i1, 1);
}
} // namespace internal

View File

@ -30,10 +30,10 @@ public:
};
enum class Domain : uint8_t {
kChars, // domain indices map to glyph indices
// kCharsExcludingSpaces, // domain indices map to glyph indices (ignoring spaces)
// kWords, // domain indices map to word indices
// kLines, // domain indices map to line indices
kChars, // domain indices map to glyph indices
kCharsExcludingSpaces, // domain indices map to glyph indices (ignoring spaces)
kWords, // domain indices map to word indices
kLines, // domain indices map to line indices
};
enum class Mode : uint8_t {
@ -54,7 +54,7 @@ public:
kSmooth,
};
void modulateCoverage(TextAnimator::ModulatorBuffer&) const;
void modulateCoverage(const TextAnimator::DomainMaps&, TextAnimator::ModulatorBuffer&) const;
private:
RangeSelector(Units, Domain, Mode, Shape);

View File

@ -62,6 +62,7 @@ public:
void beginLine() override {
fLineGlyphs.reset(0);
fLinePos.reset(0);
fLineClusters.reset(0);
fLineRuns.reset();
fLineGlyphCount = 0;
@ -81,15 +82,16 @@ public:
fLineGlyphs.realloc(fLineGlyphCount);
fLinePos.realloc(fLineGlyphCount);
fLineClusters.realloc(fLineGlyphCount);
fLineRuns.push_back({info.fFont, info.glyphCount});
SkVector alignmentOffset { fHAlignFactor * (fPendingLineAdvance.x() - fBox.width()), 0 };
return {
fLineGlyphs.get() + run_start_index,
fLinePos.get() + run_start_index,
nullptr,
fLineGlyphs.get() + run_start_index,
fLinePos.get() + run_start_index,
nullptr,
fLineClusters.get() + run_start_index,
fCurrentPosition + alignmentOffset
};
}
@ -103,59 +105,22 @@ public:
// TODO: justification adjustments
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 (blob) per glyph. Glyph positioning is externalized
// (positions returned in Fragment::fPos).
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] = blob_buffer.pos[1] = 0;
result->fFragments.push_back({builder->make(), { box.x() + pos[i].fX,
box.y() + pos[i].fY }});
}
};
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(). Glyph positions are baked in the
// blob (Fragment::fPos only reflects the box origin).
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;
? &BlobMaker::commitFragementedRun
: &BlobMaker::commitConsolidatedRun;
size_t run_offset = 0;
for (const auto& rec : fLineRuns) {
SkASSERT(run_offset < fLineGlyphCount);
commit_proc(rec, fBox,
fLineGlyphs.get() + run_offset,
fLinePos.get() + run_offset,
&fBuilder, &fResult);
(this->*commit_proc)(rec,
fLineGlyphs.get() + run_offset,
fLinePos.get() + run_offset,
fLineClusters.get() + run_offset,
fLineIndex);
run_offset += rec.fGlyphCount;
}
fLineIndex++;
}
Shaper::Result finalize() {
@ -163,7 +128,7 @@ public:
// 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()}});
fResult.fFragments.push_back({fBuilder.make(), {fBox.x(), fBox.y()}, 0, false});
}
// By default, first line is vertical-aligned on a baseline of 0.
@ -206,10 +171,57 @@ public:
const auto shape_width = fBox.isEmpty() ? SK_ScalarMax
: fBox.width();
fUTF8 = start;
fShaper->shape(start, SkToSizeT(end - start), fFont, true, shape_width, this);
fUTF8 = nullptr;
}
private:
struct RunRec {
SkFont fFont;
size_t fGlyphCount;
};
void commitFragementedRun(const RunRec& rec,
const SkGlyphID* glyphs,
const SkPoint* pos,
const uint32_t* clusters,
uint32_t line_index) {
static const auto is_whitespace = [](char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
};
// In fragmented mode we immediately push the glyphs to fResult,
// one fragment (blob) per glyph. Glyph positioning is externalized
// (positions returned in Fragment::fPos).
for (size_t i = 0; i < rec.fGlyphCount; ++i) {
const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, 1);
blob_buffer.glyphs[0] = glyphs[i];
blob_buffer.pos[0] = blob_buffer.pos[1] = 0;
// Note: we only check the first code point in the cluster for whitespace.
// It's unclear whether thers's a saner approach.
fResult.fFragments.push_back({fBuilder.make(),
{ fBox.x() + pos[i].fX, fBox.y() + pos[i].fY },
line_index, is_whitespace(fUTF8[clusters[i]])
});
}
}
void commitConsolidatedRun(const RunRec& rec,
const SkGlyphID* glyphs,
const SkPoint* pos,
const uint32_t*,
uint32_t) {
// In consolidated mode we just accumulate glyphs to the blob builder, then push
// to fResult as a single blob in finalize(). Glyph positions are baked in the
// blob (Fragment::fPos only reflects the box origin).
const auto& blob_buffer = fBuilder.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));
}
static float HAlignFactor(SkTextUtils::Align align) {
switch (align) {
case SkTextUtils::kLeft_Align: return 0.0f;
@ -227,19 +239,18 @@ private:
SkTextBlobBuilder fBuilder;
std::unique_ptr<SkShaper> fShaper;
struct RunRec {
SkFont fFont;
size_t fGlyphCount;
};
SkAutoSTMalloc<64, SkGlyphID> fLineGlyphs;
SkAutoSTMalloc<64, SkPoint> fLinePos;
SkSTArray<16, RunRec> fLineRuns;
size_t fLineGlyphCount = 0;
SkAutoSTMalloc<64, SkGlyphID> fLineGlyphs;
SkAutoSTMalloc<64, SkPoint> fLinePos;
SkAutoSTMalloc<64, uint32_t> fLineClusters;
SkSTArray<16, RunRec> fLineRuns;
size_t fLineGlyphCount = 0;
SkPoint fCurrentPosition{ 0, 0 };
SkPoint fOffset{ 0, 0 };
SkVector fPendingLineAdvance{ 0, 0 };
uint32_t fLineIndex = 0;
const char* fUTF8 = nullptr; // only valid during shapeLine() calls
Shaper::Result fResult;
};

View File

@ -24,6 +24,11 @@ public:
struct Fragment {
sk_sp<SkTextBlob> fBlob;
SkPoint fPos;
// Only valid for kFragmentGlyphs
uint32_t fLineIndex; // 0-based index for the line this fragment belongs to.
bool fIsWhitespace; // True if the first code point in the corresponding
// cluster is whitespace.
};
struct Result {

View File

@ -7,7 +7,6 @@
#include "modules/skottie/src/text/TextAdapter.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/SkSGGroup.h"
@ -25,15 +24,7 @@ TextAdapter::TextAdapter(sk_sp<sksg::Group> root, bool hasAnimators)
TextAdapter::~TextAdapter() = default;
struct TextAdapter::FragmentRec {
SkPoint fOrigin; // fragment position
sk_sp<sksg::Matrix<SkMatrix>> fMatrixNode;
sk_sp<sksg::Color> fFillColorNode,
fStrokeColorNode;
};
void TextAdapter::addFragment(const skottie::Shaper::Fragment& frag) {
void TextAdapter::addFragment(const Shaper::Fragment& frag) {
// For a given shaped fragment, build a corresponding SG fragment:
//
// [TransformEffect] -> [Transform]
@ -77,6 +68,52 @@ void TextAdapter::addFragment(const skottie::Shaper::Fragment& frag) {
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;
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});
}
} else {
fMaps.fNonWhitespaceMap.push_back({i, 1});
if (!in_word) {
in_word = true;
word_start = i;
}
}
if (frag.fLineIndex != line) {
SkASSERT(frag.fLineIndex == line + 1);
fMaps.fLinesMap.push_back({line_start, i - line_start});
line = frag.fLineIndex;
line_start = i;
}
}
if (i > word_start) {
fMaps.fWordsMap.push_back({word_start, i - word_start});
}
if (i > line_start) {
fMaps.fLinesMap.push_back({line_start, i - line_start});
}
}
void TextAdapter::apply() {
if (!fText.fHasFill && !fText.fHasStroke) {
return;
@ -102,6 +139,11 @@ void TextAdapter::apply() {
this->addFragment(frag);
}
if (fHasAnimators) {
// Range selectors require fragment domain maps.
this->buildDomainMaps(shape_result);
}
#if (0)
// Enable for text box debugging/visualization.
auto box_color = sksg::Color::Make(0xffff0000);
@ -138,7 +180,7 @@ void TextAdapter::applyAnimators(const std::vector<sk_sp<TextAnimator>>& animato
// Apply all animators to the modulator buffer.
for (const auto& animator : animators) {
animator->modulateProps(buf);
animator->modulateProps(fMaps, buf);
}
// Finally, push all props to their corresponding fragment.

View File

@ -15,10 +15,6 @@
#include <vector>
namespace sksg {
class Group;
} // namespace sksg
namespace skottie {
namespace internal {
@ -34,9 +30,16 @@ public:
void applyAnimators(const std::vector<sk_sp<TextAnimator>>&);
private:
struct FragmentRec;
struct FragmentRec {
SkPoint fOrigin; // fragment position
void addFragment(const skottie::Shaper::Fragment&);
sk_sp<sksg::Matrix<SkMatrix>> fMatrixNode;
sk_sp<sksg::Color> fFillColorNode,
fStrokeColorNode;
};
void addFragment(const Shaper::Fragment&);
void buildDomainMaps(const Shaper::Result&);
void apply();
@ -44,6 +47,7 @@ private:
sk_sp<sksg::Group> fRoot;
std::vector<FragmentRec> fFragments;
TextAnimator::DomainMaps fMaps;
const bool fHasAnimators;
};

View File

@ -80,7 +80,7 @@ sk_sp<TextAnimator> TextAnimator::Make(const skjson::ObjectValue* janimator,
: nullptr;
}
void TextAnimator::modulateProps(ModulatorBuffer& buf) const {
void TextAnimator::modulateProps(const DomainMaps& maps, ModulatorBuffer& buf) const {
// Coverage is scoped per animator.
for (auto& mod : buf) {
mod.coverage = 0;
@ -88,7 +88,7 @@ void TextAnimator::modulateProps(ModulatorBuffer& buf) const {
// Accumulate selector coverage.
for (const auto& selector : fSelectors) {
selector->modulateCoverage(buf);
selector->modulateCoverage(maps, buf);
}
// Modulate animated props.

View File

@ -44,7 +44,23 @@ public:
};
using ModulatorBuffer = std::vector<AnimatedPropsModulator>;
void modulateProps(ModulatorBuffer&) const;
// Domain maps describe how a given index domain (words, lines, etc) relates
// to the full fragment index range.
//
// Each domain[i] represents a [domain[i].fOffset.. domain[i].fOffset+domain[i].fCount-1]
// fragment subset.
struct DomainSpan {
size_t fOffset, fCount;
};
using DomainMap = std::vector<DomainSpan>;
struct DomainMaps {
DomainMap fNonWhitespaceMap,
fWordsMap,
fLinesMap;
};
void modulateProps(const DomainMaps&, ModulatorBuffer&) const;
private:
TextAnimator(std::vector<sk_sp<RangeSelector>>&& selectors,

File diff suppressed because one or more lines are too long