# Shaders and effects

> GPU effects in Python desktop apps: eleven shader graphs made of nodes in Pyzed Design (plasma, fire, nebula, synthwave, radar...), and the glass, bloom, glow, blur and animation effects every widget has.

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

Pyzed draws everything with the GPU, so a desktop app can have what games have: moving backgrounds, light that
spills out of bright things, frosted glass. There are two layers to it:

- **Shaders**: a small program that the graphics card runs for every pixel of a widget, every frame. In Pyzed you
  make them as **graphs of nodes** in the Shader view of [Pyzed Design](https://albakore.github.io/pyzed/shading/), saved as `.pzshad` files,
  or in Python with `pyzed.shading.Graph`. No shader language to learn.
- **Effects**: `glass`, `bloom`, `glow`, `blur`, rotation and animations, which any widget has through
  `widget.style(...)` and `widget.animate(...)`.

## The shader gallery

Six graphs side by side, with a slider that changes the `speed` variable of all of them at once. Variables
(uniforms) are just values, so changing them never recompiles the shader.

```python
from pyzed import shading

plasma = shading.load("shaders/plasma.pzshad")          # compile once
pz.shader(plasma, uniforms={"speed": self.speed},       # variables by name, from your state
          width=340, height=210,
          fallback=pz.Gradient(["#7c3aed", "#ec4899"]))  # shown where shaders are not available
```

<details>
<summary>Full code: <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>

## Every graph

All eleven graphs live in [`examples/showcase/shaders`](https://github.com/albakore/pyzed/tree/main/examples/showcase/shaders).
Click one to see its file, or open it in the designer to see and change the nodes:
`pyzed design examples/showcase/shaders/fire.pzshad`.

.pzshad</code> drawn with <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 nodes' },
{ title: 'Ripple', text: 'Rings of waves around the pointer. 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: 'A checkered tunnel rushing towards you. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/tunnel.pzshad', image: tunnel, tag: '29 nodes' },
{ title: 'CRT', text: 'Color bars on a curved screen, with scanlines and a rolling band. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/crt.pzshad', image: crt, tag: '28 nodes' },
{ title: 'Voronoi', text: 'Wandering cells with glowing borders. Variables: <code>speed</code>, <code>tint</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/voronoi.pzshad', image: voronoi, tag: '14 nodes' },
{ title: 'Fire', text: 'Fractal noise flames rising from the bottom. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/fire.pzshad', image: fire, tag: '23 nodes' },
{ title: 'Nebula', text: 'Warped noise clouds and twinkling stars; the HUD\'s backdrop. Variable: <code>drift</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/nebula.pzshad', image: nebula, tag: '40 nodes' },
{ title: 'Radar', text: 'A sweep with a fading trail, rings and a cross; the HUD\'s minimap. Variables: <code>accent</code>, <code>sweep</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/radar.pzshad', image: radar, tag: '41 nodes' },
{ title: 'Embers', text: 'Sparks drifting up from a warm glow; the inventory\'s backdrop. Variable: <code>rise</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/embers.pzshad', image: embers, tag: '100 nodes' },
{ title: 'Synthwave', text: 'Dusk sky, stars, a striped sun and a grid that leans towards the pointer; the main menu\'s backdrop.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/synthwave.pzshad', image: synthwave, tag: 'Mouse' },
{ title: 'Blobs', text: 'Soft color blobs drifting slowly, made to sit behind glass panels. Variable: <code>speed</code>.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/blobs.pzshad', image: blobs, tag: '74 nodes' },
]}
/>

**Make your own:**

Start from a template in Pyzed Design (<kbd>Ctrl</kbd>+<kbd>N</kbd> → **Shader**) and follow
[A shader effect from the Shader view](https://albakore.github.io/pyzed/tutorials/intermediate/shader-effect/), or build one node by node in
Python with [A custom shader graph](https://albakore.github.io/pyzed/tutorials/advanced/shader-graph/). The [Shading](https://albakore.github.io/pyzed/shading/)
section documents every node.

## Effects on any widget

Rotation, frosted glass, bloom and blur, applied with `style(...)` and animated. The buttons at the bottom animate
blur and size; size, padding and font size reflow the layout as they move.

```python
card.style(rotate=-8)                       # turn it
card.style(glass=True)                      # a tinted panel that blurs what is behind it
card.style(bloom=("#fbbf24", 0.8))          # light around its bright parts
card.style(blur=4)                          # blur the widget itself
spinner.animate("spin")                     # a looping preset
```

<details>
<summary>Full code: <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>

## Animations

Every preset, played on click or on hover; glass, glow, bloom and opacity over a gradient; and transitions that
animate a panel, a page and a wrong password.

```python
button.animate("lift", on="hover").animate("shrink", on="press")   # on events
title.animate("fade_in_up")                                         # when it appears
panel.transition(x=0 if self.open else -260, opacity=1 if self.open else 0)  # when a value changes
pz.presence(page, key=self.page)                                    # out and in when the key changes
```

<details>
<summary>Full code: <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>

## Styles

The CSS-like side: borders, radius, gradients, shadows, typography, named styles with `pz.define_style`, and
paints with an opacity mask.

More in [Animation and effects](https://albakore.github.io/pyzed/animation/) and the [Theming](https://albakore.github.io/pyzed/theming/) section.

## Where they are used

- [Game HUD](https://albakore.github.io/pyzed/showcase/hud/): the Nebula and Radar graphs, glass panels and bloom.
- [Game main menu](https://albakore.github.io/pyzed/showcase/main-menu/): the Synthwave graph and a glowing title.
- [RPG inventory](https://albakore.github.io/pyzed/showcase/inventory/): the Embers graph, bloom by rarity and animated stats.
- [Glass control center](https://albakore.github.io/pyzed/showcase/glass-dashboard/): the Blobs graph behind frosted-glass cards.

**When there is no GPU:**

On machines without a supported graphics card (some virtual machines and remote desktops) shaders cannot run, and
Pyzed draws the widget's `fallback` instead, a gradient or a color. Always give one to shaders that sit behind
your content.
