Skip to content

Shaders and effects

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

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.

Six shaders: plasma, ripple, tunnel, CRT, Voronoi cells and fire
Windows, macOS and Linux
uv run --with pyzed-ui python examples/showcase/shaders.py
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
Full code: 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)

All eleven graphs live in 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.

Six of the graphs, animated: each one is a .pzshad drawn with pz.shader.

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.

Four cards over a gradient: rotated, glass, bloom and blurred
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
Windows, macOS and Linux
uv run --with pyzed-ui python examples/effects.py
Full code: 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()

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.

A grid of animation presets and four visual effects
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
Windows, macOS and Linux
uv run --with pyzed-ui python examples/animations.py
Full code: 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)

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

Buttons and a card styled with borders, gradients and pills
Windows, macOS and Linux
uv run --with pyzed-ui python examples/styles.py

More in Animation and effects and the Theming section.