blob: db7c1fd86be0b4cd5be4c94a269b6e970b956af5 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.ripple
import android.graphics.PointF
import android.graphics.RuntimeShader
import android.util.MathUtils
/**
* Shader class that renders an expanding ripple effect. The ripple contains three elements:
*
* 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away
* 2. an expanding ring that appears throughout the effect
* 3. an expanding ring-shaped area that reveals noise over #2.
*
* The ripple shader will be default to the circle shape if not specified.
*
* Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java.
*/
class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) :
RuntimeShader(buildShader(rippleShape)) {
/** Shapes that the [RippleShader] supports. */
enum class RippleShape {
CIRCLE,
ROUNDED_BOX,
ELLIPSE
}
//language=AGSL
companion object {
private const val SHADER_UNIFORMS = """uniform vec2 in_center;
uniform vec2 in_size;
uniform float in_progress;
uniform float in_cornerRadius;
uniform float in_thickness;
uniform float in_time;
uniform float in_distort_radial;
uniform float in_distort_xy;
uniform float in_fadeSparkle;
uniform float in_fadeFill;
uniform float in_fadeRing;
uniform float in_blur;
uniform float in_pixelDensity;
layout(color) uniform vec4 in_color;
uniform float in_sparkle_strength;"""
private const val SHADER_CIRCLE_MAIN = """vec4 main(vec2 p) {
vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
float radius = in_size.x * 0.5;
float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur);
float inside = soften(sdCircle(p_distorted-in_center, radius * 1.2), in_blur);
float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
* (1.-sparkleRing) * in_fadeSparkle;
float rippleInsideAlpha = (1.-inside) * in_fadeFill;
float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
vec4 ripple = in_color * rippleAlpha;
return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
}
"""
private const val SHADER_ROUNDED_BOX_MAIN = """vec4 main(vec2 p) {
float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius,
in_thickness), in_blur);
float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius),
in_blur);
float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
* (1.-sparkleRing) * in_fadeSparkle;
float rippleInsideAlpha = (1.-inside) * in_fadeFill;
float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
vec4 ripple = in_color * rippleAlpha;
return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
}
"""
private const val SHADER_ELLIPSE_MAIN = """vec4 main(vec2 p) {
vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur);
float inside = soften(sdEllipse(p_distorted-in_center, in_size * 1.2), in_blur);
float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
* (1.-sparkleRing) * in_fadeSparkle;
float rippleInsideAlpha = (1.-inside) * in_fadeFill;
float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
vec4 ripple = in_color * rippleAlpha;
return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
}
"""
private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.CIRCLE_SDF +
SHADER_CIRCLE_MAIN
private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS +
RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
SdfShaderLibrary.ROUNDED_BOX_SDF + SHADER_ROUNDED_BOX_MAIN
private const val ELLIPSE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.ELLIPSE_SDF +
SHADER_ELLIPSE_MAIN
private fun buildShader(rippleShape: RippleShape): String =
when (rippleShape) {
RippleShape.CIRCLE -> CIRCLE_SHADER
RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER
RippleShape.ELLIPSE -> ELLIPSE_SHADER
}
private fun subProgress(start: Float, end: Float, progress: Float): Float {
val min = Math.min(start, end)
val max = Math.max(start, end)
val sub = Math.min(Math.max(progress, min), max)
return (sub - start) / (end - start)
}
}
/**
* Sets the center position of the ripple.
*/
fun setCenter(x: Float, y: Float) {
setFloatUniform("in_center", x, y)
}
/** Max width of the ripple. */
private var maxSize: PointF = PointF()
fun setMaxSize(width: Float, height: Float) {
maxSize.x = width
maxSize.y = height
}
/**
* Progress of the ripple. Float value between [0, 1].
*/
var progress: Float = 0.0f
set(value) {
field = value
setFloatUniform("in_progress", value)
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
setFloatUniform("in_size", /* width= */ maxSize.x * curvedProg,
/* height= */ maxSize.y * curvedProg)
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
// radius should not exceed width and height values.
setFloatUniform("in_cornerRadius",
Math.min(maxSize.x, maxSize.y) * curvedProg)
setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
val fadeIn = subProgress(0f, 0.1f, value)
val fadeOutNoise = subProgress(0.4f, 1f, value)
var fadeOutRipple = 0f
var fadeFill = 0f
if (!rippleFill) {
fadeFill = subProgress(0f, 0.6f, value)
fadeOutRipple = subProgress(0.3f, 1f, value)
}
setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
setFloatUniform("in_fadeFill", 1 - fadeFill)
setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
}
/**
* Play time since the start of the effect.
*/
var time: Float = 0.0f
set(value) {
field = value
setFloatUniform("in_time", value)
}
/**
* A hex value representing the ripple color, in the format of ARGB
*/
var color: Int = 0xffffff
set(value) {
field = value
setColorUniform("in_color", value)
}
/**
* Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus
* with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1
* it's opaque white and looks the most grainy.
*/
var sparkleStrength: Float = 0.0f
set(value) {
field = value
setFloatUniform("in_sparkle_strength", value)
}
/**
* Distortion strength of the ripple. Expected value between[0, 1].
*/
var distortionStrength: Float = 0.0f
set(value) {
field = value
setFloatUniform("in_distort_radial", 75 * progress * value)
setFloatUniform("in_distort_xy", 75 * value)
}
var pixelDensity: Float = 1.0f
set(value) {
field = value
setFloatUniform("in_pixelDensity", value)
}
/**
* True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
* False for a ring effect.
*/
var rippleFill: Boolean = false
}