# Shaders y efectos

> Efectos de GPU en apps de escritorio con Python: once grafos de shader hechos con nodos en Pyzed Design (plasma, fuego, nebulosa, synthwave, radar...) y los efectos de cristal, bloom, resplandor, desenfoque y animación que tiene cada widget.

Source: https://albakore.github.io/pyzed/es/showcase/shaders/

Pyzed lo dibuja todo con la GPU, así que una app de escritorio puede tener lo que tienen los videojuegos: fondos en
movimiento, luz que desborda de lo brillante, cristal esmerilado. Hay dos capas:

- **Shaders**: un pequeño programa que la tarjeta gráfica ejecuta para cada píxel de un widget, en cada fotograma.
  En Pyzed los haces como **grafos de nodos** en la vista de shader de [Pyzed Design](https://albakore.github.io/pyzed/es/shading/), guardados
  como archivos `.pzshad`, o en Python con `pyzed.shading.Graph`. No hay que aprender un lenguaje de shaders.
- **Efectos**: `glass`, `bloom`, `glow`, `blur`, rotación y animaciones, que cualquier widget tiene con
  `widget.style(...)` y `widget.animate(...)`.

## Seis shaders a la vez

Seis grafos lado a lado, con un deslizador que cambia la variable `speed` de todos a la vez. Las variables
(uniforms) son solo valores, así que cambiarlas nunca recompila el shader.

```python
from pyzed import shading

plasma = shading.load("shaders/plasma.pzshad")          # se compila una vez
pz.shader(plasma, uniforms={"speed": self.speed},       # variables por nombre, desde tu estado
          width=340, height=210,
          fallback=pz.Gradient(["#7c3aed", "#ec4899"]))  # se muestra donde no hay shaders
```

<details>
<summary>Código completo: <code>examples/showcase/shaders.py</code></summary>
`examples/showcase/shaders.py`:

```py
"""A gallery of shader graphs: plasma, a ripple that follows the pointer, a tunnel, a CRT
screen, Voronoi cells and a fire. Each is a ``.pzshad`` file in ``shaders/`` made of nodes
(no WGSL to write): open one in Pyzed Design (``pyzed design shaders/fire.pzshad``) to see
and change its graph.

    python examples/showcase/shaders.py

``pz.shader`` compiles the graph and runs it on the GPU for every pixel, every frame::

    pz.shader("shaders/plasma.pzshad", uniforms={"speed": 2.0}, width=340, height=210)

``uniforms`` sets the shader's mutable variables by name; the slider below changes
``speed`` on every shader without recompiling.
"""

from pathlib import Path

import pyzed as pz
from pyzed import shading

HERE = Path(__file__).parent / "shaders"
NAMES = {"Plasma": "plasma", "Ripple": "ripple", "Tunnel": "tunnel", "CRT": "crt", "Voronoi": "voronoi",
         "Fire": "fire"}
SHADERS = {name: shading.load(HERE / f"{file}.pzshad") for name, file in NAMES.items()}
FALLBACKS = {
    "Plasma": pz.Gradient(["#7c3aed", "#ec4899", "#f59e0b"], angle=45),
    "Ripple": pz.Gradient(["#0c4a6e", "#0891b2"], angle=180),
    "Tunnel": pz.Gradient(["#1e1b4b", "#0e7490"], angle=135),
    "CRT": pz.Gradient(["#ef4444", "#22c55e", "#3b82f6"], angle=90),
    "Voronoi": pz.Gradient(["#111827", "#4c1d95"], angle=135),
    "Fire": pz.Gradient(["#000000", "#b91c1c", "#fbbf24"], angle=180),
}
TINTS = {"Ripple": "#7dd3fc", "Voronoi": "#a78bfa"}

class Shaders(pz.App):
    title = "Shaders"
    size = (1140, 720)
    theme = pz.themes.ONE_DARK

    def __init__(self):
        self.speed = 1.0

    def set_speed(self, value):
        self.speed = value

    def tile(self, name):
        shader = SHADERS[name]
        uniforms = {"speed": self.speed}
        if name in TINTS:
            uniforms["tint"] = TINTS[name]
        art = pz.shader(shader, uniforms=uniforms, width=340, height=210, fallback=FALLBACKS[name])
        label = pz.row([
            pz.text(name, size=14, weight="semibold", color="#ffffff"),
            pz.space(width="fill"),
            pz.text(f"{len(shader.graph.nodes)} nodes", size=11, color="#ffffff88", font="mono"),
        ], width=340, align="center")
        return pz.column([art, label],
                         spacing=8).animate("lift", on="hover")

    def view(self):
        names = list(SHADERS)
        grid = pz.column([pz.row([self.tile(n) for n in names[i:i + 3]], spacing=20) for i in (0, 3)],
                         spacing=22)
        header = pz.row([
            pz.column([
                pz.text("Shaders", size=28, weight="bold", color="#ffffff"),
                pz.text("Node graphs from Pyzed Design, running on the GPU", size=13, color="#ffffffaa"),
            ], spacing=2),
            pz.space(width="fill"),
            pz.text("Speed", size=13, color="#ffffffaa"),
            pz.slider(0.0, 3.0, self.speed, step=0.05, on_change=self.set_speed, width=200),
            pz.text(f"{self.speed:.2f}×", size=13, color="#ffffff", font="mono", width=48),
        ], spacing=12, align="center", width=1060)
        return pz.container(pz.column([header, pz.row(pz.stagger([grid], "fade_in_up"))], spacing=24),
                            width="fill", height="fill", align_x="center", align_y="center").style(
            background="#0b0d14")

if __name__ == "__main__":
    pz.run(Shaders)
```
</details>

## Todos los grafos

Los once grafos viven en
[`examples/showcase/shaders`](https://github.com/albakore/pyzed/tree/main/examples/showcase/shaders). Haz clic en uno
para ver su archivo, o ábrelo en el diseñador para ver y cambiar los nodos:
`pyzed design examples/showcase/shaders/fire.pzshad`.

.pzshad</code> dibujado con <code>pz.shader</code>." size="wide" />

speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/plasma.pzshad', image: plasma, tag: '28 nodos' },
{ title: 'Ripple', text: 'Anillos de ondas alrededor del puntero. Variables: <code>speed</code>, <code>tint</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/ripple.pzshad', image: ripple, tag: 'Mouse' },
{ title: 'Tunnel', text: 'Un túnel a cuadros que viene hacia ti. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/tunnel.pzshad', image: tunnel, tag: '29 nodos' },
{ title: 'CRT', text: 'Barras de color en una pantalla curva, con líneas de barrido y una banda que rueda. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/crt.pzshad', image: crt, tag: '28 nodos' },
{ title: 'Voronoi', text: 'Celdas que se desplazan con bordes brillantes. Variables: <code>speed</code>, <code>tint</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/voronoi.pzshad', image: voronoi, tag: '14 nodos' },
{ title: 'Fire', text: 'Llamas de ruido fractal que suben desde abajo. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/fire.pzshad', image: fire, tag: '23 nodos' },
{ title: 'Nebula', text: 'Nubes de ruido deformado y estrellas que titilan; el fondo del HUD. Variable: <code>drift</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/nebula.pzshad', image: nebula, tag: '40 nodos' },
{ title: 'Radar', text: 'Un barrido con estela, anillos y una cruz; el minimapa del HUD. Variables: <code>accent</code>, <code>sweep</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/radar.pzshad', image: radar, tag: '41 nodos' },
{ title: 'Embers', text: 'Chispas que suben desde un resplandor cálido; el fondo del inventario. Variable: <code>rise</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/embers.pzshad', image: embers, tag: '100 nodos' },
{ title: 'Synthwave', text: 'Cielo de atardecer, estrellas, un sol a rayas y una cuadrícula que se inclina hacia el puntero; el fondo del menú principal.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/synthwave.pzshad', image: synthwave, tag: 'Mouse' },
{ title: 'Blobs', text: 'Manchas de color suaves que se desplazan lento, pensadas para ir detrás de paneles de cristal. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/blobs.pzshad', image: blobs, tag: '74 nodos' },
]}
/>

**Haz el tuyo:**

Empieza desde una plantilla en Pyzed Design (<kbd>Ctrl</kbd>+<kbd>N</kbd> → **Shader**) y sigue
[Un efecto de shader desde la vista de shader](https://albakore.github.io/pyzed/es/tutorials/intermediate/shader-effect/), o construye uno
nodo a nodo en Python con [Un grafo de shader propio](https://albakore.github.io/pyzed/es/tutorials/advanced/shader-graph/). La sección
[Shading](https://albakore.github.io/pyzed/es/shading/) documenta cada nodo.

## Efectos en cualquier widget

Rotación, cristal esmerilado, bloom y desenfoque, aplicados con `style(...)` y animados. Los botones de abajo animan
el desenfoque y el tamaño; el tamaño, el relleno y el tamaño de letra recolocan el layout mientras se mueven.

```python
card.style(rotate=-8)                       # girarla
card.style(glass=True)                      # un panel tintado que desenfoca lo que tiene detrás
card.style(bloom=("#fbbf24", 0.8))          # luz alrededor de sus partes brillantes
card.style(blur=4)                          # desenfocar el propio widget
spinner.animate("spin")                     # un preset que se repite
```

<details>
<summary>Código completo: <code>examples/effects.py</code></summary>
`examples/effects.py`:

```py
"""Effects drawn by the renderer: rotation, blur, backdrop blur, glass and bloom, animated.

    python examples/effects.py

- ``style(rotate=-8)`` turns a widget, ``blur=4`` blurs it and ``backdrop_blur=12`` blurs
  what is behind it; ``glass=True`` is a tinted panel with a backdrop blur.
- ``bloom=(color, intensity)`` lights up the area around the bright parts of a widget.
- ``animate("spin")`` and ``transition(rotate=..., blur=..., width=..., font_size=...)``
  animate them; size, padding and font size reflow the layout as they move.
"""

import pyzed as pz
from pyzed import ui

BACKDROP = pz.Gradient(["#ff006e", "#3a86ff", "#06d6a0"], angle=45)

class Effects(pz.App):
    title = "Effects"
    size = (900, 460)

    def __init__(self):
        self.focused = True
        self.wide = False

    def toggle_focus(self):
        self.focused = not self.focused

    def toggle_wide(self):
        self.wide = not self.wide

    def card(self, title, **style):
        return pz.container(pz.text(title, size=18), padding=20, width=150, height=100,
                            align_x="center", align_y="center").style(radius=12, **style)

    def view(self):
        cards = pz.row([
            self.card("Rotate", background="panel", rotate=-10).animate("tilt", on="hover"),
            self.card("Glass", glass=True),
            self.card("Bloom", background="#ffffff", color="#1f2937", bloom=("#ffbe0b", 1.0)),
            self.card("Blur", background="panel").transition(blur=0 if self.focused else 6),
        ], spacing=32, align="center")
        loader = pz.container(pz.icon("loader", size=28), padding=4).animate("spin")
        bar = pz.container(pz.text("Resizable", size=14), padding=10).style(background="panel", radius=8) \
            .transition(width=420 if self.wide else 160, font_size=20 if self.wide else 14, duration=260,
                        easing="ease_out_cubic")
        controls = pz.row([
            ui.button("Blur" if self.focused else "Focus", self.toggle_focus, variant="outline"),
            ui.button("Resize", self.toggle_wide, variant="outline"),
            loader,
            bar,
        ], spacing=16, align="center")
        return pz.container(pz.column([cards, controls], spacing=48, align="center"), width="fill",
                            height="fill", align_x="center", align_y="center").style(background=BACKDROP)

if __name__ == "__main__":
    Effects().run()
```
</details>

## Animaciones

Cada preset, que se reproduce al hacer clic o al pasar el puntero; cristal, resplandor, bloom y opacidad sobre un
degradado; y transiciones que animan un panel, una página y una contraseña incorrecta.

```python
button.animate("lift", on="hover").animate("shrink", on="press")   # con eventos
title.animate("fade_in_up")                                         # al aparecer
panel.transition(x=0 if self.open else -260, opacity=1 if self.open else 0)  # al cambiar un valor
pz.presence(page, key=self.page)                                    # sale y entra al cambiar la key
```

<details>
<summary>Código completo: <code>examples/animations.py</code></summary>
`examples/animations.py`:

```py
"""Animations and effects: presets, hover and press motion, transitions, presence and glass.

    python examples/animations.py

- ``widget.animate("fade_in_up")`` plays a preset when the widget appears; ``on="hover"``,
  ``"press"``, ``"focus"``, ``"click"`` and ``"active"`` play it on events.
- ``widget.transition(opacity=...)`` animates a value whenever it changes, like CSS.
- ``pz.presence(content, key=...)`` animates content out and in when ``key`` changes.
- ``style(glass=True)``, ``glow=``, ``bloom=`` and ``opacity=`` are visual effects.
"""

import pyzed as pz
from pyzed import ui

AURORA = pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=135)
PAGES = ("Overview", "Activity", "Settings")

class Animations(pz.App):
    title = "Animations"
    size = (980, 700)
    transition = "zoom"

    def __init__(self):
        self.page = 0
        self.panel = True
        self.errors = 0
        self.recording = False

    def toggle_panel(self):
        self.panel = not self.panel

    def next_page(self):
        self.page = (self.page + 1) % len(PAGES)

    def fail(self):
        self.errors += 1

    def toggle_recording(self):
        self.recording = not self.recording

    def tile(self, name):
        preset = pz.animation.PRESETS[name]
        on = preset.on if preset.on != "mount" else "click"
        repeat = 1 if preset.category == "loop" and on == "click" else None
        return pz.column([
            pz.text(name.replace("_", " ")).style(weight="semibold"),
            pz.text(f"{preset.category} · {on}").style(size=11, color="text_muted"),
        ], spacing=2).style(background="elevated", border=(1, "border"), radius=8, padding=(10, 12),
                           width=150).animate(name, on=on, repeat=repeat)

    def effects(self):
        card = lambda label, **style: pz.column([  # noqa: E731
            pz.text(label).style(weight="semibold", color="#ffffff"),
            pz.text(", ".join(f"{k}={v!r}" for k, v in style.items())).style(size=11, color="#ffffffaa"),
        ], spacing=4).style(padding=14, width=200, radius=12, **style)
        return pz.row([
            card("Glass", glass=True),
            card("Glow", glow=("#22d3ee", 18), background="#0b1220"),
            card("Bloom", bloom=("#f472b6", 0.8), background="#1a0b16"),
            card("Opacity", opacity=0.55, background="#0b1220", border=(1, "#ffffff33")),
        ], spacing=18).style(padding=24, background=AURORA, radius=14, width="fill")

    def view(self):
        tiles = [self.tile(n) for n in ("pulse", "shake", "bounce", "tada", "lift", "grow", "glow", "bloom",
                                        "float", "glow_pulse")]
        side = pz.column([
            pz.text("Side panel").style(weight="semibold"),
            pz.text("transition(x=..., opacity=...)").style(size=11, color="text_muted"),
        ], spacing=4).style(background="elevated", radius=8, padding=12, width=220).transition(
            x=0 if self.panel else -24, opacity=1 if self.panel else 0, duration=220, easing="ease_out_cubic")
        page = pz.presence(
            pz.text(PAGES[self.page]).style(size=20, weight="bold"),
            key=self.page, enter="fade_in_right", exit="fade_out_left", id="pages")
        controls = pz.row([
            ui.button("Toggle panel", self.toggle_panel),
            ui.button("Next page", self.next_page),
            ui.button("Wrong password", self.fail).animate("shake", key=self.errors),
            ui.button("Record" if not self.recording else "Stop", self.toggle_recording).animate(
                "glow_pulse", active=self.recording).animate("shrink"),
        ], spacing=10)
        return pz.column([
            pz.text("Animations").style(size=24, weight="bold").animate("fade_in_down"),
            pz.row(pz.stagger(tiles, "fade_in_up", step=40), spacing=10, wrap=True, line_spacing=10),
            self.effects().animate("fade_in_up", delay=200),
            controls,
            pz.row([side, page], spacing=20),
        ], spacing=18).style(padding=24, background="background", width="fill", height="fill")

if __name__ == "__main__":
    pz.run(Animations)
```
</details>

## Estilos

La parte parecida a CSS: bordes, radios, degradados, sombras, tipografía, estilos con nombre con
`pz.define_style` y pinturas con máscara de opacidad.

Más en [Animación y efectos](https://albakore.github.io/pyzed/es/animation/) y en la sección de [Temas y estilos](https://albakore.github.io/pyzed/es/theming/).

## Dónde se usan

- [HUD de videojuego](https://albakore.github.io/pyzed/es/showcase/hud/): los grafos Nebula y Radar, paneles de cristal y bloom.
- [Menú principal](https://albakore.github.io/pyzed/es/showcase/main-menu/): el grafo Synthwave y un título brillante.
- [Inventario RPG](https://albakore.github.io/pyzed/es/showcase/inventory/): el grafo Embers, bloom según la rareza y estadísticas animadas.
- [Centro de control de cristal](https://albakore.github.io/pyzed/es/showcase/glass-dashboard/): el grafo Blobs detrás de tarjetas de cristal
  esmerilado.

**Cuando no hay GPU:**

En máquinas sin una tarjeta gráfica compatible (algunas máquinas virtuales y escritorios remotos) los shaders no
pueden correr, y Pyzed dibuja en su lugar el `fallback` del widget, un degradado o un color. Dale siempre uno a los
shaders que van detrás de tu contenido.
