2D drawing¶
A node draws in on_draw(renderer). The renderer is Draw2D
(simvx.graphics.draw2d), and it draws in the node’s own space: (0, 0) is the node’s
origin, and its transform and every ancestor’s apply to what it draws.
Method |
Draws |
|---|---|
|
A rectangle |
|
A circle |
|
A polyline |
|
A polygon, convex or not |
|
Text in the project font; |
|
An image: a file path, image bytes, an RGBA array or a |
|
A 9-slice panel that stretches without distorting its corners |
Drawing is retained¶
The 2D renderer keeps the geometry each on_draw produced and re-runs on_draw only when the
node is marked dirty, so a busy scene stays cheap. Three rules cover every case:
What |
What to do |
|---|---|
A |
Nothing: the write marks the node dirty |
Per-frame animation in plain attributes (a timer, a pulse, a scroll offset) |
|
A plain attribute changed on an event (a score bumped in a signal handler) |
|
import math
from simvx.core import Node2D, Property
class Paddle(Node2D):
speed = Property(400.0)
def on_update(self, dt):
self.position.y += self.speed * dt # moving the transform marks it dirty
def on_draw(self, renderer):
renderer.rect((-6, -40), (12, 80), fill=(1, 1, 1))
class Pulse(Node2D):
dynamic = True # on_draw reads self.t every frame
def on_ready(self):
self.t = 0.0
def on_update(self, dt):
self.t += dt
def on_draw(self, renderer):
renderer.circle((0, 0), 12 + 8 * math.sin(self.t), fill=(0.4, 0.8, 1, 1))
class Scoreboard(Node2D):
def on_ready(self):
self.scores = [0, 0]
def add_point(self, side):
self.scores[side] += 1
self.queue_redraw() # scores is not a Property
def on_draw(self, renderer):
renderer.text(f"{self.scores[0]} {self.scores[1]}", (0, 0), fill=(1, 1, 1, 1), scale=2.0)
Prefer queue_redraw() to dynamic when the content changes rarely: re-drawing a large static
picture every frame is wasted work.
A node that should have been dynamic renders once and then looks frozen. While developing, set
Node.dev_warnings = True (or run under python -X dev): a freeze detector re-draws one clean
node every Node.freeze_check_interval seconds (1.0 by default) and issues a DevWarning
naming the node when its output changed with nothing marking it dirty. It is off by default, so
a shipped game never pays for it.
Order¶
Within one on_draw, call order is draw order: later calls paint over earlier ones, whatever
their kind.
def on_draw(self, renderer):
renderer.texture(self.background, (0, 0), (320, 180))
renderer.rect((0, 0), (320, 180), fill=(0, 0, 0, 0.7)) # dims the background
renderer.text("Paused", (160, 80), fill=(1, 1, 1, 1), scale=2.0, align="centre")
Between nodes, Node2D.z_index orders siblings, then tree order applies. Adjacent calls that
share a pipeline, a clip and a texture are batched into one GPU draw, so drawing many sprites
from one atlas in a row is cheap.
Clipping and transforms¶
renderer.clip(rect) scissors everything drawn inside the block to (x, y, w, h) in the
drawing code’s own space; nested clips intersect. renderer.transform(a, b, c, d, tx, ty)
draws through an extra affine transform.
with renderer.clip((0, 0, 200, 120)):
renderer.text(self.long_line, (4, 4), fill=(1, 1, 1, 1))
See Examples Gallery for the 2D feature examples, and simvx.graphics.draw2d
for every method and parameter.