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 "modules/skottie/src/SkottieJson.h"
11 #include "modules/skottie/src/SkottieValue.h"
12
13 #include <algorithm>
14 #include <cmath>
15
16 namespace skottie {
17 namespace internal {
18
19 namespace {
20
21 // Maps a 1-based JSON enum to one of the values in the array.
22 template <typename T, typename TArray>
ParseEnum(const TArray & arr,const skjson::Value & jenum,const AnimationBuilder * abuilder,const char * warn_name)23 T ParseEnum(const TArray& arr, const skjson::Value& jenum,
24 const AnimationBuilder* abuilder, const char* warn_name) {
25
26 const auto idx = ParseDefault<int>(jenum, 1);
27
28 if (idx > 0 && SkToSizeT(idx) <= SK_ARRAY_COUNT(arr)) {
29 return arr[idx - 1];
30 }
31
32 // For animators without selectors, BM emits dummy selector entries with 0 (inval) props.
33 // Supress warnings for these as they are "normal".
34 if (idx != 0) {
35 abuilder->log(Logger::Level::kWarning, nullptr,
36 "Ignoring unknown range selector %s '%d'", warn_name, idx);
37 }
38
39 static_assert(SK_ARRAY_COUNT(arr) > 0, "");
40 return arr[0];
41 }
42
43 template <RangeSelector::Units>
44 struct UnitTraits;
45
46 template <>
47 struct UnitTraits<RangeSelector::Units::kPercentage> {
Defaultsskottie::internal::__anon4a3cc3110111::UnitTraits48 static constexpr auto Defaults() {
49 return std::make_tuple<float, float, float>(0, 100, 0);
50 }
51
Resolveskottie::internal::__anon4a3cc3110111::UnitTraits52 static auto Resolve(float s, float e, float o, size_t domain_size) {
53 return std::make_tuple(domain_size * (s + o) / 100,
54 domain_size * (e + o) / 100);
55 }
56 };
57
58 template <>
59 struct UnitTraits<RangeSelector::Units::kIndex> {
Defaultsskottie::internal::__anon4a3cc3110111::UnitTraits60 static constexpr auto Defaults() {
61 // It's OK to default fEnd to FLOAT_MAX, as it gets clamped when resolved.
62 return std::make_tuple<float, float, float>(0, std::numeric_limits<float>::max(), 0);
63 }
64
Resolveskottie::internal::__anon4a3cc3110111::UnitTraits65 static auto Resolve(float s, float e, float o, size_t domain_size) {
66 return std::make_tuple(s + o, e + o);
67 }
68 };
69
70 class CoverageProcessor {
71 public:
CoverageProcessor(const TextAnimator::DomainMaps & maps,RangeSelector::Domain domain,RangeSelector::Mode mode,TextAnimator::ModulatorBuffer & dst)72 CoverageProcessor(const TextAnimator::DomainMaps& maps,
73 RangeSelector::Domain domain,
74 RangeSelector::Mode mode,
75 TextAnimator::ModulatorBuffer& dst)
76 : fDst(dst)
77 , fDomainSize(dst.size()) {
78
79 SkASSERT(mode == RangeSelector::Mode::kAdd);
80 fProc = &CoverageProcessor::add_proc;
81
82 switch (domain) {
83 case RangeSelector::Domain::kChars:
84 // Direct (1-to-1) index mapping.
85 break;
86 case RangeSelector::Domain::kCharsExcludingSpaces:
87 fMap = &maps.fNonWhitespaceMap;
88 break;
89 case RangeSelector::Domain::kWords:
90 fMap = &maps.fWordsMap;
91 break;
92 case RangeSelector::Domain::kLines:
93 fMap = &maps.fLinesMap;
94 break;
95 }
96
97 // When no domain map is active, fProc points directly to the mode proc.
98 // Otherwise, we punt through a domain mapper proxy.
99 if (fMap) {
100 fMappedProc = fProc;
101 fProc = &CoverageProcessor::domain_map_proc;
102 fDomainSize = fMap->size();
103 }
104 }
105
size() const106 size_t size() const { return fDomainSize; }
107
operator ()(float amount,size_t offset,size_t count) const108 void operator()(float amount, size_t offset, size_t count) const {
109 (this->*fProc)(amount, offset, count);
110 }
111
112 private:
113 // mode: kAdd
add_proc(float amount,size_t offset,size_t count) const114 void add_proc(float amount, size_t offset, size_t count) const {
115 if (!amount || !count) return;
116
117 for (auto* dst = fDst.data() + offset; dst < fDst.data() + offset + count; ++dst) {
118 dst->coverage = SkTPin<float>(dst->coverage + amount, -1, 1);
119 }
120 }
121
122 // A proxy for mapping domain indices to the target buffer.
domain_map_proc(float amount,size_t offset,size_t count) const123 void domain_map_proc(float amount, size_t offset, size_t count) const {
124 SkASSERT(fMap);
125 SkASSERT(fMappedProc);
126
127 for (auto i = offset; i < offset + count; ++i) {
128 const auto& span = (*fMap)[i];
129 (this->*fMappedProc)(amount, span.fOffset, span.fCount);
130 }
131 }
132
133 using ProcT = void(CoverageProcessor::*)(float amount, size_t offset, size_t count) const;
134
135 TextAnimator::ModulatorBuffer& fDst;
136 ProcT fProc,
137 fMappedProc = nullptr;
138 const TextAnimator::DomainMap* fMap = nullptr;
139 size_t fDomainSize;
140 };
141
142 // Each shape generator is defined in a normalized domain, over three |t| intervals:
143 //
144 // (-inf..0) -> lo (constant value)
145 // [0..1] -> func(t)
146 // (1..+inf) -> hi (constant value)
147 //
148 struct ShapeGenerator {
149 float lo, // constant value for t < 0
150 hi; // constant value for t > 1
151 float (*func)(float); // shape generator for t in [0..1]
152
operator ()skottie::internal::__anon4a3cc3110111::ShapeGenerator153 float operator()(float t) const { return this->func(t); }
154 };
155
156 static const ShapeGenerator gShapeGenerators[] = {
157 // Shape::kSquare
__anon4a3cc3110202()158 { 0, 0, [](float )->float { return 1.0f; }},
159 // Shape::kRampUp
__anon4a3cc3110302()160 { 0, 1, [](float t)->float { return t; }},
161 // Shape::kRampDown
__anon4a3cc3110402()162 { 1, 0, [](float t)->float { return 1 - t; }},
163 // Shape::kTriangle
__anon4a3cc3110502()164 { 0, 0, [](float t)->float { return 1 - std::abs(0.5f - t) / 0.5f; }},
165 // Shape::kRound
__anon4a3cc3110602()166 { 0, 0, [](float t)->float {
167 static constexpr auto cx = 0.5f,
168 cx2 = cx * cx;
169 return std::sqrt(cx2 - (t - cx) * (t - cx));
170 }},
171 // Shape::kSmooth
__anon4a3cc3110702()172 { 0, 0, [](float t)->float { return (std::cos(SK_FloatPI * (1 + 2 * t)) + 1) * 0.5f; }},
173 };
174
Lerp(float a,float b,float t)175 float Lerp(float a, float b, float t) { return a + (b - a) * t; }
176
177 } // namespace
178
Make(const skjson::ObjectValue * jrange,const AnimationBuilder * abuilder)179 sk_sp<RangeSelector> RangeSelector::Make(const skjson::ObjectValue* jrange,
180 const AnimationBuilder* abuilder) {
181 if (!jrange) {
182 return nullptr;
183 }
184
185 enum : int32_t {
186 kRange_SelectorType = 0,
187 kExpression_SelectorType = 1,
188
189 // kWiggly_SelectorType = ? (not exported)
190 };
191
192 {
193 const auto type = ParseDefault<int>((*jrange)["t"], kRange_SelectorType);
194 if (type != kRange_SelectorType) {
195 abuilder->log(Logger::Level::kWarning, nullptr,
196 "Ignoring unsupported selector type '%d'", type);
197 return nullptr;
198 }
199 }
200
201 static constexpr Units gUnitMap[] = {
202 Units::kPercentage, // 'r': 1
203 Units::kIndex, // 'r': 2
204 };
205
206 static constexpr Domain gDomainMap[] = {
207 Domain::kChars, // 'b': 1
208 Domain::kCharsExcludingSpaces, // 'b': 2
209 Domain::kWords, // 'b': 3
210 Domain::kLines, // 'b': 4
211 };
212
213 static constexpr Mode gModeMap[] = {
214 Mode::kAdd, // 'm': 1
215 };
216
217 static constexpr Shape gShapeMap[] = {
218 Shape::kSquare, // 'sh': 1
219 Shape::kRampUp, // 'sh': 2
220 Shape::kRampDown, // 'sh': 3
221 Shape::kTriangle, // 'sh': 4
222 Shape::kRound, // 'sh': 5
223 Shape::kSmooth, // 'sh': 6
224 };
225
226 auto selector = sk_sp<RangeSelector>(
227 new RangeSelector(ParseEnum<Units> (gUnitMap , (*jrange)["r" ], abuilder, "units" ),
228 ParseEnum<Domain>(gDomainMap, (*jrange)["b" ], abuilder, "domain"),
229 ParseEnum<Mode> (gModeMap , (*jrange)["m" ], abuilder, "mode" ),
230 ParseEnum<Shape> (gShapeMap , (*jrange)["sh"], abuilder, "shape" )));
231
232 abuilder->bindProperty<ScalarValue>((*jrange)["s"],
233 [selector](const ScalarValue& s) {
234 selector->fStart = s;
235 });
236 abuilder->bindProperty<ScalarValue>((*jrange)["e"],
237 [selector](const ScalarValue& e) {
238 selector->fEnd = e;
239 });
240 abuilder->bindProperty<ScalarValue>((*jrange)["o"],
241 [selector](const ScalarValue& o) {
242 selector->fOffset = o;
243 });
244 abuilder->bindProperty<ScalarValue>((*jrange)["a"],
245 [selector](const ScalarValue& a) {
246 selector->fAmount = a;
247 });
248
249 return selector;
250 }
251
RangeSelector(Units u,Domain d,Mode m,Shape sh)252 RangeSelector::RangeSelector(Units u, Domain d, Mode m, Shape sh)
253 : fUnits(u)
254 , fDomain(d)
255 , fMode(m)
256 , fShape(sh) {
257
258 // Range defaults are unit-specific.
259 switch (fUnits) {
260 case Units::kPercentage:
261 std::tie(fStart, fEnd, fOffset) = UnitTraits<Units::kPercentage>::Defaults();
262 break;
263 case Units::kIndex:
264 std::tie(fStart, fEnd, fOffset) = UnitTraits<Units::kIndex >::Defaults();
265 break;
266 }
267 }
268
resolve(size_t len) const269 std::tuple<float, float> RangeSelector::resolve(size_t len) const {
270 float f_i0, f_i1;
271
272 SkASSERT(fUnits == Units::kPercentage || fUnits == Units::kIndex);
273 const auto resolver = (fUnits == Units::kPercentage)
274 ? UnitTraits<Units::kPercentage>::Resolve
275 : UnitTraits<Units::kIndex >::Resolve;
276
277 std::tie(f_i0, f_i1) = resolver(fStart, fEnd, fOffset, len);
278 if (f_i0 > f_i1) {
279 std::swap(f_i0, f_i1);
280 }
281
282 return std::make_tuple(f_i0, f_i1);
283 }
284
285 /*
286 * General RangeSelector operation:
287 *
288 * 1) The range is resolved to a target domain (characters, words, etc) interval, based on
289 * |start|, |end|, |offset|, |units|.
290 *
291 * 2) A shape generator is mapped to this interval and applied across the whole domain, yielding
292 * coverage values in [0..1].
293 *
294 * 2') When the interval extremes don't coincide with fragment boundaries, the corresponding
295 * fragment coverage is further modulated for partial interval overlap.
296 *
297 * 3) The coverage is then scaled by the |amount| parameter.
298 *
299 * 4) Finally, the resulting coverage is accumulated to existing fragment coverage based on
300 * the specified Mode (add, difference, etc).
301 */
modulateCoverage(const TextAnimator::DomainMaps & maps,TextAnimator::ModulatorBuffer & mbuf) const302 void RangeSelector::modulateCoverage(const TextAnimator::DomainMaps& maps,
303 TextAnimator::ModulatorBuffer& mbuf) const {
304 const CoverageProcessor coverage_proc(maps, fDomain, fMode, mbuf);
305 if (coverage_proc.size() == 0) {
306 return;
307 }
308
309 // Amount is percentage based [-100% .. 100%].
310 const auto amount = SkTPin<float>(fAmount / 100, -1, 1);
311
312 // First, resolve to a float range in the given domain.
313 const auto f_range = this->resolve(coverage_proc.size());
314
315 // f_range pinned to [0..domain_size].
316 const auto f_dom_size = static_cast<float>(coverage_proc.size()),
317 f0 = SkTPin(std::get<0>(f_range), 0.0f, f_dom_size),
318 f1 = SkTPin(std::get<1>(f_range), 0.0f, f_dom_size);
319
320 SkASSERT(static_cast<size_t>(fShape) < SK_ARRAY_COUNT(gShapeGenerators));
321 const auto& generator = gShapeGenerators[static_cast<size_t>(fShape)];
322
323 // Blit constant coverage outside the shape.
324 {
325 // Constant coverage count before the shape left edge, and after the right edge.
326 const auto count_lo = static_cast<size_t>(std::floor(f0)),
327 count_hi = static_cast<size_t>(f_dom_size - std::ceil (f1));
328 SkASSERT(count_lo <= coverage_proc.size());
329 SkASSERT(count_hi <= coverage_proc.size());
330
331 coverage_proc(amount * generator.lo, 0 , count_lo);
332 coverage_proc(amount * generator.hi, coverage_proc.size() - count_hi, count_hi);
333
334 if (count_lo == coverage_proc.size() || count_hi == coverage_proc.size()) {
335 // The shape is completely outside the domain - we're done.
336 return;
337 }
338 }
339
340 // Integral/index range.
341 const auto i0 = std::min<size_t>(f0, coverage_proc.size() - 1),
342 i1 = std::min<size_t>(f1, coverage_proc.size() - 1);
343 SkASSERT(i0 <= i1);
344
345 const auto range_span = std::get<1>(f_range) - std::get<0>(f_range);
346 if (SkScalarNearlyZero(range_span)) {
347 // Empty range - the shape is collapsed. Modulate with lo/hi weighted average.
348 SkASSERT(i0 == i1);
349 const auto ratio = f0 - i0,
350 coverage = Lerp(generator.lo, generator.hi, ratio);
351 coverage_proc(amount * coverage, i0, 1);
352
353 return;
354 }
355
356 // At this point the clamped range maps to the index interval [i0..i1],
357 // with the left/right edges falling within i0/i1, respectively:
358 //
359 // ----------- ------------------ ------------------ -----------
360 // | 0 | | .. | | i0 | | .. | | i1 | | .. | | N |
361 // ----------- ------------------ ------------------ -----------
362 // ^ ^
363 // [___________________]
364 //
365 // f0 f1
366 //
367 // Note: i0 and i1 can have partial coverage, and also i0 may be the same as i1.
368
369 // Computes partial coverage when one or both range edges fall within the same index [i].
370 const auto partial_coverage = [&](float shape_val, float i) {
371 // At least one of the range edges falls within the current fragment.
372 SkASSERT(SkScalarNearlyEqual(i, std::round(i)));
373 SkASSERT((i <= f0 && f0 <= i + 1) || (i <= f1 && f1 <= i + 1));
374
375 // The resulting coverage is a three-way weighted average
376 // of the three range segments (lo, shape_val, hi).
377 const auto lo_weight = std::max(f0 - i, 0.0f),
378 mi_weight = std::min(f1 - i, 1.0f) - lo_weight,
379 hi_weight = std::max(i + 1 - f1, 0.0f);
380
381 SkASSERT(0 <= lo_weight && lo_weight <= 1);
382 SkASSERT(0 <= mi_weight && mi_weight <= 1);
383 SkASSERT(0 <= hi_weight && hi_weight <= 1);
384 SkASSERT(SkScalarNearlyEqual(lo_weight + mi_weight + hi_weight, 1));
385
386 return lo_weight * generator.lo +
387 mi_weight * shape_val +
388 hi_weight * generator.hi;
389 };
390
391 // The shape domain [0..1] is mapped to the range.
392 const auto dt = 1 / range_span;
393 // note: we sample mid-fragment
394 auto t = (i0 + 0.5f - std::get<0>(f_range)) / range_span;
395
396 // [i0] may have partial coverage.
397 coverage_proc(amount * partial_coverage(generator(std::max(t, 0.0f)), i0), i0, 1);
398
399 // If the whole range falls within a single fragment, we're done.
400 if (i0 == i1) {
401 return;
402 }
403
404 t += dt;
405
406 // [i0+1..i1-1] has full coverage.
407 for (auto i = i0 + 1; i < i1; ++i) {
408 SkASSERT(0 <= t && t <= 1);
409 coverage_proc(amount * generator(t), i, 1);
410 t += dt;
411 }
412
413 // [i1] may have partial coverage.
414 coverage_proc(amount * partial_coverage(generator(std::min(t, 1.0f)), i1), i1, 1);
415 }
416
417 } // namespace internal
418 } // namespace skottie
419