Source code for simvx.core.nodes_2d.canvas

"""Canvas layer nodes: CanvasLayer, ParallaxBackground, ParallaxLayer, CanvasModulate."""

from __future__ import annotations

import math

import numpy as np

from ..descriptors import Property
from ..node import Node
from .node2d import Node2D


[docs] class CanvasLayer(Node): """Renders children at a fixed Z-layer with optional transform offset. Children of a CanvasLayer are drawn together at the specified layer order. Higher layer values draw on top. The canvas transform (offset, rotation, scale) is applied to all children. """ layer = Property(0, hint="Draw order (higher = on top)") offset = Property((0.0, 0.0), hint="Layer transform offset") rotation = Property(0.0, hint="Layer rotation in radians") scale_val = Property((1.0, 1.0), hint="Layer scale") follow_viewport = Property(True, hint="Whether layer follows viewport scroll") def _get_canvas_transform(self) -> np.ndarray: """Returns the 3x3 affine canvas transform matrix for this layer.""" ox, oy = self.offset sx, sy = self.scale_val c, s = math.cos(self.rotation), math.sin(self.rotation) return np.array([ [sx * c, -sy * s, ox], [sx * s, sy * c, oy], [0.0, 0.0, 1.0], ], dtype=np.float32) def _draw_recursive(self, renderer): """Override: children draw in screen-space, ignoring active camera transform.""" if not self.visible: return if hasattr(renderer, 'push_identity'): renderer.push_identity() self.draw(renderer) for child in self.children.safe_iter(): child._draw_recursive(renderer) if hasattr(renderer, 'pop_transform'): renderer.pop_transform()
[docs] class ParallaxBackground(CanvasLayer): """Container for ParallaxLayer children. Scrolls based on camera movement. Set scroll_offset each frame (e.g. from camera position) and child ParallaxLayer nodes will move at their configured motion_scale rate. """ scroll_offset = Property((0.0, 0.0), hint="Current scroll position") scroll_base_offset = Property((0.0, 0.0), hint="Base offset added before scaling") scroll_base_scale = Property((1.0, 1.0), hint="Base scale multiplier")
[docs] class ParallaxLayer(Node2D): """Layer within a ParallaxBackground. Scrolls at motion_scale rate. motion_scale controls how fast this layer moves relative to the camera. Values < 1 create distant/slow layers, > 1 creates foreground/fast layers. mirroring enables seamless tiling by repeating at the given interval. """ motion_scale = Property((1.0, 1.0), hint="Scroll rate relative to camera") motion_offset = Property((0.0, 0.0), hint="Static offset") mirroring = Property((0.0, 0.0), hint="Auto-repeat size (0 = no repeat)") @property def effective_offset(self) -> tuple[float, float]: """Calculate offset based on parent ParallaxBackground scroll.""" if isinstance(self.parent, ParallaxBackground): sx, sy = self.parent.scroll_offset bx, by = self.parent.scroll_base_offset bsx, bsy = self.parent.scroll_base_scale mx, my = self.motion_scale ox, oy = self.motion_offset eff_x = (sx + bx) * bsx * mx + ox eff_y = (sy + by) * bsy * my + oy # Apply mirroring (wrap within repeat interval) mir_x, mir_y = self.mirroring if mir_x > 0: eff_x = eff_x % mir_x if mir_y > 0: eff_y = eff_y % mir_y return (eff_x, eff_y) return tuple(self.motion_offset)
[docs] class CanvasModulate(Node2D): """Tints all 2D rendering on the canvas. When active, the colour is multiplied with every 2D draw call. Default white (1,1,1,1) means no tinting. Use for day/night cycles, underwater effects, etc. """ colour = Property((1.0, 1.0, 1.0, 1.0), hint="RGBA tint (default white = no change)")