From 1139bd6be67969010ce3cb452d42c3b6c13233d7 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 9 Feb 2026 11:33:23 +0500 Subject: [PATCH] fix: fix gooey animation --- .../ui/components/metaball/MetaballPath.kt | 85 ++++---- .../metaball/ProfileMetaballOverlay.kt | 188 ++++++++++++------ 2 files changed, 171 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/metaball/MetaballPath.kt b/app/src/main/java/com/rosetta/messenger/ui/components/metaball/MetaballPath.kt index 2f645a8..6c69978 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/metaball/MetaballPath.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/metaball/MetaballPath.kt @@ -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,49 +47,50 @@ 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)) { return false } - + // 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) getVector(c1, angle2, radius1, p2) @@ -84,9 +99,9 @@ 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 val r2 = radius2 * d2 @@ -100,15 +115,11 @@ 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 } diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt b/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt index 8c9f5fb..c453a52 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt @@ -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,