# Centro de control de cristal

> Un panel profesional de cristal esmerilado en Python: reproductor, luces, clima y agenda con desenfoque real del fondo sobre colores en movimiento.

Source: https://albakore.github.io/pyzed/es/showcase/glass-dashboard/

Un centro de control domótico con el estilo de cristal esmerilado de los sistemas operativos modernos. Las manchas de color detrás de las tarjetas son un grafo de shader (`shaders/blobs.pzshad`), así que el desenfoque que ves a través de cada tarjeta se mueve con ellas.

(Image: Centro de control de cristal)

## Cómo está hecho

### Desenfoque real del fondo

`glass` desenfoca lo que hay detrás de la tarjeta; no es un degradado falso.

```python
GLASS = {"tint": "#ffffff", "strength": 0.10, "blur": 28}
card.style(glass=GLASS, radius=22, padding=20)
```

### Detalles vivos

La portada gira mientras suena la música; las bombillas encendidas brillan con bloom.

```python
cover.animate("spin", duration=12000)
```

### Controles reales

Los sliders y switches cambian el estado como en cualquier app de Pyzed.

```python
pz.slider(0, 100, self.volume, on_change=self.set_volume)
```

## Prueba esto

- Cambia `tint` y `strength` en `GLASS` y mira cuánto del fondo se ve a través.
- Enciende todas las luces y mira el bloom.
- Cambia las manchas por otro grafo de [Shaders y efectos](https://albakore.github.io/pyzed/es/showcase/shaders/#todos-los-grafos), como
  `nebula.pzshad`.

**Tip:**

El cristal luce mejor sobre algo que se mueve y tiene color. Sobre un fondo plano es solo un panel tintado.

## Código completo

`examples/showcase/glass_dashboard.py`:

```py
"""A frosted-glass control center: music, lights, climate and a schedule over moving color.

    python examples/showcase/glass_dashboard.py

- The background is a shader graph of soft color blobs that drift with time
  (``shaders/blobs.pzshad``, made of nodes in Pyzed Design).
- Every card is ``glass``: a tinted panel that blurs what moves behind it (``backdrop_blur``).
- Switches, sliders and the play button update the cards; values move with ``transition``.
"""

from pathlib import Path

import pyzed as pz

SHADER = Path(__file__).parent / "shaders" / "blobs.pzshad"
from pyzed import ui

GLASS = {"tint": "#ffffff", "strength": 0.10, "blur": 28}
WHITE = "#ffffff"
MUTED = "#ffffffaa"

class Dashboard(pz.App):
    title = "Control center"
    size = (1180, 700)
    theme = pz.themes.ONE_DARK

    def __init__(self):
        self.playing = True
        self.volume = 64.0
        self.lights = {"Living room": True, "Kitchen": False, "Studio": True}
        self.brightness = 72.0
        self.temperature = 21.5

    def toggle_play(self):
        self.playing = not self.playing

    def set_volume(self, value):
        self.volume = value

    def toggle_light(self, name):
        self.lights[name] = not self.lights[name]

    def set_brightness(self, value):
        self.brightness = value

    def warmer(self):
        self.temperature = min(28.0, self.temperature + 0.5)

    def cooler(self):
        self.temperature = max(16.0, self.temperature - 0.5)

    def card(self, title, icon, body, width):
        head = pz.row([pz.icon(icon, size=16, color=WHITE), pz.text(title, size=13, weight="semibold", color=WHITE)],
                      spacing=8, align="center")
        return pz.column([head, body], spacing=14).style(glass=GLASS, radius=22, padding=20, width=width)

    def music(self):
        cover = pz.container(pz.icon("disc-3", size=44, color="#ffffffdd"), width=112, height=112,
                             align_x="center", align_y="center").style(
            background=pz.Gradient(["#f472b6", "#8b5cf6", "#22d3ee"], angle=135), radius=18,
            rotate=0, bloom=("#c084fc", 0.4))
        if self.playing:
            cover = cover.animate("spin", duration=12000)
        controls = pz.row([
            pz.icon("skip-back", size=20, color=WHITE),
            pz.mouse_area(pz.container(pz.icon("pause" if self.playing else "play", size=22, color="#111827"),
                                       width=48, height=48, align_x="center", align_y="center").style(
                background=WHITE, radius=24, glow=("#ffffff88", 14)), on_press=self.toggle_play).animate(
                "shrink", on="press"),
            pz.icon("skip-forward", size=20, color=WHITE),
        ], spacing=22, align="center")
        info = pz.column([
            pz.text("Midnight City", size=20, weight="bold", color=WHITE),
            pz.text("Neon Avenue · Afterglow", size=13, color=MUTED),
            pz.container(pz.container(None, height=4).style(background=WHITE, radius=2).transition(
                width=230 * (0.42 if self.playing else 0.41), duration=400), width=230).style(
                background="#ffffff33", radius=2),
            pz.row([pz.text("1:48", size=11, color=MUTED), pz.space(width="fill"),
                    pz.text("4:16", size=11, color=MUTED)], width=230),
            controls,
        ], spacing=8)
        volume = pz.row([
            pz.icon("volume-1", size=16, color=MUTED),
            pz.slider(0, 100, self.volume, on_change=self.set_volume, width="fill", accent=WHITE,
                      bg="#ffffff33"),
            pz.icon("volume-2", size=16, color=MUTED),
        ], spacing=10, align="center")
        return self.card("Now playing", "music", pz.column([pz.row([cover, info], spacing=20, align="center"),
                                                           volume], spacing=16), 440)

    def lights_card(self):
        rows = []
        for name, on in self.lights.items():
            rows.append(pz.row([
                pz.container(pz.icon("lightbulb", size=16, color="#1f2937" if on else MUTED), width=34,
                             height=34, align_x="center", align_y="center").style(
                    background="#fde68a" if on else "#ffffff1a", radius=17,
                    bloom=("#fbbf24", 0.6) if on else None),
                pz.column([pz.text(name, size=14, weight="semibold", color=WHITE),
                           pz.text(f"{round(self.brightness)}%" if on else "Off", size=11, color=MUTED)],
                          spacing=1, width="fill"),
                ui.switch(on, lambda _v, name=name: self.toggle_light(name)),
            ], spacing=12, align="center"))
        rows.append(pz.slider(0, 100, self.brightness, on_change=self.set_brightness, width="fill",
                              accent="#fde68a", bg="#ffffff33"))
        return self.card("Lights", "sun", pz.column(rows, spacing=12), 330)

    def climate(self):
        ring = pz.column([
            pz.text(f"{self.temperature:.1f}°", size=44, weight="bold", color=WHITE).transition(
                scale=1.0, duration=200),
            pz.text("Heating to 22°", size=12, color=MUTED),
        ], spacing=0, align="center")
        buttons = pz.row([
            pz.mouse_area(pz.container(pz.icon("minus", size=18, color=WHITE), width=44, height=44,
                                       align_x="center", align_y="center").style(
                background="#ffffff1f", radius=22), on_press=self.cooler).animate("shrink", on="press"),
            pz.mouse_area(pz.container(pz.icon("plus", size=18, color=WHITE), width=44, height=44,
                                       align_x="center", align_y="center").style(
                background="#ffffff1f", radius=22), on_press=self.warmer).animate("shrink", on="press"),
        ], spacing=16)
        dial = pz.container(ring, width=170, height=170, align_x="center", align_y="center").style(
            radius=85, border=(6, "#fb923c"), bloom=("#fb923c", 0.5), background="#ffffff0d")
        return self.card("Climate", "thermometer", pz.column([dial, buttons], spacing=14, align="center", width="fill"), 330)

    def schedule(self):
        events = (
            ("09:30", "Design review", "#22d3ee"),
            ("12:00", "Lunch with Sam", "#a3e635"),
            ("15:00", "Ship 1.2", "#f472b6"),
            ("18:30", "Climbing", "#fbbf24"),
        )
        rows = [pz.row([
            pz.text(time, size=12, color=MUTED, font="mono", width=48),
            pz.container(None, width=4, height=28).style(background=color, radius=2, bloom=(color, 0.6)),
            pz.text(title, size=14, color=WHITE),
        ], spacing=12, align="center") for time, title, color in events]
        return self.card("Today", "calendar", pz.column(rows, spacing=10), 440)

    def header(self):
        return pz.row([
            pz.column([pz.text("Good evening, Kim", size=28, weight="bold", color=WHITE),
                       pz.text("Saturday · 3 rooms on · 21.5° inside", size=14, color=MUTED)], spacing=2),
            pz.space(width="fill"),
            pz.row([pz.icon("search", size=16, color=MUTED), pz.text("Search devices", size=13, color=MUTED)],
                   spacing=8, align="center").style(glass=GLASS, radius=20, padding=(10, 16), width=240),
        ], align="center", width=1124)

    def view(self):
        grid = pz.row([
            pz.column([self.music(), self.schedule()], spacing=20),
            self.lights_card(),
            self.climate(),
        ], spacing=20, align="start")
        content = pz.column([self.header(), pz.row(pz.stagger([grid], "fade_in_up", step=60))], spacing=24)
        return pz.stack([
            pz.shader(SHADER, width="fill", height="fill",
                      fallback=pz.Gradient(["#0b1026", "#5b1a4a", "#0f4c5c"], angle=135)),
            pz.container(content, width="fill", height="fill", align_x="center", align_y="center"),
        ])

if __name__ == "__main__":
    pz.run(Dashboard)
```
