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 package com.android.systemui.surfaceeffects.ripple 17 18 import android.graphics.RuntimeShader 19 import android.util.Log 20 import android.util.MathUtils 21 import androidx.annotation.VisibleForTesting 22 import com.android.systemui.animation.Interpolators 23 import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary 24 import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary 25 26 /** 27 * Shader class that renders an expanding ripple effect. The ripple contains three elements: 28 * 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away 29 * 2. an expanding ring that appears throughout the effect 30 * 3. an expanding ring-shaped area that reveals noise over #2. 31 * 32 * The ripple shader will be default to the circle shape if not specified. 33 * 34 * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java. 35 */ 36 class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : 37 RuntimeShader(buildShader(rippleShape)) { 38 39 /** Shapes that the [RippleShader] supports. */ 40 enum class RippleShape { 41 CIRCLE, 42 ROUNDED_BOX, 43 ELLIPSE 44 } 45 // language=AGSL 46 companion object { 47 private val TAG = RippleShader::class.simpleName 48 49 // Default fade in/ out values. The value range is [0,1]. 50 const val DEFAULT_FADE_IN_START = 0f 51 const val DEFAULT_FADE_OUT_END = 1f 52 53 const val DEFAULT_BASE_RING_FADE_IN_END = 0.1f 54 const val DEFAULT_BASE_RING_FADE_OUT_START = 0.3f 55 56 const val DEFAULT_SPARKLE_RING_FADE_IN_END = 0.1f 57 const val DEFAULT_SPARKLE_RING_FADE_OUT_START = 0.4f 58 59 const val DEFAULT_CENTER_FILL_FADE_IN_END = 0f 60 const val DEFAULT_CENTER_FILL_FADE_OUT_START = 0f 61 const val DEFAULT_CENTER_FILL_FADE_OUT_END = 0.6f 62 63 private const val SHADER_UNIFORMS = 64 """ 65 uniform vec2 in_center; 66 uniform vec2 in_size; 67 uniform float in_cornerRadius; 68 uniform float in_thickness; 69 uniform float in_time; 70 uniform float in_distort_radial; 71 uniform float in_distort_xy; 72 uniform float in_fadeSparkle; 73 uniform float in_fadeFill; 74 uniform float in_fadeRing; 75 uniform float in_blur; 76 uniform float in_pixelDensity; 77 layout(color) uniform vec4 in_color; 78 uniform float in_sparkle_strength; 79 """ 80 81 private const val SHADER_CIRCLE_MAIN = 82 """ 83 vec4 main(vec2 p) { 84 vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); 85 float radius = in_size.x * 0.5; 86 float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur); 87 float inside = soften(sdCircle(p_distorted-in_center, radius * 1.25), in_blur); 88 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) 89 * (1.-sparkleRing) * in_fadeSparkle; 90 91 float rippleInsideAlpha = (1.-inside) * in_fadeFill; 92 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; 93 float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; 94 vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; 95 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); 96 } 97 """ 98 99 private const val SHADER_ROUNDED_BOX_MAIN = 100 """ 101 vec4 main(vec2 p) { 102 float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius, 103 in_thickness), in_blur); 104 float inside = soften(sdRoundedBox(p-in_center, in_size * 1.25, in_cornerRadius), 105 in_blur); 106 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) 107 * (1.-sparkleRing) * in_fadeSparkle; 108 109 float rippleInsideAlpha = (1.-inside) * in_fadeFill; 110 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; 111 float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; 112 vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; 113 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); 114 } 115 """ 116 117 private const val SHADER_ELLIPSE_MAIN = 118 """ 119 vec4 main(vec2 p) { 120 vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); 121 122 float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur); 123 float inside = soften(sdEllipse(p_distorted-in_center, in_size * 1.2), in_blur); 124 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) 125 * (1.-sparkleRing) * in_fadeSparkle; 126 127 float rippleInsideAlpha = (1.-inside) * in_fadeFill; 128 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; 129 float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; 130 vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; 131 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); 132 } 133 """ 134 135 private const val CIRCLE_SHADER = 136 SHADER_UNIFORMS + 137 ShaderUtilLibrary.SHADER_LIB + 138 SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + 139 SdfShaderLibrary.CIRCLE_SDF + 140 SHADER_CIRCLE_MAIN 141 private const val ROUNDED_BOX_SHADER = 142 SHADER_UNIFORMS + 143 ShaderUtilLibrary.SHADER_LIB + 144 SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + 145 SdfShaderLibrary.ROUNDED_BOX_SDF + 146 SHADER_ROUNDED_BOX_MAIN 147 private const val ELLIPSE_SHADER = 148 SHADER_UNIFORMS + 149 ShaderUtilLibrary.SHADER_LIB + 150 SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + 151 SdfShaderLibrary.ELLIPSE_SDF + 152 SHADER_ELLIPSE_MAIN 153 buildShadernull154 private fun buildShader(rippleShape: RippleShape): String = 155 when (rippleShape) { 156 RippleShape.CIRCLE -> CIRCLE_SHADER 157 RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER 158 RippleShape.ELLIPSE -> ELLIPSE_SHADER 159 } 160 subProgressnull161 private fun subProgress(start: Float, end: Float, progress: Float): Float { 162 // Avoid division by 0. 163 if (start == end) { 164 // If start and end are the same and progress has exceeded the start/ end point, 165 // treat it as 1, otherwise 0. 166 return if (progress > start) 1f else 0f 167 } 168 169 val min = Math.min(start, end) 170 val max = Math.max(start, end) 171 val sub = Math.min(Math.max(progress, min), max) 172 return (sub - start) / (end - start) 173 } 174 getFadenull175 private fun getFade(fadeParams: FadeParams, rawProgress: Float): Float { 176 val fadeIn = subProgress(fadeParams.fadeInStart, fadeParams.fadeInEnd, rawProgress) 177 val fadeOut = 178 1f - subProgress(fadeParams.fadeOutStart, fadeParams.fadeOutEnd, rawProgress) 179 180 return Math.min(fadeIn, fadeOut) 181 } 182 } 183 184 /** Sets the center position of the ripple. */ setCenternull185 fun setCenter(x: Float, y: Float) { 186 setFloatUniform("in_center", x, y) 187 } 188 189 /** 190 * Blur multipliers for the ripple. 191 * 192 * <p>It interpolates from [blurStart] to [blurEnd] based on the [progress]. Increase number to 193 * add more blur. 194 */ 195 var blurStart: Float = 1.25f 196 var blurEnd: Float = 0.5f 197 198 /** Size of the ripple. */ 199 val rippleSize = RippleSize() 200 201 /** 202 * Linear progress of the ripple. Float value between [0, 1]. 203 * 204 * <p>Note that the progress here is expected to be linear without any curve applied. 205 */ 206 var rawProgress: Float = 0.0f 207 set(value) { 208 field = value 209 progress = Interpolators.STANDARD.getInterpolation(value) 210 211 setFloatUniform("in_fadeSparkle", getFade(sparkleRingFadeParams, value)) 212 setFloatUniform("in_fadeRing", getFade(baseRingFadeParams, value)) 213 setFloatUniform("in_fadeFill", getFade(centerFillFadeParams, value)) 214 } 215 216 /** Progress with Standard easing curve applied. */ 217 private var progress: Float = 0.0f 218 set(value) { 219 field = value 220 221 rippleSize.update(value) 222 223 setFloatUniform("in_size", rippleSize.currentWidth, rippleSize.currentHeight) 224 setFloatUniform("in_thickness", rippleSize.currentHeight * 0.5f) 225 // Corner radius is always max of the min between the current width and height. 226 setFloatUniform( 227 "in_cornerRadius", 228 Math.min(rippleSize.currentWidth, rippleSize.currentHeight) 229 ) 230 231 setFloatUniform("in_blur", MathUtils.lerp(blurStart, blurEnd, value)) 232 } 233 234 /** Play time since the start of the effect. */ 235 var time: Float = 0.0f 236 set(value) { 237 field = value 238 setFloatUniform("in_time", value) 239 } 240 241 /** A hex value representing the ripple color, in the format of ARGB */ 242 var color: Int = 0xffffff 243 set(value) { 244 field = value 245 setColorUniform("in_color", value) 246 } 247 248 /** 249 * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with 250 * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's 251 * opaque white and looks the most grainy. 252 */ 253 var sparkleStrength: Float = 0.0f 254 set(value) { 255 field = value 256 setFloatUniform("in_sparkle_strength", value) 257 } 258 259 /** Distortion strength of the ripple. Expected value between[0, 1]. */ 260 var distortionStrength: Float = 0.0f 261 set(value) { 262 field = value 263 setFloatUniform("in_distort_radial", 75 * rawProgress * value) 264 setFloatUniform("in_distort_xy", 75 * value) 265 } 266 267 /** 268 * Pixel density of the screen that the effects are rendered to. 269 * 270 * <p>This value should come from [resources.displayMetrics.density]. 271 */ 272 var pixelDensity: Float = 1.0f 273 set(value) { 274 field = value 275 setFloatUniform("in_pixelDensity", value) 276 } 277 278 /** Parameters that are used to fade in/ out of the sparkle ring. */ 279 val sparkleRingFadeParams = 280 FadeParams( 281 DEFAULT_FADE_IN_START, 282 DEFAULT_SPARKLE_RING_FADE_IN_END, 283 DEFAULT_SPARKLE_RING_FADE_OUT_START, 284 DEFAULT_FADE_OUT_END 285 ) 286 287 /** 288 * Parameters that are used to fade in/ out of the base ring. 289 * 290 * <p>Note that the shader draws the sparkle ring on top of the base ring. 291 */ 292 val baseRingFadeParams = 293 FadeParams( 294 DEFAULT_FADE_IN_START, 295 DEFAULT_BASE_RING_FADE_IN_END, 296 DEFAULT_BASE_RING_FADE_OUT_START, 297 DEFAULT_FADE_OUT_END 298 ) 299 300 /** Parameters that are used to fade in/ out of the center fill. */ 301 val centerFillFadeParams = 302 FadeParams( 303 DEFAULT_FADE_IN_START, 304 DEFAULT_CENTER_FILL_FADE_IN_END, 305 DEFAULT_CENTER_FILL_FADE_OUT_START, 306 DEFAULT_CENTER_FILL_FADE_OUT_END 307 ) 308 309 /** 310 * Parameters used for fade in and outs of the ripple. 311 * 312 * <p>Note that all the fade in/ outs are "linear" progression. 313 * 314 * ``` 315 * (opacity) 316 * 1 317 * │ 318 * maxAlpha ← ―――――――――――― 319 * │ / \ 320 * │ / \ 321 * minAlpha ←――――/ \―――― (alpha change) 322 * │ 323 * │ 324 * 0 ―――↑―――↑―――――――――↑―――↑――――1 (progress) 325 * fadeIn fadeOut 326 * Start & End Start & End 327 * ``` 328 * 329 * <p>If no fade in/ out is needed, set [fadeInStart] and [fadeInEnd] to 0; [fadeOutStart] and 330 * [fadeOutEnd] to 1. 331 */ 332 data class FadeParams( 333 /** 334 * The starting point of the fade out which ends at [fadeInEnd], given that the animation 335 * goes from 0 to 1. 336 */ 337 var fadeInStart: Float = DEFAULT_FADE_IN_START, 338 /** 339 * The endpoint of the fade in when the fade in starts at [fadeInStart], given that the 340 * animation goes from 0 to 1. 341 */ 342 var fadeInEnd: Float, 343 /** 344 * The starting point of the fade out which ends at 1, given that the animation goes from 0 345 * to 1. 346 */ 347 var fadeOutStart: Float, 348 349 /** The endpoint of the fade out, given that the animation goes from 0 to 1. */ 350 var fadeOutEnd: Float = DEFAULT_FADE_OUT_END, 351 ) 352 353 /** 354 * Desired size of the ripple at a point t in [progress]. 355 * 356 * <p>Note that [progress] is curved and normalized. Below is an example usage: 357 * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 0.2f, width= 500f, height= 358 * 700f), SizeAtProgress(t= 1f, width= 100f, height= 300f) 359 * 360 * <p>For simple ripple effects, you will want to use [setMaxSize] as it is translated into: 361 * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 1f, width= maxWidth, height= 362 * maxHeight) 363 */ 364 data class SizeAtProgress( 365 /** Time t in [0,1] progress range. */ 366 var t: Float, 367 /** Target width size of the ripple at time [t]. */ 368 var width: Float, 369 /** Target height size of the ripple at time [t]. */ 370 var height: Float 371 ) 372 373 /** Updates and stores the ripple size. */ 374 inner class RippleSize { 375 @VisibleForTesting var sizes = mutableListOf<SizeAtProgress>() 376 @VisibleForTesting var currentSizeIndex = 0 377 @VisibleForTesting val initialSize = SizeAtProgress(0f, 0f, 0f) 378 379 var currentWidth: Float = 0f 380 private set 381 var currentHeight: Float = 0f 382 private set 383 384 /** 385 * Sets the max size of the ripple. 386 * 387 * <p>Use this if the ripple shape simply changes linearly. 388 */ setMaxSizenull389 fun setMaxSize(width: Float, height: Float) { 390 setSizeAtProgresses(initialSize, SizeAtProgress(1f, width, height)) 391 } 392 393 /** 394 * Sets the list of [sizes]. 395 * 396 * <p>Note that setting this clears the existing sizes. 397 */ setSizeAtProgressesnull398 fun setSizeAtProgresses(vararg sizes: SizeAtProgress) { 399 // Reset everything. 400 this.sizes.clear() 401 currentSizeIndex = 0 402 403 this.sizes.addAll(sizes) 404 this.sizes.sortBy { it.t } 405 } 406 407 /** 408 * Updates the current ripple size based on the progress. 409 * 410 * <p>Should be called when progress updates. 411 */ updatenull412 fun update(progress: Float) { 413 val targetIndex = updateTargetIndex(progress) 414 val prevIndex = Math.max(targetIndex - 1, 0) 415 416 val targetSize = sizes[targetIndex] 417 val prevSize = sizes[prevIndex] 418 419 val subProgress = subProgress(prevSize.t, targetSize.t, progress) 420 421 currentWidth = targetSize.width * subProgress + prevSize.width 422 currentHeight = targetSize.height * subProgress + prevSize.height 423 } 424 updateTargetIndexnull425 private fun updateTargetIndex(progress: Float): Int { 426 if (sizes.isEmpty()) { 427 // It could be empty on init. 428 if (progress > 0f) { 429 Log.e( 430 TAG, 431 "Did you forget to set the ripple size? Use [setMaxSize] or " + 432 "[setSizeAtProgresses] before playing the animation." 433 ) 434 } 435 // If there's no size is set, we set everything to 0 and return early. 436 setSizeAtProgresses(initialSize) 437 return currentSizeIndex 438 } 439 440 var candidate = sizes[currentSizeIndex] 441 442 while (progress > candidate.t) { 443 currentSizeIndex = Math.min(currentSizeIndex + 1, sizes.size - 1) 444 candidate = sizes[currentSizeIndex] 445 } 446 447 return currentSizeIndex 448 } 449 } 450 } 451