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/SkTableColorFilter.h"
11 #include "include/private/SkTPin.h"
12 #include "modules/skottie/src/Adapter.h"
13 #include "modules/skottie/src/SkottieValue.h"
14 #include "modules/sksg/include/SkSGColorFilter.h"
15 #include "src/utils/SkJSON.h"
16
17 #include <array>
18 #include <cmath>
19
20 namespace skottie {
21 namespace internal {
22
23 namespace {
24
25 struct ClipInfo {
26 ScalarValue fClipBlack = 1, // 1: clip, 2/3: don't clip
27 fClipWhite = 1; // ^
28 };
29
30 struct ChannelMapper {
31 ScalarValue fInBlack = 0,
32 fInWhite = 1,
33 fOutBlack = 0,
34 fOutWhite = 1,
35 fGamma = 1;
36
build_lutskottie::internal::__anon033931380111::ChannelMapper37 const uint8_t* build_lut(std::array<uint8_t, 256>& lut_storage,
38 const ClipInfo& clip_info) const {
39 auto in_0 = fInBlack,
40 in_1 = fInWhite,
41 out_0 = fOutBlack,
42 out_1 = fOutWhite,
43 g = sk_ieee_float_divide(1, std::max(fGamma, 0.0f));
44
45 float clip[] = {0, 1};
46 const auto kLottieDoClip = 1;
47 if (SkScalarTruncToInt(clip_info.fClipBlack) == kLottieDoClip) {
48 const auto idx = fOutBlack <= fOutWhite ? 0 : 1;
49 clip[idx] = SkTPin(out_0, 0.0f, 1.0f);
50 }
51 if (SkScalarTruncToInt(clip_info.fClipWhite) == kLottieDoClip) {
52 const auto idx = fOutBlack <= fOutWhite ? 1 : 0;
53 clip[idx] = SkTPin(out_1, 0.0f, 1.0f);
54 }
55 SkASSERT(clip[0] <= clip[1]);
56
57 if (SkScalarNearlyEqual(in_0, out_0) &&
58 SkScalarNearlyEqual(in_1, out_1) &&
59 SkScalarNearlyEqual(g, 1)) {
60 // no-op
61 return nullptr;
62 }
63
64 auto dIn = in_1 - in_0,
65 dOut = out_1 - out_0;
66
67 if (SkScalarNearlyZero(dIn)) {
68 // Degenerate dIn == 0 makes the arithmetic below explode.
69 //
70 // We could specialize the builder to deal with that case, or we could just
71 // nudge by epsilon to make it all work. The latter approach is simpler
72 // and doesn't have any noticeable downsides.
73 //
74 // Also nudge in_0 towards 0.5, in case it was sqashed against an extremity.
75 // This allows for some abrupt transition when the output interval is not
76 // collapsed, and produces results closer to AE.
77 static constexpr auto kEpsilon = 2 * SK_ScalarNearlyZero;
78 dIn += std::copysign(kEpsilon, dIn);
79 in_0 += std::copysign(kEpsilon, .5f - in_0);
80 SkASSERT(!SkScalarNearlyZero(dIn));
81 }
82
83 auto t = -in_0 / dIn,
84 dT = 1 / 255.0f / dIn;
85
86 for (size_t i = 0; i < 256; ++i) {
87 const auto out = out_0 + dOut * std::pow(std::max(t, 0.0f), g);
88 SkASSERT(!SkScalarIsNaN(out));
89
90 lut_storage[i] = static_cast<uint8_t>(std::round(SkTPin(out, clip[0], clip[1]) * 255));
91
92 t += dT;
93 }
94
95 return lut_storage.data();
96 }
97 };
98
99 // ADBE Easy Levels2 color correction effect.
100 //
101 // Maps the selected channel(s) from [inBlack...inWhite] to [outBlack, outWhite],
102 // based on a gamma exponent.
103 //
104 // For [i0..i1] -> [o0..o1]:
105 //
106 // c' = o0 + (o1 - o0) * ((c - i0) / (i1 - i0)) ^ G
107 //
108 // The output is optionally clipped to the output range.
109 //
110 // In/out intervals are clampped to [0..1]. Inversion is allowed.
111
112 class EasyLevelsEffectAdapter final : public DiscardableAdapterBase<EasyLevelsEffectAdapter,
113 sksg::ExternalColorFilter> {
114 public:
EasyLevelsEffectAdapter(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer,const AnimationBuilder * abuilder)115 EasyLevelsEffectAdapter(const skjson::ArrayValue& jprops,
116 sk_sp<sksg::RenderNode> layer,
117 const AnimationBuilder* abuilder)
118 : INHERITED(sksg::ExternalColorFilter::Make(std::move(layer))) {
119 enum : size_t {
120 kChannel_Index = 0,
121 // kHist_Index = 1,
122 kInBlack_Index = 2,
123 kInWhite_Index = 3,
124 kGamma_Index = 4,
125 kOutBlack_Index = 5,
126 kOutWhite_Index = 6,
127 kClipToOutBlack_Index = 7,
128 kClipToOutWhite_Index = 8,
129 };
130
131 EffectBinder(jprops, *abuilder, this)
132 .bind( kChannel_Index, fChannel )
133 .bind( kInBlack_Index, fMapper.fInBlack )
134 .bind( kInWhite_Index, fMapper.fInWhite )
135 .bind( kGamma_Index, fMapper.fGamma )
136 .bind( kOutBlack_Index, fMapper.fOutBlack)
137 .bind( kOutWhite_Index, fMapper.fOutWhite)
138 .bind(kClipToOutBlack_Index, fClip.fClipBlack )
139 .bind(kClipToOutWhite_Index, fClip.fClipWhite );
140 }
141
142 private:
onSync()143 void onSync() override {
144 enum LottieChannel {
145 kRGB_Channel = 1,
146 kR_Channel = 2,
147 kG_Channel = 3,
148 kB_Channel = 4,
149 kA_Channel = 5,
150 };
151
152 const auto channel = SkScalarTruncToInt(fChannel);
153 std::array<uint8_t, 256> lut;
154 if (channel < kRGB_Channel || channel > kA_Channel || !fMapper.build_lut(lut, fClip)) {
155 this->node()->setColorFilter(nullptr);
156 return;
157 }
158
159 this->node()->setColorFilter(SkTableColorFilter::MakeARGB(
160 channel == kA_Channel ? lut.data() : nullptr,
161 channel == kR_Channel || channel == kRGB_Channel ? lut.data() : nullptr,
162 channel == kG_Channel || channel == kRGB_Channel ? lut.data() : nullptr,
163 channel == kB_Channel || channel == kRGB_Channel ? lut.data() : nullptr
164 ));
165 }
166
167 ChannelMapper fMapper;
168 ClipInfo fClip;
169 ScalarValue fChannel = 1; // 1: RGB, 2: R, 3: G, 4: B, 5: A
170
171 using INHERITED = DiscardableAdapterBase<EasyLevelsEffectAdapter, sksg::ExternalColorFilter>;
172 };
173
174 // ADBE Pro Levels2 color correction effect.
175 //
176 // Similar to ADBE Easy Levels2, but offers separate controls for each channel.
177
178 class ProLevelsEffectAdapter final : public DiscardableAdapterBase<ProLevelsEffectAdapter,
179 sksg::ExternalColorFilter> {
180 public:
ProLevelsEffectAdapter(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer,const AnimationBuilder * abuilder)181 ProLevelsEffectAdapter(const skjson::ArrayValue& jprops,
182 sk_sp<sksg::RenderNode> layer,
183 const AnimationBuilder* abuilder)
184 : INHERITED(sksg::ExternalColorFilter::Make(std::move(layer))) {
185 enum : size_t {
186 // kHistChan_Index = 0,
187 // kHist_Index = 1,
188 // kRGBBegin_Index = 2,
189 kRGBInBlack_Index = 3,
190 kRGBInWhite_Index = 4,
191 kRGBGamma_Index = 5,
192 kRGBOutBlack_Index = 6,
193 kRGBOutWhite_Index = 7,
194 // kRGBEnd_Index = 8,
195 // kRBegin_Index = 9,
196 kRInBlack_Index = 10,
197 kRInWhite_Index = 11,
198 kRGamma_Index = 12,
199 kROutBlack_Index = 13,
200 kROutWhite_Index = 14,
201 // kREnd_Index = 15,
202 // kGBegin_Index = 16,
203 kGInBlack_Index = 17,
204 kGInWhite_Index = 18,
205 kGGamma_Index = 19,
206 kGOutBlack_Index = 20,
207 kGOutWhite_Index = 21,
208 // kGEnd_Index = 22,
209 // kBBegin_Index = 23,
210 kBInBlack_Index = 24,
211 kBInWhite_Index = 25,
212 kBGamma_Index = 26,
213 kBOutBlack_Index = 27,
214 kBOutWhite_Index = 28,
215 // kBEnd_Index = 29,
216 // kABegin_Index = 30,
217 kAInBlack_Index = 31,
218 kAInWhite_Index = 32,
219 kAGamma_Index = 33,
220 kAOutBlack_Index = 34,
221 kAOutWhite_Index = 35,
222 // kAEnd_Index = 36,
223 kClipToOutBlack_Index = 37,
224 kClipToOutWhite_Index = 38,
225 };
226
227 EffectBinder(jprops, *abuilder, this)
228 .bind( kRGBInBlack_Index, fRGBMapper.fInBlack )
229 .bind( kRGBInWhite_Index, fRGBMapper.fInWhite )
230 .bind( kRGBGamma_Index, fRGBMapper.fGamma )
231 .bind(kRGBOutBlack_Index, fRGBMapper.fOutBlack)
232 .bind(kRGBOutWhite_Index, fRGBMapper.fOutWhite)
233
234 .bind( kRInBlack_Index, fRMapper.fInBlack )
235 .bind( kRInWhite_Index, fRMapper.fInWhite )
236 .bind( kRGamma_Index, fRMapper.fGamma )
237 .bind(kROutBlack_Index, fRMapper.fOutBlack)
238 .bind(kROutWhite_Index, fRMapper.fOutWhite)
239
240 .bind( kGInBlack_Index, fGMapper.fInBlack )
241 .bind( kGInWhite_Index, fGMapper.fInWhite )
242 .bind( kGGamma_Index, fGMapper.fGamma )
243 .bind(kGOutBlack_Index, fGMapper.fOutBlack)
244 .bind(kGOutWhite_Index, fGMapper.fOutWhite)
245
246 .bind( kBInBlack_Index, fBMapper.fInBlack )
247 .bind( kBInWhite_Index, fBMapper.fInWhite )
248 .bind( kBGamma_Index, fBMapper.fGamma )
249 .bind(kBOutBlack_Index, fBMapper.fOutBlack)
250 .bind(kBOutWhite_Index, fBMapper.fOutWhite)
251
252 .bind( kAInBlack_Index, fAMapper.fInBlack )
253 .bind( kAInWhite_Index, fAMapper.fInWhite )
254 .bind( kAGamma_Index, fAMapper.fGamma )
255 .bind(kAOutBlack_Index, fAMapper.fOutBlack)
256 .bind(kAOutWhite_Index, fAMapper.fOutWhite);
257 }
258
259 private:
onSync()260 void onSync() override {
261 std::array<uint8_t, 256> a_lut_storage,
262 r_lut_storage,
263 g_lut_storage,
264 b_lut_storage;
265
266 auto cf = SkTableColorFilter::MakeARGB(fAMapper.build_lut(a_lut_storage, fClip),
267 fRMapper.build_lut(r_lut_storage, fClip),
268 fGMapper.build_lut(g_lut_storage, fClip),
269 fBMapper.build_lut(b_lut_storage, fClip));
270
271 // The RGB mapper composes outside individual channel mappers.
272 if (const auto* rgb_lut = fRGBMapper.build_lut(a_lut_storage, fClip)) {
273 cf = SkColorFilters::Compose(SkTableColorFilter::MakeARGB(nullptr,
274 rgb_lut,
275 rgb_lut,
276 rgb_lut),
277 std::move(cf));
278 }
279
280 this->node()->setColorFilter(std::move(cf));
281 }
282
283 ChannelMapper fRGBMapper,
284 fRMapper,
285 fGMapper,
286 fBMapper,
287 fAMapper;
288
289 ClipInfo fClip;
290
291 using INHERITED = DiscardableAdapterBase<ProLevelsEffectAdapter, sksg::ExternalColorFilter>;
292 };
293
294 } // namespace
295
attachEasyLevelsEffect(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer) const296 sk_sp<sksg::RenderNode> EffectBuilder::attachEasyLevelsEffect(const skjson::ArrayValue& jprops,
297 sk_sp<sksg::RenderNode> layer) const {
298 return fBuilder->attachDiscardableAdapter<EasyLevelsEffectAdapter>(jprops,
299 std::move(layer),
300 fBuilder);
301 }
302
attachProLevelsEffect(const skjson::ArrayValue & jprops,sk_sp<sksg::RenderNode> layer) const303 sk_sp<sksg::RenderNode> EffectBuilder::attachProLevelsEffect(const skjson::ArrayValue& jprops,
304 sk_sp<sksg::RenderNode> layer) const {
305 return fBuilder->attachDiscardableAdapter<ProLevelsEffectAdapter>(jprops,
306 std::move(layer),
307 fBuilder);
308 }
309
310 } // namespace internal
311 } // namespace skottie
312