| /* |
| * Copyright 2019 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "modules/skottie/src/effects/Effects.h" |
| |
| #include "include/effects/SkTableColorFilter.h" |
| #include "include/private/SkTPin.h" |
| #include "modules/skottie/src/Adapter.h" |
| #include "modules/skottie/src/SkottieValue.h" |
| #include "modules/sksg/include/SkSGColorFilter.h" |
| #include "src/utils/SkJSON.h" |
| |
| #include <array> |
| #include <cmath> |
| |
| namespace skottie { |
| namespace internal { |
| |
| namespace { |
| |
| struct ClipInfo { |
| ScalarValue fClipBlack = 1, // 1: clip, 2/3: don't clip |
| fClipWhite = 1; // ^ |
| }; |
| |
| struct ChannelMapper { |
| ScalarValue fInBlack = 0, |
| fInWhite = 1, |
| fOutBlack = 0, |
| fOutWhite = 1, |
| fGamma = 1; |
| |
| const uint8_t* build_lut(std::array<uint8_t, 256>& lut_storage, |
| const ClipInfo& clip_info) const { |
| auto in_0 = fInBlack, |
| in_1 = fInWhite, |
| out_0 = fOutBlack, |
| out_1 = fOutWhite, |
| g = sk_ieee_float_divide(1, std::max(fGamma, 0.0f)); |
| |
| float clip[] = {0, 1}; |
| const auto kLottieDoClip = 1; |
| if (SkScalarTruncToInt(clip_info.fClipBlack) == kLottieDoClip) { |
| const auto idx = fOutBlack <= fOutWhite ? 0 : 1; |
| clip[idx] = SkTPin(out_0, 0.0f, 1.0f); |
| } |
| if (SkScalarTruncToInt(clip_info.fClipWhite) == kLottieDoClip) { |
| const auto idx = fOutBlack <= fOutWhite ? 1 : 0; |
| clip[idx] = SkTPin(out_1, 0.0f, 1.0f); |
| } |
| SkASSERT(clip[0] <= clip[1]); |
| |
| if (SkScalarNearlyEqual(in_0, out_0) && |
| SkScalarNearlyEqual(in_1, out_1) && |
| SkScalarNearlyEqual(g, 1)) { |
| // no-op |
| return nullptr; |
| } |
| |
| auto dIn = in_1 - in_0, |
| dOut = out_1 - out_0; |
| |
| if (SkScalarNearlyZero(dIn)) { |
| // Degenerate dIn == 0 makes the arithmetic below explode. |
| // |
| // We could specialize the builder to deal with that case, or we could just |
| // nudge by epsilon to make it all work. The latter approach is simpler |
| // and doesn't have any noticeable downsides. |
| // |
| // Also nudge in_0 towards 0.5, in case it was sqashed against an extremity. |
| // This allows for some abrupt transition when the output interval is not |
| // collapsed, and produces results closer to AE. |
| static constexpr auto kEpsilon = 2 * SK_ScalarNearlyZero; |
| dIn += std::copysign(kEpsilon, dIn); |
| in_0 += std::copysign(kEpsilon, .5f - in_0); |
| SkASSERT(!SkScalarNearlyZero(dIn)); |
| } |
| |
| auto t = -in_0 / dIn, |
| dT = 1 / 255.0f / dIn; |
| |
| for (size_t i = 0; i < 256; ++i) { |
| const auto out = out_0 + dOut * std::pow(std::max(t, 0.0f), g); |
| SkASSERT(!SkScalarIsNaN(out)); |
| |
| lut_storage[i] = static_cast<uint8_t>(std::round(SkTPin(out, clip[0], clip[1]) * 255)); |
| |
| t += dT; |
| } |
| |
| return lut_storage.data(); |
| } |
| }; |
| |
| // ADBE Easy Levels2 color correction effect. |
| // |
| // Maps the selected channel(s) from [inBlack...inWhite] to [outBlack, outWhite], |
| // based on a gamma exponent. |
| // |
| // For [i0..i1] -> [o0..o1]: |
| // |
| // c' = o0 + (o1 - o0) * ((c - i0) / (i1 - i0)) ^ G |
| // |
| // The output is optionally clipped to the output range. |
| // |
| // In/out intervals are clampped to [0..1]. Inversion is allowed. |
| |
| class EasyLevelsEffectAdapter final : public DiscardableAdapterBase<EasyLevelsEffectAdapter, |
| sksg::ExternalColorFilter> { |
| public: |
| EasyLevelsEffectAdapter(const skjson::ArrayValue& jprops, |
| sk_sp<sksg::RenderNode> layer, |
| const AnimationBuilder* abuilder) |
| : INHERITED(sksg::ExternalColorFilter::Make(std::move(layer))) { |
| enum : size_t { |
| kChannel_Index = 0, |
| // kHist_Index = 1, |
| kInBlack_Index = 2, |
| kInWhite_Index = 3, |
| kGamma_Index = 4, |
| kOutBlack_Index = 5, |
| kOutWhite_Index = 6, |
| kClipToOutBlack_Index = 7, |
| kClipToOutWhite_Index = 8, |
| }; |
| |
| EffectBinder(jprops, *abuilder, this) |
| .bind( kChannel_Index, fChannel ) |
| .bind( kInBlack_Index, fMapper.fInBlack ) |
| .bind( kInWhite_Index, fMapper.fInWhite ) |
| .bind( kGamma_Index, fMapper.fGamma ) |
| .bind( kOutBlack_Index, fMapper.fOutBlack) |
| .bind( kOutWhite_Index, fMapper.fOutWhite) |
| .bind(kClipToOutBlack_Index, fClip.fClipBlack ) |
| .bind(kClipToOutWhite_Index, fClip.fClipWhite ); |
| } |
| |
| private: |
| void onSync() override { |
| enum LottieChannel { |
| kRGB_Channel = 1, |
| kR_Channel = 2, |
| kG_Channel = 3, |
| kB_Channel = 4, |
| kA_Channel = 5, |
| }; |
| |
| const auto channel = SkScalarTruncToInt(fChannel); |
| std::array<uint8_t, 256> lut; |
| if (channel < kRGB_Channel || channel > kA_Channel || !fMapper.build_lut(lut, fClip)) { |
| this->node()->setColorFilter(nullptr); |
| return; |
| } |
| |
| this->node()->setColorFilter(SkTableColorFilter::MakeARGB( |
| channel == kA_Channel ? lut.data() : nullptr, |
| channel == kR_Channel || channel == kRGB_Channel ? lut.data() : nullptr, |
| channel == kG_Channel || channel == kRGB_Channel ? lut.data() : nullptr, |
| channel == kB_Channel || channel == kRGB_Channel ? lut.data() : nullptr |
| )); |
| } |
| |
| ChannelMapper fMapper; |
| ClipInfo fClip; |
| ScalarValue fChannel = 1; // 1: RGB, 2: R, 3: G, 4: B, 5: A |
| |
| using INHERITED = DiscardableAdapterBase<EasyLevelsEffectAdapter, sksg::ExternalColorFilter>; |
| }; |
| |
| // ADBE Pro Levels2 color correction effect. |
| // |
| // Similar to ADBE Easy Levels2, but offers separate controls for each channel. |
| |
| class ProLevelsEffectAdapter final : public DiscardableAdapterBase<ProLevelsEffectAdapter, |
| sksg::ExternalColorFilter> { |
| public: |
| ProLevelsEffectAdapter(const skjson::ArrayValue& jprops, |
| sk_sp<sksg::RenderNode> layer, |
| const AnimationBuilder* abuilder) |
| : INHERITED(sksg::ExternalColorFilter::Make(std::move(layer))) { |
| enum : size_t { |
| // kHistChan_Index = 0, |
| // kHist_Index = 1, |
| // kRGBBegin_Index = 2, |
| kRGBInBlack_Index = 3, |
| kRGBInWhite_Index = 4, |
| kRGBGamma_Index = 5, |
| kRGBOutBlack_Index = 6, |
| kRGBOutWhite_Index = 7, |
| // kRGBEnd_Index = 8, |
| // kRBegin_Index = 9, |
| kRInBlack_Index = 10, |
| kRInWhite_Index = 11, |
| kRGamma_Index = 12, |
| kROutBlack_Index = 13, |
| kROutWhite_Index = 14, |
| // kREnd_Index = 15, |
| // kGBegin_Index = 16, |
| kGInBlack_Index = 17, |
| kGInWhite_Index = 18, |
| kGGamma_Index = 19, |
| kGOutBlack_Index = 20, |
| kGOutWhite_Index = 21, |
| // kGEnd_Index = 22, |
| // kBBegin_Index = 23, |
| kBInBlack_Index = 24, |
| kBInWhite_Index = 25, |
| kBGamma_Index = 26, |
| kBOutBlack_Index = 27, |
| kBOutWhite_Index = 28, |
| // kBEnd_Index = 29, |
| // kABegin_Index = 30, |
| kAInBlack_Index = 31, |
| kAInWhite_Index = 32, |
| kAGamma_Index = 33, |
| kAOutBlack_Index = 34, |
| kAOutWhite_Index = 35, |
| // kAEnd_Index = 36, |
| kClipToOutBlack_Index = 37, |
| kClipToOutWhite_Index = 38, |
| }; |
| |
| EffectBinder(jprops, *abuilder, this) |
| .bind( kRGBInBlack_Index, fRGBMapper.fInBlack ) |
| .bind( kRGBInWhite_Index, fRGBMapper.fInWhite ) |
| .bind( kRGBGamma_Index, fRGBMapper.fGamma ) |
| .bind(kRGBOutBlack_Index, fRGBMapper.fOutBlack) |
| .bind(kRGBOutWhite_Index, fRGBMapper.fOutWhite) |
| |
| .bind( kRInBlack_Index, fRMapper.fInBlack ) |
| .bind( kRInWhite_Index, fRMapper.fInWhite ) |
| .bind( kRGamma_Index, fRMapper.fGamma ) |
| .bind(kROutBlack_Index, fRMapper.fOutBlack) |
| .bind(kROutWhite_Index, fRMapper.fOutWhite) |
| |
| .bind( kGInBlack_Index, fGMapper.fInBlack ) |
| .bind( kGInWhite_Index, fGMapper.fInWhite ) |
| .bind( kGGamma_Index, fGMapper.fGamma ) |
| .bind(kGOutBlack_Index, fGMapper.fOutBlack) |
| .bind(kGOutWhite_Index, fGMapper.fOutWhite) |
| |
| .bind( kBInBlack_Index, fBMapper.fInBlack ) |
| .bind( kBInWhite_Index, fBMapper.fInWhite ) |
| .bind( kBGamma_Index, fBMapper.fGamma ) |
| .bind(kBOutBlack_Index, fBMapper.fOutBlack) |
| .bind(kBOutWhite_Index, fBMapper.fOutWhite) |
| |
| .bind( kAInBlack_Index, fAMapper.fInBlack ) |
| .bind( kAInWhite_Index, fAMapper.fInWhite ) |
| .bind( kAGamma_Index, fAMapper.fGamma ) |
| .bind(kAOutBlack_Index, fAMapper.fOutBlack) |
| .bind(kAOutWhite_Index, fAMapper.fOutWhite); |
| } |
| |
| private: |
| void onSync() override { |
| std::array<uint8_t, 256> a_lut_storage, |
| r_lut_storage, |
| g_lut_storage, |
| b_lut_storage; |
| |
| auto cf = SkTableColorFilter::MakeARGB(fAMapper.build_lut(a_lut_storage, fClip), |
| fRMapper.build_lut(r_lut_storage, fClip), |
| fGMapper.build_lut(g_lut_storage, fClip), |
| fBMapper.build_lut(b_lut_storage, fClip)); |
| |
| // The RGB mapper composes outside individual channel mappers. |
| if (const auto* rgb_lut = fRGBMapper.build_lut(a_lut_storage, fClip)) { |
| cf = SkColorFilters::Compose(SkTableColorFilter::MakeARGB(nullptr, |
| rgb_lut, |
| rgb_lut, |
| rgb_lut), |
| std::move(cf)); |
| } |
| |
| this->node()->setColorFilter(std::move(cf)); |
| } |
| |
| ChannelMapper fRGBMapper, |
| fRMapper, |
| fGMapper, |
| fBMapper, |
| fAMapper; |
| |
| ClipInfo fClip; |
| |
| using INHERITED = DiscardableAdapterBase<ProLevelsEffectAdapter, sksg::ExternalColorFilter>; |
| }; |
| |
| } // namespace |
| |
| sk_sp<sksg::RenderNode> EffectBuilder::attachEasyLevelsEffect(const skjson::ArrayValue& jprops, |
| sk_sp<sksg::RenderNode> layer) const { |
| return fBuilder->attachDiscardableAdapter<EasyLevelsEffectAdapter>(jprops, |
| std::move(layer), |
| fBuilder); |
| } |
| |
| sk_sp<sksg::RenderNode> EffectBuilder::attachProLevelsEffect(const skjson::ArrayValue& jprops, |
| sk_sp<sksg::RenderNode> layer) const { |
| return fBuilder->attachDiscardableAdapter<ProLevelsEffectAdapter>(jprops, |
| std::move(layer), |
| fBuilder); |
| } |
| |
| } // namespace internal |
| } // namespace skottie |