Splash Screen¶
the branded boot splash and a reusable loading screen.
▶ Run in browserTags: 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 viaApp(splash=..., splash_min_time=..., unbranded=...).Reusing
SplashScreenfor scene transitions: bindprogressto any() -> floator an object with a.progressattribute (an assetBatchHandleworks as-is).skippable=True: once loading completes, any key or click skips the hold.The
finishedsignal 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()