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