blob: 5f76b5db4620ecf6821039c8977c5f98813ce843 [file] [log] [blame]
/*
* 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