A small PySide6 splash screen composer with layered images and animated typography.
Project description
InspyreSplash
inspyre-splash is a small PySide6-powered splash screen composer for Python desktop apps. It builds transparent, frameless splash windows from ordered layers: static images, animated images decoded by Pillow, and animated typography.
Author: Taylor B. | Inspyre-Softworks
This is an MVP. It intentionally avoids progress bars, themes, audio, video, Lottie, and other extras so the public API stays practical.
Features
- Transparent, frameless PySide6 splash windows.
- Ordered layers where earlier layers render behind later layers.
- Static PNG/JPEG/WebP image layers backed by
QPixmap. - Animated WebP, GIF, and APNG layers decoded through Pillow.
- Text sequence layers with built-in
FadeIn,FlyIn,Typewriter,WipeIn, andExplodeIneffects. - Optional text backgrounds, opacity, shadows, custom fonts, absolute positioning, and loop control.
- Blocking
run_until()startup work for app launch flows. - Non-blocking
start_until()startup work for IPython, ptipython, notebooks, and other interactive prompts. - Cooperative cancellation through
SplashTask.cancel()and caller-ownedthreading.Eventobjects. - Headless-friendly tests with
QT_QPA_PLATFORM=offscreen.
Documentation
The full Sphinx documentation is configured for Read the Docs and lives under docs/.
poetry install --with docs --no-interaction
poetry run sphinx-build -W -b html docs docs/_build/local-html
Read the Docs builds from docs/conf.py using .readthedocs.yaml.
Install
From the project root:
poetry install
For an editable pip install:
python -m pip install -e .
Basic Example
from pathlib import Path
from inspyre_splash import Splash
from inspyre_splash.layers import AnimatedImageLayer
splash = Splash(width=700, height=700, transparent=True, stay_on_top=True)
splash.add_layer(
AnimatedImageLayer(
Path('assets/transparent_splash.webp'),
loops=None,
scale=1.0,
position='center',
)
)
splash.close_after_ms(5000)
splash.show()
splash.run()
The splash can also be used as a context manager. Exiting the block stops all layers and closes the splash widget:
with Splash(width=700, height=700) as splash:
splash.add_layer(AnimatedImageLayer(Path('assets/transparent_splash.webp')))
splash.close_after_ms(5000)
splash.show()
splash.run()
For startup work, use run_until(). It runs your callable while the splash is active, keeps Qt's event loop alive for animations, then closes the splash when the callable returns:
def load_application():
return build_main_window()
with Splash(width=700, height=700) as splash:
splash.add_layer(AnimatedImageLayer(Path('assets/transparent_splash.webp')))
main_window = splash.run_until(load_application)
splash.finish(main_window)
splash.run()
For an interactive prompt, notebook, or ptipython session where you want the prompt back immediately, use start_until() instead. It shows the splash, starts your callable on a worker thread, and returns a SplashTask handle.
%gui qt
splash = Splash(width=1200, height=1200, transparent=True, stay_on_top=True)
splash.add_layer(ImageLayer(Path(PNG), scale=1.0, position='center'))
splash.add_layer(AnimatedImageLayer(Path(WEBP), scale=1.0, position='center'))
splash.add_layer(text_sequence)
task = splash.start_until(__load__, 5, cancel_kwarg='cancel_event')
start_until() does not use the Splash context manager because leaving a with Splash(...) block closes the splash. Keep splash and task assigned until the work is done. It also shows the splash by default, so do not call splash.show() or splash.run() after start_until().
To cancel from the prompt, call:
task.cancel()
Cancellation is cooperative: the worker callable must accept and check the injected cancel_event to stop its own Python work:
def __load__(temporal_pad: int, cancel_event):
for _ in range(temporal_pad):
if cancel_event.is_set():
return 'cancelled'
sleep(1)
return 'loaded'
Later, inspect the result or any exception:
task.done()
task.result()
task.exception()
If you prefer to own the event yourself, pass it to your worker and then either set it directly or call task.cancel():
event = Event()
task = splash.start_until(__load__, 5, event)
event.set()
# or:
task.cancel()
splash.run() blocks while Qt's event loop is active. If you manage your own callbacks, use splash.stop() or splash.exit() when the work is done and you want run() to return. stop() is safe to call from a worker thread because it queues the shutdown back onto Qt's thread.
For synchronous code inside a with Splash(...) block, use splash.leave() to close the splash and leave the context immediately:
with Splash(width=700, height=700) as splash:
splash.show()
if startup_failed:
splash.leave()
splash.run()
Use leave() from normal Python control flow. Use exit() from Qt callbacks, because exceptions raised inside Qt callbacks do not reliably unwind the Python context manager.
Package Asset Discovery
Host applications can keep splash assets beside their own package and let inspyre-splash discover them. Discovery is on by default for the helper calls, but it can be disabled per call, globally, or with the INSPYRE_SPLASH_AUTO_DISCOVERY=0 environment variable.
Supported package-side layouts:
your_app/
splash.json
splash/
intro/
splash.json
transparent_splash.webp
glow.png
updater/
splash.json
updater.webp
assets/
splashes/
release/
inspyre-splash.json
release.webp
The same layouts are supported below PlatformDirs(appname='your_app').user_data_path, which lets users or installers override bundled splash assets without writing inside the package directory.
From inside the host package, call auto_splash() without arguments and it will infer the calling package:
from inspyre_splash import auto_splash
def launch():
splash = auto_splash(name='intro')
if splash is None:
return load_application()
main_window = splash.run_until(load_application)
splash.finish(main_window)
splash.run()
You can also be explicit, which is nicer for tests and command-line entry points:
from inspyre_splash import auto_splash, configure_auto_discovery
configure_auto_discovery(True)
splash = auto_splash('your_app', enabled=True)
A splash JSON definition can describe window options and ordered layers:
{
"name": "intro",
"splash": {
"width": 700,
"height": 700,
"transparent": true,
"stay_on_top": true
},
"layers": [
{
"type": "image",
"path": "glow.png",
"scale": 1.15,
"position": "center"
},
{
"type": "animated_image",
"path": "transparent_splash.webp",
"loops": null,
"scale": 1.0,
"position": "center"
},
{
"type": "text_sequence",
"font_family": "Segoe UI",
"font_size": 34,
"color": "#ffffff",
"x": 70,
"y": 565,
"background_color": "#111827",
"background_opacity": 0.62,
"shadow_color": "#000000",
"shadow_opacity": 0.7,
"loops": null,
"entries": [
{"text": "TAYLOR SUITE", "effect": "wipe_in"},
{"text": "Loading modules...", "effect": {"type": "fly_in", "direction": "bottom"}},
{"text": "Almost there...", "effect": {"type": "typewriter", "speed_ms": 42}},
{"text": "READY", "effect": "explode_in"}
]
}
]
}
Layer paths and custom font paths are resolved relative to the folder containing the JSON file, so every animation can be a self-contained folder.
Layered Typography Example
from pathlib import Path
from inspyre_splash import Splash
from inspyre_splash.effects import ExplodeIn, FlyIn, Typewriter, WipeIn
from inspyre_splash.layers import AnimatedImageLayer, ImageLayer, TextSequenceLayer
splash = Splash(width=700, height=700, transparent=True, stay_on_top=True)
splash.add_layer(
ImageLayer(
Path('assets/transparent_45deg_glow_ellipse.png'),
scale=1.15,
position='center',
)
)
splash.add_layer(
AnimatedImageLayer(
Path('assets/transparent_splash.webp'),
loops=None,
scale=1.0,
position='center',
)
)
splash.add_layer(
TextSequenceLayer(
entries=[
('TAYLOR SUITE', WipeIn()),
('Loading modules...', FlyIn(direction='bottom')),
('Almost there...', Typewriter(speed_ms=42)),
('READY', ExplodeIn()),
],
font_family='Segoe UI',
font_size=34,
color='#ffffff',
x=70,
y=565,
background_color='#111827',
background_opacity=0.62,
shadow_color='#000000',
shadow_opacity=0.7,
shadow_x_offset=3,
shadow_y_offset=3,
loops=None,
)
)
splash.show()
splash.run()
Development
Use Poetry for contributor and AI-helper workflows:
poetry install --with dev --no-interaction
$env:QT_QPA_PLATFORM = 'offscreen'
poetry run python -m unittest discover -s tests
poetry run python -m compileall src tests examples docs
See CONTRIBUTING.md for the full contributor workflow.
Notes
Animated WebP, GIF, and APNG files are decoded through Pillow with ImageSequence. Per-frame durations are respected when Pillow exposes them.
Layer order is simple: earlier layers are behind later layers. Add a static transparent PNG first, then an animated WebP, then text if the text should render above the images.
Text sequence layers use position='center' by default, or position='bottom' for bottom placement. Pass x and y to place the text at an exact top-left pixel inside the transparent splash window. Pass background_color and background_opacity to paint a colored backing behind the text. Pass shadow_color, shadow_opacity, shadow_x_offset, and shadow_y_offset to paint a drop shadow with the text.
Image layers fit inside the splash bounds by default so oversized PNG/WebP/GIF/APNG assets are not unexpectedly clipped. Pass fit_to_parent=False if you want native pixel sizing where an oversized layer can intentionally bleed outside the splash.
Animated image layers and text sequence layers accept loops=None for forever or an integer for a finite number of loops. Any layer can be interrupted with layer.interrupt(); animated layers stop their timers, text layers stop the active effect, and static layers hide themselves.
Transparent window behavior can vary by operating system, display server, and window compositor. The package requests a translucent, frameless PySide6 window, but final blending is controlled by the host OS.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file inspyre_splash-0.2.0.tar.gz.
File metadata
- Download URL: inspyre_splash-0.2.0.tar.gz
- Upload date:
- Size: 41.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 CPython/3.14.5 Windows/11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
744f603f56463ebe498c3b416036efe1e8c84370a210ff83da881ee36260c5d1
|
|
| MD5 |
c1db1ae29ac0cd4b50c5282c8c65fe00
|
|
| BLAKE2b-256 |
ec280bf823094bac4cb27209a150ac3bcfac232c05101dcc5303a34ade54cfc0
|
File details
Details for the file inspyre_splash-0.2.0-py3-none-any.whl.
File metadata
- Download URL: inspyre_splash-0.2.0-py3-none-any.whl
- Upload date:
- Size: 29.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 CPython/3.14.5 Windows/11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ef88a6a824c4e980352a1200ec58f830fd4924d128e9a1a239eefe7ab14f3ad
|
|
| MD5 |
d243270707c85f447dd2fbf7c1fd1aec
|
|
| BLAKE2b-256 |
06bb80c6c72fcf7feed133d9676a3042188fd7cbf13e907917264e20b00a90fe
|