/* * 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 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 make_saturate(float) { return nullptr; } #endif // SK_ENABLE_SKSL class HueSaturationEffectAdapter final : public AnimatablePropertyContainer { public: static sk_sp Make(const skjson::ArrayValue& jprops, sk_sp layer, const AnimationBuilder* abuilder) { return sk_sp( new HueSaturationEffectAdapter(jprops, std::move(layer), abuilder)); } const sk_sp& node() const { return fColorFilter; } private: HueSaturationEffectAdapter(const skjson::ArrayValue& jprops, sk_sp 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 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(fChanCtrl) != kMaster_Chan) { return nullptr; } sk_sp 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 fColorFilter; float fChanCtrl = 0.0f, fMasterHue = 0.0f, fMasterSat = 0.0f, fMasterLight = 0.0f; }; } // namespace sk_sp EffectBuilder::attachHueSaturationEffect( const skjson::ArrayValue& jprops, sk_sp layer) const { return fBuilder->attachDiscardableAdapter(jprops, std::move(layer), fBuilder); } } // namespace skottie::internal