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