Canvas básico

// Canvas se comporta como cualquier composable — tiene tamaño y se integra en layouts
Canvas(
    modifier = Modifier
        .fillMaxWidth()
        .height(200.dp)
) {
    // this: DrawScope — acceso a size, center, drawCircle, drawLine, etc.

    // size.width y size.height en píxeles (ya convertidos de Dp)
    val centroX = size.width / 2
    val centroY = size.height / 2

    // Dibujar un círculo
    drawCircle(
        color = Color.Blue,
        radius = 50.dp.toPx(),
        center = Offset(centroX, centroY)
    )

    // Dibujar una línea
    drawLine(
        color = Color.Red,
        start = Offset(0f, 0f),
        end = Offset(size.width, size.height),
        strokeWidth = 2.dp.toPx()
    )

    // Dibujar un rectángulo
    drawRect(
        color = Color.Green.copy(alpha = 0.3f),
        topLeft = Offset(20.dp.toPx(), 20.dp.toPx()),
        size = Size(100.dp.toPx(), 60.dp.toPx())
    )
}

Formas con Path

// Un indicador de progreso en arco
@Composable
fun IndicadorArco(
    progreso: Float,  // 0f a 1f
    modifier: Modifier = Modifier,
    color: Color = MaterialTheme.colorScheme.primary,
    trackColor: Color = MaterialTheme.colorScheme.surfaceVariant
) {
    Canvas(modifier = modifier.size(120.dp)) {
        val strokeWidth = 12.dp.toPx()
        val radio = (size.minDimension - strokeWidth) / 2
        val centroX = size.width / 2
        val centroY = size.height / 2

        // Track de fondo (arco completo gris)
        drawArc(
            color = trackColor,
            startAngle = 135f,
            sweepAngle = 270f,
            useCenter = false,
            topLeft = Offset(centroX - radio, centroY - radio),
            size = Size(radio * 2, radio * 2),
            style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
        )

        // Progreso (arco proporcional al progreso)
        drawArc(
            color = color,
            startAngle = 135f,
            sweepAngle = 270f * progreso,
            useCenter = false,
            topLeft = Offset(centroX - radio, centroY - radio),
            size = Size(radio * 2, radio * 2),
            style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
        )
    }
}

// Path personalizado — forma de diamante
@Composable
fun Diamante(color: Color, modifier: Modifier = Modifier) {
    Canvas(modifier = modifier.size(80.dp)) {
        val path = Path().apply {
            moveTo(size.width / 2, 0f)         // arriba
            lineTo(size.width, size.height / 2) // derecha
            lineTo(size.width / 2, size.height) // abajo
            lineTo(0f, size.height / 2)         // izquierda
            close()
        }
        drawPath(path = path, color = color)
    }
}

Gradientes

Canvas(modifier = Modifier.fillMaxWidth().height(150.dp)) {

    // Gradiente lineal horizontal
    val gradienteHorizontal = Brush.horizontalGradient(
        colors = listOf(Color.Blue, Color.Cyan, Color.Green)
    )
    drawRect(brush = gradienteHorizontal, size = size)

    // Gradiente radial
    val gradienteRadial = Brush.radialGradient(
        colors = listOf(Color.Yellow, Color.Transparent),
        center = center,
        radius = size.minDimension / 2
    )
    drawCircle(brush = gradienteRadial, radius = size.minDimension / 2)

    // Gradiente en un path
    val gradientePath = Brush.linearGradient(
        0.0f to Color.Red,
        0.5f to Color.Magenta,
        1.0f to Color.Blue,
        start = Offset(0f, 0f),
        end = Offset(size.width, size.height)
    )
    drawRoundRect(
        brush = gradientePath,
        cornerRadius = CornerRadius(16.dp.toPx()),
        size = Size(size.width * 0.8f, size.height * 0.6f),
        topLeft = Offset(size.width * 0.1f, size.height * 0.2f)
    )
}

Texto en Canvas

@Composable
fun CanvasConTexto() {
    // Para texto en Canvas necesitamos TextMeasurer
    val textMeasurer = rememberTextMeasurer()

    Canvas(modifier = Modifier.fillMaxWidth().height(100.dp)) {
        val textResult = textMeasurer.measure(
            text = "Compose Canvas",
            style = TextStyle(
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold,
                color = Color.White
            )
        )

        // Centrar el texto
        val textoX = (size.width - textResult.size.width) / 2
        val textoY = (size.height - textResult.size.height) / 2

        // Fondo
        drawRoundRect(
            color = Color.DarkGray,
            size = size,
            cornerRadius = CornerRadius(8.dp.toPx())
        )

        // Texto
        drawText(
            textLayoutResult = textResult,
            topLeft = Offset(textoX, textoY)
        )
    }
}

Transformaciones

Canvas(modifier = Modifier.size(200.dp)) {

    // Rotar alrededor del centro
    rotate(degrees = 45f, pivot = center) {
        drawRect(color = Color.Blue, size = Size(100.dp.toPx(), 20.dp.toPx()),
            topLeft = Offset(center.x - 50.dp.toPx(), center.y - 10.dp.toPx()))
    }

    // Escalar desde el centro
    scale(scaleX = 1.5f, scaleY = 0.8f, pivot = center) {
        drawCircle(color = Color.Red.copy(alpha = 0.5f), radius = 30.dp.toPx())
    }

    // Transladar (mover el origen)
    translate(left = 50.dp.toPx(), top = 20.dp.toPx()) {
        drawRect(color = Color.Green, size = Size(40.dp.toPx(), 40.dp.toPx()))
    }

    // Apilar transformaciones
    withTransform({
        translate(left = size.width / 2, top = size.height / 2)
        rotate(degrees = 30f)
        scale(0.8f)
    }) {
        drawRect(color = Color.Magenta, size = Size(60.dp.toPx(), 60.dp.toPx()),
            topLeft = Offset(-30.dp.toPx(), -30.dp.toPx()))
    }
}

Canvas animado

// Indicador de carga pulsante
@Composable
fun PulsingLoader(color: Color = MaterialTheme.colorScheme.primary) {
    val infiniteTransition = rememberInfiniteTransition(label = "pulse")

    val escala by infiniteTransition.animateFloat(
        initialValue = 0.6f,
        targetValue = 1.0f,
        animationSpec = infiniteRepeatable(
            animation = tween(800, easing = FastOutSlowInEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "escala"
    )

    val alpha by infiniteTransition.animateFloat(
        initialValue = 0.3f,
        targetValue = 1.0f,
        animationSpec = infiniteRepeatable(
            animation = tween(800),
            repeatMode = RepeatMode.Reverse
        ),
        label = "alpha"
    )

    Canvas(modifier = Modifier.size(60.dp)) {
        drawCircle(
            color = color.copy(alpha = alpha),
            radius = (size.minDimension / 2) * escala
        )
    }
}

// Gráfico de línea animado que se dibuja progresivamente
@Composable
fun LineaAnimada(puntos: List<Float>) {
    var progreso by remember { mutableStateOf(0f) }

    LaunchedEffect(puntos) {
        animate(0f, 1f, animationSpec = tween(1500)) { valor, _ ->
            progreso = valor
        }
    }

    Canvas(modifier = Modifier.fillMaxWidth().height(150.dp)) {
        if (puntos.isEmpty()) return@Canvas

        val puntosADibujar = (puntos.size * progreso).toInt().coerceAtLeast(2)
        val pasoX = size.width / (puntos.size - 1)
        val maxValor = puntos.max()

        val path = Path()
        puntos.take(puntosADibujar).forEachIndexed { index, valor ->
            val x = index * pasoX
            val y = size.height - (valor / maxValor) * size.height
            if (index == 0) path.moveTo(x, y) else path.lineTo(x, y)
        }
        drawPath(path = path, color = Color.Blue,
            style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round))
    }
}

drawBehind y drawWithContent

// drawBehind: dibujar detrás del contenido del composable
// Más eficiente que Canvas para decoraciones simples
Box(
    modifier = Modifier
        .padding(16.dp)
        .drawBehind {
            // Sombra custom
            drawRoundRect(
                color = Color.Black.copy(alpha = 0.15f),
                topLeft = Offset(4.dp.toPx(), 4.dp.toPx()),
                size = Size(size.width, size.height),
                cornerRadius = CornerRadius(8.dp.toPx())
            )
            // Fondo de la card
            drawRoundRect(
                color = Color.White,
                size = size,
                cornerRadius = CornerRadius(8.dp.toPx())
            )
        }
) {
    Text("Contenido con sombra custom", modifier = Modifier.padding(16.dp))
}

// drawWithContent: dibujar antes Y después del contenido
Text(
    text = "Texto con subrayado animado",
    modifier = Modifier.drawWithContent {
        drawContent()  // primero el texto
        // Luego el subrayado encima
        drawLine(
            color = Color.Blue,
            start = Offset(0f, size.height),
            end = Offset(size.width, size.height),
            strokeWidth = 2.dp.toPx()
        )
    }
)