Skip to content

Instantly share code, notes, and snippets.

@ali1234
Last active March 4, 2018 19:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ali1234/81fdc3a52a00e383cfd4cb490399ad41 to your computer and use it in GitHub Desktop.
Save ali1234/81fdc3a52a00e383cfd4cb490399ad41 to your computer and use it in GitHub Desktop.
Unified Graphics Library Yarrrrrrr
import subprocess, time, sys
import numpy as np
from PIL import Image
class GraphicsDeviceBase(object):
def __init__(self, buf, depth):
self.__buf = buf
self.__depth = depth
@property
def buf(self):
return self.__buf
@property
def width(self):
return self.__buf.shape[1]
@property
def height(self):
return self.__buf.shape[0]
@property
def channels(self):
return self.__buf.shape[2]
@property
def depth(self):
return self.__depth
# TODO: friendly graphics API goes here
class GraphicsDeviceFramebuffer(GraphicsDeviceBase):
def __init__(self, width, height, channels, depth):
super().__init__(np.zeros((height, width, channels), dtype=np.uint8), depth)
class Terminal(GraphicsDeviceFramebuffer):
# shows the output on your terminal. no hardware required.
def __init__(self, width, height, channels, depth):
super().__init__(width, height, channels, depth)
sys.stdout.write('\033[2J\033[?25l')
def rotation(self, n):
pass
def show(self):
r = 0
g = 1%self.channels
b = 2%self.channels
if self.depth == 1:
outbuf = ((self.buf & self.depth)>0) * 255
else:
outbuf = self.buf
for n, row in enumerate(outbuf):
sys.stdout.write('\033[{};0H'.format(n+2))
s = ' '.join(
('\033[38;2;{:d};{:d};{:d}m\u25cf'.format(pixel[r], pixel[g], pixel[b]) for pixel in row)
)
sys.stdout.write(' ')
sys.stdout.write(s)
sys.stdout.flush()
time.sleep(0.01)
def off(self):
sys.stdout.write('\033[0m\033[{};0H\033[?25h'.format(self.height+3))
class Ffmpeg(GraphicsDeviceFramebuffer):
# records output to a video file
def __init__(self, width, height, channels, depth, scale=8):
super().__init__(width, height, channels, depth)
self.scale = scale
self.ffmpeg = subprocess.Popen("ffmpeg -i pipe: -r 30 -pix_fmt yuv420p video.webm".split(), stdin=subprocess.PIPE)
def show():
imbuf = np.repeat(np.repeat(self._buf, self.scale, axis=0), self.scale, axis=1)
for i in range(0, self.width):
imbuf[:,i*scale,:] = 0
for i in range(0, self.height):
imbuf[i*scale,:,:] = 0
imbuf = np.pad(imbuf, ((0,1), (0,1), (0,1)), mode='wrap')
Image.fromarray(imbuf).save(self.ffmpeg.stdin, format='png')
def off():
self.ffmpeg.stdin.close()
self.ffmpeg.wait()
class Legacy(GraphicsDeviceBase):
# TODO: wrapper for legacy drivers
# you may need more than one of these since the
# drivers are all different :)
def __init__(self, legacy, depth):
super().__init__(legacy._buf, depth)
self._legacy = legacy
def rotation(self, n):
return self._legacy.rotation(n)
def show(self):
self._legacy.show()
def off(self):
self._legacy.off()
# TODO: implement brightness etc
def AutoEmulator(width, height, channels, depth, driver=None):
if driver == 'terminal' or driver == None:
return Terminal(width, height, channels, depth)
if driver == 'ffmpeg': # don't select this one by default
return Ffmpeg(width, height, channels, depth)
raise ImportError
def UnicornHatHD(driver=None):
if driver == 'legacy' or driver == None:
try:
import unicornhathd
return Legacy(unicornhathd, 8)
except ImportError:
pass
return AutoEmulator(16, 16, 3, 0, driver=driver)
# i have not tested the rest after this point on real hardware
def UnicornHat(driver=None):
if driver == 'legacy' or driver == None:
try:
import unicornhat
return Legacy(unicornhat, 8)
except ImportError:
pass
return AutoEmulator(8, 8, 3, 0, driver=driver)
def UnicornPhat(driver=None):
if driver == 'legacy' or driver == None:
try:
import unicornhat
return Legacy(unicornhat, 8)
except ImportError:
pass
return AutoEmulator(8, 4, 3, 0, driver=driver)
def ScrollPhatHD(driver=None):
if driver == 'legacy' or driver == None:
try:
import scrollphathd
return Legacy(scrollphathd, 8)
except ImportError:
pass
return AutoEmulator(17, 7, 1, 0, driver=driver)
def ScrollPhat(driver=None):
if driver == 'legacy' or driver == None:
try:
import scrollphat
return Legacy(scrollphat, 1)
except ImportError:
pass
return AutoEmulator(11, 5, 1, 1, driver=driver)
def Blinkt(driver=None):
if driver == 'legacy' or driver == None:
try:
import blinkt
return Legacy(blinkt, 8)
except ImportError:
pass
return AutoEmulator(8, 1, 3, 8, driver=driver)
#!/usr/bin/env python3
import time, math, random
import numpy as np
print("""Unicorn HAT HD: demo.py
Press Ctrl+C to exit!
""")
# here just import whichever display you want to use
# this could be autodetected if the hardware supports it
from devices import UnicornHatHD as Display
display = Display(driver=None) # autodetect driver
display.rotation(180)
# helper functions
def distance_from_centre(dx = 0, dy = 0):
ox = dx + ((display.width/2) - 0.5)
oy = dy + ((display.height/2) - 0.5)
# nasty hack warning: the +1 here is to make it work with blinkt which is 1 pixel tall
x = np.squeeze(np.arange(0, display.width+1, dtype=np.float)[:, np.newaxis] - ox)
y = np.squeeze(np.arange(0, display.height+1, dtype=np.float)[:, np.newaxis] - oy)
return x[:-1], y[:-1]
def solid(a):
s = np.array([[a]], dtype=np.float)
def _solid(t):
return s
return _solid
def cycle(t):
return np.array([[t*0.2]], dtype=np.float)
# RGB effects
matrix_buf = np.random.randint(0, 0x10f, (display.height, display.width), dtype=np.int16)
matrix_t = 0
def matrix(t):
global matrix_buf, matrix_t
matrix_buf = matrix_buf + (np.random.randint(-4, 6, (display.height, display.width), dtype=np.int16))
if t - matrix_t > 0.05:
fall = matrix_buf > 0xff
subs = fall * np.random.randint(0x7f, 0x17f, (display.height, display.width), dtype=np.int16)
adds = np.roll(subs, 1, axis=0) * 0.75
adds[0, :] = (np.random.randint(0, 64, (1, display.width), dtype=np.uint8) == 0) * 0xff
matrix_buf = matrix_buf + adds - subs
matrix_t = t
return np.clip(np.stack([matrix_buf-0xff, matrix_buf, matrix_buf-0xff], axis=-1), 0, 0xff)
# boolean effects
def checker(t):
x, y = distance_from_centre(dx = math.sin(t * 1.333) * display.width, dy = math.cos(t * 2.0) * display.width)
sc = (math.cos(-t*0.75)) + 2.0
s = math.sin(t);
c = math.cos(t);
xs = ((x * c - y[:, np.newaxis] * s) - math.sin(t / 2.0) * 0.1) / sc;
ys = ((x * s + y[:, np.newaxis] * c) - math.cos(t / 2.0) * 0.1) / sc;
return (np.sin(xs) > 0) ^ (np.cos(ys) > 0)
def beams(nbeams = None):
if nbeams is None:
nbeams = random.choice([3, 5, 6])
m = 2 * math.pi / nbeams
s = 3 / nbeams
def _beams(t):
x, y = distance_from_centre(dx = math.sin(t * 3.2) * 3.0, dy = math.cos(t * 1.5) * 3.0)
return (np.mod(np.arctan2(x, y[:, np.newaxis]) + (math.pi) + t, m) - s) > 0
return _beams
def zoomrings(t):
x, y = distance_from_centre(dx = math.sin(t * 3.2) * 3.0, dy = math.cos(t * 1.5) * 3.0)
return (np.mod(np.sqrt((x**2) + (y**2)[:, np.newaxis])-(t*10), 10) - 5) > 0
_roni = np.unpackbits(np.array([255, 255, 254, 127, 252, 63, 225, 135, 193, 131, 192, 3, 192,
3, 199, 227, 229, 163, 247, 231, 240, 39, 243, 135, 224, 15,
240, 31, 253, 127, 255, 255], dtype=np.uint8)).reshape(16, 16) > 0
def roni(t):
if display.width == 16 and display.height == 16:
return _roni
else:
return np.array([[True]], dtype=np.bool)
def metaballs(num=5):
dt = np.random.ranf((1, num))*15.0
sx = np.random.ranf((1, num))*2.0+1.5
sy = np.random.ranf((1, num))*2.0+1.5
def _metaballs(t):
x, y = distance_from_centre(dx=np.sin(sx*t+dt)*6, dy=np.cos(sy*t+dt)*6)
return np.sum(1 / np.sqrt((x**2) + (y**2)[:, np.newaxis]), axis=-1) > 0.9
return _metaballs
life_buf = np.random.randint(0, 255, (display.height+2, display.width), dtype=np.uint8) == 0
life_t = 0
def life(t):
global life_buf, life_t
if t - life_t > 0.05:
life_buf = np.roll(life_buf, -1, axis=0)
life_buf[-1] = np.random.randint(0, 3, (1, display.width), dtype=np.uint8) == 0
hneighbours = np.roll(life_buf, 1, axis=0)*1 + life_buf*1 + np.roll(life_buf, -1, axis=0)*1
neighbours = np.roll(hneighbours, 1, axis=1)*1 + hneighbours*1 + np.roll(hneighbours, -1, axis=1)*1 - life_buf
life_buf = life_buf & (neighbours > 1) & (neighbours < 4)
life_buf = life_buf | (neighbours == 3)
life_t = t
return life_buf[2:]
# grey effects
tinylife_buf = np.random.randint(0, 255, ((display.height*4)+2, display.width*4), dtype=np.uint8) == 0
tinylife_t = 0
def tinylife(t):
global tinylife_buf, tinylife_t
if t - tinylife_t > 0.02:
tinylife_buf = np.roll(tinylife_buf, 1, axis=0)
tinylife_buf[0] = np.random.randint(0, 3, (1, (display.width*4)), dtype=np.uint8) == 0
hneighbours = np.roll(tinylife_buf, 1, axis=0)*1 + tinylife_buf*1 + np.roll(tinylife_buf, -1, axis=0)*1
neighbours = np.roll(hneighbours, 1, axis=1)*1 + hneighbours*1 + np.roll(hneighbours, -1, axis=1)*1 - tinylife_buf
tinylife_buf = tinylife_buf & (neighbours > 1) & (neighbours < 4)
tinylife_buf = tinylife_buf | (neighbours == 3)
tinylife_t = t
return np.sum(np.sum(tinylife_buf[:-2].reshape(display.height, 4, display.width, 4), axis=3), axis=1) / 8
def rings(t):
x, y = distance_from_centre(dx = math.sin(t * 2.0) * display.width, dy = math.cos(t * 3.0) * display.width)
sc = (math.cos(t * 5.0) * 10.0) + 20.0
return np.mod(np.sqrt((x**2) + (y**2)[:, np.newaxis])/sc, 1)
def swirl(t):
x, y = distance_from_centre()
dist = (np.sqrt((x**2) + (y**2)[:, np.newaxis]) * (0.5 + (0.2*math.sin(t*0.5)))) + (-t * 5)
s = np.sin(dist);
c = np.cos(dist);
xs = x * c - y[:, np.newaxis] * s;
ys = x * s + y[:, np.newaxis] * c;
return np.mod((np.abs(xs + ys) * 0.05) + (0.1 * t), 1)
def tunnel(nbeams=None):
b = beams(nbeams)
def _tunnel(t):
return (zoomrings(t) * 0.45) + (b(t) * 0.45) + 0.1
return _tunnel
wind_buf = np.random.randint(0, 6, (display.height, display.width), dtype=np.uint8) == 0
wind_acc_buf = np.zeros((display.height, display.width), dtype=np.float)
wind_t = 0
def wind(t):
global wind_buf, wind_acc_buf, wind_t
if t - wind_t > 0.02:
wind_buf = np.roll(wind_buf, 1, axis=1)
wind_buf[:,0] = (np.random.randint(0, 6, (display.height,), dtype=np.uint8) == 0)
wind_acc_buf = np.clip((wind_acc_buf - 0.1), 0, 1) + wind_buf
wind_t = t
wind_acc_buf = np.clip(wind_acc_buf, 0.1, 1.0)
return wind_acc_buf
diamonds_buf = np.random.randint(0, 8, (display.height, display.width), dtype=np.uint16)
diamonds_t = 0
def diamonds(t):
global diamonds_buf, diamonds_t
if t - diamonds_t > 0.05:
diamonds_buf = np.amax(np.stack([
diamonds_buf,
np.roll(diamonds_buf, 1, axis=0),
np.roll(diamonds_buf, -1, axis=0),
np.roll(diamonds_buf, 1, axis=1),
np.roll(diamonds_buf, -1, axis=1)
]), axis=0)
diamonds_buf[random.randint(0, display.height-1), random.randint(0, display.width-1)] += 1
if np.all(diamonds_buf > 256):
diamonds_buf = diamonds_buf - 256
diamonds_t = t
return (diamonds_buf & 0x3) / 3
# colorspace conversions
def grey_to_rgb(grey, r=0, g=0.333, b=0.666, x=0.333, y=0.5, z=6):
t = grey[:, :, np.newaxis] + np.array([r, g, b])[np.newaxis, np.newaxis, :]
return np.clip((x - np.abs(np.mod(t, 1)-y))*z, 0, 1) * 255
def hue(grey_func):
def _hue(t):
return grey_to_rgb(grey_func(t))
return _hue
def trippy(grey_func):
def _trippy(t):
return grey_to_rgb(grey_func(t), 0, 0.333+math.sin(t), 0.666-math.sin(t))
return _trippy
def rgb_bars(grey_func):
def _rgb_bars(t):
return grey_to_rgb(grey_func(t), z=3)
return _rgb_bars
# misc
def triple_grey_to_rgb(ga, gb, gc, offa=lambda t: t, offb=lambda t: t, offc=lambda t: t):
def _triple_grey_to_rgb(t):
return np.stack([ga(offa(t)), gb(offb(t)), gc(offc(t))], axis=-1)*255
return _triple_grey_to_rgb
def chromatic_aberration(grey):
return triple_grey_to_rgb(grey, grey, grey, offa=lambda t: t+(math.sin(t)*0.075), offc=lambda t: t-(math.sin(t)*0.075))
# operators
def multiply(a, b):
def _multiply(t):
return a(t) * b(t)[:,:,np.newaxis]
return _multiply
def invert(a):
def _invert(t):
return ~a(t)
return _invert
def shift(a):
def _shift(t):
return a(t)+0.5
return _shift
def add(a, b):
def _add(t):
return a(t) + b(t)
return _add
# choose a random effect
def random_ca():
mask = random.choice([checker, zoomrings, beams(), metaballs()])
return chromatic_aberration(mask)
def random_mult():
grey = random.choice([swirl, rings])
colr = random.choice([hue, trippy])
mask = random.choice([checker, diamonds, tunnel(), zoomrings, beams(), life, tinylife, wind, metaballs()])
return multiply(colr(grey), mask)
def random_invert_mult():
# functions are composable in complex ways but on the small screen
# it mostly ends up looking messy if too much stuff is going on
grey = random.choice([swirl])
colr = random.choice([hue, trippy])
if random.choice([True, False]):
mask = random.choice([zoomrings, beams()])
return add(multiply(colr(grey), mask), multiply(colr(shift(grey)), invert(mask)))
else:
mask = random.choice([life, metaballs()])
return multiply(colr(grey), invert(mask))
def random_matrix():
if random.randint(0,4) == 0:
return multiply(matrix, roni)
else:
return matrix
def random_simple():
grey = random.choice([swirl, rings])
colour = random.choice([hue, trippy])
return colour(grey)
def random_effect():
return random.choice([
random_ca,
random_mult,
random_mult,
random_matrix,
random_simple
])()
# playlist
effect_time = 10 # seconds
effects_count = 0
effects_limit = None
effects = [
(multiply(hue(rings), roni), effect_time),
(random_effect(), effect_time),
]
# main loop
now = time.monotonic()
try:
while True:
start = now
while True:
now = time.monotonic()
remaining = effects[0][1] - (now - start)
if remaining < 0:
remaining = 0
if display.channels == 3:
display.buf[:] = effects[0][0](now)
if remaining < 1:
display.buf[:] = display.buf * remaining
display.buf[:] = display.buf + (effects[1][0](now) * (1-remaining))
elif display.channels == 1:
display.buf[:,:,0] = effects[0][0](now)[:,:,1]
if remaining < 1:
display.buf[:,:,0] = display.buf[:,:,0] * remaining
display.buf[:,:,0] = display.buf[:,:,0] + (effects[1][0](now)[:,:,1] * (1-remaining))
display.show()
if remaining == 0:
break
time.sleep(0.001)
effect = effects.pop(0)
effects.append((random_effect(), effect_time))
effects_count += 1
if effects_limit is not None and effects_count >= effects_limit:
break
except KeyboardInterrupt:
pass
except Exception as e:
raise
finally:
display.off()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment