[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:
Florin Malita 2019-04-10 15:44:51 -04:00 committed by Skia Commit-Bot
parent 347a9709bb
commit b72dee21c1
6 changed files with 240 additions and 1 deletions

View File

@ -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())

View File

@ -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);

View File

@ -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);

File diff suppressed because one or more lines are too long

View File

@ -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.
*/

View File

@ -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),