fix: fix gooey animation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user