Layer post¶
per-CanvasLayer post-processing (bloom the world, keep the HUD crisp).
▶ Run in browserTags: 2d bloom post-processing canvaslayer
A CanvasLayer can carry its own environment (a WorldEnvironment), so its post
effects apply to that layer’s band ONLY. Here the game-world layer blooms while the
HUD layer on top stays crisp – the classic “glow the world, not the UI” case, with
no manual masking. A layer with no environment is the shared/global path and
costs nothing (the feature is zero-cost when unused).
What to look for:
The world layer’s bright shapes GLOW (its environment enables bloom).
The HUD layer’s panel + text are CRISP (no environment -> no post on that band).
Toggle the world layer’s bloom with B to compare.
The star row and the note are laid out from the tree’s screen_resized Signal,
so the scene recomputes its geometry when the window changes size instead of
every frame.
Controls: B - Toggle the world layer’s bloom Escape - Quit
Run: uv run python examples/features/2d/layer_post.py
Source¶
1"""Layer post: per-CanvasLayer post-processing (bloom the world, keep the HUD crisp).
2
3# /// simvx
4# tags = ["2d", "bloom", "post-processing", "canvaslayer"]
5# web = { width = 1280, height = 720, reason = "Per-layer post; the HUD layer must stay crisp." }
6# ///
7
8A CanvasLayer can carry its own ``environment`` (a WorldEnvironment), so its post
9effects apply to that layer's band ONLY. Here the game-world layer blooms while the
10HUD layer on top stays crisp -- the classic "glow the world, not the UI" case, with
11no manual masking. A layer with no ``environment`` is the shared/global path and
12costs nothing (the feature is zero-cost when unused).
13
14What to look for:
15 - The world layer's bright shapes GLOW (its environment enables bloom).
16 - The HUD layer's panel + text are CRISP (no environment -> no post on that band).
17 - Toggle the world layer's bloom with B to compare.
18
19The star row and the note are laid out from the tree's ``screen_resized`` Signal,
20so the scene recomputes its geometry when the window changes size instead of
21every frame.
22
23Controls:
24 B - Toggle the world layer's bloom
25 Escape - Quit
26
27Run: uv run python examples/features/2d/layer_post.py
28"""
29
30import math
31
32from simvx.core import CanvasLayer, Input, InputMap, Key, Node2D, Polygon2D, Text2D, WorldEnvironment
33from simvx.graphics import App
34
35W, H = 1280, 720
36
37
38def _star(cx, cy, r, colour, points=5):
39 q = Polygon2D()
40 q.polygon = [
41 (
42 math.cos(-math.pi / 2 + i * math.pi / points) * (r if i % 2 == 0 else r * 0.45),
43 math.sin(-math.pi / 2 + i * math.pi / points) * (r if i % 2 == 0 else r * 0.45),
44 )
45 for i in range(points * 2)
46 ]
47 q.colour = colour
48 q.position = (cx, cy)
49 return q
50
51
52def _rect(cx, cy, w, h, colour):
53 q = Polygon2D()
54 q.polygon = [(0, 0), (w, 0), (w, h), (0, h)]
55 q.colour = colour
56 q.position = (cx, cy)
57 return q
58
59
60class LayerPostDemo(Node2D):
61 def on_ready(self):
62 InputMap.add_action("toggle_bloom", [Key.B])
63 InputMap.add_action("quit", [Key.ESCAPE])
64
65 # --- World layer (band 0): its own environment -> blooms. ---
66 self._world = self.add_child(CanvasLayer(layer=0))
67 self._world_env = WorldEnvironment()
68 self._world_env.bloom_enabled = True
69 self._world_env.bloom_threshold = 0.7
70 self._world_env.bloom_intensity = 1.4
71 self._world.environment = self._world_env
72 neon = [(2.6, 0.4, 0.4), (0.4, 2.6, 0.8), (0.5, 0.7, 2.8), (2.6, 2.4, 0.5)]
73 self._stars = [self._world.add_child(_star(0, 0, 80, (*c, 1.0))) for c in neon]
74
75 # --- HUD layer (band 100): NO environment -> crisp, no glow. ---
76 hud = self.add_child(CanvasLayer(layer=100))
77 hud.add_child(_rect(40, 40, 360, 96, (0.10, 0.12, 0.18, 0.92)))
78 hud.add_child(Text2D(text="HUD layer (crisp)\nScore: 1234 Lives: 3", font_scale=1.1, position=(56, 56)))
79
80 self._hud_note = self.add_child(Text2D(text="", font_scale=1.0))
81 self._update_note()
82 # Lay out once now, then only when the window actually changes size: the
83 # tree's screen_resized Signal carries the new (width, height).
84 self._layout(self.tree.screen_size)
85 self.tree.screen_resized.connect(self._layout)
86
87 def on_exit_tree(self):
88 self.tree.screen_resized.disconnect(self._layout)
89
90 def _layout(self, size):
91 # Derive the layout from the live window size so nothing crops off-screen.
92 w, h = size
93 r = min(w, h) * 0.11
94 for i, star in enumerate(self._stars):
95 star.position = (w * (2 * i + 1) / (2 * len(self._stars)), h * 0.42)
96 star.scale = (r / 80, r / 80)
97 self._hud_note.position = (40, h - 60)
98
99 def on_update(self, dt):
100 if Input.is_action_just_pressed("quit"):
101 self.app.quit()
102 return
103 if Input.is_action_just_pressed("toggle_bloom"):
104 self._world_env.bloom_enabled = not self._world_env.bloom_enabled
105 self._update_note()
106
107 def _update_note(self):
108 on = self._world_env.bloom_enabled
109 self._hud_note.text = (
110 f"World layer bloom: {'ON' if on else 'OFF'} (B to toggle). "
111 "The world blooms; the HUD layer stays crisp -- per-CanvasLayer post."
112 )
113
114
115if __name__ == "__main__":
116 app = App(title="Per-Layer Post", width=W, height=H)
117 app.run(LayerPostDemo())