1/* 2 * Copyright 2019 Google Inc. 3 * 4 * Use of this source code is governed by a BSD-style license that can be 5 * found in the LICENSE file. 6 */ 7 8#include "modules/skottie/src/text/RangeSelector.h" 9 10#include "include/core/SkCubicMap.h" 11#include "include/private/SkTPin.h" 12#include "modules/skottie/src/SkottieJson.h" 13#include "modules/skottie/src/SkottieValue.h" 14#include "modules/skottie/src/animator/Animator.h" 15 16#include <algorithm> 17#include <cmath> 18 19namespace skottie { 20namespace internal { 21 22namespace { 23 24// Maps a 1-based JSON enum to one of the values in the array. 25template <typename T, typename TArray> 26T ParseEnum(const TArray& arr, const skjson::Value& jenum, 27 const AnimationBuilder* abuilder, const char* warn_name) { 28 29 const auto idx = ParseDefault<int>(jenum, 1); 30 31 if (idx > 0 && SkToSizeT(idx) <= SK_ARRAY_COUNT(arr)) { 32 return arr[idx - 1]; 33 } 34 35 // For animators without selectors, BM emits placeholder selector entries with 0 (inval) props. 36 // Supress warnings for these as they are "normal". 37 if (idx != 0) { 38 abuilder->log(Logger::Level::kWarning, nullptr, 39 "Ignoring unknown range selector %s '%d'", warn_name, idx); 40 } 41 42 static_assert(SK_ARRAY_COUNT(arr) > 0, ""); 43 return arr[0]; 44} 45 46template <RangeSelector::Units> 47struct UnitTraits; 48 49template <> 50struct UnitTraits<RangeSelector::Units::kPercentage> { 51 static constexpr auto Defaults() { 52 return std::make_tuple<float, float, float>(0, 100, 0); 53 } 54 55 static auto Resolve(float s, float e, float o, size_t domain_size) { 56 return std::make_tuple(domain_size * (s + o) / 100, 57 domain_size * (e + o) / 100); 58 } 59}; 60 61template <> 62struct UnitTraits<RangeSelector::Units::kIndex> { 63 static constexpr auto Defaults() { 64 // It's OK to default fEnd to FLOAT_MAX, as it gets clamped when resolved. 65 return std::make_tuple<float, float, float>(0, std::numeric_limits<float>::max(), 0); 66 } 67 68 static auto Resolve(float s, float e, float o, size_t domain_size) { 69 return std::make_tuple(s + o, e + o); 70 } 71}; 72 73class CoverageProcessor { 74public: 75 CoverageProcessor(const TextAnimator::DomainMaps& maps, 76 RangeSelector::Domain domain, 77 RangeSelector::Mode mode, 78 TextAnimator::ModulatorBuffer& dst) 79 : fDst(dst) 80 , fDomainSize(dst.size()) { 81 82 SkASSERT(mode == RangeSelector::Mode::kAdd); 83 fProc = &CoverageProcessor::add_proc; 84 85 switch (domain) { 86 case RangeSelector::Domain::kChars: 87 // Direct (1-to-1) index mapping. 88 break; 89 case RangeSelector::Domain::kCharsExcludingSpaces: 90 fMap = &maps.fNonWhitespaceMap; 91 break; 92 case RangeSelector::Domain::kWords: 93 fMap = &maps.fWordsMap; 94 break; 95 case RangeSelector::Domain::kLines: 96 fMap = &maps.fLinesMap; 97 break; 98 } 99 100 // When no domain map is active, fProc points directly to the mode proc. 101 // Otherwise, we punt through a domain mapper proxy. 102 if (fMap) { 103 fMappedProc = fProc; 104 fProc = &CoverageProcessor::domain_map_proc; 105 fDomainSize = fMap->size(); 106 } 107 } 108 109 size_t size() const { return fDomainSize; } 110 111 void operator()(float amount, size_t offset, size_t count) const { 112 (this->*fProc)(amount, offset, count); 113 } 114 115private: 116 // mode: kAdd 117 void add_proc(float amount, size_t offset, size_t count) const { 118 if (!amount || !count) return; 119 120 for (auto* dst = fDst.data() + offset; dst < fDst.data() + offset + count; ++dst) { 121 dst->coverage = SkTPin<float>(dst->coverage + amount, -1, 1); 122 } 123 } 124 125 // A proxy for mapping domain indices to the target buffer. 126 void domain_map_proc(float amount, size_t offset, size_t count) const { 127 SkASSERT(fMap); 128 SkASSERT(fMappedProc); 129 130 for (auto i = offset; i < offset + count; ++i) { 131 const auto& span = (*fMap)[i]; 132 (this->*fMappedProc)(amount, span.fOffset, span.fCount); 133 } 134 } 135 136 using ProcT = void(CoverageProcessor::*)(float amount, size_t offset, size_t count) const; 137 138 TextAnimator::ModulatorBuffer& fDst; 139 ProcT fProc, 140 fMappedProc = nullptr; 141 const TextAnimator::DomainMap* fMap = nullptr; 142 size_t fDomainSize; 143}; 144 145 146/* 147 Selector shapes can be generalized as a signal generator with the following 148 parameters/properties: 149 150 151 1 + ------------------------- 152 | /. . .\ 153 | / . . . \ 154 | / . . . \ 155 | / . . . \ 156 | / . . . \ 157 | / . . . \ 158 | / . . . \ 159 | / . . . \ 160 0 +---------------------------------------------------------- 161 ^ <-----> ^ <-----> ^ 162 e0 crs sp crs e1 163 164 165 * e0, e1: left/right edges 166 * sp : symmetry/reflection point (sp == (e0+e1)/2) 167 * crs : cubic ramp size (transitional portion mapped using a Bezier easing function) 168 169 Based on these, 170 171 | 0 , t <= e0 172 | 173 | Bez((t-e0)/crs) , e0 < t < e0+crs 174 F(t) = | 175 | 1 , e0 + crs <= t <= sp 176 | 177 | F(reflect(t,sp)) , t > sp 178 179 180 Tweaking this function's parameters, we can achieve all range selectors shapes: 181 182 - square -> e0: 0, e1: 1, crs: 0 183 - ramp up -> e0: 0, e1: +inf, crs: 1 184 - ramp down -> e0: -inf, e1: 1, crs: 1 185 - triangle -> e0: 0, e1: 1, crs: 0.5 186 - round -> e0: 0, e1: 1, crs: 0.5 (nonlinear cubic mapper) 187 - smooth -> e0: 0, e1: 1, crs: 0.5 (nonlinear cubic mapper) 188 189*/ 190 191struct ShapeInfo { 192 SkVector ctrl0, 193 ctrl1; 194 float e0, e1, crs; 195}; 196 197SkVector EaseVec(float ease) { 198 return (ease < 0) ? SkVector{0, -ease} : SkVector{ease, 0}; 199} 200 201struct ShapeGenerator { 202 SkCubicMap shape_mapper, 203 ease_mapper; 204 float e0, e1, crs; 205 206 ShapeGenerator(const ShapeInfo& sinfo, float ease_lo, float ease_hi) 207 : shape_mapper(sinfo.ctrl0, sinfo.ctrl1) 208 , ease_mapper(EaseVec(ease_lo), SkVector{1,1} - EaseVec(ease_hi)) 209 , e0(sinfo.e0) 210 , e1(sinfo.e1) 211 , crs(sinfo.crs) {} 212 213 float operator()(float t) const { 214 // SkCubicMap clamps its input, so we can let it all hang out. 215 t = std::min(t - e0, e1 - t); 216 t = sk_ieee_float_divide(t, crs); 217 218 return ease_mapper.computeYFromX(shape_mapper.computeYFromX(t)); 219 } 220}; 221 222static constexpr ShapeInfo gShapeInfo[] = { 223 { {0 ,0 }, {1 ,1}, 0 , 1 , 0.0f }, // Shape::kSquare 224 { {0 ,0 }, {1 ,1}, 0 , SK_FloatInfinity, 1.0f }, // Shape::kRampUp 225 { {0 ,0 }, {1 ,1}, SK_FloatNegativeInfinity, 1 , 1.0f }, // Shape::kRampDown 226 { {0 ,0 }, {1 ,1}, 0 , 1 , 0.5f }, // Shape::kTriangle 227 { {0 ,.5f}, {.5f,1}, 0 , 1 , 0.5f }, // Shape::kRound 228 { {.5f,0 }, {.5f,1}, 0 , 1 , 0.5f }, // Shape::kSmooth 229}; 230 231} // namespace 232 233sk_sp<RangeSelector> RangeSelector::Make(const skjson::ObjectValue* jrange, 234 const AnimationBuilder* abuilder, 235 AnimatablePropertyContainer* acontainer) { 236 if (!jrange) { 237 return nullptr; 238 } 239 240 enum : int32_t { 241 kRange_SelectorType = 0, 242 kExpression_SelectorType = 1, 243 244 // kWiggly_SelectorType = ? (not exported) 245 }; 246 247 { 248 const auto type = ParseDefault<int>((*jrange)["t"], kRange_SelectorType); 249 if (type != kRange_SelectorType) { 250 abuilder->log(Logger::Level::kWarning, nullptr, 251 "Ignoring unsupported selector type '%d'", type); 252 return nullptr; 253 } 254 } 255 256 static constexpr Units gUnitMap[] = { 257 Units::kPercentage, // 'r': 1 258 Units::kIndex, // 'r': 2 259 }; 260 261 static constexpr Domain gDomainMap[] = { 262 Domain::kChars, // 'b': 1 263 Domain::kCharsExcludingSpaces, // 'b': 2 264 Domain::kWords, // 'b': 3 265 Domain::kLines, // 'b': 4 266 }; 267 268 static constexpr Mode gModeMap[] = { 269 Mode::kAdd, // 'm': 1 270 }; 271 272 static constexpr Shape gShapeMap[] = { 273 Shape::kSquare, // 'sh': 1 274 Shape::kRampUp, // 'sh': 2 275 Shape::kRampDown, // 'sh': 3 276 Shape::kTriangle, // 'sh': 4 277 Shape::kRound, // 'sh': 5 278 Shape::kSmooth, // 'sh': 6 279 }; 280 281 auto selector = sk_sp<RangeSelector>( 282 new RangeSelector(ParseEnum<Units> (gUnitMap , (*jrange)["r" ], abuilder, "units" ), 283 ParseEnum<Domain>(gDomainMap, (*jrange)["b" ], abuilder, "domain"), 284 ParseEnum<Mode> (gModeMap , (*jrange)["m" ], abuilder, "mode" ), 285 ParseEnum<Shape> (gShapeMap , (*jrange)["sh"], abuilder, "shape" ))); 286 287 acontainer->bind(*abuilder, (*jrange)["s" ], &selector->fStart ); 288 acontainer->bind(*abuilder, (*jrange)["e" ], &selector->fEnd ); 289 acontainer->bind(*abuilder, (*jrange)["o" ], &selector->fOffset); 290 acontainer->bind(*abuilder, (*jrange)["a" ], &selector->fAmount); 291 acontainer->bind(*abuilder, (*jrange)["ne"], &selector->fEaseLo); 292 acontainer->bind(*abuilder, (*jrange)["xe"], &selector->fEaseHi); 293 294 // Optional square "smoothness" prop. 295 if (selector->fShape == Shape::kSquare) { 296 acontainer->bind(*abuilder, (*jrange)["sm" ], &selector->fSmoothness); 297 } 298 299 return selector; 300} 301 302RangeSelector::RangeSelector(Units u, Domain d, Mode m, Shape sh) 303 : fUnits(u) 304 , fDomain(d) 305 , fMode(m) 306 , fShape(sh) { 307 308 // Range defaults are unit-specific. 309 switch (fUnits) { 310 case Units::kPercentage: 311 std::tie(fStart, fEnd, fOffset) = UnitTraits<Units::kPercentage>::Defaults(); 312 break; 313 case Units::kIndex: 314 std::tie(fStart, fEnd, fOffset) = UnitTraits<Units::kIndex >::Defaults(); 315 break; 316 } 317} 318 319std::tuple<float, float> RangeSelector::resolve(size_t len) const { 320 float f_i0, f_i1; 321 322 SkASSERT(fUnits == Units::kPercentage || fUnits == Units::kIndex); 323 const auto resolver = (fUnits == Units::kPercentage) 324 ? UnitTraits<Units::kPercentage>::Resolve 325 : UnitTraits<Units::kIndex >::Resolve; 326 327 std::tie(f_i0, f_i1) = resolver(fStart, fEnd, fOffset, len); 328 if (f_i0 > f_i1) { 329 std::swap(f_i0, f_i1); 330 } 331 332 return std::make_tuple(f_i0, f_i1); 333} 334 335/* 336 * General RangeSelector operation: 337 * 338 * 1) The range is resolved to a target domain (characters, words, etc) interval, based on 339 * |start|, |end|, |offset|, |units|. 340 * 341 * 2) A shape generator is mapped to this interval and applied across the whole domain, yielding 342 * coverage values in [0..1]. 343 * 344 * 3) The coverage is then scaled by the |amount| parameter. 345 * 346 * 4) Finally, the resulting coverage is accumulated to existing fragment coverage based on 347 * the specified Mode (add, difference, etc). 348 */ 349void RangeSelector::modulateCoverage(const TextAnimator::DomainMaps& maps, 350 TextAnimator::ModulatorBuffer& mbuf) const { 351 const CoverageProcessor coverage_proc(maps, fDomain, fMode, mbuf); 352 if (coverage_proc.size() == 0) { 353 return; 354 } 355 356 // Amount, ease-low and ease-high are percentage-based [-100% .. 100%]. 357 const auto amount = SkTPin<float>(fAmount / 100, -1, 1), 358 ease_lo = SkTPin<float>(fEaseLo / 100, -1, 1), 359 ease_hi = SkTPin<float>(fEaseHi / 100, -1, 1); 360 361 // Resolve to a float range in the given domain. 362 const auto range = this->resolve(coverage_proc.size()); 363 auto r0 = std::get<0>(range), 364 len = std::max(std::get<1>(range) - r0, std::numeric_limits<float>::epsilon()); 365 366 SkASSERT(static_cast<size_t>(fShape) < SK_ARRAY_COUNT(gShapeInfo)); 367 ShapeGenerator gen(gShapeInfo[static_cast<size_t>(fShape)], ease_lo, ease_hi); 368 369 if (fShape == Shape::kSquare) { 370 // Canonical square generators have collapsed ramps, but AE square selectors have 371 // an additional "smoothness" property (0..1) which introduces a non-zero transition. 372 // We achieve this by moving the range edges outward by |smoothness|/2, and adjusting 373 // the generator cubic ramp size. 374 375 // smoothness is percentage-based [0..100] 376 const auto smoothness = SkTPin<float>(fSmoothness / 100, 0, 1); 377 378 r0 -= smoothness / 2; 379 len += smoothness; 380 381 gen.crs += smoothness / len; 382 } 383 384 SkASSERT(len > 0); 385 const auto dt = 1 / len; 386 auto t = (0.5f - r0) / len; // sampling bias: mid-unit 387 388 for (size_t i = 0; i < coverage_proc.size(); ++i, t += dt) { 389 coverage_proc(amount * gen(t), i, 1); 390 } 391} 392 393} // namespace internal 394} // namespace skottie 395