Ir al contenido

Shaders y efectos

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, 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 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.

Seis shaders: plasma, ondas, túnel, CRT, celdas de Voronoi y fuego
Windows, macOS y Linux
uv run --with pyzed-ui python examples/showcase/shaders.py
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
Código completo: examples/showcase/shaders.py
examples/showcase/shaders.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)

Los once grafos viven en 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.

Seis de los grafos, animados: cada uno es un .pzshad dibujado con pz.shader.

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.

Cuatro tarjetas sobre un degradado: girada, de cristal, con bloom y desenfocada
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
Windows, macOS y Linux
uv run --with pyzed-ui python examples/effects.py
Código completo: examples/effects.py
examples/effects.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()

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.

Una cuadrícula de presets de animación y cuatro efectos visuales
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
Windows, macOS y Linux
uv run --with pyzed-ui python examples/animations.py
Código completo: examples/animations.py
examples/animations.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)

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.

Botones y una tarjeta con bordes, degradados y etiquetas redondeadas
Windows, macOS y Linux
uv run --with pyzed-ui python examples/styles.py

Más en Animación y efectos y en la sección de Temas y estilos.