* 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/RangeSelector.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include <algorithm>
#include <cmath>
namespace skottie {
namespace internal {
namespace {
template <typename T, typename TArray>
T ParseEnum(const TArray& arr, const skjson::Value& jenum,
const AnimationBuilder* abuilder, const char* warn_name) {
const auto idx = ParseDefault<int>(jenum, 1);
if (idx > 0 && SkToSizeT(idx) <= SK_ARRAY_COUNT(arr)) {
return arr[idx - 1];
}
if (idx != 0) {
abuilder->log(Logger::Level::kWarning, nullptr,
"Ignoring unknown range selector %s '%d'", warn_name, idx);
}
static_assert(SK_ARRAY_COUNT(arr) > 0, "");
return arr[0];
}
template <RangeSelector::Units>
struct UnitTraits;
template <>
struct UnitTraits<RangeSelector::Units::kPercentage> {
static constexpr auto Defaults() {
return std::make_tuple<float, float, float>(0, 100, 0);
}
static auto Resolve(float s, float e, float o, size_t domain_size) {
return std::make_tuple(domain_size * (s + o) / 100,
domain_size * (e + o) / 100);
}
};
template <>
struct UnitTraits<RangeSelector::Units::kIndex> {
static constexpr auto Defaults() {
return std::make_tuple<float, float, float>(0, std::numeric_limits<float>::max(), 0);
}
static auto Resolve(float s, float e, float o, size_t domain_size) {
return std::make_tuple(s + o, e + o);
}
};
class CoverageProcessor {
public:
CoverageProcessor(const TextAnimator::DomainMaps& maps,
RangeSelector::Domain domain,
RangeSelector::Mode mode,
TextAnimator::ModulatorBuffer& dst)
: fDst(dst)
, fDomainSize(dst.size()) {
SkASSERT(mode == RangeSelector::Mode::kAdd);
fProc = &CoverageProcessor::add_proc;
switch (domain) {
case RangeSelector::Domain::kChars:
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;
}
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:
void add_proc(float amount, size_t offset, size_t count) const {
if (!amount || !count) return;
for (auto* dst = fDst.data() + offset; dst < fDst.data() + offset + count; ++dst) {
dst->coverage = SkTPin<float>(dst->coverage + amount, -1, 1);
}
}
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;
};
struct ShapeGenerator {
float lo,
hi;
float (*func)(float);
float operator()(float t) const { return this->func(t); }
};
static const ShapeGenerator gShapeGenerators[] = {
{ 0, 0, [](float )->float { return 1.0f; }},
{ 0, 1, [](float t)->float { return t; }},
{ 1, 0, [](float t)->float { return 1 - t; }},
{ 0, 0, [](float t)->float { return 1 - std::abs(0.5f - t) / 0.5f; }},
{ 0, 0, [](float t)->float {
static constexpr auto cx = 0.5f,
cx2 = cx * cx;
return std::sqrt(cx2 - (t - cx) * (t - cx));
}},
{ 0, 0, [](float t)->float { return (std::cos(SK_FloatPI * (1 + 2 * t)) + 1) * 0.5f; }},
};
float Lerp(float a, float b, float t) { return a + (b - a) * t; }
}
sk_sp<RangeSelector> RangeSelector::Make(const skjson::ObjectValue* jrange,
const AnimationBuilder* abuilder) {
if (!jrange) {
return nullptr;
}
enum : int32_t {
kRange_SelectorType = 0,
kExpression_SelectorType = 1,
};
{
const auto type = ParseDefault<int>((*jrange)["t"], kRange_SelectorType);
if (type != kRange_SelectorType) {
abuilder->log(Logger::Level::kWarning, nullptr,
"Ignoring unsupported selector type '%d'", type);
return nullptr;
}
}
static constexpr Units gUnitMap[] = {
Units::kPercentage,
Units::kIndex,
};
static constexpr Domain gDomainMap[] = {
Domain::kChars,
Domain::kCharsExcludingSpaces,
Domain::kWords,
Domain::kLines,
};
static constexpr Mode gModeMap[] = {
Mode::kAdd,
};
static constexpr Shape gShapeMap[] = {
Shape::kSquare,
Shape::kRampUp,
Shape::kRampDown,
Shape::kTriangle,
Shape::kRound,
Shape::kSmooth,
};
auto selector = sk_sp<RangeSelector>(
new RangeSelector(ParseEnum<Units> (gUnitMap , (*jrange)["r" ], abuilder, "units" ),
ParseEnum<Domain>(gDomainMap, (*jrange)["b" ], abuilder, "domain"),
ParseEnum<Mode> (gModeMap , (*jrange)["m" ], abuilder, "mode" ),
ParseEnum<Shape> (gShapeMap , (*jrange)["sh"], abuilder, "shape" )));
abuilder->bindProperty<ScalarValue>((*jrange)["s"],
[selector](const ScalarValue& s) {
selector->fStart = s;
});
abuilder->bindProperty<ScalarValue>((*jrange)["e"],
[selector](const ScalarValue& e) {
selector->fEnd = e;
});
abuilder->bindProperty<ScalarValue>((*jrange)["o"],
[selector](const ScalarValue& o) {
selector->fOffset = o;
});
abuilder->bindProperty<ScalarValue>((*jrange)["a"],
[selector](const ScalarValue& a) {
selector->fAmount = a;
});
return selector;
}
RangeSelector::RangeSelector(Units u, Domain d, Mode m, Shape sh)
: fUnits(u)
, fDomain(d)
, fMode(m)
, fShape(sh) {
switch (fUnits) {
case Units::kPercentage:
std::tie(fStart, fEnd, fOffset) = UnitTraits<Units::kPercentage>::Defaults();
break;
case Units::kIndex:
std::tie(fStart, fEnd, fOffset) = UnitTraits<Units::kIndex >::Defaults();
break;
}
}
std::tuple<float, float> RangeSelector::resolve(size_t len) const {
float f_i0, f_i1;
SkASSERT(fUnits == Units::kPercentage || fUnits == Units::kIndex);
const auto resolver = (fUnits == Units::kPercentage)
? UnitTraits<Units::kPercentage>::Resolve
: UnitTraits<Units::kIndex >::Resolve;
std::tie(f_i0, f_i1) = resolver(fStart, fEnd, fOffset, len);
if (f_i0 > f_i1) {
std::swap(f_i0, f_i1);
}
return std::make_tuple(f_i0, f_i1);
}
* General RangeSelector operation:
*
* 1) The range is resolved to a target domain (characters, words, etc) interval, based on
* |start|, |end|, |offset|, |units|.
*
* 2) A shape generator is mapped to this interval and applied across the whole domain, yielding
* coverage values in [0..1].
*
* 2') When the interval extremes don't coincide with fragment boundaries, the corresponding
* fragment coverage is further modulated for partial interval overlap.
*
* 3) The coverage is then scaled by the |amount| parameter.
*
* 4) Finally, the resulting coverage is accumulated to existing fragment coverage based on
* the specified Mode (add, difference, etc).
*/
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;
}
const auto amount = SkTPin<float>(fAmount / 100, -1, 1);
const auto f_range = this->resolve(coverage_proc.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)];
{
const auto count_lo = static_cast<size_t>(std::floor(f0)),
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, 0 , count_lo);
coverage_proc(amount * generator.hi, coverage_proc.size() - count_hi, count_hi);
if (count_lo == coverage_proc.size() || count_hi == coverage_proc.size()) {
return;
}
}
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);
if (SkScalarNearlyZero(range_span)) {
SkASSERT(i0 == i1);
const auto ratio = f0 - i0,
coverage = Lerp(generator.lo, generator.hi, ratio);
coverage_proc(amount * coverage, i0, 1);
return;
}
const auto partial_coverage = [&](float shape_val, float i) {
SkASSERT(SkScalarNearlyEqual(i, std::round(i)));
SkASSERT((i <= f0 && f0 <= i + 1) || (i <= f1 && f1 <= i + 1));
const auto lo_weight = std::max(f0 - i, 0.0f),
mi_weight = std::min(f1 - i, 1.0f) - lo_weight,
hi_weight = std::max(i + 1 - f1, 0.0f);
SkASSERT(0 <= lo_weight && lo_weight <= 1);
SkASSERT(0 <= mi_weight && mi_weight <= 1);
SkASSERT(0 <= hi_weight && hi_weight <= 1);
SkASSERT(SkScalarNearlyEqual(lo_weight + mi_weight + hi_weight, 1));
return lo_weight * generator.lo +
mi_weight * shape_val +
hi_weight * generator.hi;
};
const auto dt = 1 / range_span;
auto t = (i0 + 0.5f - std::get<0>(f_range)) / range_span;
coverage_proc(amount * partial_coverage(generator(std::max(t, 0.0f)), i0), i0, 1);
if (i0 == i1) {
return;
}
t += dt;
for (auto i = i0 + 1; i < i1; ++i) {
SkASSERT(0 <= t && t <= 1);
coverage_proc(amount * generator(t), i, 1);
t += dt;
}
coverage_proc(amount * partial_coverage(generator(std::min(t, 1.0f)), i1), i1, 1);
}
}
}