fix: fix gooey animation

This commit is contained in:
k1ngsterr1
2026-02-09 11:33:23 +05:00
parent 8dfcf1c410
commit 1139bd6be6
2 changed files with 171 additions and 102 deletions

View File

@@ -13,6 +13,20 @@ import kotlin.math.sqrt
*/
data class Point(var x: Float = 0f, var y: Float = 0f)
/**
* Reusable scratch points to avoid per-frame allocations during scroll.
*/
class MetaballWorkspace {
val p1 = Point()
val p2 = Point()
val p3 = Point()
val p4 = Point()
val h1 = Point()
val h2 = Point()
val h3 = Point()
val h4 = Point()
}
/**
* Creates a metaball path between two circles - exact port from Telegram's ProfileMetaballView.java
*
@@ -33,28 +47,18 @@ fun createMetaballPath(
radius1: Float,
radius2: Float,
v: Float,
path: Path
path: Path,
workspace: MetaballWorkspace = MetaballWorkspace()
): Boolean {
val HALF_PI = (Math.PI / 2).toFloat()
val HANDLE_SIZE = 20.0f
// Helper points for path calculation
val p1 = Point()
val p2 = Point()
val p3 = Point()
val p4 = Point()
val h1 = Point()
val h2 = Point()
val h3 = Point()
val h4 = Point()
val a1 = Point()
val a2 = Point()
val HANDLE_SIZE = 3.6f
// Distance between centers
val d = dist(c1, c2)
val maxDist = radius1 + radius2 * 2.25f
val maxDist = radius1 + radius2 * 2.32f
var u1 = 0f
var u2 = 0f
val connector = v.coerceIn(0f, 1f)
// Check if circles can be connected
if (radius1 == 0f || radius2 == 0f || d > maxDist || d <= abs(radius1 - radius2)) {
@@ -63,18 +67,29 @@ fun createMetaballPath(
// Calculate overlap angles
if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
val acosArg1 = ((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d)).coerceIn(-1f, 1f)
val acosArg2 = ((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d)).coerceIn(-1f, 1f)
u1 = acos(acosArg1)
u2 = acos(acosArg2)
}
val angleBetweenCenters = angle(c2, c1)
val maxSpread = acos((radius1 - radius2) / d)
val maxSpread = acos(((radius1 - radius2) / d).coerceIn(-1f, 1f))
// Calculate connection angles
val angle1 = angleBetweenCenters + u1 + (maxSpread - u1) * v
val angle2 = angleBetweenCenters - u1 - (maxSpread - u1) * v
val angle3 = angleBetweenCenters + Math.PI.toFloat() - u2 - (Math.PI.toFloat() - u2 - maxSpread) * v
val angle4 = angleBetweenCenters - Math.PI.toFloat() + u2 + (Math.PI.toFloat() - u2 - maxSpread) * v
val angle1 = angleBetweenCenters + u1 + (maxSpread - u1) * connector
val angle2 = angleBetweenCenters - u1 - (maxSpread - u1) * connector
val angle3 = angleBetweenCenters + Math.PI.toFloat() - u2 - (Math.PI.toFloat() - u2 - maxSpread) * connector
val angle4 = angleBetweenCenters - Math.PI.toFloat() + u2 + (Math.PI.toFloat() - u2 - maxSpread) * connector
val p1 = workspace.p1
val p2 = workspace.p2
val p3 = workspace.p3
val p4 = workspace.p4
val h1 = workspace.h1
val h2 = workspace.h2
val h3 = workspace.h3
val h4 = workspace.h4
// Get points on circle edges
getVector(c1, angle1, radius1, p1)
@@ -84,7 +99,7 @@ fun createMetaballPath(
// Calculate handle lengths for Bezier curves
val totalRadius = radius1 + radius2
val d2Base = minOf(v * HANDLE_SIZE, dist(p1, p3) / totalRadius)
val d2Base = minOf(connector * HANDLE_SIZE, dist(p1, p3) / totalRadius)
val d2 = d2Base * minOf(1f, d * 2 / totalRadius)
val r1 = radius1 * d2
@@ -100,13 +115,9 @@ fun createMetaballPath(
path.rewind()
path.moveTo(p1.x, p1.y)
path.cubicTo(h1.x, h1.y, h3.x, h3.y, p3.x, p3.y)
// Smooth curve around bottom circle
getVector(p3, angle3 + HALF_PI, r2 * 0.55f, a1)
getVector(p4, angle4 - HALF_PI, r2 * 0.55f, a2)
path.cubicTo(a1.x, a1.y, a2.x, a2.y, p4.x, p4.y)
path.lineTo(p4.x, p4.y)
path.cubicTo(h4.x, h4.y, h2.x, h2.y, p2.x, p2.y)
path.lineTo(p1.x, p1.y)
path.close()
return true

View File

@@ -108,8 +108,8 @@ object ProfileMetaballConstants {
const val MERGE_START_PROGRESS = 0.5f
const val MERGE_COMPLETE_PROGRESS = 0.99f // Let opacity handle fade, don't cut off early
// Blur settings for gooey effect (like Telegram's intensity = 15f)
const val BLUR_RADIUS = 15f
// Slightly stronger blur because our avatar base size is larger than Telegram's 96dp.
const val BLUR_RADIUS = 18f
// Blur range for avatar (like Telegram: 2 + (1-fraction) * 20)
const val BLUR_MIN = 2f
@@ -218,7 +218,8 @@ private fun computeAvatarState(
collapseProgress > 0f -> {
// Telegram: avatarScale = lerp(intoSize, 96, diff) / 100f
// vr = baseWidth * avatarScale * 0.5
val intoSize = notchRadiusPx * 2f // Target size = notch diameter
// Keep a minimum collapsed size so tiny punch-hole cameras don't make the blob too small.
val intoSize = max(avatarSizeMinPx, notchRadiusPx * 2f)
val normalSize = avatarSizeExpandedPx
val avatarWidth = lerpFloat(intoSize, normalSize, diff)
avatarWidth / 2f // radius = half of width
@@ -343,6 +344,12 @@ private fun lerpFloat(start: Float, stop: Float, fraction: Float): Float {
return start + (stop - start) * fraction
}
private fun easeOutCubic(value: Float): Float {
val t = value.coerceIn(0f, 1f)
val inv = 1f - t
return 1f - inv * inv * inv
}
/**
* Profile Metaball Effect with real Bezier path - like Telegram!
*
@@ -370,9 +377,6 @@ fun ProfileMetaballOverlay(
val screenWidth = configuration.screenWidthDp.dp
val density = LocalDensity.current
// Debug: log screen dimensions once
val TAG = "ProfileMetaball"
// Convert to pixels for path calculations
val screenWidthPx = with(density) { screenWidth.toPx() }
val statusBarHeightPx = with(density) { statusBarHeight.toPx() }
@@ -386,10 +390,13 @@ fun ProfileMetaballOverlay(
NotchInfoUtils.getInfo(context) ?: NotchInfoUtils.getInfoFromCutout(view)
}
// Debug log notch info once
LaunchedEffect(notchInfo, screenWidthPx, statusBarHeightPx, headerHeightPx) {
Log.d(TAG, "NotchInfo: gravity=${notchInfo?.gravity}, isCircle=${notchInfo?.isLikelyCircle}, bounds=${notchInfo?.bounds}, raw=${notchInfo?.rawPath}")
Log.d(TAG, "Screen: width=${screenWidthPx}px, statusBar=${statusBarHeightPx}px, header=${headerHeightPx}px")
// Only log in explicit debug mode to keep production scroll clean.
val debugLogsEnabled = MetaballDebug.forceMode != null || MetaballDebug.forceNoNotch
LaunchedEffect(debugLogsEnabled, notchInfo, screenWidthPx, statusBarHeightPx, headerHeightPx) {
if (debugLogsEnabled) {
Log.d("ProfileMetaball", "NotchInfo: gravity=${notchInfo?.gravity}, isCircle=${notchInfo?.isLikelyCircle}, bounds=${notchInfo?.bounds}, raw=${notchInfo?.rawPath}")
Log.d("ProfileMetaball", "Screen: width=${screenWidthPx}px, statusBar=${statusBarHeightPx}px, header=${headerHeightPx}px")
}
}
// Calculate notch radius based on real device notch (like Telegram's ProfileGooeyView)
@@ -431,12 +438,13 @@ fun ProfileMetaballOverlay(
}
}
// Telegram thresholds in pixels
val dp40 = with(density) { ProfileMetaballConstants.THRESHOLD_DRAWING.toPx() }
val dp34 = with(density) { ProfileMetaballConstants.THRESHOLD_SHAPE_END.toPx() }
val dp32 = with(density) { ProfileMetaballConstants.THRESHOLD_NEAR.toPx() }
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() }
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() }
// Scale Telegram thresholds to our bigger base avatar size (120dp vs 96dp).
val thresholdScale = (avatarSizeExpandedPx / with(density) { 96.dp.toPx() }).coerceIn(1f, 1.35f)
val dp40 = with(density) { ProfileMetaballConstants.THRESHOLD_DRAWING.toPx() } * thresholdScale
val dp34 = with(density) { ProfileMetaballConstants.THRESHOLD_SHAPE_END.toPx() } * thresholdScale
val dp32 = with(density) { ProfileMetaballConstants.THRESHOLD_NEAR.toPx() } * thresholdScale
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() } * thresholdScale
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() } * thresholdScale
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
// derivedStateOf автоматически отслеживает их как зависимости внутри лямбды
@@ -469,9 +477,16 @@ fun ProfileMetaballOverlay(
val metaballPath = remember { Path() }
val c1 = remember { Point() } // Notch center
val c2 = remember { Point() } // Avatar center
val metaballWorkspace = remember { MetaballWorkspace() }
// Reusable RectF like Telegram's AndroidUtilities.rectTmp
val rectTmp = remember { RectF() }
val blackPaint = remember {
android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
color = android.graphics.Color.BLACK
style = android.graphics.Paint.Style.FILL
}
}
// Black bar height for no-notch fallback (Telegram's BLACK_KING_BAR)
val blackBarHeightPx = with(density) { BLACK_BAR_HEIGHT_DP.dp.toPx() }
@@ -484,24 +499,36 @@ fun ProfileMetaballOverlay(
val distance = avatarState.centerY - notchCenterY
val maxDist = avatarSizeExpandedPx
val c = (distance / maxDist).coerceIn(-1f, 1f)
val connectorProgressRaw = ((dp40 - avatarState.radius) / (dp40 - dp18)).coerceIn(0f, 1f)
val connectorProgress = easeOutCubic(connectorProgressRaw)
val layerVisibilityBase = easeOutCubic((collapseProgress / 0.08f).coerceIn(0f, 1f))
val layerVisibility = lerpFloat(0.35f, 1f, layerVisibilityBase)
val baseV = ((1f - c / 1.3f) / 2f).coerceIn(0f, 0.8f)
val near = if (avatarState.isNear) 1f else 1f - (avatarState.radius - dp32) / (dp40 - dp32)
val v = if (!avatarState.isNear) {
val vBase = if (!avatarState.isNear) {
min(lerpFloat(0f, 0.2f, near), baseV)
} else {
baseV
}
}.coerceIn(0.08f, 0.90f)
val v = lerpFloat(vBase * 0.92f, (vBase + 0.14f).coerceAtMost(0.98f), connectorProgress)
.coerceIn(0.08f, 0.98f)
// Show connector when avatar is small enough (isDrawing) and not expanding
// No longer requires hasRealNotch — works with black bar fallback too
val showConnector = hasAvatar && expansionProgress == 0f && avatarState.isDrawing && avatarState.showBlob && distance < maxDist * 1.5f
val showConnector =
hasAvatar && expansionProgress == 0f && avatarState.showBlob &&
distance < maxDist * 1.6f && connectorProgress > 0.001f
// Show metaball layer when collapsing with avatar
val showMetaballLayer = hasAvatar && expansionProgress == 0f
val showMetaballLayer =
hasAvatar && expansionProgress == 0f && avatarState.showBlob &&
collapseProgress > 0.015f && layerVisibility > 0.01f
val connectorPaintAlpha = lerpFloat(96f, 255f, connectorProgress).toInt().coerceIn(0, 255)
// Adjusted blur radius based on device performance (factorMult)
val adjustedBlurRadius = ProfileMetaballConstants.BLUR_RADIUS / factorMult
val adjustedBlurRadius = (ProfileMetaballConstants.BLUR_RADIUS / factorMult) *
lerpFloat(0.9f, 1.45f, connectorProgress)
Box(modifier = modifier
.fillMaxSize()
@@ -512,6 +539,9 @@ fun ProfileMetaballOverlay(
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
alpha = layerVisibility
}
.graphicsLayer {
// ColorMatrixColorFilter for alpha threshold (API 31+)
// Replaces AGSL RuntimeShader — same gooey effect
@@ -528,38 +558,35 @@ fun ProfileMetaballOverlay(
.asComposeRenderEffect()
}
) {
val paint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
color = android.graphics.Color.BLACK
style = android.graphics.Paint.Style.FILL
}
drawIntoCanvas { canvas ->
val nativeCanvas = canvas.nativeCanvas
// Draw target shape at top (notch or black bar fallback)
if (showConnector) {
blackPaint.alpha = connectorPaintAlpha
if (hasRealNotch && notchInfo != null && notchInfo.isLikelyCircle) {
nativeCanvas.drawCircle(
notchCenterX,
notchCenterY,
notchRadiusPx,
paint
blackPaint
)
} else if (hasRealNotch && notchInfo != null && notchInfo.isAccurate && notchInfo.path != null) {
nativeCanvas.drawPath(notchInfo.path, paint)
nativeCanvas.drawPath(notchInfo.path, blackPaint)
} else if (hasRealNotch && notchInfo != null) {
val bounds = notchInfo.bounds
val rad = kotlin.math.max(bounds.width(), bounds.height()) / 2f
nativeCanvas.drawRoundRect(bounds, rad, rad, paint)
nativeCanvas.drawRoundRect(bounds, rad, rad, blackPaint)
} else {
// No notch fallback: full-width black bar at top
// Like Telegram's ProfileGooeyView when notchInfo == null
nativeCanvas.drawRect(0f, 0f, screenWidthPx, blackBarHeightPx, paint)
nativeCanvas.drawRect(0f, 0f, screenWidthPx, blackBarHeightPx, blackPaint)
}
}
// Draw avatar shape - circle or rounded rect depending on state
if (avatarState.showBlob) {
blackPaint.alpha = 255
val cx = avatarState.centerX
val cy = avatarState.centerY
val r = avatarState.radius
@@ -569,26 +596,28 @@ fun ProfileMetaballOverlay(
// If cornerRadius is close to radius, draw circle; otherwise rounded rect
if (cornerR >= r * 0.95f) {
// Draw circle
nativeCanvas.drawCircle(cx, cy, r, paint)
nativeCanvas.drawCircle(cx, cy, r, blackPaint)
} else {
// Draw rounded rect (like Telegram when isDrawing)
// Reuse rectTmp to avoid allocations
rectTmp.set(cx - r, cy - r, cx + r, cy + r)
nativeCanvas.drawRoundRect(rectTmp, cornerR, cornerR, paint)
nativeCanvas.drawRoundRect(rectTmp, cornerR, cornerR, blackPaint)
}
}
// Draw metaball connector path
if (showConnector && avatarState.showBlob) {
blackPaint.alpha = connectorPaintAlpha
c1.x = notchCenterX
c1.y = notchCenterY
c2.x = avatarState.centerX
c2.y = avatarState.centerY
if (createMetaballPath(c1, c2, notchRadiusPx, avatarState.radius, v, metaballPath)) {
nativeCanvas.drawPath(metaballPath, paint)
if (createMetaballPath(c1, c2, notchRadiusPx, avatarState.radius, v, metaballPath, metaballWorkspace)) {
nativeCanvas.drawPath(metaballPath, blackPaint)
}
}
blackPaint.alpha = 255
}
}
} // END if (showMetaballLayer)
@@ -717,12 +746,12 @@ fun ProfileMetaballOverlayCompat(
val notchCenterY = blackBarHeightPx / 2f
val notchRadiusPx = with(density) { ProfileMetaballConstants.FALLBACK_CAMERA_SIZE.toPx() }
// Telegram thresholds in pixels
val dp40 = with(density) { ProfileMetaballConstants.THRESHOLD_DRAWING.toPx() }
val dp34 = with(density) { ProfileMetaballConstants.THRESHOLD_SHAPE_END.toPx() }
val dp32 = with(density) { ProfileMetaballConstants.THRESHOLD_NEAR.toPx() }
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() }
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() }
val thresholdScale = (avatarSizeExpandedPx / with(density) { 96.dp.toPx() }).coerceIn(1f, 1.35f)
val dp40 = with(density) { ProfileMetaballConstants.THRESHOLD_DRAWING.toPx() } * thresholdScale
val dp34 = with(density) { ProfileMetaballConstants.THRESHOLD_SHAPE_END.toPx() } * thresholdScale
val dp32 = with(density) { ProfileMetaballConstants.THRESHOLD_NEAR.toPx() } * thresholdScale
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() } * thresholdScale
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() } * thresholdScale
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
// derivedStateOf автоматически отслеживает их как зависимости
@@ -894,12 +923,12 @@ fun ProfileMetaballOverlayCpu(
}
}
// Thresholds
val dp40 = with(density) { ProfileMetaballConstants.THRESHOLD_DRAWING.toPx() }
val dp34 = with(density) { ProfileMetaballConstants.THRESHOLD_SHAPE_END.toPx() }
val dp32 = with(density) { ProfileMetaballConstants.THRESHOLD_NEAR.toPx() }
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() }
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() }
val thresholdScale = (avatarSizeExpandedPx / with(density) { 96.dp.toPx() }).coerceIn(1f, 1.35f)
val dp40 = with(density) { ProfileMetaballConstants.THRESHOLD_DRAWING.toPx() } * thresholdScale
val dp34 = with(density) { ProfileMetaballConstants.THRESHOLD_SHAPE_END.toPx() } * thresholdScale
val dp32 = with(density) { ProfileMetaballConstants.THRESHOLD_NEAR.toPx() } * thresholdScale
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() } * thresholdScale
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() } * thresholdScale
val avatarState by remember(screenWidthPx, statusBarHeightPx, headerHeightPx, notchCenterX, notchCenterY, notchRadiusPx) {
derivedStateOf {
@@ -925,22 +954,35 @@ fun ProfileMetaballOverlayCpu(
val metaballPath = remember { android.graphics.Path() }
val c1 = remember { Point() }
val c2 = remember { Point() }
val metaballWorkspace = remember { MetaballWorkspace() }
val rectTmp = remember { RectF() }
// Connector calculations
val distance = avatarState.centerY - notchCenterY
val maxDist = avatarSizeExpandedPx
val connectorProgressRaw = ((dp40 - avatarState.radius) / (dp40 - dp18)).coerceIn(0f, 1f)
val connectorProgress = easeOutCubic(connectorProgressRaw)
val layerVisibilityBase = easeOutCubic((collapseProgress / 0.08f).coerceIn(0f, 1f))
val layerVisibility = lerpFloat(0.35f, 1f, layerVisibilityBase)
val cParam = (distance / maxDist).coerceIn(-1f, 1f)
val baseV = ((1f - cParam / 1.3f) / 2f).coerceIn(0f, 0.8f)
val near = if (avatarState.isNear) 1f else 1f - (avatarState.radius - dp32) / (dp40 - dp32)
val v = if (!avatarState.isNear) min(lerpFloat(0f, 0.2f, near), baseV) else baseV
val vBase = if (!avatarState.isNear) min(lerpFloat(0f, 0.2f, near), baseV) else baseV
val vBaseClamped = vBase.coerceIn(0.08f, 0.90f)
val v = lerpFloat(vBaseClamped * 0.92f, (vBaseClamped + 0.14f).coerceAtMost(0.98f), connectorProgress)
.coerceIn(0.08f, 0.98f)
val showConnector = hasAvatar && expansionProgress == 0f && avatarState.isDrawing && avatarState.showBlob && distance < maxDist * 1.5f
val showMetaballLayer = hasAvatar && expansionProgress == 0f
val showConnector =
hasAvatar && expansionProgress == 0f && avatarState.showBlob &&
distance < maxDist * 1.6f && connectorProgress > 0.001f
val showMetaballLayer =
hasAvatar && expansionProgress == 0f && avatarState.showBlob &&
collapseProgress > 0.015f && layerVisibility > 0.01f
val connectorPaintAlpha = lerpFloat(96f, 255f, connectorProgress).toInt().coerceIn(0, 255)
// CPU-specific: downscaled bitmap for blur
// Reduced from 5 to 3 for higher resolution — smoother edges after alpha threshold
val scaleConst = 3f
// Higher internal resolution for cleaner, denser metaball edges.
val scaleConst = 2.5f
val optimizedW = min(with(density) { 120.dp.toPx() }.toInt(), screenWidthPx.toInt())
val optimizedH = min(with(density) { 220.dp.toPx() }.toInt(), headerHeightPx.toInt() + blackBarHeightPx.toInt())
val bitmapW = (optimizedW / scaleConst).toInt().coerceAtLeast(1)
@@ -982,7 +1024,13 @@ fun ProfileMetaballOverlayCpu(
) {
// LAYER 1: CPU-rendered metaball shapes
if (showMetaballLayer) {
Canvas(modifier = Modifier.fillMaxSize()) {
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
alpha = layerVisibility
}
) {
drawIntoCanvas { canvas ->
val nativeCanvas = canvas.nativeCanvas
val optimizedOffsetX = (screenWidthPx - optimizedW) / 2f
@@ -1000,6 +1048,7 @@ fun ProfileMetaballOverlayCpu(
// Draw target (notch or black bar)
if (showConnector) {
blackPaint.alpha = connectorPaintAlpha
if (hasRealNotch && notchInfo != null && notchInfo.isLikelyCircle) {
val rad = min(notchInfo.bounds.width(), notchInfo.bounds.height()) / 2f
offscreenCanvas.drawCircle(
@@ -1021,6 +1070,7 @@ fun ProfileMetaballOverlayCpu(
// Draw avatar shape
if (avatarState.showBlob) {
blackPaint.alpha = 255
val cx = avatarState.centerX
val cy = avatarState.centerY
val r = avatarState.radius
@@ -1036,20 +1086,23 @@ fun ProfileMetaballOverlayCpu(
// Draw metaball connector
if (showConnector && avatarState.showBlob) {
blackPaint.alpha = connectorPaintAlpha
c1.x = notchCenterX
c1.y = notchCenterY
c2.x = avatarState.centerX
c2.y = avatarState.centerY
if (createMetaballPath(c1, c2, notchRadiusPx, avatarState.radius, v, metaballPath)) {
if (createMetaballPath(c1, c2, notchRadiusPx, avatarState.radius, v, metaballPath, metaballWorkspace)) {
offscreenCanvas.drawPath(metaballPath, blackPaint)
}
}
blackPaint.alpha = 255
offscreenCanvas.restore()
// Apply stack blur on CPU
val blurRadius = (ProfileMetaballConstants.BLUR_RADIUS * 2 / scaleConst).toInt().coerceAtLeast(1)
val blurRadius = ((ProfileMetaballConstants.BLUR_RADIUS * 2f / scaleConst) *
lerpFloat(0.9f, 1.45f, connectorProgress)).toInt().coerceAtLeast(1)
stackBlurBitmapInPlace(offscreenBitmap, blurRadius)
// Draw blurred bitmap with color matrix filter (alpha threshold)
@@ -1229,9 +1282,8 @@ fun MetaballDebugPanel(modifier: Modifier = Modifier) {
"cpu" -> "CPU (forced)"
"compat" -> "Compat (forced)"
else -> when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && perfClass >= PerformanceClass.AVERAGE -> "GPU (auto)"
perfClass >= PerformanceClass.HIGH -> "CPU (auto)"
else -> "Compat (auto)"
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> "GPU (auto)"
else -> "CPU (auto)"
}
}
Text(
@@ -1287,13 +1339,15 @@ fun ProfileMetaballEffect(
"cpu" -> "CPU (forced)"
"compat" -> "Compat (forced)"
else -> when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && performanceClass >= PerformanceClass.AVERAGE -> "GPU (auto)"
performanceClass >= PerformanceClass.HIGH -> "CPU (auto)"
else -> "Compat (auto)"
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> "GPU (auto)"
else -> "CPU (auto)"
}
}
LaunchedEffect(selectedPath) {
Log.d("MetaballDebug", "Rendering path: $selectedPath, forceNoNotch: ${MetaballDebug.forceNoNotch}, perf: $performanceClass, API: ${Build.VERSION.SDK_INT}")
val debugLogsEnabled = MetaballDebug.forceMode != null || MetaballDebug.forceNoNotch
LaunchedEffect(selectedPath, debugLogsEnabled, performanceClass) {
if (debugLogsEnabled) {
Log.d("MetaballDebug", "Rendering path: $selectedPath, forceNoNotch: ${MetaballDebug.forceNoNotch}, perf: $performanceClass, API: ${Build.VERSION.SDK_INT}")
}
}
// Resolve actual mode
@@ -1301,18 +1355,22 @@ fun ProfileMetaballEffect(
"gpu" -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S // still need API 31
"cpu" -> false
"compat" -> false
else -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && performanceClass >= PerformanceClass.AVERAGE
else -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
}
val useCpu = when (MetaballDebug.forceMode) {
"gpu" -> false
"cpu" -> true
"compat" -> false
else -> !useGpu && performanceClass >= PerformanceClass.HIGH
else -> !useGpu
}
when {
useGpu -> {
val factorMult = if (performanceClass == PerformanceClass.HIGH) 1f else 1.5f
val factorMult = when (performanceClass) {
PerformanceClass.HIGH -> 1f
PerformanceClass.AVERAGE -> 1.2f
PerformanceClass.LOW -> 1.35f
}
ProfileMetaballOverlay(
collapseProgress = collapseProgress,
expansionProgress = expansionProgress,