Source code for simvx.core.nodes_2d.background_bands

"""``BackgroundBands`` / ``SkyHorizon``: horizontal colour bands relative to the camera.

Renders a stack of full-width horizontal stripes that fill the visible
viewport. Useful for sky / sea / cave-tier backdrops where you want a few
solid bands (or gradient bands) without authoring a backdrop texture.

Two scroll modes:

* ``world_space=True`` (default): bands are anchored at fixed world-Y
  values: the camera can scroll past them, so a "horizon" stays put.
* ``world_space=False``: bands lock to the screen: the sky always stays
  at the top of the viewport, irrespective of camera Y. Equivalent to
  parenting a ``Node2D`` under a ``CanvasLayer``.

A band entry is ``(top_y, colour, [bottom_colour])``:

* ``top_y``: Y in *the chosen space* (world or screen) for the top of
  this band. Bands are sorted ascending; each band extends down to the
  next band's ``top_y`` (or the viewport bottom for the last band).
* ``colour``: solid RGBA fill, or top colour for a vertical gradient.
* ``bottom_colour``: optional; if given, the band is a vertical gradient
  from ``colour`` (top) to ``bottom_colour`` (bottom).
"""

from __future__ import annotations

from collections.abc import Sequence

from ..descriptors import Property
from .node2d import Node2D

Band = tuple[float, tuple[float, ...], tuple[float, ...]] | tuple[float, tuple[float, ...]]


[docs] class BackgroundBands(Node2D): """Horizontal colour bands that follow the camera. Example: sky / sea horizon at world-Y = 0:: bg = root.add_child(BackgroundBands()) bg.bands = [ (-1e9, (0.4, 0.7, 1.0, 1.0), (0.7, 0.85, 1.0, 1.0)), # sky (0.0, (0.1, 0.3, 0.6, 1.0), (0.05, 0.15, 0.4, 1.0)), # sea ] """ world_space = Property(True, hint="If True, bands anchor to world Y; else to screen Y") def __init__(self, bands: Sequence[Band] | None = None, world_space: bool = True, **kwargs): super().__init__(**kwargs) self._bands: list[Band] = list(bands) if bands else [] self.world_space = world_space @property def bands(self) -> list[Band]: return list(self._bands)
[docs] @bands.setter def bands(self, value: Sequence[Band]) -> None: self._bands = list(value)
[docs] def add_band(self, top_y: float, colour: tuple[float, ...], bottom_colour: tuple[float, ...] | None = None) -> None: """Append a band entry. Bands are sorted by ``top_y`` at draw time.""" if bottom_colour is None: self._bands.append((top_y, colour)) else: self._bands.append((top_y, colour, bottom_colour))
[docs] def clear_bands(self) -> None: self._bands.clear()
def _viewport_world_rect(self) -> tuple[float, float, float, float] | None: """Return (min_x, min_y, max_x, max_y) of the visible area in world space.""" if self._tree is None: return None sw, sh = self._tree._screen_size cam = self._tree._current_camera_2d if cam is not None: zoom = cam.zoom if cam.zoom > 0 else 1.0 cx, cy = cam.current.x, cam.current.y half_w, half_h = (sw / zoom) * 0.5, (sh / zoom) * 0.5 return cx - half_w, cy - half_h, cx + half_w, cy + half_h return 0.0, 0.0, float(sw), float(sh)
[docs] def on_draw(self, renderer) -> None: """Emit the band rects covering the visible viewport.""" if not self._bands or not self.visible: return rect = self._viewport_world_rect() if rect is None: return wx0, wy0, wx1, wy1 = rect # Sort bands ascending by their top-Y. sorted_bands = sorted(self._bands, key=lambda b: b[0]) # In screen-locked mode, transform band Y values from screen space to # the *current* world top by adding the world-space view top. if not self.world_space: offset = wy0 sorted_bands = [ (b[0] + offset, *b[1:]) for b in sorted_bands ] # Clip band stack to the viewport range. view_top, view_bottom = wy0, wy1 # Compute each band's [top, bottom] interval inside the viewport. n = len(sorted_bands) for i, band in enumerate(sorted_bands): top_y = band[0] bottom_y = sorted_bands[i + 1][0] if i + 1 < n else view_bottom # Clip the band's Y range to the viewport. y0 = max(top_y, view_top) y1 = min(bottom_y, view_bottom) if y1 <= y0: continue colour_top = band[1] colour_bottom = band[2] if len(band) > 2 else None if colour_bottom is None or not hasattr(renderer, "fill_rect_gradient"): # Solid fill: Draw2D.draw_rect takes (pos, size, ..., filled=True). renderer.draw_rect( (wx0, y0), (wx1 - wx0, y1 - y0), colour=colour_top, filled=True, ) else: renderer.fill_rect_gradient( wx0, y0, wx1 - wx0, y1 - y0, colour_top, colour_bottom, )