Screen Effects 2D

CRT scanlines, pixelate and blur.

▶ Run in browser

Tags: 2d post-processing crt pixelate blur

A pure-2D scene opts into post-processing by adding a WorldEnvironment. Three of its screen-space effects suit 2D art, and each is a flag with one setting:

  • crt_enabled darkens the frame along horizontal scanlines and shades the corners like a curved tube. crt_intensity (0 to 1) is how strong both are.

  • pixelate_enabled samples the frame on a coarse grid so it reads as a mosaic. pixelate_size (1 to 64) is the edge of one block in pixels.

  • blur_enabled softens the frame with a small box blur. blur_radius (0 to 8) is how far apart its taps are, in pixels.

The settings are ranged Properties that clamp what they are given, so the arrow keys can nudge them without bounds checks. The effects stack, and one that is off costs nothing: a 2D scene with none of them on never takes the post-processing path.

The colour bars are world content and go through the effects. The buttons and the status line sit on a CanvasLayer, which pins them to the screen and keeps them sharp. Each button is an action button, Button(action="crt", text="CRT"): a click, a tap and the C key all reach the same Input.just_pressed("crt").

Controls: C, P and B (or the buttons) toggle CRT, pixelate and blur; Up / Down adjust the setting of the first effect that is on, in that order.

Source

  1# /// simvx
  2# tags = ["2d", "post-processing", "crt", "pixelate", "blur"]
  3# web = { width = 1280, height = 720, reason = "2D screen effects route through the full HDR pipeline on web." }
  4# ///
  5"""Screen Effects 2D: CRT scanlines, pixelate and blur.
  6
  7A pure-2D scene opts into post-processing by adding a ``WorldEnvironment``. Three of its
  8screen-space effects suit 2D art, and each is a flag with one setting:
  9
 10- ``crt_enabled`` darkens the frame along horizontal scanlines and shades the corners
 11  like a curved tube. ``crt_intensity`` (0 to 1) is how strong both are.
 12- ``pixelate_enabled`` samples the frame on a coarse grid so it reads as a mosaic.
 13  ``pixelate_size`` (1 to 64) is the edge of one block in pixels.
 14- ``blur_enabled`` softens the frame with a small box blur. ``blur_radius`` (0 to 8) is
 15  how far apart its taps are, in pixels.
 16
 17The settings are ranged Properties that clamp what they are given, so the arrow keys
 18can nudge them without bounds checks. The effects stack, and one that is off costs
 19nothing: a 2D scene with none of them on never takes the post-processing path.
 20
 21The colour bars are world content and go through the effects. The buttons and the
 22status line sit on a ``CanvasLayer``, which pins them to the screen and keeps them
 23sharp. Each button is an action button, ``Button(action="crt", text="CRT")``: a click,
 24a tap and the C key all reach the same ``Input.just_pressed("crt")``.
 25
 26Controls: C, P and B (or the buttons) toggle CRT, pixelate and blur; Up / Down adjust
 27the setting of the first effect that is on, in that order.
 28"""
 29
 30from simvx.core import (
 31    Anchor,
 32    Axis1D,
 33    Button,
 34    CanvasLayer,
 35    Input,
 36    Key,
 37    Label,
 38    Node2D,
 39    Polygon2D,
 40    VBoxContainer,
 41    WorldEnvironment,
 42)
 43from simvx.graphics import App
 44
 45PALETTE = [
 46    (0.85, 0.25, 0.30, 1.0),
 47    (0.25, 0.70, 0.85, 1.0),
 48    (0.95, 0.80, 0.25, 1.0),
 49    (0.40, 0.85, 0.45, 1.0),
 50    (0.70, 0.45, 0.90, 1.0),
 51    (0.95, 0.55, 0.25, 1.0),
 52]
 53COLS, ROWS = 6, 4
 54EFFECTS = {"crt": "CRT", "pixelate": "Pixelate", "blur": "Blur"}
 55
 56
 57class ScreenEffects2D(Node2D):
 58    input_context = {
 59        "crt": [Key.C],
 60        "pixelate": [Key.P],
 61        "blur": [Key.B],
 62        "adjust": [Axis1D(low=Key.DOWN, high=Key.UP)],
 63    }
 64
 65    def on_ready(self):
 66        self.env = self.add_child(WorldEnvironment(crt_enabled=True))
 67        # A grid of crisp bars: pixelate turns their edges into blocks, blur softens them
 68        # and the scanlines run across the whole field.
 69        # Each bar is a unit square, sized to its grid cell through its scale.
 70        square = [(0, 0), (1, 0), (1, 1), (0, 1)]
 71        self.bars = [
 72            self.add_child(Polygon2D(polygon=square, colour=PALETTE[(i // COLS + i % COLS) % len(PALETTE)]))
 73            for i in range(COLS * ROWS)
 74        ]
 75
 76        hud = self.add_child(CanvasLayer())
 77        buttons = hud.add_child(VBoxContainer(anchor=Anchor.TOP_LEFT, inset=12))
 78        self.buttons = {name: buttons.add_child(Button(action=name, text=text)) for name, text in EFFECTS.items()}
 79        self.status = hud.add_child(Label(anchor=Anchor.BOTTOM_LEFT, inset=12))
 80
 81    def on_update(self, dt):
 82        env = self.env
 83        if Input.just_pressed("crt"):
 84            env.crt_enabled = not env.crt_enabled
 85        if Input.just_pressed("pixelate"):
 86            env.pixelate_enabled = not env.pixelate_enabled
 87        if Input.just_pressed("blur"):
 88            env.blur_enabled = not env.blur_enabled
 89
 90        step = Input.value("adjust") * dt
 91        if env.crt_enabled:
 92            env.crt_intensity += step
 93        elif env.pixelate_enabled:
 94            env.pixelate_size += 20.0 * step
 95        elif env.blur_enabled:
 96            env.blur_radius += 4.0 * step
 97
 98        # Lay the bars out across the window, leaving a margin around the grid.
 99        w, h = self.viewport_size
100        cell_w, cell_h = w * 0.9 / COLS, h * 0.72 / ROWS
101        for i, bar in enumerate(self.bars):
102            bar.scale = (cell_w * 0.9, cell_h * 0.85)
103            bar.position = (w * 0.05 + (i % COLS) * cell_w, h * 0.18 + (i // COLS) * cell_h)
104
105        # Example help overlay: not part of the technique shown.
106        self.buttons["crt"].checked = env.crt_enabled
107        self.buttons["pixelate"].checked = env.pixelate_enabled
108        self.buttons["blur"].checked = env.blur_enabled
109        self.status.text = (
110            f"CRT intensity {env.crt_intensity:.2f}   pixelate size {env.pixelate_size:.0f} px   "
111            f"blur radius {env.blur_radius:.2f}\nUp / Down adjust the first effect that is on."
112        )
113
114
115if __name__ == "__main__":
116    App(title="Screen Effects 2D", width=1280, height=720).run(ScreenEffects2D())