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