Skip to content

xnano.art

xnano.art

xnano.art

Pixel art written as text, and pixel worlds: art that holds other art, seen through a camera, with solid ground to collide against.

Classes:

  • Camera –

    Which part of a world is on screen.

  • Art –

    Pixel art, and a world that holds other art.

Functions:

  • draw –

    Build pixel art from text, a picture, bytes or nothing (does not draw).

Camera

Camera()

Which part of a world is on screen.

By default the camera stays put. follow an art to keep it in the middle, or set x and y yourself. Either way the camera stays inside the world, and a world smaller than the screen is centered.

Attributes:

  • x –

    The world column at the left edge of the screen, when nothing is followed.

  • y –

    The world row at the top edge of the screen, when nothing is followed.

  • target (Art | None) –

    The art the camera follows, or None.

Examples:

import xnano

world = xnano.draw(None, width=100, height=60, background='black')
hero = world.add(xnano.draw(['H', 'H'], {'H': 'red'}), x=50, y=30)
world.camera.follow(hero)
assert world.camera.target is hero

Create a camera. Every art has one as art.camera.

Methods:

  • follow –

    Keep an art in the middle of the screen, or pass None to stop following.

Source code in xnano/art.py
def __init__(self) -> None:
    """Create a camera. Every art has one as ``art.camera``."""
    self.x = 0
    """The world column at the left edge of the screen, when nothing is followed."""
    self.y = 0
    """The world row at the top edge of the screen, when nothing is followed."""
    self.target: Art | None = None
    """The art the camera follows, or ``None``."""

x instance-attribute

x = 0

The world column at the left edge of the screen, when nothing is followed.

y instance-attribute

y = 0

The world row at the top edge of the screen, when nothing is followed.

target instance-attribute

target: Art | None = None

The art the camera follows, or None.

follow

follow(art: Art | None) -> None

Keep an art in the middle of the screen, or pass None to stop following.

Parameters:

  • art (Art | None) –

    The art to follow.

Source code in xnano/art.py
def follow(self, art: Art | None) -> None:
    """
    Keep an art in the middle of the screen, or pass ``None`` to stop following.

    Args:
        art: The art to follow.
    """
    self.target = art

Art

Art(rgba: bytes | bytearray | None, width: int, height: int, *, background: ColorValue | None = None, symbol: str | None = None, effects: Sequence[RenderableEffectSpecification] = ())

Pixel art, and a world that holds other art.

Make one with draw. A pixel is half a cell tall, so art that is 8 pixels tall takes 4 rows of a canvas. Write it with canvas.write(art), or print it on its own with render.

An art can hold other art: add puts a child at a position in the world, and the world is drawn through its camera. Change any attribute of an art or of a child and the canvas draws it again on the next frame; nothing else is needed, and a world that stands still costs nothing. Positions and sizes of art are in pixels and may be fractional; a child is drawn at its rounded position. A child's x and y place it inside its parent in pixels, while placement.x and placement.y place the whole art on the canvas in cells: art.up() moves one pixel, half a cell, and placement.up() moves one cell.

Attributes:

  • width (int) –

    The width in pixels.

  • height (int) –

    The height in pixels (two pixel rows make one cell row).

  • x (float) –

    The pixel column of the anchor inside the parent art.

  • y (float) –

    The pixel row of the anchor inside the parent art.

  • layer (int) –

    Children with a higher layer are drawn over lower ones.

  • is_visible (bool) –

    Whether the art is drawn.

  • anchor (Anchor) –

    The (x, y) pixel inside the art that sits at (x, y).

  • background (Color | None) –

    The color behind the art and its children, or None for none.

  • solid_map (bytes | bytearray | None) –

    Which pixels of the art's world cannot be walked on, or None.

  • camera –

    What part of the world is on screen.

  • symbol (str | None) –

    The name the art is registered under in symbols, or None.

Examples:

import xnano

world = xnano.draw(None, width=40, height=20, background='black')
hero = world.add(xnano.draw(['HH', 'HH'], {'H': 'red'}), x=10, y=10, anchor=(1, 1))
hero.move(2, 0)
assert hero.x == 12

with xnano.new(width=20, height=5) as canvas:
    canvas.write(world, width='100%', height='100%')
    canvas.step()

Create art from pixels. Use draw instead of calling this.

Parameters:

  • rgba (bytes | bytearray | None) –

    width * height * 4 bytes of red, green, blue and alpha, or None for a world with no pixels of its own.

  • width (int) –

    The width in pixels.

  • height (int) –

    The height in pixels.

  • background (ColorValue | None, default: None ) –

    The color behind the art.

  • symbol (str | None, default: None ) –

    The name to register the art under.

  • effects (Sequence[RenderableEffectSpecification], default: () ) –

    Effects to start on the placement when the art is written.

Methods:

  • add –

    Put another art in this one, at a position in pixels.

  • replace –

    Swap the pixels for others, keeping the position, layer and parent.

  • remove –

    Take the art out of the art that holds it. Removing a loose art does nothing.

  • move –

    Move the art by a number of pixels, sliding along solid ground instead of stopping.

  • up –

    Move up by count pixels, sliding along solid ground, and say how far it got.

  • down –

    Move down by count pixels, sliding along solid ground, and say how far it got.

  • left –

    Move left by count pixels, sliding along solid ground, and say how far it got.

  • right –

    Move right by count pixels, sliding along solid ground, and say how far it got.

  • is_solid –

    Whether a pixel of this art's world cannot be walked on.

  • world_for_cell –

    The world pixel under a cell of the screen, such as the one the pointer is over.

  • render –

    Draw the art to a string, without a canvas.

  • __xnano_render__ –

    Return the cells that show the art, as large as the area it is written in.

Source code in xnano/art.py
def __init__(
    self,
    rgba: bytes | bytearray | None,
    width: int,
    height: int,
    *,
    background: ColorValue | None = None,
    symbol: str | None = None,
    effects: Sequence[RenderableEffectSpecification] = (),
) -> None:
    """
    Create art from pixels. Use [`draw`][xnano.draw] instead of calling this.

    Args:
        rgba: ``width * height * 4`` bytes of red, green, blue and alpha, or ``None`` for
            a world with no pixels of its own.
        width: The width in pixels.
        height: The height in pixels.
        background: The color behind the art.
        symbol: The name to register the art under.
        effects: Effects to start on the placement when the art is written.
    """
    if width < 1 or height < 1:
        raise ValueError(
            f'Art needs at least 1 by 1 pixels, but got {width} by {height}. Give '
            f'draw(...) rows of text, or width= and height= in pixels.'
        )
    if rgba is not None and len(rgba) != width * height * _RGBA:
        raise ValueError(
            f'{width} by {height} pixels need {width * height * _RGBA} bytes of red, '
            f'green, blue and alpha, but got {len(rgba)}.'
        )
    self._rgba: bytes | None = None if rgba is None else bytes(rgba)
    self._width = width
    self._height = height
    self._x = 0.0
    self._y = 0.0
    self._layer = 0
    self._is_visible = True
    self._anchor: Anchor = (0, 0)
    self._background: tuple[int, int, int] | None = (
        None if background is None else to_rgb(background)
    )
    self._solid_map: bytes | bytearray | None = None
    self._blocks_movement = False
    self._children: list[Art] = []
    self._parent: Art | None = None
    self._symbol: str | None = None
    effects = cast(
        'tuple[RenderableEffectSpecification, ...]',
        effect_sequence(effects, 'renderable'),
    )
    for effect in effects:
        normalize_effect(effect, 'renderable')
    self._effects: tuple[RenderableEffectSpecification, ...] = effects
    self._version = 0
    self._cache_key: tuple[int, ...] | None = None
    self._cache_buffer: _core.Buffer | None = None
    self._last_view: tuple[int, int] | None = None
    self._observers: weakref.WeakSet[Placement[object, object]] = weakref.WeakSet()
    self.camera = Camera()
    """What part of the world is on screen."""
    if symbol is not None:
        symbols.register(symbol, self)
        self._symbol = symbol

camera instance-attribute

camera = Camera()

What part of the world is on screen.

width property

width: int

The width in pixels.

height property

height: int

The height in pixels.

symbol property

symbol: str | None

The name the art is registered under in symbols, or None.

x property writable

x: float

The pixel column of the anchor inside the parent art.

y property writable

y: float

The pixel row of the anchor inside the parent art.

layer property writable

layer: int

Children with a higher layer are drawn over lower ones.

is_visible property writable

is_visible: bool

Whether the art is drawn.

anchor property writable

anchor: Anchor

The (x, y) pixel inside the art that sits at (x, y).

background property writable

background: Color | None

The color behind the art and its children, or None for none.

solid_map property writable

solid_map: bytes | bytearray | None

Which pixels of the art's world cannot be walked on, or None when all can: one byte per pixel, row after row (width * height bytes), with a non-zero byte for solid ground.

add

add(art: Art, /, *, x: float = 0, y: float = 0, layer: int = 0, anchor: Anchor = (0, 0), is_solid: bool = False) -> Art

Put another art in this one, at a position in pixels.

Parameters:

  • art (Art) –

    The art to add. It may be added to one art only.

  • x (float, default: 0 ) –

    The pixel column of the child's anchor inside this art.

  • y (float, default: 0 ) –

    The pixel row of the child's anchor inside this art.

  • layer (int, default: 0 ) –

    Children with a higher layer are drawn over lower ones; children on the same layer are drawn in the order they were added.

  • anchor (Anchor, default: (0, 0) ) –

    The (x, y) pixel of the child that sits at (x, y).

  • is_solid (bool, default: False ) –

    Whether the child's opaque pixels block the movement of other children.

Returns:

  • Art –

    The child, to move, change or remove.

Raises:

  • ValueError –

    The art already belongs to an art, or is this art or one that holds it.

Examples:

import xnano

world = xnano.draw(None, width=8, height=8)
hero = world.add(xnano.draw(['R', 'R'], {'R': 'red'}), x=3, y=2, anchor=(0, 1))
assert (hero.x, hero.y) == (3, 2)
Source code in xnano/art.py
def add(
    self,
    art: Art,
    /,
    *,
    x: float = 0,
    y: float = 0,
    layer: int = 0,
    anchor: Anchor = (0, 0),
    is_solid: bool = False,
) -> Art:
    """
    Put another art in this one, at a position in pixels.

    Args:
        art: The art to add. It may be added to one art only.
        x: The pixel column of the child's anchor inside this art.
        y: The pixel row of the child's anchor inside this art.
        layer: Children with a higher layer are drawn over lower ones; children on the
            same layer are drawn in the order they were added.
        anchor: The ``(x, y)`` pixel of the child that sits at ``(x, y)``.
        is_solid: Whether the child's opaque pixels block the movement of other children.

    Returns:
        The child, to move, change or remove.

    Raises:
        ValueError: The art already belongs to an art, or is this art or one that holds
            it.

    Examples:
        ```python
        import xnano

        world = xnano.draw(None, width=8, height=8)
        hero = world.add(xnano.draw(['R', 'R'], {'R': 'red'}), x=3, y=2, anchor=(0, 1))
        assert (hero.x, hero.y) == (3, 2)
        ```
    """
    if not isinstance(art, Art):
        raise TypeError(
            f'add() takes an Art made with xnano.draw(...), but got '
            f'{type(art).__name__}. Wrap a picture with xnano.draw(image) first.'
        )
    ancestor: Art | None = self
    while ancestor is not None:
        if ancestor is art:
            raise ValueError(
                'An art cannot be added to itself or to an art it holds. Add it to '
                'another world.'
            )
        ancestor = ancestor._parent
    if art._parent is not None:
        raise ValueError(
            'This art already belongs to another art. Call art.remove() first, or draw '
            'a second one.'
        )
    art._parent = self
    art._x = float(x)
    art._y = float(y)
    art._layer = layer
    art._anchor = anchor
    art._blocks_movement = is_solid
    self._children.append(art)
    self._changed()
    return art

replace

replace(source: str | Sequence[str] | Image | TerminalImage | bytes | bytearray | None, palette: Mapping[str, ColorValue] | str | None = None, *, width: int | None = None, height: int | None = None) -> None

Swap the pixels for others, keeping the position, layer and parent.

The solid ground is kept when the new pixels have the same size and cleared when the size changes.

This is how a walk cycle or a zoom is made: draw every frame once, and replace the pixels of the art that is on screen. The sources are the ones draw takes, except a symbol name.

Parameters:

  • source (str | Sequence[str] | Image | TerminalImage | bytes | bytearray | None) –

    Text rows, an image, RGBA bytes or None.

  • palette (Mapping[str, ColorValue] | str | None, default: None ) –

    What each character of text rows means.

  • width (int | None, default: None ) –

    The width in pixels of RGBA bytes or of a blank world.

  • height (int | None, default: None ) –

    The height in pixels.

Examples:

import xnano

frame_one = ['AB']
hero = xnano.draw(frame_one, {'A': 'red', 'B': 'blue'})
hero.replace(['BA'], {'A': 'red', 'B': 'blue'})
assert hero.width == 2
Source code in xnano/art.py
def replace(
    self,
    source: str | Sequence[str] | Image | TerminalImage | bytes | bytearray | None,
    palette: Mapping[str, ColorValue] | str | None = None,
    *,
    width: int | None = None,
    height: int | None = None,
) -> None:
    """
    Swap the pixels for others, keeping the position, layer and parent.

    The solid ground is kept when the new pixels have the same size and cleared when the
    size changes.

    This is how a walk cycle or a zoom is made: draw every frame once, and replace the
    pixels of the art that is on screen. The sources are the ones
    [`draw`][xnano.draw] takes, except a symbol name.

    Args:
        source: Text rows, an image, RGBA bytes or ``None``.
        palette: What each character of text rows means.
        width: The width in pixels of RGBA bytes or of a blank world.
        height: The height in pixels.

    Examples:
        ```python
        import xnano

        frame_one = ['AB']
        hero = xnano.draw(frame_one, {'A': 'red', 'B': 'blue'})
        hero.replace(['BA'], {'A': 'red', 'B': 'blue'})
        assert hero.width == 2
        ```
    """
    rgba, pixel_width, pixel_height = _pixels_of(source, palette, width, height)
    previous_width, previous_height = self._width, self._height
    self._rgba = None if rgba is None else bytes(rgba)
    self._width = pixel_width
    self._height = pixel_height
    if (pixel_width, pixel_height) != (previous_width, previous_height):
        self._solid_map = None
    self._changed()

remove

remove() -> None

Take the art out of the art that holds it. Removing a loose art does nothing.

Source code in xnano/art.py
def remove(self) -> None:
    """Take the art out of the art that holds it. Removing a loose art does nothing."""
    parent = self._parent
    if parent is None:
        return
    self._parent = None
    if self in parent._children:
        parent._children.remove(self)
    if parent.camera.target is self:
        parent.camera.target = None
    parent._changed()

move

move(x: float = 0, y: float = 0) -> tuple[float, float]

Move the art by a number of pixels, sliding along solid ground instead of stopping.

The two axes are tried one after the other, a pixel at a time, so walking diagonally into a wall keeps moving along it and a thin wall cannot be stepped over. The anchor pixel is what collides. An art without a parent art, or whose parent has no solid ground, moves freely. Positive x moves right and positive y moves down.

Pixels are half a cell: art.move(y=-1) moves one pixel up, where placement.move(y=-1) moves the whole art one cell up.

Parameters:

  • x (float, default: 0 ) –

    How far to move along x, in pixels.

  • y (float, default: 0 ) –

    How far to move along y, in pixels.

Returns:

  • tuple[float, float] –

    The distance moved along each axis, (0.0, 0.0) when blocked.

Examples:

import xnano

world = xnano.draw(None, width=8, height=8)
solid = bytearray(64)
for row in range(8):
    solid[row * 8 + 5] = 1
world.solid_map = solid
hero = world.add(xnano.draw(['R'], {'R': 'red'}), x=4, y=2)
assert hero.move(2, 1) == (0.0, 1.0)
assert (hero.x, hero.y) == (4, 3)
Source code in xnano/art.py
def move(self, x: float = 0, y: float = 0) -> tuple[float, float]:
    """
    Move the art by a number of pixels, sliding along solid ground instead of stopping.

    The two axes are tried one after the other, a pixel at a time, so walking diagonally
    into a wall keeps moving along it and a thin wall cannot be stepped over. The anchor
    pixel is what collides. An art without a parent art, or whose parent has no solid
    ground, moves freely. Positive ``x`` moves right and positive ``y`` moves down.

    Pixels are half a cell: ``art.move(y=-1)`` moves one pixel up, where
    ``placement.move(y=-1)`` moves the whole art one cell up.

    Args:
        x: How far to move along ``x``, in pixels.
        y: How far to move along ``y``, in pixels.

    Returns:
        The distance moved along each axis, ``(0.0, 0.0)`` when blocked.

    Examples:
        ```python
        import xnano

        world = xnano.draw(None, width=8, height=8)
        solid = bytearray(64)
        for row in range(8):
            solid[row * 8 + 5] = 1
        world.solid_map = solid
        hero = world.add(xnano.draw(['R'], {'R': 'red'}), x=4, y=2)
        assert hero.move(2, 1) == (0.0, 1.0)
        assert (hero.x, hero.y) == (4, 3)
        ```
    """
    _check_pixels(x, name='x', verb='move')
    _check_pixels(y, name='y', verb='move')
    before_x, before_y = self._x, self._y
    self.x = self._advance(self._x, self._y, x, is_horizontal=True)
    self.y = self._advance(self._x, self._y, y, is_horizontal=False)
    return (self._x - before_x, self._y - before_y)

up

up(count: float = 1) -> float

Move up by count pixels, sliding along solid ground, and say how far it got.

Unlike Placement.up, which returns the placement so calls chain, this returns the distance moved, so a game can tell when the art was blocked.

Parameters:

  • count (float, default: 1 ) –

    How many pixels to move; pixels are half a cell.

Returns:

  • float –

    The distance moved in pixels, 0.0 when blocked.

Source code in xnano/art.py
def up(self, count: float = 1) -> float:
    """
    Move up by ``count`` pixels, sliding along solid ground, and say how far it got.

    Unlike [`Placement.up`][xnano.placement.Placement.up], which returns the placement
    so calls chain, this returns the distance moved, so a game can tell when the art was
    blocked.

    Args:
        count: How many pixels to move; pixels are half a cell.

    Returns:
        The distance moved in pixels, ``0.0`` when blocked.
    """
    _check_pixels(count, name='count', verb='up', opposite='down')
    return abs(self.move(y=-count)[1])

down

down(count: float = 1) -> float

Move down by count pixels, sliding along solid ground, and say how far it got.

Unlike Placement.down, which returns the placement so calls chain, this returns the distance moved, so a game can tell when the art was blocked.

Parameters:

  • count (float, default: 1 ) –

    How many pixels to move; pixels are half a cell.

Returns:

  • float –

    The distance moved in pixels, 0.0 when blocked.

Source code in xnano/art.py
def down(self, count: float = 1) -> float:
    """
    Move down by ``count`` pixels, sliding along solid ground, and say how far it got.

    Unlike [`Placement.down`][xnano.placement.Placement.down], which returns the placement
    so calls chain, this returns the distance moved, so a game can tell when the art was
    blocked.

    Args:
        count: How many pixels to move; pixels are half a cell.

    Returns:
        The distance moved in pixels, ``0.0`` when blocked.
    """
    _check_pixels(count, name='count', verb='down', opposite='up')
    return abs(self.move(y=count)[1])

left

left(count: float = 1) -> float

Move left by count pixels, sliding along solid ground, and say how far it got.

Unlike Placement.left, which returns the placement so calls chain, this returns the distance moved, so a game can tell when the art was blocked.

Parameters:

  • count (float, default: 1 ) –

    How many pixels to move; pixels are half a cell.

Returns:

  • float –

    The distance moved in pixels, 0.0 when blocked.

Source code in xnano/art.py
def left(self, count: float = 1) -> float:
    """
    Move left by ``count`` pixels, sliding along solid ground, and say how far it got.

    Unlike [`Placement.left`][xnano.placement.Placement.left], which returns the placement
    so calls chain, this returns the distance moved, so a game can tell when the art was
    blocked.

    Args:
        count: How many pixels to move; pixels are half a cell.

    Returns:
        The distance moved in pixels, ``0.0`` when blocked.
    """
    _check_pixels(count, name='count', verb='left', opposite='right')
    return abs(self.move(x=-count)[0])

right

right(count: float = 1) -> float

Move right by count pixels, sliding along solid ground, and say how far it got.

Unlike Placement.right, which returns the placement so calls chain, this returns the distance moved, so a game can tell when the art was blocked.

Parameters:

  • count (float, default: 1 ) –

    How many pixels to move; pixels are half a cell.

Returns:

  • float –

    The distance moved in pixels, 0.0 when blocked.

Source code in xnano/art.py
def right(self, count: float = 1) -> float:
    """
    Move right by ``count`` pixels, sliding along solid ground, and say how far it got.

    Unlike [`Placement.right`][xnano.placement.Placement.right], which returns the placement
    so calls chain, this returns the distance moved, so a game can tell when the art was
    blocked.

    Args:
        count: How many pixels to move; pixels are half a cell.

    Returns:
        The distance moved in pixels, ``0.0`` when blocked.
    """
    _check_pixels(count, name='count', verb='right', opposite='left')
    return abs(self.move(x=count)[0])

is_solid

is_solid(x: float, y: float) -> bool

Whether a pixel of this art's world cannot be walked on.

Everything outside the world is solid. Inside it, a pixel is solid where solid_map holds a non-zero byte, or where an opaque pixel of a child added with is_solid=True is.

Parameters:

  • x (float) –

    The world column.

  • y (float) –

    The world row.

Examples:

import xnano

world = xnano.draw(None, width=4, height=4)
assert world.is_solid(-1, 0)
assert not world.is_solid(2, 2)
Source code in xnano/art.py
def is_solid(self, x: float, y: float) -> bool:
    """
    Whether a pixel of this art's world cannot be walked on.

    Everything outside the world is solid. Inside it, a pixel is solid where
    [`solid_map`][xnano.art.Art.solid_map] holds a non-zero byte, or where an opaque pixel
    of a child added with ``is_solid=True`` is.

    Args:
        x: The world column.
        y: The world row.

    Examples:
        ```python
        import xnano

        world = xnano.draw(None, width=4, height=4)
        assert world.is_solid(-1, 0)
        assert not world.is_solid(2, 2)
        ```
    """
    return self._is_solid_for(x, y, mover=None)

world_for_cell

world_for_cell(x: int, y: int) -> tuple[int, int]

The world pixel under a cell of the screen, such as the one the pointer is over.

The cell must be one of the cells the art was last drawn in; a cell is two pixels tall, and the pixel returned is the lower one.

Parameters:

  • x (int) –

    The cell's column in the placement of the art.

  • y (int) –

    The cell's row in the placement of the art.

Examples:

import xnano

world = xnano.draw(None, width=40, height=40)
world.camera.x, world.camera.y = 16, 16
with xnano.new(width=8, height=4) as canvas:
    canvas.write(world, width='100%', height='100%')
    canvas.step()
assert world.world_for_cell(0, 0) == (16, 17)
Source code in xnano/art.py
def world_for_cell(self, x: int, y: int) -> tuple[int, int]:
    """
    The world pixel under a cell of the screen, such as the one the pointer is over.

    The cell must be one of the cells the art was last drawn in; a cell is two pixels
    tall, and the pixel returned is the lower one.

    Args:
        x: The cell's column in the placement of the art.
        y: The cell's row in the placement of the art.

    Examples:
        ```python
        import xnano

        world = xnano.draw(None, width=40, height=40)
        world.camera.x, world.camera.y = 16, 16
        with xnano.new(width=8, height=4) as canvas:
            canvas.write(world, width='100%', height='100%')
            canvas.step()
        assert world.world_for_cell(0, 0) == (16, 17)
        ```
    """
    left, top = self._origin()
    return left + x, top + _PIXELS_PER_CELL_ROW * y + 1

render

render(*, width: int | None = None, height: int | None = None, format: Literal['text', 'ansi'] = 'text') -> str

Draw the art to a string, without a canvas.

Parameters:

  • width (int | None, default: None ) –

    The width in cells. The art's own width by default.

  • height (int | None, default: None ) –

    The height in cells. The art's own height in cells by default.

  • format (Literal['text', 'ansi'], default: 'text' ) –

    'text' for the characters only, 'ansi' with colors.

Examples:

import xnano

heart = xnano.draw(['R.R', 'RRR'], {'R': 'red'})
assert heart.render() == ' ▄'
Source code in xnano/art.py
def render(
    self,
    *,
    width: int | None = None,
    height: int | None = None,
    format: Literal['text', 'ansi'] = 'text',
) -> str:
    """
    Draw the art to a string, without a canvas.

    Args:
        width: The width in cells. The art's own width by default.
        height: The height in cells. The art's own height in cells by default.
        format: ``'text'`` for the characters only, ``'ansi'`` with colors.

    Examples:
        ```python
        import xnano

        heart = xnano.draw(['R.R', 'RRR'], {'R': 'red'})
        assert heart.render() == ' ▄'
        ```
    """
    columns = self._width if width is None else width
    rows = (
        math.ceil(self._height / _PIXELS_PER_CELL_ROW) if height is None else height
    )
    return self._compose(columns, rows).render(format=format)

__xnano_render__

__xnano_render__(context: Context) -> Buffer

Return the cells that show the art, as large as the area it is written in.

Source code in xnano/art.py
def __xnano_render__(self, context: Context) -> _core.Buffer:
    """Return the cells that show the art, as large as the area it is written in."""
    owner = context.buffer
    area = owner.region
    if hasattr(owner, 'refresh'):
        self._observers.add(cast('Placement[object, object]', owner))
    columns = area.width or self._width
    rows = area.height or math.ceil(self._height / _PIXELS_PER_CELL_ROW)
    return self._compose(columns, rows)

draw

draw(source: str | Sequence[str] | Image | TerminalImage | bytes | bytearray | None = None, palette: Mapping[str, ColorValue] | str | None = None, *, width: int | None = None, height: int | None = None, background: ColorValue | None = None, symbol: str | None = None, effects: Sequence[RenderableEffectSpecification] = ()) -> Art

Build pixel art from text, a picture, bytes or nothing (does not draw).

This is how pixel-art sprites are written in code, one character per pixel. Nothing is drawn: the art is a value that you write into a canvas. Text rows (a multi-line string, which may be indented, or a list of rows) are painted with a palette that says what each character means; any character the palette does not name, such as '.', is a transparent pixel. The palette is a mapping, or one string of character=color pairs.

What source is decides what is made:

  • Text with a newline, or a list of rows: pixel art painted with palette.
  • Text without a newline: the name of a registered art or image (see symbols); you get a copy of the art, or the pixels of the image. A one-row picture is a list, draw(['GG'], palette), so that a call never changes meaning because a symbol was registered later.
  • An Image or a picture from xnano.image: its pixels are copied (the first frame of an animation).
  • bytes of red, green, blue and alpha, with width and height.
  • None: a blank world of width by height pixels filled with background.

Parameters:

  • source (str | Sequence[str] | Image | TerminalImage | bytes | bytearray | None, default: None ) –

    What to draw.

  • palette (Mapping[str, ColorValue] | str | None, default: None ) –

    The color of each character of text rows, as a mapping to names, '#rrggbb' strings or (red, green, blue) tuples, or as a string such as 'R=#e03040 G=green'.

  • width (int | None, default: None ) –

    The width in pixels of bytes or of a blank world.

  • height (int | None, default: None ) –

    The height in pixels.

  • background (ColorValue | None, default: None ) –

    The color behind the art. Transparent pixels show it.

  • symbol (str | None, default: None ) –

    A name to register the art under, so draw('name') returns a copy of it.

  • effects (Sequence[RenderableEffectSpecification], default: () ) –

    Effects to start on the placement when the art is written, as dictionaries with a kind key.

Raises:

  • ValueError –

    The rows are empty or differ in length, the palette or a color in it is not valid, a text without a newline is not a known symbol, or the sizes do not match the bytes.

  • TypeError –

    source is not one of the kinds above.

Examples:

import xnano

heart = xnano.draw(
    '''
    .RR.RR.
    RRRRRRR
    .RRRRR.
    ..RRR..
    ...R...
    ''',
    'R=#e03040',
)
assert (heart.width, heart.height) == (7, 5)
print(heart.render())

xnano.draw(['G'], {'G': 'green'}, symbol='grass')
assert xnano.draw('grass').width == 1
Source code in xnano/art.py
def draw(
    source: str | Sequence[str] | Image | TerminalImage | bytes | bytearray | None = None,
    palette: Mapping[str, ColorValue] | str | None = None,
    *,
    width: int | None = None,
    height: int | None = None,
    background: ColorValue | None = None,
    symbol: str | None = None,
    effects: Sequence[RenderableEffectSpecification] = (),
) -> Art:
    """
    Build pixel art from text, a picture, bytes or nothing (does not draw).

    This is how pixel-art sprites are written in code, one character per pixel. Nothing is
    drawn: the art is a value that you write into a canvas. Text rows (a multi-line string, which
    may be indented, or a list of rows) are painted with a palette that says what each
    character means; any character the palette does not name, such as ``'.'``, is a
    transparent pixel. The palette is a mapping, or one string of ``character=color`` pairs.

    What ``source`` is decides what is made:

    * Text with a newline, or a list of rows: pixel art painted with ``palette``.
    * Text without a newline: the name of a registered art or image (see
      [`symbols`][xnano.symbols]); you get a copy of the art, or the pixels of the image.
      A one-row picture is a list, ``draw(['GG'], palette)``, so that a call never changes
      meaning because a symbol was registered later.
    * An [`Image`][xnano.Image] or a picture from ``xnano.image``: its pixels are copied
      (the first frame of an animation).
    * ``bytes`` of red, green, blue and alpha, with ``width`` and ``height``.
    * ``None``: a blank world of ``width`` by ``height`` pixels filled with ``background``.

    Args:
        source: What to draw.
        palette: The color of each character of text rows, as a mapping to names,
            ``'#rrggbb'`` strings or ``(red, green, blue)`` tuples, or as a string such as
            ``'R=#e03040 G=green'``.
        width: The width in pixels of ``bytes`` or of a blank world.
        height: The height in pixels.
        background: The color behind the art. Transparent pixels show it.
        symbol: A name to register the art under, so ``draw('name')`` returns a copy of it.
        effects: Effects to start on the placement when the art is written, as dictionaries
            with a ``kind`` key.

    Raises:
        ValueError: The rows are empty or differ in length, the palette or a color in it is
            not valid, a text without a newline is not a known symbol, or the sizes do not
            match the bytes.
        TypeError: ``source`` is not one of the kinds above.

    Examples:
        ```python
        import xnano

        heart = xnano.draw(
            '''
            .RR.RR.
            RRRRRRR
            .RRRRR.
            ..RRR..
            ...R...
            ''',
            'R=#e03040',
        )
        assert (heart.width, heart.height) == (7, 5)
        print(heart.render())

        xnano.draw(['G'], {'G': 'green'}, symbol='grass')
        assert xnano.draw('grass').width == 1
        ```
    """
    if isinstance(source, str) and '\n' not in source:
        from xnano._media import Image as MediaImage

        if source in symbols.names(kind=MediaImage):
            # The name of a registered image draws that image.
            source = symbols.lookup(source, kind=MediaImage)
    if isinstance(source, str) and '\n' not in source:
        try:
            registered = symbols.lookup(source, kind=Art)
        except ValueError as error:
            raise ValueError(
                f'{error} To draw a one-row picture, pass a list: draw([{source!r}], '
                f'palette).'
            ) from None
        art = registered._copy()
        if background is not None:
            art.background = background
        effects = cast(
            'tuple[RenderableEffectSpecification, ...]',
            effect_sequence(effects, 'renderable'),
        )
        if effects:
            for effect in effects:
                normalize_effect(effect, 'renderable')
            art._effects = effects
        if symbol is not None:
            symbols.register(symbol, art)
            art._symbol = symbol
        return art
    rgba, pixel_width, pixel_height = _pixels_of(source, palette, width, height)
    return Art(
        rgba,
        pixel_width,
        pixel_height,
        background=background,
        symbol=symbol,
        effects=effects,
    )