Skip to content

Commit

Permalink
Formalize colors api (#25)
Browse files Browse the repository at this point in the history
- Break colors module up into submodules;
- support web colors;
- formalize API
- documentation
  • Loading branch information
corranwebster authored Oct 26, 2024
1 parent 183037d commit 9873b12
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 80 deletions.
1 change: 1 addition & 0 deletions ci/deploy_to_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def deploy():
deploy_py_files(Path("examples/data"), ":/data")
deploy_py_files(Path("examples/example_fonts"), ":/example_fonts")
deploy_py_files(Path("src/tempe"), ":/lib/tempe")
deploy_py_files(Path("src/tempe/colors"), ":/lib/tempe/colors")
# deploy_py_files(Path("src/tempe/fonts"), ":/lib/tempe/fonts")
deploy_py_files(Path("src/tempe/colormaps"), ":/lib/tempe/colormaps")
except subprocess.CalledProcessError as exc:
Expand Down
49 changes: 49 additions & 0 deletions docs/source/user_guide/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,55 @@ write::
and scripting code that wants to do drawing with Tempe should use the
higher-level convenience API in most cases.

Colors and Colormaps
--------------------

Tempe assumes that all colors are 16-bit/2-byte, but most of the code doesn't
care exactly what encoding is being used: the raw values are passed directly
through to the device. However, working directly with integer color values
in a format like RGB565 is awkward, particularly if the endianness of the
target device is different from the microcontroller.

From a high-level API, we'd like to be able to specify the colors used in a
more human-friendly way. Tempe provides a number of facilities in
~:py:mod:`tempe.colors` to help with this:

- a number of basic colors are available as module variables. This include
the basic web/VGA colors, as well as a series of greys as ``grey_1`` through
``grey_f`` which correspond to 3-digit hex colors ``#111`` through ``#fff``.

- the :py:func:`~tempe.colors.from_str` function accepts 3- and 6-digit hex
codes of the form ``"#abc"`` and ``"#abcdef"``, as well as all extended web
color names, and returns a matching RGB565 color.

- the :py:func:`~tempe.colors.rgb565` method takes floating point r, g, b values
in the range 0.0-1.0 and converts them to RGB565 colors. There are several other
conversion functions for other common formats.

In addition, when creating data visualizations it is common to map numerical
values to colors. The :py:mod:`tempe.colormaps` package has sub-modules for
a number of common color maps. These include:

- :py:mod:`tempe.colormaps.viridis.viridis`, :py:mod:`~tempe.colormaps.magma.magma`,
:py:mod:`~tempe.colormaps.plasma.plasma`, and :py:mod:`~tempe.colormaps.inferno.inferno`
are perceptually uniform color maps from Matplotlib.

- :py:mod:`tempe.colormaps.twilight.twilight` is a circular perceptually uniform color
map from Matplotlib.

These are provided as arrays of 256 colors, allowing them to be used either in
custom mapping functions, or passed as palettes for 8-bit Bitmaps (see below).
Each colormap is 512 bytes, which is why they are stored in separate modules:
import only what you need to save memory.

.. note::

Since development of Tempe has so-far been done on screens that expect
data to be transmitted in big-endian byte order, the byte order of colors
and colormaps is big-endian. This can be confusing on a system like a
Raspberry Pi Pico, which is little-endian.


Complex Shapes
==============

Expand Down
77 changes: 0 additions & 77 deletions src/tempe/colors.py

This file was deleted.

23 changes: 23 additions & 0 deletions src/tempe/colors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from .basic import (
aqua, black, blue, fuchsia, gray, grey, green, lime,
maroon, navy, olive, purple, red, silver, teal, white,
yellow,
)
from .convert import from_str, rgb444_to_rgb565, rgb565

grey_0 = 0x0000
grey_1 = rgb444_to_rgb565(0x1, 0x1, 0x1)
grey_2 = rgb444_to_rgb565(0x2, 0x2, 0x2)
grey_3 = rgb444_to_rgb565(0x3, 0x3, 0x3)
grey_4 = rgb444_to_rgb565(0x4, 0x4, 0x4)
grey_5 = rgb444_to_rgb565(0x5, 0x5, 0x5)
grey_6 = rgb444_to_rgb565(0x6, 0x6, 0x6)
grey_7 = rgb444_to_rgb565(0x7, 0x7, 0x7)
grey_8 = rgb444_to_rgb565(0x8, 0x8, 0x8)
grey_9 = rgb444_to_rgb565(0x9, 0x9, 0x9)
grey_a = rgb444_to_rgb565(0xA, 0xA, 0xA)
grey_b = rgb444_to_rgb565(0xB, 0xB, 0xB)
grey_c = rgb444_to_rgb565(0xC, 0xC, 0xC)
grey_d = rgb444_to_rgb565(0xD, 0xD, 0xD)
grey_e = rgb444_to_rgb565(0xE, 0xE, 0xE)
grey_f = rgb444_to_rgb565(0xF, 0xF, 0xF)
22 changes: 22 additions & 0 deletions src/tempe/colors/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Basic colors
These are basic named colors from VGA and early HTML and CSS definitions.
"""

aqua = 0xff07
black = 0x0000
blue = 0x1f00
fuchsia = 0x1ff8
gray = 0x1084
green = 0x0004
grey = 0x1084
lime = 0xe007
maroon = 0x0080
navy = 0x1000
olive = 0x0084
purple = 0x1080
red = 0x00f8
silver = 0x18c6
teal = 0x1004
white = 0xffff
yellow = 0xe0ff
60 changes: 60 additions & 0 deletions src/tempe/colors/convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

def rgb_to_rgb565(colors):
rgb565_colors = []
for color in colors:
r, g, b = color
bytes = (int(r * 0x1F) << 11) | (int(g * 0x3F) << 5) | int(b * 0x1F)
rgb565_colors.append((bytes >> 8) | ((bytes & 0xFF) << 8))
return rgb565_colors


def rgb444_to_rgb565(r, g, b, big_endian=True):
bytes = (r << 12) | (g << 7) | (b << 1)
if big_endian:
return (bytes >> 8) | ((bytes & 0xFF) << 8)
else:
return bytes


def rgb24_to_rgb565(r, g, b, big_endian=True):
bytes = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
if big_endian:
return (bytes >> 8) | ((bytes & 0xFF) << 8)
else:
return bytes


def rgb565(r, g, b, big_endian=True):
bytes = (
(int(round(r * 0x1f)) << 11)
| (int(round(g * 0x3f)) << 5)
| int(round(b * 0x1f))
)
if big_endian:
return (bytes >> 8) | ((bytes & 0xFF) << 8)
else:
return bytes


def from_str(color_str):
color_str = color_str.lower()
if color_str.startswith("#"):
if len(color_str) == 4:
return rgb444_to_rgb565(*(int(c, 16) for c in color_str[1:]))
elif len(color_str) == 7:
return rgb24_to_rgb565(
*(int(f"{color_str[i:i+2]}", 16) for i in range(1, len(color_str), 2))
)
else:
# is it a named Tempe color?
from tempe import colors
if (c := getattr(colors, color_str, None)) and isinstance(c, int):
return c

# try a named web color
from .web import color
c = color(color_str)
if c != 0:
return c
else:
raise ValueError(f"Unknown color string {color_str!r}")
93 changes: 93 additions & 0 deletions src/tempe/colors/web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Named web color lookup
This uses a hashing approach, so will give false positives.
"""
import array

# This module works by using a perfect hash of the names of the extended
# web colors into an array of ~325 colors (the _data array). The hashing
# algorithm was generated by gperf and manually translated into Python.
# This approach vastly reduces the memory footprint of a lookup table of
# web colors even given the relative inefficiency of the hash.

# It may be possible to do slightly better, as "gray" and "grey" map to
# different entries in the current scheme, and it includes the basic web
# colors which have their own module.

_data = array.array(
"H",
b"\x07\xff\x00\x00\x84\x10\x00\x00\x84\x10\x04\x00\x00\x00\x04\x10"
b"\xfb\xea\x00\x00\x07\xe0\x9a\x85\x00\x00\x00\x00\xfd\x20\xff\xbd"
b"\x6c\x12\xd6\x9a\x97\x72\x00\x00\x6c\x12\xd6\x9a\xfc\xef\xfa\x20"
b"\x36\x66\xec\x10\xdb\x7a\x07\xef\x00\x00\x00\x00\xfb\x08\x74\x33"
b"\xf8\x1f\x03\x00\x00\x00\x74\x33\xfe\xa0\x46\xfa\x00\x00\xc6\x18"
b"\x88\x00\x88\x11\x00\x00\x00\x00\x90\x1a\x00\x00\xe7\x3f\x2c\x4a"
b"\x99\x99\x00\x00\x00\x00\xec\xaf\xff\x7c\xff\xda\x00\x10\xf8\x00"
b"\xd5\xb1\xcc\x27\x80\x00\xff\x16\x00\x00\x00\x00\x80\x10\x1d\x95"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x1f\x48\x10\xff\x7e"
b"\xfc\x40\xfc\x0e\xff\xbc\x86\x7d\x7f\xe0\xb1\x04\xff\xd9\x8d\xd1"
b"\xf7\xff\x00\x00\x53\x45\x00\x00\x00\x00\x00\x00\xbc\x21\x00\x00"
b"\x24\x44\x00\x00\x00\x00\x61\x93\x00\x00\xdd\x23\x00\x11\xae\x3b"
b"\xff\x1c\x00\x00\x97\xd2\xfd\xb8\xfe\x19\x00\x00\x00\x00\x00\x00"
b"\xca\xeb\x3d\x8e\x00\x00\xff\x18\xf6\xf6\x07\xff\xef\xff\x18\xcd"
b"\x00\x00\x8a\x22\x00\x00\xf5\x0b\x41\xf1\x6a\xd9\xae\xbc\x44\x16"
b"\x00\x00\xba\xba\x43\x5c\x7f\xc0\x00\x00\x1c\x9f\xff\xfb\xdf\xff"
b"\x7f\xfa\xaf\xe5\xdb\x72\x00\x00\x00\x00\xff\xdf\xff\xfd\xff\x59"
b"\xef\x31\x00\x19\x6b\x4d\x00\x00\xad\x55\x00\x00\x6b\x4d\x00\x00"
b"\xad\x55\x84\x00\x00\x1f\x00\x00\x00\x00\x7b\x5d\xc0\xb0\xef\x55"
b"\xd5\xfa\x00\x00\x00\x00\xdc\xfb\xff\xdb\x00\x00\x00\x00\xd3\x43"
b"\x00\x00\x00\x00\x07\xd3\x2a\x69\x00\x00\x00\x00\x00\x00\x2a\x69"
b"\x00\x00\x00\x00\x00\x00\x89\x5c\x00\x00\x5c\xf3\x6c\x64\xa1\x45"
b"\x00\x00\xf7\xbb\x04\x51\x00\x00\x00\x00\x00\x00\xec\x1d\x46\x99"
b"\x00\x00\xdd\xd0\xbd\xad\xd8\x87\xff\x7a\xbc\x71\x00\x00\x00\x00"
b"\x00\x00\x00\x00\xef\xfd\x00\x00\xf8\x92\x00\x00\xfe\xf5\x00\x00"
b"\x00\x00\xde\xfb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x64\xbd\x00\x00\x93\x7b\x00\x00\x00\x00\x00\x00\xae\xfc"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\xff\x5a\x00\x00\xfb\x56\x00\x00\xfe\xd7"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x7a\x00\x00\x00\x00"
b"\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x66\x75\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x00\x00\x00\x00\x00"
b"\xef\xdf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x86\x7f\x9e\x66\x00\x00\x00\x00\x00\x00\xff\xdf\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\xf7\xbe\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaf\x7d\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x05\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xdd",
)

_assoc_values = array.array(
"H",
b"P\x01\x07\x00,\x00\x03\x00\x0f\x00\x03\x00;\x00\x05\x00\x11\x00"
b"\x03\x00P\x01\x8a\x00\x04\x00\x0f\x00%\x00\x0c\x005\x00\x88\x00"
b"\x03\x00\x03\x00\n\x00@\x00(\x00l\x00\x17\x00o\x00P\x01\x08\x00"
b"P\x01"
)


def _perf_hash(s):
"""Hash function that gives a unique value for each web color.
"""
l = len(s)
v = l
for i in [0, 2, 5, 6, 7, 11, 12]:
if i >= l or v >= 336:
break
c = max(0, min(len(_assoc_values) - 1, ord(s[i]) - 96))
if i == 2:
v += _assoc_values[c + 2]
else:
v += _assoc_values[c]
return v


def color(name):
"""Return the RGB565 value for a web color, or 0x0000 if no match."""
index = _perf_hash(name) - 10
if 0 <= index < len(_data):
return _data[index]
else:
return 0x0000
18 changes: 16 additions & 2 deletions src/tempe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,29 @@
["tempe/bitmaps.py", "github:unital/tempe/src/tempe/bitmaps.py"],
["tempe/data_view.py", "github:unital/tempe/src/tempe/data_view.py"],
["tempe/display.py", "github:unital/tempe/src/tempe/display.py"],
["tempe/font.py", "github:unital/tempe/src/tempe/font.py"],
["tempe/geometry.py", "github:unital/tempe/src/tempe/geometry.py"],
["tempe/lines.py", "github:unital/tempe/src/tempe/lines.py"],
["tempe/markers.py", "github:unital/tempe/src/tempe/markers.py"],
["tempe/polar.py", "github:unital/tempe/src/tempe/polar.py"],
["tempe/polar_geometry.py", "github:unital/tempe/src/tempe/polar_geometry.py"],
["tempe/raster.py", "github:unital/tempe/src/tempe/raster.py"],
["tempe/shapes.py", "github:unital/tempe/src/tempe/shapes.py"],
["tempe/surface.py", "github:unital/tempe/src/tempe/surface.py"],
["tempe/text.py", "github:unital/tempe/src/tempe/text.py"],
["tempe/util.py", "github:unital/tempe/src/tempe/util.py"]
["tempe/util.py", "github:unital/tempe/src/tempe/util.py"],
["tempe/colormaps/__init__.py", "github:unital/tempe/src/tempe/colormaps/__init__.py"],
["tempe/colormaps/inferno.py", "github:unital/tempe/src/tempe/colormaps/inferno.py"],
["tempe/colormaps/magma.py", "github:unital/tempe/src/tempe/colormaps/magma.py"],
["tempe/colormaps/plasma.py", "github:unital/tempe/src/tempe/colormaps/plasma.py"],
["tempe/colormaps/twilight.py", "github:unital/tempe/src/tempe/colormaps/twilight.py"],
["tempe/colormaps/viridis.py", "github:unital/tempe/src/tempe/colormaps/viridis.py"],
["tempe/colors/__init__.py", "github:unital/tempe/src/tempe/colors/__init__.py"],
["tempe/colors/basic.py", "github:unital/tempe/src/tempe/colors/basic.py"],
["tempe/colors/convert.py", "github:unital/tempe/src/tempe/colors/convert.py"],
["tempe/colors/web.py", "github:unital/tempe/src/tempe/colors/web.py"],
["tempe/fonts/__init__.py", "github:unital/tempe/src/tempe/fonts/__init__.py"],
["tempe/fonts/roboto16.py", "github:unital/tempe/src/tempe/fonts/roboto16.py"],
["tempe/fonts/roboto16bold.py", "github:unital/tempe/src/tempe/fonts/roboto16bold.py"]
],
"version": "0.1"
}
2 changes: 1 addition & 1 deletion src/tempe/surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def _check_geometry(self, geometry, coords):

def _check_colors(self, colors):
if isinstance(colors, str):
from colors import from_str
from .colors import from_str

return Repeat(from_str(colors))
elif isinstance(colors, int):
Expand Down

0 comments on commit 9873b12

Please sign in to comment.