Skip to main content

A small PySide6 splash screen composer with layered images and animated typography.

Project description

InspyreSplash

Documentation Status PyPI version Python versions License: MIT

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, and ExplodeIn effects.
  • 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-owned threading.Event objects.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

inspyre_splash-0.2.0.tar.gz (41.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

inspyre_splash-0.2.0-py3-none-any.whl (29.5 kB view details)

Uploaded Python 3

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

Hashes for inspyre_splash-0.2.0.tar.gz
Algorithm Hash digest
SHA256 744f603f56463ebe498c3b416036efe1e8c84370a210ff83da881ee36260c5d1
MD5 c1db1ae29ac0cd4b50c5282c8c65fe00
BLAKE2b-256 ec280bf823094bac4cb27209a150ac3bcfac232c05101dcc5303a34ade54cfc0

See more details on using hashes here.

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

Hashes for inspyre_splash-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8ef88a6a824c4e980352a1200ec58f830fd4924d128e9a1a239eefe7ab14f3ad
MD5 d243270707c85f447dd2fbf7c1fd1aec
BLAKE2b-256 06bb80c6c72fcf7feed133d9676a3042188fd7cbf13e907917264e20b00a90fe

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page