Layer post

per-CanvasLayer post-processing (bloom the world, keep the HUD crisp).

▶ Run in browser

Tags: 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())