capen_inkplate/gfx.py

546 lines
20 KiB
Python

# The MIT License (MIT)
#
# Copyright (c) 2018 Kattni Rembor for Adafruit Industries
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`gfx`
====================================================
CircuitPython pixel graphics drawing library.
* Author(s): Kattni Rembor, Tony DiCola, Jonah Yolles-Murphy, based on code by Phil Burgess
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
"""
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_GFX.git"
# pylint: disable=invalid-name
class GFX:
# pylint: disable=too-many-instance-attributes
"""Create an instance of the GFX drawing class.
:param width: The width of the drawing area in pixels.
:param height: The height of the drawing area in pixels.
:param pixel: A function to call when a pixel is drawn on the display. This function
should take at least an x and y position and then any number of optional
color or other parameters.
:param hline: A function to quickly draw a horizontal line on the display.
This should take at least an x, y, and width parameter and
any number of optional color or other parameters.
:param vline: A function to quickly draw a vertical line on the display.
This should take at least an x, y, and height paraemter and
any number of optional color or other parameters.
:param fill_rect: A funtion to quickly draw a solid rectangle with four
input parameters: x,y, width, and height. Any number of other
parameters for color or screen specific data.
:param text: A function to quickly place text on the screen. The inputs include:
x, y data(top left as starting point).
:param font: An optional input to augment the default text method with a new font.
The input shoudl be a properly formatted dict.
"""
# pylint: disable=too-many-arguments
def __init__(
self,
width,
height,
pixel,
hline=None,
vline=None,
fill_rect=None,
text=None,
font=None,
):
# pylint: disable=too-many-instance-attributes
self.width = width
self.height = height
self._pixel = pixel
# Default to slow horizontal & vertical line implementations if no
# faster versions are provided.
if hline is None:
self.hline = self._slow_hline
else:
self.hline = hline
if vline is None:
self.vline = self._slow_vline
else:
self.vline = vline
if fill_rect is None:
self.fill_rect = self._fill_rect
else:
self.fill_rect = fill_rect
if text is None:
self.text = self._very_slow_text
# if no supplied font set to std
if font is None:
from gfx_standard_font_01 import ( # pylint: disable=import-outside-toplevel # changed
text_dict as std_font,
)
self.font = std_font
self.set_text_background()
else:
self.font = font
if not isinstance(self.font, dict):
raise ValueError(
"Font definitions must be contained in a dictionary object."
)
del self.set_text_background
else:
self.text = text
def pixel(self, x0, y0, *args, **kwargs):
"""A function to pass through in input pixel functionality."""
# This was added to mainitatn the abstrtion between gfx and the dislay library
self._pixel(x0, y0, *args, **kwargs)
def _slow_hline(self, x0, y0, width, *args, **kwargs):
"""Slow implementation of a horizontal line using pixel drawing.
This is used as the default horizontal line if no faster override
is provided."""
if y0 < 0 or y0 > self.height or x0 < -width or x0 > self.width:
return
for i in range(width):
self._pixel(x0 + i, y0, *args, **kwargs)
def _slow_vline(self, x0, y0, height, *args, **kwargs):
"""Slow implementation of a vertical line using pixel drawing.
This is used as the default vertical line if no faster override
is provided."""
if y0 < -height or y0 > self.height or x0 < 0 or x0 > self.width:
return
for i in range(height):
self._pixel(x0, y0 + i, *args, **kwargs)
def rect(self, x0, y0, width, height, *args, **kwargs):
"""Rectangle drawing function. Will draw a single pixel wide rectangle
starting in the upper left x0, y0 position and width, height pixels in
size."""
if y0 < -height or y0 > self.height or x0 < -width or x0 > self.width:
return
self.hline(x0, y0, width, *args, **kwargs)
self.hline(x0, y0 + height - 1, width, *args, **kwargs)
self.vline(x0, y0, height, *args, **kwargs)
self.vline(x0 + width - 1, y0, height, *args, **kwargs)
def _fill_rect(self, x0, y0, width, height, *args, **kwargs):
"""Filled rectangle drawing function. Will draw a single pixel wide
rectangle starting in the upper left x0, y0 position and width, height
pixels in size."""
if y0 < -height or y0 > self.height or x0 < -width or x0 > self.width:
return
for i in range(x0, x0 + width):
self.vline(i, y0, height, *args, **kwargs)
def line(self, x0, y0, x1, y1, *args, **kwargs):
"""Line drawing function. Will draw a single pixel wide line starting at
x0, y0 and ending at x1, y1."""
steep = abs(y1 - y0) > abs(x1 - x0)
if steep:
x0, y0 = y0, x0
x1, y1 = y1, x1
if x0 > x1:
x0, x1 = x1, x0
y0, y1 = y1, y0
dx = x1 - x0
dy = abs(y1 - y0)
err = dx // 2
ystep = 0
if y0 < y1:
ystep = 1
else:
ystep = -1
while x0 <= x1:
if steep:
self._pixel(y0, x0, *args, **kwargs)
else:
self._pixel(x0, y0, *args, **kwargs)
err -= dy
if err < 0:
y0 += ystep
err += dx
x0 += 1
def circle(self, x0, y0, radius, *args, **kwargs):
"""Circle drawing function. Will draw a single pixel wide circle with
center at x0, y0 and the specified radius."""
f = 1 - radius
ddF_x = 1
ddF_y = -2 * radius
x = 0
y = radius
self._pixel(x0, y0 + radius, *args, **kwargs) # bottom
self._pixel(x0, y0 - radius, *args, **kwargs) # top
self._pixel(x0 + radius, y0, *args, **kwargs) # right
self._pixel(x0 - radius, y0, *args, **kwargs) # left
while x < y:
if f >= 0:
y -= 1
ddF_y += 2
f += ddF_y
x += 1
ddF_x += 2
f += ddF_x
# angle notations are based on the unit circle and in diection of being drawn
self._pixel(x0 + x, y0 + y, *args, **kwargs) # 270 to 315
self._pixel(x0 - x, y0 + y, *args, **kwargs) # 270 to 255
self._pixel(x0 + x, y0 - y, *args, **kwargs) # 90 to 45
self._pixel(x0 - x, y0 - y, *args, **kwargs) # 90 to 135
self._pixel(x0 + y, y0 + x, *args, **kwargs) # 0 to 315
self._pixel(x0 - y, y0 + x, *args, **kwargs) # 180 to 225
self._pixel(x0 + y, y0 - x, *args, **kwargs) # 0 to 45
self._pixel(x0 - y, y0 - x, *args, **kwargs) # 180 to 135
def fill_circle(self, x0, y0, radius, *args, **kwargs):
"""Filled circle drawing function. Will draw a filled circule with
center at x0, y0 and the specified radius."""
self.vline(x0, y0 - radius, 2 * radius + 1, *args, **kwargs)
f = 1 - radius
ddF_x = 1
ddF_y = -2 * radius
x = 0
y = radius
while x < y:
if f >= 0:
y -= 1
ddF_y += 2
f += ddF_y
x += 1
ddF_x += 2
f += ddF_x
self.vline(x0 + x, y0 - y, 2 * y + 1, *args, **kwargs)
self.vline(x0 + y, y0 - x, 2 * x + 1, *args, **kwargs)
self.vline(x0 - x, y0 - y, 2 * y + 1, *args, **kwargs)
self.vline(x0 - y, y0 - x, 2 * x + 1, *args, **kwargs)
def triangle(self, x0, y0, x1, y1, x2, y2, *args, **kwargs):
# pylint: disable=too-many-arguments
"""Triangle drawing function. Will draw a single pixel wide triangle
around the points (x0, y0), (x1, y1), and (x2, y2)."""
self.line(x0, y0, x1, y1, *args, **kwargs)
self.line(x1, y1, x2, y2, *args, **kwargs)
self.line(x2, y2, x0, y0, *args, **kwargs)
def fill_triangle(self, x0, y0, x1, y1, x2, y2, *args, **kwargs):
# Filled triangle drawing function. Will draw a filled triangle around
# the points (x0, y0), (x1, y1), and (x2, y2).
if y0 > y1:
y0, y1 = y1, y0
x0, x1 = x1, x0
if y1 > y2:
y2, y1 = y1, y2
x2, x1 = x1, x2
if y0 > y1:
y0, y1 = y1, y0
x0, x1 = x1, x0
a = 0
b = 0
y = 0
last = 0
if y0 == y2:
a = x0
b = x0
if x1 < a:
a = x1
elif x1 > b:
b = x1
if x2 < a:
a = x2
elif x2 > b:
b = x2
self.hline(a, y0, b-a+1, *args, **kwargs)
return
dx01 = x1 - x0
dy01 = y1 - y0
dx02 = x2 - x0
dy02 = y2 - y0
dx12 = x2 - x1
dy12 = y2 - y1
if dy01 == 0:
dy01 = 1
if dy02 == 0:
dy02 = 1
if dy12 == 0:
dy12 = 1
sa = 0
sb = 0
if y1 == y2:
last = y1
else:
last = y1-1
for y in range(y0, last+1):
a = x0 + sa // dy01
b = x0 + sb // dy02
sa += dx01
sb += dx02
if a > b:
a, b = b, a
self.hline(a, y, b-a+1, *args, **kwargs)
y = last
sa = dx12 * (y - y1)
sb = dx02 * (y - y0)
while y <= y2:
a = x1 + sa // dy12
b = x0 + sb // dy02
sa += dx12
sb += dx02
if a > b:
a, b = b, a
self.hline(a, y, b-a+1, *args, **kwargs)
y += 1
def round_rect(self, x0, y0, width, height, radius, *args, **kwargs):
"""Rectangle with rounded corners drawing function.
This works like a regular rect though! if radius = 0
Will draw the outline of a rextabgle with rounded corners with (x0,y0) at the top left"""
# shift to correct for start point location
x0 += radius
y0 += radius
# ensure that the radius will only ever half of the shortest side or less
radius = int(min(radius, width / 2, height / 2))
if radius:
f = 1 - radius
ddF_x = 1
ddF_y = -2 * radius
x = 0
y = radius
self.vline(
x0 - radius, y0, height - 2 * radius + 1, *args, **kwargs
) # left
self.vline(
x0 + width - radius, y0, height - 2 * radius + 1, *args, **kwargs
) # right
self.hline(
x0, y0 + height - radius + 1, width - 2 * radius + 1, *args, **kwargs
) # bottom
self.hline(x0, y0 - radius, width - 2 *
radius + 1, *args, **kwargs) # top
while x < y:
if f >= 0:
y -= 1
ddF_y += 2
f += ddF_y
x += 1
ddF_x += 2
f += ddF_x
# angle notations are based on the unit circle and in diection of being drawn
# top left
self._pixel(x0 - y, y0 - x, *args, **kwargs) # 180 to 135
self._pixel(x0 - x, y0 - y, *args, **kwargs) # 90 to 135
# top right
self._pixel(
x0 + x + width - 2 * radius, y0 - y, *args, **kwargs
) # 90 to 45
self._pixel(
x0 + y + width - 2 * radius, y0 - x, *args, **kwargs
) # 0 to 45
# bottom right
self._pixel(
x0 + y + width - 2 * radius,
y0 + x + height - 2 * radius,
*args,
**kwargs,
) # 0 to 315
self._pixel(
x0 + x + width - 2 * radius,
y0 + y + height - 2 * radius,
*args,
**kwargs,
) # 270 to 315
# bottom left
self._pixel(
x0 - x, y0 + y + height - 2 * radius, *args, **kwargs
) # 270 to 255
self._pixel(
x0 - y, y0 + x + height - 2 * radius, *args, **kwargs
) # 180 to 225
def fill_round_rect(self, x0, y0, width, height, radius, *args, **kwargs):
"""Filled circle drawing function. Will draw a filled circule with
center at x0, y0 and the specified radius."""
# shift to correct for start point location
x0 += radius
y0 += radius
# ensure that the radius will only ever half of the shortest side or less
radius = int(min(radius, width / 2, height / 2))
self.fill_rect(
x0, y0 - radius, width - 2 * radius + 2, height + 2, *args, **kwargs
)
if radius:
f = 1 - radius
ddF_x = 1
ddF_y = -2 * radius
x = 0
y = radius
while x < y:
if f >= 0:
y -= 1
ddF_y += 2
f += ddF_y
x += 1
ddF_x += 2
f += ddF_x
# part notation starts with 0 on left and 1 on right, and direction is noted
# top left
self.vline(
x0 - y, y0 - x, 2 * x + 1 + height - 2 * radius, *args, **kwargs
) # 0 to .25
self.vline(
x0 - x, y0 - y, 2 * y + 1 + height - 2 * radius, *args, **kwargs
) # .5 to .25
# top right
self.vline(
x0 + x + width - 2 * radius,
y0 - y,
2 * y + 1 + height - 2 * radius,
*args,
**kwargs,
) # .5 to .75
self.vline(
x0 + y + width - 2 * radius,
y0 - x,
2 * x + 1 + height - 2 * radius,
*args,
**kwargs,
) # 1 to .75
def _place_char(self, x0, y0, char, size, *args, **kwargs):
"""A sub class used for placing a single character on the screen"""
# pylint: disable=undefined-loop-variable
arr = self.font[char]
width = arr[0]
height = arr[1]
# extract the char section of the data
data = arr[2:]
for x in range(width):
for y in range(height):
bit = bool(data[x] & 2 ** y)
# char pixel
if bit:
self.fill_rect(
size * x + x0,
size * (height - y - 1) + y0,
size,
size,
*args,
**kwargs,
)
# else background pixel
else:
try:
self.fill_rect(
size * x + x0,
size * (height - y - 1) + y0,
size,
size,
*self.text_bkgnd_args,
**self.text_bkgnd_kwargs,
)
except TypeError:
pass
del arr, width, height, data, x, y, x0, y0, char, size
def _very_slow_text(self, x0, y0, string, size, *args, **kwargs):
"""a function to place text on the display.(temporary)
to use special characters put "__" on either side of the desired characters.
letter format:
{'character_here' : bytearray(b',WIDTH,HEIGHT,right-most-data,
more-bytes-here,left-most-data') ,}
(replace the "," with backslashes!!)
each byte:
| lower most bit(lowest on display)
V
x0110100
^c
| top most bit (highest on display)"""
x_roll = x0 # rolling x
y_roll = y0 # rolling y
# highest_height = 0#wrap
sep_string = string.split("__")
for chunk in sep_string:
# print(chunk)
try:
self._place_char(x_roll, y_roll, chunk, size, *args, **kwargs)
x_roll += size * self.font[chunk][0] + size
# highest_height = max(highest_height, size*self.font[chunk][1] + 1) #wrap
except KeyError:
while chunk:
char = chunk[0]
# make sure something is sent even if not in font dict
try:
self._place_char(x_roll, y_roll, char,
size, *args, **kwargs)
except KeyError:
self._place_char(
x_roll, y_roll, "?CHAR?", size, *args, **kwargs
)
char = "?CHAR?"
x_roll += size * self.font[char][0]
# gap between letters
try:
self.fill_rect(
x_roll,
y_roll,
size,
size * self.font[char][1],
*self.text_bkgnd_args,
**self.text_bkgnd_kwargs,
)
except TypeError:
pass
x_roll += size
# highest_height = max(highest_height, size*self.font[char][1] + 1) #wrap
chunk = chunk[1:] # wrap
# if (x_roll >= self.width) or (chunk[0:2] == """\n"""): #wrap
# self._text(x0,y0+highest_height,"__".join(sep_string),size) #wrap
# print(highest_height) #wrap
def set_text_background(self, *args, **kwargs):
"""A method to change the background color of text, input any and all color paramsself.
run without any inputs to return to "clear" background
"""
self.text_bkgnd_args = args
self.text_bkgnd_kwargs = kwargs
# pylint: enable=too-many-arguments