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) } } /** * @brief Displays a banner with the given message on the provided canvas. * * The banner is centered horizontally and offset vertically. It uses * `StaticLayout` to support multi-line text and scales its size depending * on whether the canvas is for live UI or recording output. * * @param canvas The canvas to draw the banner on. * @param message The text message to display inside the banner. * @param forRecord If true, scales the banner relative to recording canvas * dimensions; otherwise uses live UI scale. */ 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() } /** * @brief Draws glowing circles around all provided landmarks. * * Each landmark’s coordinates are transformed by the given matrix before * drawing. Circles are rendered with both a white stroke and a red glow * effect for visibility. * * @param canvas The canvas to draw circles on. * @param landmarks A map of landmark indices to smoothed landmark positions. * @param transform A matrix applied to landmark coordinates before drawing. * @param forRecord If true, scales circle radius and stroke width for * recording output. */ // --- 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) } } /** * @brief Draws a single circle with both a solid stroke and a glowing outline. * * This method renders a circle at the specified coordinates using two * layered paints: a white stroke (`circlePaint`) and a red glow (`glowPaint`). * The radius and stroke widths are scaled depending on whether the canvas * is for live UI or recording output, ensuring consistent visual feedback * across different resolutions. * * @param canvas The canvas to draw the circle on. * @param x The x‑coordinate of the circle’s center. * @param y The y‑coordinate of the circle’s center. * @param radius The circle radius. If set to 0, defaults to [CIRCLE_RADIUS]. * @param forRecord If true, applies recording scale factor to radius and * stroke width; otherwise uses live UI scale. */ 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) } /** * @brief Updates stored live UI dimensions based on the root view. * * This method caches the width and height of the root view so that * recording canvas scaling can be computed consistently later. * * @param rootView The root view whose dimensions are measured. */ fun setLiveUiSize(rootView: View) { liveUiWidth = rootView.width liveUiHeight = rootView.height } /** * @brief Computes the scaling factor between live UI and recording canvas. * * The scale is determined by comparing the recording canvas dimensions * against the cached live UI dimensions, using the smaller ratio to * preserve aspect consistency. * * @param recordCanvas The canvas used for recording output. * @return A float scale factor to apply when drawing to the 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) } }