[skottie] Initial "Levels" effect support
Introduce sksg::LevelsColorFilter and hook into Skottie's effects builder. Limitations: - BM/Lottie does not export animated paramaters (static values only) - BM/Lottie only exports the first modified channel Change-Id: I9ef389478c2eaa0d13794abe6a8089a8b3c0c62e Reviewed-on: https://skia-review.googlesource.com/c/skia/+/206269 Commit-Queue: Florin Malita <fmalita@chromium.org> Reviewed-by: Mike Reed <reed@google.com>
This commit is contained in:
parent
347a9709bb
commit
b72dee21c1
@ -13,6 +13,7 @@
|
||||
#include "SkMatrix44.h"
|
||||
#include "SkPath.h"
|
||||
#include "SkRRect.h"
|
||||
#include "SkSGColorFilter.h"
|
||||
#include "SkSGDraw.h"
|
||||
#include "SkSGGradient.h"
|
||||
#include "SkSGGroup.h"
|
||||
@ -23,6 +24,7 @@
|
||||
#include "SkSGText.h"
|
||||
#include "SkSGTransform.h"
|
||||
#include "SkSGTrimEffect.h"
|
||||
#include "SkTableColorFilter.h"
|
||||
#include "SkTo.h"
|
||||
#include "SkottieShaper.h"
|
||||
#include "SkottieValue.h"
|
||||
@ -433,6 +435,101 @@ void GaussianBlurEffectAdapter::apply() {
|
||||
fBlur->setTileMode(kRepeatEdgeMap[repeat_index]);
|
||||
}
|
||||
|
||||
|
||||
// Levels color correction effect.
|
||||
//
|
||||
// Maps the selected channels 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.
|
||||
LevelsEffectAdapter::LevelsEffectAdapter(sk_sp<sksg::RenderNode> child)
|
||||
: fEffect(sksg::ExternalColorFilter::Make(std::move(child))) {
|
||||
SkASSERT(fEffect);
|
||||
}
|
||||
|
||||
LevelsEffectAdapter::~LevelsEffectAdapter() = default;
|
||||
|
||||
void LevelsEffectAdapter::apply() {
|
||||
enum LottieChannel {
|
||||
kRGB_Channel = 1,
|
||||
kR_Channel = 2,
|
||||
kG_Channel = 3,
|
||||
kB_Channel = 4,
|
||||
kA_Channel = 5,
|
||||
};
|
||||
|
||||
const auto channel = SkScalarTruncToInt(fChannel);
|
||||
if (channel < kRGB_Channel || channel > kA_Channel) {
|
||||
fEffect->setColorFilter(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
auto in_0 = SkTPin(fInBlack, 0.0f, 1.0f),
|
||||
in_1 = SkTPin(fInWhite, 0.0f, 1.0f),
|
||||
out_0 = SkTPin(fOutBlack, 0.0f, 1.0f),
|
||||
out_1 = SkTPin(fOutWhite, 0.0f, 1.0f),
|
||||
g = 1 / SkTMax(fGamma, 0.0f);
|
||||
|
||||
float clip[] = {0, 1};
|
||||
const auto kLottieDoClip = 1;
|
||||
if (SkScalarTruncToInt(fClipBlack) == kLottieDoClip) {
|
||||
const auto idx = fOutBlack <= fOutWhite ? 0 : 1;
|
||||
clip[idx] = out_0;
|
||||
}
|
||||
if (SkScalarTruncToInt(fClipWhite) == kLottieDoClip) {
|
||||
const auto idx = fOutBlack <= fOutWhite ? 1 : 0;
|
||||
clip[idx] = out_1;
|
||||
}
|
||||
SkASSERT(clip[0] <= clip[1]);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
uint8_t lut[256];
|
||||
|
||||
auto t = -in_0 / dIn,
|
||||
dT = 1 / 255.0f / dIn;
|
||||
|
||||
// TODO: is linear gamma common-enough to warrant a fast path?
|
||||
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[i] = static_cast<uint8_t>(std::round(SkTPin(out, clip[0], clip[1]) * 255));
|
||||
|
||||
t += dT;
|
||||
}
|
||||
|
||||
fEffect->setColorFilter(SkTableColorFilter::MakeARGB(
|
||||
channel == kA_Channel ? lut : nullptr,
|
||||
channel == kR_Channel || channel == kRGB_Channel ? lut : nullptr,
|
||||
channel == kG_Channel || channel == kRGB_Channel ? lut : nullptr,
|
||||
channel == kB_Channel || channel == kRGB_Channel ? lut : nullptr
|
||||
));
|
||||
}
|
||||
|
||||
TextAdapter::TextAdapter(sk_sp<sksg::Group> root)
|
||||
: fRoot(std::move(root))
|
||||
, fTextNode(sksg::TextBlob::Make())
|
||||
|
@ -19,6 +19,7 @@ class BlurImageFilter;
|
||||
class Color;
|
||||
class Draw;
|
||||
class DropShadowImageFilter;
|
||||
class ExternalColorFilter;
|
||||
class Gradient;
|
||||
class Group;
|
||||
class LinearGradient;
|
||||
@ -329,6 +330,30 @@ private:
|
||||
const sk_sp<sksg::BlurImageFilter> fBlur;
|
||||
};
|
||||
|
||||
class LevelsEffectAdapter final : public SkNVRefCnt<LevelsEffectAdapter> {
|
||||
public:
|
||||
explicit LevelsEffectAdapter(sk_sp<sksg::RenderNode> child);
|
||||
~LevelsEffectAdapter();
|
||||
|
||||
// 1: RGB, 2: R, 3: G, 4: B, 5: A
|
||||
ADAPTER_PROPERTY( Channel, SkScalar, 1)
|
||||
ADAPTER_PROPERTY( InBlack, SkScalar, 0)
|
||||
ADAPTER_PROPERTY( InWhite, SkScalar, 1)
|
||||
ADAPTER_PROPERTY( OutBlack, SkScalar, 0)
|
||||
ADAPTER_PROPERTY( OutWhite, SkScalar, 1)
|
||||
ADAPTER_PROPERTY( Gamma, SkScalar, 1)
|
||||
// 1: clip, 2,3: don't clip
|
||||
ADAPTER_PROPERTY(ClipBlack, SkScalar, 1)
|
||||
ADAPTER_PROPERTY(ClipWhite, SkScalar, 1)
|
||||
|
||||
const sk_sp<sksg::ExternalColorFilter>& root() const { return fEffect; }
|
||||
|
||||
private:
|
||||
void apply();
|
||||
|
||||
sk_sp<sksg::ExternalColorFilter> fEffect;
|
||||
};
|
||||
|
||||
class TextAdapter final : public SkNVRefCnt<TextAdapter> {
|
||||
public:
|
||||
explicit TextAdapter(sk_sp<sksg::Group> root);
|
||||
|
@ -322,6 +322,81 @@ sk_sp<sksg::RenderNode> AttachGaussianBlurLayerEffect(const skjson::ArrayValue&
|
||||
return sksg::ImageFilterEffect::Make(std::move(layer), std::move(blur_effect));
|
||||
}
|
||||
|
||||
sk_sp<sksg::RenderNode> AttachLevelsLayerEffect(const skjson::ArrayValue& jprops,
|
||||
const AnimationBuilder* abuilder,
|
||||
AnimatorScope* ascope,
|
||||
sk_sp<sksg::RenderNode> layer) {
|
||||
enum : size_t {
|
||||
kChannel_Index = 0,
|
||||
// ??? = 1,
|
||||
kInputBlack_Index = 2,
|
||||
kInputWhite_Index = 3,
|
||||
kGamma_Index = 4,
|
||||
kOutputBlack_Index = 5,
|
||||
kOutputWhite_Index = 6,
|
||||
kClipToOutBlack_Index = 7,
|
||||
kClipToOutWhite_Index = 8,
|
||||
|
||||
kMax_Index = kClipToOutWhite_Index,
|
||||
};
|
||||
|
||||
if (jprops.size() <= kMax_Index) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const skjson::ObjectValue* channel_prop = jprops[ kChannel_Index];
|
||||
const skjson::ObjectValue* iblack_prop = jprops[ kInputBlack_Index];
|
||||
const skjson::ObjectValue* iwhite_prop = jprops[ kInputWhite_Index];
|
||||
const skjson::ObjectValue* gamma_prop = jprops[ kGamma_Index];
|
||||
const skjson::ObjectValue* oblack_prop = jprops[ kOutputBlack_Index];
|
||||
const skjson::ObjectValue* owhite_prop = jprops[ kOutputWhite_Index];
|
||||
const skjson::ObjectValue* clip_black_prop = jprops[kClipToOutBlack_Index];
|
||||
const skjson::ObjectValue* clip_white_prop = jprops[kClipToOutWhite_Index];
|
||||
|
||||
if (!channel_prop || !iblack_prop || !iwhite_prop || !gamma_prop ||
|
||||
!oblack_prop || !owhite_prop || !clip_black_prop || !clip_white_prop) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto adapter = sk_make_sp<LevelsEffectAdapter>(std::move(layer));
|
||||
|
||||
abuilder->bindProperty<ScalarValue>((*channel_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& channel) {
|
||||
adapter->setChannel(channel);
|
||||
});
|
||||
abuilder->bindProperty<ScalarValue>((*iblack_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& ib) {
|
||||
adapter->setInBlack(ib);
|
||||
});
|
||||
abuilder->bindProperty<ScalarValue>((*iwhite_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& iw) {
|
||||
adapter->setInWhite(iw);
|
||||
});
|
||||
abuilder->bindProperty<ScalarValue>((*oblack_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& ob) {
|
||||
adapter->setOutBlack(ob);
|
||||
});
|
||||
abuilder->bindProperty<ScalarValue>((*owhite_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& ow) {
|
||||
adapter->setOutWhite(ow);
|
||||
});
|
||||
abuilder->bindProperty<ScalarValue>((*gamma_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& g) {
|
||||
adapter->setGamma(g);
|
||||
});
|
||||
|
||||
abuilder->bindProperty<ScalarValue>((*clip_black_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& cb) {
|
||||
adapter->setClipBlack(cb);
|
||||
});
|
||||
abuilder->bindProperty<ScalarValue>((*clip_white_prop)["v"], ascope,
|
||||
[adapter](const ScalarValue& cw) {
|
||||
adapter->setClipWhite(cw);
|
||||
});
|
||||
|
||||
return adapter->root();
|
||||
}
|
||||
|
||||
using EffectBuilderT = sk_sp<sksg::RenderNode> (*)(const skjson::ArrayValue&,
|
||||
const AnimationBuilder*,
|
||||
AnimatorScope*,
|
||||
@ -359,10 +434,12 @@ EffectBuilderT FindEffectBuilder(const AnimationBuilder* abuilder,
|
||||
// Try a name-based lookup.
|
||||
|
||||
if (const skjson::StringValue* mn = jeffect["mn"]) {
|
||||
// Just gradient ramp for now.
|
||||
if (!strcmp(mn->begin(), "ADBE Ramp")) {
|
||||
return AttachGradientLayerEffect;
|
||||
}
|
||||
if (!strcmp(mn->begin(), "ADBE Easy Levels2")) {
|
||||
return AttachLevelsLayerEffect;
|
||||
}
|
||||
}
|
||||
|
||||
abuilder->log(Logger::Level::kWarning, nullptr, "Unsupported layer effect type: %d.", ty);
|
||||
|
1
modules/skottie/tests/levels-effect.json
Normal file
1
modules/skottie/tests/levels-effect.json
Normal file
File diff suppressed because one or more lines are too long
@ -40,6 +40,30 @@ private:
|
||||
typedef EffectNode INHERITED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for externally-managed SkColorFilters.
|
||||
*
|
||||
* Allows attaching non-sksg color filters to the render tree.
|
||||
*/
|
||||
class ExternalColorFilter final : public EffectNode {
|
||||
public:
|
||||
static sk_sp<ExternalColorFilter> Make(sk_sp<RenderNode> child);
|
||||
|
||||
~ExternalColorFilter() override;
|
||||
|
||||
SG_ATTRIBUTE(ColorFilter, sk_sp<SkColorFilter>, fColorFilter)
|
||||
|
||||
protected:
|
||||
void onRender(SkCanvas*, const RenderContext*) const override;
|
||||
|
||||
private:
|
||||
explicit ExternalColorFilter(sk_sp<RenderNode>);
|
||||
|
||||
sk_sp<SkColorFilter> fColorFilter;
|
||||
|
||||
using INHERITED = EffectNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Concrete SkModeColorFilter Effect node.
|
||||
*/
|
||||
|
@ -38,6 +38,21 @@ SkRect ColorFilter::onRevalidate(InvalidationController* ic, const SkMatrix& ctm
|
||||
return this->INHERITED::onRevalidate(ic, ctm);
|
||||
}
|
||||
|
||||
sk_sp<ExternalColorFilter> ExternalColorFilter::Make(sk_sp<RenderNode> child) {
|
||||
return child ? sk_sp<ExternalColorFilter>(new ExternalColorFilter(std::move(child)))
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
ExternalColorFilter::ExternalColorFilter(sk_sp<RenderNode> child) : INHERITED(std::move(child)) {}
|
||||
|
||||
ExternalColorFilter::~ExternalColorFilter() = default;
|
||||
|
||||
void ExternalColorFilter::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
|
||||
const auto local_ctx = ScopedRenderContext(canvas, ctx).modulateColorFilter(fColorFilter);
|
||||
|
||||
this->INHERITED::onRender(canvas, local_ctx);
|
||||
}
|
||||
|
||||
sk_sp<ModeColorFilter> ModeColorFilter::Make(sk_sp<RenderNode> child, sk_sp<Color> color,
|
||||
SkBlendMode mode) {
|
||||
return (child && color) ? sk_sp<ModeColorFilter>(new ModeColorFilter(std::move(child),
|
||||
|
Loading…
Reference in New Issue
Block a user