diff --git a/adafruit_displayio_layout/utilities/wheel_maker.py b/adafruit_displayio_layout/utilities/wheel_maker.py new file mode 100644 index 0000000..ff58b71 --- /dev/null +++ b/adafruit_displayio_layout/utilities/wheel_maker.py @@ -0,0 +1,190 @@ +# SPDY-FileCopyrightText: 2012 jacksongabbard +# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries +# SPDX-FileCopyrightText: 2021 Kevin Matocha, Jose David M. +# +# SPDX-License-Identifier: MIT +# SPDY-License-Identifier: Unlicense + +""" +`adafruit_wheel_maker` +================================================================================ + +Save a displayio.Bitmap (and associated displayio.Palette) in a BMP file. +This script is adapted in the works from Dave Astels on the ``adafruit_bitmapsaver`` +and the works of Jackson Glabbard +https://jg.gg/2012/05/28/generating-a-color-picker-style-color-wheel-in-python/ +https://github.com/jacksongabbard/Python-Color-Gamut-Generator/blob/master/color-wheel-generator.py +and Kevin Matocha on the ``switch_round`` for the ``_color_to_tuple`` function + +* Author(s): Dave Astels, Jackson Glabbard, Kevin Matocha, Jose David M. + +Implementation Notes +-------------------- + +**Hardware:** + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import math +import struct +import gc +import board +import digitalio +import busio + +try: + import adafruit_sdcard + import storage +except ImportError: + pass + +# pylint: disable=invalid-name, no-member, too-many-locals + + +def _write_bmp_header(output_file, filesize): + output_file.write(bytes("BM", "ascii")) + output_file.write(struct.pack(" 1: + shade_ratio = shade - 1 + new_chan = (0xFF * shade_ratio) + (new_chan * (1 - shade_ratio)) + + output = output + (int(new_chan) << bit) + bit = bit + 8 + return output + + +def color_to_tuple(value): + """Converts a color from a 24-bit integer to a tuple. + :param value: RGB LED desired value - can be a RGB tuple or a 24-bit integer. + """ + if isinstance(value, tuple): + return value + if isinstance(value, int): + if value >> 24: + raise ValueError("Only bits 0->23 valid for integer input") + r = value >> 16 + g = (value >> 8) & 0xFF + b = value & 0xFF + return [r, g, b] + + raise ValueError("Color must be a tuple or 24-bit integer value.") + + +color_wheel = [ + [0xFF, 0x00, 0xFF], + [0xFF, 0x00, 0x00], + [0xFF, 0xFF, 0x00], + [0x00, 0xFF, 0x00], + [0x00, 0xFF, 0xFF], + [0x00, 0x00, 0xFF], + [0xFF, 0x00, 0xFF], +] + + +def make_wheel(image_name, img_size, bg_color): + """ + :param image_name: TBD + :param img_size: TBD + :param bg_color: TBD + :return: color + """ + img_size_width = img_size + img_size_height = img_size + img_half = img_size / 2 + outer_radius = img_size // 2 + background_color = color_to_tuple(bg_color) + row_buffer = bytearray(_bytes_per_row(img_size_width)) + result_buffer = bytearray(2048) + + spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) + cs = digitalio.DigitalInOut(board.SD_CS) + sdcard = adafruit_sdcard.SDCard(spi, cs) + vfs = storage.VfsFat(sdcard) + storage.mount(vfs, "/sd") + file_path = "/sd" + image_name + print("saving starts") + output_file = open(file_path, "wb") + filesize = 54 + img_size_height * _bytes_per_row(img_size_width) + _write_bmp_header(output_file, filesize) + _write_dib_header(output_file, img_size_width, img_size_height) + + for y in range(img_size, 0, -1): + buffer_index = 0 + for x in range(img_size): + dist = abs(math.sqrt((x - img_half) ** 2 + (y - img_half) ** 2)) + shade = 1 * dist / outer_radius + if x - img_half == 0: + angle = -90 + if y > img_half: + angle = 90 + else: + angle = math.atan2((y - img_half), (x - img_half)) * 180 / math.pi + + angle = (angle - 30) % 360 + + idx = angle / 60 + if idx < 0: + idx = 6 + idx + base = int(round(idx)) + + adj = (6 + base + (-1 if base > idx else 1)) % 6 + ratio = max(idx, base) - min(idx, base) + color = make_color(base, adj, ratio, shade) + + if dist > outer_radius: + color_rgb = background_color + else: + color_rgb = color_to_tuple(color) + + for b in color_rgb: + row_buffer[buffer_index] = b & 0xFF + buffer_index += 1 + output_file.write(row_buffer) + for i in range(img_size_width * 2): + result_buffer[i] = 0 + gc.collect() + + output_file.close() + print("saving done") diff --git a/adafruit_displayio_layout/widgets/color_picker.py b/adafruit_displayio_layout/widgets/color_picker.py new file mode 100644 index 0000000..b6b5b47 --- /dev/null +++ b/adafruit_displayio_layout/widgets/color_picker.py @@ -0,0 +1,236 @@ +# SPDX-FileCopyrightText: 2021 Jose David +# +# SPDX-License-Identifier: MIT +""" + +`color_picker` +================================================================================ +A colorpicker using a existing bitmap created by the make_wheel utility. + +* Author(s): Jose David M. + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +################################ +# A color picker for CircuitPython, using displayio +# +# Features: +# - color picker using a existing Bitmap +# +# Future options to consider: +# --------------------------- +# Better color wheel logic +# Sliders to mix Red, Green and Blue +# + +import math +from displayio import TileGrid, OnDiskBitmap, ColorConverter +from adafruit_displayio_layout.widgets.widget import Widget +from adafruit_displayio_layout.widgets.control import Control + + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git" + + +class ColorPicker(Widget, Control): + """A widget to be used to select colors from a heel. + + :param str filename: name of the bitmap file to be used as a ColorPicker + + :param int x: x position of the color picker origin + :param int y: y position of the color picker origin + + :param int imagesize: size of the bitmap file. The bitmap colorwheels are squares. + + + **Quickstart: Importing and using the Color Picker** + + Here is one way of importing the ``ColorPicker`` class so you can use: + + .. code-block:: python + + from adafruit_displayio_layout.widgets.color_picker import ColorPicker + + Now you can create an Slider at pixel position x=20, y=30 using: + + .. code-block:: python + + my_colorpicker=ColorPicker(x=20, y=30) + + Once you setup your display, you can now add ``my_colorpicker`` to your display using: + + .. code-block:: python + + display.show(my_colorpicker) # add the group to the display + + If you want to have multiple display elements, you can create a group and then + append the slider and the other elements to the group. Then, you can add the full + group to the display as in this example: + + .. code-block:: python + + my_colorpicker= ColorPicker(20, 30) + my_group = displayio.Group(max_size=10) # make a group that can hold 10 items + my_group.append(my_colorpicker) # Add my_slider to the group + + # + # Append other display elements to the group + # + + display.show(my_group) # add the group to the display + + + **Final Notes** + + Depending on the screen results may vary. Resolution of the bitmap will no be as seen in a PC. + Sensitivity of the screen could also affect the behaviour of the library. + + + **The Color Picker Widget** + + .. figure:: color_picker.png + :scale: 100 % + :align: center + :alt: Example of the color picker widget. + + Example of the color picker. representation will vary according to screen used. + + + + """ + + # pylint: disable=too-many-lines, too-many-instance-attributes, too-many-arguments + # pylint: disable=too-many-locals, too-many-statements + + def __init__( + self, + filename: str = None, + x: int = 0, + y: int = 0, + image_size: int = 100, + **kwargs, + ): + + Widget.__init__( + self, x=x, y=y, height=image_size, width=image_size, **kwargs, max_size=4 + ) + Control.__init__(self) + + self._file = open(filename, "rb") + image = OnDiskBitmap(self._file) + tile_grid = TileGrid(image, pixel_shader=ColorConverter()) + + self._image_size = image_size + + self.append(tile_grid) + + self.touch_boundary = ( + 0, + 0, + image.width, + image.height, + ) + + def contains(self, touch_point): # overrides, then calls Control.contains(x,y) + + """Checks if the ColorPicker was touched. Returns True if the touch_point is + within the ColorPicker's touch_boundary. + + :param touch_point: x,y location of the screen, converted to local coordinates. + :type touch_point: Tuple[x,y] + :return: Boolean + """ + + touch_x = ( + touch_point[0] - self.x + ) # adjust touch position for the local position + touch_y = touch_point[1] - self.y + + return super().contains((touch_x, touch_y, 0)) + + def when_selected(self, touch_point, screen_height): + """Response function when ColorPicker is selected. When selected, the ColorPicker + will give the color corresponding with the position + + :param touch_point: x,y location of the screen, in absolute display coordinates. + :param int screen_height: screen height + :return: Color + + """ + + touch_x = ( + touch_point[0] - self.x + ) # adjust touch position for the local position + touch_y = screen_height - touch_point[1] - self.y + + # Call the parent's .selected function in case there is any work up there. + # touch_point is adjusted for group's x,y position before sending to super() + super().selected((touch_x, touch_y, 0)) + return self._color_from_position(touch_x, touch_y, self._image_size) + + def _color_from_position(self, x, y, image_size): + img_half = image_size // 2 + dist = abs(math.sqrt((x - img_half) ** 2 + (y - img_half) ** 2)) + if x - img_half == 0: + angle = -90 + if y > img_half: + angle = 90 + else: + angle = math.atan2((y - img_half), (x - img_half)) * 180 / math.pi + + angle = (angle + 30) % 360 + + shade = 1 * dist / img_half + idx = angle / 60 + base = int(round(idx)) + adj = (6 + base + (-1 if base > idx else 1)) % 6 + ratio = max(idx, base) - min(idx, base) + color = self._make_color(base, adj, ratio, shade) + + return color + + @staticmethod + def _make_color(base, adj, ratio, shade): + """ + Go through each bit of the colors adjusting blue with blue, red with red, + green with green, etc. + """ + color_wheel = [ + [0xFF, 0x00, 0xFF], + [0xFF, 0x00, 0x00], + [0xFF, 0xFF, 0x00], + [0x00, 0xFF, 0x00], + [0x00, 0xFF, 0xFF], + [0x00, 0x00, 0xFF], + [0xFF, 0x00, 0xFF], + ] + + output = 0x0 + bit = 0 + + for pos in range(3): + base_chan = color_wheel[base][pos] + adj_chan = color_wheel[adj][pos] + new_chan = int(round(base_chan * (1 - ratio) + adj_chan * ratio)) + + # now alter the channel by the shade + if shade < 1: + new_chan = new_chan * shade + elif shade > 1: + shade_ratio = shade - 1 + new_chan = (0xFF * shade_ratio) + (new_chan * (1 - shade_ratio)) + + output = output + (int(new_chan) << bit) + bit = bit + 8 + return output diff --git a/docs/api.rst b/docs/api.rst index 8b504f8..860a19b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -52,3 +52,10 @@ :inherited-members: .. inheritance-diagram:: adafruit_displayio_layout.widgets.cartesian + +.. automodule:: adafruit_displayio_layout.widgets.color_picker + :members: + :member-order: bysource + :inherited-members: + +.. inheritance-diagram:: adafruit_displayio_layout.widgets.color_picker diff --git a/docs/color_picker.png b/docs/color_picker.png new file mode 100644 index 0000000..87a0021 Binary files /dev/null and b/docs/color_picker.png differ diff --git a/docs/color_picker.png.license b/docs/color_picker.png.license new file mode 100644 index 0000000..a55a809 --- /dev/null +++ b/docs/color_picker.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 Jose David M. + +SPDX-License-Identifier: MIT diff --git a/docs/conf.py b/docs/conf.py index 554e2bf..ae7a682 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,8 @@ "adafruit_imageload", "adafruit_display_text", "bitmaptools", + "adafruit_sdcard", + "storage", ] diff --git a/docs/examples.rst b/docs/examples.rst index 2c46c7f..d85d37a 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -69,3 +69,12 @@ Create a simple plot plane. .. literalinclude:: ../examples/displayio_layout_cartesian_simpletest.py :caption: examples/displayio_layout_cartesian_simpletest.py :linenos: + +Color Picker example +-------------------- + +Simple example of the use of the Color Picker using a 200x200 pixels Bitmap + +.. literalinclude:: ../examples/displayio_layout_color_picker_simpletest.py + :caption: examples/displayio_layout_color_picker_simpletest.py + :linenos: diff --git a/docs/wheel200.bmp b/docs/wheel200.bmp new file mode 100644 index 0000000..0ffcaa3 Binary files /dev/null and b/docs/wheel200.bmp differ diff --git a/docs/wheel200.bmp.license b/docs/wheel200.bmp.license new file mode 100644 index 0000000..a55a809 --- /dev/null +++ b/docs/wheel200.bmp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 Jose David M. + +SPDX-License-Identifier: MIT diff --git a/examples/displayio_layout_color_picker_simpletest.py b/examples/displayio_layout_color_picker_simpletest.py new file mode 100644 index 0000000..b02428e --- /dev/null +++ b/examples/displayio_layout_color_picker_simpletest.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2021 Jose David +# +# SPDX-License-Identifier: MIT +""" +Creates a single color picker +""" + +import time +import board +import terminalio +from displayio import Group, TileGrid, Bitmap, Palette +from adafruit_display_text import bitmap_label +from adafruit_displayio_layout.widgets.color_picker import ColorPicker +import adafruit_touchscreen + + +display = board.DISPLAY + +# TouchScreen Configuration +ts = adafruit_touchscreen.Touchscreen( + board.TOUCH_XL, + board.TOUCH_XR, + board.TOUCH_YD, + board.TOUCH_YU, + calibration=((5200, 59000), (5800, 57000)), + size=(display.width, display.height), +) + +# Colorwheel Bitmap file +filename = "wheel200.bmp" # You can find this file in the examples directory in the library Github +# Change the imagesize_used according to the bitmap file used. Colorwheel are identified +# according to the size in pixels +imagesize_used = 200 +my_colorpicker = ColorPicker( + filename, + display.width // 2 - imagesize_used // 2, + display.height // 2 - imagesize_used // 2, + imagesize_used, +) +my_group = Group(max_size=4) +my_group.append(my_colorpicker) + +palette = Palette(2) +palette[0] = 0x990099 +palette[1] = 0x00FFFF + +bitmap = Bitmap(100, 20, 2) +color_square = TileGrid(bitmap, pixel_shader=palette, x=display.width - 100, y=10) +my_group.append(color_square) +# Adding text information +text_area = bitmap_label.Label( + terminalio.FONT, + text="Color", + x=display.width - 100, + y=35, +) +my_group.append(text_area) + +# Add my_group to the display +display.show(my_group) + +p = False +# Start the main loop +while True: + p = ts.touch_point + if p: # Check if colorpicker is selected + if my_colorpicker.contains(p): + color = my_colorpicker.when_selected(p, display.height) + palette[0] = color + print(f"Color Selected is: {hex(color)}") + text_area.text = str(hex(color)) + time.sleep(1.5) + + time.sleep(0.05) # touch response on PyPortal is more accurate with a small delay