Splash Screen

the branded boot splash and a reusable loading screen.

▶ Run in browser

Tags: ui splash loading branding

Every windowed SimVX game boots with the engine’s branded splash: a fade in from black, the SIMVX wordmark over a faint centre-lit grid, a segmented gradient progress bar, then a fade back to black once the first frame is ready (a one-second minimum stops sub-second flicker on fast hardware). This demo runs with that default, then lets you re-trigger the same SplashScreen control as an in-game loading screen driven by a real 0..1 progress source: the pattern for heavy scene transitions.

What it demonstrates

  • The default boot splash (App(splash=None)): automatic on windowed runs; configure via App(splash=..., splash_min_time=..., unbranded=...).

  • Reusing SplashScreen for scene transitions: bind progress to any () -> float or an object with a .progress attribute (an asset BatchHandle works as-is).

  • skippable=True: once loading completes, any key or click skips the hold.

  • The finished signal driving teardown (splash.destroy).

Controls: L / Click / Tap - Simulate loading a level behind a SplashScreen Escape - Quit

Usage: uv run python examples/features/ui/splash_screen.py uv run python examples/features/ui/splash_screen.py –test

Source

  1"""Splash Screen: the branded boot splash and a reusable loading screen.
  2
  3Every windowed SimVX game boots with the engine's branded splash: a fade in
  4from black, the SIMVX wordmark over a faint centre-lit grid, a segmented
  5gradient progress bar, then a fade back to black once the first frame is ready
  6(a one-second minimum stops sub-second flicker on fast hardware). This demo
  7runs with that default, then lets you re-trigger the same `SplashScreen`
  8control as an in-game loading screen driven by a real 0..1 progress source:
  9the pattern for heavy scene transitions.
 10
 11# /// simvx
 12# tags = ["ui", "splash", "loading", "branding"]
 13# web = { root = "SplashDemo", width = 800, height = 600, responsive = true }
 14# ///
 15
 16## What it demonstrates
 17
 18- The default boot splash (`App(splash=None)`): automatic on windowed runs;
 19  configure via `App(splash=..., splash_min_time=..., unbranded=...)`.
 20- Reusing `SplashScreen` for scene transitions: bind `progress` to any
 21  `() -> float` or an object with a `.progress` attribute (an asset
 22  `BatchHandle` works as-is).
 23- `skippable=True`: once loading completes, any key or click skips the hold.
 24- The `finished` signal driving teardown (`splash.destroy`).
 25
 26Controls:
 27  L / Click / Tap - Simulate loading a level behind a SplashScreen
 28  Escape          - Quit
 29
 30Usage:
 31    uv run python examples/features/ui/splash_screen.py
 32    uv run python examples/features/ui/splash_screen.py --test
 33"""
 34
 35import math
 36import sys
 37
 38from simvx.core import AnchorPreset, Colour, Control, Input, Key, Label, MouseButton, SplashScreen
 39from simvx.graphics import App
 40
 41WIDTH, HEIGHT = 800, 600
 42LOAD_TIME = 2.5  # seconds the simulated level load takes
 43
 44
 45class SplashDemo(Control):
 46    input_actions = {"load": [Key.L, MouseButton.LEFT], "quit": [Key.ESCAPE]}
 47
 48    def on_ready(self):
 49        self.set_anchor_preset(AnchorPreset.FULL_RECT)  # top-level Controls anchor, never absolute-size
 50        self._t = 0.0
 51        self._level = 1
 52        self._splash: SplashScreen | None = None
 53        self._load_started_at = 0.0
 54        self._hint = self.add_child(
 55            Label("Press L (or click) to load the next level behind a SplashScreen", font_size=18)
 56        )
 57        self._hint.set_anchor_preset(AnchorPreset.TOP_LEFT)
 58        self._hint.margin_left = 24
 59        self._hint.margin_top = 24
 60
 61    # ------------------------------------------------------------- loading
 62
 63    def _begin_load(self):
 64        if self._splash is not None:
 65            return
 66        self._load_started_at = self.tree.now
 67        # A real game passes an asset BatchHandle here; this demo fakes the
 68        # same 0..1 contract off scene time.
 69        self._splash = SplashScreen(
 70            progress=self._load_progress,
 71            min_display_time=1.0,
 72            skippable=True,
 73        )
 74        self._splash.set_progress(0.0, f"loading level {self._level + 1}")
 75        self._splash.finished.connect(self._end_load)
 76        self.add_child(self._splash)  # last child: draws over the scene
 77
 78    def _load_progress(self) -> float:
 79        return min(1.0, (self.tree.now - self._load_started_at) / LOAD_TIME)
 80
 81    def _end_load(self):
 82        self._level += 1
 83        self._hint.text = f"Level {self._level} loaded. Press L to load another."
 84        splash, self._splash = self._splash, None
 85        splash.destroy()
 86
 87    # ------------------------------------------------------------- frame
 88
 89    def on_update(self, dt: float):
 90        self._t += dt
 91        if Input.is_action_just_pressed("load"):
 92            self._begin_load()
 93        if Input.is_action_just_pressed("quit"):
 94            self.app.quit()
 95        self.queue_redraw()  # animated background below
 96
 97    def on_draw(self, renderer):
 98        w, h = self.tree.screen_size
 99        # A live background per level hue, so the loading screen visibly covers
100        # a running scene and the reveal fades back onto it.
101        hue = (self._level * 0.13) % 1.0
102        base = Colour.hex("#173042") if self._level % 2 else Colour.hex("#2b1a38")
103        renderer.draw_rect((0, 0), (w, h), colour=base, filled=True)
104        cx, cy = w / 2, h / 2
105        for i in range(8):
106            a = self._t * 0.8 + i * math.tau / 8
107            x = cx + math.cos(a) * (120 + 40 * math.sin(self._t + i))
108            y = cy + math.sin(a) * (90 + 30 * math.cos(self._t + i))
109            radius = 14 + 6 * math.sin(self._t * 2 + i)
110            renderer.draw_circle((x, y), radius, colour=(0.2 + hue * 0.5, 0.8, 0.7, 0.85), filled=True)
111
112
113def _selftest() -> bool:
114    """Headless: run a whole load behind the splash and check every stage of it.
115
116    The load is started by the key the hint advertises, and ended by the skip the
117    demo opts into -- an unbound key press once loading has finished -- so both the
118    `load` action and `skippable` are exercised through the real input path. The
119    progress bar's source is the demo's own 0..1 callable, so what is sampled is
120    what a game's asset batch would drive.
121    """
122    from simvx.core.testing import InputSimulator
123    from simvx.graphics.testing import assert_not_blank, save_png
124
125    app = App(title="SimVX Splash Screen", width=WIDTH, height=HEIGHT, visible=False)
126    scene = SplashDemo(name="SplashDemo")
127    sim = InputSimulator()
128    seen: dict[str, object] = {}
129
130    START, RETRY = 4, 8
131    SAMPLE_AT = (30, 90, 160)  # a fifth, half way, and past the end of a 2.5s load
132    SKIP = 170
133
134    def on_frame(idx: int, _t: float) -> bool:
135        if idx == 0:
136            seen["level_before"] = scene._level
137        elif idx == START:
138            sim.tap_key(Key.L)
139        elif idx == START + 2:
140            splash = scene._splash
141            seen["opened"] = splash is not None
142            seen["on_top"] = splash is not None and scene.children[len(scene.children) - 1] is splash
143        elif idx == RETRY:
144            sim.tap_key(Key.L)  # a second press while one is up must be ignored
145        elif idx == RETRY + 2:
146            seen["splash_count"] = sum(1 for c in scene.children if isinstance(c, SplashScreen))
147        elif idx in SAMPLE_AT and scene._splash is not None:
148            seen.setdefault("progress", []).append((idx, scene._load_progress(), scene._splash.loaded))
149        elif idx == SKIP:
150            seen["state_before_skip"] = scene._splash.state if scene._splash else "gone"
151            sim.tap_key(Key.SPACE)  # unbound by the demo, so the splash sees it as a skip
152        elif idx == SKIP + 40:
153            seen["level_after"] = scene._level
154            seen["splash_left"] = sum(1 for c in scene.children if isinstance(c, SplashScreen))
155            seen["hint"] = scene._hint.text
156        return True
157
158    frames = app.run_headless(scene, frames=SKIP + 50, on_frame=on_frame, capture_frames=[SKIP + 10])
159    assert_not_blank(frames[0])
160    save_png(frames[0], "/tmp/splash_test.png")
161
162    ok = True
163
164    def check(label: str, passed: bool, detail: str) -> None:
165        nonlocal ok
166        ok = ok and passed
167        print(f"{'ok  ' if passed else 'FAIL'} {label}: {detail}")
168
169    check(
170        "L raises a SplashScreen over the running scene, as the last child",
171        seen["opened"] and seen["on_top"],
172        "splash added last, so it draws over the animated background",
173    )
174    check(
175        "pressing L again while one is up does not stack a second",
176        seen["splash_count"] == 1,
177        f"{seen['splash_count']} SplashScreen child after two presses",
178    )
179
180    progress = seen["progress"]
181    check(
182        "the bar's bound source climbs to 1.0 over the load, and only then reports loaded",
183        [p for _, p, _ in progress] == sorted(p for _, p, _ in progress)
184        and progress[-1][1] == 1.0
185        and progress[-1][2]
186        and not progress[0][2],
187        ", ".join(f"frame {i}: {p:.2f}{' loaded' if done else ''}" for i, p, done in progress),
188    )
189    check(
190        "it is still on screen when loading finishes, holding for its minimum time",
191        seen["state_before_skip"] in ("loading", "fade_out"),
192        f"state {seen['state_before_skip']!r} at the moment of the skip",
193    )
194    check(
195        "a key press after loading skips the hold and the splash tears itself down",
196        seen["splash_left"] == 0 and scene._splash is None,
197        f"{seen['splash_left']} SplashScreen children left",
198    )
199    check(
200        "the finished signal ran the demo's own teardown, so the level advanced",
201        seen["level_after"] == seen["level_before"] + 1 and str(seen["level_after"]) in seen["hint"],
202        f"level {seen['level_before']} -> {seen['level_after']}: {seen['hint']!r}",
203    )
204
205    print("screenshot: /tmp/splash_test.png")
206    print("SELFTEST:", "PASS" if ok else "FAIL")
207    return ok
208
209
210def main():
211    App(title="SimVX Splash Screen", width=WIDTH, height=HEIGHT).run(SplashDemo())
212
213
214if __name__ == "__main__":
215    if "--test" in sys.argv:
216        sys.exit(0 if _selftest() else 1)
217    main()