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