1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "StretchEffect.h"
18 #include <SkImageFilter.h>
19 #include <SkRefCnt.h>
20 #include <SkRuntimeEffect.h>
21 #include <SkString.h>
22 #include <SkSurface.h>
23 #include <include/effects/SkImageFilters.h>
24
25 #include <memory>
26
27 namespace android::uirenderer {
28
29 static const SkString stretchShader = SkString(R"(
30 uniform shader uContentTexture;
31
32 // multiplier to apply to scale effect
33 uniform float uMaxStretchIntensity;
34
35 // Maximum percentage to stretch beyond bounds of target
36 uniform float uStretchAffectedDistX;
37 uniform float uStretchAffectedDistY;
38
39 // Distance stretched as a function of the normalized overscroll times
40 // scale intensity
41 uniform float uDistanceStretchedX;
42 uniform float uDistanceStretchedY;
43 uniform float uInverseDistanceStretchedX;
44 uniform float uInverseDistanceStretchedY;
45 uniform float uDistDiffX;
46
47 // Difference between the peak stretch amount and overscroll amount normalized
48 uniform float uDistDiffY;
49
50 // Horizontal offset represented as a ratio of pixels divided by the target width
51 uniform float uScrollX;
52 // Vertical offset represented as a ratio of pixels divided by the target height
53 uniform float uScrollY;
54
55 // Normalized overscroll amount in the horizontal direction
56 uniform float uOverscrollX;
57
58 // Normalized overscroll amount in the vertical direction
59 uniform float uOverscrollY;
60 uniform float viewportWidth; // target height in pixels
61 uniform float viewportHeight; // target width in pixels
62
63 // uInterpolationStrength is the intensity of the interpolation.
64 // if uInterpolationStrength is 0, then the stretch is constant for all the
65 // uStretchAffectedDist. if uInterpolationStrength is 1, then stretch intensity
66 // is interpolated based on the pixel position in the uStretchAffectedDist area;
67 // The closer we are from the scroll anchor point, the more it stretches,
68 // and the other way around.
69 uniform float uInterpolationStrength;
70
71 float easeIn(float t, float d) {
72 return t * d;
73 }
74
75 float computeOverscrollStart(
76 float inPos,
77 float overscroll,
78 float uStretchAffectedDist,
79 float uInverseStretchAffectedDist,
80 float distanceStretched,
81 float interpolationStrength
82 ) {
83 float offsetPos = uStretchAffectedDist - inPos;
84 float posBasedVariation = mix(
85 1. ,easeIn(offsetPos, uInverseStretchAffectedDist), interpolationStrength);
86 float stretchIntensity = overscroll * posBasedVariation;
87 return distanceStretched - (offsetPos / (1. + stretchIntensity));
88 }
89
90 float computeOverscrollEnd(
91 float inPos,
92 float overscroll,
93 float reverseStretchDist,
94 float uStretchAffectedDist,
95 float uInverseStretchAffectedDist,
96 float distanceStretched,
97 float interpolationStrength,
98 float viewportDimension
99 ) {
100 float offsetPos = inPos - reverseStretchDist;
101 float posBasedVariation = mix(
102 1. ,easeIn(offsetPos, uInverseStretchAffectedDist), interpolationStrength);
103 float stretchIntensity = (-overscroll) * posBasedVariation;
104 return viewportDimension - (distanceStretched - (offsetPos / (1. + stretchIntensity)));
105 }
106
107 // Prefer usage of return values over out parameters as it enables
108 // SKSL to properly inline method calls and works around potential GPU
109 // driver issues on Wembly. See b/182566543 for details
110 float computeOverscroll(
111 float inPos,
112 float overscroll,
113 float uStretchAffectedDist,
114 float uInverseStretchAffectedDist,
115 float distanceStretched,
116 float distanceDiff,
117 float interpolationStrength,
118 float viewportDimension
119 ) {
120 if (overscroll > 0) {
121 if (inPos <= uStretchAffectedDist) {
122 return computeOverscrollStart(
123 inPos,
124 overscroll,
125 uStretchAffectedDist,
126 uInverseStretchAffectedDist,
127 distanceStretched,
128 interpolationStrength
129 );
130 } else {
131 return distanceDiff + inPos;
132 }
133 } else if (overscroll < 0) {
134 float stretchAffectedDist = viewportDimension - uStretchAffectedDist;
135 if (inPos >= stretchAffectedDist) {
136 return computeOverscrollEnd(
137 inPos,
138 overscroll,
139 stretchAffectedDist,
140 uStretchAffectedDist,
141 uInverseStretchAffectedDist,
142 distanceStretched,
143 interpolationStrength,
144 viewportDimension
145 );
146 } else {
147 return -distanceDiff + inPos;
148 }
149 } else {
150 return inPos;
151 }
152 }
153
154 vec4 main(vec2 coord) {
155 float inU = coord.x;
156 float inV = coord.y;
157 float outU;
158 float outV;
159
160 inU += uScrollX;
161 inV += uScrollY;
162 outU = computeOverscroll(
163 inU,
164 uOverscrollX,
165 uStretchAffectedDistX,
166 uInverseDistanceStretchedX,
167 uDistanceStretchedX,
168 uDistDiffX,
169 uInterpolationStrength,
170 viewportWidth
171 );
172 outV = computeOverscroll(
173 inV,
174 uOverscrollY,
175 uStretchAffectedDistY,
176 uInverseDistanceStretchedY,
177 uDistanceStretchedY,
178 uDistDiffY,
179 uInterpolationStrength,
180 viewportHeight
181 );
182 coord.x = outU;
183 coord.y = outV;
184 return uContentTexture.eval(coord);
185 })");
186
187 static const float ZERO = 0.f;
188 static const float INTERPOLATION_STRENGTH_VALUE = 0.7f;
189 static const char CONTENT_TEXTURE[] = "uContentTexture";
190
getShader(float width,float height,const sk_sp<SkImage> & snapshotImage,const SkMatrix * matrix) const191 sk_sp<SkShader> StretchEffect::getShader(float width, float height,
192 const sk_sp<SkImage>& snapshotImage,
193 const SkMatrix* matrix) const {
194 if (isEmpty()) {
195 return nullptr;
196 }
197
198 float normOverScrollDistX = mStretchDirection.x();
199 float normOverScrollDistY = mStretchDirection.y();
200 float distanceStretchedX = width / (1 + abs(normOverScrollDistX));
201 float distanceStretchedY = height / (1 + abs(normOverScrollDistY));
202 float inverseDistanceStretchedX = 1.f / width;
203 float inverseDistanceStretchedY = 1.f / height;
204 float diffX = distanceStretchedX - width;
205 float diffY = distanceStretchedY - height;
206
207 if (mBuilder == nullptr) {
208 mBuilder = std::make_unique<SkRuntimeShaderBuilder>(getStretchEffect());
209 }
210
211 mBuilder->child(CONTENT_TEXTURE) =
212 snapshotImage->makeShader(SkTileMode::kClamp, SkTileMode::kClamp,
213 SkSamplingOptions(SkFilterMode::kLinear), matrix);
214 mBuilder->uniform("uInterpolationStrength").set(&INTERPOLATION_STRENGTH_VALUE, 1);
215 mBuilder->uniform("uStretchAffectedDistX").set(&width, 1);
216 mBuilder->uniform("uStretchAffectedDistY").set(&height, 1);
217 mBuilder->uniform("uDistanceStretchedX").set(&distanceStretchedX, 1);
218 mBuilder->uniform("uDistanceStretchedY").set(&distanceStretchedY, 1);
219 mBuilder->uniform("uInverseDistanceStretchedX").set(&inverseDistanceStretchedX, 1);
220 mBuilder->uniform("uInverseDistanceStretchedY").set(&inverseDistanceStretchedY, 1);
221 mBuilder->uniform("uDistDiffX").set(&diffX, 1);
222 mBuilder->uniform("uDistDiffY").set(&diffY, 1);
223 mBuilder->uniform("uOverscrollX").set(&normOverScrollDistX, 1);
224 mBuilder->uniform("uOverscrollY").set(&normOverScrollDistY, 1);
225 mBuilder->uniform("uScrollX").set(&ZERO, 1);
226 mBuilder->uniform("uScrollY").set(&ZERO, 1);
227 mBuilder->uniform("viewportWidth").set(&width, 1);
228 mBuilder->uniform("viewportHeight").set(&height, 1);
229
230 auto result = mBuilder->makeShader();
231 mBuilder->child(CONTENT_TEXTURE) = nullptr;
232 return result;
233 }
234
getStretchEffect()235 sk_sp<SkRuntimeEffect> StretchEffect::getStretchEffect() {
236 const static SkRuntimeEffect::Result instance = SkRuntimeEffect::MakeForShader(stretchShader);
237 return instance.effect;
238 }
239
240 /**
241 * Helper method that maps the input texture position to the stretch position
242 * based on the given overscroll value that represents an overscroll from
243 * either the top or left
244 * @param overscroll current overscroll value
245 * @param input normalized input position (can be x or y) on the input texture
246 * @return stretched position of the input normalized from 0 to 1
247 */
reverseMapStart(float overscroll,float input)248 float reverseMapStart(float overscroll, float input) {
249 float numerator = (-input * overscroll * overscroll) -
250 (2 * input * overscroll) - input;
251 float denominator = 1.f + (.3f * overscroll) +
252 (.7f * input * overscroll * overscroll) + (.7f * input * overscroll);
253 return -(numerator / denominator);
254 }
255
256 /**
257 * Helper method that maps the input texture position to the stretch position
258 * based on the given overscroll value that represents an overscroll from
259 * either the bottom or right
260 * @param overscroll current overscroll value
261 * @param input normalized input position (can be x or y) on the input texture
262 * @return stretched position of the input normalized from 0 to 1
263 */
reverseMapEnd(float overscroll,float input)264 float reverseMapEnd(float overscroll, float input) {
265 float numerator = (.3f * overscroll * overscroll) -
266 (.3f * input * overscroll * overscroll) +
267 (1.3f * input * overscroll) - overscroll - input;
268 float denominator = (.7f * input * overscroll * overscroll) -
269 (.7f * input * overscroll) - (.7f * overscroll * overscroll) +
270 overscroll - 1.f;
271 return numerator / denominator;
272 }
273
274 /**
275 * Calculates the normalized stretch position given the normalized input
276 * position. This handles calculating the overscroll from either the
277 * top or left vs bottom or right depending on the sign of the given overscroll
278 * value
279 *
280 * @param overscroll unit vector of overscroll from -1 to 1 indicating overscroll
281 * from the bottom or right vs top or left respectively
282 * @param normalizedInput the
283 * @return
284 */
computeReverseOverscroll(float overscroll,float normalizedInput)285 float computeReverseOverscroll(float overscroll, float normalizedInput) {
286 float distanceStretched = 1.f / (1.f + abs(overscroll));
287 float distanceDiff = distanceStretched - 1.f;
288 if (overscroll > 0) {
289 float output = reverseMapStart(overscroll, normalizedInput);
290 if (output <= 1.0f) {
291 return output;
292 } else if (output >= distanceStretched){
293 return output - distanceDiff;
294 }
295 }
296
297 if (overscroll < 0) {
298 float output = reverseMapEnd(overscroll, normalizedInput);
299 if (output >= 0.f) {
300 return output;
301 } else if (output < 0.f){
302 return output + distanceDiff;
303 }
304 }
305 return normalizedInput;
306 }
307
computeStretchedPositionX(float normalizedX) const308 float StretchEffect::computeStretchedPositionX(float normalizedX) const {
309 return computeReverseOverscroll(mStretchDirection.x(), normalizedX);
310 }
311
computeStretchedPositionY(float normalizedY) const312 float StretchEffect::computeStretchedPositionY(float normalizedY) const {
313 return computeReverseOverscroll(mStretchDirection.y(), normalizedY);
314 }
315
316 } // namespace android::uirenderer