blob: a9a705b6f4a2c55d3cae137d81686b995a10d125 [file] [log] [blame]
/*
* 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 "include/core/SkCubicMap.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include <algorithm>
#include <cmath>
namespace skottie {
namespace internal {
namespace {
// Maps a 1-based JSON enum to one of the values in the array.
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];
}
// For animators without selectors, BM emits dummy selector entries with 0 (inval) props.
// Supress warnings for these as they are "normal".
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() {
// It's OK to default fEnd to FLOAT_MAX, as it gets clamped when resolved.
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:
// 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 (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;
};
/*
Selector shapes can be generalized as a signal generator with the following
parameters/properties:
1 + -------------------------
| /. . .\
| / . . . \
| / . . . \
| / . . . \
| / . . . \
| / . . . \
| / . . . \
| / . . . \
0 +----------------------------------------------------------
^ <-----> ^ <-----> ^
e0 crs sp crs e1
* e0, e1: left/right edges
* sp : symmetry/reflection point (sp == (e0+e1)/2)
* crs : cubic ramp size (transitional portion mapped using a Bezier easing function)
Based on these,
| 0 , t <= e0
|
| Bez((t-e0)/crs) , e0 < t < e0+crs
F(t) = |
| 1 , e0 + crs <= t <= sp
|
| F(reflect(t,sp)) , t > sp
Tweaking this function's parameters, we can achieve all range selectors shapes:
- square -> e0: 0, e1: 1, crs: 0
- ramp up -> e0: 0, e1: +inf, crs: 1
- ramp down -> e0: -inf, e1: 1, crs: 1
- triangle -> e0: 0, e1: 1, crs: 0.5
- round -> e0: 0, e1: 1, crs: 0.5 (nonlinear cubic mapper)
- smooth -> e0: 0, e1: 1, crs: 0.5 (nonlinear cubic mapper)
*/
struct ShapeInfo {
SkVector ctrl0,
ctrl1;
float e0, e1, crs;
};
SkVector EaseVec(float ease) {
return (ease < 0) ? SkVector{0, -ease} : SkVector{ease, 0};
}
struct ShapeGenerator {
SkCubicMap shape_mapper,
ease_mapper;
float e0, e1, crs;
ShapeGenerator(const ShapeInfo& sinfo, float ease_lo, float ease_hi)
: shape_mapper(sinfo.ctrl0, sinfo.ctrl1)
, ease_mapper(EaseVec(ease_lo), SkVector{1,1} - EaseVec(ease_hi))
, e0(sinfo.e0)
, e1(sinfo.e1)
, crs(sinfo.crs) {}
float operator()(float t) const {
// SkCubicMap clamps its input, so we can let it all hang out.
t = std::min(t - e0, e1 - t);
t = sk_ieee_float_divide(t, crs);
return ease_mapper.computeYFromX(shape_mapper.computeYFromX(t));
}
};
static constexpr ShapeInfo gShapeInfo[] = {
{ {0 ,0 }, {1 ,1}, 0 , 1 , 0.0f }, // Shape::kSquare
{ {0 ,0 }, {1 ,1}, 0 , SK_FloatInfinity, 1.0f }, // Shape::kRampUp
{ {0 ,0 }, {1 ,1}, SK_FloatNegativeInfinity, 1 , 1.0f }, // Shape::kRampDown
{ {0 ,0 }, {1 ,1}, 0 , 1 , 0.5f }, // Shape::kTriangle
{ {0 ,.5f}, {.5f,1}, 0 , 1 , 0.5f }, // Shape::kRound
{ {.5f,0 }, {.5f,1}, 0 , 1 , 0.5f }, // Shape::kSmooth
};
} // namespace
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,
// kWiggly_SelectorType = ? (not exported)
};
{
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, // 'r': 1
Units::kIndex, // 'r': 2
};
static constexpr Domain gDomainMap[] = {
Domain::kChars, // 'b': 1
Domain::kCharsExcludingSpaces, // 'b': 2
Domain::kWords, // 'b': 3
Domain::kLines, // 'b': 4
};
static constexpr Mode gModeMap[] = {
Mode::kAdd, // 'm': 1
};
static constexpr Shape gShapeMap[] = {
Shape::kSquare, // 'sh': 1
Shape::kRampUp, // 'sh': 2
Shape::kRampDown, // 'sh': 3
Shape::kTriangle, // 'sh': 4
Shape::kRound, // 'sh': 5
Shape::kSmooth, // 'sh': 6
};
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" )));
auto* raw_selector = selector.get();
abuilder->bindProperty<ScalarValue>((*jrange)["s"],
[raw_selector](const ScalarValue& s) {
raw_selector->fStart = s;
});
abuilder->bindProperty<ScalarValue>((*jrange)["e"],
[raw_selector](const ScalarValue& e) {
raw_selector->fEnd = e;
});
abuilder->bindProperty<ScalarValue>((*jrange)["o"],
[raw_selector](const ScalarValue& o) {
raw_selector->fOffset = o;
});
abuilder->bindProperty<ScalarValue>((*jrange)["a"],
[raw_selector](const ScalarValue& a) {
raw_selector->fAmount = a;
});
abuilder->bindProperty<ScalarValue>((*jrange)["ne"],
[raw_selector](const ScalarValue& ne) {
raw_selector->fEaseLo = ne;
});
abuilder->bindProperty<ScalarValue>((*jrange)["xe"],
[raw_selector](const ScalarValue& xe) {
raw_selector->fEaseHi = xe;
});
// Optional square "smoothness" prop.
if (selector->fShape == Shape::kSquare) {
abuilder->bindProperty<ScalarValue>((*jrange)["sm"],
[selector](const ScalarValue& sm) {
selector->fSmoothness = sm;
});
}
return selector;
}
RangeSelector::RangeSelector(Units u, Domain d, Mode m, Shape sh)
: fUnits(u)
, fDomain(d)
, fMode(m)
, fShape(sh) {
// Range defaults are unit-specific.
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].
*
* 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;
}
// Amount, ease-low and ease-high are percentage-based [-100% .. 100%].
const auto amount = SkTPin<float>(fAmount / 100, -1, 1),
ease_lo = SkTPin<float>(fEaseLo / 100, -1, 1),
ease_hi = SkTPin<float>(fEaseHi / 100, -1, 1);
// Resolve to a float range in the given domain.
const auto range = this->resolve(coverage_proc.size());
auto r0 = std::get<0>(range),
len = std::max(std::get<1>(range) - r0, std::numeric_limits<float>::epsilon());
SkASSERT(static_cast<size_t>(fShape) < SK_ARRAY_COUNT(gShapeInfo));
ShapeGenerator gen(gShapeInfo[static_cast<size_t>(fShape)], ease_lo, ease_hi);
if (fShape == Shape::kSquare) {
// Canonical square generators have collapsed ramps, but AE square selectors have
// an additional "smoothness" property (0..1) which introduces a non-zero transition.
// We achieve this by moving the range edges outward by |smoothness|/2, and adjusting
// the generator cubic ramp size.
// smoothness is percentage-based [0..100]
const auto smoothness = SkTPin<float>(fSmoothness / 100, 0, 1);
r0 -= smoothness / 2;
len += smoothness;
gen.crs += smoothness / len;
}
SkASSERT(len > 0);
const auto dt = 1 / len;
auto t = (0.5f - r0) / len; // sampling bias: mid-unit
for (size_t i = 0; i < coverage_proc.size(); ++i, t += dt) {
coverage_proc(amount * gen(t), i, 1);
}
}
} // namespace internal
} // namespace skottie