xref: /aosp_15_r20/external/skia/modules/skottie/src/effects/HueSaturationEffect.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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 "include/core/SkColorFilter.h"
9 #include "include/core/SkData.h"
10 #include "include/core/SkRefCnt.h"
11 #include "include/core/SkScalar.h"
12 #include "include/core/SkString.h"
13 #include "include/effects/SkRuntimeEffect.h"
14 #include "include/private/base/SkAssert.h"
15 #include "include/private/base/SkTPin.h"
16 #include "modules/skottie/src/SkottiePriv.h"
17 #include "modules/skottie/src/animator/Animator.h"
18 #include "modules/skottie/src/effects/Effects.h"
19 #include "modules/sksg/include/SkSGColorFilter.h"
20 #include "modules/sksg/include/SkSGRenderNode.h"
21 
22 #include <algorithm>
23 #include <cmath>
24 #include <cstddef>
25 #include <cstdint>
26 #include <utility>
27 
28 namespace skjson {
29 class ArrayValue;
30 }
31 
32 namespace skottie::internal {
33 namespace {
34 
35 // AE Saturation semantics:
36 //
37 //   - saturation is applied as a component-wise scale (interpolation/extrapolation)
38 //     relative to chroma mid point
39 //   - the scale factor is clamped such that none of the components over/under saturates
40 //     (e.g. below G/R and B are constrained to low_range and high_range, respectively)
41 //   - the scale is also clammped to a maximum value of 126, empirically
42 //   - the control is mapped linearly when desaturating, and non-linearly (1/1-S) when saturating
43 //
44 // 0               G    R                  B                                   1
45 // |---------------+----+------------------+-----------------------------------|
46 //                 |           |           |
47 //                min         mid         max
48 //                  <------- chroma ------>
49 //  <------- low_range -------> <---------------- high_range ----------------->
50 //
51 // With some care, we can stay in premul for these calculations.
52 static constexpr char gSaturateSkSL[] =
53 "uniform half u_scale;"
54 
55 "half4 main(half4 c) {"
56     // component min/max
57     "half2 rg_srt = (c.r < c.g) ? c.rg : c.gr;"
58     "half c_min = min(rg_srt.x, c.b),"
59          "c_max = max(rg_srt.y, c.b),"
60 
61     // chroma and mid-chroma (epsilon to avoid blowing up in the division below)
62     "ch     = max(c_max - c_min, 0.0001),"
63     "ch_mid = (c_min + c_max)*0.5,"
64 
65     // clamp scale to the maximum value which doesn't over/under saturate individual components
66     "scale_max = min(ch_mid, c.a - ch_mid)/ch*2,"
67     "scale = min(u_scale, scale_max);"
68 
69     // lerp
70     "c.rgb = ch_mid + (c.rgb - ch_mid)*scale;"
71 
72     "return c;"
73 "}";
74 
make_saturate(float chroma_scale)75 static sk_sp<SkColorFilter> make_saturate(float chroma_scale) {
76     static const auto* effect =
77             SkRuntimeEffect::MakeForColorFilter(SkString(gSaturateSkSL), {}).effect.release();
78     SkASSERT(effect);
79 
80     return effect->makeColorFilter(SkData::MakeWithCopy(&chroma_scale, sizeof(chroma_scale)));
81 }
82 
83 class HueSaturationEffectAdapter final : public AnimatablePropertyContainer {
84 public:
Make(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer,const AnimationBuilder * abuilder)85     static sk_sp<HueSaturationEffectAdapter> Make(const skjson::ArrayValue& jprops,
86                                                   sk_sp<sksg::RenderNode> layer,
87                                                   const AnimationBuilder* abuilder) {
88 
89         return sk_sp<HueSaturationEffectAdapter>(
90                     new HueSaturationEffectAdapter(jprops, std::move(layer), abuilder));
91     }
92 
node() const93     const sk_sp<sksg::ExternalColorFilter>& node() const { return fColorFilter; }
94 
95 private:
HueSaturationEffectAdapter(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer,const AnimationBuilder * abuilder)96     HueSaturationEffectAdapter(const skjson::ArrayValue& jprops,
97                                sk_sp<sksg::RenderNode> layer,
98                                const AnimationBuilder* abuilder)
99         : fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {
100         enum : size_t {
101                kChannelControl_Index = 0,
102                  kChannelRange_Index = 1,
103                     kMasterHue_Index = 2,
104                     kMasterSat_Index = 3,
105               kMasterLightness_Index = 4,
106                      kColorize_Index = 5,
107                   kColorizeHue_Index = 6,
108                   kColorizeSat_Index = 7,
109             kColorizeLightness_Index = 8,
110         };
111 
112         EffectBinder(jprops, *abuilder, this)
113                 .bind( kChannelControl_Index, fChanCtrl   )
114                 .bind(      kMasterHue_Index, fMasterHue  )
115                 .bind(      kMasterSat_Index, fMasterSat  )
116                 .bind(kMasterLightness_Index, fMasterLight);
117 
118         // TODO: colorize support?
119     }
120 
onSync()121     void onSync() override {
122         fColorFilter->setColorFilter(this->makeColorFilter());
123     }
124 
makeColorFilter() const125     sk_sp<SkColorFilter> makeColorFilter() const {
126         enum : uint8_t {
127             kMaster_Chan   = 0x01,
128             kReds_Chan     = 0x02,
129             kYellows_Chan  = 0x03,
130             kGreens_Chan   = 0x04,
131             kCyans_Chan    = 0x05,
132             kBlues_Chan    = 0x06,
133             kMagentas_Chan = 0x07,
134         };
135 
136         // We only support master channel controls at this point.
137         if (static_cast<int>(fChanCtrl) != kMaster_Chan) {
138             return nullptr;
139         }
140 
141         sk_sp<SkColorFilter> cf;
142 
143         if (!SkScalarNearlyZero(fMasterHue)) {
144             // Linear control mapping hue(degrees) -> hue offset]
145             const auto h = fMasterHue/360;
146 
147             const float cm[20] = {
148                 1, 0, 0, 0, h,
149                 0, 1, 0, 0, 0,
150                 0, 0, 1, 0, 0,
151                 0, 0, 0, 1, 0,
152             };
153 
154             cf = SkColorFilters::HSLAMatrix(cm);
155         }
156 
157         if (!SkScalarNearlyZero(fMasterSat)) {
158             // AE clamps the max chroma scale to this value.
159             static constexpr auto kMaxScale = 126.0f;
160 
161             // Control mapping:
162             //   * sat [-100 .. 0) -> scale [0 .. 1)   , linear
163             //   * sat  [0 .. 100] -> scale [1 .. max] , nonlinear: 100/(100 - sat)
164             const auto s            = SkTPin(fMasterSat/100, -1.0f, 1.0f),
165                        chroma_scale = s < 0 ? s + 1 : std::min(1/(1 - s), kMaxScale);
166 
167             cf = SkColorFilters::Compose(std::move(cf), make_saturate(chroma_scale));
168         }
169 
170         if (!SkScalarNearlyZero(fMasterLight)) {
171             // AE implements Lightness as a component-wise interpolation to 0 (for L < 0),
172             // or 1 (for L > 0).
173             //
174             // Control mapping:
175             //   * lightness [-100 .. 0) -> lerp[0 .. 1) from 0, linear
176             //   * lightness  [0 .. 100] -> lerp[1 .. 0] from 1, linear
177             const auto l  = SkTPin(fMasterLight/100, -1.0f, 1.0f),
178                        ls = 1 - std::abs(l),    // scale
179                        lo = l < 0 ? 0 : 1 - ls; // offset
180 
181             const float cm[20] = {
182                 ls,  0,  0, 0, lo,
183                  0, ls,  0, 0, lo,
184                  0,  0, ls, 0, lo,
185                  0,  0,  0, 1,  0,
186             };
187 
188             cf = SkColorFilters::Compose(std::move(cf), SkColorFilters::Matrix(cm));
189         }
190 
191         return cf;
192     }
193 
194     const sk_sp<sksg::ExternalColorFilter> fColorFilter;
195 
196     float fChanCtrl    = 0.0f,
197           fMasterHue   = 0.0f,
198           fMasterSat   = 0.0f,
199           fMasterLight = 0.0f;
200 };
201 
202 } // namespace
203 
attachHueSaturationEffect(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer) const204 sk_sp<sksg::RenderNode> EffectBuilder::attachHueSaturationEffect(
205         const skjson::ArrayValue& jprops, sk_sp<sksg::RenderNode> layer) const {
206     return fBuilder->attachDiscardableAdapter<HueSaturationEffectAdapter>(jprops,
207                                                                           std::move(layer),
208                                                                           fBuilder);
209 }
210 
211 } // namespace skottie::internal
212