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/base/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
19 namespace skottie {
20 namespace internal {
21
22 namespace {
23
24 // Maps a 1-based JSON enum to one of the values in the array.
25 template <typename T, typename TArray>
ParseEnum(const TArray & arr,const skjson::Value & jenum,const AnimationBuilder * abuilder,const char * warn_name)26 T 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) <= std::size(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 SkASSERT(std::size(arr) > 0);
43 return arr[0];
44 }
45
46 template <RangeSelector::Units>
47 struct UnitTraits;
48
49 template <>
50 struct UnitTraits<RangeSelector::Units::kPercentage> {
Defaultsskottie::internal::__anon97c8ca510111::UnitTraits51 static constexpr auto Defaults() {
52 return std::make_tuple<float, float, float>(0, 100, 0);
53 }
54
Resolveskottie::internal::__anon97c8ca510111::UnitTraits55 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
61 template <>
62 struct UnitTraits<RangeSelector::Units::kIndex> {
Defaultsskottie::internal::__anon97c8ca510111::UnitTraits63 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
Resolveskottie::internal::__anon97c8ca510111::UnitTraits68 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
73 class CoverageProcessor {
74 public:
CoverageProcessor(const TextAnimator::DomainMaps & maps,RangeSelector::Domain domain,RangeSelector::Mode mode,TextAnimator::ModulatorBuffer & dst)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
size() const109 size_t size() const { return fDomainSize; }
110
operator ()(float amount,size_t offset,size_t count) const111 void operator()(float amount, size_t offset, size_t count) const {
112 (this->*fProc)(amount, offset, count);
113 }
114
115 private:
116 // mode: kAdd
add_proc(float amount,size_t offset,size_t count) const117 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.
domain_map_proc(float amount,size_t offset,size_t count) const126 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
191 struct ShapeInfo {
192 SkVector ctrl0,
193 ctrl1;
194 float e0, e1, crs;
195 };
196
EaseVec(float ease)197 SkVector EaseVec(float ease) {
198 return (ease < 0) ? SkVector{0, -ease} : SkVector{ease, 0};
199 }
200
201 struct ShapeGenerator {
202 SkCubicMap shape_mapper,
203 ease_mapper;
204 float e0, e1, crs;
205
ShapeGeneratorskottie::internal::__anon97c8ca510111::ShapeGenerator206 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
operator ()skottie::internal::__anon97c8ca510111::ShapeGenerator213 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
222 static 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
Make(const skjson::ObjectValue * jrange,const AnimationBuilder * abuilder,AnimatablePropertyContainer * acontainer)233 sk_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
RangeSelector(Units u,Domain d,Mode m,Shape sh)302 RangeSelector::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
resolve(size_t len) const319 std::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 */
modulateCoverage(const TextAnimator::DomainMaps & maps,TextAnimator::ModulatorBuffer & mbuf) const349 void 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) < std::size(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