"""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)")