← Volver al listado de tecnologías

Canvas y Gráficos

Por: SiempreListo
kivycanvasgráficosdibujo

Sistema de Canvas

Cada widget tiene un canvas para dibujar. Tres capas disponibles:

canvas.before  →  Dibuja primero (fondo)
canvas         →  Capa principal
canvas.after   →  Dibuja último (overlay)

Instrucciones de Color

Widget:
    canvas:
        Color:
            rgba: 1, 0, 0, 1       # Rojo opaco
            # rgb: 1, 0, 0         # Sin alpha
            # hsv: 0, 1, 1         # HSV
from kivy.graphics import Color

with self.canvas:
    Color(1, 0, 0, 1)  # RGBA
    Color(rgb=(0, 1, 0))
    Color(hsv=(0.5, 1, 1))

Formas Básicas

Rectangle

Widget:
    canvas:
        Color:
            rgba: 0.3, 0.6, 0.9, 1
        Rectangle:
            pos: self.pos
            size: self.size

RoundedRectangle

canvas:
    RoundedRectangle:
        pos: self.pos
        size: self.size
        radius: [20, 20, 20, 20]  # Esquinas

Ellipse

canvas:
    Ellipse:
        pos: self.pos
        size: self.size
        # Arco parcial
        angle_start: 0
        angle_end: 180

Line

canvas:
    Line:
        points: [100, 100, 200, 200, 300, 100]
        width: 2
        close: True  # Cerrar forma

Triangle

canvas:
    Triangle:
        points: [100, 100, 200, 300, 300, 100]

Instrucciones en Python

from kivy.uix.widget import Widget
from kivy.graphics import Color, Rectangle

class MiCanvas(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        with self.canvas:
            Color(1, 0, 0, 1)
            self.rect = Rectangle(pos=self.pos, size=self.size)

        # Actualizar cuando cambie tamaño/posición
        self.bind(pos=self.actualizar, size=self.actualizar)

    def actualizar(self, *args):
        self.rect.pos = self.pos
        self.rect.size = self.size

Texturas e Imágenes

canvas:
    Rectangle:
        pos: self.pos
        size: self.size
        source: 'imagen.png'
from kivy.graphics import Rectangle

with self.canvas:
    Rectangle(
        pos=self.pos,
        size=self.size,
        source='imagen.png'
    )

Transformaciones

Translate (Mover)

canvas:
    PushMatrix:
    Translate:
        x: 50
        y: 50
    Rectangle:
        size: 100, 100
    PopMatrix:

Rotate (Rotar)

canvas:
    PushMatrix:
    Rotate:
        angle: 45
        origin: self.center
    Rectangle:
        pos: self.pos
        size: self.size
    PopMatrix:

Scale (Escalar)

canvas:
    PushMatrix:
    Scale:
        x: 2
        y: 2
        origin: self.center
    Rectangle:
        pos: self.pos
        size: self.size
    PopMatrix:

Ejemplo: Widget con Fondo

<CardWidget>:
    canvas.before:
        # Sombra
        Color:
            rgba: 0, 0, 0, 0.3
        RoundedRectangle:
            pos: self.x + 3, self.y - 3
            size: self.size
            radius: [10]

        # Fondo
        Color:
            rgba: 1, 1, 1, 1
        RoundedRectangle:
            pos: self.pos
            size: self.size
            radius: [10]

    canvas.after:
        # Borde
        Color:
            rgba: 0.8, 0.8, 0.8, 1
        Line:
            rounded_rectangle: self.x, self.y, self.width, self.height, 10
            width: 1

Dibujo Dinámico

from kivy.uix.widget import Widget
from kivy.graphics import Color, Line

class Pizarra(Widget):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            with self.canvas:
                Color(1, 1, 1, 1)
                touch.ud['linea'] = Line(points=[touch.x, touch.y], width=2)
            return True
        return super().on_touch_down(touch)

    def on_touch_move(self, touch):
        if 'linea' in touch.ud:
            touch.ud['linea'].points += [touch.x, touch.y]
            return True
        return super().on_touch_move(touch)

    def limpiar(self):
        self.canvas.clear()

Instrucciones Avanzadas

Bezier

canvas:
    Bezier:
        points: [0, 0, 100, 200, 200, 200, 300, 0]
        segments: 50

Mesh

canvas:
    Mesh:
        vertices: [0, 0, 0, 0, 100, 0, 0, 0, 100, 100, 0, 0]
        indices: [0, 1, 2]
        mode: 'triangle_fan'

SmoothLine

canvas:
    SmoothLine:
        points: self.puntos
        width: 2

Gradientes

from kivy.graphics import Rectangle
from kivy.graphics.texture import Texture

def crear_gradiente(self):
    texture = Texture.create(size=(2, 1))
    # Gradiente horizontal rojo a azul
    buf = bytes([255, 0, 0, 255, 0, 0, 255, 255])
    texture.blit_buffer(buf, colorfmt='rgba', bufferfmt='ubyte')
    texture.wrap = 'repeat'

    with self.canvas:
        Rectangle(texture=texture, pos=self.pos, size=self.size)

StencilView (Máscara)

StencilView:
    size_hint: None, None
    size: 200, 200
    pos: 100, 100

    Image:
        source: 'imagen.png'
        size: 400, 400
        # Solo se ve dentro del StencilView

Ejemplo: Gráfico de Barras

from kivy.uix.widget import Widget
from kivy.graphics import Color, Rectangle
from kivy.properties import ListProperty

class BarChart(Widget):
    datos = ListProperty([30, 50, 80, 40, 90])
    colores = ListProperty([
        (0.9, 0.3, 0.3, 1),
        (0.3, 0.9, 0.3, 1),
        (0.3, 0.3, 0.9, 1),
        (0.9, 0.9, 0.3, 1),
        (0.9, 0.3, 0.9, 1),
    ])

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.bind(pos=self.dibujar, size=self.dibujar, datos=self.dibujar)

    def dibujar(self, *args):
        self.canvas.clear()
        if not self.datos:
            return

        ancho_barra = self.width / len(self.datos)
        max_valor = max(self.datos)

        with self.canvas:
            for i, valor in enumerate(self.datos):
                Color(*self.colores[i % len(self.colores)])
                altura = (valor / max_valor) * self.height
                Rectangle(
                    pos=(self.x + i * ancho_barra + 5, self.y),
                    size=(ancho_barra - 10, altura)
                )

Testing de Canvas

# test_canvas.py
import unittest
from kivy.uix.widget import Widget
from kivy.graphics import Color, Rectangle, Ellipse

class TestCanvas(unittest.TestCase):
    def test_canvas_instrucciones(self):
        w = Widget()
        with w.canvas:
            Color(1, 0, 0, 1)
            rect = Rectangle(pos=(0, 0), size=(100, 100))
        self.assertIsNotNone(rect)
        self.assertEqual(rect.size, (100, 100))

    def test_canvas_before_after(self):
        w = Widget()
        with w.canvas.before:
            Color(0, 1, 0, 1)
        with w.canvas.after:
            Color(0, 0, 1, 1)
        self.assertTrue(len(w.canvas.before.children) > 0)
        self.assertTrue(len(w.canvas.after.children) > 0)

    def test_modificar_instruccion(self):
        w = Widget()
        with w.canvas:
            rect = Rectangle(size=(50, 50))
        rect.size = (100, 100)
        self.assertEqual(rect.size, (100, 100))

if __name__ == '__main__':
    unittest.main()

Probar en Dispositivo con ADB

# Activar modo de depuración GPU
adb shell setprop debug.hwui.overdraw show

# Ver estadísticas de renderizado
adb shell dumpsys gfxinfo com.ejemplo.miapp framestats

# Capturar frame para análisis
adb shell screencap /sdcard/frame.png
adb pull /sdcard/frame.png

# Perfilar rendimiento OpenGL
adb shell setprop debug.egl.trace systrace

Resumen