package com.example.jnicpp.bowling import android.text.Layout import android.text.TextPaint import android.text.StaticLayout import android.graphics.Canvas import android.graphics.RectF import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.content.Context import android.graphics.Paint import android.view.View import android.widget.TextView import com.example.jnicpp.R import android.graphics.BlurMaskFilter import android.graphics.Matrix import kotlin.math.min import androidx.constraintlayout.widget.ConstraintLayout class FeedbackUI(private val context: Context, private val rootView: View) { private var landmarks: Map? = null private var CIRCLE_RADIUS = 128f private val uiTextSize = 32f private val uiStrokeWidth = 12f private val bannerPaddingY = 24 private val bannerPaddingX = 24 private val camScale = 0.5f private var liveUiWidth: Int = 0 private var liveUiHeight: Int = 0 private val glowPaint = Paint().apply { color = Color.RED style = Paint.Style.STROKE isAntiAlias = true strokeWidth = uiStrokeWidth // thicker stroke so glow is visible maskFilter = BlurMaskFilter(25f, BlurMaskFilter.Blur.OUTER) } private val circlePaint = Paint().apply { color = Color.WHITE style = Paint.Style.STROKE isAntiAlias = true strokeWidth = uiStrokeWidth } private val textPaint = TextPaint().apply { color = Color.WHITE textSize = uiTextSize isAntiAlias = true } private val bgPaint = Paint().apply { color = Color.argb(64, 255, 255, 255) isAntiAlias = true } init { rootView.viewTreeObserver.addOnGlobalLayoutListener { setLiveUiSize(rootView) } } fun showBanner(canvas: Canvas, message: String, forRecord: Boolean = false) { val scale = if (forRecord) computeCamScale(canvas) else 1f val paddingX = bannerPaddingX * scale val paddingY = bannerPaddingY * scale val maxWidth = (canvas.width * 0.8f).toInt() // Build StaticLayout for multi-line text textPaint.textSize = uiTextSize * scale val staticLayout = StaticLayout.Builder .obtain(message, 0, message.length, textPaint, maxWidth) .setAlignment(Layout.Alignment.ALIGN_CENTER) // center text horizontally .setLineSpacing(0f, 1f) .setIncludePad(false) .build() // Use StaticLayout dimensions val textWidth = staticLayout.width.toFloat() val textHeight = staticLayout.height.toFloat() val bannerWidth = textWidth + paddingX * 2 val bannerHeight = textHeight + paddingY * 2 // Center horizontally val left = (canvas.width - bannerWidth) / 2f val top = 172f * scale val right = left + bannerWidth val bottom = top + bannerHeight // Draw background val rect = RectF(left, top, right, bottom) // Scale corner radius too val cornerRadius = 24f * scale canvas.drawRoundRect(rect, cornerRadius, cornerRadius, bgPaint) // Draw text layout inside background canvas.save() canvas.translate(left + paddingX, top + paddingY) staticLayout.draw(canvas) canvas.restore() } // --- Shape overlay (circle) --- fun drawCircles(canvas: Canvas, landmarks: Map, transform: Matrix, forRecord: Boolean = false) { for (landmark in landmarks.values) { val point = floatArrayOf(landmark.x, landmark.y) transform.mapPoints(point) drawCircle(canvas, point[0], point[1], 0f, forRecord) } } fun drawCircle(canvas: Canvas, x: Float, y: Float, radius: Float = 0f, forRecord: Boolean = false) { val scale = if (forRecord) camScale else 1f var rad = (if (radius == 0f) CIRCLE_RADIUS else radius) * scale circlePaint.strokeWidth = uiStrokeWidth * scale glowPaint.strokeWidth = uiStrokeWidth * scale canvas.drawCircle(x, y, rad, circlePaint) canvas.drawCircle(x, y, rad, glowPaint) } // Store live UI size once fun setLiveUiSize(rootView: View) { liveUiWidth = rootView.width liveUiHeight = rootView.height } // Compute scale when drawing to recording canvas private fun computeCamScale(recordCanvas: Canvas): Float { if (liveUiWidth == 0 || liveUiHeight == 0) return 1f val scaleX = recordCanvas.width.toFloat() / liveUiWidth.toFloat() val scaleY = recordCanvas.height.toFloat() / liveUiHeight.toFloat() return min(scaleX, scaleY) } }