"""Text2D -- 2D text drawn through the one 2D text builder."""
from ..descriptors import Property
from ..properties import Colour
from .node2d import Node2D
[docs]
class Text2D(Node2D):
"""Text placed in the 2D world like any other node. Works in both 2D and 3D modes.
Text2D draws through the ONE 2D text builder (``renderer.text``): the
item path emits a native kerned MSDF ``GLYPH`` item, and the ``Draw2D``
``_draw_recursive`` walk (editor play-mode / web) appends a matching ``TEXT``
op -- both from the same layout, so they render identically.
The renderer maps the run through the node's full world transform, as it
maps every 2D node's drawing, and the active Camera2D pans and zooms it: an
ancestor's rotation turns it, and its scale sizes it, a non-uniform scale
stretching the glyphs as it stretches a sprite. Text meant to stay on the
screen, a HUD, goes under a :class:`CanvasLayer`, which pins its subtree to
the screen and keeps it at its authored colour after tonemapping; without one,
text in a scene with post-processing is exposed and tonemapped like the rest
of the world.
Sizing: ``font_scale`` is a multiplier on the 16px logical em (``font_scale=1``
-> 16 logical px), before the node's world scale. The builder's readable-pixel
floor clamps tiny text up to ~10px; a scaled label opts out of it, so its
glyphs and its ``rect`` box shrink together, and a label scaled to nothing is
hidden, as a sprite is.
Common port patterns are first-class node Properties:
* ``outline`` / ``outline_colour`` -- a readable halo on any background.
* ``rect`` -- position + align inside a box instead of at a point.
* ``fit_to_width`` -- shrink ``font_scale`` so the text fits ``rect`` width.
"""
text = Property("", hint="Text to display")
font_scale = Property(1.0, range=(0.1, 100.0), hint="Font size (multiplier on 16px em)")
colour = Colour((1.0, 1.0, 1.0, 1.0))
# Horizontal anchor for ``position`` / box alignment inside ``rect``: "left"
# puts position.x at the text's left edge, "centre" centres on it, "right" puts
# it at the right edge. Each line is measured by the builder, so centring is
# exact.
align = Property("left", enum=["left", "centre", "right"], hint="Horizontal anchor")
# Outline halo: a 4-direction offset copy in
# ``outline_colour`` under the glyphs. ``outline`` is in glyph-em units (~0.08
# is a typical 1-2px halo); 0 disables it (the common case, zero added cost).
outline = Property(0.0, range=(0.0, 0.5), hint="Outline thickness (em units; 0 = none)")
outline_colour = Colour((0.0, 0.0, 0.0, 1.0))
# Box mode: when set, the text is positioned + aligned inside this rect
# (x, y, w, h), given in the label's own space, instead of anchored at its
# origin. ``fit_to_width`` fits the text to the box's width in that space.
rect = Property(None, hint="(x, y, w, h) box to align text inside (None = at the origin)")
# Shrink font_scale so the text fits the box (or position.x..+width) horizontally.
fit_to_width = Property(False, hint="Shrink font_scale so text fits the rect/box width")
[docs]
def on_draw(self, renderer) -> None:
if not self.text:
return
fc = self.colour
if fc[0] > 1.0 or fc[1] > 1.0 or fc[2] > 1.0: # normalise 0-255 int colours
fc = tuple(c / 255.0 for c in fc)
oc = self.outline_colour
if oc[0] > 1.0 or oc[1] > 1.0 or oc[2] > 1.0:
oc = tuple(c / 255.0 for c in oc)
sx, sy = self.world_transform[1].tolist()
scaled = sx != 1.0 or sy != 1.0
if scaled and sx * sy == 0.0:
return # a zero world scale hides the label, as it hides a sprite
common = {
"scale": self.font_scale,
"fill": fc,
"align": self.align,
"fit_to_width": self.fit_to_width,
# A scaled label waives the readable-pixel floor so box and glyphs shrink together.
"min_scale": 0.0 if scaled else None,
"stroke_width": self.outline,
"stroke": oc,
}
box = self.rect
if box is not None:
renderer.text(self.text, rect=tuple(float(v) for v in box), **common)
else:
renderer.text(self.text, (0.0, 0.0), **common)