Skip to content

Commit 0ab6a34

Browse files
committed
Update set_tile docs and add procedural_block_elements.
`procedural_block_elements` was an example that got so large that it became an actual function.
1 parent d7815bb commit 0ab6a34

4 files changed

Lines changed: 163 additions & 6 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
"pathfinding",
209209
"pathlib",
210210
"PILCROW",
211+
"pilmode",
211212
"PRINTF",
212213
"printn",
213214
"pycall",
@@ -280,6 +281,7 @@
280281
"vline",
281282
"VOLUMEUP",
282283
"voronoi",
284+
"VRAM",
283285
"vsync",
284286
"WASD",
285287
"WINDOWRESIZED",

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ v2.0.0
88

99
Unreleased
1010
------------------
11+
Added
12+
- Added the `tcod.tileset.procedural_block_elements` function.
1113
Removed
1214
- Python 3.6 is no longer supported.
1315

tcod/tileset.py

Lines changed: 151 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,54 @@ def get_tile(self, codepoint: int) -> NDArray[np.uint8]:
8282
)
8383
return tile
8484

85-
def set_tile(self, codepoint: int, tile: ArrayLike) -> None:
85+
def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) -> None:
8686
"""Upload a tile into this array.
8787
88-
The tile can be in 32-bit color (height, width, rgba), or grey-scale
89-
(height, width). The tile should have a dtype of ``np.uint8``.
88+
Args:
89+
codepoint (int): The Unicode codepoint you are assigning to.
90+
If the tile is a sprite rather than a common glyph then consider assigning it to a
91+
`Private Use Area <https://en.wikipedia.org/wiki/Private_Use_Areas>`_.
92+
tile (Union[ArrayLike, NDArray[np.uint8]]):
93+
The pixels to use for this tile in row-major order and must be in the same shape as :any:`tile_shape`.
94+
`tile` can be an RGBA array with the shape of ``(height, width, rgba)``, or a grey-scale array with the
95+
shape ``(height, width)``.
96+
The `tile` array will be converted to a dtype of ``np.uint8``.
97+
98+
An RGB array as an input is too ambiguous and an alpha channel must be added, for example if an image has a key
99+
color than the key color pixels must have their alpha channel set to zero.
100+
101+
This data may be immediately sent to VRAM, which can be a slow operation.
102+
103+
Example::
104+
105+
# Examples use imageio for image loading, see https://imageio.readthedocs.io
106+
tileset: tcod.tileset.Tileset # This example assumes you are modifying an existing tileset.
107+
108+
# Normal usage when a tile already has its own alpha channel.
109+
# The loaded tile must be the correct shape for the tileset you assign it to.
110+
# The tile is assigned to a private use area and will not conflict with any exiting codepoint.
111+
tileset.set_tile(0x100000, imageio.load("rgba_tile.png"))
112+
113+
# Load a greyscale tile.
114+
tileset.set_tile(0x100001, imageio.load("greyscale_tile.png"), pilmode="L")
115+
# If you are stuck with an RGB array then you can use the red channel as the input: `rgb[:, :, 0]`
116+
117+
# Loads an RGB sprite without a background.
118+
tileset.set_tile(0x100002, imageio.load("rgb_no_background.png", pilmode="RGBA"))
119+
# If you're stuck with an RGB array then you can pad the channel axis with an alpha of 255:
120+
# rgba = np.pad(rgb, pad_width=((0, 0), (0, 0), (0, 1)), constant_values=255)
121+
122+
# Loads an RGB sprite with a key color background.
123+
KEY_COLOR = np.asarray((255, 0, 255), dtype=np.uint8)
124+
sprite_rgb = imageio.load("rgb_tile.png")
125+
# Compare the RGB colors to KEY_COLOR, compress full matches to a 2D mask.
126+
sprite_mask = (sprite_rgb != KEY_COLOR).all(axis=2)
127+
# Generate the alpha array, with 255 as the foreground and 0 as the background.
128+
sprite_alpha = sprite_mask.astype(np.uint8) * 255
129+
# Combine the RGB and alpha arrays into an RGBA array.
130+
sprite_rgba = np.append(sprite_rgb, sprite_alpha, axis=2)
131+
tileset.set_tile(0x100003, sprite_rgba)
90132
91-
This data may need to be sent to graphics card memory, this is a slow
92-
operation.
93133
"""
94134
tile = np.ascontiguousarray(tile, dtype=np.uint8)
95135
if tile.shape == self.tile_shape:
@@ -99,7 +139,13 @@ def set_tile(self, codepoint: int, tile: ArrayLike) -> None:
99139
return self.set_tile(codepoint, full_tile)
100140
required = self.tile_shape + (4,)
101141
if tile.shape != required:
102-
raise ValueError("Tile shape must be %r or %r, got %r." % (required, self.tile_shape, tile.shape))
142+
note = ""
143+
if len(tile.shape) == 3 and tile.shape[2] == 3:
144+
note = (
145+
"\nNote: An RGB array is too ambiguous,"
146+
" an alpha channel must be added to this array to divide the background/foreground areas."
147+
)
148+
raise ValueError(f"Tile shape must be {required} or {self.tile_shape}, got {tile.shape}.{note}")
103149
lib.TCOD_tileset_set_tile_(
104150
self._tileset_p,
105151
codepoint,
@@ -298,6 +344,105 @@ def load_tilesheet(path: Union[str, Path], columns: int, rows: int, charmap: Opt
298344
return Tileset._claim(cdata)
299345

300346

347+
def procedural_block_elements(tileset: Tileset) -> None:
348+
"""Overwrites the block element codepoints in `tileset` with prodecually generated glyphs.
349+
350+
Args:
351+
tileset (Tileset): A :any:`Tileset` with tiles of any shape.
352+
353+
This will overwrite all of the codepoints `listed here <https://en.wikipedia.org/wiki/Block_Elements>`_
354+
except for the shade glyphs.
355+
356+
This function is useful for other functions such as :any:`Console.draw_semigraphics` which use more types of block
357+
elements than are found in Code Page 437.
358+
359+
.. versionadded:: 13.1
360+
361+
Example::
362+
363+
>>> tileset = tcod.tileset.Tileset(8, 8)
364+
>>> tcod.tileset.procedural_block_elements(tileset)
365+
>>> tileset.get_tile(0x259E)[:, :, 3] # "▞" Quadrant upper right and lower left.
366+
array([[ 0, 0, 0, 0, 255, 255, 255, 255],
367+
[ 0, 0, 0, 0, 255, 255, 255, 255],
368+
[ 0, 0, 0, 0, 255, 255, 255, 255],
369+
[ 0, 0, 0, 0, 255, 255, 255, 255],
370+
[255, 255, 255, 255, 0, 0, 0, 0],
371+
[255, 255, 255, 255, 0, 0, 0, 0],
372+
[255, 255, 255, 255, 0, 0, 0, 0],
373+
[255, 255, 255, 255, 0, 0, 0, 0]], dtype=uint8)
374+
>>> tileset.get_tile(0x2581)[:, :, 3] # "▁" Lower one eighth block.
375+
array([[ 0, 0, 0, 0, 0, 0, 0, 0],
376+
[ 0, 0, 0, 0, 0, 0, 0, 0],
377+
[ 0, 0, 0, 0, 0, 0, 0, 0],
378+
[ 0, 0, 0, 0, 0, 0, 0, 0],
379+
[ 0, 0, 0, 0, 0, 0, 0, 0],
380+
[ 0, 0, 0, 0, 0, 0, 0, 0],
381+
[ 0, 0, 0, 0, 0, 0, 0, 0],
382+
[255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
383+
>>> tileset.get_tile(0x258D)[:, :, 3] # "▍" Left three eighths block.
384+
array([[255, 255, 255, 0, 0, 0, 0, 0],
385+
[255, 255, 255, 0, 0, 0, 0, 0],
386+
[255, 255, 255, 0, 0, 0, 0, 0],
387+
[255, 255, 255, 0, 0, 0, 0, 0],
388+
[255, 255, 255, 0, 0, 0, 0, 0],
389+
[255, 255, 255, 0, 0, 0, 0, 0],
390+
[255, 255, 255, 0, 0, 0, 0, 0],
391+
[255, 255, 255, 0, 0, 0, 0, 0]], dtype=uint8)
392+
"""
393+
quadrants: NDArray[np.uint8] = np.zeros(tileset.tile_shape, dtype=np.uint8)
394+
half_height = tileset.tile_height // 2
395+
half_width = tileset.tile_width // 2
396+
quadrants[:half_height, :half_width] = 0b1000 # Top-left.
397+
quadrants[:half_height, half_width:] = 0b0100 # Top-right.
398+
quadrants[half_height:, :half_width] = 0b0010 # Bottom-left.
399+
quadrants[half_height:, half_width:] = 0b0001 # Bottom-right.
400+
401+
for codepoint, quad_mask in (
402+
(0x2580, 0b1100), # "▀" Upper half block.
403+
(0x2584, 0b0011), # "▄" Lower half block.
404+
(0x2588, 0b1111), # "█" Full block.
405+
(0x258C, 0b1010), # "▌" Left half block.
406+
(0x2590, 0b0101), # "▐" Right half block.
407+
(0x2596, 0b0010), # "▖" Quadrant lower left.
408+
(0x2597, 0b0001), # "▗" Quadrant lower right.
409+
(0x2598, 0b1000), # "▘" Quadrant upper left.
410+
(0x2599, 0b1011), # "▙" Quadrant upper left and lower left and lower right.
411+
(0x259A, 0b1001), # "▚" Quadrant upper left and lower right.
412+
(0x259B, 0b1110), # "▛" Quadrant upper left and upper right and lower left.
413+
(0x259C, 0b1101), # "▜" Quadrant upper left and upper right and lower right.
414+
(0x259D, 0b0100), # "▝" Quadrant upper right.
415+
(0x259E, 0b0110), # "▞" Quadrant upper right and lower left.
416+
(0x259F, 0b0111), # "▟" Quadrant upper right and lower left and lower right.
417+
):
418+
alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8) * 255
419+
tileset.set_tile(codepoint, alpha)
420+
421+
for codepoint, axis, fraction, negative in (
422+
(0x2581, 0, 7, True), # "▁" Lower one eighth block.
423+
(0x2582, 0, 6, True), # "▂" Lower one quarter block.
424+
(0x2583, 0, 5, True), # "▃" Lower three eighths block.
425+
(0x2585, 0, 3, True), # "▅" Lower five eighths block.
426+
(0x2586, 0, 2, True), # "▆" Lower three quarters block.
427+
(0x2587, 0, 1, True), # "▇" Lower seven eighths block.
428+
(0x2589, 1, 7, False), # "▉" Left seven eighths block.
429+
(0x258A, 1, 6, False), # "▊" Left three quarters block.
430+
(0x258B, 1, 5, False), # "▋" Left five eighths block.
431+
(0x258D, 1, 3, False), # "▍" Left three eighths block.
432+
(0x258E, 1, 2, False), # "▎" Left one quarter block.
433+
(0x258F, 1, 1, False), # "▏" Left one eighth block.
434+
(0x2594, 0, 1, False), # "▔" Upper one eighth block.
435+
(0x2595, 1, 7, True), # "▕" Right one eighth block .
436+
):
437+
indexes = [slice(None), slice(None)]
438+
divide = tileset.tile_shape[axis] * fraction // 8
439+
# If negative then shade from the far corner, otherwise shade from the near corner.
440+
indexes[axis] = slice(divide, None) if negative else slice(None, divide)
441+
alpha = np.zeros(tileset.tile_shape, dtype=np.uint8)
442+
alpha[tuple(indexes)] = 255
443+
tileset.set_tile(codepoint, alpha)
444+
445+
301446
CHARMAP_CP437 = [
302447
0x0000,
303448
0x263A,

tests/test_tileset.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import tcod
2+
3+
4+
def test_proc_block_elements() -> None:
5+
tileset = tcod.tileset.Tileset(8, 8)
6+
tcod.tileset.procedural_block_elements(tileset)
7+
tileset = tcod.tileset.Tileset(0, 0)
8+
tcod.tileset.procedural_block_elements(tileset)

0 commit comments

Comments
 (0)