grid.py
  1  # SPDX-FileCopyrightText: 2020 Kattni Rembor for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  """
  6  `adafruit_led_animation.grid`
  7  ================================================================================
  8  
  9  PixelGrid helper for 2D animations.
 10  
 11  * Author(s): Kattni Rembor
 12  
 13  Implementation Notes
 14  --------------------
 15  
 16  **Hardware:**
 17  
 18  * `Adafruit NeoPixels <https://www.adafruit.com/category/168>`_
 19  * `Adafruit DotStars <https://www.adafruit.com/category/885>`_
 20  
 21  **Software and Dependencies:**
 22  
 23  * Adafruit CircuitPython firmware for the supported boards:
 24    https://circuitpython.org/downloads
 25  
 26  """
 27  from micropython import const
 28  
 29  from .helper import PixelMap, horizontal_strip_gridmap, vertical_strip_gridmap
 30  
 31  
 32  HORIZONTAL = const(1)
 33  VERTICAL = const(2)
 34  
 35  
 36  class PixelGrid:
 37      """
 38      PixelGrid lets you address a pixel strip with x and y coordinates.
 39  
 40      :param strip: An object that implements the Neopixel or Dotstar protocol.
 41      :param width: Grid width.
 42      :param height: Grid height.
 43      :param orientation: Orientation of the strip pixels - HORIZONTAL (default) or VERTICAL.
 44      :param alternating: Whether the strip alternates direction from row to row (default True).
 45      :param reverse_x: Whether the strip X origin is on the right side (default False).
 46      :param reverse_y: Whether the strip Y origin is on the bottom (default False).
 47      :param tuple top: (x, y) coordinates of grid top left corner (Optional)
 48      :param tuple bottom: (x, y) coordinates of grid bottom right corner (Optional)
 49  
 50      To use with individual pixels:
 51  
 52      .. code-block:: python
 53  
 54          import board
 55          import neopixel
 56          import time
 57          from adafruit_led_animation.grid import PixelGrid, VERTICAL
 58  
 59          pixels = neopixel.NeoPixel(board.D11, 256, auto_write=False)
 60  
 61          grid = PixelGrid(pixels, 32, 8, orientation=VERTICAL, alternating=True)
 62  
 63          for x in range(32):
 64              for y in range(8):
 65                  # pg[x, y] = (y*32) + x
 66                  pg[x][y] = ((y*32) + x) << 8
 67          pg.show()
 68  
 69      """
 70  
 71      def __init__(
 72          self,
 73          strip,
 74          width,
 75          height,
 76          orientation=HORIZONTAL,
 77          alternating=True,
 78          reverse_x=False,
 79          reverse_y=False,
 80          top=0,
 81          bottom=0,
 82      ):  # pylint: disable=too-many-arguments,too-many-locals
 83          self._pixels = strip
 84          self._x = []
 85          self.height = height
 86          self.width = width
 87  
 88          if orientation == HORIZONTAL:
 89              mapper = horizontal_strip_gridmap(width, alternating)
 90          else:
 91              mapper = vertical_strip_gridmap(height, alternating)
 92  
 93          if reverse_x:
 94              mapper = reverse_x_mapper(width, mapper)
 95  
 96          if reverse_y:
 97              mapper = reverse_y_mapper(height, mapper)
 98  
 99          x_start = 0
100          x_end = width
101          y_start = 0
102          y_end = height
103          if top:
104              x_start, y_start = top
105          if bottom:
106              x_end, y_end = bottom
107  
108          self.height = y_end - y_start
109          self.width = x_end - x_start
110  
111          for x in range(x_start, x_end):
112              self._x.append(
113                  PixelMap(
114                      strip,
115                      [mapper(x, y) for y in range(y_start, y_end)],
116                      individual_pixels=True,
117                  )
118              )
119          self.n = len(self._x)
120  
121      def __repr__(self):
122          return "[" + ", ".join([str(self[x]) for x in range(self.n)]) + "]"
123  
124      def __setitem__(self, index, val):
125          if isinstance(index, slice):
126              raise NotImplementedError("PixelGrid does not support slices")
127  
128          if isinstance(index, tuple):
129              self._x[index[0]][index[1]] = val
130          else:
131              raise ValueError("PixelGrid assignment needs a sub-index or x,y coordinate")
132  
133          if self._pixels.auto_write:
134              self.show()
135  
136      def __getitem__(self, index):
137          if isinstance(index, slice):
138              raise NotImplementedError("PixelGrid does not support slices")
139          if index < 0:
140              index += len(self)
141          if index >= self.n or index < 0:
142              raise IndexError("x is out of range")
143          return self._x[index]
144  
145      def __len__(self):
146          return self.n
147  
148      @property
149      def brightness(self):
150          """
151          brightness from the underlying strip.
152          """
153          return self._pixels.brightness
154  
155      @brightness.setter
156      def brightness(self, brightness):
157          # pylint: disable=attribute-defined-outside-init
158          self._pixels.brightness = min(max(brightness, 0.0), 1.0)
159  
160      def fill(self, color):
161          """
162          Fill the PixelGrid with the specified color.
163  
164          :param color: Color to use.
165          """
166          for strip in self._x:
167              strip.fill(color)
168  
169      def show(self):
170          """
171          Shows the pixels on the underlying strip.
172          """
173          self._pixels.show()
174  
175      @property
176      def auto_write(self):
177          """
178          auto_write from the underlying strip.
179          """
180          return self._pixels.auto_write
181  
182      @auto_write.setter
183      def auto_write(self, value):
184          self._pixels.auto_write = value
185  
186  
187  def reverse_x_mapper(width, mapper):
188      """
189      Returns a coordinate mapper function for grids with reversed X coordinates.
190  
191      :param width: width of strip
192      :param mapper: grid mapper to wrap
193      :return: mapper(x, y)
194      """
195      max_x = width - 1
196  
197      def x_mapper(x, y):
198          return mapper(max_x - x, y)
199  
200      return x_mapper
201  
202  
203  def reverse_y_mapper(height, mapper):
204      """
205      Returns a coordinate mapper function for grids with reversed Y coordinates.
206  
207      :param height: width of strip
208      :param mapper: grid mapper to wrap
209      :return: mapper(x, y)
210      """
211      max_y = height - 1
212  
213      def y_mapper(x, y):
214          return mapper(x, max_y - y)
215  
216      return y_mapper