blob: 818617c2750bccec051f9bae754c3ccd80b550e7 [file]
/*
* Copyright 2019 Google LLC
*
* 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 "include/core/SkPoint.h"
#include "include/private/SkAssert.h"
#include "include/private/SkFloatingPoint.h"
#include "include/private/SkTPin.h"
#include "include/private/SkTo.h"
#include "modules/jsonreader/SkJSONReader.h"
#include "modules/skottie/include/Skottie.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/skottie/src/animator/Animator.h"
#include "modules/skottie/src/text/TextAnimator.h"
#include <algorithm>
#include <limits>
#include <random>
#include <vector>
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) <= std::size(arr)) {
return arr[idx - 1];
}
// For animators without selectors, BM emits placeholder 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);
}
SkASSERT(std::size(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::DomainMap* map,
RangeSelector::Domain domain,
RangeSelector::Mode mode,
TextAnimator::ModulatorBuffer& dst)
: fDst(dst)
, fMap(map)
, fDomainSize(dst.size()) {
SkASSERT(mode == RangeSelector::Mode::kAdd);
fProc = &CoverageProcessor::add_proc;
// 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,
AnimatablePropertyContainer* acontainer) {
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
};
// 'rn': 0 | 1
const bool randomize_order = SkToBool(ParseDefault((*jrange)["rn"], 0));
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" ),
randomize_order));
acontainer->bind(*abuilder, (*jrange)["s" ], &selector->fStart );
acontainer->bind(*abuilder, (*jrange)["e" ], &selector->fEnd );
acontainer->bind(*abuilder, (*jrange)["o" ], &selector->fOffset);
acontainer->bind(*abuilder, (*jrange)["a" ], &selector->fAmount);
acontainer->bind(*abuilder, (*jrange)["ne"], &selector->fEaseLo);
acontainer->bind(*abuilder, (*jrange)["xe"], &selector->fEaseHi);
// Optional square "smoothness" prop.
if (selector->fShape == Shape::kSquare) {
acontainer->bind(*abuilder, (*jrange)["sm" ], &selector->fSmoothness);
}
return selector;
}
RangeSelector::RangeSelector(Units u, Domain d, Mode m, Shape sh, bool ro)
: fUnits(u)
, fDomain(d)
, fMode(m)
, fShape(sh)
, fRandomizeOrder(ro) {
// 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);
}
void RangeSelector::updateDomainMap(const TextAnimator::DomainMaps& maps, size_t fragment_count) {
switch (fDomain) {
case Domain::kChars:
fMap = nullptr;
break;
case Domain::kCharsExcludingSpaces:
fMap = &maps.fNonWhitespaceMap;
break;
case Domain::kWords:
fMap = &maps.fWordsMap;
break;
case Domain::kLines:
fMap = &maps.fLinesMap;
break;
}
if (fRandomizeOrder) {
if (fMap) {
fLocalMap = *fMap;
} else {
SkASSERT(fDomain == Domain::kChars);
// We don't normally generate a map for Domain::kChars, since it's trivial (1:1).
// So we generate one on the fly here, for randomization.
if (!fLocalMap) {
fLocalMap.emplace();
} else {
fLocalMap->clear();
}
fLocalMap->reserve(fragment_count);
for (size_t i = 0; i < fragment_count; ++i) {
fLocalMap->push_back({
.fOffset = i,
.fCount = 1,
.fAdvance = 0, // not used for range selectors
.fAscent = 0, // not used for range selectors
});
}
}
static constexpr int kSeed = 42;
std::mt19937 rng_engine(kSeed);
std::shuffle(fLocalMap->begin(), fLocalMap->end(), rng_engine);
}
}
/*
* 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(TextAnimator::ModulatorBuffer& mbuf) const {
const TextAnimator::DomainMap* map = fLocalMap ? &fLocalMap.value() : fMap;
const CoverageProcessor coverage_proc(map, 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) < std::size(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