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