From b352feb5d741aeb9c14dfd2304646e8243dd4a50 Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Sun, 14 Mar 2021 19:17:47 -0500 Subject: [PATCH 1/9] add optional animation to icon_widget --- .../widgets/icon_widget.py | 179 ++++++++++++++++-- docs/conf.py | 1 + 2 files changed, 169 insertions(+), 11 deletions(-) diff --git a/adafruit_displayio_layout/widgets/icon_widget.py b/adafruit_displayio_layout/widgets/icon_widget.py index a678455..bd9efc0 100644 --- a/adafruit_displayio_layout/widgets/icon_widget.py +++ b/adafruit_displayio_layout/widgets/icon_widget.py @@ -21,25 +21,37 @@ https://github.com/adafruit/circuitpython/releases """ - - +import gc +import time import terminalio -from displayio import TileGrid +import bitmaptools +from displayio import TileGrid, Bitmap, Palette import adafruit_imageload from adafruit_display_text import bitmap_label from adafruit_displayio_layout.widgets.control import Control from adafruit_displayio_layout.widgets.widget import Widget +from adafruit_displayio_layout.widgets.easing import quadratic_easeout as easein +from adafruit_displayio_layout.widgets.easing import quadratic_easein as easeout class IconWidget(Widget, Control): """ A touch enabled widget that holds an icon image loaded with - adafruit_imageload and a text label centered beneath it. + adafruit_imageload and a text label centered beneath it. Includes optional + animation to increase the icon size when pressed. :param string label_text: the text that will be shown beneath the icon image. :param string icon: the filepath of the bmp image to be used as the icon. + :param float max_scale: the maximum zoom during animation, set 1.0 for no animation + a value of 1.4 is a good starting point (default: 1.0, no animation), + ``max_scale`` must be >= 1.0 + :param float max_angle: the maximum degrees of rotation during animation, set 0 for + no rotation, in degrees (default: 0 degrees) + :param float animation_time: the time for the animation in seconds, set to 0.0 for + no animation, a value of 0.15 is a good starting point (default: 0.15 seconds) + :param int x: x location the icon widget should be placed. Pixel coordinates. :param int y: y location the icon widget should be placed. Pixel coordinates. :param anchor_point: (X,Y) values from 0.0 to 1.0 to define the anchor point relative to the @@ -53,26 +65,63 @@ class IconWidget(Widget, Control): """ - def __init__(self, label_text, icon, **kwargs): - super().__init__(**kwargs) - image, palette = adafruit_imageload.load(icon) - tile_grid = TileGrid(image, pixel_shader=palette) + # pylint: disable=bad-super-call, too-many-instance-attributes + # pylint: disable=too-many-arguments, unused-argument + + def __init__( + self, + display, + label_text, + icon, + max_scale=1.0, + max_angle=8, + animation_time=0.0, + **kwargs + ): + + super().__init__(**kwargs) # initialize superclasses + super(Control, self).__init__() + + self.display = display + + self._image, self._palette = adafruit_imageload.load(icon) + tile_grid = TileGrid(self._image, pixel_shader=self._palette) self.append(tile_grid) _label = bitmap_label.Label( terminalio.FONT, scale=1, text=label_text, anchor_point=(0.5, 0), - anchored_position=(image.width // 2, image.height), + anchored_position=(self._image.width // 2, self._image.height), ) self.append(_label) self.touch_boundary = ( self.x, self.y, - image.width, - image.height + _label.bounding_box[3], + self._image.width, + self._image.height + _label.bounding_box[3], ) + # verify the animation settings + self._start_scale = 1.0 + + max_scale = max(1.0, max_scale) # constrain to > 1.0 + if max_scale == 1.0: # no animation + self._animation_time = 0 + else: + self._animation_time = animation_time # in seconds + self._end_scale = max_scale + + self._angle = (max_angle / 360) * 2 * 3.14 # 5 degrees, convert to radians + + # define zoom attributes + self._zoom_color_depth = None + self._zoom_palette = None + self._zoom_bitmap = None + self._zoom_tilegrid = None + + self.value = False # initial value + def contains(self, touch_point): # overrides, then calls Control.contains(x,y) """Checks if the IconWidget was touched. Returns True if the touch_point is @@ -89,3 +138,111 @@ def contains(self, touch_point): # overrides, then calls Control.contains(x,y) touch_y = touch_point[1] - self.y return super().contains((touch_x, touch_y, 0)) + + def selected(self, touch_point): + """Performs zoom animation when pressed. + + :param touch_point: x,y location of the screen, converted to local coordinates. + :type touch_point: Tuple[x,y] + :return: None + """ + + self.value = True + + if self._animation_time > 0: + ### + ## Create the zoom palette, bitmap and tilegrid + ### + + # copy the image palette, add a transparent color at the end + self._zoom_color_depth = len(self._palette) + 1 + + self._zoom_palette = Palette(self._zoom_color_depth) + for i in range(len(self._palette)): + self._zoom_palette[i] = self._palette[i] + self._zoom_palette[self._zoom_color_depth - 1] = 0x000000 + self._zoom_palette.make_transparent(self._zoom_color_depth - 1) + + # create the zoom bitmap larger than the original image to allow for zooming + self._zoom_bitmap = Bitmap( + round(self._image.width * self._end_scale), + round(self._image.height * self._end_scale), + len(self._zoom_palette), + ) + self._zoom_bitmap.fill(self._zoom_color_depth - 1) # transparent fill + self._zoom_bitmap.blit( + (self._zoom_bitmap.width - self._image.width) // 2, + (self._zoom_bitmap.height - self._image.height) // 2, + self._image, + ) # blit the image into the center of the zoom_bitmap + + # place zoom_bitmap at same location as image + self._zoom_tilegrid = TileGrid( + self._zoom_bitmap, pixel_shader=self._zoom_palette + ) + self._zoom_tilegrid.x = -(self._zoom_bitmap.width - self._image.width) // 2 + self._zoom_tilegrid.y = ( + -(self._zoom_bitmap.height - self._image.height) // 2 + ) + self.append(self._zoom_tilegrid) # add to the self group. + + # Animation: zoom larger + start_time = time.monotonic() + while True: + elapsed_time = time.monotonic() - start_time + position = min( + 1.0, easein(elapsed_time / self._animation_time) + ) # fractional position + bitmaptools.rotozoom( + dest_bitmap=self._zoom_bitmap, + ox=self._zoom_bitmap.width // 2, + oy=self._zoom_bitmap.height // 2, + source_bitmap=self._image, + px=self._image.width // 2, + py=self._image.height // 2, + scale=self._start_scale + + position * (self._end_scale - self._start_scale), + angle=position * self._angle / 2, + ) + self.display.refresh() + if elapsed_time > self._animation_time: + break + + def released(self, touch_point): + """Performs un-zoom animation when released. + + :param touch_point: x,y location of the screen, converted to local coordinates. + :type touch_point: Tuple[x,y] + :return: None + """ + + if (self._animation_time > 0) and self.value: + # Animation: shrink down to the original size + start_time = time.monotonic() + while True: + elapsed_time = time.monotonic() - start_time + position = max(0.0, easeout(1 - (elapsed_time / self._animation_time))) + self._zoom_bitmap.fill(self._zoom_color_depth - 1) + bitmaptools.rotozoom( + dest_bitmap=self._zoom_bitmap, + ox=self._zoom_bitmap.width // 2, + oy=self._zoom_bitmap.height // 2, + source_bitmap=self._image, + px=self._image.width // 2, + py=self._image.height // 2, + scale=self._start_scale + + position * (self._end_scale - self._start_scale), + angle=position * self._angle / 2, + ) + self.display.refresh() + if elapsed_time > self._animation_time: + break + + # clean up the zoom display elements + self.pop(-1) # remove self from the group + del self._zoom_tilegrid + del self._zoom_bitmap + del self._zoom_palette + gc.collect() + + self.value = False diff --git a/docs/conf.py b/docs/conf.py index fca1f66..bedbdf3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,7 @@ "terminalio", "adafruit_imageload", "adafruit_display_text", + "bitmaptools", ] From f6a1fddaf6789f1417db62866b3223718b319f0c Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Sun, 14 Mar 2021 19:24:39 -0500 Subject: [PATCH 2/9] docstring cleanup --- adafruit_displayio_layout/widgets/icon_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_displayio_layout/widgets/icon_widget.py b/adafruit_displayio_layout/widgets/icon_widget.py index bd9efc0..e83befc 100644 --- a/adafruit_displayio_layout/widgets/icon_widget.py +++ b/adafruit_displayio_layout/widgets/icon_widget.py @@ -50,7 +50,7 @@ class IconWidget(Widget, Control): :param float max_angle: the maximum degrees of rotation during animation, set 0 for no rotation, in degrees (default: 0 degrees) :param float animation_time: the time for the animation in seconds, set to 0.0 for - no animation, a value of 0.15 is a good starting point (default: 0.15 seconds) + no animation, a value of 0.15 is a good starting point (default: 0.0 seconds) :param int x: x location the icon widget should be placed. Pixel coordinates. :param int y: y location the icon widget should be placed. Pixel coordinates. From f3393cfb2f96860c6423cb24430824bc9488b36f Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Tue, 16 Mar 2021 14:15:52 -0500 Subject: [PATCH 3/9] add palette animation --- .../widgets/icon_widget.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/adafruit_displayio_layout/widgets/icon_widget.py b/adafruit_displayio_layout/widgets/icon_widget.py index e83befc..d76eda0 100644 --- a/adafruit_displayio_layout/widgets/icon_widget.py +++ b/adafruit_displayio_layout/widgets/icon_widget.py @@ -25,6 +25,7 @@ import time import terminalio import bitmaptools +from _pixelbuf import colorwheel from displayio import TileGrid, Bitmap, Palette import adafruit_imageload from adafruit_display_text import bitmap_label @@ -62,6 +63,17 @@ class IconWidget(Widget, Control): :param int max_size: (Optional) this will get passed through to the displayio.Group constructor. If omitted we default to grid_size width * grid_size height to make room for all (1, 1) sized cells. + :param int wheel_initial_value: When using palette animation, this is the initial value + of the colorwheel parameter used with ``_pixelbuf.colorwheel`` + :param int wheel_increment: To add palette animation, set this to the value of + how much you want the ``_pixelbuf.colorwheel`` function to increment each time that + ``unselected`` is called (default: 0 for no palette animation) + :param int wheel_grading: This is the step sized used when calling colorwheel for + each color index in the palette (default: 5), basically it's how far apart each + color in the palette will be set. Use a low value if you want each color to be + close to each other or a high value to spread out into a wider range of colors. + :param int palette_skip_indices: integer or list of integers with the palette + indices that should not be changed when using the palette animations (default: None) """ @@ -76,7 +88,12 @@ def __init__( max_scale=1.0, max_angle=8, animation_time=0.0, - **kwargs + wheel_initial_value=1, # initial wheel color value + wheel_increment=0, # how much the wheel + wheel_grading=5, # sets the colorwheel distance between palette colors + palette_skip_indices=None, # single value or list of palette color indices to + # remain constant during animations + **kwargs, ): super().__init__(**kwargs) # initialize superclasses @@ -122,6 +139,16 @@ def __init__( self.value = False # initial value + self._wheel_value = wheel_initial_value # initial color wheel value + self._wheel_increment = ( + wheel_increment # color wheel increment for palette changing + ) + self._wheel_grading = wheel_grading + if isinstance(palette_skip_indices, list): + self._palette_skip_indices = palette_skip_indices + else: + self._palette_skip_indices = [palette_skip_indices] + def contains(self, touch_point): # overrides, then calls Control.contains(x,y) """Checks if the IconWidget was touched. Returns True if the touch_point is @@ -246,3 +273,17 @@ def released(self, touch_point): gc.collect() self.value = False + + def unselected(self): + """Performs animations when the button is not selected. Provides a color + wheel palette animation (if wheel_increment > 0).""" + if self._wheel_increment: + self._wheel_value = self._wheel_value + self._wheel_increment + for i in range(0, len(self._palette)): + if i in self._palette_skip_indices: + pass + else: + self._palette[i] = colorwheel( + self._wheel_value + i * self._wheel_grading + ) + self.display.refresh() From 21e570358be2b25b3f5a8028ebc47b5659483699 Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Wed, 17 Mar 2021 13:15:45 -0500 Subject: [PATCH 4/9] cleanup docstrings, remove colorwheel --- .../widgets/icon_widget.py | 46 ++++--------------- 1 file changed, 9 insertions(+), 37 deletions(-) mode change 100644 => 100755 adafruit_displayio_layout/widgets/icon_widget.py diff --git a/adafruit_displayio_layout/widgets/icon_widget.py b/adafruit_displayio_layout/widgets/icon_widget.py old mode 100644 new mode 100755 index c9f9290..878a7ae --- a/adafruit_displayio_layout/widgets/icon_widget.py +++ b/adafruit_displayio_layout/widgets/icon_widget.py @@ -41,16 +41,18 @@ class IconWidget(Widget, Control): adafruit_imageload and a text label centered beneath it. Includes optional animation to increase the icon size when pressed. - :param string label_text: the text that will be shown beneath the icon image. - :param string icon: the filepath of the bmp image to be used as the icon. - :param boolean on_disk: if True use OnDiskBitmap instead of imageload. + :param displayio.Display display: the display where the ``IconWidget`` is displayed, + used for handling animations. + :param str label_text: the text that will be shown beneath the icon image. + :param str icon: the filepath of the bmp image to be used as the icon. + :param bool on_disk: if True use OnDiskBitmap instead of imageload. This can be helpful to save memory. Defaults to False :param float max_scale: the maximum zoom during animation, set 1.0 for no animation a value of 1.4 is a good starting point (default: 1.0, no animation), ``max_scale`` must be between 1.0 and 1.5. - :param float max_angle: the maximum degrees of rotation during animation, set 0 for - no rotation, in degrees (default: 0 degrees) + :param float max_angle: the maximum degrees of rotation during animation, positive values + are clockwise, set 0 for no rotation, in degrees (default: 0 degrees) :param float animation_time: the time for the animation in seconds, set to 0.0 for no animation, a value of 0.15 is a good starting point (default: 0.0 seconds) @@ -64,18 +66,6 @@ class IconWidget(Widget, Control): :param int max_size: (Optional) this will get passed through to the displayio.Group constructor. If omitted we default to grid_size width * grid_size height to make room for all (1, 1) sized cells. - :param int wheel_initial_value: When using palette animation, this is the initial value - of the colorwheel parameter used with ``_pixelbuf.colorwheel`` - :param int wheel_increment: To add palette animation, set this to the value of - how much you want the ``_pixelbuf.colorwheel`` function to increment each time that - ``unselected`` is called (default: 0 for no palette animation) - :param int wheel_grading: This is the step sized used when calling colorwheel for - each color index in the palette (default: 5), basically it's how far apart each - color in the palette will be set. Use a low value if you want each color to be - close to each other or a high value to spread out into a wider range of colors. - :param int palette_skip_indices: integer or list of integers with the palette - indices that should not be changed when using the palette animations (default: None) - """ # pylint: disable=bad-super-call, too-many-instance-attributes, too-many-locals @@ -100,16 +90,9 @@ def __init__( max_scale=1.0, max_angle=8, animation_time=0.0, - wheel_initial_value=1, # initial wheel color value - wheel_increment=0, # how much the wheel - wheel_grading=5, # sets the colorwheel distance between palette colors - palette_skip_indices=None, # single value or list of palette color indices to - # remain constant during animations **kwargs, ): - print("kwargs: {}".format(kwargs)) - super().__init__(**kwargs) # initialize superclasses super(Control, self).__init__() @@ -117,7 +100,6 @@ def __init__( self._icon = icon if on_disk: - print("on_disk") self._file = open(icon, "rb") image = OnDiskBitmap(self._file) tile_grid = TileGrid(image, pixel_shader=ColorConverter()) @@ -162,16 +144,6 @@ def __init__( self.value = False # initial value - self._wheel_value = wheel_initial_value # initial color wheel value - self._wheel_increment = ( - wheel_increment # color wheel increment for palette changing - ) - self._wheel_grading = wheel_grading - if isinstance(palette_skip_indices, list): - self._palette_skip_indices = palette_skip_indices - else: - self._palette_skip_indices = [palette_skip_indices] - def contains(self, touch_point): # overrides, then calls Control.contains(x,y) """Checks if the IconWidget was touched. Returns True if the touch_point is @@ -189,7 +161,7 @@ def contains(self, touch_point): # overrides, then calls Control.contains(x,y) return super().contains((touch_x, touch_y, 0)) - def selected(self, touch_point): + def zoom_animation(self, touch_point): """Performs zoom animation when pressed. :param touch_point: x,y location of the screen, converted to local coordinates. @@ -257,7 +229,7 @@ def selected(self, touch_point): del _palette gc.collect() - def released(self, touch_point): + def zoom_out_animation(self, touch_point): """Performs un-zoom animation when released. :param touch_point: x,y location of the screen, converted to local coordinates. From 25098ece38a479387cf7e5b582afcb80473b4534 Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Thu, 18 Mar 2021 14:01:00 -0500 Subject: [PATCH 5/9] refactor IconAnimated into a subclass of IconWidget --- .../widgets/icon_animated.py | 243 ++++++++++++++++++ .../widgets/icon_widget.py | 193 +------------- docs/api.rst | 4 + 3 files changed, 259 insertions(+), 181 deletions(-) create mode 100644 adafruit_displayio_layout/widgets/icon_animated.py mode change 100755 => 100644 adafruit_displayio_layout/widgets/icon_widget.py diff --git a/adafruit_displayio_layout/widgets/icon_animated.py b/adafruit_displayio_layout/widgets/icon_animated.py new file mode 100644 index 0000000..44e56bd --- /dev/null +++ b/adafruit_displayio_layout/widgets/icon_animated.py @@ -0,0 +1,243 @@ +# SPDX-FileCopyrightText: 2021 Kevin Matocha +# +# SPDX-License-Identifier: MIT +""" + +`icon_animated` +================================================================================ +A touch enabled widget that includes an animated icon image with a small text label +centered below it. + +* Author(s): Kevin Matocha + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" +import gc +import time +import bitmaptools +from displayio import TileGrid, Bitmap, Palette +import adafruit_imageload +from adafruit_displayio_layout.widgets.icon_widget import IconWidget +from adafruit_displayio_layout.widgets.easing import quadratic_easeout as easein +from adafruit_displayio_layout.widgets.easing import quadratic_easein as easeout + + +class IconAnimated(IconWidget): + + """ + An animated touch enabled widget that holds an icon image loaded with + OnDiskBitmap and a text label centered beneath it. Includes optional + animation to increase the icon size when pressed. + + .. Warning:: The `init_class` class function should be called before instancing any + IconAnimated widgets. + + :param str label_text: the text that will be shown beneath the icon image. + :param str icon: the filepath of the bmp image to be used as the icon. + :param bool on_disk: if True use OnDiskBitmap instead of imageload. + This can be helpful to save memory. Defaults to False + + :param float max_scale: the maximum zoom during animation, set 1.0 for no animation + a value of 1.4 is a good starting point (default: 1.0, no animation), + ``max_scale`` must be between 1.0 and 1.5. + :param float max_angle: the maximum degrees of rotation during animation, positive values + are clockwise, set 0 for no rotation, in degrees (default: 0 degrees) + :param float animation_time: the time for the animation in seconds, set to 0.0 for + no animation, a value of 0.15 is a good starting point (default: 0.0 seconds) + + :param int x: x location the icon widget should be placed. Pixel coordinates. + :param int y: y location the icon widget should be placed. Pixel coordinates. + :param anchor_point: (X,Y) values from 0.0 to 1.0 to define the anchor point relative to the + widget bounding box + :type anchor_point: Tuple[float,float] + :param int anchored_position: (x,y) pixel value for the location of the anchor_point + :type anchored_position: Tuple[int, int] + :param int max_size: (Optional) this will get passed through to the + displayio.Group constructor. If omitted we default to + grid_size width * grid_size height to make room for all (1, 1) sized cells. + """ + + # pylint: disable=bad-super-call, too-many-instance-attributes, too-many-locals + # pylint: disable=too-many-arguments, unused-argument + + display = None + # The other Class variables are created in Class method `init_class`: + # max_scale, bitmap_buffer, palette_buffer + + @classmethod + def init_class( + cls, display=None, max_scale=1.5, max_size=(80, 80), max_color_depth=512 + ): + """ + Initializes the IconAnimated Class variables, including preallocating memory + buffers for the icon zoom bitmap and icon palette. + + .. Note:: The `init_class` class function should be called before instancing any + IconAnimated widgets. Usage example: + ``IconAnimated(display=board.DISPLAY, max_scale=1.5, + max_size=(80,80), max_color_depth=256)`` + + :param displayio.Display display: The display where the icons will be displayed. + :param float max_scale: The maximum zoom of the any of the icons, should be > 1.0, + (default: 1.5) + :param max_size: The maximum (x,y) pixel dimensions of any `IconAnimated` bitmap size + that will be created (default: (80,80)) + :type max_size: Tuple[int,int] + :param int max_color_depth: The maximum color depth of any `IconAnimated` + bitmap that will be created (default: 512) + """ + if display is None: + raise ValueError("Must provide display parameter for IconAnimated.") + cls.display = display + cls.max_scale = max(1.0, max_scale) + cls.bitmap_buffer = Bitmap( + round(max_scale * max_size[0]), + round(max_scale * max_size[1]), + max_color_depth + 1, + ) + cls.palette_buffer = Palette(max_color_depth + 1) + + def __init__( + self, + label_text, + icon, + on_disk=False, + max_scale=1.4, + max_angle=8, + animation_time=0.15, + **kwargs, + ): + + if self.__class__.display is None: + raise ValueError( + "Must initialize class using\n" + "`IconAnimated.init_class(display, max_scale, max_size, max_color_depth)`\n" + "prior to instancing IconAnimated widgets." + ) + + super().__init__(label_text, icon, on_disk, **kwargs) # initialize superclasses + + # constrain instance's maximum_scaling between 1.0 and the Class's max_scale + self._max_scale = min(max(1.0, max_scale), self.__class__.max_scale) + if max_scale == 1.0: # no animation + self._animation_time = 0 + else: + self._animation_time = animation_time # in seconds + self._angle = (max_angle / 360) * 2 * 3.14 # 5 degrees, convert to radians + self._zoomed = False # state variable for zoom status + + def zoom_animation(self, touch_point): + """Performs zoom animation when icon is pressed. + + :param touch_point: x,y location of the screen. + :type touch_point: Tuple[x,y] + :return: None + """ + + if self._animation_time > 0: + ### + ## Update the zoom palette and bitmap buffers and append the tilegrid + ### + + _image, _palette = adafruit_imageload.load(self._icon) + animation_bitmap = self.__class__.bitmap_buffer + animation_palette = self.__class__.palette_buffer + + # copy the image palette, add a transparent color at the end + for i, color in enumerate(_palette): + animation_palette[i] = color + animation_palette[len(animation_palette) - 1] = 0x000000 + animation_palette.make_transparent(len(animation_palette) - 1) + + # create the zoom bitmap larger than the original image to allow for zooming + animation_bitmap.fill(len(animation_palette) - 1) # transparent fill + animation_bitmap.blit( + (animation_bitmap.width - _image.width) // 2, + (animation_bitmap.height - _image.height) // 2, + _image, + ) # blit the image into the center of the zoom_bitmap + + # place zoom_bitmap at same location as image + animation_tilegrid = TileGrid( + animation_bitmap, pixel_shader=animation_palette + ) + animation_tilegrid.x = -(animation_bitmap.width - _image.width) // 2 + animation_tilegrid.y = -(animation_bitmap.height - _image.height) // 2 + self.append(animation_tilegrid) # add to the self group. + + # Animation: zoom larger + start_time = time.monotonic() + while True: + elapsed_time = time.monotonic() - start_time + position = min( + 1.0, easein(elapsed_time / self._animation_time) + ) # fractional position + bitmaptools.rotozoom( + dest_bitmap=animation_bitmap, + ox=animation_bitmap.width // 2, + oy=animation_bitmap.height // 2, + source_bitmap=_image, + px=_image.width // 2, + py=_image.height // 2, + scale=1.0 # start scaling at 1.0 + + position * (self._max_scale - 1.0), + angle=position * self._angle / 2, + ) + self.__class__.display.refresh() + if elapsed_time > self._animation_time: + break + del _image + del _palette + gc.collect() + + self._zoomed = True + + def zoom_out_animation(self, touch_point): + """Performs un-zoom animation when icon is released. + + :param touch_point: x,y location of the screen. + :type touch_point: Tuple[x,y] + :return: None + """ + + _image, _palette = adafruit_imageload.load(self._icon) + animation_bitmap = self.__class__.bitmap_buffer + animation_palette = self.__class__.palette_buffer + + if (self._animation_time > 0) and self._zoomed: + # Animation: shrink down to the original size + start_time = time.monotonic() + while True: + elapsed_time = time.monotonic() - start_time + position = max(0.0, easeout(1 - (elapsed_time / self._animation_time))) + animation_bitmap.fill(len(animation_palette) - 1) + bitmaptools.rotozoom( + dest_bitmap=animation_bitmap, + ox=animation_bitmap.width // 2, + oy=animation_bitmap.height // 2, + source_bitmap=_image, + px=_image.width // 2, + py=_image.height // 2, + scale=1.0 + position * (self._max_scale - 1.0), + angle=position * self._angle / 2, + ) + self.__class__.display.refresh() + if elapsed_time > self._animation_time: + break + + # clean up the zoom display elements + self.pop(-1) # remove self from the group + del _image + del _palette + gc.collect() + + self._zoomed = False diff --git a/adafruit_displayio_layout/widgets/icon_widget.py b/adafruit_displayio_layout/widgets/icon_widget.py old mode 100755 new mode 100644 index 878a7ae..aae4f8e --- a/adafruit_displayio_layout/widgets/icon_widget.py +++ b/adafruit_displayio_layout/widgets/icon_widget.py @@ -21,41 +21,27 @@ https://github.com/adafruit/circuitpython/releases """ -import gc -import time + + import terminalio -import bitmaptools -from displayio import TileGrid, OnDiskBitmap, ColorConverter, Bitmap, Palette +from displayio import TileGrid, OnDiskBitmap, ColorConverter import adafruit_imageload from adafruit_display_text import bitmap_label from adafruit_displayio_layout.widgets.control import Control from adafruit_displayio_layout.widgets.widget import Widget -from adafruit_displayio_layout.widgets.easing import quadratic_easeout as easein -from adafruit_displayio_layout.widgets.easing import quadratic_easein as easeout class IconWidget(Widget, Control): """ A touch enabled widget that holds an icon image loaded with - adafruit_imageload and a text label centered beneath it. Includes optional - animation to increase the icon size when pressed. + adafruit_imageload and a text label centered beneath it. - :param displayio.Display display: the display where the ``IconWidget`` is displayed, - used for handling animations. - :param str label_text: the text that will be shown beneath the icon image. - :param str icon: the filepath of the bmp image to be used as the icon. - :param bool on_disk: if True use OnDiskBitmap instead of imageload. + :param string label_text: the text that will be shown beneath the icon image. + :param string icon: the filepath of the bmp image to be used as the icon. + :param boolean on_disk: if True use OnDiskBitmap instead of imageload. This can be helpful to save memory. Defaults to False - :param float max_scale: the maximum zoom during animation, set 1.0 for no animation - a value of 1.4 is a good starting point (default: 1.0, no animation), - ``max_scale`` must be between 1.0 and 1.5. - :param float max_angle: the maximum degrees of rotation during animation, positive values - are clockwise, set 0 for no rotation, in degrees (default: 0 degrees) - :param float animation_time: the time for the animation in seconds, set to 0.0 for - no animation, a value of 0.15 is a good starting point (default: 0.0 seconds) - :param int x: x location the icon widget should be placed. Pixel coordinates. :param int y: y location the icon widget should be placed. Pixel coordinates. :param anchor_point: (X,Y) values from 0.0 to 1.0 to define the anchor point relative to the @@ -66,47 +52,21 @@ class IconWidget(Widget, Control): :param int max_size: (Optional) this will get passed through to the displayio.Group constructor. If omitted we default to grid_size width * grid_size height to make room for all (1, 1) sized cells. - """ - - # pylint: disable=bad-super-call, too-many-instance-attributes, too-many-locals - # pylint: disable=too-many-arguments, unused-argument - _max_scale = 1.5 - _max_pixels = 80 - _max_color_depth = 512 - _zoom_bitmap = Bitmap( - round(_max_scale * _max_pixels), - round(_max_scale * _max_pixels), - _max_color_depth, - ) - _zoom_palette = Palette(_max_color_depth) - - def __init__( - self, - display, - label_text, - icon, - on_disk=False, - max_scale=1.0, - max_angle=8, - animation_time=0.0, - **kwargs, - ): + """ - super().__init__(**kwargs) # initialize superclasses - super(Control, self).__init__() + def __init__(self, label_text, icon, on_disk=False, **kwargs): + super().__init__(**kwargs) - self.display = display self._icon = icon if on_disk: - self._file = open(icon, "rb") + self._file = open(self._icon, "rb") image = OnDiskBitmap(self._file) tile_grid = TileGrid(image, pixel_shader=ColorConverter()) else: - image, palette = adafruit_imageload.load(self._icon) + image, palette = adafruit_imageload.load(icon) tile_grid = TileGrid(image, pixel_shader=palette) - self.append(tile_grid) _label = bitmap_label.Label( terminalio.FONT, @@ -123,27 +83,6 @@ def __init__( image.height + _label.bounding_box[3], ) - # verify the animation settings - self._start_scale = 1.0 - - # constrain maximum_scaling between 1.0 and 1.5 - self._max_scale = min(max(1.0, max_scale), IconWidget._max_scale) - if max_scale == 1.0: # no animation - self._animation_time = 0 - else: - self._animation_time = animation_time # in seconds - self._end_scale = max_scale - - self._angle = (max_angle / 360) * 2 * 3.14 # 5 degrees, convert to radians - - # define zoom attributes - self._zoom_color_depth = None - self._zoom_palette = None - self._zoom_bitmap = None - self._zoom_tilegrid = None - - self.value = False # initial value - def contains(self, touch_point): # overrides, then calls Control.contains(x,y) """Checks if the IconWidget was touched. Returns True if the touch_point is @@ -160,111 +99,3 @@ def contains(self, touch_point): # overrides, then calls Control.contains(x,y) touch_y = touch_point[1] - self.y return super().contains((touch_x, touch_y, 0)) - - def zoom_animation(self, touch_point): - """Performs zoom animation when pressed. - - :param touch_point: x,y location of the screen, converted to local coordinates. - :type touch_point: Tuple[x,y] - :return: None - """ - - self.value = True - - if self._animation_time > 0: - ### - ## Update the zoom palette and bitmap buffers and append the tilegrid - ### - - _image, _palette = adafruit_imageload.load(self._icon) - - # copy the image palette, add a transparent color at the end - for i, color in enumerate(_palette): - IconWidget._zoom_palette[i] = color - IconWidget._zoom_palette[len(IconWidget._zoom_palette) - 1] = 0x000000 - IconWidget._zoom_palette.make_transparent(len(IconWidget._zoom_palette) - 1) - - # create the zoom bitmap larger than the original image to allow for zooming - IconWidget._zoom_bitmap.fill( - len(IconWidget._zoom_palette) - 1 - ) # transparent fill - IconWidget._zoom_bitmap.blit( - (IconWidget._zoom_bitmap.width - _image.width) // 2, - (IconWidget._zoom_bitmap.height - _image.height) // 2, - _image, - ) # blit the image into the center of the zoom_bitmap - - # place zoom_bitmap at same location as image - self._zoom_tilegrid = TileGrid( - IconWidget._zoom_bitmap, pixel_shader=IconWidget._zoom_palette - ) - self._zoom_tilegrid.x = -(IconWidget._zoom_bitmap.width - _image.width) // 2 - self._zoom_tilegrid.y = ( - -(IconWidget._zoom_bitmap.height - _image.height) // 2 - ) - self.append(self._zoom_tilegrid) # add to the self group. - - # Animation: zoom larger - start_time = time.monotonic() - while True: - elapsed_time = time.monotonic() - start_time - position = min( - 1.0, easein(elapsed_time / self._animation_time) - ) # fractional position - bitmaptools.rotozoom( - dest_bitmap=IconWidget._zoom_bitmap, - ox=IconWidget._zoom_bitmap.width // 2, - oy=IconWidget._zoom_bitmap.height // 2, - source_bitmap=_image, - px=_image.width // 2, - py=_image.height // 2, - scale=self._start_scale - + position * (self._end_scale - self._start_scale), - angle=position * self._angle / 2, - ) - self.display.refresh() - if elapsed_time > self._animation_time: - break - del _image - del _palette - gc.collect() - - def zoom_out_animation(self, touch_point): - """Performs un-zoom animation when released. - - :param touch_point: x,y location of the screen, converted to local coordinates. - :type touch_point: Tuple[x,y] - :return: None - """ - - _image, _palette = adafruit_imageload.load(self._icon) - - if (self._animation_time > 0) and self.value: - # Animation: shrink down to the original size - start_time = time.monotonic() - while True: - elapsed_time = time.monotonic() - start_time - position = max(0.0, easeout(1 - (elapsed_time / self._animation_time))) - IconWidget._zoom_bitmap.fill(len(IconWidget._zoom_palette) - 1) - bitmaptools.rotozoom( - dest_bitmap=IconWidget._zoom_bitmap, - ox=IconWidget._zoom_bitmap.width // 2, - oy=IconWidget._zoom_bitmap.height // 2, - source_bitmap=_image, - px=_image.width // 2, - py=_image.height // 2, - scale=self._start_scale - + position * (self._end_scale - self._start_scale), - angle=position * self._angle / 2, - ) - self.display.refresh() - if elapsed_time > self._animation_time: - break - - # clean up the zoom display elements - self.pop(-1) # remove self from the group - del _image - del _palette - gc.collect() - - self.value = False diff --git a/docs/api.rst b/docs/api.rst index 0ab5305..374a256 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -26,3 +26,7 @@ .. automodule:: adafruit_displayio_layout.widgets.icon_widget :members: :member-order: bysource + +.. automodule:: adafruit_displayio_layout.widgets.icon_animated + :members: + :member-order: bysource From 8a436b5db6e41b910cdff83cf5d3af0ac747f054 Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Sat, 20 Mar 2021 21:30:00 -0500 Subject: [PATCH 6/9] correct docstring for IconAnimation.init_class, thanks foamyguy --- adafruit_displayio_layout/widgets/icon_animated.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adafruit_displayio_layout/widgets/icon_animated.py b/adafruit_displayio_layout/widgets/icon_animated.py index 44e56bd..7cce26a 100644 --- a/adafruit_displayio_layout/widgets/icon_animated.py +++ b/adafruit_displayio_layout/widgets/icon_animated.py @@ -38,7 +38,7 @@ class IconAnimated(IconWidget): OnDiskBitmap and a text label centered beneath it. Includes optional animation to increase the icon size when pressed. - .. Warning:: The `init_class` class function should be called before instancing any + .. Warning:: The `init_class` class function must be called before instancing any IconAnimated widgets. :param str label_text: the text that will be shown beneath the icon image. @@ -81,9 +81,9 @@ def init_class( Initializes the IconAnimated Class variables, including preallocating memory buffers for the icon zoom bitmap and icon palette. - .. Note:: The `init_class` class function should be called before instancing any + .. Note:: The `init_class` class function must be called before instancing any IconAnimated widgets. Usage example: - ``IconAnimated(display=board.DISPLAY, max_scale=1.5, + ``IconAnimated.init_class(display=board.DISPLAY, max_scale=1.5, max_size=(80,80), max_color_depth=256)`` :param displayio.Display display: The display where the icons will be displayed. From f046082649eb60cd316f7bd473a7534b21fc6171 Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Mon, 22 Mar 2021 12:28:21 -0500 Subject: [PATCH 7/9] add error handling and warning messages, update docstrings, clarify input parameter naming --- .../widgets/icon_animated.py | 160 +++++++++++++----- .../widgets/icon_widget.py | 8 +- 2 files changed, 121 insertions(+), 47 deletions(-) diff --git a/adafruit_displayio_layout/widgets/icon_animated.py b/adafruit_displayio_layout/widgets/icon_animated.py index 7cce26a..a5de983 100644 --- a/adafruit_displayio_layout/widgets/icon_animated.py +++ b/adafruit_displayio_layout/widgets/icon_animated.py @@ -23,6 +23,7 @@ """ import gc import time +from math import pi import bitmaptools from displayio import TileGrid, Bitmap, Palette import adafruit_imageload @@ -43,16 +44,18 @@ class IconAnimated(IconWidget): :param str label_text: the text that will be shown beneath the icon image. :param str icon: the filepath of the bmp image to be used as the icon. - :param bool on_disk: if True use OnDiskBitmap instead of imageload. - This can be helpful to save memory. Defaults to False - - :param float max_scale: the maximum zoom during animation, set 1.0 for no animation - a value of 1.4 is a good starting point (default: 1.0, no animation), - ``max_scale`` must be between 1.0 and 1.5. - :param float max_angle: the maximum degrees of rotation during animation, positive values - are clockwise, set 0 for no rotation, in degrees (default: 0 degrees) + :param bool on_disk: if True use OnDiskBitmap instead of imageload to load static + icon image. This can be helpful to save memory. (default: False) Note: Bitmap + file must use indexed colors to allow animations in the IconAnimated widget. + + :param float scale: the maximum zoom during animation, set 1.0 for no zoom. + A value of 1.5 is a good starting point. The ``scale`` can be less than + 1.0 for shrink animations. (default: same as ``max_scale`` set in ``init_class``), + + :param float angle: the maximum degrees of rotation during animation, positive values + are clockwise, set 0 for no rotation, in degrees (default: 4 degrees) :param float animation_time: the time for the animation in seconds, set to 0.0 for - no animation, a value of 0.15 is a good starting point (default: 0.0 seconds) + no animation, a value of 0.15 is a good starting point (default: 0.15 seconds) :param int x: x location the icon widget should be placed. Pixel coordinates. :param int y: y location the icon widget should be placed. Pixel coordinates. @@ -62,8 +65,8 @@ class IconAnimated(IconWidget): :param int anchored_position: (x,y) pixel value for the location of the anchor_point :type anchored_position: Tuple[int, int] :param int max_size: (Optional) this will get passed through to the - displayio.Group constructor. If omitted we default to - grid_size width * grid_size height to make room for all (1, 1) sized cells. + displayio.Group constructor. ``max_size`` should be set to the maximum number of + graphical elements that will be held within the Group of this widget. """ # pylint: disable=bad-super-call, too-many-instance-attributes, too-many-locals @@ -75,33 +78,55 @@ class IconAnimated(IconWidget): @classmethod def init_class( - cls, display=None, max_scale=1.5, max_size=(80, 80), max_color_depth=512 + cls, display=None, max_scale=1.5, max_icon_size=(80, 80), max_color_depth=256 ): """ Initializes the IconAnimated Class variables, including preallocating memory - buffers for the icon zoom bitmap and icon palette. + buffers for the icon zoom bitmap and icon zoom palette. .. Note:: The `init_class` class function must be called before instancing any IconAnimated widgets. Usage example: ``IconAnimated.init_class(display=board.DISPLAY, max_scale=1.5, - max_size=(80,80), max_color_depth=256)`` + max_icon_size=(80,80), max_color_depth=256)`` :param displayio.Display display: The display where the icons will be displayed. - :param float max_scale: The maximum zoom of the any of the icons, should be > 1.0, + :param float max_scale: The maximum zoom of the any of the icons, should be >= 1.0, (default: 1.5) - :param max_size: The maximum (x,y) pixel dimensions of any `IconAnimated` bitmap size - that will be created (default: (80,80)) - :type max_size: Tuple[int,int] + :param max_icon_size: The maximum (x,y) pixel dimensions of any `IconAnimated` bitmap size + that will be created (default: (80,80)). Note: This is the original pixel size, + before scaling + :type max_icon_size: Tuple[int,int] :param int max_color_depth: The maximum color depth of any `IconAnimated` - bitmap that will be created (default: 512) + bitmap that will be created (default: 256) """ if display is None: - raise ValueError("Must provide display parameter for IconAnimated.") + raise ValueError( + "IconAninmated.init_class: Must provide display parameter for IconAnimated." + ) + + if ( + isinstance(max_icon_size, tuple) + and len(max_icon_size) == 2 # validate max_icon_size input + and isinstance(max_icon_size[0], int) + and isinstance(max_icon_size[1], int) + ): + pass + else: + raise ValueError( + "IconAninmated.init_class: max_icon_size must be an (x,y) " + "tuple of integer pixel sizes." + ) + cls.display = display + if max_scale < 1.0: + print( + "Warning: IconAnimated.init_class - max_scale value was " + "constrained to minimum of 1.0" + ) cls.max_scale = max(1.0, max_scale) cls.bitmap_buffer = Bitmap( - round(max_scale * max_size[0]), - round(max_scale * max_size[1]), + round(cls.max_scale * max_icon_size[0]), + round(cls.max_scale * max_icon_size[1]), max_color_depth + 1, ) cls.palette_buffer = Palette(max_color_depth + 1) @@ -111,8 +136,8 @@ def __init__( label_text, icon, on_disk=False, - max_scale=1.4, - max_angle=8, + scale=None, + angle=4, animation_time=0.15, **kwargs, ): @@ -125,14 +150,23 @@ def __init__( ) super().__init__(label_text, icon, on_disk, **kwargs) # initialize superclasses + print("icon: {}".format(self._icon)) # constrain instance's maximum_scaling between 1.0 and the Class's max_scale - self._max_scale = min(max(1.0, max_scale), self.__class__.max_scale) - if max_scale == 1.0: # no animation - self._animation_time = 0 + if scale is None: + self._scale = self.__class__.max_scale else: - self._animation_time = animation_time # in seconds - self._angle = (max_angle / 360) * 2 * 3.14 # 5 degrees, convert to radians + if scale > self.__class__.max_scale: + print( + "Warning - IconAnimated: max_scale is constrained by value of " + "IconAnimated.max_scale set by IconAnimated.init_class(): {}".format( + self.__class__.max_scale + ) + ) + self._scale = max(0, min(scale, self.__class__.max_scale)) + + self._animation_time = animation_time # in seconds + self._angle = (angle / 360) * 2 * pi # in degrees, convert to radians self._zoomed = False # state variable for zoom status def zoom_animation(self, touch_point): @@ -144,14 +178,35 @@ def zoom_animation(self, touch_point): """ if self._animation_time > 0: - ### - ## Update the zoom palette and bitmap buffers and append the tilegrid - ### + try: + _image, _palette = adafruit_imageload.load(self._icon) + + if len(self.__class__.palette_buffer) < len(_palette) + 1: + self._animation_time = 0 # skip any animation + print( + "Warning: IconAnimated - icon bitmap exceeds IconAnimated.max_color_depth;" + " defaulting to no animation" + ) + + except NotImplementedError: + self._animation_time = 0 # skip any animation + print( + "Warning: IconAnimated - True color BMP unsupported for animation;" + " defaulting to no animation" + ) + + if self._animation_time > 0: - _image, _palette = adafruit_imageload.load(self._icon) animation_bitmap = self.__class__.bitmap_buffer animation_palette = self.__class__.palette_buffer + # store the current display refresh setting + refresh_status = self.__class__.display.auto_refresh + + ### + ## Update the zoom palette and bitmap buffers and append the tilegrid + ### + # copy the image palette, add a transparent color at the end for i, color in enumerate(_palette): animation_palette[i] = color @@ -172,15 +227,20 @@ def zoom_animation(self, touch_point): ) animation_tilegrid.x = -(animation_bitmap.width - _image.width) // 2 animation_tilegrid.y = -(animation_bitmap.height - _image.height) // 2 + + self.__class__.display.auto_refresh = False # set auto_refresh off + self[0].hidden = True # hide the original icon self.append(animation_tilegrid) # add to the self group. # Animation: zoom larger start_time = time.monotonic() + while True: elapsed_time = time.monotonic() - start_time position = min( 1.0, easein(elapsed_time / self._animation_time) ) # fractional position + animation_bitmap.fill(len(animation_palette) - 1) bitmaptools.rotozoom( dest_bitmap=animation_bitmap, ox=animation_bitmap.width // 2, @@ -188,13 +248,16 @@ def zoom_animation(self, touch_point): source_bitmap=_image, px=_image.width // 2, py=_image.height // 2, - scale=1.0 # start scaling at 1.0 - + position * (self._max_scale - 1.0), - angle=position * self._angle / 2, + scale=1.0 + position * (self._scale - 1.0), # start scaling at 1.0 + angle=position * self._angle, ) self.__class__.display.refresh() if elapsed_time > self._animation_time: break + + # set display.auto_refresh back to original value + self.__class__.display.auto_refresh = refresh_status + del _image del _palette gc.collect() @@ -209,11 +272,16 @@ def zoom_out_animation(self, touch_point): :return: None """ - _image, _palette = adafruit_imageload.load(self._icon) - animation_bitmap = self.__class__.bitmap_buffer - animation_palette = self.__class__.palette_buffer - if (self._animation_time > 0) and self._zoomed: + _image, _palette = adafruit_imageload.load(self._icon) + animation_bitmap = self.__class__.bitmap_buffer + animation_palette = self.__class__.palette_buffer + + # store the current display refresh setting + refresh_status = self.__class__.display.auto_refresh + + self.__class__.display.auto_refresh = False # set auto_refresh off + # Animation: shrink down to the original size start_time = time.monotonic() while True: @@ -227,15 +295,21 @@ def zoom_out_animation(self, touch_point): source_bitmap=_image, px=_image.width // 2, py=_image.height // 2, - scale=1.0 + position * (self._max_scale - 1.0), - angle=position * self._angle / 2, + scale=1.0 + position * (self._scale - 1.0), + angle=position * self._angle, ) self.__class__.display.refresh() if elapsed_time > self._animation_time: break # clean up the zoom display elements - self.pop(-1) # remove self from the group + self[0].hidden = False # unhide the original icon + self.pop(-1) # remove zoom tilegrid from the group + self.__class__.display.refresh() + + # set display.auto_refresh back to original value + self.__class__.display.auto_refresh = refresh_status + del _image del _palette gc.collect() diff --git a/adafruit_displayio_layout/widgets/icon_widget.py b/adafruit_displayio_layout/widgets/icon_widget.py index aae4f8e..e46ef1b 100644 --- a/adafruit_displayio_layout/widgets/icon_widget.py +++ b/adafruit_displayio_layout/widgets/icon_widget.py @@ -50,8 +50,8 @@ class IconWidget(Widget, Control): :param int anchored_position: (x,y) pixel value for the location of the anchor_point :type anchored_position: Tuple[int, int] :param int max_size: (Optional) this will get passed through to the - displayio.Group constructor. If omitted we default to - grid_size width * grid_size height to make room for all (1, 1) sized cells. + displayio.Group constructor. ``max_size`` should be set to the maximum number of + graphical elements that will be held within the Group of this widget. """ @@ -77,8 +77,8 @@ def __init__(self, label_text, icon, on_disk=False, **kwargs): ) self.append(_label) self.touch_boundary = ( - self.x, - self.y, + 0, + 0, image.width, image.height + _label.bounding_box[3], ) From c9e883ab002d32678df19bbf595e0f14ae9b2c79 Mon Sep 17 00:00:00 2001 From: Kevin Matocha Date: Mon, 22 Mar 2021 12:48:39 -0500 Subject: [PATCH 8/9] add example simpletest and icon --- ...splayio_layout_icon_animated_simpletest.py | 77 ++++++++++++++++++ examples/icons/Play_48x48_small.bmp | Bin 0 -> 1302 bytes examples/icons/Play_48x48_small.bmp.license | 2 + 3 files changed, 79 insertions(+) create mode 100644 examples/displayio_layout_icon_animated_simpletest.py create mode 100755 examples/icons/Play_48x48_small.bmp create mode 100644 examples/icons/Play_48x48_small.bmp.license diff --git a/examples/displayio_layout_icon_animated_simpletest.py b/examples/displayio_layout_icon_animated_simpletest.py new file mode 100644 index 0000000..c5bf6a6 --- /dev/null +++ b/examples/displayio_layout_icon_animated_simpletest.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2021 Kevin Matocha +# +# SPDX-License-Identifier: MIT +""" +Creates two animated icons with touch response: zoom and shrink animations. +""" +import gc +import time +import board +import displayio +import adafruit_touchscreen +from adafruit_displayio_layout.widgets.icon_animated import IconAnimated + +display = board.DISPLAY + +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), +) + + +IconAnimated.init_class( + display, max_scale=1.5, max_icon_size=(48, 48), max_color_depth=255 +) + +icon_zoom = IconAnimated( + "Zoom", + "icons/Play_48x48_small.bmp", + x=50, + y=40, + on_disk=True, + scale=1.5, # zoom animation + angle=5, +) + +icon_shrink = IconAnimated( + "Shrink", + "icons/Play_48x48_small.bmp", + x=180, + y=40, + on_disk=True, + scale=0.7, # shrink animation + angle=-10, +) + +icons = [icon_zoom, icon_shrink] + +main_group = displayio.Group(max_size=2) +main_group.append(icon_zoom) +main_group.append(icon_shrink) + +display.show(main_group) + + +COOLDOWN_TIME = 0.25 +LAST_PRESS_TIME = -1 + +display.auto_refresh = True + +while True: + time.sleep(0.05) + p = ts.touch_point + if p: + _now = time.monotonic() + if _now - LAST_PRESS_TIME > COOLDOWN_TIME: + for icon in icons: + if icon.contains(p): + icon.zoom_animation(p) + LAST_PRESS_TIME = time.monotonic() + + else: + for icon in icons: + icon.zoom_out_animation(p) diff --git a/examples/icons/Play_48x48_small.bmp b/examples/icons/Play_48x48_small.bmp new file mode 100755 index 0000000000000000000000000000000000000000..eb446a1dbbab3c873e99b15c0a1f65dbb549c628 GIT binary patch literal 1302 zcmc(bzfQw25Qnd*EnC4Y>H}2iUa1xjkY2-tl$Ml{solDDVM06sBP%Ns6Kl6h6>q@% zaQ+kf?6^owoaET~?&r@=^4nntbiLAZOJib33)-4L({${aaZW^y{ARh+%|ATOqU^=n z7JBbHxcq#C!Tt%xUoUVt9^m2o16KcA<%#3VZn{yzJlQA#HmL^j_6ZK1M1%54ljaGN z=Fq&X;mylh-inX5ykRn22oZhuf{>~3vllfM!{7+d#h5o37u>}872Ci$KZ5iA$eRow z*~*-0StB*e+}(-u43qf`r`~V!Q&l*dJN@cTBaQLeW6$MUGVHj-nU_ Date: Mon, 22 Mar 2021 12:52:16 -0500 Subject: [PATCH 9/9] delete unnecessary print statement --- adafruit_displayio_layout/widgets/icon_animated.py | 1 - 1 file changed, 1 deletion(-) diff --git a/adafruit_displayio_layout/widgets/icon_animated.py b/adafruit_displayio_layout/widgets/icon_animated.py index a5de983..9545c8e 100644 --- a/adafruit_displayio_layout/widgets/icon_animated.py +++ b/adafruit_displayio_layout/widgets/icon_animated.py @@ -150,7 +150,6 @@ def __init__( ) super().__init__(label_text, icon, on_disk, **kwargs) # initialize superclasses - print("icon: {}".format(self._icon)) # constrain instance's maximum_scaling between 1.0 and the Class's max_scale if scale is None: