/* * 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 "include/private/SkTPin.h" #include "modules/skottie/src/SkottieJson.h" #include "modules/skottie/src/SkottieValue.h" #include "modules/skottie/src/animator/Animator.h" #include #include namespace skottie { namespace internal { namespace { // Maps a 1-based JSON enum to one of the values in the array. template T ParseEnum(const TArray& arr, const skjson::Value& jenum, const AnimationBuilder* abuilder, const char* warn_name) { const auto idx = ParseDefault(jenum, 1); if (idx > 0 && SkToSizeT(idx) <= SK_ARRAY_COUNT(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); } static_assert(SK_ARRAY_COUNT(arr) > 0, ""); return arr[0]; } template struct UnitTraits; template <> struct UnitTraits { static constexpr auto Defaults() { return std::make_tuple(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 { static constexpr auto Defaults() { // It's OK to default fEnd to FLOAT_MAX, as it gets clamped when resolved. return std::make_tuple(0, std::numeric_limits::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(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::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((*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( new RangeSelector(ParseEnum (gUnitMap , (*jrange)["r" ], abuilder, "units" ), ParseEnum(gDomainMap, (*jrange)["b" ], abuilder, "domain"), ParseEnum (gModeMap , (*jrange)["m" ], abuilder, "mode" ), ParseEnum (gShapeMap , (*jrange)["sh"], abuilder, "shape" ))); 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) : fUnits(u) , fDomain(d) , fMode(m) , fShape(sh) { // Range defaults are unit-specific. switch (fUnits) { case Units::kPercentage: std::tie(fStart, fEnd, fOffset) = UnitTraits::Defaults(); break; case Units::kIndex: std::tie(fStart, fEnd, fOffset) = UnitTraits::Defaults(); break; } } std::tuple 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::Resolve : UnitTraits::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(fAmount / 100, -1, 1), ease_lo = SkTPin(fEaseLo / 100, -1, 1), ease_hi = SkTPin(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::epsilon()); SkASSERT(static_cast(fShape) < SK_ARRAY_COUNT(gShapeInfo)); ShapeGenerator gen(gShapeInfo[static_cast(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(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