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