1/*
2 * Copyright 2020 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "modules/skottie/src/effects/Effects.h"
9
10#include "include/core/SkColorFilter.h"
11#include "include/effects/SkColorMatrix.h"
12#include "include/effects/SkImageFilters.h"
13#include "modules/skottie/src/Adapter.h"
14#include "modules/skottie/src/SkottieJson.h"
15#include "modules/skottie/src/SkottieValue.h"
16#include "modules/sksg/include/SkSGRenderEffect.h"
17
18#include <cmath>
19
20namespace skottie::internal {
21
22namespace {
23
24class GlowAdapter final : public DiscardableAdapterBase<GlowAdapter, sksg::ExternalImageFilter> {
25public:
26 enum Type {
27 kOuterGlow,
28 kInnerGlow,
29 };
30
31 GlowAdapter(const skjson::ObjectValue& jstyle, const AnimationBuilder& abuilder, Type type)
32 : fType(type) {
33 this->bind(abuilder, jstyle["c" ], fColor);
34 this->bind(abuilder, jstyle["o" ], fOpacity);
35 this->bind(abuilder, jstyle["s" ], fSize);
36 this->bind(abuilder, jstyle["sr"], fInnerSource);
37 this->bind(abuilder, jstyle["ch"], fChoke);
38 }
39
40private:
41 void onSync() override {
42 const auto sigma = fSize * kBlurSizeToSigma,
43 opacity = SkTPin(fOpacity / 100, 0.0f, 1.0f),
44 choke = SkTPin(fChoke / 100, 0.0f, 1.0f);
45 const auto color = static_cast<SkColor4f>(fColor);
46
47 // Select the source alpha channel.
48 SkColorMatrix mask_cm{
49 0, 0, 0, 0, 0,
50 0, 0, 0, 0, 0,
51 0, 0, 0, 0, 0,
52 0, 0, 0, 1, 0
53 };
54
55 // Inner glows with an edge source use the alpha inverse.
56 if (fType == Type::kInnerGlow && SkScalarRoundToInt(fInnerSource) == kEdge) {
57 mask_cm.preConcat({
58 1, 0, 0, 0, 0,
59 0, 1, 0, 0, 0,
60 0, 0, 1, 0, 0,
61 0, 0, 0,-1, 1
62 });
63 }
64
65 // Add glow color and opacity.
66 const SkColorMatrix color_cm {
67 0, 0, 0, 0, color.fR,
68 0, 0, 0, 0, color.fG,
69 0, 0, 0, 0, color.fB,
70 0, 0, 0, opacity * color.fA, 0
71 };
72
73 // Alpha choke only applies when a blur is in use.
74 const auto requires_alpha_choke = (sigma > 0 && choke > 0);
75
76 if (!requires_alpha_choke) {
77 // We can fold the colorization step with the initial source alpha.
78 mask_cm.postConcat(color_cm);
79 }
80
81 auto f = SkImageFilters::ColorFilter(SkColorFilters::Matrix(mask_cm), nullptr);
82
83 if (sigma > 0) {
84 f = SkImageFilters::Blur(sigma, sigma, std::move(f));
85 }
86
87 if (requires_alpha_choke) {
88 // Choke/spread semantics (applied to the blur result):
89 //
90 // 0 -> no effect
91 // 1 -> all non-transparent values turn opaque (the blur "spreads" all the way)
92 // (0..1) -> some form of gradual/nonlinear transition between the two.
93 //
94 // One way to emulate this effect is by upscaling the blur alpha by 1 / (1 - choke):
95 static constexpr float kMaxAlphaScale = 1e6f,
96 kChokeGamma = 0.2f;
97 const auto alpha_scale =
98 std::min(sk_ieee_float_divide(1, 1 - std::pow(choke, kChokeGamma)), kMaxAlphaScale);
99
100 SkColorMatrix choke_cm(1, 0, 0, 0, 0,
101 0, 1, 0, 0, 0,
102 0, 0, 1, 0, 0,
103 0, 0, 0, alpha_scale, 0);
104
105 f = SkImageFilters::ColorFilter(SkColorFilters::Matrix(choke_cm), std::move(f));
106
107 // Colorization is deferred until after alpha choke. It also must be applied as a
108 // separate color filter to ensure the choke scale above is clamped.
109 f = SkImageFilters::ColorFilter(SkColorFilters::Matrix(color_cm), std::move(f));
110 }
111
112 sk_sp<SkImageFilter> source;
113
114 if (fType == Type::kInnerGlow) {
115 // Inner glows draw on top of, and are masked with, the source.
116 f = SkImageFilters::Xfermode(SkBlendMode::kDstIn, std::move(f));
117
118 std::swap(source, f);
119 }
120
121 this->node()->setImageFilter(SkImageFilters::Merge(std::move(f),
122 std::move(source)));
123 }
124
125 enum InnerSource {
126 kEdge = 1,
127 kCenter = 2,
128 };
129
130 const Type fType;
131
132 VectorValue fColor;
133 ScalarValue fOpacity = 100, // percentage
134 fSize = 0,
135 fChoke = 0,
136 fInnerSource = kEdge;
137
138 using INHERITED = DiscardableAdapterBase<GlowAdapter, sksg::ExternalImageFilter>;
139};
140
141static sk_sp<sksg::RenderNode> make_glow_effect(const skjson::ObjectValue& jstyle,
142 const AnimationBuilder& abuilder,
143 sk_sp<sksg::RenderNode> layer,
144 GlowAdapter::Type type) {
145 auto filter_node = abuilder.attachDiscardableAdapter<GlowAdapter>(jstyle, abuilder, type);
146
147 return sksg::ImageFilterEffect::Make(std::move(layer), std::move(filter_node));
148}
149
150} // namespace
151
152sk_sp<sksg::RenderNode> EffectBuilder::attachOuterGlowStyle(const skjson::ObjectValue& jstyle,
153 sk_sp<sksg::RenderNode> layer) const {
154 return make_glow_effect(jstyle, *fBuilder, std::move(layer), GlowAdapter::Type::kOuterGlow);
155}
156
157sk_sp<sksg::RenderNode> EffectBuilder::attachInnerGlowStyle(const skjson::ObjectValue& jstyle,
158 sk_sp<sksg::RenderNode> layer) const {
159 return make_glow_effect(jstyle, *fBuilder, std::move(layer), GlowAdapter::Type::kInnerGlow);
160}
161
162} // namespace skottie::internal
163