Merge "Update ripple spec" into sc-dev

This commit is contained in:
Lucas Dupin
2021-03-10 19:10:58 +00:00
committed by Android (Google) Code Review
3 changed files with 248 additions and 113 deletions

View File

@@ -27,8 +27,9 @@ import android.graphics.Paint;
import android.graphics.RecordingCanvas; import android.graphics.RecordingCanvas;
import android.graphics.animation.RenderNodeAnimator; import android.graphics.animation.RenderNodeAnimator;
import android.util.ArraySet; import android.util.ArraySet;
import android.view.animation.DecelerateInterpolator; import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator; import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -36,32 +37,41 @@ import java.util.function.Consumer;
* @hide * @hide
*/ */
public final class RippleAnimationSession { public final class RippleAnimationSession {
private static final int ENTER_ANIM_DURATION = 350; private static final String TAG = "RippleAnimationSession";
private static final int EXIT_ANIM_OFFSET = ENTER_ANIM_DURATION; private static final int ENTER_ANIM_DURATION = 300;
private static final int EXIT_ANIM_DURATION = 350; private static final int SLIDE_ANIM_DURATION = 450;
private static final int EXIT_ANIM_DURATION = 300;
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
// Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that private static final TimeInterpolator PATH_INTERPOLATOR =
private static final TimeInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); new PathInterpolator(.2f, 0, 0, 1f);
private Consumer<RippleAnimationSession> mOnSessionEnd; private Consumer<RippleAnimationSession> mOnSessionEnd;
private AnimationProperties<Float, Paint> mProperties; private final AnimationProperties<Float, Paint> mProperties;
private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties; private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties;
private Runnable mOnUpdate; private Runnable mOnUpdate;
private long mStartTime; private long mStartTime;
private boolean mForceSoftware; private boolean mForceSoftware;
private ArraySet<Animator> mActiveAnimations = new ArraySet(3); private final float mWidth, mHeight;
private final ValueAnimator mSparkle = ValueAnimator.ofFloat(0, 1);
private final ArraySet<Animator> mActiveAnimations = new ArraySet<>(3);
RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties, RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties,
boolean forceSoftware) { boolean forceSoftware, float width, float height) {
mProperties = properties; mProperties = properties;
mForceSoftware = forceSoftware; mForceSoftware = forceSoftware;
} mWidth = width;
mHeight = height;
void end() { mSparkle.addUpdateListener(anim -> {
for (Animator anim: mActiveAnimations) { final long now = AnimationUtils.currentAnimationTimeMillis();
if (anim != null) anim.end(); final long elapsed = now - mStartTime - ENTER_ANIM_DURATION;
} final float phase = (float) elapsed / 1000f;
mActiveAnimations.clear(); mProperties.getShader().setSecondsOffset(phase);
notifyUpdate();
});
mSparkle.setDuration(ENTER_ANIM_DURATION);
mSparkle.setStartDelay(ENTER_ANIM_DURATION);
mSparkle.setInterpolator(LINEAR_INTERPOLATOR);
mSparkle.setRepeatCount(ValueAnimator.INFINITE);
} }
@NonNull RippleAnimationSession enter(Canvas canvas) { @NonNull RippleAnimationSession enter(Canvas canvas) {
@@ -70,17 +80,19 @@ public final class RippleAnimationSession {
} else { } else {
enterSoftware(); enterSoftware();
} }
mStartTime = System.nanoTime(); mStartTime = AnimationUtils.currentAnimationTimeMillis();
return this; return this;
} }
@NonNull RippleAnimationSession exit(Canvas canvas) { @NonNull RippleAnimationSession exit(Canvas canvas) {
mSparkle.end();
if (isHwAccelerated(canvas)) exitHardware((RecordingCanvas) canvas); if (isHwAccelerated(canvas)) exitHardware((RecordingCanvas) canvas);
else exitSoftware(); else exitSoftware();
return this; return this;
} }
private void onAnimationEnd(Animator anim) { private void onAnimationEnd(Animator anim) {
notifyUpdate();
mActiveAnimations.remove(anim); mActiveAnimations.remove(anim);
} }
@@ -92,7 +104,6 @@ public final class RippleAnimationSession {
RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) { RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) {
mOnUpdate = run; mOnUpdate = run;
mProperties.setOnChange(mOnUpdate);
return this; return this;
} }
@@ -122,14 +133,12 @@ public final class RippleAnimationSession {
} }
private long computeDelay() { private long computeDelay() {
long currentTime = System.nanoTime(); final long timePassed = AnimationUtils.currentAnimationTimeMillis() - mStartTime;
long timePassed = (currentTime - mStartTime) / 1_000_000; return Math.max((long) SLIDE_ANIM_DURATION - timePassed, 0);
long difference = EXIT_ANIM_OFFSET;
return Math.max(difference - timePassed, 0);
} }
private void notifyUpdate() { private void notifyUpdate() {
Runnable onUpdate = mOnUpdate; if (mOnUpdate != null) mOnUpdate.run();
if (onUpdate != null) onUpdate.run();
} }
RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) { RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) {
@@ -153,7 +162,7 @@ public final class RippleAnimationSession {
} }
}); });
exit.setTarget(canvas); exit.setTarget(canvas);
exit.setInterpolator(DECELERATE_INTERPOLATOR); exit.setInterpolator(LINEAR_INTERPOLATOR);
long delay = computeDelay(); long delay = computeDelay();
exit.setStartDelay(delay); exit.setStartDelay(delay);
@@ -161,36 +170,67 @@ public final class RippleAnimationSession {
mActiveAnimations.add(exit); mActiveAnimations.add(exit);
} }
private void enterHardware(RecordingCanvas can) { private void enterHardware(RecordingCanvas canvas) {
AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>>
props = getCanvasProperties(); props = getCanvasProperties();
RenderNodeAnimator expand = RenderNodeAnimator expand =
new RenderNodeAnimator(props.getProgress(), .5f); new RenderNodeAnimator(props.getProgress(), .5f);
expand.setTarget(can); RenderNodeAnimator slideX =
expand.setDuration(ENTER_ANIM_DURATION); new RenderNodeAnimator(props.getX(), mWidth / 2);
expand.addListener(new AnimatorListener(this)); RenderNodeAnimator slideY =
new RenderNodeAnimator(props.getY(), mHeight / 2);
expand.setTarget(canvas);
slideX.setTarget(canvas);
slideY.setTarget(canvas);
startAnimation(expand, slideX, slideY);
}
private void startAnimation(Animator expand,
Animator slideX, Animator slideY) {
expand.setDuration(SLIDE_ANIM_DURATION);
slideX.setDuration(SLIDE_ANIM_DURATION);
slideY.setDuration(SLIDE_ANIM_DURATION);
slideX.addListener(new AnimatorListener(this));
expand.setInterpolator(LINEAR_INTERPOLATOR); expand.setInterpolator(LINEAR_INTERPOLATOR);
slideX.setInterpolator(PATH_INTERPOLATOR);
slideY.setInterpolator(PATH_INTERPOLATOR);
expand.start(); expand.start();
slideX.start();
slideY.start();
if (!mSparkle.isRunning()) {
mSparkle.start();
mActiveAnimations.add(mSparkle);
}
mActiveAnimations.add(expand); mActiveAnimations.add(expand);
mActiveAnimations.add(slideX);
mActiveAnimations.add(slideY);
} }
private void enterSoftware() { private void enterSoftware() {
ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f); ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f);
ValueAnimator slideX = ValueAnimator.ofFloat(
mProperties.getX(), mWidth / 2);
ValueAnimator slideY = ValueAnimator.ofFloat(
mProperties.getY(), mHeight / 2);
expand.addUpdateListener(updatedAnimation -> { expand.addUpdateListener(updatedAnimation -> {
notifyUpdate(); notifyUpdate();
mProperties.getShader().setProgress((Float) expand.getAnimatedValue()); mProperties.getShader().setProgress((Float) expand.getAnimatedValue());
}); });
expand.addListener(new AnimatorListener(this)); slideX.addUpdateListener(anim -> {
expand.setInterpolator(LINEAR_INTERPOLATOR); float x = (float) slideX.getAnimatedValue();
expand.start(); float y = (float) slideY.getAnimatedValue();
mActiveAnimations.add(expand); mProperties.setOrigin(x, y);
mProperties.getShader().setOrigin(x, y);
});
startAnimation(expand, slideX, slideY);
} }
@NonNull AnimationProperties<Float, Paint> getProperties() { @NonNull AnimationProperties<Float, Paint> getProperties() {
return mProperties; return mProperties;
} }
@NonNull AnimationProperties getCanvasProperties() { @NonNull
AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> getCanvasProperties() {
if (mCanvasProperties == null) { if (mCanvasProperties == null) {
mCanvasProperties = new AnimationProperties<>( mCanvasProperties = new AnimationProperties<>(
CanvasProperty.createFloat(mProperties.getX()), CanvasProperty.createFloat(mProperties.getX()),
@@ -209,6 +249,7 @@ public final class RippleAnimationSession {
AnimatorListener(RippleAnimationSession session) { AnimatorListener(RippleAnimationSession session) {
mSession = session; mSession = session;
} }
@Override @Override
public void onAnimationStart(Animator animation) { public void onAnimationStart(Animator animation) {
@@ -231,21 +272,12 @@ public final class RippleAnimationSession {
} }
static class AnimationProperties<FloatType, PaintType> { static class AnimationProperties<FloatType, PaintType> {
private final FloatType mY; private final FloatType mProgress;
private FloatType mProgress; private final FloatType mMaxRadius;
private FloatType mMaxRadius;
private final PaintType mPaint; private final PaintType mPaint;
private final FloatType mX;
private final RippleShader mShader; private final RippleShader mShader;
private Runnable mOnChange; private FloatType mX;
private FloatType mY;
private void onChange() {
if (mOnChange != null) mOnChange.run();
}
private void setOnChange(Runnable onChange) {
mOnChange = onChange;
}
AnimationProperties(FloatType x, FloatType y, FloatType maxRadius, AnimationProperties(FloatType x, FloatType y, FloatType maxRadius,
PaintType paint, FloatType progress, RippleShader shader) { PaintType paint, FloatType progress, RippleShader shader) {
@@ -261,6 +293,11 @@ public final class RippleAnimationSession {
return mProgress; return mProgress;
} }
void setOrigin(FloatType x, FloatType y) {
mX = x;
mY = y;
}
FloatType getX() { FloatType getX() {
return mX; return mX;
} }

View File

@@ -48,6 +48,7 @@ import android.graphics.RecordingCanvas;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Shader; import android.graphics.Shader;
import android.os.Build; import android.os.Build;
import android.os.SystemProperties;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.animation.LinearInterpolator; import android.view.animation.LinearInterpolator;
@@ -159,6 +160,9 @@ public class RippleDrawable extends LayerDrawable {
/** The maximum number of ripples supported. */ /** The maximum number of ripples supported. */
private static final int MAX_RIPPLES = 10; private static final int MAX_RIPPLES = 10;
private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
/** Temporary flag for teamfood. **/
private static final boolean FORCE_PATTERNED_STYLE =
SystemProperties.getBoolean("persist.material.patternedripple", false);
private final Rect mTempRect = new Rect(); private final Rect mTempRect = new Rect();
@@ -361,7 +365,9 @@ public class RippleDrawable extends LayerDrawable {
} }
} else { } else {
if (focused || hovered) { if (focused || hovered) {
enterPatternedBackgroundAnimation(focused, hovered); if (!pressed) {
enterPatternedBackgroundAnimation(focused, hovered);
}
} else { } else {
exitPatternedBackgroundAnimation(); exitPatternedBackgroundAnimation();
} }
@@ -571,7 +577,10 @@ public class RippleDrawable extends LayerDrawable {
mState.mMaxRadius = a.getDimensionPixelSize( mState.mMaxRadius = a.getDimensionPixelSize(
R.styleable.RippleDrawable_radius, mState.mMaxRadius); R.styleable.RippleDrawable_radius, mState.mMaxRadius);
mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle, STYLE_SOLID); if (!FORCE_PATTERNED_STYLE) {
mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle,
mState.mRippleStyle);
}
} }
private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
@@ -812,21 +821,25 @@ public class RippleDrawable extends LayerDrawable {
} }
private void drawPatterned(@NonNull Canvas canvas) { private void drawPatterned(@NonNull Canvas canvas) {
final Rect bounds = getBounds(); final Rect bounds = getDirtyBounds();
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
boolean useCanvasProps = shouldUseCanvasProps(canvas); boolean useCanvasProps = shouldUseCanvasProps(canvas);
boolean changedHotspotBounds = !bounds.equals(mHotspotBounds); boolean changedHotspotBounds = !bounds.equals(mHotspotBounds);
if (isBounded()) { if (isBounded()) {
canvas.clipRect(mHotspotBounds); canvas.clipRect(bounds);
} }
float x, y; float x, y, w, h;
if (changedHotspotBounds) { if (changedHotspotBounds) {
x = mHotspotBounds.exactCenterX(); x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY(); y = mHotspotBounds.exactCenterY();
w = mHotspotBounds.width();
h = mHotspotBounds.height();
useCanvasProps = false; useCanvasProps = false;
} else { } else {
x = mPendingX; x = mPendingX;
y = mPendingY; y = mPendingY;
w = bounds.width();
h = bounds.height();
} }
boolean shouldAnimate = mRippleActive; boolean shouldAnimate = mRippleActive;
boolean shouldExit = mExitingAnimation; boolean shouldExit = mExitingAnimation;
@@ -837,9 +850,9 @@ public class RippleDrawable extends LayerDrawable {
drawPatternedBackground(canvas); drawPatternedBackground(canvas);
if (shouldAnimate && mRunningAnimations.size() <= MAX_RIPPLES) { if (shouldAnimate && mRunningAnimations.size() <= MAX_RIPPLES) {
RippleAnimationSession.AnimationProperties<Float, Paint> properties = RippleAnimationSession.AnimationProperties<Float, Paint> properties =
createAnimationProperties(x, y); createAnimationProperties(x, y, w, h);
mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps) mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps, w, h)
.setOnAnimationUpdated(useCanvasProps ? null : () -> invalidateSelf(false)) .setOnAnimationUpdated(() -> invalidateSelf(false))
.setOnSessionEnd(session -> { .setOnSessionEnd(session -> {
mRunningAnimations.remove(session); mRunningAnimations.remove(session);
}) })
@@ -864,20 +877,8 @@ public class RippleDrawable extends LayerDrawable {
} else { } else {
RippleAnimationSession.AnimationProperties<Float, Paint> p = RippleAnimationSession.AnimationProperties<Float, Paint> p =
s.getProperties(); s.getProperties();
float posX, posY;
if (changedHotspotBounds) {
posX = x;
posY = y;
if (p.getPaint().getShader() instanceof RippleShader) {
RippleShader shader = (RippleShader) p.getPaint().getShader();
shader.setOrigin(posX, posY);
}
} else {
posX = p.getX();
posY = p.getY();
}
float radius = p.getMaxRadius(); float radius = p.getMaxRadius();
canvas.drawCircle(posX, posY, radius, p.getPaint()); canvas.drawCircle(p.getX(), p.getY(), radius, p.getPaint());
} }
} }
canvas.restoreToCount(saveCount); canvas.restoreToCount(saveCount);
@@ -905,14 +906,13 @@ public class RippleDrawable extends LayerDrawable {
private float computeRadius() { private float computeRadius() {
Rect b = getDirtyBounds(); Rect b = getDirtyBounds();
float gap = 0; float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2;
float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2 + gap;
return radius; return radius;
} }
@NonNull @NonNull
private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties( private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties(
float x, float y) { float x, float y, float w, float h) {
Paint p = new Paint(mRipplePaint); Paint p = new Paint(mRipplePaint);
float radius = mState.mMaxRadius; float radius = mState.mMaxRadius;
RippleAnimationSession.AnimationProperties<Float, Paint> properties; RippleAnimationSession.AnimationProperties<Float, Paint> properties;
@@ -920,19 +920,19 @@ public class RippleDrawable extends LayerDrawable {
int color = mMaskColorFilter == null int color = mMaskColorFilter == null
? mState.mColor.getColorForState(getState(), Color.BLACK) ? mState.mColor.getColorForState(getState(), Color.BLACK)
: mMaskColorFilter.getColor(); : mMaskColorFilter.getColor();
color = color | 0xFF000000;
shader.setColor(color); shader.setColor(color);
shader.setOrigin(x, y); shader.setOrigin(x, y);
shader.setResolution(w, h);
shader.setSecondsOffset(0);
shader.setRadius(radius); shader.setRadius(radius);
shader.setProgress(.0f); shader.setProgress(.0f);
properties = new RippleAnimationSession.AnimationProperties<>( properties = new RippleAnimationSession.AnimationProperties<>(
x, y, radius, p, 0f, x, y, radius, p, 0f,
shader); shader);
if (mMaskShader == null) { if (mMaskShader == null) {
shader.setHasMask(false); shader.setShader(null);
} else { } else {
shader.setShader(mMaskShader); shader.setShader(mMaskShader);
shader.setHasMask(true);
} }
p.setShader(shader); p.setShader(shader);
p.setColorFilter(null); p.setColorFilter(null);
@@ -1160,7 +1160,7 @@ public class RippleDrawable extends LayerDrawable {
// The ripple timing depends on the paint's alpha value, so we need // The ripple timing depends on the paint's alpha value, so we need
// to push just the alpha channel into the paint and let the filter // to push just the alpha channel into the paint and let the filter
// handle the full-alpha color. // handle the full-alpha color.
int maskColor = color | 0xFF000000; int maskColor = mState.mRippleStyle == STYLE_PATTERNED ? color : color | 0xFF000000;
if (mMaskColorFilter.getColor() != maskColor) { if (mMaskColorFilter.getColor() != maskColor) {
mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode()); mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode());
} }
@@ -1276,7 +1276,7 @@ public class RippleDrawable extends LayerDrawable {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
int mMaxRadius = RADIUS_AUTO; int mMaxRadius = RADIUS_AUTO;
int mRippleStyle = STYLE_SOLID; int mRippleStyle = FORCE_PATTERNED_STYLE ? STYLE_PATTERNED : STYLE_SOLID;
public RippleState(LayerState orig, RippleDrawable owner, Resources res) { public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
super(orig, owner, res); super(orig, owner, res);

View File

@@ -17,58 +17,153 @@
package android.graphics.drawable; package android.graphics.drawable;
import android.annotation.ColorInt; import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.RuntimeShader; import android.graphics.RuntimeShader;
import android.graphics.Shader; import android.graphics.Shader;
final class RippleShader extends RuntimeShader { final class RippleShader extends RuntimeShader {
private static final String SHADER = "uniform float2 in_origin;\n" private static final String SHADER_UNIFORMS = "uniform vec2 in_origin;\n"
+ "uniform float in_maxRadius;\n"
+ "uniform float in_progress;\n" + "uniform float in_progress;\n"
+ "uniform float in_maxRadius;\n"
+ "uniform vec2 in_resolution;\n"
+ "uniform float in_hasMask;\n" + "uniform float in_hasMask;\n"
+ "uniform float4 in_color;\n" + "uniform float in_secondsOffset;\n"
+ "uniform shader in_shader;\n" + "uniform vec4 in_color;\n"
+ "float dist2(float2 p0, float2 pf) { return sqrt((pf.x - p0.x) * (pf.x - p0.x) + " + "uniform shader in_shader;\n";
+ "(pf.y - p0.y) * (pf.y - p0.y)); }\n" private static final String SHADER_LIB =
+ "float mod2(float a, float b) { return a - (b * floor(a / b)); }\n" "float triangleNoise(vec2 n) {\n"
+ "float rand(float2 src) { return fract(sin(dot(src.xy, float2(12.9898, 78.233))) * " + " n = fract(n * vec2(5.3987, 5.4421));\n"
+ "43758.5453123); }\n" + " n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));\n"
+ "float4 main(float2 p)\n" + " float xy = n.x * n.y;\n"
+ "{\n" + " return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;\n"
+ " float fraction = in_progress;\n" + "}"
+ " float2 fragCoord = p;//sk_FragCoord.xy;\n" + "const float PI = 3.1415926535897932384626;\n"
+ " float maxDist = in_maxRadius;\n" + "\n"
+ " float fragDist = dist2(in_origin, fragCoord.xy);\n" + "float threshold(float v, float l, float h) {\n"
+ " float circleRadius = maxDist * fraction;\n" + " return step(l, v) * (1.0 - step(h, v));\n"
+ " float colorVal = (fragDist - circleRadius) / maxDist;\n" + "}\n"
+ " float d = fragDist < circleRadius \n" + "\n"
+ " ? 1. - abs(colorVal * 3. * smoothstep(0., 1., fraction)) \n" + "float sparkles(vec2 uv, float t) {\n"
+ " : 1. - abs(colorVal * 5.);\n" + " float n = triangleNoise(uv);\n"
+ " d = smoothstep(0., 1., d);\n" + " float s = 0.0;\n"
+ " float divider = 2.;\n" + " for (float i = 0; i < 4; i += 1) {\n"
+ " float x = floor(fragCoord.x / divider);\n" + " float l = i * 0.25;\n"
+ " float y = floor(fragCoord.y / divider);\n" + " float h = l + 0.025;\n"
+ " float density = .95;\n" + " float o = abs(sin(0.1 * PI * (t + i)));\n"
+ " d = rand(float2(x, y)) > density ? d : d * .2;\n" + " s += threshold(n + o, l, h);\n"
+ " d = d * rand(float2(fraction, x * y));\n" + " }\n"
+ " float alpha = 1. - pow(fraction, 2.);\n" + " return saturate(s);\n"
+ " if (in_hasMask != 0.) {return sample(in_shader).a * in_color * d * alpha;}\n" + "}\n"
+ " return in_color * d * alpha;\n" + "\n"
+ "float softCircle(vec2 uv, vec2 xy, float radius, float blur) {\n"
+ " float blurHalf = blur * 0.5;\n"
+ " float d = distance(uv, xy);\n"
+ " return 1. - smoothstep(1. - blurHalf, 1. + blurHalf, d / radius);\n"
+ "}\n"
+ "\n"
+ "float softRing(vec2 uv, vec2 xy, float radius, float blur) {\n"
+ " float thickness = 0.4;\n"
+ " float circle_outer = softCircle(uv, xy, radius + thickness * 0.5, blur);\n"
+ " float circle_inner = softCircle(uv, xy, radius - thickness * 0.5, blur);\n"
+ " return circle_outer - circle_inner;\n"
+ "}\n"
+ "\n"
+ "struct Viewport {\n"
+ " float aspect;\n"
+ " vec2 uv;\n"
+ " vec2 resolution_pixels;\n"
+ "};\n"
+ "\n"
+ "Viewport getViewport(vec2 frag_coord, vec2 resolution_pixels) {\n"
+ " Viewport v;\n"
+ " v.aspect = resolution_pixels.y / resolution_pixels.x;\n"
+ " v.uv = frag_coord / resolution_pixels;\n"
+ " v.uv.y = (1.0 - v.uv.y) * v.aspect;\n"
+ " v.resolution_pixels = resolution_pixels;\n"
+ " return v;\n"
+ "}\n"
+ "\n"
+ "vec2 getTouch(vec2 touch_position_pixels, Viewport viewport) {\n"
+ " vec2 touch = touch_position_pixels / viewport.resolution_pixels;\n"
+ " touch.y *= viewport.aspect;\n"
+ " return touch;\n"
+ "}\n"
+ "\n"
+ "struct Wave {\n"
+ " float ring;\n"
+ " float circle;\n"
+ "};\n"
+ "\n"
+ "Wave getWave(Viewport viewport, vec2 touch, float progress) {\n"
+ " float fade = pow((clamp(progress, 0.8, 1.0)), 8.);\n"
+ " Wave w;\n"
+ " w.ring = max(softRing(viewport.uv, touch, progress, 0.45) - fade, 0.);\n"
+ " w.circle = softCircle(viewport.uv, touch, 2.0 * progress, 0.2) - progress;\n"
+ " return w;\n"
+ "}\n"
+ "\n"
+ "vec4 getRipple(vec4 color, float loudness, float sparkle, Wave wave) {\n"
+ " float alpha = wave.ring * sparkle * loudness\n"
+ " + wave.circle * color.a;\n"
+ " return vec4(color.rgb, saturate(alpha));\n"
+ "}\n"
+ "\n"
+ "float getRingMask(vec2 frag, vec2 center, float r, float progress) {\n"
+ " float dist = distance(frag, center);\n"
+ " float expansion = r * .6;\n"
+ " r = r * min(1.,progress);\n"
+ " float minD = max(r - expansion, 0.);\n"
+ " float maxD = r + expansion;\n"
+ " if (dist > maxD || dist < minD) return .0;\n"
+ " return min(maxD - dist, dist - minD) / expansion; \n"
+ "}\n"
+ "\n"
+ "float subProgress(float start, float end, float progress) {\n"
+ " float sub = clamp(progress, start, end);\n"
+ " return (sub - start) / (end - start); \n"
+ "}\n"; + "}\n";
private static final String SHADER_MAIN = "vec4 main(vec2 p) {\n"
+ " float fadeIn = subProgress(0., 0.175, in_progress);\n"
+ " float fadeOutNoise = subProgress(0.375, 1., in_progress);\n"
+ " float fadeOutRipple = subProgress(0.375, 0.75, in_progress);\n"
+ " Viewport vp = getViewport(p, in_resolution);\n"
+ " vec2 touch = getTouch(in_origin, vp);\n"
+ " Wave w = getWave(vp, touch, in_progress * 0.25);\n"
+ " float ring = getRingMask(p, in_origin, in_maxRadius, fadeIn);\n"
+ " float alpha = min(fadeIn, 1. - fadeOutNoise);\n"
+ " float sparkle = sparkles(p, in_progress * 0.25 + in_secondsOffset)\n"
+ " * ring * alpha;\n"
+ " vec4 r = getRipple(in_color, 1., sparkle, w);\n"
+ " float fade = min(fadeIn, 1.-fadeOutRipple);\n"
+ " vec4 circle = vec4(in_color.rgb, softCircle(p, in_origin, in_maxRadius "
+ " * fadeIn, 0.2) * fade * in_color.a);\n"
+ " float mask = in_hasMask == 1. ? sample(in_shader).a > 0. ? 1. : 0. : 1.;\n"
+ " return mix(circle, vec4(1.), sparkle * mask);\n"
+ "}";
private static final String SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN;
RippleShader() { RippleShader() {
super(SHADER, false); super(SHADER, false);
} }
public void setShader(@NonNull Shader s) { public void setShader(Shader shader) {
setInputShader("in_shader", s); if (shader != null) {
setInputShader("in_shader", shader);
}
setUniform("in_hasMask", shader == null ? 0 : 1);
} }
public void setRadius(float radius) { public void setRadius(float radius) {
setUniform("in_maxRadius", radius); setUniform("in_maxRadius", radius);
} }
/**
* Continuous offset used as noise phase.
*/
public void setSecondsOffset(float t) {
setUniform("in_secondsOffset", t);
}
public void setOrigin(float x, float y) { public void setOrigin(float x, float y) {
setUniform("in_origin", new float[] {x, y}); setUniform("in_origin", new float[] {x, y});
} }
@@ -77,13 +172,16 @@ final class RippleShader extends RuntimeShader {
setUniform("in_progress", progress); setUniform("in_progress", progress);
} }
public void setHasMask(boolean hasMask) { /**
setUniform("in_hasMask", hasMask ? 1 : 0); * Color of the circle that's under the sparkles. Sparkles will always be white.
} */
public void setColor(@ColorInt int colorIn) { public void setColor(@ColorInt int colorIn) {
Color color = Color.valueOf(colorIn); Color color = Color.valueOf(colorIn);
this.setUniform("in_color", new float[] {color.red(), this.setUniform("in_color", new float[] {color.red(),
color.green(), color.blue(), color.alpha()}); color.green(), color.blue(), color.alpha()});
} }
public void setResolution(float w, float h) {
setUniform("in_resolution", w, h);
}
} }