[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:
parent
22ea7e994b
commit
e695e53f1c
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
1
resources/skottie/skottie-text-animator-5.json
Normal file
1
resources/skottie/skottie-text-animator-5.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user