blob: 4aef7bf5eb50a7ed488e62c88fd13b5d1d6ce502 [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/SkRuntimeEffect.h"
#include "include/private/SkTPin.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/sksg/include/SkSGColorFilter.h"
#include "src/utils/SkJSON.h"
namespace skottie::internal {
namespace {
#ifdef SK_ENABLE_SKSL
// AE Saturation semantics:
//
// - saturation is applied as a component-wise scale (interpolation/extrapolation)
// relative to chroma mid point
// - the scale factor is clamped such that none of the components over/under saturates
// (e.g. below G/R and B are constrained to low_range and high_range, respectively)
// - the scale is also clammped to a maximum value of 126, empirically
// - the control is mapped linearly when desaturating, and non-linearly (1/1-S) when saturating
//
// 0 G R B 1
// |---------------+----+------------------+-----------------------------------|
// | | |
// min mid max
// <------- chroma ------>
// <------- low_range -------> <---------------- high_range ----------------->
//
// With some care, we can stay in premul for these calculations.
static constexpr char gSaturateSkSL[] =
"uniform half u_scale;"
"half4 main(half4 c) {"
// component min/max
"half2 rg_srt = (c.r < c.g) ? c.rg : c.gr;"
"half c_min = min(rg_srt.x, c.b),"
"c_max = max(rg_srt.y, c.b),"
// chroma and mid-chroma (epsilon to avoid blowing up in the division below)
"ch = max(c_max - c_min, 0.0001),"
"ch_mid = (c_min + c_max)*0.5,"
// clamp scale to the maximum value which doesn't over/under saturate individual components
"scale_max = min(ch_mid, c.a - ch_mid)/ch*2,"
"scale = min(u_scale, scale_max);"
// lerp
"c.rgb = ch_mid + (c.rgb - ch_mid)*scale;"
"return c;"
"}";
static sk_sp<SkColorFilter> make_saturate(float chroma_scale) {
static const auto* effect =
SkRuntimeEffect::MakeForColorFilter(SkString(gSaturateSkSL), {}).effect.release();
SkASSERT(effect);
return effect->makeColorFilter(SkData::MakeWithCopy(&chroma_scale, sizeof(chroma_scale)));
}
#else
static sk_sp<SkColorFilter> make_saturate(float) { return nullptr; }
#endif // SK_ENABLE_SKSL
class HueSaturationEffectAdapter final : public AnimatablePropertyContainer {
public:
static sk_sp<HueSaturationEffectAdapter> Make(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder) {
return sk_sp<HueSaturationEffectAdapter>(
new HueSaturationEffectAdapter(jprops, std::move(layer), abuilder));
}
const sk_sp<sksg::ExternalColorFilter>& node() const { return fColorFilter; }
private:
HueSaturationEffectAdapter(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder)
: fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {
enum : size_t {
kChannelControl_Index = 0,
kChannelRange_Index = 1,
kMasterHue_Index = 2,
kMasterSat_Index = 3,
kMasterLightness_Index = 4,
kColorize_Index = 5,
kColorizeHue_Index = 6,
kColorizeSat_Index = 7,
kColorizeLightness_Index = 8,
};
EffectBinder(jprops, *abuilder, this)
.bind( kChannelControl_Index, fChanCtrl )
.bind( kMasterHue_Index, fMasterHue )
.bind( kMasterSat_Index, fMasterSat )
.bind(kMasterLightness_Index, fMasterLight);
// TODO: colorize support?
}
void onSync() override {
fColorFilter->setColorFilter(this->makeColorFilter());
}
sk_sp<SkColorFilter> makeColorFilter() const {
enum : uint8_t {
kMaster_Chan = 0x01,
kReds_Chan = 0x02,
kYellows_Chan = 0x03,
kGreens_Chan = 0x04,
kCyans_Chan = 0x05,
kBlues_Chan = 0x06,
kMagentas_Chan = 0x07,
};
// We only support master channel controls at this point.
if (static_cast<int>(fChanCtrl) != kMaster_Chan) {
return nullptr;
}
sk_sp<SkColorFilter> cf;
if (!SkScalarNearlyZero(fMasterHue)) {
// Linear control mapping hue(degrees) -> hue offset]
const auto h = fMasterHue/360;
const float cm[20] = {
1, 0, 0, 0, h,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
};
cf = SkColorFilters::HSLAMatrix(cm);
}
if (!SkScalarNearlyZero(fMasterSat)) {
// AE clamps the max chroma scale to this value.
static constexpr auto kMaxScale = 126.0f;
// Control mapping:
// * sat [-100 .. 0) -> scale [0 .. 1) , linear
// * sat [0 .. 100] -> scale [1 .. max] , nonlinear: 100/(100 - sat)
const auto s = SkTPin(fMasterSat/100, -1.0f, 1.0f),
chroma_scale = s < 0 ? s + 1 : std::min(1/(1 - s), kMaxScale);
cf = SkColorFilters::Compose(std::move(cf), make_saturate(chroma_scale));
}
if (!SkScalarNearlyZero(fMasterLight)) {
// AE implements Lightness as a component-wise interpolation to 0 (for L < 0),
// or 1 (for L > 0).
//
// Control mapping:
// * lightness [-100 .. 0) -> lerp[0 .. 1) from 0, linear
// * lightness [0 .. 100] -> lerp[1 .. 0] from 1, linear
const auto l = SkTPin(fMasterLight/100, -1.0f, 1.0f),
ls = 1 - std::abs(l), // scale
lo = l < 0 ? 0 : 1 - ls; // offset
const float cm[20] = {
ls, 0, 0, 0, lo,
0, ls, 0, 0, lo,
0, 0, ls, 0, lo,
0, 0, 0, 1, 0,
};
cf = SkColorFilters::Compose(std::move(cf), SkColorFilters::Matrix(cm));
}
return cf;
}
const sk_sp<sksg::ExternalColorFilter> fColorFilter;
float fChanCtrl = 0.0f,
fMasterHue = 0.0f,
fMasterSat = 0.0f,
fMasterLight = 0.0f;
};
} // namespace
sk_sp<sksg::RenderNode> EffectBuilder::attachHueSaturationEffect(
const skjson::ArrayValue& jprops, sk_sp<sksg::RenderNode> layer) const {
return fBuilder->attachDiscardableAdapter<HueSaturationEffectAdapter>(jprops,
std::move(layer),
fBuilder);
}
} // namespace skottie::internal